├── .github
└── FUNDING.yml
├── .gitignore
├── .metadata
├── .vscode
├── launch.json
└── settings.json
├── .weblate
├── CHANGELOG.md
├── LICENSE
├── README.md
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── aefyr
│ │ │ │ └── sai
│ │ │ │ ├── installer
│ │ │ │ ├── ApkSourceBuilder.java
│ │ │ │ ├── PackageInstallerProvider.java
│ │ │ │ ├── QueuedInstallation.java
│ │ │ │ ├── SAIPackageInstaller.java
│ │ │ │ ├── ShellSAIPackageInstaller.java
│ │ │ │ ├── rooted
│ │ │ │ │ └── RootedSAIPackageInstaller.java
│ │ │ │ ├── rootless
│ │ │ │ │ ├── RootlessSAIPIService.java
│ │ │ │ │ └── RootlessSAIPackageInstaller.java
│ │ │ │ └── shizuku
│ │ │ │ │ └── ShizukuSAIPackageInstaller.java
│ │ │ │ ├── installer2
│ │ │ │ ├── base
│ │ │ │ │ ├── SaiPackageInstaller.java
│ │ │ │ │ ├── SaiPiSessionObserver.java
│ │ │ │ │ └── model
│ │ │ │ │ │ ├── AndroidPackageInstallerError.java
│ │ │ │ │ │ ├── SaiPiSessionParams.java
│ │ │ │ │ │ ├── SaiPiSessionState.java
│ │ │ │ │ │ └── SaiPiSessionStatus.java
│ │ │ │ └── impl
│ │ │ │ │ ├── BaseSaiPackageInstaller.java
│ │ │ │ │ ├── FlexSaiPackageInstaller.java
│ │ │ │ │ ├── rootless
│ │ │ │ │ ├── ConfirmationIntentWrapperActivity2.java
│ │ │ │ │ ├── RootlessSaiPackageInstaller.java
│ │ │ │ │ └── RootlessSaiPiBroadcastReceiver.java
│ │ │ │ │ └── shell
│ │ │ │ │ ├── RootedSaiPackageInstaller.java
│ │ │ │ │ ├── ShellSaiPackageInstaller.java
│ │ │ │ │ └── ShizukuSaiPackageInstaller.java
│ │ │ │ ├── legal
│ │ │ │ └── LegalStuffProvider.java
│ │ │ │ ├── model
│ │ │ │ ├── apksource
│ │ │ │ │ ├── ApkSource.java
│ │ │ │ │ ├── CopyToFileApkSource.java
│ │ │ │ │ ├── DefaultApkSource.java
│ │ │ │ │ ├── FilterApkSource.java
│ │ │ │ │ ├── SignerApkSource.java
│ │ │ │ │ ├── ZipApkSource.java
│ │ │ │ │ ├── ZipBackedApkSource.java
│ │ │ │ │ ├── ZipExtractorApkSource.java
│ │ │ │ │ └── ZipFileApkSource.java
│ │ │ │ ├── common
│ │ │ │ │ ├── AppFeature.java
│ │ │ │ │ └── PackageMeta.java
│ │ │ │ ├── filedescriptor
│ │ │ │ │ ├── ContentUriFileDescriptor.java
│ │ │ │ │ ├── FileDescriptor.java
│ │ │ │ │ └── NormalFileDescriptor.java
│ │ │ │ └── licenses
│ │ │ │ │ └── License.java
│ │ │ │ ├── shell
│ │ │ │ ├── Shell.java
│ │ │ │ ├── ShizukuShell.java
│ │ │ │ └── SuShell.java
│ │ │ │ ├── shizuku
│ │ │ │ └── SuiInitProvider.java
│ │ │ │ └── utils
│ │ │ │ ├── DbgPreferencesHelper.java
│ │ │ │ ├── DbgPreferencesKeys.java
│ │ │ │ ├── IOUtils.java
│ │ │ │ ├── Locker.java
│ │ │ │ ├── MapBackedLocker.java
│ │ │ │ ├── MathUtils.java
│ │ │ │ ├── MiuiUtils.java
│ │ │ │ ├── NotificationHelper.java
│ │ │ │ ├── PermissionsUtils.java
│ │ │ │ ├── PreferencesHelper.java
│ │ │ │ ├── PreferencesKeys.java
│ │ │ │ ├── PreferencesValues.java
│ │ │ │ ├── RwLock.java
│ │ │ │ ├── SimpleAsyncTask.java
│ │ │ │ ├── Stopwatch.java
│ │ │ │ ├── TextUtils.java
│ │ │ │ ├── TriConsumer.java
│ │ │ │ ├── Utils.java
│ │ │ │ └── saf
│ │ │ │ ├── FileUtils.java
│ │ │ │ └── SafUtils.java
│ │ ├── kotlin
│ │ │ └── app
│ │ │ │ └── skydroid
│ │ │ │ ├── FileProvider.java
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-hdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable-mdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable-xhdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable-xxhdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ └── ic_launcher.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ ├── values
│ │ │ ├── colors.xml
│ │ │ └── styles.xml
│ │ │ └── xml
│ │ │ └── provider_paths.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
├── icon
│ ├── adaptive_icon.png
│ ├── fallback.png
│ ├── full_icon.png
│ ├── icon.png
│ ├── icon.svg
│ └── icon_without_background.png
└── l10n
│ ├── app_cs.arb
│ ├── app_de.arb
│ ├── app_en.arb
│ ├── app_eo.arb
│ ├── app_fr.arb
│ ├── app_it.arb
│ ├── app_nb.arb
│ ├── app_nl.arb
│ ├── app_pl.arb
│ ├── app_pt_BR.arb
│ ├── app_ru.arb
│ ├── app_si.arb
│ └── app_zh_Hans.arb
├── ios
├── .gitignore
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ └── Release.xcconfig
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
└── Runner
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon-App-1024x1024@1x.png
│ │ ├── Icon-App-20x20@1x.png
│ │ ├── Icon-App-20x20@2x.png
│ │ ├── Icon-App-20x20@3x.png
│ │ ├── Icon-App-29x29@1x.png
│ │ ├── Icon-App-29x29@2x.png
│ │ ├── Icon-App-29x29@3x.png
│ │ ├── Icon-App-40x40@1x.png
│ │ ├── Icon-App-40x40@2x.png
│ │ ├── Icon-App-40x40@3x.png
│ │ ├── Icon-App-60x60@2x.png
│ │ ├── Icon-App-60x60@3x.png
│ │ ├── Icon-App-76x76@1x.png
│ │ ├── Icon-App-76x76@2x.png
│ │ └── Icon-App-83.5x83.5@2x.png
│ └── LaunchImage.imageset
│ │ ├── Contents.json
│ │ ├── LaunchImage.png
│ │ ├── LaunchImage@2x.png
│ │ ├── LaunchImage@3x.png
│ │ └── README.md
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── Info.plist
│ └── Runner-Bridging-Header.h
├── l10n.yaml
├── lib
├── app.dart
├── data
│ └── categories.dart
├── main.dart
├── model
│ ├── app.dart
│ ├── app.g.dart
│ ├── collection.dart
│ └── collection.g.dart
├── page
│ ├── app.dart
│ ├── collections.dart
│ ├── settings.dart
│ └── widget
│ │ └── install.dart
├── theme.dart
├── util.dart
└── util
│ ├── install_task.dart
│ └── sai_str_map.dart
├── minimal-app-template.yaml
├── pubspec.lock
├── pubspec.yaml
├── screenshots
├── screen1.jpg
├── screen2.jpg
└── screen3.jpg
├── skydroid-app.yaml
├── skydroid-dev.yaml
└── test
└── widget_test.dart
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: redsolver
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # The .vscode folder contains launch configuration and tasks you configure in
19 | # VS Code which you may wish to be included in version control, so this line
20 | # is commented out by default.
21 | #.vscode/
22 |
23 | # Flutter/Dart/Pub related
24 | **/doc/api/
25 | .dart_tool/
26 | .flutter-plugins
27 | .flutter-plugins-dependencies
28 | .packages
29 | .pub-cache/
30 | .pub/
31 | /build/
32 |
33 | # Web related
34 | lib/generated_plugin_registrant.dart
35 |
36 | # Symbolication related
37 | app.*.symbols
38 |
39 | # Obfuscation related
40 | app.*.map.json
41 |
42 | # Exceptions to above rules.
43 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
44 |
45 | /data/
46 |
47 | web
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 8af6b2f038c1172e61d418869363a28dffec3cb4
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Flutter",
9 | "program": "lib/main.dart",
10 | "request": "launch",
11 | "type": "dart"
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "svg.preview.background": "transparent"
3 | }
--------------------------------------------------------------------------------
/.weblate:
--------------------------------------------------------------------------------
1 | [weblate]
2 | url = https://weblate.bubu1.eu/api/
3 | translation = skydroid/skydroid-app
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.5.5
4 |
5 | - Added support for different versionCodes for different ABIs for the same versionName (This fixes the bug that some apps can't be installed)
6 |
7 | ## 0.5.4
8 |
9 | - Added new languages and updated translations (Thanks to all contributors!)
10 | - Added new URI scheme to make app links more reliable
11 | - Small improvements and bugfixes
12 |
13 | ## 0.5.3
14 |
15 | Important bug fix: Wrong language used instead of English on unsupported locales
16 |
17 | ## 0.5.2
18 |
19 | - Updated translations
20 | - Changed app icon
21 | - Updated recommended collections
22 | - Fixed some small bugs
23 |
24 | ## 0.5.1
25 |
26 | - Skydroid now prefers the `_skydroid` subdomain for `TXT` records to prevent collision with existing records (Both ways are still supported)
27 | - Changed domain of the Shizuku Service App to bridged IzzyOnDroid repo (https://collection.skydroid.app/izzyondroid)
28 |
29 | ## 0.4.1
30 |
31 | - Added batch selection and processing actions
32 | - Added deeplink support for collections
33 | - Added automatic cache cleanup
34 | - Improved error messages
35 | - Translated in Czech
36 |
37 | ## 0.3.0
38 |
39 | - Translated in English, German, French and Dutch
40 | - Shizuku Service Support for easier app installation
41 | - Split-APK support for up to 3x smaller download sizes
42 | - Changed accent color
43 | - Fixed some bugs and added some optimizations
44 |
45 | ## 0.2.4
46 |
47 | Fixed error when categories are missing in the metadata
48 |
49 | ## 0.2.3
50 |
51 | Performance optimizations to limit the number of concurrent network requests
52 |
53 | ## 0.2.2
54 |
55 | Fixed Out of Memory when downloading APKs bug on devices with less RAM
56 |
57 | ## 0.2.0
58 |
59 | You can now share apps via the to.skydroid.app domain and open them directly in SkyDroid!
60 |
61 | ## 0.1.4
62 |
63 | - Changed Domain of F-Droid Collection to "fdroid-app"
64 | - Fixed a small bug which marked an app downgrade as an update in some cases
65 |
66 | ## 0.1.3
67 |
68 | - Added option to remove all apps from a specific collection
69 |
70 | ## 0.1.2
71 |
72 | - Fixed some bugs
73 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 | /key.properties
9 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | def localProperties = new Properties()
2 | def localPropertiesFile = rootProject.file('local.properties')
3 | if (localPropertiesFile.exists()) {
4 | localPropertiesFile.withReader('UTF-8') { reader ->
5 | localProperties.load(reader)
6 | }
7 | }
8 |
9 | def flutterRoot = localProperties.getProperty('flutter.sdk')
10 | if (flutterRoot == null) {
11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
12 | }
13 |
14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
15 | if (flutterVersionCode == null) {
16 | flutterVersionCode = '1'
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | flutterVersionName = '1.0'
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply plugin: 'kotlin-android'
26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
27 |
28 | def keystorePropertiesFile = rootProject.file("key.properties")
29 | def keystoreProperties = new Properties()
30 | if (keystorePropertiesFile.exists()) {
31 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
32 | }
33 |
34 | android {
35 | compileSdkVersion 28
36 |
37 | sourceSets {
38 | main.java.srcDirs += 'src/main/kotlin'
39 | }
40 |
41 | lintOptions {
42 | disable 'InvalidPackage'
43 | }
44 |
45 | defaultConfig {
46 | applicationId "app.skydroid"
47 | minSdkVersion 16
48 | targetSdkVersion 28
49 | versionCode flutterVersionCode.toInteger()
50 | versionName flutterVersionName
51 | }
52 |
53 | signingConfigs {
54 | release {
55 | keyAlias keystoreProperties['keyAlias']
56 | keyPassword keystoreProperties['keyPassword']
57 | storeFile file(keystoreProperties['storeFile'])
58 | storePassword keystoreProperties['storePassword']
59 | }
60 | }
61 |
62 | buildTypes {
63 | release {
64 | signingConfig signingConfigs.release
65 | }
66 | }
67 | }
68 |
69 | flutter {
70 | source '../..'
71 | }
72 |
73 | dependencies {
74 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
75 |
76 |
77 | implementation 'androidx.documentfile:documentfile:1.0.1'
78 | implementation 'androidx.preference:preference:1.1.1'
79 |
80 | //Shizuku/Sui
81 | def shizuku_version = '11.0.1'
82 | implementation "rikka.shizuku:api:$shizuku_version"
83 | implementation "rikka.shizuku:provider:$shizuku_version"
84 | }
85 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
23 |
24 |
27 |
28 |
35 |
39 |
43 |
48 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
66 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
80 |
83 |
84 |
85 |
86 |
88 |
91 |
100 |
101 |
106 |
109 |
110 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer/ApkSourceBuilder.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.installer;
2 |
3 | import android.content.Context;
4 | import android.net.Uri;
5 |
6 | import com.aefyr.sai.model.apksource.ApkSource;
7 | import com.aefyr.sai.model.apksource.CopyToFileApkSource;
8 | import com.aefyr.sai.model.apksource.DefaultApkSource;
9 | import com.aefyr.sai.model.apksource.FilterApkSource;
10 | //import com.aefyr.sai.model.apksource.SignerApkSource;
11 | //import com.aefyr.sai.model.apksource.ZipApkSource;
12 | import com.aefyr.sai.model.apksource.ZipBackedApkSource;
13 | //import com.aefyr.sai.model.apksource.ZipFileApkSource;
14 | import com.aefyr.sai.model.filedescriptor.ContentUriFileDescriptor;
15 | import com.aefyr.sai.model.filedescriptor.FileDescriptor;
16 | import com.aefyr.sai.model.filedescriptor.NormalFileDescriptor;
17 |
18 | import java.io.File;
19 | import java.util.ArrayList;
20 | import java.util.List;
21 | import java.util.Set;
22 |
23 | public class ApkSourceBuilder {
24 |
25 | private Context mContext;
26 |
27 | private boolean mSourceSet;
28 | private List mApkFiles;
29 | private File mZipFile;
30 | private Uri mZipUri;
31 | private List mApkUris;
32 |
33 | private boolean mSigningEnabled;
34 | private boolean mZipExtractionEnabled;
35 | private boolean mReadZipViaZipFileEnabled;
36 |
37 | private Set mFilteredApks;
38 | private boolean mBlacklist;
39 |
40 | public ApkSourceBuilder(Context c) {
41 | mContext = c;
42 | }
43 |
44 | public ApkSourceBuilder fromApkFiles(List apkFiles) {
45 | ensureSourceSetOnce();
46 | mApkFiles = apkFiles;
47 | return this;
48 | }
49 |
50 | public ApkSourceBuilder fromZipFile(File zipFile) {
51 | ensureSourceSetOnce();
52 | mZipFile = zipFile;
53 | return this;
54 | }
55 |
56 | public ApkSourceBuilder fromZipContentUri(Uri zipUri) {
57 | ensureSourceSetOnce();
58 | mZipUri = zipUri;
59 | return this;
60 | }
61 |
62 | public ApkSourceBuilder fromApkContentUris(List uris) {
63 | ensureSourceSetOnce();
64 | mApkUris = uris;
65 | return this;
66 | }
67 |
68 | public ApkSourceBuilder setSigningEnabled(boolean enabled) {
69 | mSigningEnabled = enabled;
70 | return this;
71 | }
72 |
73 | public ApkSourceBuilder setZipExtractionEnabled(boolean enabled) {
74 | mZipExtractionEnabled = enabled;
75 | return this;
76 | }
77 |
78 | public ApkSourceBuilder setReadZipViaZipFileEnabled(boolean enabled) {
79 | mReadZipViaZipFileEnabled = enabled;
80 | return this;
81 | }
82 |
83 | public ApkSourceBuilder filterApksByLocalPath(Set filteredApks, boolean blacklist) {
84 | mFilteredApks = filteredApks;
85 | mBlacklist = blacklist;
86 | return this;
87 | }
88 |
89 | public ApkSource build() {
90 | ApkSource apkSource;
91 |
92 | boolean sourceIsZip = false;
93 |
94 | if (mApkFiles != null) {
95 | List apkFileDescriptors = new ArrayList<>(mApkFiles.size());
96 | for (File apkFile : mApkFiles)
97 | apkFileDescriptors.add(new NormalFileDescriptor(apkFile));
98 |
99 | apkSource = new DefaultApkSource(apkFileDescriptors);
100 | }else if (mApkUris != null) {
101 | List apkUriDescriptors = new ArrayList<>(mApkUris.size());
102 | for (Uri apkUri : mApkUris)
103 | apkUriDescriptors.add(new ContentUriFileDescriptor(mContext, apkUri));
104 |
105 | apkSource = new DefaultApkSource(apkUriDescriptors);
106 | } else {
107 | throw new IllegalStateException("No source set");
108 | }
109 |
110 | // if (mSigningEnabled)
111 | // apkSource = new SignerApkSource(mContext, apkSource);
112 |
113 | //Signing already uses temp files, so there's not reason to use CopyToFileApkSource with it
114 | if (mZipExtractionEnabled && sourceIsZip && !mSigningEnabled) {
115 | apkSource = new CopyToFileApkSource(mContext, apkSource);
116 | }
117 |
118 | if (mFilteredApks != null)
119 | apkSource = new FilterApkSource(apkSource, mFilteredApks, mBlacklist);
120 |
121 | return apkSource;
122 | }
123 |
124 | private void ensureSourceSetOnce() {
125 | if (mSourceSet)
126 | throw new IllegalStateException("Source can be only be set once");
127 | mSourceSet = true;
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer/PackageInstallerProvider.java:
--------------------------------------------------------------------------------
1 | /* package com.aefyr.sai.installer;
2 |
3 | import android.content.Context;
4 |
5 | import com.aefyr.sai.installer.rooted.RootedSAIPackageInstaller;
6 | import com.aefyr.sai.installer.rootless.RootlessSAIPackageInstaller;
7 | import com.aefyr.sai.installer.shizuku.ShizukuSAIPackageInstaller;
8 | import com.aefyr.sai.utils.PreferencesHelper;
9 | import com.aefyr.sai.utils.PreferencesValues;
10 |
11 | public class PackageInstallerProvider {
12 | public static SAIPackageInstaller getInstaller(Context c) {
13 |
14 | switch (PreferencesHelper.getInstance(c).getInstaller()) {
15 | case PreferencesValues.INSTALLER_ROOTLESS:
16 | return RootlessSAIPackageInstaller.getInstance(c);
17 | case PreferencesValues.INSTALLER_ROOTED:
18 | return RootedSAIPackageInstaller.getInstance(c);
19 | case PreferencesValues.INSTALLER_SHIZUKU:
20 | return ShizukuSAIPackageInstaller.getInstance(c);
21 | }
22 |
23 | return RootlessSAIPackageInstaller.getInstance(c);
24 | }
25 | }
26 | */
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer/QueuedInstallation.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.installer;
2 |
3 | import android.content.Context;
4 |
5 | import com.aefyr.sai.model.apksource.ApkSource;
6 |
7 | public class QueuedInstallation {
8 |
9 | private Context mContext;
10 | private ApkSource mApkSource;
11 | private long mId;
12 |
13 | QueuedInstallation(Context c, ApkSource apkSource, long id) {
14 | mContext = c;
15 | mApkSource = apkSource;
16 | mId = id;
17 | }
18 |
19 | public long getId() {
20 | return mId;
21 | }
22 |
23 | ApkSource getApkSource() {
24 | return mApkSource;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer/SAIPackageInstaller.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.installer;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.os.Handler;
6 | import android.os.Looper;
7 | import android.util.Log;
8 | import android.util.LongSparseArray;
9 |
10 | import androidx.annotation.Nullable;
11 |
12 | import com.aefyr.sai.model.apksource.ApkSource;
13 | // import com.aefyr.sai.utils.Logs;
14 |
15 | import java.util.ArrayDeque;
16 | import java.util.ArrayList;
17 | import java.util.concurrent.ExecutorService;
18 | import java.util.concurrent.Executors;
19 |
20 | @SuppressLint("DefaultLocale")
21 | public abstract class SAIPackageInstaller {
22 | private static final String TAG = "SAIPI";
23 |
24 | public enum InstallationStatus {
25 | QUEUED, INSTALLING, INSTALLATION_SUCCEED, INSTALLATION_FAILED
26 | }
27 |
28 | private Context mContext;
29 | private Handler mHandler = new Handler(Looper.getMainLooper());
30 | private ExecutorService mExecutor = Executors.newSingleThreadExecutor();
31 |
32 | private ArrayDeque mInstallationQueue = new ArrayDeque<>();
33 | private ArrayList mListeners = new ArrayList<>();
34 | private LongSparseArray mCreatedInstallationSessions = new LongSparseArray<>();
35 |
36 | private boolean mInstallationInProgress;
37 | private long mLastInstallationID = 0;
38 | private QueuedInstallation mOngoingInstallation;
39 |
40 | protected SAIPackageInstaller(Context c) {
41 | mContext = c.getApplicationContext();
42 | }
43 |
44 | protected Context getContext() {
45 | return mContext;
46 | }
47 |
48 | public interface InstallationStatusListener {
49 | void onStatusChanged(long installationID, InstallationStatus status, @Nullable String packageNameOrErrorDescription);
50 | }
51 |
52 | public void addStatusListener(InstallationStatusListener listener) {
53 | mListeners.add(listener);
54 | }
55 |
56 | public void removeStatusListener(InstallationStatusListener listener) {
57 | mListeners.remove(listener);
58 | }
59 |
60 | public long createInstallationSession(ApkSource apkSource) {
61 | long installationID = mLastInstallationID++;
62 | mCreatedInstallationSessions.put(installationID, new QueuedInstallation(getContext(), apkSource, installationID));
63 | return installationID;
64 | }
65 |
66 | public void startInstallationSession(long sessionID) {
67 | QueuedInstallation installation = mCreatedInstallationSessions.get(sessionID);
68 | mCreatedInstallationSessions.remove(sessionID);
69 | if (installation == null)
70 | return;
71 |
72 | mInstallationQueue.addLast(installation);
73 | dispatchSessionUpdate(installation.getId(), InstallationStatus.QUEUED, null);
74 | processQueue();
75 | }
76 |
77 | public boolean isInstallationInProgress() {
78 | return mInstallationInProgress;
79 | }
80 |
81 | private void processQueue() {
82 | if (mInstallationQueue.size() == 0 || mInstallationInProgress)
83 | return;
84 |
85 | QueuedInstallation installation = mInstallationQueue.removeFirst();
86 | mOngoingInstallation = installation;
87 | mInstallationInProgress = true;
88 |
89 | dispatchCurrentSessionUpdate(InstallationStatus.INSTALLING, null);
90 |
91 | mExecutor.execute(() -> installApkFiles(installation.getApkSource()));
92 | }
93 |
94 | protected abstract void installApkFiles(ApkSource apkSource);
95 |
96 | protected void installationCompleted() {
97 | Log.d(TAG, String.format("%s->installationCompleted(); mOngoingInstallation.id=%d", getClass().getSimpleName(), dbgGetOngoingInstallationId()));
98 | mInstallationInProgress = false;
99 | mOngoingInstallation = null;
100 | processQueue();
101 | }
102 |
103 | protected void dispatchSessionUpdate(long sessionID, InstallationStatus status, String packageNameOrError) {
104 | mHandler.post(() -> {
105 | Log.d(TAG, String.format("%s->dispatchSessionUpdate(%d, %s, %s)", getClass().getSimpleName(), sessionID, status.name(), packageNameOrError));
106 | for (InstallationStatusListener listener : mListeners)
107 | listener.onStatusChanged(sessionID, status, packageNameOrError);
108 | });
109 | }
110 |
111 | protected void dispatchCurrentSessionUpdate(InstallationStatus status, String packageNameOrError) {
112 | Log.d(TAG, String.format("%s->dispatchCurrentSessionUpdate(%s, %s); mOngoingInstallation.id=%d", getClass().getSimpleName(), status.name(), packageNameOrError, dbgGetOngoingInstallationId()));
113 | dispatchSessionUpdate(mOngoingInstallation.getId(), status, packageNameOrError);
114 | }
115 |
116 | private long dbgGetOngoingInstallationId() {
117 | return mOngoingInstallation != null ? mOngoingInstallation.getId() : -1;
118 | }
119 |
120 | protected QueuedInstallation getOngoingInstallation() {
121 | return mOngoingInstallation;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer/rooted/RootedSAIPackageInstaller.java:
--------------------------------------------------------------------------------
1 | /* package com.aefyr.sai.installer.rooted;
2 |
3 | import android.content.Context;
4 |
5 | import com.aefyr.sai.R;
6 | import com.aefyr.sai.installer.ShellSAIPackageInstaller;
7 | import com.aefyr.sai.shell.Shell;
8 | import com.aefyr.sai.shell.SuShell;
9 |
10 | public class RootedSAIPackageInstaller extends ShellSAIPackageInstaller {
11 | private static RootedSAIPackageInstaller sInstance;
12 |
13 | public static RootedSAIPackageInstaller getInstance(Context c) {
14 | synchronized (RootedSAIPackageInstaller.class) {
15 | return sInstance != null ? sInstance : new RootedSAIPackageInstaller(c);
16 | }
17 | }
18 |
19 | private RootedSAIPackageInstaller(Context c) {
20 | super(c);
21 | sInstance = this;
22 | }
23 |
24 | @Override
25 | protected Shell getShell() {
26 | return SuShell.getInstance();
27 | }
28 |
29 | @Override
30 | protected String getInstallerName() {
31 | return "Rooted";
32 | }
33 |
34 | @Override
35 | protected String getShellUnavailableMessage() {
36 | return getContext().getString(R.string.installer_error_root_no_root);
37 | }
38 | }
39 | */
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer/rootless/RootlessSAIPIService.java:
--------------------------------------------------------------------------------
1 | /* package com.aefyr.sai.installer.rootless;
2 |
3 | import android.app.Service;
4 | import android.content.Intent;
5 | import android.content.pm.PackageInstaller;
6 | import android.os.IBinder;
7 | import android.util.Log;
8 |
9 | import androidx.annotation.Nullable;
10 |
11 | import com.aefyr.sai.R;
12 | import com.aefyr.sai.ui.activities.ConfirmationIntentWrapperActivity;
13 | import com.aefyr.sai.utils.Utils;
14 |
15 |
16 | public class RootlessSAIPIService extends Service {
17 | private static final String TAG = "RootlessSAIPIService";
18 |
19 | public static final String ACTION_INSTALLATION_STATUS_NOTIFICATION = "com.aefyr.sai.action.INSTALLATION_STATUS_NOTIFICATION";
20 | public static final String EXTRA_INSTALLATION_STATUS = "com.aefyr.sai.extra.INSTALLATION_STATUS";
21 | public static final String EXTRA_SESSION_ID = "com.aefyr.sai.extra.SESSION_ID";
22 | public static final String EXTRA_PACKAGE_NAME = "com.aefyr.sai.extra.PACKAGE_NAME";
23 | public static final String EXTRA_ERROR_DESCRIPTION = "com.aefyr.sai.extra.ERROR_DESCRIPTION";
24 |
25 | public static final int STATUS_SUCCESS = 0;
26 | public static final int STATUS_CONFIRMATION_PENDING = 1;
27 | public static final int STATUS_FAILURE = 2;
28 |
29 | @Override
30 | public int onStartCommand(Intent intent, int flags, int startId) {
31 | int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999);
32 | switch (status) {
33 | case PackageInstaller.STATUS_PENDING_USER_ACTION:
34 | Log.d(TAG, "Requesting user confirmation for installation");
35 | sendStatusChangeBroadcast(intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1), STATUS_CONFIRMATION_PENDING, intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME));
36 | Intent confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
37 |
38 | ConfirmationIntentWrapperActivity.start(this, intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1), confirmationIntent);
39 | break;
40 | case PackageInstaller.STATUS_SUCCESS:
41 | Log.d(TAG, "Installation succeed");
42 | sendStatusChangeBroadcast(intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1), STATUS_SUCCESS, intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME));
43 | break;
44 | default:
45 | Log.d(TAG, "Installation failed");
46 | sendErrorBroadcast(intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1), getErrorString(status, intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)));
47 | break;
48 | }
49 | stopSelf();
50 | return START_NOT_STICKY;
51 | }
52 |
53 | private void sendStatusChangeBroadcast(int sessionID, int status, String packageName) {
54 | Intent statusIntent = new Intent(ACTION_INSTALLATION_STATUS_NOTIFICATION);
55 | statusIntent.putExtra(EXTRA_INSTALLATION_STATUS, status);
56 | statusIntent.putExtra(EXTRA_SESSION_ID, sessionID);
57 |
58 | if (packageName != null)
59 | statusIntent.putExtra(EXTRA_PACKAGE_NAME, packageName);
60 |
61 | sendBroadcast(statusIntent);
62 | }
63 |
64 | private void sendErrorBroadcast(int sessionID, String error) {
65 | Intent statusIntent = new Intent(ACTION_INSTALLATION_STATUS_NOTIFICATION);
66 | statusIntent.putExtra(EXTRA_INSTALLATION_STATUS, STATUS_FAILURE);
67 | statusIntent.putExtra(EXTRA_SESSION_ID, sessionID);
68 | statusIntent.putExtra(EXTRA_ERROR_DESCRIPTION, error);
69 |
70 | sendBroadcast(statusIntent);
71 | }
72 |
73 | public String getErrorString(int status, String blockingPackage) {
74 | switch (status) {
75 | case PackageInstaller.STATUS_FAILURE_ABORTED:
76 | return getString(R.string.installer_error_aborted);
77 |
78 | case PackageInstaller.STATUS_FAILURE_BLOCKED:
79 | String blocker = getString(R.string.installer_error_blocked_device);
80 | if (blockingPackage != null) {
81 | String appLabel = Utils.getAppLabel(getApplicationContext(), blockingPackage);
82 | if (appLabel != null)
83 | blocker = appLabel;
84 | }
85 | return getString(R.string.installer_error_blocked, blocker);
86 |
87 | case PackageInstaller.STATUS_FAILURE_CONFLICT:
88 | return getString(R.string.installer_error_conflict);
89 |
90 | case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE:
91 | return getString(R.string.installer_error_incompatible);
92 |
93 | case PackageInstaller.STATUS_FAILURE_INVALID:
94 | return getString(R.string.installer_error_bad_apks);
95 |
96 | case PackageInstaller.STATUS_FAILURE_STORAGE:
97 | return getString(R.string.installer_error_storage);
98 | }
99 | return getString(R.string.installer_error_generic);
100 | }
101 |
102 | @Nullable
103 | @Override
104 | public IBinder onBind(Intent intent) {
105 | return null;
106 | }
107 | }
108 | */
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer/rootless/RootlessSAIPackageInstaller.java:
--------------------------------------------------------------------------------
1 | /* package com.aefyr.sai.installer.rootless;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.app.PendingIntent;
5 | import android.content.BroadcastReceiver;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.content.IntentFilter;
9 | import android.content.pm.PackageInstaller;
10 | import android.content.pm.PackageManager;
11 | import android.os.Build;
12 | import android.util.Log;
13 | import android.util.SparseLongArray;
14 |
15 | import com.aefyr.sai.R;
16 | import com.aefyr.sai.installer.SAIPackageInstaller;
17 | import com.aefyr.sai.model.apksource.ApkSource;
18 | import com.aefyr.sai.utils.IOUtils;
19 | import com.aefyr.sai.utils.PreferencesHelper;
20 | import com.aefyr.sai.utils.Utils;
21 |
22 | import java.io.InputStream;
23 | import java.io.OutputStream;
24 |
25 | public class RootlessSAIPackageInstaller extends SAIPackageInstaller {
26 | private static final String TAG = "RootlessSAIPI";
27 |
28 | @SuppressLint("StaticFieldLeak")//This is application context, lul
29 | private static RootlessSAIPackageInstaller sInstance;
30 |
31 | private BroadcastReceiver mFurtherInstallationEventsReceiver = new BroadcastReceiver() {
32 | @Override
33 | public void onReceive(Context context, Intent intent) {
34 | long sessionId = mSessionsMap.get(intent.getIntExtra(RootlessSAIPIService.EXTRA_SESSION_ID, -1), -1);
35 | if (sessionId == -1)
36 | return;
37 | switch (intent.getIntExtra(RootlessSAIPIService.EXTRA_INSTALLATION_STATUS, -1)) {
38 | case RootlessSAIPIService.STATUS_SUCCESS:
39 | dispatchSessionUpdate(sessionId, SAIPackageInstaller.InstallationStatus.INSTALLATION_SUCCEED, intent.getStringExtra(RootlessSAIPIService.EXTRA_PACKAGE_NAME));
40 | if (getOngoingInstallation() != null && sessionId == getOngoingInstallation().getId())
41 | installationCompleted();
42 | break;
43 | case RootlessSAIPIService.STATUS_FAILURE:
44 | dispatchSessionUpdate(sessionId, SAIPackageInstaller.InstallationStatus.INSTALLATION_FAILED, intent.getStringExtra(RootlessSAIPIService.EXTRA_ERROR_DESCRIPTION));
45 | if (getOngoingInstallation() != null && sessionId == getOngoingInstallation().getId())
46 | installationCompleted();
47 | break;
48 | }
49 | }
50 | };
51 |
52 | private PackageInstaller mPackageInstaller;
53 |
54 |
55 | private SparseLongArray mSessionsMap = new SparseLongArray();
56 |
57 |
58 | public static RootlessSAIPackageInstaller getInstance(Context c) {
59 | return sInstance != null ? sInstance : new RootlessSAIPackageInstaller(c);
60 | }
61 |
62 | private RootlessSAIPackageInstaller(Context c) {
63 | super(c);
64 | mPackageInstaller = getContext().getPackageManager().getPackageInstaller();
65 | getContext().registerReceiver(mFurtherInstallationEventsReceiver, new IntentFilter(RootlessSAIPIService.ACTION_INSTALLATION_STATUS_NOTIFICATION));
66 | sInstance = this;
67 | }
68 |
69 | @SuppressLint("DefaultLocale")
70 | @Override
71 | protected void installApkFiles(ApkSource aApkSource) {
72 | cleanOldSessions();
73 |
74 | PackageInstaller.Session session = null;
75 | try (ApkSource apkSource = aApkSource) {
76 | PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
77 | sessionParams.setInstallLocation(PreferencesHelper.getInstance(getContext()).getInstallLocation());
78 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
79 | sessionParams.setInstallReason(PackageManager.INSTALL_REASON_USER);
80 |
81 | int sessionID = mPackageInstaller.createSession(sessionParams);
82 | mSessionsMap.put(sessionID, getOngoingInstallation().getId());
83 |
84 | session = mPackageInstaller.openSession(sessionID);
85 | int currentApkFile = 0;
86 | while (apkSource.nextApk()) {
87 | try (InputStream inputStream = apkSource.openApkInputStream(); OutputStream outputStream = session.openWrite(String.format("%d.apk", currentApkFile++), 0, apkSource.getApkLength())) {
88 | IOUtils.copyStream(inputStream, outputStream);
89 | session.fsync(outputStream);
90 | }
91 | }
92 |
93 | Intent callbackIntent = new Intent(getContext(), RootlessSAIPIService.class);
94 | PendingIntent pendingIntent = PendingIntent.getService(getContext(), 0, callbackIntent, 0);
95 | session.commit(pendingIntent.getIntentSender());
96 | } catch (Exception e) {
97 | Log.w(TAG, e);
98 | dispatchCurrentSessionUpdate(InstallationStatus.INSTALLATION_FAILED, getContext().getString(R.string.installer_error_rootless, Utils.throwableToString(e)));
99 | installationCompleted();
100 | } finally {
101 | if (session != null)
102 | session.close();
103 | }
104 | }
105 |
106 | private void cleanOldSessions() {
107 | int cleanedSessions = 0;
108 | long start = System.currentTimeMillis();
109 |
110 | for (PackageInstaller.SessionInfo sessionInfo : mPackageInstaller.getMySessions()) {
111 | try {
112 | mPackageInstaller.abandonSession(sessionInfo.getSessionId());
113 | cleanedSessions++;
114 | } catch (Exception e) {
115 | Log.w(TAG, "Unable to abandon session", e);
116 | }
117 | }
118 |
119 | Log.d(TAG, String.format("Cleaned %d sessions in %d ms.", cleanedSessions, (System.currentTimeMillis() - start)));
120 | }
121 | }
122 | */
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer/shizuku/ShizukuSAIPackageInstaller.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.installer.shizuku;
2 |
3 | import android.content.Context;
4 |
5 |
6 | import com.aefyr.sai.installer.ShellSAIPackageInstaller;
7 | import com.aefyr.sai.shell.Shell;
8 | import com.aefyr.sai.shell.ShizukuShell;
9 |
10 | public class ShizukuSAIPackageInstaller extends ShellSAIPackageInstaller {
11 | private static ShizukuSAIPackageInstaller sInstance;
12 |
13 | public static ShizukuSAIPackageInstaller getInstance(Context c) {
14 | synchronized (ShizukuSAIPackageInstaller.class) {
15 | return sInstance != null ? sInstance : new ShizukuSAIPackageInstaller(c);
16 | }
17 | }
18 |
19 | private ShizukuSAIPackageInstaller(Context c) {
20 | super(c);
21 | sInstance = this;
22 | }
23 |
24 | @Override
25 | protected Shell getShell() {
26 | return ShizukuShell.getInstance();
27 | }
28 |
29 | @Override
30 | protected String getInstallerName() {
31 | return "Shizuku";
32 | }
33 |
34 | @Override
35 | protected String getShellUnavailableMessage() {
36 | return "installer_error_shizuku_unavailable";
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer2/base/SaiPackageInstaller.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.installer2.base;
2 |
3 | import com.aefyr.sai.installer2.base.model.SaiPiSessionParams;
4 | import com.aefyr.sai.installer2.base.model.SaiPiSessionState;
5 |
6 | import java.util.List;
7 |
8 | public interface SaiPackageInstaller {
9 |
10 | String createSession(SaiPiSessionParams params);
11 |
12 | void enqueueSession(String sessionId);
13 |
14 | void registerSessionObserver(SaiPiSessionObserver observer);
15 |
16 | void unregisterSessionObserver(SaiPiSessionObserver observer);
17 |
18 | List getSessions();
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer2/base/SaiPiSessionObserver.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.installer2.base;
2 |
3 | import com.aefyr.sai.installer2.base.model.SaiPiSessionState;
4 |
5 | public interface SaiPiSessionObserver {
6 |
7 | void onSessionStateChanged(SaiPiSessionState state);
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer2/base/model/SaiPiSessionParams.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.installer2.base.model;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.aefyr.sai.model.apksource.ApkSource;
6 |
7 | public class SaiPiSessionParams {
8 |
9 | private ApkSource mApkSource;
10 |
11 | public SaiPiSessionParams(@NonNull ApkSource apkSource) {
12 | mApkSource = apkSource;
13 | }
14 |
15 | @NonNull
16 | public ApkSource apkSource() {
17 | return mApkSource;
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer2/base/model/SaiPiSessionState.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.installer2.base.model;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 |
6 | import androidx.annotation.NonNull;
7 | import androidx.annotation.Nullable;
8 |
9 | import com.aefyr.sai.model.common.PackageMeta;
10 | import com.aefyr.sai.utils.Stopwatch;
11 |
12 | public class SaiPiSessionState implements Comparable {
13 |
14 | private String mSessionId;
15 | private SaiPiSessionStatus mStatus;
16 | private String mPackageName;
17 | private String mAppTempName;
18 | private PackageMeta mPackageMeta;
19 | private long mLastUpdate;
20 | private String mShortError;
21 | private String mFullError;
22 |
23 | private SaiPiSessionState(String sessionId, SaiPiSessionStatus status) {
24 | mSessionId = sessionId;
25 | mStatus = status;
26 | mLastUpdate = System.currentTimeMillis();
27 | }
28 |
29 | public String sessionId() {
30 | return mSessionId;
31 | }
32 |
33 | public SaiPiSessionStatus status() {
34 | return mStatus;
35 | }
36 |
37 | @Nullable
38 | public String packageName() {
39 | return mPackageName;
40 | }
41 |
42 | @Nullable
43 | public String appTempName() {
44 | if (mAppTempName != null)
45 | return mAppTempName;
46 |
47 | if (mPackageName != null)
48 | return mPackageName;
49 |
50 | return null;
51 | }
52 |
53 | @Nullable
54 | public PackageMeta packageMeta() {
55 | return mPackageMeta;
56 | }
57 |
58 | /**
59 | * @return user-readable error description
60 | */
61 | @Nullable
62 | public String shortError() {
63 | return mShortError;
64 | }
65 |
66 | /**
67 | * @return full error info for debugging and stuff. May be same as {@link #shortError()} if there's no better info
68 | */
69 | @Nullable
70 | public String fullError() {
71 | return mFullError;
72 | }
73 |
74 | public long lastUpdate() {
75 | return mLastUpdate;
76 | }
77 |
78 | public Builder newBuilder() {
79 | return new Builder(mSessionId, mStatus)
80 | .packageName(packageName())
81 | .appTempName(appTempName())
82 | .packageMeta(packageMeta())
83 | .error(shortError(), fullError());
84 | }
85 |
86 | @Override
87 | public int hashCode() {
88 | return sessionId().hashCode();
89 | }
90 |
91 | @Override
92 | public boolean equals(@Nullable Object obj) {
93 | return obj instanceof SaiPiSessionState && ((SaiPiSessionState) obj).sessionId().equals(sessionId());
94 | }
95 |
96 | @NonNull
97 | @Override
98 | public String toString() {
99 | StringBuilder sb = new StringBuilder();
100 | sb.append(String.format("SaiPiSessionState: sessionId=%s, status=%s", sessionId(), status()));
101 | return sb.toString();
102 | }
103 |
104 | @Override
105 | public int compareTo(SaiPiSessionState o) {
106 | return Long.compare(o.lastUpdate(), lastUpdate());
107 | }
108 |
109 | public static class Builder {
110 | private SaiPiSessionState mState;
111 |
112 | public Builder(@NonNull String sessionId, @NonNull SaiPiSessionStatus status) {
113 | mState = new SaiPiSessionState(sessionId, status);
114 | }
115 |
116 | public Builder packageName(@Nullable String packageName) {
117 | mState.mPackageName = packageName;
118 | return this;
119 | }
120 |
121 | public Builder appTempName(@Nullable String tempAppName) {
122 | mState.mAppTempName = tempAppName;
123 | return this;
124 | }
125 |
126 | public Builder resolvePackageMeta(Context c) {
127 | if (mState.mPackageName == null)
128 | return this;
129 |
130 | Stopwatch sw = new Stopwatch();
131 | mState.mPackageMeta = PackageMeta.forPackage(c, mState.mPackageName);
132 | Log.d("SaiPiSessionState", String.format("Got PackageMeta in %d ms.", sw.millisSinceStart()));
133 | return this;
134 | }
135 |
136 | public Builder packageMeta(@Nullable PackageMeta packageMeta) {
137 | mState.mPackageMeta = packageMeta;
138 | return this;
139 | }
140 |
141 | public Builder error(String shortError, @Nullable String fullError) {
142 | mState.mShortError = shortError;
143 | if (fullError == null)
144 | fullError = shortError;
145 |
146 | mState.mFullError = fullError;
147 | return this;
148 | }
149 |
150 | public SaiPiSessionState build() {
151 | mState.mLastUpdate = System.currentTimeMillis();
152 | return mState;
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer2/base/model/SaiPiSessionStatus.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.installer2.base.model;
2 |
3 | import android.content.Context;
4 |
5 |
6 | public enum SaiPiSessionStatus {
7 | CREATED, QUEUED, INSTALLING, INSTALLATION_SUCCEED, INSTALLATION_FAILED;
8 |
9 | public String getReadableName(Context c) {
10 | switch (this) {
11 | case CREATED:
12 | return "installer_state_created";
13 | case QUEUED:
14 | return "installer_state_queued";
15 | case INSTALLING:
16 | return "installer_state_installing";
17 | case INSTALLATION_SUCCEED:
18 | return "installer_state_installed";
19 | case INSTALLATION_FAILED:
20 | return "installer_state_failed";
21 | }
22 |
23 | throw new IllegalStateException("wtf");
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer2/impl/BaseSaiPackageInstaller.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.installer2.impl;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.util.Log;
6 |
7 | import com.aefyr.sai.installer2.base.SaiPackageInstaller;
8 | import com.aefyr.sai.installer2.base.SaiPiSessionObserver;
9 | import com.aefyr.sai.installer2.base.model.SaiPiSessionParams;
10 | import com.aefyr.sai.installer2.base.model.SaiPiSessionState;
11 | import com.aefyr.sai.installer2.base.model.SaiPiSessionStatus;
12 | import com.aefyr.sai.utils.Utils;
13 |
14 | import java.util.ArrayList;
15 | import java.util.Collections;
16 | import java.util.List;
17 | import java.util.Set;
18 | import java.util.concurrent.ConcurrentHashMap;
19 | import java.util.concurrent.ConcurrentSkipListMap;
20 |
21 | @SuppressLint("UseSparseArrays")
22 | public abstract class BaseSaiPackageInstaller implements SaiPackageInstaller {
23 |
24 | private Context mContext;
25 | private long mLastSessionId = 0;
26 |
27 | private ConcurrentHashMap mCreatedSessions = new ConcurrentHashMap<>();
28 |
29 | private ConcurrentSkipListMap mSessionStates = new ConcurrentSkipListMap<>();
30 |
31 | private Set mObservers = Collections.newSetFromMap(new ConcurrentHashMap<>());
32 |
33 | protected BaseSaiPackageInstaller(Context c) {
34 | mContext = c.getApplicationContext();
35 | }
36 |
37 | @Override
38 | public String createSession(SaiPiSessionParams params) {
39 | String sessionId = newSessionId();
40 | mCreatedSessions.put(sessionId, params);
41 | setSessionState(sessionId, new SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.CREATED).build());
42 | return sessionId;
43 | }
44 |
45 | @Override
46 | public void registerSessionObserver(SaiPiSessionObserver observer) {
47 | mObservers.add(observer);
48 | }
49 |
50 | @Override
51 | public void unregisterSessionObserver(SaiPiSessionObserver observer) {
52 | mObservers.remove(observer);
53 | }
54 |
55 | @Override
56 | public List getSessions() {
57 | return Collections.unmodifiableList(new ArrayList<>(mSessionStates.values()));
58 | }
59 |
60 | protected void setSessionState(String sessionId, SaiPiSessionState state) {
61 | Log.d(tag(), String.format("%s->setSessionState(%s, %s)", getClass().getSimpleName(), sessionId, state));
62 | mSessionStates.put(sessionId, state);
63 | Utils.onMainThread(() -> {
64 | for (SaiPiSessionObserver observer : mObservers)
65 | observer.onSessionStateChanged(state);
66 | });
67 | }
68 |
69 | protected SaiPiSessionParams takeCreatedSession(String sessionId) {
70 | return mCreatedSessions.remove(sessionId);
71 | }
72 |
73 | @SuppressLint("DefaultLocale")
74 | protected String newSessionId() {
75 | long sessionId = mLastSessionId++;
76 | return String.format("%d@%s", sessionId, getClass().getName());
77 | }
78 |
79 | protected Context getContext() {
80 | return mContext;
81 | }
82 |
83 | protected abstract String tag();
84 | }
85 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer2/impl/FlexSaiPackageInstaller.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.installer2.impl;
2 |
3 | import android.content.Context;
4 |
5 | import com.aefyr.sai.installer2.base.SaiPackageInstaller;
6 | import com.aefyr.sai.installer2.base.SaiPiSessionObserver;
7 | import com.aefyr.sai.installer2.base.model.SaiPiSessionParams;
8 | import com.aefyr.sai.installer2.base.model.SaiPiSessionState;
9 | import com.aefyr.sai.installer2.impl.shell.ShizukuSaiPackageInstaller;
10 | import com.aefyr.sai.utils.PreferencesValues;
11 |
12 | import java.util.ArrayList;
13 | import java.util.Collections;
14 | import java.util.HashMap;
15 | import java.util.List;
16 | import java.util.Objects;
17 | import java.util.Set;
18 | import java.util.concurrent.ConcurrentHashMap;
19 |
20 | public class FlexSaiPackageInstaller implements SaiPackageInstaller, SaiPiSessionObserver {
21 |
22 | private static FlexSaiPackageInstaller sInstance;
23 |
24 | private Context mContext;
25 |
26 | private SaiPackageInstaller mDefaultInstaller;
27 | private HashMap mInstallers = new HashMap<>();
28 | private ConcurrentHashMap mSessionIdToInstaller = new ConcurrentHashMap<>();
29 |
30 | private Set mObservers = Collections.newSetFromMap(new ConcurrentHashMap<>());
31 |
32 | public static FlexSaiPackageInstaller getInstance(Context c) {
33 | synchronized (FlexSaiPackageInstaller.class) {
34 | return sInstance != null ? sInstance : new FlexSaiPackageInstaller(c);
35 | }
36 | }
37 |
38 | private FlexSaiPackageInstaller(Context c) {
39 | mContext = c.getApplicationContext();
40 | // addInstaller(PreferencesValues.INSTALLER_ROOTLESS, RootlessSaiPackageInstaller.getInstance(mContext));
41 | // addInstaller(PreferencesValues.INSTALLER_ROOTED, RootedSaiPackageInstaller.getInstance(mContext));
42 | addInstaller(PreferencesValues.INSTALLER_SHIZUKU, ShizukuSaiPackageInstaller.getInstance(mContext));
43 | sInstance = this;
44 | }
45 |
46 | public void addInstaller(int id, SaiPackageInstaller installer) {
47 | if (mInstallers.containsKey(id))
48 | throw new IllegalStateException("Installer with this id already added");
49 |
50 | if (mDefaultInstaller == null)
51 | mDefaultInstaller = installer;
52 |
53 | mInstallers.put(id, installer);
54 | installer.registerSessionObserver(this);
55 | }
56 |
57 | public String createSessionOnInstaller(int installerId, SaiPiSessionParams params) {
58 | return createSessionOnInstaller(Objects.requireNonNull(mInstallers.get(installerId)), params);
59 | }
60 |
61 | private String createSessionOnInstaller(SaiPackageInstaller installer, SaiPiSessionParams params) {
62 | String sessionId = installer.createSession(params);
63 | mSessionIdToInstaller.put(sessionId, installer);
64 | return sessionId;
65 | }
66 |
67 | @Override
68 | public String createSession(SaiPiSessionParams params) {
69 | return createSessionOnInstaller(mDefaultInstaller, params);
70 | }
71 |
72 | @Override
73 | public void enqueueSession(String sessionId) {
74 | SaiPackageInstaller installer = mSessionIdToInstaller.remove(sessionId);
75 | if (installer == null)
76 | throw new IllegalArgumentException("Unknown sessionId");
77 |
78 | installer.enqueueSession(sessionId);
79 | }
80 |
81 | @Override
82 | public void registerSessionObserver(SaiPiSessionObserver observer) {
83 | mObservers.add(observer);
84 | }
85 |
86 | @Override
87 | public void unregisterSessionObserver(SaiPiSessionObserver observer) {
88 | mObservers.remove(observer);
89 | }
90 |
91 | @Override
92 | public List getSessions() {
93 | ArrayList sessions = new ArrayList<>();
94 |
95 | for (SaiPackageInstaller installer : mInstallers.values())
96 | sessions.addAll(installer.getSessions());
97 |
98 | Collections.sort(sessions);
99 |
100 | return sessions;
101 | }
102 |
103 | @Override
104 | public void onSessionStateChanged(SaiPiSessionState state) {
105 | for (SaiPiSessionObserver observer : mObservers)
106 | observer.onSessionStateChanged(state);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer2/impl/rootless/ConfirmationIntentWrapperActivity2.java:
--------------------------------------------------------------------------------
1 | /*package com.aefyr.sai.installer2.impl.rootless;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.content.pm.PackageInstaller;
6 | import android.os.Bundle;
7 |
8 | import androidx.annotation.Nullable;
9 | import androidx.appcompat.app.AppCompatActivity;
10 |
11 | import com.aefyr.sai.utils.Logs;
12 |
13 | public class ConfirmationIntentWrapperActivity2 extends AppCompatActivity {
14 |
15 | private static final String EXTRA_CONFIRMATION_INTENT = "confirmation_intent";
16 | public static final String EXTRA_SESSION_ID = "session_id";
17 |
18 | private static final int REQUEST_CODE_CONFIRM_INSTALLATION = 322;
19 |
20 | private boolean mFinishedProperly = false;
21 |
22 | private int mSessionId;
23 | private Intent mConfirmationIntent;
24 |
25 | @Override
26 | protected void onCreate(@Nullable Bundle savedInstanceState) {
27 | super.onCreate(savedInstanceState);
28 |
29 | Intent intent = getIntent();
30 |
31 | mSessionId = intent.getIntExtra(EXTRA_SESSION_ID, -1);
32 | mConfirmationIntent = intent.getParcelableExtra(EXTRA_CONFIRMATION_INTENT);
33 |
34 | if (savedInstanceState == null) {
35 | try {
36 | startActivityForResult(mConfirmationIntent, REQUEST_CODE_CONFIRM_INSTALLATION);
37 | } catch (Exception e) {
38 | Logs.logException(e);
39 | sendErrorBroadcast(mSessionId, RootlessSaiPiBroadcastReceiver.STATUS_BAD_ROM);
40 | finish();
41 | }
42 | }
43 | }
44 |
45 | @Override
46 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
47 | super.onActivityResult(requestCode, resultCode, data);
48 |
49 | if (requestCode == REQUEST_CODE_CONFIRM_INSTALLATION) {
50 | mFinishedProperly = true;
51 | finish();
52 | }
53 | }
54 |
55 | @Override
56 | protected void onDestroy() {
57 | super.onDestroy();
58 |
59 | if (isFinishing() && !mFinishedProperly)
60 | start(this, mSessionId, mConfirmationIntent); //Because if user doesn't confirm/cancel the installation, PackageInstaller session will hang
61 |
62 | }
63 |
64 | public static void start(Context c, int sessionId, Intent confirmationIntent) {
65 | Intent intent = new Intent(c, ConfirmationIntentWrapperActivity2.class);
66 | intent.putExtra(EXTRA_CONFIRMATION_INTENT, confirmationIntent);
67 | intent.putExtra(EXTRA_SESSION_ID, sessionId);
68 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
69 |
70 | c.startActivity(intent);
71 | }
72 |
73 | private void sendErrorBroadcast(int sessionID, int status) {
74 | Intent statusIntent = new Intent(RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT);
75 | statusIntent.putExtra(PackageInstaller.EXTRA_STATUS, status);
76 | statusIntent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionID);
77 |
78 | sendBroadcast(statusIntent);
79 | }
80 |
81 | }
82 | */
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer2/impl/rootless/RootlessSaiPackageInstaller.java:
--------------------------------------------------------------------------------
1 | /*package com.aefyr.sai.installer2.impl.rootless;
2 |
3 | import android.app.PendingIntent;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.content.IntentFilter;
7 | import android.content.pm.PackageInstaller;
8 | import android.content.pm.PackageManager;
9 | import android.os.Build;
10 | import android.os.Handler;
11 | import android.os.HandlerThread;
12 | import android.util.Log;
13 |
14 | import androidx.annotation.Nullable;
15 | import androidx.annotation.RequiresApi;
16 |
17 | import com.aefyr.sai.installer2.base.model.SaiPiSessionParams;
18 | import com.aefyr.sai.installer2.base.model.SaiPiSessionState;
19 | import com.aefyr.sai.installer2.base.model.SaiPiSessionStatus;
20 | import com.aefyr.sai.installer2.impl.BaseSaiPackageInstaller;
21 | import com.aefyr.sai.model.apksource.ApkSource;
22 | import com.aefyr.sai.utils.IOUtils;
23 | import com.aefyr.sai.utils.PreferencesHelper;
24 | import com.aefyr.sai.utils.Utils;
25 |
26 | import java.io.InputStream;
27 | import java.io.OutputStream;
28 | import java.util.concurrent.ConcurrentHashMap;
29 | import java.util.concurrent.ExecutorService;
30 | import java.util.concurrent.Executors;
31 |
32 | public class RootlessSaiPackageInstaller extends BaseSaiPackageInstaller implements RootlessSaiPiBroadcastReceiver.EventObserver {
33 | private static final String TAG = "RootlessSaiPi";
34 |
35 | private static RootlessSaiPackageInstaller sInstance;
36 |
37 | private PackageInstaller mPackageInstaller;
38 | private ExecutorService mExecutor = Executors.newFixedThreadPool(4);
39 | private final HandlerThread mWorkerThread = new HandlerThread("RootlessSaiPi Worker");
40 | private final Handler mWorkerHandler;
41 |
42 | private ConcurrentHashMap mAndroidPiSessionIdToSaiPiSessionId = new ConcurrentHashMap<>();
43 | private ConcurrentHashMap mSessionIdToAppTempName = new ConcurrentHashMap<>();
44 |
45 | private final RootlessSaiPiBroadcastReceiver mBroadcastReceiver;
46 |
47 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
48 | public static RootlessSaiPackageInstaller getInstance(Context c) {
49 | synchronized (RootlessSaiPackageInstaller.class) {
50 | return sInstance != null ? sInstance : new RootlessSaiPackageInstaller(c);
51 | }
52 | }
53 |
54 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
55 | private RootlessSaiPackageInstaller(Context c) {
56 | super(c);
57 | mPackageInstaller = getContext().getPackageManager().getPackageInstaller();
58 |
59 | mWorkerThread.start();
60 | mWorkerHandler = new Handler(mWorkerThread.getLooper());
61 |
62 | mBroadcastReceiver = new RootlessSaiPiBroadcastReceiver(getContext());
63 | mBroadcastReceiver.addEventObserver(this);
64 | getContext().registerReceiver(mBroadcastReceiver, new IntentFilter(RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT), null, mWorkerHandler);
65 |
66 | sInstance = this;
67 | }
68 |
69 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
70 | @Override
71 | public void enqueueSession(String sessionId) {
72 | SaiPiSessionParams params = takeCreatedSession(sessionId);
73 | setSessionState(sessionId, new SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.QUEUED).appTempName(params.apkSource().getAppName()).build());
74 | mExecutor.submit(() -> install(sessionId, params));
75 | }
76 |
77 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
78 | private void install(String sessionId, SaiPiSessionParams params) {
79 | PackageInstaller.Session session = null;
80 | String appTempName = null;
81 | try (ApkSource apkSource = params.apkSource()) {
82 | appTempName = apkSource.getAppName();
83 | if (appTempName != null)
84 | mSessionIdToAppTempName.put(sessionId, appTempName);
85 |
86 | setSessionState(sessionId, new SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLING).appTempName(appTempName).build());
87 |
88 | PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
89 | sessionParams.setInstallLocation(PreferencesHelper.getInstance(getContext()).getInstallLocation());
90 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
91 | sessionParams.setInstallReason(PackageManager.INSTALL_REASON_USER);
92 |
93 | int androidSessionId = mPackageInstaller.createSession(sessionParams);
94 | mAndroidPiSessionIdToSaiPiSessionId.put(androidSessionId, sessionId);
95 |
96 | session = mPackageInstaller.openSession(androidSessionId);
97 | int currentApkFile = 0;
98 | while (apkSource.nextApk()) {
99 | try (InputStream inputStream = apkSource.openApkInputStream(); OutputStream outputStream = session.openWrite(String.format("%d.apk", currentApkFile++), 0, apkSource.getApkLength())) {
100 | IOUtils.copyStream(inputStream, outputStream);
101 | session.fsync(outputStream);
102 | }
103 | }
104 |
105 | Intent callbackIntent = new Intent(RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT);
106 | PendingIntent pendingIntent = PendingIntent.getBroadcast(getContext(), 0, callbackIntent, 0);
107 | session.commit(pendingIntent.getIntentSender());
108 | } catch (Exception e) {
109 | Log.w(TAG, e);
110 | if (session != null)
111 | session.abandon();
112 |
113 | setSessionState(sessionId, new SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).appTempName(appTempName).error(e.getLocalizedMessage(), Utils.throwableToString(e)).build());
114 | } finally {
115 | if (session != null)
116 | session.close();
117 | }
118 | }
119 |
120 | @Override
121 | public void onInstallationSucceeded(int androidSessionId, String packageName) {
122 | String sessionId = mAndroidPiSessionIdToSaiPiSessionId.get(androidSessionId);
123 | if (sessionId == null)
124 | return;
125 |
126 | setSessionState(sessionId, new SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_SUCCEED).packageName(packageName).resolvePackageMeta(getContext()).build());
127 | }
128 |
129 | @Override
130 | public void onInstallationFailed(int androidSessionId, String shortError, @Nullable String fullError, @Nullable Exception exception) {
131 | String sessionId = mAndroidPiSessionIdToSaiPiSessionId.get(androidSessionId);
132 | if (sessionId == null)
133 | return;
134 |
135 | setSessionState(sessionId, new SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED)
136 | .appTempName(mSessionIdToAppTempName.remove(sessionId))
137 | .error(shortError, fullError)
138 | .build());
139 |
140 | }
141 |
142 | @Override
143 | protected String tag() {
144 | return TAG;
145 | }
146 | }
147 | */
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer2/impl/shell/RootedSaiPackageInstaller.java:
--------------------------------------------------------------------------------
1 | /*package com.aefyr.sai.installer2.impl.shell;
2 |
3 | import android.content.Context;
4 |
5 | import com.aefyr.sai.R;
6 | import com.aefyr.sai.shell.Shell;
7 | import com.aefyr.sai.shell.SuShell;
8 |
9 | public class RootedSaiPackageInstaller extends ShellSaiPackageInstaller {
10 |
11 | private static RootedSaiPackageInstaller sInstance;
12 |
13 | public static RootedSaiPackageInstaller getInstance(Context c) {
14 | synchronized (RootedSaiPackageInstaller.class) {
15 | return sInstance != null ? sInstance : new RootedSaiPackageInstaller(c);
16 | }
17 | }
18 |
19 | private RootedSaiPackageInstaller(Context c) {
20 | super(c);
21 | sInstance = this;
22 | }
23 |
24 | @Override
25 | protected Shell getShell() {
26 | return SuShell.getInstance();
27 | }
28 |
29 | @Override
30 | protected String getInstallerName() {
31 | return "Rooted";
32 | }
33 |
34 | @Override
35 | protected String getShellUnavailableMessage() {
36 | return getContext().getString(R.string.installer_error_root_no_root);
37 | }
38 |
39 | @Override
40 | protected String tag() {
41 | return "RootedSaiPi";
42 | }
43 | }*/
44 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/installer2/impl/shell/ShizukuSaiPackageInstaller.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.installer2.impl.shell;
2 |
3 | import android.content.Context;
4 |
5 | import com.aefyr.sai.shell.Shell;
6 | import com.aefyr.sai.shell.ShizukuShell;
7 |
8 | public class ShizukuSaiPackageInstaller extends ShellSaiPackageInstaller {
9 |
10 | private static ShizukuSaiPackageInstaller sInstance;
11 |
12 | public static ShizukuSaiPackageInstaller getInstance(Context c) {
13 | synchronized (ShizukuSaiPackageInstaller.class) {
14 | return sInstance != null ? sInstance : new ShizukuSaiPackageInstaller(c);
15 | }
16 | }
17 |
18 | private ShizukuSaiPackageInstaller(Context c) {
19 | super(c);
20 | sInstance = this;
21 | }
22 |
23 | @Override
24 | protected Shell getShell() {
25 | return ShizukuShell.getInstance();
26 | }
27 |
28 | @Override
29 | protected String getInstallerName() {
30 | return "Shizuku";
31 | }
32 |
33 | @Override
34 | protected String getShellUnavailableMessage() {
35 | return "installer_error_shizuku_unavailable";
36 | }
37 |
38 | @Override
39 | protected String tag() {
40 | return "ShizukuSaiPi";
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/legal/LegalStuffProvider.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.legal;
2 |
3 | public interface LegalStuffProvider {
4 |
5 | boolean hasPrivacyPolicy();
6 |
7 | String getPrivacyPolicyUrl();
8 |
9 | boolean hasEula();
10 |
11 | String getEulaUrl();
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/apksource/ApkSource.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.model.apksource;
2 |
3 | import androidx.annotation.Nullable;
4 |
5 | import java.io.InputStream;
6 |
7 | public interface ApkSource extends AutoCloseable {
8 |
9 | boolean nextApk() throws Exception;
10 |
11 | InputStream openApkInputStream() throws Exception;
12 |
13 | long getApkLength() throws Exception;
14 |
15 | String getApkName() throws Exception;
16 |
17 | String getApkLocalPath() throws Exception;
18 |
19 | @Override
20 | default void close() throws Exception {
21 |
22 | }
23 |
24 | /**
25 | * Returns the name of the app this ApkSource will install or null if unknown
26 | *
27 | * @return name of the app this ApkSource will install or null if unknown
28 | */
29 | @Nullable
30 | default String getAppName() {
31 | return null;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/apksource/CopyToFileApkSource.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.model.apksource;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.annotation.Nullable;
6 |
7 | import com.aefyr.sai.utils.IOUtils;
8 |
9 | import java.io.File;
10 | import java.io.FileInputStream;
11 | import java.io.FileOutputStream;
12 | import java.io.InputStream;
13 | import java.io.OutputStream;
14 |
15 | /**
16 | * An ApkSource implementation that copies APK files from the wrapped ApkSource to a temp file. Used to fix unknown APK sizes when necessary
17 | */
18 | public class CopyToFileApkSource implements ApkSource {
19 |
20 | private Context mContext;
21 | private ApkSource mWrappedApkSource;
22 |
23 | private File mTempDir;
24 | private File mCurrentApkFile;
25 |
26 | public CopyToFileApkSource(Context context, ApkSource wrappedApkSource) {
27 | mContext = context.getApplicationContext();
28 | mWrappedApkSource = wrappedApkSource;
29 | }
30 |
31 | @Override
32 | public boolean nextApk() throws Exception {
33 | if (!mWrappedApkSource.nextApk())
34 | return false;
35 |
36 | if (mTempDir == null)
37 | mTempDir = createTempDir();
38 |
39 | if (mCurrentApkFile != null)
40 | IOUtils.deleteRecursively(mCurrentApkFile);
41 |
42 |
43 | mCurrentApkFile = new File(mTempDir, mWrappedApkSource.getApkName());
44 |
45 | try (InputStream in = mWrappedApkSource.openApkInputStream(); OutputStream out = new FileOutputStream(mCurrentApkFile)) {
46 | IOUtils.copyStream(in, out);
47 | }
48 |
49 | return true;
50 | }
51 |
52 | @Override
53 | public InputStream openApkInputStream() throws Exception {
54 | return new FileInputStream(mCurrentApkFile);
55 | }
56 |
57 | @Override
58 | public long getApkLength() {
59 | return mCurrentApkFile.length();
60 | }
61 |
62 | @Override
63 | public String getApkName() throws Exception {
64 | return mWrappedApkSource.getApkName();
65 | }
66 |
67 | @Override
68 | public String getApkLocalPath() throws Exception {
69 | return mWrappedApkSource.getApkLocalPath();
70 | }
71 |
72 | @Override
73 | public void close() throws Exception {
74 | Exception suppressedException = null;
75 | try {
76 | mWrappedApkSource.close();
77 | } catch (Exception e) {
78 | suppressedException = e;
79 | }
80 |
81 | if (mTempDir != null) {
82 | IOUtils.deleteRecursively(mTempDir);
83 | }
84 |
85 | if (suppressedException != null)
86 | throw suppressedException;
87 | }
88 |
89 | @Nullable
90 | @Override
91 | public String getAppName() {
92 | return mWrappedApkSource.getAppName();
93 | }
94 |
95 | private File createTempDir() {
96 | File tempDir = new File(mContext.getFilesDir(), "CopyToFileApkSource");
97 | tempDir = new File(tempDir, String.valueOf(System.currentTimeMillis()));
98 | tempDir.mkdirs();
99 | return tempDir;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/apksource/DefaultApkSource.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.model.apksource;
2 |
3 | import android.util.Log;
4 |
5 | import androidx.annotation.Nullable;
6 |
7 | import com.aefyr.sai.model.filedescriptor.FileDescriptor;
8 |
9 | import java.io.InputStream;
10 | import java.util.List;
11 |
12 | public class DefaultApkSource implements ApkSource {
13 |
14 | private List mApkFileDescriptors;
15 | private FileDescriptor mCurrentApk;
16 |
17 | public DefaultApkSource(List apkFileDescriptors) {
18 | mApkFileDescriptors = apkFileDescriptors;
19 | }
20 |
21 | @Override
22 | public boolean nextApk() {
23 | if (mApkFileDescriptors.size() == 0)
24 | return false;
25 |
26 | mCurrentApk = mApkFileDescriptors.remove(0);
27 | return true;
28 | }
29 |
30 | @Override
31 | public InputStream openApkInputStream() throws Exception {
32 | return mCurrentApk.open();
33 | }
34 |
35 | @Override
36 | public long getApkLength() throws Exception {
37 | return mCurrentApk.length();
38 | }
39 |
40 | @Override
41 | public String getApkName() throws Exception {
42 | return mCurrentApk.name();
43 | }
44 |
45 | @Override
46 | public String getApkLocalPath() throws Exception {
47 | return mCurrentApk.name();
48 | }
49 |
50 | @Nullable
51 | @Override
52 | public String getAppName() {
53 | try {
54 | return mApkFileDescriptors.size() == 1 ? mApkFileDescriptors.get(0).name() : null;
55 | } catch (Exception e) {
56 | Log.w("DefaultApkSource", "Unable to get app name", e);
57 | return null;
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/apksource/FilterApkSource.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.model.apksource;
2 |
3 | import androidx.annotation.Nullable;
4 |
5 | import java.io.InputStream;
6 | import java.util.Set;
7 |
8 | /**
9 | * An ApkSource that can filter out APK files from the backing ZipBackedApkSource
10 | */
11 | public class FilterApkSource implements ApkSource {
12 |
13 | private ApkSource mWrappedApkSource;
14 | private Set mFilteredEntries;
15 | private boolean mBlacklist;
16 |
17 | public FilterApkSource(ApkSource apkSource, Set filteredEntries, boolean blacklist) {
18 | mWrappedApkSource = apkSource;
19 | mFilteredEntries = filteredEntries;
20 | mBlacklist = blacklist;
21 | }
22 |
23 | @Override
24 | public boolean nextApk() throws Exception {
25 | if (!mWrappedApkSource.nextApk())
26 | return false;
27 |
28 | while (shouldSkip(getApkLocalPath())) {
29 | if (!mWrappedApkSource.nextApk())
30 | return false;
31 | }
32 |
33 | return true;
34 | }
35 |
36 | private boolean shouldSkip(String localPath) {
37 | if (mBlacklist)
38 | return mFilteredEntries.contains(localPath);
39 | else
40 | return !mFilteredEntries.contains(localPath);
41 | }
42 |
43 | @Override
44 | public InputStream openApkInputStream() throws Exception {
45 | return mWrappedApkSource.openApkInputStream();
46 | }
47 |
48 | @Override
49 | public long getApkLength() throws Exception {
50 | return mWrappedApkSource.getApkLength();
51 | }
52 |
53 | @Override
54 | public String getApkName() throws Exception {
55 | return mWrappedApkSource.getApkName();
56 | }
57 |
58 | @Override
59 | public String getApkLocalPath() throws Exception {
60 | return mWrappedApkSource.getApkLocalPath();
61 | }
62 |
63 | @Override
64 | public void close() throws Exception {
65 | mWrappedApkSource.close();
66 | }
67 |
68 | @Nullable
69 | @Override
70 | public String getAppName() {
71 | return mWrappedApkSource.getAppName();
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/apksource/SignerApkSource.java:
--------------------------------------------------------------------------------
1 | /* package com.aefyr.sai.model.apksource;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 |
6 | import androidx.annotation.Nullable;
7 |
8 | import com.aefyr.pseudoapksigner.PseudoApkSigner;
9 | import com.aefyr.sai.utils.IOUtils;
10 |
11 | import java.io.File;
12 | import java.io.FileInputStream;
13 | import java.io.FileOutputStream;
14 | import java.io.InputStream;
15 |
16 | public class SignerApkSource implements ApkSource {
17 | private static final String TAG = "SignerApkSource";
18 | private static final String FILE_NAME_PAST = "testkey.past";
19 | private static final String FILE_NAME_PRIVATE_KEY = "testkey.pk8";
20 |
21 | private ApkSource mWrappedApkSource;
22 | private Context mContext;
23 | private boolean mIsPrepared;
24 | private PseudoApkSigner mApkSigner;
25 | private File mTempDir;
26 |
27 | private File mCurrentSignedApkFile;
28 |
29 | public SignerApkSource(Context c, ApkSource apkSource) {
30 | mContext = c;
31 | mWrappedApkSource = apkSource;
32 | }
33 |
34 | @Override
35 | public boolean nextApk() throws Exception {
36 | if (!mWrappedApkSource.nextApk()) {
37 | return false;
38 | }
39 |
40 | if (!mIsPrepared) {
41 | checkAndPrepareSigningEnvironment();
42 | createTempDir();
43 | mApkSigner = new PseudoApkSigner(new File(getSigningEnvironmentDir(), FILE_NAME_PAST), new File(getSigningEnvironmentDir(), FILE_NAME_PRIVATE_KEY));
44 | }
45 |
46 | mCurrentSignedApkFile = new File(mTempDir, getApkName());
47 | mApkSigner.sign(mWrappedApkSource.openApkInputStream(), new FileOutputStream(mCurrentSignedApkFile));
48 |
49 | return true;
50 | }
51 |
52 | @Override
53 | public InputStream openApkInputStream() throws Exception {
54 | return new FileInputStream(mCurrentSignedApkFile);
55 | }
56 |
57 | @Override
58 | public long getApkLength() {
59 | return mCurrentSignedApkFile.length();
60 | }
61 |
62 | @Override
63 | public String getApkName() throws Exception {
64 | return mWrappedApkSource.getApkName();
65 | }
66 |
67 | @Override
68 | public String getApkLocalPath() throws Exception {
69 | return mWrappedApkSource.getApkLocalPath();
70 | }
71 |
72 | @Override
73 | public void close() throws Exception {
74 | if (mTempDir != null)
75 | IOUtils.deleteRecursively(mTempDir);
76 |
77 | mWrappedApkSource.close();
78 | }
79 |
80 | @Nullable
81 | @Override
82 | public String getAppName() {
83 | return mWrappedApkSource.getAppName();
84 | }
85 |
86 | private void checkAndPrepareSigningEnvironment() throws Exception {
87 | File signingEnvironment = getSigningEnvironmentDir();
88 | File pastFile = new File(signingEnvironment, FILE_NAME_PAST);
89 | File privateKeyFile = new File(signingEnvironment, FILE_NAME_PRIVATE_KEY);
90 |
91 | if (pastFile.exists() && privateKeyFile.exists()) {
92 | mIsPrepared = true;
93 | return;
94 | }
95 |
96 | Log.d(TAG, "Preparing signing environment...");
97 | signingEnvironment.mkdir();
98 |
99 | IOUtils.copyFileFromAssets(mContext, FILE_NAME_PAST, pastFile);
100 | IOUtils.copyFileFromAssets(mContext, FILE_NAME_PRIVATE_KEY, privateKeyFile);
101 |
102 | mIsPrepared = true;
103 | }
104 |
105 | private File getSigningEnvironmentDir() {
106 | return new File(mContext.getFilesDir(), "signing");
107 | }
108 |
109 | private void createTempDir() {
110 | mTempDir = new File(mContext.getFilesDir(), String.valueOf(System.currentTimeMillis()));
111 | mTempDir.mkdirs();
112 | }
113 | }
114 | */
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/apksource/ZipApkSource.java:
--------------------------------------------------------------------------------
1 | /* package com.aefyr.sai.model.apksource;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 |
6 | import androidx.annotation.Nullable;
7 |
8 | import com.aefyr.sai.R;
9 | import com.aefyr.sai.model.filedescriptor.FileDescriptor;
10 | import com.aefyr.sai.utils.Utils;
11 |
12 | import java.io.IOException;
13 | import java.io.InputStream;
14 | import java.util.zip.ZipEntry;
15 | import java.util.zip.ZipException;
16 | import java.util.zip.ZipInputStream;
17 |
18 | public class ZipApkSource implements ZipBackedApkSource {
19 |
20 | private Context mContext;
21 | private FileDescriptor mZipFileDescriptor;
22 | private boolean mIsOpen;
23 | private int mSeenApkFiles = 0;
24 |
25 | private ZipInputStream mZipInputStream;
26 | private ZipEntry mCurrentZipEntry;
27 |
28 | private ZipInputStreamWrapper mWrappedStream;
29 |
30 | public ZipApkSource(Context c, FileDescriptor zipFileDescriptor) {
31 | mContext = c;
32 | mZipFileDescriptor = zipFileDescriptor;
33 | }
34 |
35 | @Override
36 | public boolean nextApk() throws Exception {
37 | if (!mIsOpen) {
38 | mZipInputStream = new ZipInputStream(mZipFileDescriptor.open());
39 | mWrappedStream = new ZipInputStreamWrapper(mZipInputStream);
40 | mIsOpen = true;
41 | }
42 |
43 | do {
44 | try {
45 | mCurrentZipEntry = mZipInputStream.getNextEntry();
46 | } catch (ZipException e) {
47 | if (e.getMessage().equals("only DEFLATED entries can have EXT descriptor")) {
48 | throw new ZipException(mContext.getString(R.string.installer_recoverable_error_use_zipfile));
49 | }
50 | throw e;
51 | }
52 | } while (mCurrentZipEntry != null && (mCurrentZipEntry.isDirectory() || !mCurrentZipEntry.getName().toLowerCase().endsWith(".apk")));
53 |
54 | if (mCurrentZipEntry == null) {
55 | mZipInputStream.close();
56 |
57 | if (mSeenApkFiles == 0)
58 | throw new IllegalArgumentException(mContext.getString(R.string.installer_error_zip_contains_no_apks));
59 |
60 | return false;
61 | }
62 | mSeenApkFiles++;
63 |
64 | return true;
65 | }
66 |
67 | @Override
68 | public InputStream openApkInputStream() {
69 | return mWrappedStream;
70 | }
71 |
72 | @Override
73 | public long getApkLength() {
74 | return mCurrentZipEntry.getSize();
75 | }
76 |
77 | @Override
78 | public String getApkName() {
79 | return Utils.getFileNameFromZipEntry(mCurrentZipEntry);
80 | }
81 |
82 | @Override
83 | public String getApkLocalPath() throws Exception {
84 | return mCurrentZipEntry.getName();
85 | }
86 |
87 | @Override
88 | public void close() throws Exception {
89 | if (mZipInputStream != null)
90 | mZipInputStream.close();
91 | }
92 |
93 | @Nullable
94 | @Override
95 | public String getAppName() {
96 | try {
97 | return mZipFileDescriptor.name();
98 | } catch (Exception e) {
99 | Log.w("ZipApkSource", "Unable to get app name", e);
100 | return null;
101 | }
102 | }
103 |
104 | @Override
105 | public ZipEntry getEntry() {
106 | return mCurrentZipEntry;
107 | }
108 |
109 |
110 | private static class ZipInputStreamWrapper extends InputStream {
111 |
112 | private ZipInputStream mWrappedStream;
113 |
114 | private ZipInputStreamWrapper(ZipInputStream inputStream) {
115 | mWrappedStream = inputStream;
116 | }
117 |
118 | @Override
119 | public int available() throws IOException {
120 | return mWrappedStream.available();
121 | }
122 |
123 | @Override
124 | public int read() throws IOException {
125 | return mWrappedStream.read();
126 | }
127 |
128 | @Override
129 | public int read(byte[] b) throws IOException {
130 | return mWrappedStream.read(b);
131 | }
132 |
133 | @Override
134 | public int read(byte[] b, int off, int len) throws IOException {
135 | return mWrappedStream.read(b, off, len);
136 | }
137 |
138 | @Override
139 | public void close() throws IOException {
140 | mWrappedStream.closeEntry();
141 | }
142 | }
143 | }
144 | */
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/apksource/ZipBackedApkSource.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.model.apksource;
2 |
3 | import java.util.zip.ZipEntry;
4 |
5 | /**
6 | * An ApkSource backed by a zip archive
7 | */
8 | public interface ZipBackedApkSource extends ApkSource {
9 |
10 | /**
11 | * @return ZipEntry for the current APK
12 | */
13 | ZipEntry getEntry();
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/apksource/ZipExtractorApkSource.java:
--------------------------------------------------------------------------------
1 | /* package com.aefyr.sai.model.apksource;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 |
6 | import androidx.annotation.Nullable;
7 |
8 | import com.aefyr.sai.R;
9 | import com.aefyr.sai.model.filedescriptor.FileDescriptor;
10 | import com.aefyr.sai.utils.IOUtils;
11 | import com.aefyr.sai.utils.Utils;
12 |
13 | import java.io.File;
14 | import java.io.FileInputStream;
15 | import java.io.FileOutputStream;
16 | import java.io.InputStream;
17 | import java.util.zip.ZipEntry;
18 | import java.util.zip.ZipInputStream;
19 |
20 |
21 |
22 | @Deprecated
23 | public class ZipExtractorApkSource implements ApkSource {
24 | private Context mContext;
25 | private FileDescriptor mZipFileDescriptor;
26 | private boolean mIsOpen;
27 | private int mSeenApkFiles = 0;
28 |
29 | private ZipInputStream mZipInputStream;
30 | private ZipEntry mCurrentZipEntry;
31 | private File mExtractedFilesDir;
32 | private File mCurrentExtractedZipEntryFile;
33 |
34 | public ZipExtractorApkSource(Context c, FileDescriptor zipFileDescriptor) {
35 | mContext = c;
36 | mZipFileDescriptor = zipFileDescriptor;
37 |
38 | File extractedApksDir = new File(c.getFilesDir(), "extractedApks");
39 | extractedApksDir.mkdirs();
40 | mExtractedFilesDir = new File(extractedApksDir, String.valueOf(System.currentTimeMillis()));
41 | mExtractedFilesDir.mkdirs();
42 | }
43 |
44 | @Override
45 | public boolean nextApk() throws Exception {
46 | if (!mIsOpen) {
47 | mZipInputStream = new ZipInputStream(mZipFileDescriptor.open());
48 | mIsOpen = true;
49 | }
50 |
51 | do {
52 | mCurrentZipEntry = mZipInputStream.getNextEntry();
53 | } while (mCurrentZipEntry != null && (mCurrentZipEntry.isDirectory() || !mCurrentZipEntry.getName().endsWith(".apk")));
54 |
55 | if (mCurrentZipEntry == null) {
56 | mZipInputStream.close();
57 |
58 | if (mSeenApkFiles == 0)
59 | throw new IllegalArgumentException(mContext.getString(R.string.installer_error_zip_contains_no_apks));
60 |
61 | return false;
62 | }
63 | mSeenApkFiles++;
64 |
65 | extractCurrentEntry();
66 |
67 | return true;
68 | }
69 |
70 | @Override
71 | public InputStream openApkInputStream() throws Exception {
72 | return new FileInputStream(mCurrentExtractedZipEntryFile);
73 | }
74 |
75 | @Override
76 | public long getApkLength() {
77 | return mCurrentExtractedZipEntryFile.length();
78 | }
79 |
80 | @Override
81 | public String getApkName() {
82 | return mCurrentExtractedZipEntryFile.getName();
83 | }
84 |
85 | @Override
86 | public String getApkLocalPath() throws Exception {
87 | return mCurrentZipEntry.getName();
88 | }
89 |
90 | @Override
91 | public void close() {
92 | IOUtils.deleteRecursively(mExtractedFilesDir);
93 | }
94 |
95 | @Nullable
96 | @Override
97 | public String getAppName() {
98 | try {
99 | return mZipFileDescriptor.name();
100 | } catch (Exception e) {
101 | Log.w("ZipExtractorApkSource", "Unable to get app name", e);
102 | return null;
103 | }
104 | }
105 |
106 | private void extractCurrentEntry() throws Exception {
107 | mCurrentExtractedZipEntryFile = new File(mExtractedFilesDir, Utils.getFileNameFromZipEntry(mCurrentZipEntry));
108 | try (FileOutputStream fileOutputStream = new FileOutputStream(mCurrentExtractedZipEntryFile)) {
109 | IOUtils.copyStream(mZipInputStream, fileOutputStream);
110 | }
111 | }
112 | }
113 | */
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/apksource/ZipFileApkSource.java:
--------------------------------------------------------------------------------
1 | /* package com.aefyr.sai.model.apksource;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.annotation.Nullable;
6 |
7 | import com.aefyr.sai.R;
8 | import com.aefyr.sai.model.filedescriptor.FileDescriptor;
9 | import com.aefyr.sai.utils.IOUtils;
10 | import com.aefyr.sai.utils.Utils;
11 |
12 | import java.io.File;
13 | import java.io.FileOutputStream;
14 | import java.io.InputStream;
15 | import java.io.OutputStream;
16 | import java.util.Enumeration;
17 | import java.util.zip.ZipEntry;
18 | import java.util.zip.ZipFile;
19 |
20 |
21 | public class ZipFileApkSource implements ZipBackedApkSource {
22 |
23 | private Context mContext;
24 | private FileDescriptor mZipFileDescriptor;
25 |
26 | private File mTempFile;
27 | private ZipFile mZipFile;
28 |
29 | private Enumeration extends ZipEntry> mZipEntries;
30 |
31 | private ZipEntry mCurrentEntry;
32 |
33 | private boolean mSeenApkFile;
34 |
35 | public ZipFileApkSource(Context context, FileDescriptor zipFileDescriptor) {
36 | mContext = context.getApplicationContext();
37 | mZipFileDescriptor = zipFileDescriptor;
38 | }
39 |
40 | @Override
41 | public boolean nextApk() throws Exception {
42 | if (mZipFile == null)
43 | copyAndOpenZip();
44 |
45 | mCurrentEntry = null;
46 | while (mCurrentEntry == null && mZipEntries.hasMoreElements()) {
47 | ZipEntry nextEntry = mZipEntries.nextElement();
48 | if (!nextEntry.isDirectory() && nextEntry.getName().toLowerCase().endsWith(".apk")) {
49 | mCurrentEntry = nextEntry;
50 | mSeenApkFile = true;
51 | }
52 | }
53 |
54 | if (mCurrentEntry == null) {
55 | if (!mSeenApkFile)
56 | throw new IllegalArgumentException(mContext.getString(R.string.installer_error_zip_contains_no_apks));
57 |
58 | return false;
59 | }
60 |
61 | return true;
62 | }
63 |
64 | private void copyAndOpenZip() throws Exception {
65 | mTempFile = createTempFile();
66 |
67 | try (InputStream in = mZipFileDescriptor.open(); OutputStream out = new FileOutputStream(mTempFile)) {
68 | IOUtils.copyStream(in, out);
69 | }
70 |
71 | mZipFile = new ZipFile(mTempFile);
72 | mZipEntries = mZipFile.entries();
73 | }
74 |
75 | @Override
76 | public InputStream openApkInputStream() throws Exception {
77 | return mZipFile.getInputStream(mCurrentEntry);
78 | }
79 |
80 | @Override
81 | public long getApkLength() {
82 | return mCurrentEntry.getSize();
83 | }
84 |
85 | @Override
86 | public String getApkName() {
87 | return Utils.getFileNameFromZipEntry(mCurrentEntry);
88 | }
89 |
90 | @Override
91 | public String getApkLocalPath() throws Exception {
92 | return mCurrentEntry.getName();
93 | }
94 |
95 | @Override
96 | public void close() throws Exception {
97 | if (mZipFile != null)
98 | mZipFile.close();
99 |
100 | if (mTempFile != null)
101 | IOUtils.deleteRecursively(mTempFile);
102 | }
103 |
104 | @Nullable
105 | @Override
106 | public String getAppName() {
107 | try {
108 | return mZipFileDescriptor.name();
109 | } catch (Exception e) {
110 | return null;
111 | }
112 | }
113 |
114 | private File createTempFile() {
115 | File tempFile = new File(mContext.getFilesDir(), "ZipFileApkSource");
116 | tempFile.mkdir();
117 | tempFile = new File(tempFile, System.currentTimeMillis() + ".zip");
118 | return tempFile;
119 | }
120 |
121 | @Override
122 | public ZipEntry getEntry() {
123 | return mCurrentEntry;
124 | }
125 | }
126 | */
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/common/AppFeature.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.model.common;
2 |
3 | public interface AppFeature {
4 |
5 | CharSequence toText();
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/common/PackageMeta.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.model.common;
2 |
3 | import android.content.ContentResolver;
4 | import android.content.Context;
5 | import android.content.pm.ApplicationInfo;
6 | import android.content.pm.PackageInfo;
7 | import android.content.pm.PackageManager;
8 | import android.net.Uri;
9 | import android.os.Build;
10 | import android.os.Parcel;
11 | import android.os.Parcelable;
12 |
13 | import androidx.annotation.Nullable;
14 |
15 | import com.aefyr.sai.utils.Utils;
16 |
17 | public class PackageMeta implements Parcelable {
18 |
19 | public String packageName;
20 | public String label;
21 | public boolean hasSplits;
22 | public boolean isSystemApp;
23 | public long versionCode;
24 | public String versionName;
25 | public Uri iconUri;
26 | public long installTime;
27 | public long updateTime;
28 |
29 | public PackageMeta(String packageName, String label) {
30 | this.packageName = packageName;
31 | this.label = label;
32 | }
33 |
34 | private PackageMeta(Parcel in) {
35 | packageName = in.readString();
36 | label = in.readString();
37 | hasSplits = in.readInt() == 1;
38 | isSystemApp = in.readInt() == 1;
39 | versionCode = in.readLong();
40 | versionName = in.readString();
41 | iconUri = in.readParcelable(Uri.class.getClassLoader());
42 | installTime = in.readLong();
43 | updateTime = in.readLong();
44 | }
45 |
46 | public static final Creator CREATOR = new Creator() {
47 | @Override
48 | public PackageMeta createFromParcel(Parcel in) {
49 | return new PackageMeta(in);
50 | }
51 |
52 | @Override
53 | public PackageMeta[] newArray(int size) {
54 | return new PackageMeta[size];
55 | }
56 | };
57 |
58 | @Override
59 | public int describeContents() {
60 | return 0;
61 | }
62 |
63 | @Override
64 | public void writeToParcel(Parcel dest, int flags) {
65 | dest.writeString(packageName);
66 | dest.writeString(label);
67 | dest.writeInt(hasSplits ? 1 : 0);
68 | dest.writeInt(isSystemApp ? 1 : 0);
69 | dest.writeLong(versionCode);
70 | dest.writeString(versionName);
71 | dest.writeParcelable(iconUri, 0);
72 | dest.writeLong(installTime);
73 | dest.writeLong(updateTime);
74 | }
75 |
76 | public static class Builder {
77 | private PackageMeta mPackageMeta;
78 |
79 | public Builder(String packageName) {
80 | mPackageMeta = new PackageMeta(packageName, "?");
81 | }
82 |
83 | public Builder setLabel(String label) {
84 | mPackageMeta.label = label;
85 | return this;
86 | }
87 |
88 | public Builder setHasSplits(boolean hasSplits) {
89 | mPackageMeta.hasSplits = hasSplits;
90 | return this;
91 | }
92 |
93 | public Builder setIsSystemApp(boolean isSystemApp) {
94 | mPackageMeta.isSystemApp = isSystemApp;
95 | return this;
96 | }
97 |
98 | public Builder setVersionCode(long versionCode) {
99 | mPackageMeta.versionCode = versionCode;
100 | return this;
101 | }
102 |
103 | public Builder setVersionName(String versionName) {
104 | mPackageMeta.versionName = versionName;
105 | return this;
106 | }
107 |
108 | public Builder setIcon(int iconResId) {
109 | if (iconResId == 0) {
110 | mPackageMeta.iconUri = null;
111 | return this;
112 | }
113 |
114 | mPackageMeta.iconUri = new Uri.Builder()
115 | .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
116 | .authority(mPackageMeta.packageName)
117 | .path(String.valueOf(iconResId))
118 | .build();
119 |
120 | return this;
121 | }
122 |
123 | public Builder setIconUri(Uri iconUri) {
124 | mPackageMeta.iconUri = iconUri;
125 | return this;
126 | }
127 |
128 |
129 | public Builder setInstallTime(long installTime) {
130 | mPackageMeta.installTime = installTime;
131 | return this;
132 | }
133 |
134 | public Builder setUpdateTime(long updateTime) {
135 | mPackageMeta.updateTime = updateTime;
136 | return this;
137 | }
138 |
139 | public PackageMeta build() {
140 | return mPackageMeta;
141 | }
142 | }
143 |
144 | @Nullable
145 | public static PackageMeta forPackage(Context context, String packageName) {
146 | try {
147 | PackageManager pm = context.getPackageManager();
148 |
149 | ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, 0);
150 | PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
151 |
152 | return new PackageMeta.Builder(applicationInfo.packageName)
153 | .setLabel(applicationInfo.loadLabel(pm).toString())
154 | .setHasSplits(applicationInfo.splitPublicSourceDirs != null && applicationInfo.splitPublicSourceDirs.length > 0)
155 | .setIsSystemApp((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0)
156 | .setVersionCode(Utils.apiIsAtLeast(Build.VERSION_CODES.P) ? packageInfo.getLongVersionCode() : packageInfo.versionCode)
157 | .setVersionName(packageInfo.versionName)
158 | .setIcon(applicationInfo.icon)
159 | .setInstallTime(packageInfo.firstInstallTime)
160 | .setUpdateTime(packageInfo.lastUpdateTime)
161 | .build();
162 |
163 | } catch (PackageManager.NameNotFoundException e) {
164 | return null;
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/filedescriptor/ContentUriFileDescriptor.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.model.filedescriptor;
2 |
3 | import android.content.ContentResolver;
4 | import android.content.Context;
5 | import android.net.Uri;
6 |
7 | import androidx.documentfile.provider.DocumentFile;
8 |
9 | import com.aefyr.sai.utils.saf.SafUtils;
10 |
11 | import java.io.InputStream;
12 |
13 | public class ContentUriFileDescriptor implements FileDescriptor {
14 |
15 | private ContentResolver mContentResolver;
16 | private Uri mContentUri;
17 | private DocumentFile mDocumentFile;
18 |
19 | public ContentUriFileDescriptor(Context c, Uri contentUri) {
20 | mContentResolver = c.getContentResolver();
21 | mContentUri = contentUri;
22 | mDocumentFile = SafUtils.docFileFromSingleUriOrFileUri(c, contentUri);
23 | }
24 |
25 |
26 | @Override
27 | public String name() throws Exception {
28 | String name = mDocumentFile.getName();
29 | if (name == null)
30 | throw new BadContentProviderException("DISPLAY_NAME column is null");
31 |
32 | return name;
33 | }
34 |
35 | @Override
36 | public long length() throws Exception {
37 | long length = mDocumentFile.length();
38 |
39 | if (length == 0)
40 | throw new BadContentProviderException("SIZE column is 0");
41 |
42 | return length;
43 | }
44 |
45 | @Override
46 | public InputStream open() throws Exception {
47 | return mContentResolver.openInputStream(mContentUri);
48 | }
49 |
50 | private static class BadContentProviderException extends Exception {
51 |
52 | private BadContentProviderException(String message) {
53 | super(message);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/filedescriptor/FileDescriptor.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.model.filedescriptor;
2 |
3 | import java.io.InputStream;
4 |
5 | public interface FileDescriptor {
6 |
7 | String name() throws Exception;
8 |
9 | long length() throws Exception;
10 |
11 | InputStream open() throws Exception;
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/filedescriptor/NormalFileDescriptor.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.model.filedescriptor;
2 |
3 | import java.io.File;
4 | import java.io.FileInputStream;
5 | import java.io.InputStream;
6 |
7 | public class NormalFileDescriptor implements FileDescriptor {
8 |
9 | private File mFile;
10 |
11 | public NormalFileDescriptor(File file) {
12 | mFile = file;
13 | }
14 |
15 | @Override
16 | public String name() {
17 | return mFile.getName();
18 | }
19 |
20 | @Override
21 | public long length() {
22 | return mFile.length();
23 | }
24 |
25 | @Override
26 | public InputStream open() throws Exception {
27 | return new FileInputStream(mFile);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/model/licenses/License.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.model.licenses;
2 |
3 | public class License {
4 |
5 | public String subject;
6 | public String text;
7 |
8 | public License(String subject, String text) {
9 | this.subject = subject;
10 | this.text = text;
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/shell/Shell.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.shell;
2 |
3 | import android.annotation.SuppressLint;
4 |
5 | import androidx.annotation.NonNull;
6 |
7 | import java.io.InputStream;
8 | import java.util.ArrayList;
9 | import java.util.Arrays;
10 |
11 | public interface Shell {
12 |
13 | boolean isAvailable();
14 |
15 | Result exec(Command command);
16 |
17 | Result exec(Command command, InputStream inputPipe);
18 |
19 | String makeLiteral(String arg);
20 |
21 | class Command {
22 | private ArrayList mArgs = new ArrayList<>();
23 |
24 | public Command(String command, String... args) {
25 | mArgs.add(command);
26 | mArgs.addAll(Arrays.asList(args));
27 | }
28 |
29 | public String[] toStringArray() {
30 | String[] array = new String[mArgs.size()];
31 |
32 | for (int i = 0; i < mArgs.size(); i++)
33 | array[i] = mArgs.get(i);
34 |
35 | return array;
36 | }
37 |
38 | @NonNull
39 | @Override
40 | public String toString() {
41 | StringBuilder sb = new StringBuilder();
42 |
43 | for (int i = 0; i < mArgs.size(); i++) {
44 | String arg = mArgs.get(i);
45 | sb.append(arg);
46 | if (i < mArgs.size() - 1)
47 | sb.append(" ");
48 | }
49 |
50 | return sb.toString();
51 | }
52 |
53 | public static class Builder {
54 | private Command mCommand;
55 |
56 | public Builder(String command, String... args) {
57 | mCommand = new Command(command, args);
58 | }
59 |
60 | public Builder addArg(String argument) {
61 | mCommand.mArgs.add(argument);
62 | return this;
63 | }
64 |
65 | public Command build() {
66 | return mCommand;
67 | }
68 | }
69 | }
70 |
71 | class Result {
72 | Command cmd;
73 | public int exitCode;
74 | public String out;
75 | public String err;
76 |
77 | protected Result(Command cmd, int exitCode, String out, String err) {
78 | this.cmd = cmd;
79 | this.exitCode = exitCode;
80 | this.out = out;
81 | this.err = err;
82 | }
83 |
84 | public boolean isSuccessful() {
85 | return exitCode == 0;
86 | }
87 |
88 | @SuppressLint("DefaultLocale")
89 | @NonNull
90 | @Override
91 | public String toString() {
92 | return String.format("Command: %s\nExit code: %d\nOut:\n%s\n=============\nErr:\n%s", cmd, exitCode, out, err);
93 | }
94 | }
95 |
96 | }
97 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/shell/ShizukuShell.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.shell;
2 |
3 | import android.os.Build;
4 | import android.util.Log;
5 |
6 | import androidx.annotation.Nullable;
7 |
8 | import com.aefyr.sai.utils.IOUtils;
9 | import com.aefyr.sai.utils.Utils;
10 |
11 | import java.io.InputStream;
12 | import java.io.OutputStream;
13 |
14 | import rikka.shizuku.Shizuku;
15 | import rikka.shizuku.ShizukuRemoteProcess;
16 |
17 | public class ShizukuShell implements Shell {
18 | private static final String TAG = "ShizukuShell";
19 |
20 | private static ShizukuShell sInstance;
21 |
22 | public static ShizukuShell getInstance() {
23 | synchronized (ShizukuShell.class) {
24 | return sInstance != null ? sInstance : new ShizukuShell();
25 | }
26 | }
27 |
28 | private ShizukuShell() {
29 | sInstance = this;
30 | }
31 |
32 | @Override
33 | public boolean isAvailable() {
34 | if (!Shizuku.pingBinder())
35 | return false;
36 |
37 | try {
38 | return exec(new Command("echo", "test")).isSuccessful();
39 | } catch (Exception e) {
40 | Log.w(TAG, "Unable to access shizuku: ");
41 | Log.w(TAG, e);
42 | return false;
43 | }
44 | }
45 |
46 | @Override
47 | public Result exec(Command command) {
48 | return execInternal(command, null);
49 | }
50 |
51 | @Override
52 | public Result exec(Command command, InputStream inputPipe) {
53 | return execInternal(command, inputPipe);
54 | }
55 |
56 | @Override
57 | public String makeLiteral(String arg) {
58 | return "'" + arg.replace("'", "'\\''") + "'";
59 | }
60 |
61 | private Result execInternal(Command command, @Nullable InputStream inputPipe) {
62 |
63 | // System.out.println(Shizuku.pingBinder());
64 |
65 |
66 |
67 | // TODO System.out.println(command.toString());
68 | // System.out.println(Shizuku.checkSelfPermission());
69 |
70 | /*if(!command.toString().equals("echo test")){
71 |
72 |
73 | System.out.println("isAvailable");
74 | System.out.println(isAvailable());
75 | }*/
76 |
77 |
78 | StringBuilder stdOutSb = new StringBuilder();
79 | StringBuilder stdErrSb = new StringBuilder();
80 |
81 | try {
82 | Command.Builder shCommand = new Command.Builder("sh", "-c", command.toString());
83 |
84 | ShizukuRemoteProcess process = Shizuku.newProcess(shCommand.build().toStringArray(), null, null);
85 |
86 | Thread stdOutD = IOUtils.writeStreamToStringBuilder(stdOutSb, process.getInputStream());
87 | Thread stdErrD = IOUtils.writeStreamToStringBuilder(stdErrSb, process.getErrorStream());
88 |
89 | if (inputPipe != null) {
90 | try (OutputStream outputStream = process.getOutputStream(); InputStream inputStream = inputPipe) {
91 | IOUtils.copyStream(inputStream, outputStream);
92 | } catch (Exception e) {
93 | stdOutD.interrupt();
94 | stdErrD.interrupt();
95 |
96 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
97 | process.destroyForcibly();
98 | else
99 | process.destroy();
100 |
101 | throw new RuntimeException(e);
102 | }
103 | }
104 |
105 | process.waitFor();
106 | stdOutD.join();
107 | stdErrD.join();
108 |
109 | return new Result(command, process.exitValue(), stdOutSb.toString().trim(), stdErrSb.toString().trim());
110 | } catch (Exception e) {
111 | Log.w(TAG, "Unable execute command: ");
112 | Log.w(TAG, e);
113 | return new Result(command, -1, stdOutSb.toString().trim(), stdErrSb.toString() + "\n\n SAI ShizukuShell Java exception: " + Utils.throwableToString(e));
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/shell/SuShell.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.shell;
2 |
3 | import android.os.Build;
4 | import android.util.Log;
5 |
6 | import androidx.annotation.Nullable;
7 |
8 | import com.aefyr.sai.utils.IOUtils;
9 | import com.aefyr.sai.utils.Utils;
10 |
11 | import java.io.InputStream;
12 | import java.io.OutputStream;
13 |
14 | public class SuShell implements Shell {
15 | private static final String TAG = "SuShell";
16 |
17 | private static SuShell sInstance;
18 |
19 | public static SuShell getInstance() {
20 | synchronized (SuShell.class) {
21 | return sInstance != null ? sInstance : new SuShell();
22 | }
23 | }
24 |
25 | private SuShell() {
26 | sInstance = this;
27 | }
28 |
29 | public boolean requestRoot() {
30 | try {
31 | return exec(new Command("exit")).isSuccessful();
32 | } catch (Exception e) {
33 | Log.w(TAG, "Unable to acquire root access: ");
34 | Log.w(TAG, e);
35 | return false;
36 | }
37 | }
38 |
39 | @Override
40 | public boolean isAvailable() {
41 | return requestRoot();
42 | }
43 |
44 | @Override
45 | public Result exec(Command command) {
46 | return execInternal(command, null);
47 | }
48 |
49 | @Override
50 | public Result exec(Command command, InputStream inputPipe) {
51 | return execInternal(command, inputPipe);
52 | }
53 |
54 | @Override
55 | public String makeLiteral(String arg) {
56 | return "'" + arg.replace("'", "'\\''") + "'";
57 | }
58 |
59 | private Result execInternal(Command command, @Nullable InputStream inputPipe) {
60 | StringBuilder stdOutSb = new StringBuilder();
61 | StringBuilder stdErrSb = new StringBuilder();
62 |
63 | try {
64 | Command.Builder suCommand = new Command.Builder("su", "-c", command.toString());
65 |
66 | Process process = Runtime.getRuntime().exec(suCommand.build().toStringArray());
67 |
68 |
69 | Thread stdOutD = IOUtils.writeStreamToStringBuilder(stdOutSb, process.getInputStream());
70 | Thread stdErrD = IOUtils.writeStreamToStringBuilder(stdErrSb, process.getErrorStream());
71 |
72 | if (inputPipe != null) {
73 | try (OutputStream outputStream = process.getOutputStream(); InputStream inputStream = inputPipe) {
74 | IOUtils.copyStream(inputStream, outputStream);
75 | } catch (Exception e) {
76 | stdOutD.interrupt();
77 | stdErrD.interrupt();
78 |
79 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
80 | process.destroyForcibly();
81 | else
82 | process.destroy();
83 |
84 | throw new RuntimeException(e);
85 | }
86 | }
87 |
88 | process.waitFor();
89 | stdOutD.join();
90 | stdErrD.join();
91 |
92 | return new Result(command, process.exitValue(), stdOutSb.toString().trim(), stdErrSb.toString().trim());
93 | } catch (Exception e) {
94 | Log.w(TAG, "Unable execute command: ");
95 | Log.w(TAG, e);
96 | return new Result(command, -1, stdOutSb.toString().trim(), stdErrSb.toString() + "\n\n SAI SuShell Java exception: " + Utils.throwableToString(e));
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/shizuku/SuiInitProvider.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.shizuku;
2 |
3 | import android.content.ContentProvider;
4 | import android.content.ContentValues;
5 | import android.database.Cursor;
6 | import android.net.Uri;
7 | import android.os.Build;
8 | import android.util.Log;
9 |
10 | import androidx.annotation.NonNull;
11 | import androidx.annotation.Nullable;
12 |
13 | import com.aefyr.sai.utils.Utils;
14 |
15 | import rikka.sui.Sui;
16 |
17 | public class SuiInitProvider extends ContentProvider {
18 | public static final String TAG = "SuiInitProvider";
19 |
20 | @Override
21 | public boolean onCreate() {
22 |
23 | return true;
24 | }
25 |
26 | @Nullable
27 | @Override
28 | public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
29 | return null;
30 | }
31 |
32 | @Nullable
33 | @Override
34 | public String getType(@NonNull Uri uri) {
35 | return null;
36 | }
37 |
38 | @Nullable
39 | @Override
40 | public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
41 | return null;
42 | }
43 |
44 | @Override
45 | public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
46 | return 0;
47 | }
48 |
49 | @Override
50 | public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
51 | return 0;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/DbgPreferencesHelper.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 |
6 | import androidx.preference.PreferenceManager;
7 |
8 | public class DbgPreferencesHelper {
9 | private static DbgPreferencesHelper sInstance;
10 |
11 | private SharedPreferences mPrefs;
12 |
13 | public static DbgPreferencesHelper getInstance(Context c) {
14 | return sInstance != null ? sInstance : new DbgPreferencesHelper(c);
15 | }
16 |
17 | private DbgPreferencesHelper(Context c) {
18 | mPrefs = PreferenceManager.getDefaultSharedPreferences(c);
19 | sInstance = this;
20 | }
21 |
22 | public boolean shouldReplaceDots() {
23 | return !mPrefs.getBoolean(DbgPreferencesKeys.DONT_REPLACE_DOTS, false);
24 | }
25 |
26 | public String getCustomInstallCreateCommand() {
27 | String command = mPrefs.getString(DbgPreferencesKeys.CUSTOM_INSTALL_CREATE, "null");
28 | if ("null".equalsIgnoreCase(command))
29 | return null;
30 |
31 | return command;
32 | }
33 |
34 | public boolean addFakeTimestampToBackups() {
35 | return mPrefs.getBoolean(DbgPreferencesKeys.ADD_FAKE_TIMESTAMP_TO_BACKUPS, false);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/DbgPreferencesKeys.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | public class DbgPreferencesKeys {
4 | public static final String DONT_REPLACE_DOTS = "dbg_dont_replace_dots";
5 | public static final String CUSTOM_INSTALL_CREATE = "dbg_custom_install_create";
6 | public static final String ADD_FAKE_TIMESTAMP_TO_BACKUPS = "dbg_fake_backup_timestamp";
7 | public static final String TEST_CRASH = "dbg_test_crash";
8 | }
9 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/IOUtils.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 |
6 | import java.io.BufferedReader;
7 | import java.io.ByteArrayInputStream;
8 | import java.io.ByteArrayOutputStream;
9 | import java.io.Closeable;
10 | import java.io.File;
11 | import java.io.FileInputStream;
12 | import java.io.FileOutputStream;
13 | import java.io.IOException;
14 | import java.io.InputStream;
15 | import java.io.InputStreamReader;
16 | import java.io.OutputStream;
17 | import java.nio.charset.Charset;
18 | import java.nio.charset.StandardCharsets;
19 | import java.security.DigestInputStream;
20 | import java.security.MessageDigest;
21 | import java.util.zip.CRC32;
22 |
23 | public class IOUtils {
24 | private static final String TAG = "IOUtils";
25 |
26 | public static void copyStream(InputStream from, OutputStream to) throws IOException {
27 | byte[] buf = new byte[1024 * 1024];
28 | int len;
29 | while ((len = from.read(buf)) > 0) {
30 | to.write(buf, 0, len);
31 | }
32 | }
33 |
34 | public static void copyFile(File original, File destination) throws IOException {
35 | try (FileInputStream inputStream = new FileInputStream(original); FileOutputStream outputStream = new FileOutputStream(destination)) {
36 | copyStream(inputStream, outputStream);
37 | }
38 | }
39 |
40 | public static void copyFileFromAssets(Context context, String assetFileName, File destination) throws IOException {
41 | try (InputStream inputStream = context.getAssets().open(assetFileName); FileOutputStream outputStream = new FileOutputStream(destination)) {
42 | copyStream(inputStream, outputStream);
43 | }
44 | }
45 |
46 | public static void deleteRecursively(File f) {
47 | if (f.isDirectory()) {
48 | File[] files = f.listFiles();
49 | if (files != null) {
50 | for (File child : files)
51 | deleteRecursively(child);
52 | }
53 | }
54 | f.delete();
55 | }
56 |
57 | public static long calculateFileCrc32(File file) throws IOException {
58 | return calculateCrc32(new FileInputStream(file));
59 | }
60 |
61 | public static long calculateBytesCrc32(byte[] bytes) throws IOException {
62 | return calculateCrc32(new ByteArrayInputStream(bytes));
63 | }
64 |
65 | public static long calculateCrc32(InputStream inputStream) throws IOException {
66 | try (InputStream in = inputStream) {
67 | CRC32 crc32 = new CRC32();
68 | byte[] buffer = new byte[1024 * 1024];
69 | int read;
70 |
71 | while ((read = in.read(buffer)) > 0)
72 | crc32.update(buffer, 0, read);
73 |
74 | return crc32.getValue();
75 | }
76 | }
77 |
78 | public static Thread writeStreamToStringBuilder(StringBuilder builder, InputStream inputStream) {
79 | Thread t = new Thread(() -> {
80 | try {
81 | char[] buf = new char[1024];
82 | int len;
83 | BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
84 | while ((len = reader.read(buf)) > 0)
85 | builder.append(buf, 0, len);
86 |
87 | reader.close();
88 | } catch (Exception e) {
89 | Log.wtf(TAG, e);
90 | }
91 | });
92 | t.start();
93 | return t;
94 | }
95 |
96 | /**
97 | * Read contents of input stream to a byte array and close it
98 | *
99 | * @param inputStream
100 | * @return contents of input stream
101 | * @throws IOException
102 | */
103 | public static byte[] readStream(InputStream inputStream) throws IOException {
104 | try (InputStream in = inputStream) {
105 | return readStreamNoClose(in);
106 | }
107 | }
108 |
109 | public static String readStream(InputStream inputStream, Charset charset) throws IOException {
110 | return new String(readStream(inputStream), charset);
111 | }
112 |
113 | /**
114 | * Read contents of input stream to a byte array, but don't close the stream
115 | *
116 | * @param inputStream
117 | * @return contents of input stream
118 | * @throws IOException
119 | */
120 | public static byte[] readStreamNoClose(InputStream inputStream) throws IOException {
121 | ByteArrayOutputStream buffer = new ByteArrayOutputStream();
122 | copyStream(inputStream, buffer);
123 | return buffer.toByteArray();
124 | }
125 |
126 | public static void closeSilently(Closeable closeable) {
127 | if (closeable == null)
128 | return;
129 |
130 | try {
131 | closeable.close();
132 | } catch (Exception e) {
133 | Log.w(TAG, String.format("Unable to close %s", closeable.getClass().getCanonicalName()), e);
134 | }
135 | }
136 |
137 | /**
138 | * Hashes stream content using passed {@link MessageDigest}, closes the stream and returns digest bytes
139 | *
140 | * @param inputStream
141 | * @param messageDigest
142 | * @return
143 | * @throws IOException
144 | */
145 | public static byte[] hashStream(InputStream inputStream, MessageDigest messageDigest) throws IOException {
146 | try (DigestInputStream digestInputStream = new DigestInputStream(inputStream, messageDigest);) {
147 | byte[] buffer = new byte[1024 * 64];
148 | int read;
149 | while ((read = digestInputStream.read(buffer)) > 0) {
150 | //Do nothing
151 | }
152 |
153 | return messageDigest.digest();
154 | }
155 | }
156 |
157 | public static byte[] hashString(String s, MessageDigest messageDigest) throws IOException {
158 | return hashStream(new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)), messageDigest);
159 | }
160 |
161 | public static byte[] readFile(File file) throws IOException {
162 | try (FileInputStream in = new FileInputStream(file)) {
163 | return readStream(in);
164 | }
165 | }
166 |
167 |
168 | }
169 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/Locker.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | public interface Locker {
4 |
5 | Object getLockFor(T t);
6 |
7 | default void withLock(T t, Runnable action) {
8 | synchronized (getLockFor(t)) {
9 | action.run();
10 | }
11 | }
12 |
13 | void clearLock(T t);
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/MapBackedLocker.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | import android.annotation.TargetApi;
4 | import android.os.Build;
5 |
6 | import java.util.HashMap;
7 | import java.util.concurrent.ConcurrentHashMap;
8 |
9 | public abstract class MapBackedLocker implements Locker {
10 |
11 | public static MapBackedLocker create() {
12 | if (Utils.apiIsAtLeast(Build.VERSION_CODES.N))
13 | return new ConcurrentHashMapLocker<>();
14 |
15 | return new HashMapLocker<>();
16 | }
17 |
18 | private static class HashMapLocker extends MapBackedLocker {
19 |
20 | private final HashMap mLocks = new HashMap<>();
21 |
22 | private HashMapLocker() {
23 |
24 | }
25 |
26 | @Override
27 | public Object getLockFor(T t) {
28 | synchronized (mLocks) {
29 | Object lock = mLocks.get(t);
30 | if (lock == null) {
31 | lock = new Object();
32 | mLocks.put(t, lock);
33 | }
34 |
35 | return lock;
36 | }
37 | }
38 |
39 | @Override
40 | public void clearLock(T t) {
41 | synchronized (mLocks) {
42 | mLocks.remove(t);
43 | }
44 | }
45 | }
46 |
47 | @TargetApi(Build.VERSION_CODES.N)
48 | private static class ConcurrentHashMapLocker extends MapBackedLocker {
49 |
50 | private final ConcurrentHashMap mLocks = new ConcurrentHashMap<>();
51 |
52 | private ConcurrentHashMapLocker() {
53 |
54 | }
55 |
56 | @Override
57 | public Object getLockFor(T t) {
58 | return mLocks.computeIfAbsent(t, key -> new Object());
59 | }
60 |
61 | @Override
62 | public void clearLock(T t) {
63 | mLocks.remove(t);
64 | }
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/MathUtils.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | public class MathUtils {
4 |
5 | public static int clamp(int a, int min, int max) {
6 | if (a < min)
7 | return min;
8 |
9 | if (a > max)
10 | return max;
11 |
12 | return a;
13 | }
14 |
15 | public static int closest(int x, int a, int b) {
16 | int distanceToA = Math.abs(x - a);
17 | int distanceToB = Math.abs(x - b);
18 |
19 | if (distanceToA > distanceToB)
20 | return b;
21 |
22 | return a;
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/MiuiUtils.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.os.Build;
5 | import android.text.TextUtils;
6 |
7 | import java.util.Objects;
8 |
9 | public class MiuiUtils {
10 |
11 | public static boolean isMiui() {
12 | return !TextUtils.isEmpty(Utils.getSystemProperty("ro.miui.ui.version.name"));
13 | }
14 |
15 | public static String getMiuiVersionName() {
16 | String versionName = Utils.getSystemProperty("ro.miui.ui.version.name");
17 | return !TextUtils.isEmpty(versionName) ? versionName : "???";
18 | }
19 |
20 | public static int getMiuiVersionCode() {
21 | try {
22 | return Integer.parseInt(Objects.requireNonNull(Utils.getSystemProperty("ro.miui.ui.version.code")));
23 | } catch (Exception e) {
24 | return -1;
25 | }
26 | }
27 |
28 | public static String getActualMiuiVersion() {
29 | return Build.VERSION.INCREMENTAL;
30 | }
31 |
32 | private static int[] parseVersionIntoParts(String version) {
33 | try {
34 | String[] versionParts = version.split("\\.");
35 | int[] intVersionParts = new int[versionParts.length];
36 |
37 | for (int i = 0; i < versionParts.length; i++)
38 | intVersionParts[i] = Integer.parseInt(versionParts[i]);
39 |
40 | return intVersionParts;
41 | } catch (Exception e) {
42 | return new int[]{-1};
43 | }
44 | }
45 |
46 | /**
47 | * @return 0 if versions are equal, values less than 0 if ver1 is lower than ver2, value more than 0 if ver1 is higher than ver2
48 | */
49 | private static int compareVersions(String version1, String version2) {
50 | if (version1.equals(version2))
51 | return 0;
52 |
53 | int[] version1Parts = parseVersionIntoParts(version1);
54 | int[] version2Parts = parseVersionIntoParts(version2);
55 |
56 | for (int i = 0; i < version2Parts.length; i++) {
57 | if (i >= version1Parts.length)
58 | return -1;
59 |
60 | if (version1Parts[i] < version2Parts[i])
61 | return -1;
62 |
63 | if (version1Parts[i] > version2Parts[i])
64 | return 1;
65 | }
66 |
67 | return 1;
68 | }
69 |
70 | public static boolean isActualMiuiVersionAtLeast(String targetVer) {
71 | return compareVersions(getActualMiuiVersion(), targetVer) >= 0;
72 | }
73 |
74 | @SuppressLint("PrivateApi")
75 | public static boolean isMiuiOptimizationDisabled() {
76 | if ("0".equals(Utils.getSystemProperty("persist.sys.miui_optimization")))
77 | return true;
78 |
79 | try {
80 | return (boolean) Class.forName("android.miui.AppOpsUtils")
81 | .getDeclaredMethod("isXOptMode")
82 | .invoke(null);
83 | } catch (Exception e) {
84 | return false;
85 | }
86 | }
87 |
88 | public static boolean isFixedMiui() {
89 | return isActualMiuiVersionAtLeast("20.2.20") || isMiuiOptimizationDisabled();
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/NotificationHelper.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | import android.app.Notification;
4 | import android.content.Context;
5 | import android.os.Handler;
6 | import android.os.Looper;
7 | import android.os.SystemClock;
8 |
9 | import androidx.annotation.Nullable;
10 | import androidx.core.app.NotificationManagerCompat;
11 |
12 | /**
13 | * Manages delaying notification to avoid going over notifications per second limit
14 | */
15 | public class NotificationHelper {
16 | private static final long NOTIFICATION_CD = 1000 / 4;
17 |
18 | private static NotificationHelper sInstance;
19 |
20 | private NotificationManagerCompat mNotificationManager;
21 | private Handler mHandler = new Handler(Looper.getMainLooper());
22 |
23 | private long mLastNotificationTime = 0;
24 |
25 | public static NotificationHelper getInstance(Context context) {
26 | synchronized (NotificationHelper.class) {
27 | return sInstance != null ? sInstance : new NotificationHelper(context);
28 | }
29 | }
30 |
31 | private NotificationHelper(Context c) {
32 | mNotificationManager = NotificationManagerCompat.from(c.getApplicationContext());
33 | sInstance = this;
34 | }
35 |
36 | /**
37 | * Post notification when possible or skip it, if it is skipable
38 | *
39 | * @param id id of the notification
40 | * @param notification notification to post
41 | * @param skipable if notification can be skipped (such as progress notifications)
42 | */
43 | public void notify(int id, Notification notification, boolean skipable) {
44 | notify(null, id, notification, skipable);
45 | }
46 |
47 | /**
48 | * Post notification when possible or skip it, if it is skipable
49 | *
50 | * @param tag tag on the notification
51 | * @param id id of the notification
52 | * @param notification notification to post
53 | * @param skipable if notification can be skipped (such as progress notifications)
54 | */
55 | public synchronized void notify(@Nullable String tag, int id, Notification notification, boolean skipable) {
56 | long timeSinceLastNotification = SystemClock.uptimeMillis() - mLastNotificationTime;
57 |
58 | if (timeSinceLastNotification < NOTIFICATION_CD) {
59 | if (!skipable) {
60 | mHandler.postAtTime(() -> mNotificationManager.notify(tag, id, notification), mLastNotificationTime + NOTIFICATION_CD);
61 | mLastNotificationTime = mLastNotificationTime + NOTIFICATION_CD;
62 | }
63 | return;
64 | }
65 |
66 | mLastNotificationTime = SystemClock.uptimeMillis();
67 | mNotificationManager.notify(tag, id, notification);
68 | }
69 |
70 | public void cancel(@Nullable String tag, int id) {
71 | mNotificationManager.cancel(tag, id);
72 | }
73 |
74 | public void cancel(int id) {
75 | cancel(null, id);
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/PermissionsUtils.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | import android.Manifest;
4 | import android.app.Activity;
5 | import android.content.pm.PackageManager;
6 | import android.os.Build;
7 |
8 | import androidx.core.app.ActivityCompat;
9 | import androidx.fragment.app.Fragment;
10 |
11 | public class PermissionsUtils {
12 | public static final int REQUEST_CODE_STORAGE_PERMISSIONS = 322;
13 | public static final int REQUEST_CODE_SHIZUKU = 1337;
14 |
15 | public static boolean checkAndRequestStoragePermissions(Activity a) {
16 | return checkAndRequestPermissions(a, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_STORAGE_PERMISSIONS);
17 | }
18 |
19 | public static boolean checkAndRequestStoragePermissions(Fragment f) {
20 | return checkAndRequestPermissions(f, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_STORAGE_PERMISSIONS);
21 | }
22 |
23 | public static boolean checkAndRequestShizukuPermissions(Activity a) {
24 | return checkAndRequestPermissions(a, new String[]{"moe.shizuku.manager.permission.API_V23"}, REQUEST_CODE_SHIZUKU);
25 | }
26 |
27 | public static boolean checkAndRequestShizukuPermissions(Fragment f) {
28 | return checkAndRequestPermissions(f, new String[]{"moe.shizuku.manager.permission.API_V23"}, REQUEST_CODE_SHIZUKU);
29 | }
30 |
31 | private static boolean checkAndRequestPermissions(Activity a, String[] permissions, int requestCode) {
32 | if (Build.VERSION.SDK_INT < 23)
33 | return true;
34 |
35 | for (String permission : permissions) {
36 | if ((ActivityCompat.checkSelfPermission(a, permission)) == PackageManager.PERMISSION_DENIED) {
37 | a.requestPermissions(permissions, requestCode);
38 | return false;
39 | }
40 | }
41 | return true;
42 | }
43 |
44 | private static boolean checkAndRequestPermissions(Fragment f, String[] permissions, int requestCode) {
45 | if (Build.VERSION.SDK_INT < 23)
46 | return true;
47 |
48 | for (String permission : permissions) {
49 | if ((ActivityCompat.checkSelfPermission(f.requireContext(), permission)) == PackageManager.PERMISSION_DENIED) {
50 | f.requestPermissions(permissions, requestCode);
51 | return false;
52 | }
53 | }
54 | return true;
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/PreferencesHelper.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.os.Environment;
6 |
7 | import androidx.preference.PreferenceManager;
8 |
9 | public class PreferencesHelper {
10 | private static PreferencesHelper sInstance;
11 |
12 | private SharedPreferences mPrefs;
13 |
14 | public static PreferencesHelper getInstance(Context c) {
15 | return sInstance != null ? sInstance : new PreferencesHelper(c);
16 | }
17 |
18 | private PreferencesHelper(Context c) {
19 | mPrefs = PreferenceManager.getDefaultSharedPreferences(c);
20 | sInstance = this;
21 | }
22 |
23 | public SharedPreferences getPrefs() {
24 | return mPrefs;
25 | }
26 |
27 | public String getHomeDirectory() {
28 | return mPrefs.getString(PreferencesKeys.HOME_DIRECTORY, Environment.getExternalStorageDirectory().getAbsolutePath());
29 | }
30 |
31 | public void setHomeDirectory(String homeDirectory) {
32 | mPrefs.edit().putString(PreferencesKeys.HOME_DIRECTORY, homeDirectory).apply();
33 | }
34 |
35 | public int getFilePickerRawSort() {
36 | return mPrefs.getInt(PreferencesKeys.FILE_PICKER_SORT_RAW, 0);
37 | }
38 |
39 | public void setFilePickerRawSort(int rawSort) {
40 | mPrefs.edit().putInt(PreferencesKeys.FILE_PICKER_SORT_RAW, rawSort).apply();
41 | }
42 |
43 | // public int getFilePickerSortBy() {
44 | // return mPrefs.getInt(PreferencesKeys.FILE_PICKER_SORT_BY, DialogConfigs.SORT_BY_NAME);
45 | // }
46 |
47 | public void setFilePickerSortBy(int sortBy) {
48 | mPrefs.edit().putInt(PreferencesKeys.FILE_PICKER_SORT_BY, sortBy).apply();
49 | }
50 |
51 | // public int getFilePickerSortOrder() {
52 | // return mPrefs.getInt(PreferencesKeys.FILE_PICKER_SORT_ORDER, DialogConfigs.SORT_ORDER_NORMAL);
53 | // }
54 |
55 | public void setFilePickerSortOrder(int sortOrder) {
56 | mPrefs.edit().putInt(PreferencesKeys.FILE_PICKER_SORT_ORDER, sortOrder).apply();
57 | }
58 |
59 | public boolean shouldSignApks() {
60 | return mPrefs.getBoolean(PreferencesKeys.SIGN_APKS, false);
61 | }
62 |
63 | public void setShouldSignApks(boolean signApks) {
64 | mPrefs.edit().putBoolean(PreferencesKeys.SIGN_APKS, signApks).apply();
65 | }
66 |
67 | public boolean shouldExtractArchives() {
68 | return mPrefs.getBoolean(PreferencesKeys.EXTRACT_ARCHIVES, false);
69 | }
70 |
71 | public boolean shouldUseZipFileApi() {
72 | return mPrefs.getBoolean(PreferencesKeys.USE_ZIPFILE, false);
73 | }
74 |
75 | public void setInstaller(int installer) {
76 | mPrefs.edit().putInt(PreferencesKeys.INSTALLER, installer).apply();
77 | }
78 |
79 | public int getInstaller() {
80 | return mPrefs.getInt(PreferencesKeys.INSTALLER, PreferencesValues.INSTALLER_ROOTLESS);
81 | }
82 |
83 | public void setBackupFileNameFormat(String format) {
84 | mPrefs.edit().putString(PreferencesKeys.BACKUP_FILE_NAME_FORMAT, format).apply();
85 | }
86 |
87 | public String getBackupFileNameFormat() {
88 | return mPrefs.getString(PreferencesKeys.BACKUP_FILE_NAME_FORMAT, PreferencesValues.BACKUP_FILE_NAME_FORMAT_DEFAULT);
89 | }
90 |
91 | public void setInstallLocation(int installLocation) {
92 | mPrefs.edit().putString(PreferencesKeys.INSTALL_LOCATION, String.valueOf(installLocation)).apply();
93 | }
94 |
95 | public int getInstallLocation() {
96 | String rawInstallLocation = mPrefs.getString(PreferencesKeys.INSTALL_LOCATION, "0");
97 | try {
98 | return Integer.parseInt(rawInstallLocation);
99 | } catch (NumberFormatException e) {
100 | return 0;
101 | }
102 | }
103 |
104 | public boolean useOldInstaller() {
105 | return mPrefs.getBoolean(PreferencesKeys.USE_OLD_INSTALLER, false);
106 | }
107 |
108 | public boolean showInstallerDialogs() {
109 | return mPrefs.getBoolean(PreferencesKeys.SHOW_INSTALLER_DIALOGS, true);
110 | }
111 |
112 | public boolean shouldShowAppFeatures() {
113 | return mPrefs.getBoolean(PreferencesKeys.SHOW_APP_FEATURES, true);
114 | }
115 |
116 | public boolean shouldShowSafTip() {
117 | return !mPrefs.getBoolean(PreferencesKeys.SAF_TIP_SHOWN, false);
118 | }
119 |
120 | public void setSafTipShown() {
121 | mPrefs.edit().putBoolean(PreferencesKeys.SAF_TIP_SHOWN, true).apply();
122 | }
123 |
124 | public boolean isInstallerXEnabled() {
125 | return mPrefs.getBoolean(PreferencesKeys.USE_INSTALLERX, true);
126 | }
127 |
128 | public boolean isBruteParserEnabled() {
129 | return mPrefs.getBoolean(PreferencesKeys.USE_BRUTE_PARSER, true);
130 | }
131 |
132 | public boolean isAnalyticsEnabled() {
133 | return mPrefs.getBoolean(PreferencesKeys.ENABLE_ANALYTICS, true);
134 | }
135 |
136 | public void setAnalyticsEnabled(boolean enabled) {
137 | mPrefs.edit().putBoolean(PreferencesKeys.ENABLE_ANALYTICS, enabled).apply();
138 | }
139 |
140 | public boolean isInitialIndexingDone() {
141 | return mPrefs.getBoolean(PreferencesKeys.INITIAL_INDEXING_RUN, false);
142 | }
143 |
144 | public void setInitialIndexingDone(boolean done) {
145 | mPrefs.edit().putBoolean(PreferencesKeys.INITIAL_INDEXING_RUN, done).apply();
146 | }
147 |
148 | public boolean isSingleApkExportEnabled() {
149 | return mPrefs.getBoolean(PreferencesKeys.BACKUP_APK_EXPORT, false);
150 | }
151 |
152 | public void setSingleApkExportEnabled(boolean enabled) {
153 | mPrefs.edit().putBoolean(PreferencesKeys.BACKUP_APK_EXPORT, enabled).apply();
154 | }
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/PreferencesKeys.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | public class PreferencesKeys {
4 | public static final String CURRENT_THEME = "current_theme";
5 | public static final String THEME_MODE = "theme_mode";
6 | public static final String HOME_DIRECTORY = "home_directory";
7 | public static final String FILE_PICKER_SORT_RAW = "file_picker_sort_raw";
8 | public static final String FILE_PICKER_SORT_BY = "file_picker_sort_by";
9 | public static final String FILE_PICKER_SORT_ORDER = "file_picker_sort_order";
10 | public static final String SIGN_APKS = "sign_apks";
11 | public static final String MIUI_WARNING_SHOWN = "miui_warning_shown";
12 | public static final String EXTRACT_ARCHIVES = "extract_archives";
13 | public static final String USE_ZIPFILE = "use_zipfile";
14 | public static final String INSTALLER = "installer";
15 | public static final String BACKUP_FILE_NAME_FORMAT = "backup_file_name_format";
16 | public static final String INSTALL_LOCATION = "install_location";
17 | public static final String USE_OLD_INSTALLER = "use_old_installer";
18 | public static final String SHOW_INSTALLER_DIALOGS = "show_installer_dialogs";
19 | public static final String SHOW_APP_FEATURES = "show_app_features";
20 | public static final String THEME = "theme";
21 | public static final String SAF_TIP_SHOWN = "saf_tip_shown";
22 | public static final String AUTO_THEME = "auto_theme";
23 | public static final String AUTO_THEME_PICKER = "auto_theme_picker";
24 | public static final String USE_INSTALLERX = "use_installerx";
25 | public static final String USE_BRUTE_PARSER = "use_brute_parser";
26 | public static final String ENABLE_ANALYTICS = "firebase_enabled";
27 | public static final String INITIAL_INDEXING_RUN = "initial_indexing_run";
28 | public static final String BACKUP_SETTINGS = "backup_settings";
29 | public static final String BACKUP_APK_EXPORT = "single_apk_export";
30 | public static final String ENABLE_APK_ACTION_VIEW = "enable_apk_action_view";
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/PreferencesValues.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | public class PreferencesValues {
4 |
5 | public static final int INSTALLER_ROOTLESS = 0;
6 | public static final int INSTALLER_ROOTED = 1;
7 | public static final int INSTALLER_SHIZUKU = 2;
8 |
9 | public static final String BACKUP_FILE_NAME_FORMAT_DEFAULT = "NAME_PACKAGE_VERSION";
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/RwLock.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | import java.util.concurrent.Callable;
4 | import java.util.concurrent.locks.ReadWriteLock;
5 | import java.util.concurrent.locks.ReentrantReadWriteLock;
6 |
7 | public class RwLock {
8 |
9 | private ReadWriteLock mLock;
10 |
11 | public RwLock(ReadWriteLock backingLock) {
12 | mLock = backingLock;
13 | }
14 |
15 | public RwLock() {
16 | this(new ReentrantReadWriteLock(true));
17 | }
18 |
19 | public void withReadLock(Runnable action) {
20 | mLock.readLock().lock();
21 | try {
22 | action.run();
23 | } finally {
24 | mLock.readLock().unlock();
25 | }
26 | }
27 |
28 | public T withReadLockReturn(Callable callable) {
29 | mLock.readLock().lock();
30 | try {
31 | return callable.call();
32 | } catch (Exception e) {
33 | throw new RuntimeException(e);
34 | } finally {
35 | mLock.readLock().unlock();
36 | }
37 | }
38 |
39 | public void withWriteLock(Runnable action) {
40 | mLock.writeLock().lock();
41 | try {
42 | action.run();
43 | } finally {
44 | mLock.writeLock().unlock();
45 | }
46 | }
47 |
48 | public T withWriteLockReturn(Callable callable) {
49 | mLock.writeLock().lock();
50 | try {
51 | return callable.call();
52 | } catch (Exception e) {
53 | throw new RuntimeException(e);
54 | } finally {
55 | mLock.writeLock().unlock();
56 | }
57 | }
58 |
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/SimpleAsyncTask.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | import android.os.Handler;
4 | import android.os.Looper;
5 |
6 | import java.util.concurrent.ExecutorService;
7 | import java.util.concurrent.Executors;
8 | import java.util.concurrent.atomic.AtomicBoolean;
9 |
10 | public abstract class SimpleAsyncTask {
11 |
12 | private static ExecutorService sExecutor = Executors.newCachedThreadPool();
13 | private static Handler sHandler = new Handler(Looper.getMainLooper());
14 |
15 | private Argument mArgument;
16 | private AtomicBoolean mIsCancelled = new AtomicBoolean();
17 | private AtomicBoolean mIsOngoing = new AtomicBoolean();
18 |
19 | public SimpleAsyncTask(Argument argument) {
20 | mArgument = argument;
21 | }
22 |
23 | /**
24 | * If invoked on main thread, guarantees that onWorkDone and onError will never be called
25 | */
26 | public final void cancel() {
27 | mIsCancelled.set(true);
28 | }
29 |
30 | public final boolean isCancelled() {
31 | return mIsCancelled.get();
32 | }
33 |
34 | public final boolean isOngoing() {
35 | return mIsOngoing.get();
36 | }
37 |
38 | public final > T execute() {
39 | if (isOngoing())
40 | throw new IllegalStateException("Unable to execute a task that is already ongoing");
41 |
42 | mIsOngoing.set(true);
43 | sExecutor.submit(() -> {
44 | try {
45 | Result result = doWork(mArgument);
46 | sHandler.post(() -> {
47 | if (isCancelled()) {
48 | mIsOngoing.set(false);
49 | return;
50 | }
51 |
52 | onWorkDone(result);
53 | mIsOngoing.set(false);
54 | });
55 | } catch (Exception e) {
56 | sHandler.post(() -> {
57 | if (isCancelled()) {
58 | mIsOngoing.set(false);
59 | return;
60 | }
61 |
62 | onError(e);
63 | mIsOngoing.set(false);
64 | });
65 | }
66 | });
67 |
68 | return (T) this;
69 | }
70 |
71 | protected abstract Result doWork(Argument argument) throws Exception;
72 |
73 | protected abstract void onWorkDone(Result result);
74 |
75 | protected abstract void onError(Exception exception);
76 |
77 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/Stopwatch.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | public class Stopwatch {
4 |
5 | private long mStart;
6 |
7 | public Stopwatch() {
8 | mStart = System.currentTimeMillis();
9 | }
10 |
11 | public long millisSinceStart() {
12 | return System.currentTimeMillis() - mStart;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/TextUtils.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 |
6 | public class TextUtils {
7 |
8 | @NonNull
9 | public static String requireNonEmpty(@Nullable String s) {
10 | if (android.text.TextUtils.isEmpty(s))
11 | throw new RuntimeException("String is empty");
12 |
13 | return s;
14 | }
15 |
16 | public static boolean isEmpty(@Nullable String s) {
17 | return android.text.TextUtils.isEmpty(s);
18 | }
19 |
20 | @Nullable
21 | public static String getNullIfEmpty(@Nullable String s) {
22 | if (isEmpty(s))
23 | return null;
24 |
25 | return s;
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/TriConsumer.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils;
2 |
3 | @FunctionalInterface
4 | public interface TriConsumer {
5 | void accept(A a, T t, U u);
6 | }
7 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/saf/FileUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2006 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.aefyr.sai.utils.saf;
18 |
19 | import android.text.TextUtils;
20 |
21 | import java.nio.charset.StandardCharsets;
22 |
23 | public class FileUtils {
24 |
25 | /**
26 | * Mutate the given filename to make it valid for a FAT filesystem,
27 | * replacing any invalid characters with "_".
28 | */
29 | public static String buildValidFatFilename(String name) {
30 | if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
31 | return "(invalid)";
32 | }
33 | final StringBuilder res = new StringBuilder(name.length());
34 | for (int i = 0; i < name.length(); i++) {
35 | final char c = name.charAt(i);
36 | if (isValidFatFilenameChar(c)) {
37 | res.append(c);
38 | } else {
39 | res.append('_');
40 | }
41 | }
42 | // Even though vfat allows 255 UCS-2 chars, we might eventually write to
43 | // ext4 through a FUSE layer, so use that limit.
44 | trimFilename(res, 255);
45 | return res.toString();
46 | }
47 |
48 | private static boolean isValidFatFilenameChar(char c) {
49 | if ((0x00 <= c && c <= 0x1f)) {
50 | return false;
51 | }
52 | switch (c) {
53 | case '"':
54 | case '*':
55 | case '/':
56 | case ':':
57 | case '<':
58 | case '>':
59 | case '?':
60 | case '\\':
61 | case '|':
62 | case 0x7F:
63 | return false;
64 | default:
65 | return true;
66 | }
67 | }
68 |
69 | private static void trimFilename(StringBuilder res, int maxBytes) {
70 | byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8);
71 | if (raw.length > maxBytes) {
72 | maxBytes -= 3;
73 | while (raw.length > maxBytes) {
74 | res.deleteCharAt(res.length() / 2);
75 | raw = res.toString().getBytes(StandardCharsets.UTF_8);
76 | }
77 | res.insert(res.length() / 2, "...");
78 | }
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/aefyr/sai/utils/saf/SafUtils.java:
--------------------------------------------------------------------------------
1 | package com.aefyr.sai.utils.saf;
2 |
3 | import android.content.ContentResolver;
4 | import android.content.Context;
5 | import android.net.Uri;
6 | import android.os.ParcelFileDescriptor;
7 | import android.provider.DocumentsContract;
8 |
9 | import androidx.annotation.Nullable;
10 | import androidx.documentfile.provider.DocumentFile;
11 |
12 | import java.io.File;
13 | import java.util.List;
14 |
15 | public class SafUtils {
16 |
17 | private static final String PATH_TREE = "tree";
18 |
19 | public static String getRootForPath(Uri docUri) {
20 | String path = DocumentsContract.getTreeDocumentId(docUri);
21 |
22 | int indexOfLastColon = path.lastIndexOf(':');
23 | if (indexOfLastColon == -1)
24 | throw new IllegalArgumentException("Given uri does not contain a colon: " + docUri);
25 |
26 | return path.substring(0, indexOfLastColon);
27 | }
28 |
29 | public static String getPathWithoutRoot(Uri docUri) {
30 | String path = DocumentsContract.getTreeDocumentId(docUri);
31 |
32 | int indexOfLastColon = path.lastIndexOf(':');
33 | if (indexOfLastColon == -1)
34 | throw new IllegalArgumentException("Given uri does not contain a colon: " + docUri);
35 |
36 | return path.substring(indexOfLastColon + 1);
37 | }
38 |
39 | public static Uri buildChildDocumentUri(Uri directoryUri, String childDisplayName) {
40 | if (!isTreeUri(directoryUri))
41 | throw new IllegalArgumentException("directoryUri must be a tree uri");
42 |
43 | String rootPath = getRootForPath(directoryUri);
44 | String directoryPath = getPathWithoutRoot(directoryUri);
45 |
46 | String childPath = rootPath + ":" + directoryPath + "/" + FileUtils.buildValidFatFilename(childDisplayName);
47 |
48 | return DocumentsContract.buildDocumentUriUsingTree(directoryUri, childPath);
49 | }
50 |
51 | /**
52 | * Test if the given URI represents a {@link DocumentsContract.Document} tree.
53 | **/
54 | public static boolean isTreeUri(Uri uri) {
55 | final List paths = uri.getPathSegments();
56 | return (paths.size() >= 2 && PATH_TREE.equals(paths.get(0)));
57 | }
58 |
59 | @Nullable
60 | public static DocumentFile docFileFromSingleUriOrFileUri(Context context, Uri contentUri) {
61 | if (ContentResolver.SCHEME_FILE.equals(contentUri.getScheme())) {
62 | String path = contentUri.getPath();
63 | if (path == null)
64 | return null;
65 |
66 | File file = new File(path);
67 | if (file.isDirectory())
68 | return null;
69 |
70 | return DocumentFile.fromFile(file);
71 | } else {
72 | return DocumentFile.fromSingleUri(context, contentUri);
73 | }
74 | }
75 |
76 | @Nullable
77 | public static DocumentFile docFileFromTreeUriOrFileUri(Context context, Uri contentUri) {
78 | if (ContentResolver.SCHEME_FILE.equals(contentUri.getScheme())) {
79 | String path = contentUri.getPath();
80 | if (path == null)
81 | return null;
82 |
83 | File file = new File(path);
84 | if (!file.isDirectory())
85 | return null;
86 |
87 | return DocumentFile.fromFile(file);
88 | } else {
89 | return DocumentFile.fromTreeUri(context, contentUri);
90 | }
91 | }
92 |
93 | @Nullable
94 | public static String getFileNameFromContentUri(Context context, Uri contentUri) {
95 | DocumentFile documentFile = docFileFromSingleUriOrFileUri(context, contentUri);
96 |
97 | if (documentFile == null)
98 | return null;
99 |
100 | return documentFile.getName();
101 | }
102 |
103 | public static File parcelFdToFile(ParcelFileDescriptor fd) {
104 | return new File("/proc/self/fd/" + fd.getFd());
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/app/skydroid/FileProvider.java:
--------------------------------------------------------------------------------
1 | package app.skydroid;
2 |
3 | public class FileProvider extends androidx.core.content.FileProvider {
4 | }
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #ffffff
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
10 |
13 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.3.50'
3 | repositories {
4 | google()
5 | jcenter()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:3.5.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | jcenter()
18 | maven { url 'https://dl.bintray.com/rikkaw/Libraries' }
19 | }
20 | }
21 |
22 | rootProject.buildDir = '../build'
23 | subprojects {
24 | project.buildDir = "${rootProject.buildDir}/${project.name}"
25 | }
26 | subprojects {
27 | project.evaluationDependsOn(':app')
28 | }
29 |
30 | task clean(type: Delete) {
31 | delete rootProject.buildDir
32 | }
33 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.enableR8=true
3 | android.useAndroidX=true
4 | android.enableJetifier=true
5 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
7 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Flutter Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | include ':app'
6 |
7 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
8 | def properties = new Properties()
9 |
10 | assert localPropertiesFile.exists()
11 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
12 |
13 | def flutterSdkPath = properties.getProperty("flutter.sdk")
14 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
15 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
16 |
--------------------------------------------------------------------------------
/assets/icon/adaptive_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/assets/icon/adaptive_icon.png
--------------------------------------------------------------------------------
/assets/icon/fallback.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/assets/icon/fallback.png
--------------------------------------------------------------------------------
/assets/icon/full_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/assets/icon/full_icon.png
--------------------------------------------------------------------------------
/assets/icon/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/assets/icon/icon.png
--------------------------------------------------------------------------------
/assets/icon/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/icon/icon_without_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/assets/icon/icon_without_background.png
--------------------------------------------------------------------------------
/assets/l10n/app_si.arb:
--------------------------------------------------------------------------------
1 | {
2 | "removeNameDialogTitle": "නම ඉවත් කරන්නද?",
3 | "@removeNameDialogTitle": {},
4 | "dialogCancel": "අවලංගු",
5 | "@dialogCancel": {},
6 | "navigationAppsPageTitle": "යෙදුම්",
7 | "@navigationAppsPageTitle": {},
8 | "navigationSettingsPageTitle": "සැකසුම්",
9 | "@navigationSettingsPageTitle": {},
10 | "categoryGames": "ක්රීඩා",
11 | "@categoryGames": {}
12 | }
13 |
--------------------------------------------------------------------------------
/assets/l10n/app_zh_Hans.arb:
--------------------------------------------------------------------------------
1 | {
2 | "dialogCancel": "取消",
3 | "@dialogCancel": {},
4 | "navigationAppsPageTitle": "应用",
5 | "@navigationAppsPageTitle": {},
6 | "navigationSettingsPageTitle": "设置",
7 | "@navigationSettingsPageTitle": {},
8 | "filterDialogTitle": "按分类过滤",
9 | "@filterDialogTitle": {},
10 | "categoryDevelopment": "开发",
11 | "@categoryDevelopment": {},
12 | "categoryGames": "游戏",
13 | "@categoryGames": {},
14 | "categoryMultimedia": "多媒体",
15 | "@categoryMultimedia": {},
16 | "categoryNavigation": "导航",
17 | "@categoryNavigation": {},
18 | "categoryReading": "阅读",
19 | "@categoryReading": {},
20 | "categoryScienceAndEducation": "科教",
21 | "@categoryScienceAndEducation": {},
22 | "categorySecurity": "安全",
23 | "@categorySecurity": {},
24 | "categorySportsAndHealth": "运动与健康",
25 | "@categorySportsAndHealth": {},
26 | "categorySystem": "系统",
27 | "@categorySystem": {},
28 | "categoryTime": "时间",
29 | "@categoryTime": {},
30 | "categoryWriting": "写作",
31 | "@categoryWriting": {},
32 | "appListLoadingMetadata": "正在加载元数据……",
33 | "@appListLoadingMetadata": {},
34 | "errorsSheetTitle": "错误",
35 | "@errorsSheetTitle": {},
36 | "addDomainNameDialogTitle": "添加应用或收藏集",
37 | "@addDomainNameDialogTitle": {},
38 | "removeNameDialogConfirm": "移除",
39 | "@removeNameDialogConfirm": {},
40 | "navigationCollectionsPageTitle": "收藏集",
41 | "@navigationCollectionsPageTitle": {},
42 | "appListPageEmptyWarning": "唔……什么也没有。试试点击右下角“+”号按钮添加名称。",
43 | "@appListPageEmptyWarning": {},
44 | "removeNameDialogTitle": "删除名字?",
45 | "@removeNameDialogTitle": {},
46 | "categoryConnectivity": "连接性",
47 | "@categoryConnectivity": {},
48 | "categoryGraphics": "图形",
49 | "@categoryGraphics": {},
50 | "navigationDiscoverPageTitle": "发现",
51 | "@navigationDiscoverPageTitle": {},
52 | "removeNameDialogContent": "你真的想从你的应用程序列表中删除\"{name}\"这个名字吗?这个动作并不触及你所安装的应用程序,而且这个名字可以通过一个集合再次被添加。",
53 | "@removeNameDialogContent": {
54 | "placeholders": {
55 | "name": {}
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | *.mode1v3
2 | *.mode2v3
3 | *.moved-aside
4 | *.pbxuser
5 | *.perspectivev3
6 | **/*sync/
7 | .sconsign.dblite
8 | .tags*
9 | **/.vagrant/
10 | **/DerivedData/
11 | Icon?
12 | **/Pods/
13 | **/.symlinks/
14 | profile
15 | xcuserdata
16 | **/.generated/
17 | Flutter/App.framework
18 | Flutter/Flutter.framework
19 | Flutter/Flutter.podspec
20 | Flutter/Generated.xcconfig
21 | Flutter/app.flx
22 | Flutter/app.zip
23 | Flutter/flutter_assets/
24 | Flutter/flutter_export_environment.sh
25 | ServiceDefinitions.json
26 | Runner/GeneratedPluginRegistrant.*
27 |
28 | # Exceptions to above rules.
29 | !default.mode1v3
30 | !default.mode2v3
31 | !default.pbxuser
32 | !default.perspectivev3
33 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 8.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | skydroid
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | $(FLUTTER_BUILD_NAME)
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(FLUTTER_BUILD_NUMBER)
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIMainStoryboardFile
28 | Main
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 | UIInterfaceOrientationLandscapeLeft
33 | UIInterfaceOrientationLandscapeRight
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UIViewControllerBasedStatusBarAppearance
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/l10n.yaml:
--------------------------------------------------------------------------------
1 | arb-dir: assets/l10n
2 | template-arb-file: app_en.arb
3 | output-localization-file: translations.dart
4 | output-class: Translations
--------------------------------------------------------------------------------
/lib/app.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:ui';
3 |
4 | import 'package:device_info/device_info.dart';
5 | import 'package:flutter/foundation.dart';
6 | import 'package:flutter/material.dart';
7 | import 'package:hive/hive.dart';
8 |
9 | import 'package:flutter_gen/gen_l10n/translations.dart';
10 | import 'package:logger/logger.dart';
11 | import 'package:preferences/preference_service.dart';
12 | import 'package:skydroid/model/app.dart';
13 | import 'package:skydroid/model/collection.dart';
14 |
15 | export 'package:skydroid/model/app.dart';
16 | export 'package:skydroid/util.dart';
17 |
18 | final logger = Logger(level: kDebugMode ? Level.debug : Level.warning);
19 |
20 | Box names;
21 | Box apps;
22 |
23 | Box collectionNames;
24 | Box collections;
25 |
26 | Box localVersionCodes;
27 |
28 | Box apkCacheTimes;
29 |
30 | Locale locale;
31 |
32 | List preferredLocales;
33 |
34 | List languagePreferences;
35 |
36 | const apkCacheDuration = Duration(days: 7);
37 |
38 | final globalErrorStream = StreamController();
39 | Map> globalErrors = {};
40 |
41 | Translations tr;
42 | AndroidDeviceInfo androidInfo;
43 |
44 | final selectedNames = {};
45 |
46 | TextStyle dialogActionTextStyle(BuildContext context) => TextStyle(
47 | color: Theme.of(context).accentColor,
48 | );
49 |
50 | bool get isShizukuEnabled => PrefService.getBool('use_shizuku') ?? false;
51 |
52 | const shizukuPackageName = 'moe.shizuku.privileged.api';
53 |
54 | void addError(
55 | dynamic exception,
56 | dynamic ctx,
57 | ) {
58 | final e = exception.toString();
59 |
60 | if (!globalErrors.containsKey(e)) {
61 | globalErrors[e] = [];
62 | }
63 | globalErrors[e].add(ctx.toString());
64 |
65 | globalErrorStream.add(null);
66 | }
67 |
68 | final categoryKeys = {
69 | 'Connectivity': () => tr.categoryConnectivity,
70 | 'Development': () => tr.categoryDevelopment,
71 | 'Games': () => tr.categoryGames,
72 | 'Graphics': () => tr.categoryGraphics,
73 | 'Internet': () => tr.categoryInternet,
74 | 'Money': () => tr.categoryMoney,
75 | 'Multimedia': () => tr.categoryMultimedia,
76 | 'Navigation': () => tr.categoryNavigation,
77 | 'Phone & SMS': () => tr.categoryPhoneAndSMS,
78 | 'Reading': () => tr.categoryReading,
79 | 'Science & Education': () => tr.categoryScienceAndEducation,
80 | 'Security': () => tr.categorySecurity,
81 | 'Sports & Health': () => tr.categorySportsAndHealth,
82 | 'System': () => tr.categorySystem,
83 | 'Theming': () => tr.categoryTheming,
84 | 'Time': () => tr.categoryTime,
85 | 'Writing': () => tr.categoryWriting,
86 | };
87 |
88 | String translateCategoryName(String category) {
89 | if (categoryKeys.containsKey(category)) {
90 | return categoryKeys[category]();
91 | }
92 | return category;
93 | }
94 |
--------------------------------------------------------------------------------
/lib/data/categories.dart:
--------------------------------------------------------------------------------
1 | const existingCategories = [
2 | 'Connectivity',
3 | 'Development',
4 | 'Games',
5 | 'Graphics',
6 | 'Internet',
7 | 'Money',
8 | 'Multimedia',
9 | 'Navigation',
10 | 'Phone & SMS',
11 | 'Reading',
12 | 'Science & Education',
13 | 'Security',
14 | 'Sports & Health',
15 | 'System',
16 | 'Theming',
17 | 'Time',
18 | 'Writing',
19 | ];
20 |
--------------------------------------------------------------------------------
/lib/model/collection.dart:
--------------------------------------------------------------------------------
1 | import 'package:hive/hive.dart';
2 | import 'package:json_annotation/json_annotation.dart';
3 |
4 | part 'collection.g.dart';
5 |
6 | @HiveType(typeId: 4)
7 | @JsonSerializable(anyMap: true)
8 | class Collection {
9 | @HiveField(1)
10 | String title;
11 | @HiveField(2)
12 | String description;
13 | @HiveField(3)
14 | String icon;
15 |
16 | @HiveField(4)
17 | List apps;
18 |
19 | @HiveField(250)
20 | String srcHash;
21 |
22 | Collection();
23 |
24 | factory Collection.fromJson(Map json) =>
25 | _$CollectionFromJson(json);
26 |
27 | Map toJson() => _$CollectionToJson(this);
28 | }
29 |
30 | @HiveType(typeId: 5)
31 | @JsonSerializable(anyMap: true)
32 | class AppReference {
33 | @HiveField(1)
34 | String name;
35 | @HiveField(2)
36 | List verifiedMetadataHashes;
37 |
38 | AppReference();
39 |
40 | factory AppReference.fromJson(Map json) =>
41 | _$AppReferenceFromJson(json);
42 |
43 | Map toJson() => _$AppReferenceToJson(this);
44 | }
45 |
--------------------------------------------------------------------------------
/lib/model/collection.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'collection.dart';
4 |
5 | // **************************************************************************
6 | // TypeAdapterGenerator
7 | // **************************************************************************
8 |
9 | class CollectionAdapter extends TypeAdapter {
10 | @override
11 | final int typeId = 4;
12 |
13 | @override
14 | Collection read(BinaryReader reader) {
15 | final numOfFields = reader.readByte();
16 | final fields = {
17 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
18 | };
19 | return Collection()
20 | ..title = fields[1] as String
21 | ..description = fields[2] as String
22 | ..icon = fields[3] as String
23 | ..apps = (fields[4] as List)?.cast()
24 | ..srcHash = fields[250] as String;
25 | }
26 |
27 | @override
28 | void write(BinaryWriter writer, Collection obj) {
29 | writer
30 | ..writeByte(5)
31 | ..writeByte(1)
32 | ..write(obj.title)
33 | ..writeByte(2)
34 | ..write(obj.description)
35 | ..writeByte(3)
36 | ..write(obj.icon)
37 | ..writeByte(4)
38 | ..write(obj.apps)
39 | ..writeByte(250)
40 | ..write(obj.srcHash);
41 | }
42 |
43 | @override
44 | int get hashCode => typeId.hashCode;
45 |
46 | @override
47 | bool operator ==(Object other) =>
48 | identical(this, other) ||
49 | other is CollectionAdapter &&
50 | runtimeType == other.runtimeType &&
51 | typeId == other.typeId;
52 | }
53 |
54 | class AppReferenceAdapter extends TypeAdapter {
55 | @override
56 | final int typeId = 5;
57 |
58 | @override
59 | AppReference read(BinaryReader reader) {
60 | final numOfFields = reader.readByte();
61 | final fields = {
62 | for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
63 | };
64 | return AppReference()
65 | ..name = fields[1] as String
66 | ..verifiedMetadataHashes = (fields[2] as List)?.cast();
67 | }
68 |
69 | @override
70 | void write(BinaryWriter writer, AppReference obj) {
71 | writer
72 | ..writeByte(2)
73 | ..writeByte(1)
74 | ..write(obj.name)
75 | ..writeByte(2)
76 | ..write(obj.verifiedMetadataHashes);
77 | }
78 |
79 | @override
80 | int get hashCode => typeId.hashCode;
81 |
82 | @override
83 | bool operator ==(Object other) =>
84 | identical(this, other) ||
85 | other is AppReferenceAdapter &&
86 | runtimeType == other.runtimeType &&
87 | typeId == other.typeId;
88 | }
89 |
90 | // **************************************************************************
91 | // JsonSerializableGenerator
92 | // **************************************************************************
93 |
94 | Collection _$CollectionFromJson(Map json) {
95 | return Collection()
96 | ..title = json['title'] as String
97 | ..description = json['description'] as String
98 | ..icon = json['icon'] as String
99 | ..apps = (json['apps'] as List)
100 | ?.map((e) => e == null ? null : AppReference.fromJson(e as Map))
101 | ?.toList()
102 | ..srcHash = json['srcHash'] as String;
103 | }
104 |
105 | Map _$CollectionToJson(Collection instance) =>
106 | {
107 | 'title': instance.title,
108 | 'description': instance.description,
109 | 'icon': instance.icon,
110 | 'apps': instance.apps,
111 | 'srcHash': instance.srcHash,
112 | };
113 |
114 | AppReference _$AppReferenceFromJson(Map json) {
115 | return AppReference()
116 | ..name = json['name'] as String
117 | ..verifiedMetadataHashes = (json['verifiedMetadataHashes'] as List)
118 | ?.map((e) => e as String)
119 | ?.toList();
120 | }
121 |
122 | Map _$AppReferenceToJson(AppReference instance) =>
123 | {
124 | 'name': instance.name,
125 | 'verifiedMetadataHashes': instance.verifiedMetadataHashes,
126 | };
127 |
--------------------------------------------------------------------------------
/lib/theme.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:preferences/preferences.dart';
3 | import 'package:skydroid/app.dart';
4 |
5 | typedef ThemedWidgetBuilder = Widget Function(
6 | BuildContext context, ThemeData data);
7 |
8 | typedef ThemeDataWithBrightnessBuilder = ThemeData Function(String theme);
9 |
10 | class AppTheme extends StatefulWidget {
11 | const AppTheme({Key key, this.data, this.themedWidgetBuilder})
12 | : super(key: key);
13 |
14 | final ThemedWidgetBuilder themedWidgetBuilder;
15 | final ThemeDataWithBrightnessBuilder data;
16 |
17 | @override
18 | AppThemeState createState() => AppThemeState();
19 |
20 | static AppThemeState of(BuildContext context) {
21 | return context.findAncestorStateOfType();
22 | }
23 | }
24 |
25 | class AppThemeState extends State {
26 | ThemeData _data;
27 |
28 | String _theme;
29 |
30 | ThemeData get data => _data;
31 |
32 | @override
33 | void initState() {
34 | super.initState();
35 |
36 | _theme = loadTheme();
37 | _data = widget.data(_theme);
38 |
39 | if (mounted) {
40 | setState(() {});
41 | }
42 | }
43 |
44 | @override
45 | void didChangeDependencies() {
46 | super.didChangeDependencies();
47 | _data = widget.data(_theme);
48 | }
49 |
50 | @override
51 | void didUpdateWidget(AppTheme oldWidget) {
52 | super.didUpdateWidget(oldWidget);
53 | _data = widget.data(_theme);
54 | }
55 |
56 | void setTheme(String theme) {
57 | setState(() {
58 | _data = widget.data(theme);
59 | _theme = theme;
60 | });
61 | }
62 |
63 | void setThemeData(ThemeData data) {
64 | setState(() {
65 | _data = data;
66 | });
67 | }
68 |
69 | String loadTheme() {
70 | return PrefService.getString('theme');
71 | }
72 |
73 | @override
74 | Widget build(BuildContext context) {
75 | return widget.themedWidgetBuilder(context, _data);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/lib/util.dart:
--------------------------------------------------------------------------------
1 | //const dnsUrl = 'https://easyhandshake.com:8053';
2 | import 'package:preferences/preferences.dart';
3 |
4 | String get dnsUrl => PrefService.getString('dnsUrl');
5 | String get skynetPortal => PrefService.getString('skynetPortal');
6 |
7 | String resolveLink(String apkLink) {
8 | if (apkLink.startsWith('sia://'))
9 | apkLink = skynetPortal + '/' + apkLink.substring(6);
10 |
11 | return apkLink;
12 | }
13 |
--------------------------------------------------------------------------------
/minimal-app-template.yaml:
--------------------------------------------------------------------------------
1 | name: Your app's name
2 | authorName: Your name
3 | packageName: Your app's android package name
4 |
5 | icon: # The link to your app's icon (https:// or sia://)
6 |
7 | localized:
8 | en-US:
9 | description: |-
10 | your
11 | app
12 | description
13 | summary: A short summary of your app
14 |
15 | builds:
16 | - versionName: 0.1.0
17 | versionCode: 1
18 | sha256: # The sha256 hash of your APK file
19 | apkLink: # The link to your APK File (https:// or sia://)
20 |
21 |
22 | # Change the version name and code to match your app
23 | currentVersionName: 0.1.0
24 | currentVersionCode: 1
25 |
26 | added: # Timestamp when the app was created (Unix Timestamp in Milliseconds)
27 | lastUpdated: # Timestamp when this file was last edited (Unix Timestamp in Milliseconds)
28 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: skydroid
2 | description: A decentralized domain-based App Store for Android.
3 |
4 | publish_to: "none"
5 |
6 | version: 0.5.5+55
7 |
8 | environment:
9 | sdk: ">=2.7.0 <3.0.0"
10 |
11 | dependencies:
12 | flutter:
13 | sdk: flutter
14 | # Internationalization support.
15 | flutter_localizations:
16 | sdk: flutter
17 | http: ^0.12.2
18 | path_provider: ^1.6.11
19 | permission_handler: ^5.0.1+1
20 | hive: ^1.4.4+1
21 | hive_flutter: ^0.3.1
22 | yaml: ^2.2.1
23 | crypto: ^2.1.4
24 | cached_network_image: ^2.2.0+1
25 | flutter_html: ^1.2.0
26 | markdown: ^2.1.5
27 | flutter_animation_progress_bar: ^1.0.0
28 | filesize: ^1.0.4
29 | device_apps: ^1.2.0
30 | url_launcher: ^5.5.0
31 | share: ^0.6.4+3
32 | material_design_icons_flutter: ^4.0.5345
33 | preferences: ^5.2.0
34 | json_annotation: ^3.0.1
35 | time_ago_provider: ^2.0.5
36 | flutter_device_locale: ^0.4.0
37 | package_info: ^0.4.1
38 | uni_links: ^0.4.0
39 |
40 | # The following adds the Cupertino Icons font to your application.
41 | # Use with the CupertinoIcons class for iOS style icons.
42 | cupertino_icons: ^1.0.0
43 | convert: ^2.1.1
44 | system_info: ^0.1.3
45 | device_info: ^1.0.0
46 | pool: ^1.4.0
47 | path: ^1.7.0
48 | logger: ^0.9.4
49 |
50 | dev_dependencies:
51 | flutter_test:
52 | sdk: flutter
53 | json_serializable: ^3.5.1
54 | hive_generator: ^0.8.2
55 | build_runner: ^1.11.1
56 | flutter_launcher_icons: ^0.7.3
57 |
58 | flutter_icons:
59 | android: true
60 | ios: false
61 | image_path: "assets/icon/icon.png"
62 | adaptive_icon_background: "#ffffff"
63 | adaptive_icon_foreground: "assets/icon/adaptive_icon.png"
64 |
65 | # For information on the generic Dart part of this file, see the
66 | # following page: https://dart.dev/tools/pub/pubspec
67 | # The following section is specific to Flutter.
68 | flutter:
69 | generate: true
70 | # The following line ensures that the Material Icons font is
71 | # included with your application, so that you can use the icons in
72 | # the material Icons class.
73 | uses-material-design: true
74 | # To add assets to your application, add an assets section, like this:
75 | assets:
76 | - assets/icon/fallback.png
77 | - assets/icon/icon.png
78 | # - images/a_dot_ham.jpeg
79 | # An image asset can refer to one or more resolution-specific "variants", see
80 | # https://flutter.dev/assets-and-images/#resolution-aware.
81 | # For details regarding adding assets from package dependencies, see
82 | # https://flutter.dev/assets-and-images/#from-packages
83 | # To add custom fonts to your application, add a fonts section here,
84 | # in this "flutter" section. Each entry in this list should have a
85 | # "family" key with the font family name, and a "fonts" key with a
86 | # list giving the asset and other descriptors for the font. For
87 | # example:
88 | # fonts:
89 | # - family: Schyler
90 | # fonts:
91 | # - asset: fonts/Schyler-Regular.ttf
92 | # - asset: fonts/Schyler-Italic.ttf
93 | # style: italic
94 | # - family: Trajan Pro
95 | # fonts:
96 | # - asset: fonts/TrajanPro.ttf
97 | # - asset: fonts/TrajanPro_Bold.ttf
98 | # weight: 700
99 | #
100 | # For details regarding fonts from package dependencies,
101 | # see https://flutter.dev/custom-fonts/#from-packages
102 |
--------------------------------------------------------------------------------
/screenshots/screen1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/screenshots/screen1.jpg
--------------------------------------------------------------------------------
/screenshots/screen2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/screenshots/screen2.jpg
--------------------------------------------------------------------------------
/screenshots/screen3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redsolver/skydroid/f31d36c1eaee38dfd282f6114ba990af1dbf7f8b/screenshots/screen3.jpg
--------------------------------------------------------------------------------
/skydroid-app.yaml:
--------------------------------------------------------------------------------
1 | categories:
2 | - System
3 | - Internet
4 | license: GPL-3.0-only
5 | authorName: redsolver
6 | authorEmail: info@redsolver.net
7 | sourceCode: https://github.com/redsolver/skydroid
8 | webSite: https://skydroid.app
9 | issueTracker: https://github.com/redsolver/skydroid/issues
10 | changelog: https://github.com/redsolver/skydroid/blob/HEAD/CHANGELOG.md
11 |
12 | name: SkyDroid
13 | packageName: app.skydroid
14 |
15 | icon: sia://EAAs1qVATliYr04Iahv1ROc7OJW1zXxd0lY5wNfXgOfzmg
16 |
17 | localized:
18 | en-US:
19 | description: |-
20 | Android App Store which offers fully decentralized, secure and fast app distribution.
21 | Handshake is used as a trusted way to offer apps through a specific name/domain.
22 | Sia Skynet is used as hosting infrastructure for App Metadata, APKs, screenshots and other media content.
23 | summary: A decentralized domain-based App Store for Android.
24 | whatsNew: |-
25 | • Added new languages and updated translations (Thanks to all contributors!)
26 | • Added new URI scheme to make app links more reliable
27 | • Small improvements and bugfixes
28 |
29 | builds:
30 | - versionName: 0.5.5
31 | versionCode: 55
32 | sha256: 93265676dc14d8ee89a4db5124505ee106575b2c2c9010fbbfced217ea386480
33 | apkLink: sia://AABCNokpXSqzkRo4v8PvOU5R2PteygHex3-PK0NzVqN7fA
34 | - versionName: 0.5.4
35 | versionCode: 54
36 | sha256: 282ba6b4b68a061a16efd896e76d2ef7330bc2e8793cc082ddd2b2ae4cbbfd5b
37 | apkLink: sia://AAB8LG7cx_JxFC1YkTheCzQCwppl03P3MCI069XiehgrwA
38 | - versionName: 0.5.3
39 | versionCode: 53
40 | sha256: 016923b6247f9be6c9baf93541333a77677f5ff0b36220763fe93ceef60a0250
41 | apkLink: sia://AABQKroBHN8arO6Nuk_ZH3_6NEQhtKPBKWMIIEGfT-zusA
42 | - versionName: 0.5.2
43 | versionCode: 52
44 | sha256: 696af6220c3956661a7950d450931ff77d2c854fb4c95065bed28fe8f804c0d2
45 | apkLink: sia://AAAi3-epI0OPXrDJ49bWbO9XO108RJ0ruhtkO3UmzIuI1g
46 | - versionName: 0.5.1
47 | versionCode: 51
48 | sha256: 61befb9f1c686bddc05d80a7aa047ed51db9742c48aa4c3e577977849c8983af
49 | apkLink: sia://AABlxC73HnG-847LIpenlXzSEhfOd27IgaAmTQpm5plzwg
50 | - versionName: 0.5.0
51 | versionCode: 50
52 | sha256: c2c7d61783256d32853df8decf640f96aef3ff9082bfd6cd2b337dc3453df9f0
53 | apkLink: sia://AAAbL_PbcdL6VbFT0D4QNrZPmTvNFIUPr8a_cjX4HN9IXw
54 | - versionName: 0.4.1
55 | versionCode: 41
56 | sha256: aeddeb4d28e84f19f84fbef05cedc4cdc760bdaf8df22247bb8a6610c0134309
57 | apkLink: sia://AAC5d6VZljrYFKEKO1bELlZhP0aK7VpnBweSwfok2r2omw
58 | - versionName: 0.4.0
59 | versionCode: 40
60 | sha256: 4ee2259733f79f2dafc6aac8a6aea7b0ad3a00c045272c93edc8d1f825f669d6
61 | apkLink: sia://AAANGZVGJM2dqEk1Au4qJJuoKf8opjNqfEZIOPlfpmHSkw
62 | - versionName: 0.3.0
63 | versionCode: 30
64 | sha256: 85fda8ce9a8ce6f61636049efacfc630b9b248fb95ab330f3cca11ebfc0f80be
65 | apkLink: sia://AAA-ytmdHnnK1KPvbW9FZlQ-8H9eSrRq9hR9xV_gu_b4lA
66 | - versionName: 0.2.4
67 | versionCode: 24
68 | sha256: c29efdb26132b1518d84637a663a13470f36d6f9bb1780a2bc55f790a8e0d4f8
69 | apkLink: sia://AABQLWvu9r1fpYXlWgNfOkadS5ISPrqOu_iGxJPTYOvCTw
70 | - versionName: 0.2.3
71 | versionCode: 23
72 | sha256: 02f9cc1053679d234cbccaa53e7b5582162ad418aa0a9a32181465887e0730e6
73 | apkLink: sia://AAC9rBIFryCJxokyjfvkLOtCnnfjO2yVjugufyMBeO8ypw
74 | - versionName: 0.2.2
75 | versionCode: 22
76 | sha256: ba0ffc0a1c0a1d483b41170fefd1dbf6f8f0bbeabeb2e6fd6140ae756c07ae36
77 | apkLink: sia://AACA-2JmD91FTbWk8dkhEb19IXYcykebULbdgeX07U4FXw
78 | - versionName: 0.2.1
79 | versionCode: 21
80 | sha256: 73c98cbba4d71a3c57a18d99e7817fdfc56b256466e0f40f3a634d157b0b7f9b
81 | apkLink: sia://AACk0b0iZT7bwuyMCoHRiwAQeodkYo2wCXqb_wcpM2_f9A
82 | - versionName: 0.2.0
83 | versionCode: 20
84 | sha256: 9bd9948dce16fcf75b6fff6f904e7ec910ada124e2e3217eeed29c4126a1a2a6
85 | apkLink: sia://AADYQcbtJmNWoeRx5ROkn2B7uxKQ-lRzqBiYo0I_YHt7kQ
86 | - versionName: 0.1.1
87 | versionCode: 2
88 | sha256: ce610a6919af351d2af7e9411a3d858ee22778d06cbbdb222793abfe85671df1
89 | apkLink: sia://AADACub5UCU3d6MUDOMS6gnkoz8OVORv54JnOhlQ5TMGAw
90 | - versionName: 0.1.2
91 | versionCode: 3
92 | sha256: 5fc6ec63941e456a7beb0fd4b3b6b09c44c9c6b749e55f480dcb01b5b8bd4894
93 | apkLink: sia://AADwnYcRqTKZEzCQyi07o8GxOQkX_eHvgpvsAaFpS6dbXw
94 | - versionName: 0.1.3
95 | versionCode: 4
96 | sha256: 7e44fa762998692150aea1007c3f50b28f4259fe98ce60fa40d39e9c2f46b616
97 | apkLink: sia://AAB4EJsNJQ3dDjkXKEYpEIX6W5jJ-omj_GM2wQZhGUIgZQ
98 | - versionName: 0.1.4
99 | versionCode: 5
100 | sha256: c62c2d73c0ead2fb1a287eecc6d637c318cb49b8c1543bbd2110d73b78ee7c6a
101 | apkLink: sia://AADzaC8YfqVlUAcSf8-1PdT9mLDb9KRLeV9Pi8y3l8G2KA
102 |
103 | currentVersionName: 0.5.5
104 | currentVersionCode: 55
105 |
106 | added: 1597353106000
107 | lastUpdated: 1620160465735
108 |
--------------------------------------------------------------------------------
/skydroid-dev.yaml:
--------------------------------------------------------------------------------
1 | name: skydroid.app
2 | metadataFile: skydroid-app.yaml
3 | skynetPortal: https://siasky.net
4 |
5 | checkVersion:
6 | file: pubspec.yaml
7 | versionCode: 'version:\s.+\+(\d+)'
8 | versionName: 'version:\s(.+)\+'
9 |
10 | uploadBuild:
11 | file: build/app/outputs/flutter-apk/app-release.apk
12 |
13 | updateMetadataFile:
14 | updateWhatsNew: false
15 |
16 | # uploadMetadata:
17 |
--------------------------------------------------------------------------------
/test/widget_test.dart:
--------------------------------------------------------------------------------
1 | /* // This is a basic Flutter widget test.
2 | //
3 | // To perform an interaction with a widget in your test, use the WidgetTester
4 | // utility that Flutter provides. For example, you can send tap and scroll
5 | // gestures. You can also use WidgetTester to find child widgets in the widget
6 | // tree, read text, and verify that the values of widget properties are correct.
7 |
8 | import 'package:flutter/material.dart';
9 | import 'package:flutter_test/flutter_test.dart';
10 |
11 | import 'package:skydroid/main.dart';
12 |
13 | void main() {
14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async {
15 | // Build our app and trigger a frame.
16 | await tester.pumpWidget(MyApp());
17 |
18 | // Verify that our counter starts at 0.
19 | expect(find.text('0'), findsOneWidget);
20 | expect(find.text('1'), findsNothing);
21 |
22 | // Tap the '+' icon and trigger a frame.
23 | await tester.tap(find.byIcon(Icons.add));
24 | await tester.pump();
25 |
26 | // Verify that our counter has incremented.
27 | expect(find.text('0'), findsNothing);
28 | expect(find.text('1'), findsOneWidget);
29 | });
30 | }
31 | */
--------------------------------------------------------------------------------