├── .gitignore ├── InstallerX锁定器_1.3.apk ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── schemas │ └── com.rosan.installer.data.settings.model.room.InstallerRoom │ │ ├── 1.json │ │ ├── 2.json │ │ ├── 3.json │ │ └── 4.json └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ ├── android │ │ └── content │ │ │ ├── IIntentReceiver.aidl │ │ │ └── IIntentSender.aidl │ └── com │ │ └── rosan │ │ └── installer │ │ ├── IAppProcessService.aidl │ │ ├── IDhizukuUserService.aidl │ │ ├── IPrivilegedService.aidl │ │ └── IShizukuUserService.aidl │ ├── java │ └── com │ │ └── rosan │ │ └── installer │ │ ├── App.kt │ │ ├── CrashHandler.kt │ │ ├── build │ │ ├── Level.kt │ │ └── RsConfig.java │ │ ├── data │ │ ├── app │ │ │ ├── model │ │ │ │ ├── entity │ │ │ │ │ ├── AnalyseExtraEntity.kt │ │ │ │ │ ├── AppEntity.kt │ │ │ │ │ ├── DataEntity.kt │ │ │ │ │ ├── InstallEntity.kt │ │ │ │ │ └── InstallExtraEntity.kt │ │ │ │ ├── exception │ │ │ │ │ ├── InstallFailedAlreadyExistsException.kt │ │ │ │ │ ├── InstallFailedConflictingProviderException.kt │ │ │ │ │ ├── InstallFailedContainerErrorException.kt │ │ │ │ │ ├── InstallFailedCpuAbiIncompatibleException.kt │ │ │ │ │ ├── InstallFailedDexOptException.kt │ │ │ │ │ ├── InstallFailedDuplicatePackageException.kt │ │ │ │ │ ├── InstallFailedInsufficientStorageException.kt │ │ │ │ │ ├── InstallFailedInvalidAPKException.kt │ │ │ │ │ ├── InstallFailedInvalidInstallLocationException.kt │ │ │ │ │ ├── InstallFailedInvalidURIException.kt │ │ │ │ │ ├── InstallFailedMediaUnavailableException.kt │ │ │ │ │ ├── InstallFailedMissingFeatureException.kt │ │ │ │ │ ├── InstallFailedMissingSharedLibraryException.kt │ │ │ │ │ ├── InstallFailedNewerSDKException.kt │ │ │ │ │ ├── InstallFailedNoSharedUserException.kt │ │ │ │ │ ├── InstallFailedOlderSdkException.kt │ │ │ │ │ ├── InstallFailedPackageChangedException.kt │ │ │ │ │ ├── InstallFailedRejectedByBuildTypeException.kt │ │ │ │ │ ├── InstallFailedReplaceCouldntDeleteException.kt │ │ │ │ │ ├── InstallFailedSharedUserIncompatibleException.kt │ │ │ │ │ ├── InstallFailedTestOnlyException.kt │ │ │ │ │ ├── InstallFailedUidChangedException.kt │ │ │ │ │ ├── InstallFailedUpdateIncompatibleException.kt │ │ │ │ │ ├── InstallFailedVerificationFailureException.kt │ │ │ │ │ ├── InstallFailedVerificationTimeoutException.kt │ │ │ │ │ └── InstallFailedVersionDowngradeException.kt │ │ │ │ └── impl │ │ │ │ │ ├── AnalyserRepoImpl.kt │ │ │ │ │ ├── DSRepoImpl.kt │ │ │ │ │ ├── InstallerRepoImpl.kt │ │ │ │ │ ├── analyser │ │ │ │ │ ├── ApkAnalyserRepoImpl.kt │ │ │ │ │ ├── ApkMAnalyserRepoImpl.kt │ │ │ │ │ ├── ApksAnalyserRepoImpl.kt │ │ │ │ │ └── XApkAnalyserRepoImpl.kt │ │ │ │ │ └── installer │ │ │ │ │ ├── DhizukuInstallerRepoImpl.kt │ │ │ │ │ ├── IBinderInstallerRepoImpl.kt │ │ │ │ │ ├── NoneInstallerRepoImpl.kt │ │ │ │ │ ├── ProcessInstallerRepoImpl.kt │ │ │ │ │ └── ShizukuInstallerRepoImpl.kt │ │ │ ├── repo │ │ │ │ ├── AnalyserRepo.kt │ │ │ │ ├── DefaultSetterRepo.kt │ │ │ │ └── InstallerRepo.kt │ │ │ └── util │ │ │ │ ├── AppEntityInfo.kt │ │ │ │ ├── DataType.kt │ │ │ │ ├── InstallEntityUtil.kt │ │ │ │ ├── InstallFlag.kt │ │ │ │ ├── InstalledAppInfo.kt │ │ │ │ ├── LocalIntentReceiverProxy.kt │ │ │ │ ├── PackageInstallerUtil.kt │ │ │ │ └── PackageManagerUtil.kt │ │ ├── common │ │ │ └── util │ │ │ │ ├── IntUtil.kt │ │ │ │ ├── MutableCollection.kt │ │ │ │ └── PackageManagerUtil.kt │ │ ├── installer │ │ │ ├── model │ │ │ │ ├── entity │ │ │ │ │ ├── ProgressEntity.kt │ │ │ │ │ └── SelectInstallEntity.kt │ │ │ │ ├── exception │ │ │ │ │ └── ResolveException.kt │ │ │ │ └── impl │ │ │ │ │ ├── InstallerRepoImpl.kt │ │ │ │ │ ├── InstallerService.kt │ │ │ │ │ └── installer │ │ │ │ │ ├── ActionHandler.kt │ │ │ │ │ ├── BroadcastHandler.kt │ │ │ │ │ ├── ForegroundInfoHandler.kt │ │ │ │ │ ├── Handler.kt │ │ │ │ │ └── ProgressHandler.kt │ │ │ ├── repo │ │ │ │ └── InstallerRepo.kt │ │ │ └── util │ │ │ │ └── PendingIntent.kt │ │ ├── recycle │ │ │ ├── model │ │ │ │ ├── entity │ │ │ │ │ ├── BasePrivilegedService.kt │ │ │ │ │ ├── DefaultPrivilegedService.kt │ │ │ │ │ └── DhizukuPrivilegedService.kt │ │ │ │ ├── exception │ │ │ │ │ ├── AppProcessNotWorkException.kt │ │ │ │ │ ├── DhizukuNotWorkException.kt │ │ │ │ │ ├── RootNotWorkException.kt │ │ │ │ │ └── ShizukuNotWorkException.kt │ │ │ │ └── impl │ │ │ │ │ ├── AppProcessRecycler.kt │ │ │ │ │ ├── AppProcessRecyclers.kt │ │ │ │ │ ├── DhizukuUserServiceRecycler.kt │ │ │ │ │ ├── ProcessUserServiceRecycler.kt │ │ │ │ │ ├── ProcessUserServiceRecyclers.kt │ │ │ │ │ └── ShizukuUserServiceRecycler.kt │ │ │ ├── repo │ │ │ │ ├── Recyclable.kt │ │ │ │ ├── Recycler.kt │ │ │ │ └── recyclable │ │ │ │ │ └── UserService.kt │ │ │ └── util │ │ │ │ ├── DhizukuUtil.kt │ │ │ │ ├── PrivilegedUtil.kt │ │ │ │ ├── ShizukuUtil.kt │ │ │ │ └── UserServiceUtil.kt │ │ ├── reflect │ │ │ ├── model │ │ │ │ └── impl │ │ │ │ │ └── ReflectRepoImpl.kt │ │ │ └── repo │ │ │ │ └── ReflectRepo.kt │ │ ├── res │ │ │ ├── model │ │ │ │ ├── entity │ │ │ │ │ ├── Entry.kt │ │ │ │ │ ├── EntryEntity.kt │ │ │ │ │ ├── MapEntryEntity.kt │ │ │ │ │ └── ValueEntity.kt │ │ │ │ └── impl │ │ │ │ │ └── AxmlTreeRepoImpl.kt │ │ │ └── repo │ │ │ │ ├── ArscRepo.kt │ │ │ │ ├── AxmlPullRepo.kt │ │ │ │ └── AxmlTreeRepo.kt │ │ └── settings │ │ │ ├── model │ │ │ └── room │ │ │ │ ├── InstallerRoom.kt │ │ │ │ ├── dao │ │ │ │ ├── AppDao.kt │ │ │ │ └── ConfigDao.kt │ │ │ │ ├── entity │ │ │ │ ├── AppEntity.kt │ │ │ │ ├── ConfigEntity.kt │ │ │ │ └── converter │ │ │ │ │ ├── AnalyserConverter.kt │ │ │ │ │ ├── AuthorizerConverter.kt │ │ │ │ │ └── InstallModeConverter.kt │ │ │ │ └── repo │ │ │ │ ├── AppRepoImpl.kt │ │ │ │ └── ConfigRepoImpl.kt │ │ │ ├── repo │ │ │ ├── AppRepo.kt │ │ │ └── ConfigRepo.kt │ │ │ └── util │ │ │ ├── AppOrder.kt │ │ │ ├── ConfigOrder.kt │ │ │ ├── ConfigUtil.kt │ │ │ └── OrderType.kt │ │ ├── di │ │ ├── init │ │ │ ├── app_modules.kt │ │ │ └── process_modules.kt │ │ ├── installer_module.kt │ │ ├── reflect_module.kt │ │ ├── room_module.kt │ │ ├── serialization_module.kt │ │ ├── viewmodel_module.kt │ │ └── worker_module.kt │ │ ├── test.kt │ │ ├── ui │ │ ├── activity │ │ │ ├── AboutPageActivity.kt │ │ │ ├── InstallerActivity.kt │ │ │ └── SettingsActivity.kt │ │ ├── common │ │ │ └── ViewContent.kt │ │ ├── page │ │ │ ├── installer │ │ │ │ ├── InstallerPage.kt │ │ │ │ └── dialog │ │ │ │ │ ├── DialogInnerParams.kt │ │ │ │ │ ├── DialogInnerWidget.kt │ │ │ │ │ ├── DialogPage.kt │ │ │ │ │ ├── DialogParams.kt │ │ │ │ │ ├── DialogParamsType.kt │ │ │ │ │ ├── DialogViewAction.kt │ │ │ │ │ ├── DialogViewModel.kt │ │ │ │ │ ├── DialogViewState.kt │ │ │ │ │ └── inner │ │ │ │ │ ├── AnalyseFailedDialog.kt │ │ │ │ │ ├── AnalysingDialog.kt │ │ │ │ │ ├── DialogButton.kt │ │ │ │ │ ├── DialogButtons.kt │ │ │ │ │ ├── InstallChoiceDialog.kt │ │ │ │ │ ├── InstallFailedDialog.kt │ │ │ │ │ ├── InstallInfoDialog.kt │ │ │ │ │ ├── InstallPrepareDialog.kt │ │ │ │ │ ├── InstallSuccessDialog.kt │ │ │ │ │ ├── InstallingDialog.kt │ │ │ │ │ ├── ReadyDialog.kt │ │ │ │ │ ├── ResolveFailedDialog.kt │ │ │ │ │ ├── ResolvingDialog.kt │ │ │ │ │ └── common.kt │ │ │ └── settings │ │ │ │ ├── SettingsPage.kt │ │ │ │ ├── SettingsScreen.kt │ │ │ │ ├── config │ │ │ │ ├── all │ │ │ │ │ ├── AllPage.kt │ │ │ │ │ ├── AllViewAction.kt │ │ │ │ │ ├── AllViewEvent.kt │ │ │ │ │ ├── AllViewModel.kt │ │ │ │ │ └── AllViewState.kt │ │ │ │ ├── apply │ │ │ │ │ ├── ApplyPage.kt │ │ │ │ │ ├── ApplyViewAction.kt │ │ │ │ │ ├── ApplyViewApp.kt │ │ │ │ │ ├── ApplyViewModel.kt │ │ │ │ │ └── ApplyViewState.kt │ │ │ │ └── edit │ │ │ │ │ ├── EditPage.kt │ │ │ │ │ ├── EditViewAction.kt │ │ │ │ │ ├── EditViewEvent.kt │ │ │ │ │ ├── EditViewModel.kt │ │ │ │ │ └── EditViewState.kt │ │ │ │ ├── home │ │ │ │ ├── HomeCardItem.kt │ │ │ │ ├── HomePage.kt │ │ │ │ └── HomeState.kt │ │ │ │ ├── main │ │ │ │ ├── MainPage.kt │ │ │ │ └── NavigationData.kt │ │ │ │ └── preferred │ │ │ │ ├── PreferredPage.kt │ │ │ │ ├── PreferredViewAction.kt │ │ │ │ ├── PreferredViewModel.kt │ │ │ │ └── PreferredViewState.kt │ │ ├── theme │ │ │ ├── InstallerTheme.kt │ │ │ ├── Type.kt │ │ │ └── WindowInsets.kt │ │ └── widget │ │ │ ├── dialog │ │ │ └── PositionDialog.kt │ │ │ ├── setting │ │ │ ├── BaseWidget.kt │ │ │ ├── DropDownMenuWidget.kt │ │ │ ├── LabelWidget.kt │ │ │ └── SwitchWidget.kt │ │ │ └── toggle │ │ │ ├── Toggle.kt │ │ │ └── ToggleRow.kt │ │ └── util │ │ ├── ContextUtil.kt │ │ └── ThrowableUtil.kt │ └── res │ ├── drawable-hdpi │ ├── round_hourglass_disabled_black_18.png │ ├── round_hourglass_disabled_black_20.png │ ├── round_hourglass_disabled_black_24.png │ ├── round_hourglass_disabled_black_36.png │ ├── round_hourglass_disabled_black_48.png │ ├── round_hourglass_empty_black_18.png │ ├── round_hourglass_empty_black_20.png │ ├── round_hourglass_empty_black_24.png │ ├── round_hourglass_empty_black_36.png │ └── round_hourglass_empty_black_48.png │ ├── drawable-mdpi │ ├── round_hourglass_disabled_black_18.png │ ├── round_hourglass_disabled_black_20.png │ ├── round_hourglass_disabled_black_24.png │ ├── round_hourglass_disabled_black_36.png │ ├── round_hourglass_disabled_black_48.png │ ├── round_hourglass_empty_black_18.png │ ├── round_hourglass_empty_black_20.png │ ├── round_hourglass_empty_black_24.png │ ├── round_hourglass_empty_black_36.png │ └── round_hourglass_empty_black_48.png │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable-xhdpi │ ├── round_hourglass_disabled_black_18.png │ ├── round_hourglass_disabled_black_20.png │ ├── round_hourglass_disabled_black_24.png │ ├── round_hourglass_disabled_black_36.png │ ├── round_hourglass_disabled_black_48.png │ ├── round_hourglass_empty_black_18.png │ ├── round_hourglass_empty_black_20.png │ ├── round_hourglass_empty_black_24.png │ ├── round_hourglass_empty_black_36.png │ └── round_hourglass_empty_black_48.png │ ├── drawable-xxhdpi │ ├── round_hourglass_disabled_black_18.png │ ├── round_hourglass_disabled_black_20.png │ ├── round_hourglass_disabled_black_24.png │ ├── round_hourglass_disabled_black_36.png │ ├── round_hourglass_disabled_black_48.png │ ├── round_hourglass_empty_black_18.png │ ├── round_hourglass_empty_black_20.png │ ├── round_hourglass_empty_black_24.png │ ├── round_hourglass_empty_black_36.png │ └── round_hourglass_empty_black_48.png │ ├── drawable-xxxhdpi │ ├── round_hourglass_disabled_black_18.png │ ├── round_hourglass_disabled_black_20.png │ ├── round_hourglass_disabled_black_24.png │ ├── round_hourglass_disabled_black_36.png │ ├── round_hourglass_disabled_black_48.png │ ├── round_hourglass_empty_black_18.png │ ├── round_hourglass_empty_black_20.png │ ├── round_hourglass_empty_black_24.png │ ├── round_hourglass_empty_black_36.png │ └── round_hourglass_empty_black_48.png │ ├── drawable │ ├── ic_launcher_background.xml │ ├── icon_background.xml │ ├── icon_foreground.xml │ ├── round_hourglass_disabled_20.xml │ ├── round_hourglass_disabled_24.xml │ ├── round_hourglass_empty_20.xml │ └── round_hourglass_empty_24.xml │ ├── mipmap-anydpi-v30 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── raw │ ├── empty_state.json │ └── loading.json │ ├── values-night │ └── colors.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ ├── data_extraction_rules.xml │ └── file_paths.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hidden-api ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ ├── android │ ├── content │ │ ├── pm │ │ │ ├── IPackageInstaller.java │ │ │ ├── IPackageInstallerSession.java │ │ │ ├── IPackageManager.java │ │ │ └── ParceledListSlice.java │ │ └── res │ │ │ └── ApkAssets.java │ └── os │ │ └── ServiceManager.java │ └── com │ └── android │ └── modules │ └── utils │ └── BaseParceledListSlice.java └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | /keystore 9 | .externalNativeBuild 10 | .cxx 11 | local.properties 12 | keystore.properties 13 | /.kotlin/ 14 | -------------------------------------------------------------------------------- /InstallerX锁定器_1.3.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/InstallerX锁定器_1.3.apk -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InstallerX Revived (Community Edition) 2 | 3 | [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)[![Latest Release](https://img.shields.io/github/v/release/wxxsfxyzm/InstallerX?label=稳定版)](https://github.com/wxxsfxyzm/InstallerX/releases/latest)[![Prerelease](https://img.shields.io/github/v/release/wxxsfxyzm/InstallerX?include_prereleases&label=测试版)](https://github.com/wxxsfxyzm/InstallerX/releases) 4 | 5 | - 这是一个由社区维护的分支版本, [原项目](https://github.com/iamr0s/InstallerX)已被作者归档。 6 | - 提供有限的开源更新和支持 7 | - 此分支严格遵循 GNU GPLv3,所有修改均开放源代码。 8 | 9 | ## 介绍 10 | 11 | > A modern and functional Android app installer. (You know some birds are not meant to be caged, their feathers are just too bright.) 12 | 13 | 一款应用安装程序,为什么不试试【InstallerX】? 14 | 15 | 在国产系统的魔改下,许多系统的自带安装程序体验并不是很好,你可以使用【InstallerX】替换掉系统默认安装程序。 16 | 17 | 当然,相对于原生系统,【InstallerX】也带来了更多的安装选项:对话框安装、通知栏安装、自动安装、声明安装者、选择是否安装到所有用户空间、允许测试包、允许降级安装、安装后自动删除安装包。 18 | 19 | ## 支持版本 20 | 21 | SDK 34 以上 22 | 23 | SDK 30-33有限支持,有问题发issue 24 | 25 | ## 变更内容 26 | 27 | - UI简化 28 | - 修复了原仓库项目无法正确删除安装包的问题 29 | - 文本调整,支持繁体中文。更多语言欢迎PR 30 | - 修改待安装应用版本号的显示效果 31 | - 加入了安装时显示targetSDK与minSDK的功能 32 | 33 | ## 开源协议 34 | 35 | Copyright (C) [iamr0s](https://github.com/iamr0s) and contributors 36 | 37 | InstallerX目前基于 [**GNU General Public License v3 (GPL-3)**](http://www.gnu.org/copyleft/gpl.html) 38 | 开源,但不保证未来依然继续遵循此协议或开源,有权更改开源协议或开源状态。 39 | 40 | 当您选择基于InstallerX进行开发时,需遵循所当前依赖的上游源码所规定的开源协议,不受新上游源码的开源协议影响。 41 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /debug 3 | /release -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | ############################### 24 | # kotlinx serialization rules # 25 | ############################### 26 | # Keep `Companion` object fields of serializable classes. 27 | # This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. 28 | -if @kotlinx.serialization.Serializable class ** 29 | -keepclassmembers class <1> { 30 | static <1>$Companion Companion; 31 | } 32 | 33 | # Keep `serializer()` on companion objects (both default and named) of serializable classes. 34 | -if @kotlinx.serialization.Serializable class ** { 35 | static **$* *; 36 | } 37 | -keepclassmembers class <2>$<3> { 38 | kotlinx.serialization.KSerializer serializer(...); 39 | } 40 | 41 | # Keep `INSTANCE.serializer()` of serializable objects. 42 | -if @kotlinx.serialization.Serializable class ** { 43 | public static ** INSTANCE; 44 | } 45 | -keepclassmembers class <1> { 46 | public static <1> INSTANCE; 47 | kotlinx.serialization.KSerializer serializer(...); 48 | } 49 | 50 | # @Serializable and @Polymorphic are used at runtime for polymorphic serialization. 51 | -keepattributes RuntimeVisibleAnnotations,AnnotationDefault 52 | 53 | # Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`. 54 | # If you have any, uncomment and replace classes with those containing named companion objects. 55 | #-keepattributes InnerClasses # Needed for `getDeclaredClasses`. 56 | #-if @kotlinx.serialization.Serializable class 57 | #com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions. 58 | #com.example.myapplication.HasNamedCompanion2 59 | #{ 60 | # static **$* *; 61 | #} 62 | #-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept. 63 | # static <1>$$serializer INSTANCE; 64 | #} 65 | 66 | -keep public interface ** extends android.os.IInterface {*;} 67 | -keep public class com.rosan.installer.App {*;} 68 | -keep public class com.rosan.installer.ui.activity.** extends android.app.Activity 69 | 70 | #-keep public class com.rosan.installer.data.process.model.impl.** extends com.rosan.installer.data.process.repo.ProcessRepo { 71 | #public static void main(java.lang.String[]); 72 | #} 73 | #-keep public class com.rosan.installer.** extends android.app.Service 74 | #-keep public class com.rosan.installer.** extends android.content.BroadcastReceiver 75 | #-keep public class com.rosan.installer.** extends android.content.ContentProvider 76 | #-keep class androidx.core.content.FileProvider {*;} 77 | #-keep interface androidx.core.content.FileProvider$PathStrategy {*;} 78 | 79 | -keep class rikka.shizuku.ShizukuProvider 80 | 81 | -dontwarn ** -------------------------------------------------------------------------------- /app/src/main/aidl/android/content/IIntentReceiver.aidl: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | 6 | oneway interface IIntentReceiver { 7 | void performReceive(in Intent intent, int resultCode, String data, 8 | in Bundle extras, boolean ordered, boolean sticky, int sendingUser); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/aidl/android/content/IIntentSender.aidl: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | import android.content.IIntentReceiver; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | 7 | oneway interface IIntentSender { 8 | void send(int code, in Intent intent, String resolvedType, in IBinder whitelistToken, 9 | IIntentReceiver finishedReceiver, String requiredPermission, in Bundle options); 10 | } -------------------------------------------------------------------------------- /app/src/main/aidl/com/rosan/installer/IAppProcessService.aidl: -------------------------------------------------------------------------------- 1 | package com.rosan.installer; 2 | 3 | import com.rosan.installer.IPrivilegedService; 4 | 5 | interface IAppProcessService { 6 | void quit(); 7 | 8 | IPrivilegedService getPrivilegedService(); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/aidl/com/rosan/installer/IDhizukuUserService.aidl: -------------------------------------------------------------------------------- 1 | package com.rosan.installer; 2 | 3 | import com.rosan.installer.IPrivilegedService; 4 | 5 | interface IDhizukuUserService { 6 | IPrivilegedService getPrivilegedService() = 21; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/aidl/com/rosan/installer/IPrivilegedService.aidl: -------------------------------------------------------------------------------- 1 | package com.rosan.installer; 2 | 3 | import android.content.ComponentName; 4 | 5 | interface IPrivilegedService { 6 | void delete(in String[] paths); 7 | 8 | void setDefaultInstaller(in ComponentName component, boolean enable); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/aidl/com/rosan/installer/IShizukuUserService.aidl: -------------------------------------------------------------------------------- 1 | package com.rosan.installer; 2 | 3 | import com.rosan.installer.IPrivilegedService; 4 | 5 | interface IShizukuUserService { 6 | void destroy() = 16777114; 7 | 8 | IPrivilegedService getPrivilegedService() = 1; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/App.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer 2 | 3 | import android.app.Application 4 | import com.rosan.installer.di.init.appModules 5 | import org.koin.android.ext.koin.androidContext 6 | import org.koin.android.ext.koin.androidLogger 7 | import org.koin.core.context.startKoin 8 | 9 | class App : Application() { 10 | override fun onCreate() { 11 | CrashHandler.init() 12 | super.onCreate() 13 | startKoin { 14 | // Koin Android Logger 15 | androidLogger() 16 | // Koin Android Context 17 | androidContext(this@App) 18 | // use modules 19 | modules(appModules) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/CrashHandler.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer 2 | 3 | import java.lang.Thread.UncaughtExceptionHandler 4 | 5 | class CrashHandler : UncaughtExceptionHandler { 6 | companion object { 7 | private var isInit = false 8 | 9 | private var defaultHandler: UncaughtExceptionHandler? = null 10 | 11 | fun init() { 12 | synchronized(this) { 13 | if (isInit) return 14 | isInit = true 15 | defaultHandler = Thread.getDefaultUncaughtExceptionHandler() 16 | Thread.setDefaultUncaughtExceptionHandler(CrashHandler()) 17 | } 18 | } 19 | } 20 | 21 | override fun uncaughtException(t: Thread, e: Throwable) { 22 | defaultHandler?.uncaughtException(t, e) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/build/Level.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.build 2 | 3 | enum class Level { 4 | STABLE, 5 | PREVIEW, 6 | UNSTABLE 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/build/RsConfig.java: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.build; 2 | 3 | import android.os.Build; 4 | import android.text.TextUtils; 5 | 6 | import com.rosan.installer.BuildConfig; 7 | 8 | public final class RsConfig { 9 | public static final Level LEVEL = getLevel(); 10 | 11 | private static Level getLevel() { 12 | Level level = Level.UNSTABLE; 13 | switch (BuildConfig.BUILD_LEVEL) { 14 | case 1: 15 | level = Level.PREVIEW; 16 | break; 17 | case 2: 18 | level = Level.STABLE; 19 | break; 20 | } 21 | return level; 22 | } 23 | 24 | public static final String versionName = BuildConfig.VERSION_NAME; 25 | 26 | public static final int versionCode = BuildConfig.VERSION_CODE; 27 | 28 | private static String getSystemVersion() { 29 | if (Build.VERSION.PREVIEW_SDK_INT != 0) 30 | return String.format("%1$s Preview (API %2$s)", 31 | Build.VERSION.CODENAME, 32 | Build.VERSION.SDK_INT); 33 | else return String.format("%1$s (API %2$s)", 34 | Build.VERSION.RELEASE, 35 | Build.VERSION.SDK_INT); 36 | } 37 | 38 | public static final String systemVersion = getSystemVersion(); 39 | 40 | private static String getDeviceName() { 41 | String manufacturer = Build.MANUFACTURER.toUpperCase(); 42 | String brand = Build.BRAND.toUpperCase(); 43 | if (!TextUtils.equals(brand, manufacturer)) manufacturer += " " + brand; 44 | manufacturer += " " + Build.MODEL; 45 | return manufacturer; 46 | } 47 | 48 | public static final String deviceName = getDeviceName(); 49 | 50 | private static String getSystemStruct() { 51 | String struct = System.getProperty("os.arch"); 52 | if (struct == null) struct = "unknown"; 53 | String[] abis = Build.SUPPORTED_ABIS; 54 | if (abis.length == 0) 55 | struct += " (Not Supported Native ABI)"; 56 | else { 57 | StringBuilder supportABIs = new StringBuilder(); 58 | for (int i = 0; i < abis.length; i++) { 59 | supportABIs.append(abis[i]); 60 | if (i + 1 < abis.length) supportABIs.append(", "); 61 | } 62 | struct += " (" + supportABIs + ")"; 63 | } 64 | return struct; 65 | } 66 | 67 | public static final String systemStruct = getSystemStruct(); 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/entity/AnalyseExtraEntity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.entity 2 | 3 | data class AnalyseExtraEntity( 4 | val cacheDirectory: String 5 | ) { 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/entity/AppEntity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.entity 2 | 3 | import android.graphics.drawable.Drawable 4 | 5 | sealed class AppEntity { 6 | abstract val packageName: String 7 | 8 | abstract val name: String 9 | 10 | abstract val targetSdk: String? 11 | 12 | abstract val minSdk: String? 13 | 14 | data class BaseEntity( 15 | override val packageName: String, 16 | val data: DataEntity, 17 | val versionCode: Long, 18 | val versionName: String, 19 | val label: String?, 20 | val icon: Drawable?, 21 | override val targetSdk: String?, 22 | override val minSdk: String?, 23 | ) : AppEntity() { 24 | override val name = "base.apk" 25 | } 26 | 27 | data class SplitEntity( 28 | override val packageName: String, 29 | val data: DataEntity, 30 | val splitName: String, 31 | override val targetSdk: String?, 32 | override val minSdk: String?, 33 | ) : AppEntity() { 34 | override val name = "$splitName.apk" 35 | } 36 | 37 | data class DexMetadataEntity( 38 | override val packageName: String, 39 | val data: DataEntity, 40 | val dmName: String, 41 | override val targetSdk: String?, 42 | override val minSdk: String?, 43 | ) : AppEntity() { 44 | override val name = "base.dm" 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/entity/DataEntity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.entity 2 | 3 | import android.annotation.SuppressLint 4 | import android.system.Os 5 | import android.system.OsConstants 6 | import java.io.File 7 | import java.io.FileDescriptor 8 | import java.io.FileInputStream 9 | import java.io.InputStream 10 | import java.util.zip.ZipFile 11 | import java.util.zip.ZipInputStream 12 | 13 | sealed class DataEntity(open var source: DataEntity? = null) { 14 | abstract fun getInputStream(): InputStream? 15 | 16 | fun getInputStreamWhileNotEmpty(): InputStream? = getInputStream() ?: source?.getInputStream() 17 | 18 | fun getSourceTop(): DataEntity = source?.getSourceTop() ?: this 19 | 20 | class FileEntity(val path: String) : DataEntity() { 21 | override fun getInputStream() = File(path).inputStream() 22 | 23 | override fun toString() = path 24 | } 25 | 26 | class ZipFileEntity(val name: String, val parent: FileEntity) : DataEntity() { 27 | override fun getInputStream(): InputStream? = ZipFile(parent.path).let { 28 | val entry = it.getEntry(name) ?: return@let null 29 | it.getInputStream(entry) 30 | } 31 | 32 | override var source: DataEntity? = parent.source?.let { ZipInputStreamEntity(name, it) } 33 | 34 | override fun toString() = "$parent!$name" 35 | } 36 | 37 | class FileDescriptorEntity(private val pid: Int, private val descriptor: Int) : DataEntity() { 38 | @SuppressLint("DiscouragedPrivateApi") 39 | fun getFileDescriptor(): FileDescriptor? { 40 | if (Os.getpid() != pid) return null 41 | val fileDescriptor = FileDescriptor() 42 | kotlin.runCatching { FileDescriptor::class.java.getDeclaredField("descriptor") } 43 | .onSuccess { 44 | it.isAccessible = true 45 | it.set(fileDescriptor, descriptor) 46 | }.onFailure { 47 | it.printStackTrace() 48 | } 49 | if (!fileDescriptor.valid()) return null 50 | Os.lseek(fileDescriptor, 0, OsConstants.SEEK_SET) 51 | return fileDescriptor 52 | } 53 | 54 | override fun getInputStream(): InputStream { 55 | val fileDescriptor = getFileDescriptor() 56 | if (fileDescriptor != null) { 57 | return FileInputStream(fileDescriptor) 58 | } 59 | return File("/proc/$pid/fd/$descriptor").inputStream() 60 | } 61 | 62 | override fun toString() = "/proc/$pid/fd/$descriptor" 63 | } 64 | 65 | class ZipInputStreamEntity(val name: String, val parent: DataEntity) : DataEntity() { 66 | override fun getInputStream(): InputStream? { 67 | val inputStream = parent.getInputStream() ?: return null 68 | val zip = ZipInputStream(inputStream) 69 | var result: InputStream? = null 70 | while (true) { 71 | val entry = zip.nextEntry ?: break 72 | if (entry.name != name) continue 73 | result = zip 74 | break 75 | } 76 | return result 77 | } 78 | 79 | override var source: DataEntity? = parent.source?.let { ZipInputStreamEntity(name, it) } 80 | 81 | override fun toString(): String = "$parent!$name" 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/entity/InstallEntity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.entity 2 | 3 | data class InstallEntity( 4 | val name: String, 5 | val packageName: String, 6 | val data: DataEntity 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/entity/InstallExtraEntity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.entity 2 | 3 | data class InstallExtraEntity( 4 | val userId: Int, 5 | val cacheDirectory: String 6 | ) { 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedAlreadyExistsException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedAlreadyExistsException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedConflictingProviderException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedConflictingProviderException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedContainerErrorException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedContainerErrorException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedCpuAbiIncompatibleException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedCpuAbiIncompatibleException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedDexOptException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedDexOptException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedDuplicatePackageException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedDuplicatePackageException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedInsufficientStorageException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedInsufficientStorageException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedInvalidAPKException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedInvalidAPKException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedInvalidInstallLocationException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedInvalidInstallLocationException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedInvalidURIException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedInvalidURIException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedMediaUnavailableException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedMediaUnavailableException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedMissingFeatureException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedMissingFeatureException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedMissingSharedLibraryException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedMissingSharedLibraryException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedNewerSDKException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedNewerSDKException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedNoSharedUserException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedNoSharedUserException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedOlderSdkException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedOlderSdkException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedPackageChangedException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedPackageChangedException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedRejectedByBuildTypeException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedRejectedByBuildTypeException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedReplaceCouldntDeleteException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedReplaceCouldntDeleteException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedSharedUserIncompatibleException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedSharedUserIncompatibleException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedTestOnlyException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedTestOnlyException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedUidChangedException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedUidChangedException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedUpdateIncompatibleException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedUpdateIncompatibleException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedVerificationFailureException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedVerificationFailureException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedVerificationTimeoutException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedVerificationTimeoutException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/exception/InstallFailedVersionDowngradeException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.exception 2 | 3 | class InstallFailedVersionDowngradeException : Exception { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/impl/DSRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.impl 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import com.rosan.installer.data.app.repo.DSRepo 6 | import com.rosan.installer.data.recycle.util.useUserService 7 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 8 | import com.rosan.installer.ui.activity.InstallerActivity 9 | import org.koin.core.component.KoinComponent 10 | import org.koin.core.component.inject 11 | 12 | object DSRepoImpl : DSRepo, KoinComponent { 13 | private val context by inject() 14 | 15 | override suspend fun doWork(config: ConfigEntity, enabled: Boolean) { 16 | useUserService( 17 | config, if (config.authorizer == ConfigEntity.Authorizer.Root) { 18 | { "su 1000" } 19 | } else null 20 | ) { 21 | it.privileged 22 | .setDefaultInstaller(ComponentName(context, InstallerActivity::class.java), enabled) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/impl/InstallerRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.impl 2 | 3 | import com.rosan.installer.data.app.model.entity.InstallEntity 4 | import com.rosan.installer.data.app.model.entity.InstallExtraEntity 5 | import com.rosan.installer.data.app.model.impl.installer.DhizukuInstallerRepoImpl 6 | import com.rosan.installer.data.app.model.impl.installer.ProcessInstallerRepoImpl 7 | import com.rosan.installer.data.app.model.impl.installer.ShizukuInstallerRepoImpl 8 | import com.rosan.installer.data.app.repo.InstallerRepo 9 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 10 | 11 | object InstallerRepoImpl : InstallerRepo { 12 | override suspend fun doWork( 13 | config: ConfigEntity, 14 | entities: List, 15 | extra: InstallExtraEntity 16 | ) { 17 | val repo = when (config.authorizer) { 18 | ConfigEntity.Authorizer.Shizuku -> ShizukuInstallerRepoImpl 19 | ConfigEntity.Authorizer.Dhizuku -> DhizukuInstallerRepoImpl 20 | else -> ProcessInstallerRepoImpl 21 | } 22 | repo.doWork(config, entities, extra) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/impl/installer/DhizukuInstallerRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.impl.installer 2 | 3 | import android.os.IBinder 4 | import com.rosan.dhizuku.api.Dhizuku 5 | import com.rosan.installer.data.recycle.util.requireDhizukuPermissionGranted 6 | import org.koin.core.component.KoinComponent 7 | 8 | object DhizukuInstallerRepoImpl : IBinderInstallerRepoImpl(), KoinComponent { 9 | override suspend fun iBinderWrapper(iBinder: IBinder): IBinder = 10 | requireDhizukuPermissionGranted { 11 | Dhizuku.binderWrapper(iBinder) 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/impl/installer/NoneInstallerRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.impl.installer 2 | 3 | import android.os.IBinder 4 | import com.rosan.app_process.AppProcess 5 | import com.rosan.app_process.NewProcessImpl 6 | import com.rosan.installer.data.app.model.entity.InstallEntity 7 | import com.rosan.installer.data.app.model.entity.InstallExtraEntity 8 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 9 | 10 | object NoneInstallerRepoImpl : IBinderInstallerRepoImpl() { 11 | private val newProcess = NewProcessImpl(); 12 | 13 | override suspend fun doWork( 14 | config: ConfigEntity, entities: List, extra: InstallExtraEntity 15 | ) { 16 | super.doWork(config, entities, extra) 17 | } 18 | 19 | override suspend fun iBinderWrapper(iBinder: IBinder): IBinder = 20 | AppProcess.binderWrapper(newProcess, iBinder) 21 | 22 | override suspend fun onDeleteWork( 23 | config: ConfigEntity, 24 | entities: List, 25 | extra: InstallExtraEntity 26 | ) { 27 | super.onDeleteWork(config, entities, extra) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/impl/installer/ProcessInstallerRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.impl.installer 2 | 3 | import android.os.IBinder 4 | import com.rosan.app_process.AppProcess 5 | import com.rosan.installer.data.app.model.entity.InstallEntity 6 | import com.rosan.installer.data.app.model.entity.InstallExtraEntity 7 | import com.rosan.installer.data.recycle.model.impl.AppProcessRecyclers 8 | import com.rosan.installer.data.recycle.repo.Recyclable 9 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 10 | 11 | object ProcessInstallerRepoImpl : IBinderInstallerRepoImpl() { 12 | private lateinit var recycler: Recyclable 13 | 14 | override suspend fun doWork( 15 | config: ConfigEntity, entities: List, extra: InstallExtraEntity 16 | ) { 17 | recycler = AppProcessRecyclers.get( 18 | when (config.authorizer) { 19 | ConfigEntity.Authorizer.Root -> "su" 20 | ConfigEntity.Authorizer.Customize -> config.customizeAuthorizer 21 | else -> "sh" 22 | } 23 | ).make() 24 | super.doWork(config, entities, extra) 25 | } 26 | 27 | override suspend fun iBinderWrapper(iBinder: IBinder): IBinder = 28 | recycler.entity.binderWrapper(iBinder) 29 | 30 | override suspend fun doFinishWork( 31 | config: ConfigEntity, 32 | entities: List, 33 | extra: InstallExtraEntity, 34 | result: Result 35 | ) { 36 | super.doFinishWork(config, entities, extra, result) 37 | recycler.recycle() 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/model/impl/installer/ShizukuInstallerRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.model.impl.installer 2 | 3 | import android.os.IBinder 4 | import com.rosan.installer.data.recycle.util.requireShizukuPermissionGranted 5 | import rikka.shizuku.ShizukuBinderWrapper 6 | 7 | object ShizukuInstallerRepoImpl : IBinderInstallerRepoImpl() { 8 | override suspend fun iBinderWrapper(iBinder: IBinder): IBinder = 9 | requireShizukuPermissionGranted { 10 | ShizukuBinderWrapper(iBinder) 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/repo/AnalyserRepo.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.repo 2 | 3 | import com.rosan.installer.data.app.model.entity.AnalyseExtraEntity 4 | import com.rosan.installer.data.app.model.entity.AppEntity 5 | import com.rosan.installer.data.app.model.entity.DataEntity 6 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 7 | 8 | interface AnalyserRepo { 9 | suspend fun doWork( 10 | config: ConfigEntity, 11 | data: List, 12 | extra: AnalyseExtraEntity 13 | ): List 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/repo/DefaultSetterRepo.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.repo 2 | 3 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 4 | 5 | typealias DSRepo = DefaultSetterRepo 6 | 7 | interface DefaultSetterRepo { 8 | suspend fun doWork( 9 | config: ConfigEntity, 10 | enabled: Boolean 11 | ) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/repo/InstallerRepo.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.repo 2 | 3 | import com.rosan.installer.data.app.model.entity.InstallEntity 4 | import com.rosan.installer.data.app.model.entity.InstallExtraEntity 5 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 6 | 7 | interface InstallerRepo { 8 | suspend fun doWork( 9 | config: ConfigEntity, 10 | entities: List, 11 | extra: InstallExtraEntity 12 | ) 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/util/AppEntityInfo.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.util 2 | 3 | import android.content.Context 4 | import android.content.pm.ApplicationInfo 5 | import android.content.pm.PackageManager 6 | import android.graphics.drawable.Drawable 7 | import android.os.Build 8 | import androidx.core.content.ContextCompat 9 | import com.rosan.installer.data.app.model.entity.AppEntity 10 | 11 | data class AppEntityInfo( 12 | val icon: Drawable?, 13 | val title: String 14 | ) 15 | 16 | fun AppEntity.getInfo(context: Context): AppEntityInfo = when (this) { 17 | is AppEntity.BaseEntity -> AppEntityInfo( 18 | icon = this.icon ?: ContextCompat.getDrawable(context, android.R.drawable.sym_def_app_icon), 19 | title = this.label ?: this.packageName 20 | ) 21 | else -> { 22 | val packageManager = context.packageManager 23 | var applicationInfo: ApplicationInfo? = null 24 | try { 25 | applicationInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 26 | packageManager?.getApplicationInfo( 27 | this.packageName, 28 | PackageManager.ApplicationInfoFlags.of(0L) 29 | ) 30 | else 31 | packageManager?.getApplicationInfo(this.packageName, 0) 32 | } catch (e: PackageManager.NameNotFoundException) { 33 | e.printStackTrace() 34 | } 35 | val icon = applicationInfo?.loadIcon(packageManager) 36 | val label = applicationInfo?.loadLabel(packageManager)?.toString() 37 | AppEntityInfo( 38 | icon = icon ?: ContextCompat.getDrawable(context, android.R.drawable.sym_def_app_icon), 39 | title = label ?: this.packageName 40 | ) 41 | } 42 | } 43 | 44 | fun List.sortedBest(): List = this.sortedWith( 45 | compareBy( 46 | { 47 | it.packageName 48 | }, 49 | { 50 | it.name 51 | } 52 | ) 53 | ) 54 | 55 | fun List.getInfo(context: Context): AppEntityInfo = 56 | this.sortedBest().first().getInfo(context) -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/util/DataType.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.util 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | 6 | @Serializable 7 | enum class DataType { 8 | AUTO, 9 | APK, 10 | APKS, 11 | APKM, 12 | XAPK 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/util/InstallEntityUtil.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.util 2 | 3 | import com.rosan.installer.data.app.model.entity.DataEntity 4 | import com.rosan.installer.data.app.model.entity.InstallEntity 5 | 6 | fun List.sourcePath(): Array = mapNotNull { 7 | it.data.sourcePath() 8 | }.distinct().toTypedArray() 9 | 10 | fun DataEntity.sourcePath(): String? = when (val source = this.getSourceTop()) { 11 | is DataEntity.FileEntity -> source.path 12 | is DataEntity.ZipFileEntity -> source.parent.path 13 | is DataEntity.ZipInputStreamEntity -> source.parent.sourcePath() 14 | else -> null 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/util/InstallFlag.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.util 2 | 3 | enum class InstallFlag(val value: Int) { 4 | INSTALL_REPLACE_EXISTING(0x00000002), 5 | INSTALL_ALLOW_TEST(0x00000004), 6 | INSTALL_INTERNAL(0x00000010), 7 | INSTALL_FROM_ADB(0x00000020), 8 | INSTALL_ALL_USERS(0x00000040), 9 | INSTALL_REQUEST_DOWNGRADE(0x00000080), 10 | INSTALL_GRANT_RUNTIME_PERMISSIONS(0x00000100), 11 | INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS(0x00400000), 12 | INSTALL_FORCE_VOLUME_UUID(0x00000200), 13 | INSTALL_FORCE_PERMISSION_PROMPT(0x00000400), 14 | INSTALL_INSTANT_APP(0x00000800), 15 | DONT_KILL_APP(0x00000001), 16 | INSTALL_FULL_APP(0x00004000), 17 | INSTALL_ALLOCATE_AGGRESSIVE(0x00008000), 18 | INSTALL_VIRTUAL_PRELOAD(0x00010000), 19 | INSTALL_APEX(0x00020000), 20 | INSTALL_ENABLE_ROLLBACK(0x00040000), 21 | INSTALL_ALLOW_DOWNGRADE(0x00100000), 22 | INSTALL_STAGED(0x00200000), 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/util/InstalledAppInfo.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.util 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.graphics.drawable.Drawable 6 | import android.os.Build 7 | import com.rosan.installer.data.common.util.compatVersionCode 8 | import org.koin.core.component.KoinComponent 9 | import org.koin.core.component.get 10 | 11 | data class InstalledAppInfo( 12 | val packageName: String, 13 | val icon: Drawable?, 14 | val label: String, 15 | val versionCode: Long, 16 | val versionName: String 17 | ) { 18 | companion object : KoinComponent { 19 | fun buildByPackageName(packageName: String): InstalledAppInfo? { 20 | val context: Context = get() 21 | val packageManager = context.packageManager 22 | val packageInfo = try { 23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 24 | packageManager?.getPackageInfo( 25 | packageName, 26 | PackageManager.PackageInfoFlags.of(0L) 27 | ) 28 | else 29 | packageManager?.getPackageInfo(packageName, 0) 30 | } catch (e: PackageManager.NameNotFoundException) { 31 | e.printStackTrace() 32 | null 33 | } ?: return null 34 | val applicationInfo = packageInfo.applicationInfo 35 | return InstalledAppInfo( 36 | packageName = packageName, 37 | icon = applicationInfo?.loadIcon(packageManager), 38 | label = applicationInfo?.loadLabel(packageManager)?.toString() ?: "", 39 | versionCode = packageInfo.compatVersionCode, 40 | versionName = packageInfo.versionName!! 41 | ) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/util/LocalIntentReceiverProxy.kt: -------------------------------------------------------------------------------- 1 | package com.android.server.rollback 2 | 3 | import android.content.IntentSender 4 | 5 | class LocalIntentReceiverProxy { 6 | fun a(){ 7 | // LocalIntentReceiver() 8 | // IntentSender() 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/app/util/PackageInstallerUtil.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.app.util 2 | 3 | import android.content.pm.PackageInstaller 4 | import com.rosan.installer.data.reflect.repo.ReflectRepo 5 | import org.koin.core.component.KoinComponent 6 | import org.koin.core.component.get 7 | 8 | class PackageInstallerUtil { 9 | companion object : KoinComponent { 10 | const val EXTRA_LEGACY_STATUS = "android.content.pm.extra.LEGACY_STATUS" 11 | 12 | private val installFlagsField = get().getDeclaredField( 13 | PackageInstaller.SessionParams::class.java, 14 | "installFlags" 15 | )!!.also { field -> 16 | field.isAccessible = true 17 | } 18 | 19 | var PackageInstaller.SessionParams.installFlags: Int 20 | get() = installFlagsField.getInt(this) 21 | set(value) { 22 | installFlagsField.setInt(this, value) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/common/util/IntUtil.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.common.util 2 | 3 | fun Int.hasFlag(flag: Int) = flag and this == flag 4 | fun Int.withFlag(flag: Int) = this or flag 5 | fun Int.minusFlag(flag: Int) = this and flag.inv() 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/common/util/MutableCollection.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.common.util 2 | 3 | fun MutableCollection.addAll(vararg elements: T): Boolean { 4 | return addAll(elements.asList()) 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/common/util/PackageManagerUtil.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.common.util 2 | 3 | import android.content.pm.PackageInfo 4 | import android.content.pm.PackageManager 5 | import androidx.core.content.pm.PackageInfoCompat 6 | 7 | val PackageInfo.compatVersionCode: Long 8 | get() = PackageInfoCompat.getLongVersionCode(this) 9 | 10 | fun PackageManager.getCompatInstalledPackages(flags: Int): List = 11 | getInstalledPackages(PackageManager.PackageInfoFlags.of(flags.toLong())) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/installer/model/entity/ProgressEntity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.installer.model.entity 2 | 3 | sealed class ProgressEntity { 4 | data object Finish : ProgressEntity() 5 | 6 | data object Ready : ProgressEntity() 7 | 8 | data object Error : ProgressEntity() 9 | 10 | data object Resolving : ProgressEntity() 11 | 12 | data object ResolvedFailed : ProgressEntity() 13 | 14 | data object ResolveSuccess : ProgressEntity() 15 | 16 | data object Analysing : ProgressEntity() 17 | 18 | data object AnalysedFailed : ProgressEntity() 19 | 20 | data object AnalysedSuccess : ProgressEntity() 21 | 22 | data object Installing : ProgressEntity() 23 | 24 | data object InstallFailed : ProgressEntity() 25 | 26 | data object InstallSuccess : ProgressEntity() 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/installer/model/entity/SelectInstallEntity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.installer.model.entity 2 | 3 | import com.rosan.installer.data.app.model.entity.AppEntity 4 | 5 | data class SelectInstallEntity( 6 | val app: AppEntity, 7 | val selected: Boolean 8 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/installer/model/exception/ResolveException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.installer.model.exception 2 | 3 | import android.net.Uri 4 | 5 | data class ResolveException( 6 | val action: String?, 7 | val uris: List, 8 | ) : Exception( 9 | "action: $action, uri: $uris" 10 | ) { 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/installer/model/impl/installer/Handler.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.installer.model.impl.installer 2 | 3 | import com.rosan.installer.data.installer.repo.InstallerRepo 4 | import kotlinx.coroutines.CoroutineScope 5 | 6 | abstract class Handler(val scope: CoroutineScope, open val installer: InstallerRepo) { 7 | abstract suspend fun onStart() 8 | 9 | abstract suspend fun onFinish() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/installer/model/impl/installer/ProgressHandler.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.installer.model.impl.installer 2 | 3 | import com.rosan.installer.data.installer.model.entity.ProgressEntity 4 | import com.rosan.installer.data.installer.repo.InstallerRepo 5 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Job 8 | import kotlinx.coroutines.launch 9 | 10 | class ProgressHandler(scope: CoroutineScope, installer: InstallerRepo) : Handler(scope, installer) { 11 | private var job: Job? = null 12 | 13 | override suspend fun onStart() { 14 | job = scope.launch { 15 | installer.progress.collect { 16 | when (it) { 17 | is ProgressEntity.ResolvedFailed -> onResolvedFailed() 18 | is ProgressEntity.ResolveSuccess -> onResolveSuccess() 19 | is ProgressEntity.AnalysedSuccess -> onAnalysedSuccess() 20 | else -> {} 21 | } 22 | } 23 | } 24 | } 25 | 26 | override suspend fun onFinish() { 27 | job?.cancel() 28 | } 29 | 30 | private suspend fun onResolvedFailed() { 31 | onResolved(false) 32 | } 33 | 34 | private suspend fun onResolveSuccess() { 35 | onResolved(true) 36 | } 37 | 38 | private fun onResolved(success: Boolean) { 39 | val installMode = installer.config.installMode 40 | if (installMode == ConfigEntity.InstallMode.Notification || installMode == ConfigEntity.InstallMode.AutoNotification) { 41 | installer.background(true) 42 | } 43 | if (success) { 44 | installer.analyse() 45 | } 46 | } 47 | 48 | private fun onAnalysedSuccess() { 49 | val installMode = installer.config.installMode 50 | if ( 51 | installMode != ConfigEntity.InstallMode.AutoDialog 52 | && installMode != ConfigEntity.InstallMode.AutoNotification 53 | ) return 54 | if (installer.entities.filter { it.selected } 55 | .groupBy { it.app.packageName }.keys.size != 1) return 56 | installer.install() 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/installer/repo/InstallerRepo.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.installer.repo 2 | 3 | import android.app.Activity 4 | import com.rosan.installer.data.app.model.entity.DataEntity 5 | import com.rosan.installer.data.installer.model.entity.ProgressEntity 6 | import com.rosan.installer.data.installer.model.entity.SelectInstallEntity 7 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 8 | import kotlinx.coroutines.flow.Flow 9 | import java.io.Closeable 10 | 11 | interface InstallerRepo : Closeable { 12 | val id: String 13 | 14 | var error: Throwable 15 | 16 | var config: ConfigEntity 17 | 18 | var data: List 19 | 20 | var entities: List 21 | 22 | val progress: Flow 23 | 24 | val background: Flow 25 | 26 | fun resolve(activity: Activity) 27 | 28 | fun analyse() 29 | 30 | fun install() 31 | 32 | fun background(value: Boolean) 33 | 34 | override fun close() 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/installer/util/PendingIntent.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.installer.util 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import android.content.Intent 6 | 7 | private const val defaultFlags = 8 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 9 | 10 | fun Intent.pendingBroadcast( 11 | context: Context, 12 | requestCode: Int, 13 | flags: Int = defaultFlags 14 | ): PendingIntent = PendingIntent.getBroadcast(context, requestCode, this, flags) 15 | 16 | fun Intent.pendingActivity( 17 | context: Context, 18 | requestCode: Int, 19 | flags: Int = defaultFlags 20 | ): PendingIntent = PendingIntent.getActivity(context, requestCode, this, flags) 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/model/entity/BasePrivilegedService.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.model.entity 2 | 3 | import android.content.Context 4 | import com.rosan.installer.IPrivilegedService 5 | import org.koin.core.component.KoinComponent 6 | import org.koin.core.component.inject 7 | 8 | abstract class BasePrivilegedService : IPrivilegedService.Stub(), KoinComponent { 9 | protected val context by inject() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/model/entity/DhizukuPrivilegedService.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.model.entity 2 | 3 | import android.app.admin.DevicePolicyManager 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import com.rosan.dhizuku.api.Dhizuku 7 | import com.rosan.dhizuku.shared.DhizukuVariables 8 | import com.rosan.installer.data.recycle.util.InstallIntentFilter 9 | import com.rosan.installer.data.recycle.util.delete 10 | 11 | class DhizukuPrivilegedService : BasePrivilegedService() { 12 | private val devicePolicyManager: DevicePolicyManager = 13 | context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager 14 | 15 | override fun delete(paths: Array) = paths.delete() 16 | 17 | override fun setDefaultInstaller(component: ComponentName, enable: Boolean) { 18 | devicePolicyManager.clearPackagePersistentPreferredActivities( 19 | // TODO 20 | // DhizukuVariables.PARAM_COMPONENT 21 | Dhizuku.getOwnerComponent(), 22 | component.packageName 23 | ) 24 | if (!enable) return 25 | devicePolicyManager.addPersistentPreferredActivity( 26 | // TODO 27 | // DhizukuVariables.PARAM_COMPONENT, 28 | Dhizuku.getOwnerComponent(), 29 | InstallIntentFilter, component 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/model/exception/AppProcessNotWorkException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.model.exception 2 | 3 | class AppProcessNotWorkException : RuntimeException { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | 8 | constructor(cause: Throwable?) : super(cause) 9 | 10 | constructor(message: String?, cause: Throwable?) : super(message, cause) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/model/exception/DhizukuNotWorkException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.model.exception 2 | 3 | class DhizukuNotWorkException : RuntimeException { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | 8 | constructor(cause: Throwable?) : super(cause) 9 | 10 | constructor(message: String?, cause: Throwable?) : super(message, cause) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/model/exception/RootNotWorkException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.model.exception 2 | 3 | class RootNotWorkException : RuntimeException { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | 8 | constructor(cause: Throwable?) : super(cause) 9 | 10 | constructor(message: String?, cause: Throwable?) : super(message, cause) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/model/exception/ShizukuNotWorkException.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.model.exception 2 | 3 | class ShizukuNotWorkException : RuntimeException { 4 | constructor() : super() 5 | 6 | constructor(message: String?) : super(message) 7 | 8 | constructor(cause: Throwable?) : super(cause) 9 | 10 | constructor(message: String?, cause: Throwable?) : super(message, cause) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/model/impl/AppProcessRecycler.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.model.impl 2 | 3 | import com.rosan.app_process.AppProcess 4 | import com.rosan.installer.data.recycle.model.exception.AppProcessNotWorkException 5 | import com.rosan.installer.data.recycle.model.exception.RootNotWorkException 6 | import com.rosan.installer.data.recycle.repo.Recycler 7 | import java.util.StringTokenizer 8 | 9 | class AppProcessRecycler(private val shell: String) : Recycler() { 10 | private class CustomizeAppProcess(private val shell: String) : AppProcess.Terminal() { 11 | override fun newTerminal(): MutableList { 12 | val st = StringTokenizer(shell) 13 | val cmdList = mutableListOf() 14 | while (st.hasMoreTokens()) { 15 | cmdList.add(st.nextToken()); 16 | } 17 | return cmdList; 18 | } 19 | } 20 | 21 | override fun onMake(): AppProcess { 22 | return CustomizeAppProcess(shell).apply { 23 | if (init()) return@apply 24 | if (shell == "su") throw RootNotWorkException() 25 | else throw AppProcessNotWorkException() 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/model/impl/AppProcessRecyclers.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.model.impl 2 | 3 | object AppProcessRecyclers { 4 | private val map = mutableMapOf() 5 | 6 | fun get(shell: String): AppProcessRecycler { 7 | return map.getOrPut(shell) { 8 | AppProcessRecycler(shell) 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/model/impl/DhizukuUserServiceRecycler.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.model.impl 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.ServiceConnection 6 | import android.os.IBinder 7 | import androidx.annotation.Keep 8 | import com.rosan.dhizuku.api.Dhizuku 9 | import com.rosan.dhizuku.api.DhizukuUserServiceArgs 10 | import com.rosan.installer.IDhizukuUserService 11 | import com.rosan.installer.IPrivilegedService 12 | import com.rosan.installer.data.recycle.model.entity.DhizukuPrivilegedService 13 | import com.rosan.installer.data.recycle.repo.recyclable.UserService 14 | import com.rosan.installer.data.recycle.repo.Recycler 15 | import com.rosan.installer.data.recycle.util.requireDhizukuPermissionGranted 16 | import com.rosan.installer.di.init.processModules 17 | import kotlinx.coroutines.channels.awaitClose 18 | import kotlinx.coroutines.flow.callbackFlow 19 | import kotlinx.coroutines.flow.first 20 | import kotlinx.coroutines.runBlocking 21 | import org.koin.android.ext.koin.androidContext 22 | import org.koin.core.component.KoinComponent 23 | import org.koin.core.component.inject 24 | import org.koin.core.context.startKoin 25 | 26 | object DhizukuUserServiceRecycler : Recycler(), 27 | KoinComponent { 28 | class UserServiceProxy( 29 | private val connection: ServiceConnection, 30 | val service: IDhizukuUserService 31 | ) : UserService { 32 | override val privileged: IPrivilegedService = service.privilegedService 33 | 34 | override fun close() { 35 | Dhizuku.unbindUserService(connection) 36 | } 37 | } 38 | 39 | class DhizukuUserService @Keep constructor(context: Context) : IDhizukuUserService.Stub() { 40 | init { 41 | startKoin { 42 | modules(processModules) 43 | androidContext(context) 44 | } 45 | } 46 | 47 | private val privileged = DhizukuPrivilegedService() 48 | 49 | override fun getPrivilegedService(): IPrivilegedService = privileged 50 | } 51 | 52 | private val context by inject() 53 | 54 | override fun onMake(): UserServiceProxy = runBlocking { 55 | requireDhizukuPermissionGranted { 56 | onInnerMake() 57 | } 58 | } 59 | 60 | private suspend fun onInnerMake(): UserServiceProxy = callbackFlow { 61 | val connection = object : ServiceConnection { 62 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 63 | trySend(UserServiceProxy(this, IDhizukuUserService.Stub.asInterface(service))) 64 | service?.linkToDeath({ 65 | if (entity?.service == service) recycleForcibly() 66 | }, 0) 67 | } 68 | 69 | override fun onServiceDisconnected(name: ComponentName?) { 70 | close() 71 | } 72 | } 73 | Dhizuku.bindUserService( 74 | DhizukuUserServiceArgs( 75 | ComponentName( 76 | context, DhizukuUserService::class.java 77 | ) 78 | ), connection 79 | ) 80 | awaitClose { } 81 | }.first() 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/model/impl/ProcessUserServiceRecycler.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.model.impl 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import androidx.annotation.Keep 6 | import com.rosan.app_process.AppProcess 7 | import com.rosan.installer.IAppProcessService 8 | import com.rosan.installer.IPrivilegedService 9 | import com.rosan.installer.data.recycle.model.entity.DefaultPrivilegedService 10 | import com.rosan.installer.data.recycle.repo.recyclable.UserService 11 | import com.rosan.installer.data.recycle.repo.Recycler 12 | import com.rosan.installer.di.init.processModules 13 | import org.koin.android.ext.koin.androidContext 14 | import org.koin.core.component.KoinComponent 15 | import org.koin.core.component.inject 16 | import org.koin.core.context.startKoin 17 | import kotlin.system.exitProcess 18 | 19 | class ProcessUserServiceRecycler(private val shell: String) : 20 | Recycler(), KoinComponent { 21 | class UserServiceProxy(val service: IAppProcessService) : UserService { 22 | override val privileged: IPrivilegedService = service.privilegedService 23 | 24 | override fun close() = service.quit() 25 | } 26 | 27 | class AppProcessService @Keep constructor(context: Context) : IAppProcessService.Stub() { 28 | init { 29 | startKoin { 30 | modules(processModules) 31 | androidContext(context) 32 | } 33 | } 34 | 35 | private val privileged = DefaultPrivilegedService() 36 | 37 | override fun quit() { 38 | exitProcess(0) 39 | } 40 | 41 | override fun getPrivilegedService(): IPrivilegedService = privileged 42 | } 43 | 44 | private val context by inject() 45 | 46 | private lateinit var appProcessRecycler: Recycler 47 | 48 | override fun onMake(): UserServiceProxy { 49 | appProcessRecycler = AppProcessRecyclers.get(shell) 50 | val binder = appProcessRecycler.make().entity.startProcess( 51 | ComponentName( 52 | context, AppProcessService::class.java 53 | ) 54 | ) 55 | binder.linkToDeath({ 56 | if (entity?.service == binder) recycleForcibly() 57 | }, 0) 58 | return UserServiceProxy(IAppProcessService.Stub.asInterface(binder)) 59 | } 60 | 61 | override fun onRecycle() { 62 | super.onRecycle() 63 | appProcessRecycler.recycle() 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/model/impl/ProcessUserServiceRecyclers.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.model.impl 2 | 3 | object ProcessUserServiceRecyclers { 4 | private val map = 5 | mutableMapOf() 6 | 7 | fun get(shell: String): ProcessUserServiceRecycler { 8 | return map.getOrPut(shell) { 9 | ProcessUserServiceRecycler(shell) 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/model/impl/ShizukuUserServiceRecycler.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.model.impl 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.ServiceConnection 6 | import android.os.IBinder 7 | import androidx.annotation.Keep 8 | import com.rosan.installer.IPrivilegedService 9 | import com.rosan.installer.IShizukuUserService 10 | import com.rosan.installer.data.recycle.model.entity.DefaultPrivilegedService 11 | import com.rosan.installer.data.recycle.repo.recyclable.UserService 12 | import com.rosan.installer.data.recycle.repo.Recycler 13 | import com.rosan.installer.data.recycle.util.requireShizukuPermissionGranted 14 | import com.rosan.installer.di.init.processModules 15 | import kotlinx.coroutines.channels.awaitClose 16 | import kotlinx.coroutines.flow.callbackFlow 17 | import kotlinx.coroutines.flow.first 18 | import kotlinx.coroutines.runBlocking 19 | import org.koin.android.ext.koin.androidContext 20 | import org.koin.core.component.KoinComponent 21 | import org.koin.core.component.inject 22 | import org.koin.core.context.startKoin 23 | import rikka.shizuku.Shizuku 24 | import kotlin.system.exitProcess 25 | 26 | object ShizukuUserServiceRecycler : Recycler(), 27 | KoinComponent { 28 | class UserServiceProxy(val service: IShizukuUserService) : UserService { 29 | override val privileged: IPrivilegedService = service.privilegedService 30 | 31 | override fun close() = service.destroy() 32 | } 33 | 34 | class ShizukuUserService @Keep constructor(context: Context) : IShizukuUserService.Stub() { 35 | init { 36 | startKoin { 37 | modules(processModules) 38 | androidContext(context) 39 | } 40 | } 41 | 42 | private val privileged = DefaultPrivilegedService() 43 | 44 | override fun destroy() { 45 | exitProcess(0) 46 | } 47 | 48 | override fun getPrivilegedService(): IPrivilegedService = privileged 49 | } 50 | 51 | private val context by inject() 52 | 53 | override fun onMake(): UserServiceProxy = runBlocking { 54 | requireShizukuPermissionGranted { 55 | onInnerMake() 56 | } 57 | } 58 | 59 | private suspend fun onInnerMake(): UserServiceProxy = callbackFlow { 60 | Shizuku.bindUserService(Shizuku.UserServiceArgs( 61 | ComponentName( 62 | context, ShizukuUserService::class.java 63 | ) 64 | ).processNameSuffix("shizuku_privileged"), object : ServiceConnection { 65 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 66 | trySend(UserServiceProxy(IShizukuUserService.Stub.asInterface(service))) 67 | service?.linkToDeath({ 68 | if (entity?.service == service) recycleForcibly() 69 | }, 0) 70 | } 71 | 72 | override fun onServiceDisconnected(name: ComponentName?) { 73 | close() 74 | } 75 | }) 76 | awaitClose { } 77 | }.first() 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/repo/Recyclable.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.repo 2 | 3 | import java.io.Closeable 4 | 5 | class Recyclable( 6 | // you can't use this after recycle func called 7 | val entity: T, 8 | private val recycler: Recycler<*>, 9 | ) : Closeable { 10 | private var recycled = false 11 | 12 | fun recycle() { 13 | synchronized(this) { 14 | if (recycled) return 15 | recycled = true 16 | recycler.recycle() 17 | } 18 | } 19 | 20 | override fun close() { 21 | recycle() 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/repo/Recycler.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.repo 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.Job 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.launch 8 | import okhttp3.internal.closeQuietly 9 | import java.io.Closeable 10 | 11 | abstract class Recycler { 12 | private val coroutineScope = CoroutineScope(Dispatchers.IO) 13 | 14 | protected var entity: T? = null 15 | 16 | protected var referenceCount = 0 17 | private set 18 | 19 | private var recycleJob: Job? = null 20 | 21 | protected val delayDuration = 15000L 22 | 23 | fun make(): Recyclable { 24 | synchronized(this) { 25 | val localEntity = entity ?: onMake().apply { 26 | entity = this 27 | } 28 | referenceCount += 1 29 | return Recyclable(localEntity, this) 30 | } 31 | } 32 | 33 | abstract fun onMake(): T 34 | 35 | fun recycle() { 36 | synchronized(this) { 37 | referenceCount -= 1 38 | if (referenceCount > 0) return 39 | recycleJob?.cancel() 40 | recycleJob = coroutineScope.launch { 41 | delay(delayDuration) 42 | if (referenceCount > 0) return@launch 43 | recycleForcibly() 44 | } 45 | } 46 | } 47 | 48 | fun recycleForcibly() { 49 | synchronized(this) { 50 | referenceCount = 0 51 | entity?.closeQuietly() 52 | entity = null 53 | } 54 | } 55 | 56 | open fun onRecycle() { 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/repo/recyclable/UserService.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.repo.recyclable 2 | 3 | import com.rosan.installer.IPrivilegedService 4 | import java.io.Closeable 5 | 6 | interface UserService : Closeable { 7 | val privileged: IPrivilegedService 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/util/DhizukuUtil.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.util 2 | 3 | import android.content.pm.PackageManager 4 | import com.rosan.dhizuku.api.Dhizuku 5 | import com.rosan.dhizuku.api.DhizukuRequestPermissionListener 6 | import com.rosan.installer.data.recycle.model.exception.DhizukuNotWorkException 7 | import kotlinx.coroutines.channels.awaitClose 8 | import kotlinx.coroutines.flow.callbackFlow 9 | import kotlinx.coroutines.flow.catch 10 | import kotlinx.coroutines.flow.first 11 | 12 | suspend fun requireDhizukuPermissionGranted(action: suspend () -> T): T { 13 | callbackFlow { 14 | Dhizuku.init() 15 | if (Dhizuku.isPermissionGranted()) send(Unit) 16 | else { 17 | Dhizuku.requestPermission(object : DhizukuRequestPermissionListener() { 18 | override fun onRequestPermission(grantResult: Int) { 19 | if (grantResult == PackageManager.PERMISSION_GRANTED) trySend(Unit) 20 | else close(Exception("dhizuku permission denied")) 21 | } 22 | }) 23 | } 24 | awaitClose() 25 | }.catch { 26 | throw DhizukuNotWorkException(it) 27 | }.first() 28 | return action() 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/util/PrivilegedUtil.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.util 2 | 3 | import android.content.ContentResolver 4 | import android.content.Intent 5 | import android.content.IntentFilter 6 | import android.util.Log 7 | import java.io.File 8 | 9 | fun Array.delete() { 10 | for (path in this) { 11 | Log.d("DELETE_PATH", "path: $path") 12 | val file = File( 13 | if (path.startsWith("/mnt/user/0/emulated/0")) { 14 | path.replace("/mnt/user/0/emulated/0", "/storage/emulated/0") 15 | } else path 16 | ) 17 | Log.d("DELETE_PATH", "file: $file") 18 | if (file.exists()) file.delete() 19 | } 20 | } 21 | 22 | val InstallIntentFilter = IntentFilter().apply { 23 | addAction(Intent.ACTION_MAIN) 24 | addAction(Intent.ACTION_VIEW) 25 | addAction(Intent.ACTION_INSTALL_PACKAGE) 26 | addCategory(Intent.CATEGORY_DEFAULT) 27 | addDataScheme(ContentResolver.SCHEME_CONTENT) 28 | addDataScheme(ContentResolver.SCHEME_FILE) 29 | addDataType("application/vnd.android.package-archive") 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/util/ShizukuUtil.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.util 2 | 3 | import android.content.pm.PackageManager 4 | import com.rosan.installer.BuildConfig 5 | import com.rosan.installer.data.recycle.model.exception.ShizukuNotWorkException 6 | import kotlinx.coroutines.channels.awaitClose 7 | import kotlinx.coroutines.flow.callbackFlow 8 | import kotlinx.coroutines.flow.catch 9 | import kotlinx.coroutines.flow.first 10 | import rikka.shizuku.Shizuku 11 | import rikka.sui.Sui 12 | 13 | suspend fun requireShizukuPermissionGranted(action: suspend () -> T): T { 14 | callbackFlow { 15 | Sui.init(BuildConfig.APPLICATION_ID) 16 | if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { 17 | send(Unit) 18 | awaitClose() 19 | } else { 20 | val requestCode = (Int.MIN_VALUE..Int.MAX_VALUE).random() 21 | val listener = 22 | Shizuku.OnRequestPermissionResultListener { _requestCode, grantResult -> 23 | if (_requestCode != requestCode) return@OnRequestPermissionResultListener 24 | if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) 25 | trySend(Unit) 26 | else close(Exception("sui/shizuku permission denied")) 27 | } 28 | Shizuku.addRequestPermissionResultListener(listener) 29 | Shizuku.requestPermission(requestCode) 30 | awaitClose { Shizuku.removeRequestPermissionResultListener(listener) } 31 | } 32 | }.catch { 33 | throw ShizukuNotWorkException(it) 34 | }.first() 35 | return action() 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/recycle/util/UserServiceUtil.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.recycle.util 2 | 3 | import android.util.Log 4 | import com.rosan.installer.IPrivilegedService 5 | import com.rosan.installer.data.recycle.model.entity.DefaultPrivilegedService 6 | import com.rosan.installer.data.recycle.model.impl.DhizukuUserServiceRecycler 7 | import com.rosan.installer.data.recycle.model.impl.ProcessUserServiceRecyclers 8 | import com.rosan.installer.data.recycle.model.impl.ShizukuUserServiceRecycler 9 | import com.rosan.installer.data.recycle.repo.recyclable.UserService 10 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 11 | 12 | private object DefaultUserService : UserService { 13 | override val privileged: IPrivilegedService = DefaultPrivilegedService() 14 | 15 | override fun close() { 16 | } 17 | } 18 | 19 | fun useUserService( 20 | config: ConfigEntity, 21 | special: (() -> String?)? = null, 22 | action: (UserService) -> Unit 23 | ) { 24 | // special为null时,遵循config 25 | val recycler = if (special == null) when (config.authorizer) { 26 | ConfigEntity.Authorizer.Root -> ProcessUserServiceRecyclers.get("su") 27 | .make() 28 | 29 | ConfigEntity.Authorizer.Shizuku -> ShizukuUserServiceRecycler.make() 30 | ConfigEntity.Authorizer.Dhizuku -> DhizukuUserServiceRecycler.make() 31 | ConfigEntity.Authorizer.Customize -> ProcessUserServiceRecyclers.get(config.customizeAuthorizer) 32 | .make() 33 | // 其余情况,不使用授权器 34 | else -> null 35 | } else { 36 | // special回调null时,不使用授权器 37 | special.invoke()?.let { ProcessUserServiceRecyclers.get(it).make() } 38 | } 39 | if (recycler != null) { 40 | Log.e("useUserService", "use Privileged Service: $recycler") 41 | recycler.use { action.invoke(it.entity) } 42 | } else { 43 | Log.e("useUserService", "Use Default User Service") 44 | action.invoke(DefaultUserService) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/reflect/repo/ReflectRepo.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.reflect.repo 2 | 3 | import java.lang.reflect.Constructor 4 | import java.lang.reflect.Field 5 | import java.lang.reflect.Method 6 | 7 | interface ReflectRepo { 8 | fun getConstructors(clazz: Class<*>): Array> 9 | 10 | fun getDeclaredConstructors(clazz: Class<*>): Array> 11 | 12 | fun getFields(clazz: Class<*>): Array 13 | 14 | fun getDeclaredFields(clazz: Class<*>): Array 15 | 16 | fun getMethods(clazz: Class<*>): Array 17 | 18 | fun getDeclaredMethods(clazz: Class<*>): Array 19 | 20 | fun getConstructor(clazz: Class<*>, vararg parameterTypes: Class<*>): Constructor<*>? 21 | 22 | fun getDeclaredConstructor( 23 | clazz: Class<*>, vararg parameterTypes: Class<*> 24 | ): Constructor<*>? 25 | 26 | fun getField(clazz: Class<*>, name: String): Field? 27 | 28 | fun getDeclaredField(clazz: Class<*>, name: String): Field? 29 | 30 | fun getMethod(clazz: Class<*>, name: String, vararg parameterTypes: Class<*>): Method? 31 | 32 | fun getDeclaredMethod(clazz: Class<*>, name: String, vararg parameterTypes: Class<*>): Method? 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/res/model/entity/Entry.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.res.model.entity 2 | 3 | interface Entry -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/res/model/entity/EntryEntity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.res.model.entity 2 | 3 | data class EntryEntity( 4 | val value: ValueEntity 5 | ) : Entry 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/res/model/entity/MapEntryEntity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.res.model.entity 2 | 3 | data class MapEntryEntity( 4 | val parent: Int, 5 | val valueMap: Map 6 | ) : Entry 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/res/model/entity/ValueEntity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.res.model.entity 2 | 3 | data class ValueEntity( 4 | val type: Type, 5 | val data: Int 6 | ) : Entry { 7 | enum class Type(val value: UByte) { 8 | NULL(0x00u), 9 | REFERENCE(0x01u), 10 | ATTRIBUTE(0x02u), 11 | STRING(0x03u), 12 | FLOAT(0x04u), 13 | DIMENSION(0x05u), 14 | FRACTION(0x06u), 15 | INT_DEC(0x10u), 16 | INT_HEX(0x11u), 17 | INT_BOOLEAN(0x12u), 18 | INT_COLOR_ARGB8(0x1cu), 19 | INT_COLOR_RGB8(0x1du), 20 | INT_COLOR_ARGB4(0x1eu), 21 | INT_COLOR_RGB4(0x1fu); 22 | 23 | companion object { 24 | const val FIRST_INT: UByte = 0x10u 25 | 26 | const val FIRST_COLOR_INT: UByte = 0x1cu 27 | 28 | const val LAST_COLOR_INT: UByte = 0x1fu 29 | 30 | const val LAST_INT: UByte = 0x1fu 31 | 32 | fun build(value: UByte): Type { 33 | return entries.find { it.value == value } ?: NULL 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/res/model/impl/AxmlTreeRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.res.model.impl 2 | 3 | import android.content.res.XmlResourceParser 4 | import com.rosan.installer.data.res.repo.AxmlTreeRepo 5 | import org.koin.core.component.KoinComponent 6 | import org.xmlpull.v1.XmlPullParser 7 | 8 | class AxmlTreeRepoImpl(private val xmlPull: XmlResourceParser, private val rootPath: String = "") : 9 | AxmlTreeRepo, 10 | KoinComponent { 11 | 12 | private val names = mutableListOf() 13 | 14 | private val registers = mutableMapOf Unit>() 15 | 16 | override fun register(path: String, action: XmlResourceParser.() -> Unit): AxmlTreeRepo { 17 | registers[path] = action 18 | return this 19 | } 20 | 21 | override fun unregister(path: String): AxmlTreeRepo { 22 | registers.remove(path) 23 | return this 24 | } 25 | 26 | private fun getCurrentPath(): String { 27 | return "$rootPath/${names.joinToString("/")}" 28 | } 29 | 30 | override fun map(action: XmlResourceParser.(path: String) -> Unit) { 31 | val startDepth = xmlPull.depth 32 | while (xmlPull.depth >= startDepth) { 33 | when (xmlPull.next()) { 34 | XmlPullParser.START_TAG -> { 35 | val namespace = xmlPull.namespace 36 | val name: String? = xmlPull.name 37 | if (namespace.isNullOrEmpty()) names.add("$name") 38 | else names.add("$namespace:$name") 39 | val path = getCurrentPath() 40 | registers.map { 41 | if (it.key == path) { 42 | it.value.let { xmlPull.it() } 43 | } 44 | } 45 | xmlPull.action(path) 46 | } 47 | XmlPullParser.END_TAG -> { 48 | names.removeLastOrNull() 49 | } 50 | XmlPullParser.END_DOCUMENT -> break 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/res/repo/ArscRepo.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.res.repo 2 | 3 | import android.content.res.Configuration 4 | import com.rosan.installer.data.res.model.entity.Entry 5 | 6 | /** 7 | * @id = 0xPPTTEEEE 8 | * @valueId = 0xEEEE 9 | * @typeId = 0xTT 10 | * @packageId = 0xPP 11 | * */ 12 | interface ArscRepo { 13 | fun getValue( 14 | id: Int, 15 | configuration: Configuration, 16 | densityDpi: Int? = null 17 | ): Entry? = getValue(id2PackageId(id), id2TypeId(id), id2ValueId(id), configuration, densityDpi) 18 | 19 | fun getValue( 20 | packageId: Int, 21 | typeId: Int, 22 | valueId: Int, 23 | configuration: Configuration, 24 | densityDpi: Int? = null 25 | ): Entry? 26 | 27 | fun getValueName( 28 | id: Int, 29 | configuration: Configuration, 30 | densityDpi: Int? = null 31 | ): CharSequence? = 32 | getValueName(id2PackageId(id), id2TypeId(id), id2ValueId(id), configuration, densityDpi) 33 | 34 | fun getValueName( 35 | packageId: Int, typeId: Int, valueId: Int, 36 | configuration: Configuration, 37 | densityDpi: Int? = null 38 | ): CharSequence? 39 | 40 | fun getTypeName(packageId: Int, typeId: Int): CharSequence? 41 | 42 | fun getPackageName(packageId: Int): CharSequence? 43 | 44 | fun getString(index: Int): CharSequence? 45 | 46 | companion object { 47 | fun id2PackageId(id: Int): Int { 48 | return id shr 24 49 | } 50 | 51 | fun id2TypeId(id: Int): Int { 52 | return (id shr 16) % 0x100 53 | } 54 | 55 | fun id2ValueId(id: Int): Int { 56 | return id % 0x10000 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/res/repo/AxmlPullRepo.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.res.repo 2 | 3 | import android.content.res.XmlResourceParser 4 | import android.util.TypedValue 5 | 6 | interface AxmlPullRepo : XmlResourceParser { 7 | fun getAttributeTypedValue(index: Int): TypedValue? 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/res/repo/AxmlTreeRepo.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.res.repo 2 | 3 | import android.content.res.XmlResourceParser 4 | 5 | interface AxmlTreeRepo { 6 | companion object { 7 | const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android" 8 | } 9 | 10 | fun register(path: String, action: XmlResourceParser.() -> Unit): AxmlTreeRepo 11 | 12 | fun unregister(path: String): AxmlTreeRepo 13 | 14 | fun map(action: XmlResourceParser.(path: String) -> Unit) 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/model/room/InstallerRoom.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.model.room 2 | 3 | import androidx.room.AutoMigration 4 | import androidx.room.Database 5 | import androidx.room.DeleteColumn 6 | import androidx.room.Room 7 | import androidx.room.RoomDatabase 8 | import androidx.room.TypeConverters 9 | import androidx.room.migration.AutoMigrationSpec 10 | import androidx.sqlite.db.SupportSQLiteDatabase 11 | import com.rosan.installer.data.settings.model.room.dao.AppDao 12 | import com.rosan.installer.data.settings.model.room.dao.ConfigDao 13 | import com.rosan.installer.data.settings.model.room.entity.AppEntity 14 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 15 | import com.rosan.installer.data.settings.model.room.entity.converter.AuthorizerConverter 16 | import com.rosan.installer.data.settings.model.room.entity.converter.InstallModeConverter 17 | import kotlinx.coroutines.CoroutineScope 18 | import kotlinx.coroutines.Dispatchers 19 | import kotlinx.coroutines.launch 20 | import org.koin.core.component.KoinComponent 21 | import org.koin.core.component.get 22 | 23 | @Database( 24 | entities = [AppEntity::class, ConfigEntity::class], 25 | version = 4, 26 | exportSchema = true, 27 | autoMigrations = [ 28 | AutoMigration(from = 1, to = 2), 29 | AutoMigration(from = 2, to = 3, spec = InstallerRoom.AUTO_MIGRATION_2_3::class), 30 | AutoMigration(from = 3, to = 4), 31 | ] 32 | ) 33 | @TypeConverters( 34 | AuthorizerConverter::class, 35 | InstallModeConverter::class 36 | ) 37 | abstract class InstallerRoom : RoomDatabase() { 38 | @DeleteColumn(tableName = "config", columnName = "analyser") 39 | @DeleteColumn(tableName = "config", columnName = "compat_mode") 40 | class AUTO_MIGRATION_2_3 : AutoMigrationSpec 41 | 42 | companion object : KoinComponent { 43 | 44 | fun createInstance(): InstallerRoom { 45 | return Room.databaseBuilder( 46 | get(), 47 | InstallerRoom::class.java, 48 | "installer.db", 49 | ) 50 | .addCallback(object : Callback() { 51 | override fun onCreate(db: SupportSQLiteDatabase) { 52 | // 延迟获取已初始化的 Database 实例 53 | val database = get() 54 | CoroutineScope(Dispatchers.IO).launch { 55 | database.configDao.insert(ConfigEntity.default) 56 | } 57 | } 58 | }) 59 | .build() 60 | } 61 | } 62 | 63 | abstract val appDao: AppDao 64 | 65 | abstract val configDao: ConfigDao 66 | } 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/model/room/dao/AppDao.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.model.room.dao 2 | 3 | import androidx.room.* 4 | import com.rosan.installer.data.settings.model.room.entity.AppEntity 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | @Dao 8 | interface AppDao { 9 | @Query("select * from app") 10 | fun all(): List 11 | 12 | @Query("select * from app") 13 | fun flowAll(): Flow> 14 | 15 | @Query("select * from app where id = :id limit 1") 16 | fun find(id: Long): AppEntity? 17 | 18 | @Query("select * from app where id = :id limit 1") 19 | fun flowFind(id: Long): Flow 20 | 21 | @Query("select * from app where package_name = :packageName limit 1") 22 | fun findByPackageName(packageName: String): AppEntity? 23 | 24 | @Query("select * from app where package_name is null limit 1") 25 | fun findByNullPackageName(): AppEntity? 26 | 27 | @Query("select * from app where package_name = :packageName limit 1") 28 | fun flowFindByPackageName(packageName: String): Flow 29 | 30 | @Query("select * from app where package_name is null limit 1") 31 | fun flowFindByNullPackageName(): Flow 32 | 33 | @Update 34 | suspend fun update(appEntity: AppEntity) 35 | 36 | @Insert 37 | suspend fun insert(appEntity: AppEntity) 38 | 39 | @Delete 40 | suspend fun delete(appEntity: AppEntity) 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/model/room/dao/ConfigDao.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.model.room.dao 2 | 3 | import androidx.room.* 4 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | @Dao 8 | interface ConfigDao { 9 | @Query("select * from config") 10 | suspend fun all(): List 11 | 12 | @Query("select * from config") 13 | fun flowAll(): Flow> 14 | 15 | @Query("select * from config where id = :id limit 1") 16 | suspend fun find(id: Long): ConfigEntity? 17 | 18 | @Query("select * from config where id = :id limit 1") 19 | fun flowFind(id: Long): Flow 20 | 21 | @Update 22 | suspend fun update(entity: ConfigEntity) 23 | 24 | @Insert 25 | suspend fun insert(entity: ConfigEntity) 26 | 27 | @Delete 28 | suspend fun delete(entity: ConfigEntity) 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/model/room/entity/AppEntity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.model.room.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.ForeignKey 6 | import androidx.room.Index 7 | import androidx.room.PrimaryKey 8 | 9 | @Entity( 10 | tableName = "app", 11 | indices = [ 12 | Index(value = ["package_name"], unique = true), 13 | Index(value = ["config_id"]) 14 | ], 15 | foreignKeys = [ 16 | ForeignKey( 17 | entity = ConfigEntity::class, 18 | parentColumns = ["id"], 19 | childColumns = ["config_id"], 20 | onDelete = ForeignKey.CASCADE 21 | ) 22 | ] 23 | ) 24 | data class AppEntity( 25 | @PrimaryKey(autoGenerate = true) 26 | @ColumnInfo(name = "id") 27 | var id: Long = 0L, 28 | @ColumnInfo(name = "package_name") var packageName: String?, 29 | @ColumnInfo(name = "config_id") var configId: Long, 30 | @ColumnInfo(name = "created_at") var createdAt: Long = System.currentTimeMillis(), 31 | @ColumnInfo(name = "modified_at") var modifiedAt: Long = System.currentTimeMillis(), 32 | ) { 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/model/room/entity/ConfigEntity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.model.room.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import org.koin.core.component.KoinComponent 7 | 8 | @Entity( 9 | tableName = "config", 10 | indices = [ 11 | ] 12 | ) 13 | data class ConfigEntity( 14 | @PrimaryKey(autoGenerate = true) 15 | @ColumnInfo(name = "id") 16 | var id: Long = 0L, 17 | @ColumnInfo(name = "name", defaultValue = "'Default'") var name: String = "Default", 18 | @ColumnInfo(name = "description") var description: String, 19 | @ColumnInfo(name = "authorizer") var authorizer: Authorizer, 20 | @ColumnInfo(name = "customize_authorizer") var customizeAuthorizer: String, 21 | @ColumnInfo(name = "install_mode") var installMode: InstallMode, 22 | @ColumnInfo(name = "installer") var installer: String?, 23 | @ColumnInfo(name = "for_all_user") var forAllUser: Boolean, 24 | @ColumnInfo(name = "allow_test_only") var allowTestOnly: Boolean, 25 | @ColumnInfo(name = "allow_downgrade") var allowDowngrade: Boolean, 26 | @ColumnInfo(name = "auto_delete") var autoDelete: Boolean, 27 | @ColumnInfo(name = "display_sdk", defaultValue = "0") var displaySdk: Boolean = false, 28 | @ColumnInfo(name = "created_at") var createdAt: Long = System.currentTimeMillis(), 29 | @ColumnInfo(name = "modified_at") var modifiedAt: Long = System.currentTimeMillis(), 30 | ) { 31 | companion object : KoinComponent { 32 | var default = ConfigEntity( 33 | name = "Default", 34 | description = "", 35 | authorizer = Authorizer.Global, 36 | customizeAuthorizer = "", 37 | installMode = InstallMode.Global, 38 | installer = null, 39 | forAllUser = false, 40 | allowTestOnly = false, 41 | allowDowngrade = false, 42 | autoDelete = false, 43 | displaySdk = false, 44 | ) 45 | } 46 | 47 | val isCustomizeAuthorizer: Boolean 48 | get() = authorizer == Authorizer.Customize 49 | 50 | enum class Authorizer(val value: String) { 51 | Global("global"), 52 | None("none"), 53 | Root("root"), 54 | Shizuku("shizuku"), 55 | Dhizuku("dhizuku"), 56 | Customize("customize"); 57 | } 58 | 59 | enum class InstallMode(val value: String) { 60 | Global("global"), 61 | Dialog("dialog"), 62 | AutoDialog("auto_dialog"), 63 | Notification("notification"), 64 | AutoNotification("auto_notification"), 65 | Ignore("ignore"); 66 | } 67 | 68 | enum class Analyser(val value: String) { 69 | R0s("r0s"), 70 | System("system"); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/model/room/entity/converter/AnalyserConverter.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.model.room.entity.converter 2 | 3 | import androidx.room.TypeConverter 4 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 5 | 6 | object AnalyserConverter { 7 | @TypeConverter 8 | fun revert(value: String): ConfigEntity.Analyser = 9 | ConfigEntity.Analyser.values().find { it.value == value } 10 | ?: ConfigEntity.Analyser.R0s 11 | 12 | @TypeConverter 13 | fun convert(value: ConfigEntity.Analyser): String = value.value 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/model/room/entity/converter/AuthorizerConverter.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.model.room.entity.converter 2 | 3 | import androidx.room.TypeConverter 4 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 5 | 6 | object AuthorizerConverter { 7 | @TypeConverter 8 | fun revert(value: String?): ConfigEntity.Authorizer = 9 | (if (value != null) ConfigEntity.Authorizer.values().find { it.value == value } 10 | else null) ?: ConfigEntity.Authorizer.Shizuku 11 | 12 | @TypeConverter 13 | fun convert(value: ConfigEntity.Authorizer): String = value.value 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/model/room/entity/converter/InstallModeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.model.room.entity.converter 2 | 3 | import androidx.room.TypeConverter 4 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 5 | 6 | object InstallModeConverter { 7 | @TypeConverter 8 | fun revert(value: String?): ConfigEntity.InstallMode = 9 | (if (value != null) ConfigEntity.InstallMode.entries.find { it.value == value } 10 | else null) ?: ConfigEntity.InstallMode.Dialog 11 | 12 | @TypeConverter 13 | fun convert(value: ConfigEntity.InstallMode): String = value.value 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/model/room/repo/ConfigRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.model.room.repo 2 | 3 | import com.rosan.installer.data.settings.model.room.dao.ConfigDao 4 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 5 | import com.rosan.installer.data.settings.repo.ConfigRepo 6 | import com.rosan.installer.data.settings.util.ConfigOrder 7 | import com.rosan.installer.data.settings.util.OrderType 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.map 10 | 11 | class ConfigRepoImpl( 12 | private val dao: ConfigDao 13 | ) : ConfigRepo { 14 | override suspend fun all(order: ConfigOrder): List { 15 | val configs = dao.all() 16 | return when (order.orderType) { 17 | OrderType.Ascending -> { 18 | when (order) { 19 | is ConfigOrder.Id -> configs.sortedBy { it.id } 20 | is ConfigOrder.Name -> configs.sortedBy { it.name } 21 | is ConfigOrder.CreatedAt -> configs.sortedBy { it.createdAt } 22 | is ConfigOrder.ModifiedAt -> configs.sortedBy { it.modifiedAt } 23 | } 24 | } 25 | OrderType.Descending -> { 26 | when (order) { 27 | is ConfigOrder.Id -> configs.sortedByDescending { it.id } 28 | is ConfigOrder.Name -> configs.sortedByDescending { it.name } 29 | is ConfigOrder.CreatedAt -> configs.sortedByDescending { it.createdAt } 30 | is ConfigOrder.ModifiedAt -> configs.sortedByDescending { it.modifiedAt } 31 | } 32 | } 33 | } 34 | } 35 | 36 | override fun flowAll(order: ConfigOrder): Flow> { 37 | return dao.flowAll().map { configs -> 38 | when (order.orderType) { 39 | OrderType.Ascending -> { 40 | when (order) { 41 | is ConfigOrder.Id -> configs.sortedBy { it.id } 42 | is ConfigOrder.Name -> configs.sortedBy { it.name } 43 | is ConfigOrder.CreatedAt -> configs.sortedBy { it.createdAt } 44 | is ConfigOrder.ModifiedAt -> configs.sortedBy { it.modifiedAt } 45 | } 46 | } 47 | OrderType.Descending -> { 48 | when (order) { 49 | is ConfigOrder.Id -> configs.sortedByDescending { it.id } 50 | is ConfigOrder.Name -> configs.sortedByDescending { it.name } 51 | is ConfigOrder.CreatedAt -> configs.sortedByDescending { it.createdAt } 52 | is ConfigOrder.ModifiedAt -> configs.sortedByDescending { it.modifiedAt } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | override suspend fun find(id: Long): ConfigEntity? { 60 | return dao.find(id) 61 | } 62 | 63 | override fun flowFind(id: Long): Flow { 64 | return dao.flowFind(id) 65 | } 66 | 67 | override suspend fun update(entity: ConfigEntity) { 68 | entity.modifiedAt = System.currentTimeMillis() 69 | dao.update(entity) 70 | } 71 | 72 | override suspend fun insert(entity: ConfigEntity) { 73 | entity.createdAt = System.currentTimeMillis() 74 | entity.modifiedAt = System.currentTimeMillis() 75 | dao.insert(entity) 76 | } 77 | 78 | override suspend fun delete(entity: ConfigEntity) { 79 | dao.delete(entity) 80 | } 81 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/repo/AppRepo.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.repo 2 | 3 | import com.rosan.installer.data.settings.model.room.entity.AppEntity 4 | import com.rosan.installer.data.settings.util.AppOrder 5 | import com.rosan.installer.data.settings.util.OrderType 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | interface AppRepo { 9 | fun all(order: AppOrder = AppOrder.Id(OrderType.Ascending)): List 10 | 11 | fun flowAll(order: AppOrder = AppOrder.Id(OrderType.Ascending)): Flow> 12 | 13 | fun find(id: Long): AppEntity? 14 | 15 | fun flowFind(id: Long): Flow 16 | 17 | fun findByPackageName(packageName: String?): AppEntity? 18 | 19 | fun flowFindByPackageName(packageName: String?): Flow 20 | 21 | suspend fun update(entity: AppEntity) 22 | 23 | suspend fun insert(entity: AppEntity) 24 | 25 | suspend fun delete(entity: AppEntity) 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/repo/ConfigRepo.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.repo 2 | 3 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 4 | import com.rosan.installer.data.settings.util.ConfigOrder 5 | import com.rosan.installer.data.settings.util.OrderType 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | interface ConfigRepo { 9 | suspend fun all(order: ConfigOrder = ConfigOrder.Id(OrderType.Ascending)): List 10 | 11 | fun flowAll(order: ConfigOrder = ConfigOrder.Id(OrderType.Ascending)): Flow> 12 | 13 | suspend fun find(id: Long): ConfigEntity? 14 | 15 | fun flowFind(id: Long): Flow 16 | 17 | suspend fun update(entity: ConfigEntity) 18 | 19 | suspend fun insert(entity: ConfigEntity) 20 | 21 | suspend fun delete(entity: ConfigEntity) 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/util/AppOrder.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.util 2 | 3 | sealed class AppOrder(val orderType: OrderType) { 4 | class Id(orderType: OrderType) : AppOrder(orderType) 5 | class PackageName(orderType: OrderType) : AppOrder(orderType) 6 | class ConfigId(orderType: OrderType) : AppOrder(orderType) 7 | class CreateAt(orderType: OrderType) : AppOrder(orderType) 8 | class ModifiedAt(orderType: OrderType) : AppOrder(orderType) 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/util/ConfigOrder.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.util 2 | 3 | sealed class ConfigOrder(val orderType: OrderType) { 4 | class Id(orderType: OrderType) : ConfigOrder(orderType) 5 | class Name(orderType: OrderType) : ConfigOrder(orderType) 6 | class CreatedAt(orderType: OrderType) : ConfigOrder(orderType) 7 | class ModifiedAt(orderType: OrderType) : ConfigOrder(orderType) 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/util/ConfigUtil.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.util 2 | 3 | import android.content.Context 4 | import com.rosan.installer.data.settings.model.room.entity.AppEntity 5 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 6 | import com.rosan.installer.data.settings.model.room.entity.converter.AuthorizerConverter 7 | import com.rosan.installer.data.settings.model.room.entity.converter.InstallModeConverter 8 | import com.rosan.installer.data.settings.repo.AppRepo 9 | import com.rosan.installer.data.settings.repo.ConfigRepo 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.withContext 12 | import org.koin.core.component.KoinComponent 13 | import org.koin.core.component.get 14 | import org.koin.core.component.inject 15 | 16 | class ConfigUtil { 17 | companion object : KoinComponent { 18 | private val context by inject() 19 | 20 | private val sharedPreferences = context.getSharedPreferences("app", Context.MODE_PRIVATE) 21 | 22 | val globalAuthorizer: ConfigEntity.Authorizer 23 | get() = AuthorizerConverter.revert(sharedPreferences.getString("authorizer", null)) 24 | 25 | val globalCustomizeAuthorizer: String 26 | get() = sharedPreferences.getString("customize_authorizer", null) ?: "" 27 | 28 | val globalInstallMode: ConfigEntity.InstallMode 29 | get() = InstallModeConverter.revert(sharedPreferences.getString("install_mode", null)) 30 | 31 | suspend fun getByPackageName(packageName: String? = null): ConfigEntity { 32 | var entity = getByPackageNameInner(packageName) 33 | if (entity.authorizer == ConfigEntity.Authorizer.Global) 34 | entity = entity.copy( 35 | authorizer = globalAuthorizer, 36 | customizeAuthorizer = globalCustomizeAuthorizer 37 | ) 38 | if (entity.installMode == ConfigEntity.InstallMode.Global) 39 | entity = entity.copy(installMode = globalInstallMode) 40 | return entity 41 | } 42 | 43 | private suspend fun getByPackageNameInner(packageName: String? = null): ConfigEntity = 44 | withContext(Dispatchers.IO) { 45 | val repo = get() 46 | val app = getAppByPackageName(packageName) 47 | var config: ConfigEntity? = null 48 | if (app != null) config = repo.find(app.configId) 49 | if (config != null) return@withContext config 50 | config = repo.all().firstOrNull() 51 | if (config != null) return@withContext config 52 | return@withContext ConfigEntity.default 53 | } 54 | 55 | private fun getAppByPackageName(packageName: String? = null): AppEntity? { 56 | val repo = get() 57 | var app: AppEntity? = repo.findByPackageName(packageName) 58 | if (app != null) return app 59 | if (packageName != null) app = repo.findByPackageName(null) 60 | if (app != null) return app 61 | return null 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/data/settings/util/OrderType.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.data.settings.util 2 | 3 | enum class OrderType { 4 | Ascending, 5 | Descending 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/di/init/app_modules.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.di.init 2 | 3 | import com.rosan.installer.di.installerModule 4 | import com.rosan.installer.di.reflectModule 5 | import com.rosan.installer.di.roomModule 6 | import com.rosan.installer.di.serializationModule 7 | import com.rosan.installer.di.viewModelModule 8 | import com.rosan.installer.di.workerModule 9 | 10 | val appModules = listOf( 11 | roomModule, 12 | viewModelModule, 13 | serializationModule, 14 | workerModule, 15 | installerModule, 16 | reflectModule 17 | ) 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/di/init/process_modules.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.di.init 2 | 3 | import com.rosan.installer.di.reflectModule 4 | import com.rosan.installer.di.serializationModule 5 | 6 | val processModules = listOf( 7 | serializationModule, 8 | reflectModule 9 | ) 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/di/installer_module.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.di 2 | 3 | import com.rosan.installer.data.installer.model.impl.InstallerRepoImpl 4 | import com.rosan.installer.data.installer.repo.InstallerRepo 5 | import org.koin.dsl.module 6 | 7 | val installerModule = module { 8 | factory { 9 | val id = getOrNull() 10 | InstallerRepoImpl.getOrCreate(id) 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/di/reflect_module.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.di 2 | 3 | import android.os.Build 4 | import com.rosan.installer.data.reflect.model.impl.ReflectRepoImpl 5 | import com.rosan.installer.data.reflect.repo.ReflectRepo 6 | import org.koin.dsl.module 7 | import org.lsposed.hiddenapibypass.HiddenApiBypass 8 | 9 | val reflectModule = module { 10 | single { 11 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) 12 | HiddenApiBypass.addHiddenApiExemptions("") 13 | ReflectRepoImpl() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/di/room_module.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.di 2 | 3 | import com.rosan.installer.data.settings.model.room.InstallerRoom 4 | import com.rosan.installer.data.settings.model.room.repo.AppRepoImpl 5 | import com.rosan.installer.data.settings.model.room.repo.ConfigRepoImpl 6 | import com.rosan.installer.data.settings.repo.AppRepo 7 | import com.rosan.installer.data.settings.repo.ConfigRepo 8 | import org.koin.dsl.module 9 | 10 | val roomModule = module { 11 | single { 12 | InstallerRoom.createInstance() 13 | } 14 | 15 | single { 16 | val roomDatabase by inject() 17 | AppRepoImpl(roomDatabase.appDao) 18 | } 19 | 20 | single { 21 | val roomDatabase by inject() 22 | ConfigRepoImpl(roomDatabase.configDao) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/di/serialization_module.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.di 2 | 3 | import kotlinx.serialization.json.Json 4 | import kotlinx.serialization.modules.SerializersModule 5 | import org.koin.dsl.module 6 | 7 | val serializationModule = module { 8 | single { 9 | SerializersModule { 10 | } 11 | } 12 | 13 | single { 14 | Json { 15 | serializersModule = get() 16 | ignoreUnknownKeys = true 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/di/viewmodel_module.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.di 2 | 3 | import androidx.navigation.NavController 4 | import com.rosan.installer.data.installer.repo.InstallerRepo 5 | import com.rosan.installer.ui.page.installer.dialog.DialogViewModel 6 | import com.rosan.installer.ui.page.settings.config.all.AllViewModel 7 | import com.rosan.installer.ui.page.settings.config.apply.ApplyViewModel 8 | import com.rosan.installer.ui.page.settings.config.edit.EditViewModel 9 | import com.rosan.installer.ui.page.settings.preferred.PreferredViewModel 10 | import org.koin.core.module.dsl.viewModel 11 | import org.koin.dsl.module 12 | 13 | val viewModelModule = module { 14 | viewModel { (installer: InstallerRepo) -> 15 | DialogViewModel(installer) 16 | } 17 | 18 | viewModel { 19 | PreferredViewModel() 20 | } 21 | 22 | viewModel { (navController: NavController) -> 23 | AllViewModel(navController, get()) 24 | } 25 | 26 | viewModel { (id: Long?) -> 27 | EditViewModel(get(), id) 28 | } 29 | 30 | viewModel { (id: Long) -> 31 | ApplyViewModel(get(), get(), id) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/di/worker_module.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.di 2 | 3 | import androidx.work.WorkManager 4 | import org.koin.dsl.module 5 | 6 | val workerModule = module { 7 | single { 8 | WorkManager.getInstance(get()) 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/test.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer 2 | 3 | fun main() { 4 | } 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/activity/AboutPageActivity.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.activity 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.ui.Modifier 10 | import com.rosan.installer.ui.page.settings.home.HomePage 11 | import com.rosan.installer.ui.theme.InstallerTheme 12 | import org.koin.core.component.KoinComponent 13 | 14 | class AboutPageActivity : ComponentActivity(), KoinComponent { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | enableEdgeToEdge() 17 | super.onCreate(savedInstanceState) 18 | setContent { 19 | // A surface based on material design theme. 20 | InstallerTheme { 21 | Surface( 22 | modifier = Modifier 23 | .fillMaxSize() 24 | ) { 25 | // AgreementDialog() 26 | 27 | HomePage() 28 | } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/common/ViewContent.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.common 2 | 3 | data class ViewContent( 4 | val data: T, 5 | val progress: Progress 6 | ) { 7 | sealed class Progress { 8 | object Loading : Progress() 9 | object Loaded : Progress() 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/InstallerPage.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.rosan.installer.data.installer.repo.InstallerRepo 5 | import com.rosan.installer.ui.page.installer.dialog.DialogPage 6 | 7 | @Composable 8 | fun InstallerPage(installer: InstallerRepo) { 9 | DialogPage(installer = installer) 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/DialogInnerParams.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | data class DialogInnerParams( 6 | val id: String, 7 | val content: (@Composable () -> Unit)? = null 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/DialogInnerWidget.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.animation.AnimatedContent 5 | import androidx.compose.runtime.Composable 6 | import com.rosan.installer.data.installer.repo.InstallerRepo 7 | import com.rosan.installer.ui.page.installer.dialog.inner.analyseFailedDialog 8 | import com.rosan.installer.ui.page.installer.dialog.inner.analysingDialog 9 | import com.rosan.installer.ui.page.installer.dialog.inner.installChoiceDialog 10 | import com.rosan.installer.ui.page.installer.dialog.inner.installFailedDialog 11 | import com.rosan.installer.ui.page.installer.dialog.inner.installPrepareDialog 12 | import com.rosan.installer.ui.page.installer.dialog.inner.installSuccessDialog 13 | import com.rosan.installer.ui.page.installer.dialog.inner.installingDialog 14 | import com.rosan.installer.ui.page.installer.dialog.inner.readyDialog 15 | import com.rosan.installer.ui.page.installer.dialog.inner.resolveFailedDialog 16 | import com.rosan.installer.ui.page.installer.dialog.inner.resolvingDialog 17 | 18 | // change the content when the id been changed 19 | @SuppressLint("UnusedContentLambdaTargetStateParameter") 20 | fun dialogInnerWidget( 21 | installer: InstallerRepo, 22 | params: DialogInnerParams 23 | ): @Composable (() -> Unit)? = 24 | if (params.content == null) null 25 | else { 26 | { 27 | AnimatedContent( 28 | targetState = "${installer.id}_${params.id}" 29 | ) { 30 | params.content.invoke() 31 | } 32 | } 33 | } 34 | 35 | @Composable 36 | fun dialogGenerateParams( 37 | installer: InstallerRepo, viewModel: DialogViewModel 38 | ): DialogParams { 39 | return when (viewModel.state) { 40 | is DialogViewState.Ready -> readyDialog(installer, viewModel) 41 | is DialogViewState.Resolving -> resolvingDialog(installer, viewModel) 42 | is DialogViewState.ResolveFailed -> resolveFailedDialog(installer, viewModel) 43 | is DialogViewState.Analysing -> analysingDialog(installer, viewModel) 44 | is DialogViewState.AnalyseFailed -> analyseFailedDialog(installer, viewModel) 45 | is DialogViewState.InstallChoice -> installChoiceDialog(installer, viewModel) 46 | is DialogViewState.InstallPrepare -> installPrepareDialog(installer, viewModel) 47 | is DialogViewState.Installing -> installingDialog(installer, viewModel) 48 | is DialogViewState.InstallSuccess -> installSuccessDialog(installer, viewModel) 49 | is DialogViewState.InstallFailed -> installFailedDialog(installer, viewModel) 50 | // when is exhaustive, so no need to handle the else case 51 | // else -> readyDialog(installer, viewModel) 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/DialogPage.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog 2 | 3 | import androidx.compose.animation.animateContentSize 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.LaunchedEffect 6 | import androidx.compose.ui.Modifier 7 | import com.rosan.installer.data.installer.repo.InstallerRepo 8 | import com.rosan.installer.ui.widget.dialog.PositionDialog 9 | import org.koin.compose.viewmodel.koinViewModel 10 | import org.koin.core.parameter.parametersOf 11 | 12 | @Composable 13 | fun DialogPage( 14 | installer: InstallerRepo, viewModel: DialogViewModel = koinViewModel() { 15 | parametersOf(installer) 16 | } 17 | ) { 18 | LaunchedEffect(installer.id) { 19 | viewModel.dispatch(DialogViewAction.CollectRepo(installer)) 20 | } 21 | val params = dialogGenerateParams(installer, viewModel) 22 | 23 | PositionDialog( 24 | onDismissRequest = { 25 | viewModel.dispatch(DialogViewAction.Background) 26 | }, 27 | modifier = Modifier.animateContentSize(), 28 | centerIcon = dialogInnerWidget(installer, params.icon), 29 | centerTitle = dialogInnerWidget(installer, params.title), 30 | centerSubtitle = dialogInnerWidget(installer, params.subtitle), 31 | centerText = dialogInnerWidget(installer, params.text), 32 | centerContent = dialogInnerWidget(installer, params.content), 33 | centerButton = dialogInnerWidget(installer, params.buttons) 34 | ) 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/DialogParams.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog 2 | 3 | private val emptyInnerParams = DialogInnerParams("empty") 4 | 5 | data class DialogParams( 6 | val icon: DialogInnerParams = emptyInnerParams, 7 | val title: DialogInnerParams = emptyInnerParams, 8 | val subtitle: DialogInnerParams = emptyInnerParams, 9 | val text: DialogInnerParams = emptyInnerParams, 10 | val content: DialogInnerParams = emptyInnerParams, 11 | val buttons: DialogInnerParams = emptyInnerParams 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/DialogParamsType.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog 2 | 3 | sealed class DialogParamsType(val id: String) { 4 | object IconWorking : DialogParamsType("icon_working") 5 | object IconPausing : DialogParamsType("icon_pausing") 6 | 7 | object ButtonsCancel : DialogParamsType("buttons_cancel") 8 | 9 | object InstallerReady : DialogParamsType("installer_ready") 10 | object InstallerResolving : DialogParamsType("installer_resolving") 11 | object InstallerResolveFailed : DialogParamsType("installer_resolve_failed") 12 | object InstallerAnalysing : DialogParamsType("installer_analysing") 13 | object InstallerAnalyseFailed : DialogParamsType("installer_analyse_failed") 14 | object InstallChoice : DialogParamsType("installer_choice") 15 | object InstallerPrepare : DialogParamsType("installer_prepare") 16 | object InstallerPrepareEmpty : DialogParamsType("installer_prepare_empty") 17 | object InstallerPrepareTooMany : DialogParamsType("installer_prepare_too_many") 18 | object InstallerInfo : 19 | DialogParamsType("installer_info") 20 | 21 | object InstallerPrepareInstall : 22 | DialogParamsType("installer_prepare") 23 | 24 | object InstallerInstalling : DialogParamsType("installer_installing") 25 | 26 | object InstallerInstallSuccess : DialogParamsType("install_success") 27 | object InstallerInstallFailed : DialogParamsType("install_failed") 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/DialogViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog 2 | 3 | import com.rosan.installer.data.installer.repo.InstallerRepo 4 | 5 | sealed class DialogViewAction { 6 | data class CollectRepo(val repo: InstallerRepo) : DialogViewAction() 7 | object Close : DialogViewAction() 8 | object Analyse : DialogViewAction() 9 | object InstallChoice : DialogViewAction() 10 | object InstallPrepare : DialogViewAction() 11 | object Install : DialogViewAction() 12 | object Background : DialogViewAction() 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/DialogViewState.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog 2 | 3 | sealed class DialogViewState { 4 | object Ready : DialogViewState() 5 | object Resolving : DialogViewState() 6 | object ResolveFailed : DialogViewState() 7 | object Analysing : DialogViewState() 8 | object AnalyseFailed : DialogViewState() 9 | object InstallChoice : DialogViewState() 10 | object InstallPrepare : DialogViewState() 11 | object Installing : DialogViewState() 12 | object InstallFailed : DialogViewState() 13 | object InstallSuccess : DialogViewState() 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/inner/AnalyseFailedDialog.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog.inner 2 | 3 | import androidx.compose.material3.Text 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.res.stringResource 6 | import com.rosan.installer.R 7 | import com.rosan.installer.data.installer.repo.InstallerRepo 8 | import com.rosan.installer.ui.page.installer.dialog.* 9 | 10 | @Composable 11 | fun analyseFailedDialog( 12 | installer: InstallerRepo, viewModel: DialogViewModel 13 | ): DialogParams { 14 | return DialogParams(icon = DialogInnerParams( 15 | DialogParamsType.IconPausing.id, pausingIcon 16 | ), title = DialogInnerParams( 17 | DialogParamsType.InstallerAnalyseFailed.id 18 | ) { 19 | Text(stringResource(R.string.installer_analyse_failed)) 20 | }, text = DialogInnerParams( 21 | DialogParamsType.InstallerAnalyseFailed.id, errorText(installer, viewModel) 22 | ), buttons = DialogButtons( 23 | DialogParamsType.ButtonsCancel.id 24 | ) { 25 | listOf(DialogButton(stringResource(R.string.cancel)) { 26 | viewModel.dispatch(DialogViewAction.Close) 27 | }) 28 | }) 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/inner/AnalysingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog.inner 2 | 3 | import androidx.compose.material3.Text 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.res.stringResource 6 | import com.rosan.installer.R 7 | import com.rosan.installer.data.installer.repo.InstallerRepo 8 | import com.rosan.installer.ui.page.installer.dialog.* 9 | 10 | @Composable 11 | fun analysingDialog( 12 | installer: InstallerRepo, viewModel: DialogViewModel 13 | ): DialogParams { 14 | return DialogParams(icon = DialogInnerParams( 15 | DialogParamsType.IconWorking.id, workingIcon 16 | ), title = DialogInnerParams( 17 | DialogParamsType.InstallerAnalysing.id, 18 | ) { 19 | Text(stringResource(R.string.installer_analysing)) 20 | }, buttons = DialogButtons( 21 | DialogParamsType.ButtonsCancel.id 22 | ) { 23 | listOf(DialogButton(stringResource(R.string.cancel)) { 24 | viewModel.dispatch(DialogViewAction.Close) 25 | }) 26 | }) 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/inner/DialogButton.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog.inner 2 | 3 | data class DialogButton( 4 | val text: String, 5 | val weight: Float = 1f, 6 | val onClick: () -> Unit, 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/inner/DialogButtons.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog.inner 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.ButtonDefaults 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TextButton 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.unit.dp 18 | import com.rosan.installer.ui.page.installer.dialog.DialogInnerParams 19 | 20 | @Composable 21 | fun DialogButtons( 22 | id: String, content: (@Composable () -> List) 23 | ) = DialogInnerParams(id) { 24 | val buttons = content.invoke() 25 | Column( 26 | modifier = Modifier.clip(RoundedCornerShape(12.dp)), 27 | verticalArrangement = Arrangement.spacedBy(4.dp) 28 | ) { 29 | val single = if (buttons.size > 2) buttons.size % 2 else buttons.size 30 | for (i in 0 until single) { 31 | InnerButton(buttons[i]) 32 | } 33 | for (i in single until buttons.size step 2) { 34 | Box { 35 | Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { 36 | buttons[i].let { 37 | InnerButton( 38 | it, Modifier.weight(it.weight) 39 | ) 40 | } 41 | buttons[i + 1].let { 42 | InnerButton( 43 | it, Modifier.weight(it.weight) 44 | ) 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | 53 | @Composable 54 | private fun InnerButton( 55 | button: DialogButton, modifier: Modifier = Modifier 56 | ) { 57 | TextButton( 58 | button.onClick, 59 | modifier = modifier.fillMaxWidth(), 60 | shape = RoundedCornerShape(4.dp), 61 | colors = ButtonDefaults.buttonColors( 62 | containerColor = MaterialTheme.colorScheme.primaryContainer, 63 | contentColor = MaterialTheme.colorScheme.onPrimaryContainer 64 | ), 65 | contentPadding = PaddingValues(16.dp) 66 | ) { 67 | Text(button.text) 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/inner/InstallFailedDialog.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog.inner 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.provider.Settings 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.collectAsState 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.res.stringResource 11 | import com.rosan.installer.R 12 | import com.rosan.installer.data.installer.repo.InstallerRepo 13 | import com.rosan.installer.ui.page.installer.dialog.* 14 | 15 | // Assume errorText is accessible 16 | 17 | @Composable 18 | fun installFailedDialog( // 小写开头 19 | installer: InstallerRepo, viewModel: DialogViewModel 20 | ): DialogParams { 21 | val context = LocalContext.current 22 | 23 | val preInstallAppInfo by viewModel.preInstallAppInfo.collectAsState() 24 | val currentPackageName by viewModel.currentPackageName.collectAsState() 25 | val packageName = currentPackageName ?: installer.entities.filter { it.selected }.map { it.app } 26 | .firstOrNull()?.packageName ?: "" 27 | 28 | // Call InstallInfoDialog for base structure 29 | val baseParams = InstallInfoDialog( 30 | installer = installer, 31 | viewModel = viewModel, 32 | preInstallAppInfo = preInstallAppInfo, 33 | onTitleExtraClick = { 34 | if (packageName.isNotEmpty()) { 35 | context.startActivity( 36 | Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 37 | .setData(Uri.fromParts("package", packageName, null)) 38 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 39 | ) 40 | } 41 | } 42 | ) 43 | 44 | // Override text and buttons 45 | return baseParams.copy( 46 | text = DialogInnerParams( 47 | DialogParamsType.InstallerInstallFailed.id, 48 | errorText(installer, viewModel) // Assume errorText is accessible 49 | ), 50 | buttons = DialogButtons( 51 | DialogParamsType.InstallerInstallFailed.id 52 | ) { 53 | listOf( 54 | DialogButton(stringResource(R.string.previous)) { 55 | viewModel.dispatch(DialogViewAction.InstallPrepare) 56 | }, 57 | DialogButton(stringResource(R.string.cancel)) { 58 | viewModel.dispatch(DialogViewAction.Close) 59 | } 60 | ) 61 | } 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/inner/InstallingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog.inner 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.material3.LinearProgressIndicator 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.collectAsState 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.stringResource 10 | import com.rosan.installer.R 11 | import com.rosan.installer.data.installer.repo.InstallerRepo 12 | import com.rosan.installer.ui.page.installer.dialog.* 13 | 14 | @Composable 15 | fun installingDialog( // 小写开头 16 | installer: InstallerRepo, viewModel: DialogViewModel 17 | ): DialogParams { 18 | val preInstallAppInfo by viewModel.preInstallAppInfo.collectAsState() 19 | 20 | // Call InstallInfoDialog for base structure (icon, title, subtitle with new version) 21 | val baseParams = InstallInfoDialog( 22 | installer = installer, 23 | viewModel = viewModel, 24 | preInstallAppInfo = preInstallAppInfo, 25 | onTitleExtraClick = {} 26 | ) 27 | 28 | // Override text and buttons 29 | return baseParams.copy( 30 | text = DialogInnerParams( 31 | DialogParamsType.InstallerInstalling.id 32 | ) { 33 | LinearProgressIndicator(Modifier.fillMaxWidth()) 34 | }, 35 | buttons = DialogButtons(DialogParamsType.ButtonsCancel.id) { 36 | listOf( 37 | DialogButton(stringResource(R.string.cancel)) { 38 | viewModel.dispatch(DialogViewAction.Close) 39 | } 40 | ) 41 | } 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/inner/ReadyDialog.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog.inner 2 | 3 | import androidx.compose.material3.Text 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.res.stringResource 6 | import com.rosan.installer.R 7 | import com.rosan.installer.data.installer.repo.InstallerRepo 8 | import com.rosan.installer.ui.page.installer.dialog.* 9 | 10 | @Composable 11 | fun readyDialog( 12 | installer: InstallerRepo, viewModel: DialogViewModel 13 | ): DialogParams { 14 | return DialogParams(icon = DialogInnerParams( 15 | DialogParamsType.IconWorking.id, workingIcon 16 | ), title = DialogInnerParams( 17 | DialogParamsType.InstallerReady.id, 18 | ) { 19 | Text(stringResource(R.string.installer_ready)) 20 | }, buttons = DialogButtons( 21 | DialogParamsType.ButtonsCancel.id 22 | ) { 23 | listOf(DialogButton(stringResource(R.string.cancel)) { 24 | viewModel.dispatch(DialogViewAction.Close) 25 | }) 26 | }) 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/inner/ResolveFailedDialog.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog.inner 2 | 3 | import androidx.compose.material3.Text 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.res.stringResource 6 | import com.rosan.installer.R 7 | import com.rosan.installer.data.installer.repo.InstallerRepo 8 | import com.rosan.installer.ui.page.installer.dialog.* 9 | 10 | @Composable 11 | fun resolveFailedDialog( 12 | installer: InstallerRepo, viewModel: DialogViewModel 13 | ): DialogParams { 14 | return DialogParams(icon = DialogInnerParams( 15 | DialogParamsType.IconPausing.id, pausingIcon 16 | ), title = DialogInnerParams( 17 | DialogParamsType.InstallerResolveFailed.id 18 | ) { 19 | Text(stringResource(R.string.installer_resolve_failed)) 20 | }, text = DialogInnerParams( 21 | DialogParamsType.InstallerResolveFailed.id, errorText(installer, viewModel) 22 | ), buttons = DialogButtons( 23 | DialogParamsType.ButtonsCancel.id 24 | ) { 25 | listOf(DialogButton(stringResource(R.string.cancel)) { 26 | viewModel.dispatch(DialogViewAction.Close) 27 | }) 28 | }) 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/inner/ResolvingDialog.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog.inner 2 | 3 | import androidx.compose.material3.Text 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.res.stringResource 6 | import com.rosan.installer.R 7 | import com.rosan.installer.data.installer.repo.InstallerRepo 8 | import com.rosan.installer.ui.page.installer.dialog.* 9 | 10 | @Composable 11 | fun resolvingDialog( 12 | installer: InstallerRepo, viewModel: DialogViewModel 13 | ): DialogParams { 14 | return DialogParams(icon = DialogInnerParams( 15 | DialogParamsType.IconWorking.id, workingIcon 16 | ), title = DialogInnerParams( 17 | DialogParamsType.InstallerResolving.id, 18 | ) { 19 | Text(stringResource(R.string.installer_resolving)) 20 | }, buttons = DialogButtons( 21 | DialogParamsType.ButtonsCancel.id 22 | ) { 23 | listOf(DialogButton(stringResource(R.string.cancel)) { 24 | viewModel.dispatch(DialogViewAction.Close) 25 | }) 26 | }) 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/installer/dialog/inner/common.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.installer.dialog.inner 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.lazy.LazyColumn 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.foundation.text.BasicTextField 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.twotone.HourglassDisabled 12 | import androidx.compose.material.icons.twotone.HourglassEmpty 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.LocalContentColor 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.CompositionLocalProvider 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.draw.clip 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.unit.dp 23 | import com.rosan.installer.data.installer.repo.InstallerRepo 24 | import com.rosan.installer.ui.page.installer.dialog.DialogViewModel 25 | import com.rosan.installer.util.help 26 | 27 | val pausingIcon: @Composable () -> Unit = { 28 | Icon( 29 | imageVector = Icons.TwoTone.HourglassDisabled, contentDescription = null 30 | ) 31 | } 32 | 33 | val workingIcon: @Composable () -> Unit = { 34 | Icon( 35 | imageVector = Icons.TwoTone.HourglassEmpty, contentDescription = null 36 | ) 37 | } 38 | 39 | val errorText: ((installer: InstallerRepo, viewModel: DialogViewModel) -> (@Composable () -> Unit)) = 40 | { installer, viewModel -> 41 | { 42 | CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onErrorContainer) { 43 | LazyColumn( 44 | modifier = Modifier 45 | .clip(RoundedCornerShape(12.dp)) 46 | .background(MaterialTheme.colorScheme.errorContainer) 47 | .fillMaxWidth() 48 | .padding(12.dp), 49 | verticalArrangement = Arrangement.spacedBy(8.dp) 50 | ) { 51 | item { 52 | Text(installer.error.help(), fontWeight = FontWeight.Bold) 53 | } 54 | item { 55 | BasicTextField( 56 | value = installer.error.stackTraceToString().trim(), 57 | onValueChange = {}, 58 | readOnly = true 59 | ) 60 | } 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/SettingsPage.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings 2 | 3 | import androidx.compose.animation.AnimatedContentTransitionScope 4 | import androidx.compose.runtime.Composable 5 | import androidx.navigation.NavType 6 | import androidx.navigation.compose.NavHost 7 | import androidx.navigation.compose.composable 8 | import androidx.navigation.compose.rememberNavController 9 | import androidx.navigation.navArgument 10 | import com.rosan.installer.ui.page.settings.config.apply.ApplyPage 11 | import com.rosan.installer.ui.page.settings.config.edit.EditPage 12 | import com.rosan.installer.ui.page.settings.main.MainPage 13 | 14 | @Composable 15 | fun SettingsPage() { 16 | val navController = rememberNavController() 17 | 18 | NavHost( 19 | navController = navController, 20 | startDestination = SettingsScreen.Main.route, 21 | ) { 22 | composable( 23 | route = SettingsScreen.Main.route, 24 | enterTransition = { 25 | null 26 | }, 27 | exitTransition = { 28 | null 29 | }, 30 | popEnterTransition = { 31 | null 32 | }, 33 | popExitTransition = { 34 | null 35 | } 36 | ) { 37 | MainPage(navController = navController) 38 | } 39 | composable( 40 | route = SettingsScreen.EditConfig.route, 41 | arguments = listOf( 42 | navArgument("id") { 43 | type = NavType.LongType 44 | } 45 | ), 46 | enterTransition = { 47 | slideIntoContainer( 48 | AnimatedContentTransitionScope.SlideDirection.Up, 49 | ) 50 | }, 51 | exitTransition = { 52 | null 53 | }, 54 | popEnterTransition = { 55 | null 56 | }, 57 | popExitTransition = { 58 | slideOutOfContainer( 59 | AnimatedContentTransitionScope.SlideDirection.Down, 60 | ) 61 | } 62 | ) { 63 | val id = it.arguments?.getLong("id") 64 | EditPage( 65 | navController = navController, 66 | id = if (id != -1L) id 67 | else null 68 | ) 69 | } 70 | 71 | composable( 72 | route = SettingsScreen.ApplyConfig.route, 73 | arguments = listOf( 74 | navArgument("id") { 75 | type = NavType.LongType 76 | } 77 | ), 78 | enterTransition = { null }, 79 | exitTransition = { null }, 80 | popEnterTransition = { null }, 81 | popExitTransition = { null } 82 | ) { 83 | val id = it.arguments?.getLong("id")!! 84 | ApplyPage( 85 | navController = navController, 86 | id = id 87 | ) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/SettingsScreen.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings 2 | 3 | sealed class SettingsScreen(val route: String) { 4 | data object Main : SettingsScreen("main") 5 | data object EditConfig : SettingsScreen("config/edit?id={id}") 6 | data object ApplyConfig : SettingsScreen("config/apply?id={id}") 7 | 8 | sealed class Builder(val route: String) { 9 | data object Main : SettingsScreen("main") 10 | class EditConfig(id: Long? = null) : SettingsScreen( 11 | "config/edit?id=${id ?: -1}" 12 | ) 13 | 14 | class ApplyConfig(id: Long) : SettingsScreen( 15 | "config/apply?id=$id" 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/config/all/AllViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.config.all 2 | 3 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 4 | import com.rosan.installer.data.settings.util.ConfigOrder 5 | 6 | sealed class AllViewAction { 7 | object Init : AllViewAction() 8 | object LoadData : AllViewAction() 9 | data class ChangeDataConfigOrder(val configOrder: ConfigOrder) : AllViewAction() 10 | data class DeleteDataConfig(val configEntity: ConfigEntity) : AllViewAction() 11 | data class RestoreDataConfig(val configEntity: ConfigEntity) : AllViewAction() 12 | data class EditDataConfig(val configEntity: ConfigEntity) : AllViewAction() 13 | data class ApplyConfig(val configEntity: ConfigEntity) : AllViewAction() 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/config/all/AllViewEvent.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.config.all 2 | 3 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 4 | 5 | sealed class AllViewEvent { 6 | data class DeletedConfig(val configEntity: ConfigEntity) : AllViewEvent() 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/config/all/AllViewState.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.config.all 2 | 3 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 4 | import com.rosan.installer.data.settings.util.ConfigOrder 5 | import com.rosan.installer.data.settings.util.OrderType 6 | 7 | data class AllViewState( 8 | val data: Data = Data() 9 | ) { 10 | data class Data( 11 | val configs: List = emptyList(), 12 | val configOrder: ConfigOrder = ConfigOrder.Id(OrderType.Ascending), 13 | val progress: Progress = Progress.Loading, 14 | ) { 15 | sealed class Progress { 16 | object Loading : Progress() 17 | object Loaded : Progress() 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/config/apply/ApplyViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.config.apply 2 | 3 | sealed class ApplyViewAction { 4 | object Init : ApplyViewAction() 5 | object LoadApps : ApplyViewAction() 6 | object LoadAppEntities : ApplyViewAction() 7 | data class ApplyPackageName( 8 | val packageName: String?, 9 | val applied: Boolean 10 | ) : ApplyViewAction() 11 | 12 | data class Order(val type: ApplyViewState.OrderType) : ApplyViewAction() 13 | data class OrderInReverse(val enabled: Boolean) : ApplyViewAction() 14 | data class SelectedFirst(val enabled: Boolean) : ApplyViewAction() 15 | data class ShowSystemApp(val enabled: Boolean) : ApplyViewAction() 16 | data class ShowPackageName(val enabled: Boolean) : ApplyViewAction() 17 | 18 | data class Search(val text: String) : ApplyViewAction() 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/config/apply/ApplyViewApp.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.config.apply 2 | 3 | import android.graphics.drawable.Drawable 4 | import androidx.compose.runtime.MutableState 5 | 6 | data class ApplyViewApp( 7 | val packageName: String, 8 | val versionName: String?, 9 | val versionCode: Long, 10 | val firstInstallTime: Long, 11 | val lastUpdateTime: Long, 12 | val isSystemApp: Boolean, 13 | val label: String? 14 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/config/apply/ApplyViewState.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.config.apply 2 | 3 | import com.rosan.installer.data.settings.model.room.entity.AppEntity 4 | import com.rosan.installer.ui.common.ViewContent 5 | 6 | data class ApplyViewState( 7 | val apps: ViewContent> = ViewContent( 8 | data = emptyList(), progress = ViewContent.Progress.Loading 9 | ), 10 | val appEntities: ViewContent> = ViewContent( 11 | data = emptyList(), progress = ViewContent.Progress.Loading 12 | ), 13 | val orderType: OrderType = OrderType.Label, 14 | val orderInReverse: Boolean = false, 15 | val selectedFirst: Boolean = true, 16 | val showSystemApp: Boolean = false, 17 | val showPackageName: Boolean = true, 18 | val search: String = "" 19 | ) { 20 | enum class OrderType { 21 | Label, PackageName, FirstInstallTime 22 | } 23 | 24 | private fun contains(v1: String, v2: String): Boolean = v1.lowercase().contains(v2.lowercase()) 25 | 26 | val checkedApps: List = apps.data.run { 27 | if (search.isEmpty()) this 28 | else filter { contains(it.packageName, search) || contains(it.label ?: "", search) } 29 | }.run { 30 | if (showSystemApp) this 31 | else filter { !it.isSystemApp } 32 | }.run { 33 | data class OrderData( 34 | val type: OrderType, 35 | val comparator: (ApplyViewApp) -> Comparable<*>? 36 | ) 37 | 38 | val selectors = listOf( 39 | OrderData(OrderType.Label) { it.label }, 40 | OrderData(OrderType.PackageName) { it.packageName }, 41 | OrderData(OrderType.FirstInstallTime) { it.firstInstallTime } 42 | ).sortedBy { 43 | // selected order type first 44 | if (it.type == orderType) Int.MIN_VALUE 45 | else it.type.ordinal 46 | }.map { 47 | it.comparator 48 | }.run { 49 | // selected app first 50 | if (!selectedFirst) this 51 | else listOf<(ApplyViewApp) -> Comparable<*>?> { app -> 52 | appEntities.data.find { it.packageName == app.packageName } == null 53 | } + this 54 | }.toTypedArray() 55 | if (!orderInReverse) sortedWith { a, b -> compareValuesBy(a, b, selectors = selectors) } 56 | else sortedWith { a, b -> compareValuesBy(b, a, selectors = selectors) } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/config/edit/EditViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.config.edit 2 | 3 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 4 | 5 | sealed class EditViewAction { 6 | object Init : EditViewAction() 7 | data class ChangeDataName(val name: String) : EditViewAction() 8 | data class ChangeDataDescription(val description: String) : EditViewAction() 9 | data class ChangeDataAuthorizer(val authorizer: ConfigEntity.Authorizer) : EditViewAction() 10 | data class ChangeDataCustomizeAuthorizer(val customizeAuthorizer: String) : EditViewAction() 11 | data class ChangeDataInstallMode(val installMode: ConfigEntity.InstallMode) : EditViewAction() 12 | data class ChangeDataDeclareInstaller(val declareInstaller: Boolean) : EditViewAction() 13 | data class ChangeDataInstaller(val installer: String) : EditViewAction() 14 | data class ChangeDataForAllUser(val forAllUser: Boolean) : EditViewAction() 15 | data class ChangeDataAllowTestOnly(val allowTestOnly: Boolean) : EditViewAction() 16 | data class ChangeDataAllowDowngrade(val allowDowngrade: Boolean) : EditViewAction() 17 | data class ChangeDataAutoDelete(val autoDelete: Boolean) : EditViewAction() 18 | data class ChangeDisplaySdk(val displaySdk: Boolean) : EditViewAction() 19 | object LoadData : EditViewAction() 20 | object SaveData : EditViewAction() 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/config/edit/EditViewEvent.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.config.edit 2 | 3 | sealed class EditViewEvent { 4 | data class SnackBar(val message: String) : EditViewEvent() 5 | object Saved : EditViewEvent() 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/config/edit/EditViewState.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.config.edit 2 | 3 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 4 | 5 | data class EditViewState( 6 | val data: Data = Data.build(ConfigEntity.default) 7 | ) { 8 | data class Data( 9 | val name: String, 10 | val description: String, 11 | val authorizer: ConfigEntity.Authorizer, 12 | val customizeAuthorizer: String, 13 | val installMode: ConfigEntity.InstallMode, 14 | val declareInstaller: Boolean, 15 | val installer: String, 16 | val forAllUser: Boolean, 17 | val allowTestOnly: Boolean, 18 | val allowDowngrade: Boolean, 19 | val autoDelete: Boolean, 20 | val displaySdk: Boolean, 21 | ) { 22 | val errorName = name.isEmpty()// || name == "Default" 23 | 24 | val authorizerCustomize = authorizer == ConfigEntity.Authorizer.Customize 25 | 26 | val errorCustomizeAuthorizer = authorizerCustomize && customizeAuthorizer.isEmpty() 27 | 28 | val errorInstaller = declareInstaller && installer.isEmpty() 29 | 30 | fun toConfigEntity(): ConfigEntity = ConfigEntity( 31 | name = this.name, 32 | description = this.description, 33 | authorizer = this.authorizer, 34 | customizeAuthorizer = if (this.authorizerCustomize) 35 | this.customizeAuthorizer 36 | else 37 | "", 38 | installMode = this.installMode, 39 | installer = if (this.declareInstaller) 40 | this.installer 41 | else 42 | null, 43 | forAllUser = this.forAllUser, 44 | allowTestOnly = this.allowTestOnly, 45 | allowDowngrade = this.allowDowngrade, 46 | autoDelete = this.autoDelete, 47 | displaySdk = this.displaySdk 48 | ) 49 | 50 | companion object { 51 | fun build(config: ConfigEntity): Data = Data( 52 | name = config.name, 53 | description = config.description, 54 | authorizer = config.authorizer, 55 | customizeAuthorizer = config.customizeAuthorizer, 56 | installMode = config.installMode, 57 | declareInstaller = config.installer != null, 58 | installer = config.installer ?: "", 59 | forAllUser = config.forAllUser, 60 | allowTestOnly = config.allowTestOnly, 61 | allowDowngrade = config.allowDowngrade, 62 | autoDelete = config.autoDelete, 63 | displaySdk = config.displaySdk 64 | ) 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/home/HomeCardItem.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.home 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | 5 | data class HomeCardItem( 6 | val icon: ImageVector? = null, 7 | val label: String, 8 | val content: String? = null, 9 | val onClick: (() -> Unit)? = null 10 | ) { 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/home/HomeState.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.home 2 | 3 | import android.os.Build 4 | import androidx.compose.ui.text.intl.Locale 5 | import androidx.compose.ui.text.toUpperCase 6 | import com.rosan.installer.build.RsConfig 7 | import com.rosan.installer.build.Level 8 | 9 | /** 10 | * just constant data, so just use it without ViewModel. 11 | * */ 12 | data class HomeState( 13 | val level: Level = RsConfig.LEVEL, 14 | val versionInfo: String = "${RsConfig.versionName} (${RsConfig.versionCode})", 15 | val systemVersion: String = if (Build.VERSION.PREVIEW_SDK_INT != 0) 16 | String.format("%1\$s Preview (API %2\$s)", Build.VERSION.CODENAME, Build.VERSION.SDK_INT) 17 | else 18 | String.format("%1\$s (API %2\$s)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT), 19 | val deviceName: String = run { 20 | var manufacturer = Build.MANUFACTURER.toUpperCase(Locale.current) 21 | val brand = Build.BRAND.toUpperCase(Locale.current) 22 | if (brand != manufacturer) { 23 | manufacturer += " $brand" 24 | } 25 | manufacturer += " ${Build.MODEL}" 26 | manufacturer 27 | }, 28 | val systemStruct: String = kotlin.run { 29 | var struct = System.getProperty("os.arch") ?: "unknown" 30 | val abis = Build.SUPPORTED_ABIS 31 | struct += if (abis.isEmpty()) { 32 | " (Not supported Native ABI)" 33 | } else { 34 | " (${abis.joinToString(", ")})" 35 | } 36 | struct 37 | } 38 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/main/NavigationData.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.main 2 | 3 | import androidx.compose.foundation.layout.WindowInsets 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | 7 | data class NavigationData( 8 | val icon: ImageVector, 9 | val label: String, 10 | val content: @Composable (windowInsets: WindowInsets) -> Unit 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/preferred/PreferredViewAction.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.preferred 2 | 3 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 4 | 5 | sealed class PreferredViewAction { 6 | object Init : PreferredViewAction() 7 | data class ChangeGlobalAuthorizer(val authorizer: ConfigEntity.Authorizer) : 8 | PreferredViewAction() 9 | 10 | data class ChangeGlobalCustomizeAuthorizer(val customizeAuthorizer: String) : 11 | PreferredViewAction() 12 | 13 | data class ChangeGlobalInstallMode(val installMode: ConfigEntity.InstallMode) : 14 | PreferredViewAction() 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/page/settings/preferred/PreferredViewState.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.page.settings.preferred 2 | 3 | import com.rosan.installer.data.settings.model.room.entity.ConfigEntity 4 | 5 | data class PreferredViewState( 6 | val authorizer: ConfigEntity.Authorizer = ConfigEntity.Authorizer.Shizuku, 7 | val customizeAuthorizer: String = "", 8 | val installMode: ConfigEntity.InstallMode = ConfigEntity.InstallMode.Dialog 9 | ) { 10 | val authorizerCustomize = authorizer == ConfigEntity.Authorizer.Customize 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/theme/InstallerTheme.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.theme 2 | 3 | import android.os.Build 4 | import androidx.activity.ComponentActivity 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.platform.LocalView 15 | import androidx.compose.ui.res.colorResource 16 | import androidx.core.view.WindowCompat 17 | import com.rosan.installer.R 18 | 19 | @Composable 20 | fun InstallerTheme( 21 | darkTheme: Boolean = isSystemInDarkTheme(), 22 | // 添加动态颜色支持:仅对 Android 12 (API 31) 及以上版本有效 23 | dynamicColor: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S, 24 | content: @Composable () -> Unit 25 | ) { 26 | val colorScheme = when { 27 | // 添加版本检查 28 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 29 | val context = LocalContext.current 30 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 31 | } 32 | 33 | darkTheme -> darkColorScheme( 34 | primary = colorResource(R.color.dark_primary), 35 | primaryContainer = colorResource(R.color.dark_primary_container), 36 | secondary = colorResource(R.color.dark_secondary), 37 | tertiary = colorResource(R.color.dark_tertiary), 38 | error = colorResource(R.color.dark_error) 39 | ) 40 | 41 | else -> lightColorScheme( 42 | primary = colorResource(R.color.light_primary), 43 | primaryContainer = colorResource(R.color.light_primary_container), 44 | secondary = colorResource(R.color.light_secondary), 45 | tertiary = colorResource(R.color.light_tertiary) 46 | ) 47 | } 48 | 49 | // New Status Bar Color Logic 50 | val view = LocalView.current 51 | if (!view.isInEditMode) { 52 | SideEffect { 53 | val window = (view.context as ComponentActivity).window 54 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme 55 | } 56 | } 57 | 58 | MaterialTheme( 59 | colorScheme = colorScheme, 60 | typography = Typography, 61 | content = content 62 | ) 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/theme/WindowInsets.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.theme 2 | 3 | import androidx.compose.foundation.layout.WindowInsets 4 | import androidx.compose.foundation.layout.WindowInsetsSides 5 | import androidx.compose.foundation.layout.exclude 6 | import androidx.compose.foundation.layout.only 7 | 8 | val WindowInsets.Companion.none: WindowInsets 9 | get() = WindowInsets(0) 10 | 11 | fun WindowInsets.exclude(sides: WindowInsetsSides): WindowInsets = 12 | this.exclude(this.only(sides)) -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/widget/setting/BaseWidget.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.widget.setting 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.material.icons.materialIcon 7 | import androidx.compose.material.icons.materialPath 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.vector.ImageVector 16 | import androidx.compose.ui.unit.dp 17 | 18 | @Composable 19 | fun BaseWidget( 20 | icon: ImageVector? = null, 21 | title: String, 22 | description: String? = null, 23 | enabled: Boolean = true, 24 | onClick: () -> Unit = {}, 25 | foreContent: @Composable BoxScope.() -> Unit = {}, 26 | content: @Composable BoxScope.() -> Unit, 27 | ) { 28 | Row( 29 | modifier = Modifier 30 | .fillMaxWidth() 31 | .clickable( 32 | enabled = enabled, 33 | onClick = onClick 34 | ) 35 | .padding(16.dp), 36 | horizontalArrangement = Arrangement.spacedBy(16.dp), 37 | ) { 38 | Icon( 39 | modifier = Modifier 40 | .size(24.dp) 41 | .align(Alignment.CenterVertically), 42 | imageVector = icon ?: materialIcon("") { materialPath {} }, 43 | contentDescription = null, 44 | ) 45 | Box( 46 | modifier = Modifier 47 | .weight(1f) 48 | .fillMaxHeight() 49 | .align(Alignment.CenterVertically) 50 | ) { 51 | Column { 52 | Text( 53 | text = title, 54 | color = MaterialTheme.colorScheme.onSurface, 55 | style = MaterialTheme.typography.titleMedium 56 | ) 57 | description?.let { 58 | Text( 59 | text = it, 60 | color = MaterialTheme.colorScheme.onSurface, 61 | style = MaterialTheme.typography.bodyMedium 62 | ) 63 | } 64 | } 65 | foreContent() 66 | } 67 | Box( 68 | modifier = Modifier 69 | .align(Alignment.CenterVertically) 70 | ) { 71 | content() 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/widget/setting/LabelWidget.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.widget.setting 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun LabelWidget(label: String) { 14 | Box( 15 | modifier = Modifier 16 | .fillMaxWidth() 17 | .padding(vertical = 8.dp, horizontal = 56.dp) 18 | // .padding(vertical = 8.dp, horizontal = 16.dp) 19 | .padding(top = 8.dp) 20 | ) { 21 | Text( 22 | modifier = Modifier, 23 | text = label, 24 | style = MaterialTheme.typography.titleSmall, 25 | color = MaterialTheme.colorScheme.primary, 26 | ) 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/widget/setting/SwitchWidget.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.widget.setting 2 | 3 | import androidx.compose.material3.Switch 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | 7 | @Composable 8 | fun SwitchWidget( 9 | icon: ImageVector? = null, 10 | title: String, 11 | description: String? = null, 12 | enabled: Boolean = true, 13 | checked: Boolean, 14 | onCheckedChange: (Boolean) -> Unit 15 | ) { 16 | BaseWidget( 17 | icon = icon, 18 | title = title, 19 | description = description, 20 | enabled = enabled, 21 | onClick = { 22 | onCheckedChange(!checked) 23 | } 24 | ) { 25 | Switch( 26 | enabled = enabled, 27 | checked = checked, 28 | onCheckedChange = { onCheckedChange(!checked) } 29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/ui/widget/toggle/Toggle.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.ui.widget.toggle 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.ColumnScope 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.selection.selectable 10 | import androidx.compose.material.ripple.rememberRipple 11 | import androidx.compose.material3.LocalContentColor 12 | import androidx.compose.material3.LocalTextStyle 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.ripple 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.CompositionLocalProvider 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.unit.dp 23 | 24 | @Composable 25 | fun Toggle( 26 | selected: Boolean, 27 | onSelected: () -> Unit, 28 | modifier: Modifier = Modifier, 29 | enabled: Boolean = true, 30 | selectedContentColor: Color = MaterialTheme.colorScheme.onPrimary, 31 | unselectedContentColor: Color = LocalContentColor.current, 32 | content: @Composable ColumnScope.() -> Unit 33 | ) { 34 | @Suppress("AnimateAsStateLabel") val contentColor by animateColorAsState( 35 | if (selected) selectedContentColor 36 | else unselectedContentColor 37 | ) 38 | CompositionLocalProvider( 39 | LocalContentColor provides contentColor, 40 | LocalTextStyle provides MaterialTheme.typography.labelLarge 41 | ) { 42 | Column( 43 | modifier = modifier 44 | .selectable( 45 | selected = selected, 46 | onClick = onSelected, 47 | enabled = enabled, 48 | interactionSource = remember { 49 | MutableInteractionSource() 50 | }, 51 | indication = ripple(color = contentColor) 52 | ) 53 | .padding(vertical = 8.dp, horizontal = 24.dp), 54 | horizontalAlignment = Alignment.CenterHorizontally, 55 | verticalArrangement = Arrangement.Center, 56 | content = content 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/rosan/installer/util/ContextUtil.kt: -------------------------------------------------------------------------------- 1 | package com.rosan.installer.util 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.widget.Toast 7 | import androidx.annotation.StringRes 8 | 9 | fun Context.openUrlInBrowser(url: String) { 10 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) 11 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 12 | startActivity(intent) 13 | } 14 | 15 | fun Context.toast(text: CharSequence, duration: Int = Toast.LENGTH_SHORT) { 16 | Toast.makeText(this, text, duration).show() 17 | } 18 | 19 | fun Context.toast(@StringRes resId: Int, duration: Int = Toast.LENGTH_SHORT) { 20 | Toast.makeText(this, resId, duration).show() 21 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/round_hourglass_disabled_black_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-hdpi/round_hourglass_disabled_black_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/round_hourglass_disabled_black_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-hdpi/round_hourglass_disabled_black_20.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/round_hourglass_disabled_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-hdpi/round_hourglass_disabled_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/round_hourglass_disabled_black_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-hdpi/round_hourglass_disabled_black_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/round_hourglass_disabled_black_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-hdpi/round_hourglass_disabled_black_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/round_hourglass_empty_black_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-hdpi/round_hourglass_empty_black_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/round_hourglass_empty_black_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-hdpi/round_hourglass_empty_black_20.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/round_hourglass_empty_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-hdpi/round_hourglass_empty_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/round_hourglass_empty_black_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-hdpi/round_hourglass_empty_black_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/round_hourglass_empty_black_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-hdpi/round_hourglass_empty_black_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/round_hourglass_disabled_black_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-mdpi/round_hourglass_disabled_black_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/round_hourglass_disabled_black_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-mdpi/round_hourglass_disabled_black_20.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/round_hourglass_disabled_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-mdpi/round_hourglass_disabled_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/round_hourglass_disabled_black_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-mdpi/round_hourglass_disabled_black_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/round_hourglass_disabled_black_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-mdpi/round_hourglass_disabled_black_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/round_hourglass_empty_black_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-mdpi/round_hourglass_empty_black_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/round_hourglass_empty_black_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-mdpi/round_hourglass_empty_black_20.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/round_hourglass_empty_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-mdpi/round_hourglass_empty_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/round_hourglass_empty_black_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-mdpi/round_hourglass_empty_black_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/round_hourglass_empty_black_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-mdpi/round_hourglass_empty_black_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/round_hourglass_disabled_black_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xhdpi/round_hourglass_disabled_black_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/round_hourglass_disabled_black_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xhdpi/round_hourglass_disabled_black_20.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/round_hourglass_disabled_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xhdpi/round_hourglass_disabled_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/round_hourglass_disabled_black_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xhdpi/round_hourglass_disabled_black_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/round_hourglass_disabled_black_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xhdpi/round_hourglass_disabled_black_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/round_hourglass_empty_black_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xhdpi/round_hourglass_empty_black_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/round_hourglass_empty_black_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xhdpi/round_hourglass_empty_black_20.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/round_hourglass_empty_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xhdpi/round_hourglass_empty_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/round_hourglass_empty_black_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xhdpi/round_hourglass_empty_black_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/round_hourglass_empty_black_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xhdpi/round_hourglass_empty_black_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/round_hourglass_disabled_black_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxhdpi/round_hourglass_disabled_black_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/round_hourglass_disabled_black_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxhdpi/round_hourglass_disabled_black_20.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/round_hourglass_disabled_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxhdpi/round_hourglass_disabled_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/round_hourglass_disabled_black_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxhdpi/round_hourglass_disabled_black_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/round_hourglass_disabled_black_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxhdpi/round_hourglass_disabled_black_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/round_hourglass_empty_black_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxhdpi/round_hourglass_empty_black_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/round_hourglass_empty_black_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxhdpi/round_hourglass_empty_black_20.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/round_hourglass_empty_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxhdpi/round_hourglass_empty_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/round_hourglass_empty_black_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxhdpi/round_hourglass_empty_black_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/round_hourglass_empty_black_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxhdpi/round_hourglass_empty_black_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/round_hourglass_disabled_black_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxxhdpi/round_hourglass_disabled_black_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/round_hourglass_disabled_black_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxxhdpi/round_hourglass_disabled_black_20.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/round_hourglass_disabled_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxxhdpi/round_hourglass_disabled_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/round_hourglass_disabled_black_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxxhdpi/round_hourglass_disabled_black_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/round_hourglass_disabled_black_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxxhdpi/round_hourglass_disabled_black_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/round_hourglass_empty_black_18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxxhdpi/round_hourglass_empty_black_18.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/round_hourglass_empty_black_20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxxhdpi/round_hourglass_empty_black_20.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/round_hourglass_empty_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxxhdpi/round_hourglass_empty_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/round_hourglass_empty_black_36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxxhdpi/round_hourglass_empty_black_36.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/round_hourglass_empty_black_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/drawable-xxxhdpi/round_hourglass_empty_black_48.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_background.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 13 | 16 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_hourglass_disabled_20.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_hourglass_disabled_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_hourglass_empty_20.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_hourglass_empty_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v30/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v30/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/system_surface_dark 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | #FF4A672D 12 | #FF0E2000 13 | #FFCBEEA5 14 | #FF57624A 15 | #FF386664 16 | #FFB3261E 17 | 18 | #FFAFD18C 19 | #FF334E17 20 | #FFBFCBAD 21 | #FFA0CFCC 22 | #FFF2B8B5 23 | 24 | @android:color/system_surface_light 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | @Suppress("DSL_SCOPE_VIOLATION") 3 | plugins { 4 | alias(libs.plugins.agp.lib) apply false 5 | alias(libs.plugins.agp.app) apply false 6 | alias(libs.plugins.kotlin) apply false 7 | alias(libs.plugins.kotlin.serialization) apply false 8 | alias(libs.plugins.kotlin.jvm) apply false 9 | alias(libs.plugins.ksp) apply false 10 | alias(libs.plugins.compose.compiler) apply false 11 | } 12 | 13 | 14 | tasks.register("Delete", Delete::class) { 15 | delete(rootProject.layout.buildDirectory) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+IgnoreUnrecognizedVMOptions -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.injected.testOnly=false 25 | org.gradle.configuration-cache=true 26 | org.gradle.configureondemand=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wxxsfxyzm/InstallerX-Revived/7eff5a13b751bf852bdcec629d9f2ca7af0470d1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 13 22:04:53 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /hidden-api/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /hidden-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.agp.lib) 3 | } 4 | 5 | android { 6 | namespace = "com.rosan.hidden_api" 7 | compileSdk = 35 8 | 9 | defaultConfig { 10 | minSdk = 30 11 | } 12 | 13 | compileOptions { 14 | targetCompatibility = JavaVersion.VERSION_21 15 | sourceCompatibility = JavaVersion.VERSION_21 16 | } 17 | } 18 | 19 | dependencies { 20 | } -------------------------------------------------------------------------------- /hidden-api/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/content/pm/IPackageInstaller.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | 7 | public interface IPackageInstaller extends IInterface { 8 | void uninstall(android.content.pm.VersionedPackage versionedPackage, java.lang.String callerPackageName, int flags, android.content.IntentSender statusReceiver, int userId) throws android.os.RemoteException; 9 | 10 | abstract class Stub extends Binder implements IPackageInstaller { 11 | public static IPackageInstaller asInterface(IBinder obj) { 12 | throw new UnsupportedOperationException(); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/content/pm/IPackageInstallerSession.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | 7 | public interface IPackageInstallerSession extends IInterface { 8 | abstract class Stub extends Binder implements IPackageInstallerSession { 9 | public static IPackageInstallerSession asInterface(IBinder obj) { 10 | throw new UnsupportedOperationException(); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/content/pm/ParceledListSlice.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 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 android.content.pm; 18 | 19 | import android.os.Parcel; 20 | import android.os.Parcelable; 21 | 22 | import com.android.modules.utils.BaseParceledListSlice; 23 | 24 | import java.util.List; 25 | 26 | /** 27 | * Transfer a large list of Parcelable objects across an IPC. Splits into 28 | * multiple transactions if needed. 29 | * 30 | * @hide 31 | * @see BaseParceledListSlice 32 | */ 33 | public class ParceledListSlice extends BaseParceledListSlice { 34 | public ParceledListSlice(List list) { 35 | } 36 | 37 | private ParceledListSlice(Parcel in, ClassLoader loader) { 38 | } 39 | 40 | @Override 41 | public int describeContents() { 42 | return 0; 43 | } 44 | 45 | @Override 46 | public void writeToParcel(Parcel dest, int flags) { 47 | 48 | } 49 | } -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/content/res/ApkAssets.java: -------------------------------------------------------------------------------- 1 | package android.content.res; 2 | 3 | import android.content.res.loader.AssetsProvider; 4 | 5 | import java.io.FileDescriptor; 6 | import java.io.IOException; 7 | 8 | public class ApkAssets { 9 | public static ApkAssets loadFromPath(String path) throws IOException { 10 | return null; 11 | } 12 | 13 | public static ApkAssets loadFromPath(String path, int flags) throws IOException { 14 | return null; 15 | } 16 | 17 | public static ApkAssets loadFromPath(String path, int flags, 18 | AssetsProvider assets) throws IOException { 19 | return null; 20 | } 21 | 22 | public static ApkAssets loadFromFd(FileDescriptor fd, 23 | String friendlyName, 24 | int flags, 25 | AssetsProvider assets) throws IOException { 26 | return null; 27 | } 28 | 29 | public static ApkAssets loadFromFd(FileDescriptor fd, 30 | String friendlyName, 31 | long offset, 32 | long length, 33 | int flags, 34 | AssetsProvider assets) 35 | throws IOException { 36 | return null; 37 | } 38 | 39 | public static ApkAssets loadFromFd(FileDescriptor fd, 40 | String friendlyName, boolean system, boolean forceSharedLibrary) { 41 | return null; 42 | } 43 | 44 | public static ApkAssets loadOverlayFromPath(String idmapPath, 45 | int flags) throws IOException { 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /hidden-api/src/main/java/com/android/modules/utils/BaseParceledListSlice.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 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.android.modules.utils; 18 | 19 | import android.os.Parcelable; 20 | 21 | import java.util.List; 22 | 23 | /** 24 | * Transfer a large list of Parcelable objects across an IPC. Splits into 25 | * multiple transactions if needed. 26 | *

27 | * Caveat: for efficiency and security, all elements must be the same concrete type. 28 | * In order to avoid writing the class name of each object, we must ensure that 29 | * each object is the same type, or else unparceling then reparceling the data may yield 30 | * a different result if the class name encoded in the Parcelable is a Base type. 31 | * See b/17671747. 32 | */ 33 | public abstract class BaseParceledListSlice implements Parcelable { 34 | public List getList() { 35 | throw new IllegalArgumentException(); 36 | } 37 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenLocal() 4 | // maven { setUrl("https://maven.aliyun.com/repository/public/") } 5 | // maven { setUrl("https://jitpack.io") } 6 | gradlePluginPortal() 7 | google() 8 | mavenCentral() 9 | maven { setUrl("https://maven.scijava.org/content/repositories/public/") } 10 | } 11 | } 12 | 13 | dependencyResolutionManagement { 14 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 15 | repositories { 16 | mavenLocal() 17 | // maven { setUrl("https://maven.aliyun.com/repository/public/") } 18 | // maven { setUrl("https://jitpack.io") } 19 | google() 20 | mavenCentral() 21 | maven { setUrl("https://maven.scijava.org/content/repositories/public/") } 22 | } 23 | } 24 | 25 | rootProject.name = "InstallerX" 26 | include( 27 | ":app", 28 | ":hidden-api" 29 | ) 30 | --------------------------------------------------------------------------------