├── .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 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 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | */ --------------------------------------------------------------------------------