├── .gitignore ├── .idea ├── assetWizardSettings.xml ├── codeStyles │ └── Project.xml ├── compiler.xml ├── encodings.xml ├── gradle.xml ├── jarRepositories.xml ├── markdown-navigator-enh.xml ├── markdown-navigator.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── tokyonth │ │ └── installer │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ ├── android │ │ │ ├── content │ │ │ │ ├── IIntentReceiver.java │ │ │ │ ├── IIntentSender.java │ │ │ │ └── pm │ │ │ │ │ ├── IPackageInstaller.java │ │ │ │ │ ├── IPackageInstallerCallback.java │ │ │ │ │ ├── IPackageInstallerSession.java │ │ │ │ │ ├── IPackageManager.java │ │ │ │ │ ├── ParceledListSlice.java │ │ │ │ │ └── UserInfo.java │ │ │ └── os │ │ │ │ └── IUserManager.java │ │ └── com │ │ │ └── tokyonth │ │ │ └── installer │ │ │ ├── App.kt │ │ │ ├── Constants.kt │ │ │ ├── activity │ │ │ ├── BaseActivity.kt │ │ │ ├── InstallerActivity.kt │ │ │ ├── SettingsActivity.kt │ │ │ ├── crash │ │ │ │ ├── CrashHelper.kt │ │ │ │ └── ErrorActivity.kt │ │ │ └── model │ │ │ │ └── InstallerViewModel.kt │ │ │ ├── adapter │ │ │ ├── ActivityAdapter.kt │ │ │ ├── PermissionAdapter.kt │ │ │ └── SettingsAdapter.kt │ │ │ ├── data │ │ │ ├── ApkInfoEntity.kt │ │ │ ├── PermissionInfoEntity.kt │ │ │ ├── SPDataManager.kt │ │ │ └── SettingsEntity.kt │ │ │ ├── install │ │ │ ├── APKCommander.kt │ │ │ ├── BaseInstaller.kt │ │ │ ├── InstallApkIceBoxTask.kt │ │ │ ├── InstallApkShellTask.kt │ │ │ ├── InstallApkShizukuTask.kt │ │ │ ├── InstallCallback.kt │ │ │ ├── InstallerFactory.kt │ │ │ ├── InstallerServer.kt │ │ │ ├── ParseApkTask.kt │ │ │ └── shizuku │ │ │ │ ├── IIntentSenderAdaptor.java │ │ │ │ ├── IntentSenderUtils.java │ │ │ │ ├── PackageInstallerUtils.java │ │ │ │ ├── ShizukuSystemServerApi.java │ │ │ │ └── Singleton.java │ │ │ ├── utils │ │ │ ├── AppHelper.kt │ │ │ ├── NotificationUtils.kt │ │ │ ├── PackageUtils.kt │ │ │ ├── PermissionHelper.kt │ │ │ ├── SPUtils.kt │ │ │ ├── ShellUtils.java │ │ │ ├── ktx │ │ │ │ ├── CommonUtils.kt │ │ │ │ ├── FileExt.kt │ │ │ │ ├── TaskJobKtx.kt │ │ │ │ └── ViewBindingKtx.kt │ │ │ └── path │ │ │ │ ├── FileGlobal.kt │ │ │ │ ├── FileProvider.kt │ │ │ │ └── UriPath.kt │ │ │ └── view │ │ │ ├── BaseListLayout.kt │ │ │ ├── DataLoadingView.kt │ │ │ ├── InstallHeaderLayout.kt │ │ │ └── ProgressDrawable.kt │ └── res │ │ ├── drawable │ │ ├── bg_icon.xml │ │ ├── bg_version_tips.xml │ │ ├── ic_app_icon.xml │ │ ├── ic_crash_error.xml │ │ ├── ic_launcher_round.png │ │ ├── round_attachment_24.xml │ │ ├── round_auto_mode_24.xml │ │ ├── round_bedtime_24.xml │ │ ├── round_beenhere_24.xml │ │ ├── round_chevron_right_24.xml │ │ ├── round_construction_24.xml │ │ ├── round_delete_forever_24.xml │ │ ├── round_eject_24.xml │ │ ├── round_get_app_24.xml │ │ ├── round_info_24.xml │ │ ├── round_layers_24.xml │ │ ├── round_memory_24.xml │ │ ├── round_play_for_work_24.xml │ │ ├── round_priority_high_24.xml │ │ ├── round_security_24.xml │ │ ├── round_settings_24.xml │ │ ├── round_silence_24.xml │ │ └── round_wb_sunny_24.xml │ │ ├── layout │ │ ├── activity_crash_error.xml │ │ ├── activity_installer.xml │ │ ├── activity_settings.xml │ │ ├── item_activity_detail.xml │ │ ├── item_permission_detail.xml │ │ ├── item_setting_perf.xml │ │ ├── layout_base_list.xml │ │ ├── layout_error.xml │ │ ├── layout_input_pkg.xml │ │ ├── layout_install_header.xml │ │ └── layout_loading.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── 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 │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-v23 │ │ └── styles.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values │ │ ├── attrs.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── provider_paths.xml │ └── test │ └── java │ └── com │ └── tokyonth │ └── installer │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── icebox ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── catchingnow │ │ └── icebox │ │ └── sdk_client │ │ ├── AppStateUtil.java │ │ ├── AuthorizeUtil.java │ │ ├── IceBox.java │ │ ├── ResultReceiverUtil.java │ │ ├── SdkImplement.java │ │ └── StateReceiver.java │ └── res │ └── values │ └── strings.xml ├── screenshot ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg └── 5.png ├── settings.gradle └── sigenkey /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches/build_file_checksums.ser 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | .DS_Store 9 | /build 10 | /captures 11 | .externalNativeBuild 12 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | -------------------------------------------------------------------------------- /.idea/markdown-navigator-enh.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ApkInstaller 2 | 3 | ### APK安装器 4 | 5 | - APK安装卸载 6 | - 路径显示 源文件删除 夜间模式 7 | - APK权限与Activity显示 8 | - 兼容Android10 Android11 9 | - 支持apks,xapk安装(TODO) 10 | - 支持shizuku icebox 11 | - ... 12 | 13 | ## 截图 14 | 15 | | ![ApkInstaller](screenshot/1.jpg) | ![ApkInstaller](screenshot/2.jpg) | ![ApkInstaller](screenshot/3.jpg) | 16 | | :----: | :----: | :----: | 17 | | ![ApkInstaller](screenshot/4.jpg) | ![ApkInstaller](screenshot/5.png) | | 18 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | } 6 | 7 | android { 8 | compileSdkVersion 33 9 | 10 | defaultConfig { 11 | applicationId "com.tokyonth.installer" 12 | minSdkVersion 21 13 | targetSdkVersion 33 14 | versionCode 140 15 | versionName "1.4.0" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | debug { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | buildFeatures { 36 | viewBinding true 37 | } 38 | } 39 | 40 | dependencies { 41 | implementation fileTree(dir: 'libs', include: ['*.jar']) 42 | implementation project(":icebox") 43 | 44 | implementation "androidx.core:core-ktx:1.9.0" 45 | implementation 'androidx.appcompat:appcompat:1.6.1' 46 | implementation 'androidx.palette:palette-ktx:1.0.0' 47 | implementation 'com.google.android.material:material:1.8.0' 48 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 49 | implementation 'androidx.recyclerview:recyclerview:1.3.0' 50 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' 51 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' 52 | implementation 'androidx.activity:activity-ktx:1.7.0' 53 | implementation 'androidx.fragment:fragment-ktx:1.5.6' 54 | 55 | implementation "dev.rikka.shizuku:api:13.1.0" 56 | implementation "dev.rikka.shizuku:provider:13.1.0" 57 | implementation 'me.weishu:free_reflection:3.0.1' 58 | 59 | testImplementation 'junit:junit:4.13.2' 60 | androidTestImplementation 'androidx.test:runner:1.5.2' 61 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 62 | } 63 | -------------------------------------------------------------------------------- /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. 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 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tokyonth/installer/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer; 2 | 3 | import android.content.Context; 4 | import androidx.test.platform.app.InstrumentationRegistry; 5 | import androidx.test.ext.junit.runners.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | assertEquals("com.tokyonth.installer", appContext.getPackageName()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 24 | 25 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 49 | 50 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 88 | 89 | 94 | 97 | 98 | 99 | 106 | 107 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tokyonth/ApkInstaller/fb5b41ac57a1652a093d5dda1f02a514e3864690/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/android/content/IIntentReceiver.java: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | public class IIntentReceiver { 4 | } -------------------------------------------------------------------------------- /app/src/main/java/android/content/IIntentSender.java: -------------------------------------------------------------------------------- 1 | package android.content; 2 | 3 | import android.os.Binder; 4 | import android.os.Bundle; 5 | import android.os.IBinder; 6 | import android.os.IInterface; 7 | 8 | import androidx.annotation.RequiresApi; 9 | 10 | public interface IIntentSender extends IInterface { 11 | 12 | int send(int code, Intent intent, String resolvedType, 13 | IIntentReceiver finishedReceiver, String requiredPermission, Bundle options); 14 | 15 | @RequiresApi(26) 16 | void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, 17 | IIntentReceiver finishedReceiver, String requiredPermission, Bundle options); 18 | 19 | abstract class Stub extends Binder implements IIntentSender { 20 | 21 | public Stub() { 22 | throw new UnsupportedOperationException(); 23 | } 24 | 25 | @Override 26 | public android.os.IBinder asBinder() { 27 | throw new UnsupportedOperationException(); 28 | } 29 | 30 | public static IIntentSender asInterface(IBinder binder) { 31 | throw new UnsupportedOperationException(); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/android/content/pm/IPackageInstaller.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.content.IntentSender; 4 | import android.graphics.Bitmap; 5 | import android.os.Binder; 6 | import android.os.IBinder; 7 | import android.os.IInterface; 8 | import android.os.RemoteException; 9 | 10 | import androidx.annotation.RequiresApi; 11 | 12 | import java.util.List; 13 | 14 | public interface IPackageInstaller extends IInterface { 15 | 16 | int createSession(PackageInstaller.SessionParams params, String installerPackageName, int userId) 17 | throws RemoteException; 18 | 19 | void updateSessionAppIcon(int sessionId, Bitmap appIcon) 20 | throws RemoteException; 21 | 22 | void updateSessionAppLabel(int sessionId, String appLabel) 23 | throws RemoteException; 24 | 25 | void abandonSession(int sessionId) 26 | throws RemoteException; 27 | 28 | IPackageInstallerSession openSession(int sessionId) 29 | throws RemoteException; 30 | 31 | PackageInstaller.SessionInfo getSessionInfo(int sessionId) 32 | throws RemoteException; 33 | 34 | ParceledListSlice getAllSessions(int userId) 35 | throws RemoteException; 36 | 37 | ParceledListSlice getMySessions(String installerPackageName, int userId) 38 | throws RemoteException; 39 | 40 | @RequiresApi(29) 41 | ParceledListSlice getStagedSessions() 42 | throws RemoteException; 43 | 44 | void registerCallback(IPackageInstallerCallback callback, int userId) 45 | throws RemoteException; 46 | 47 | void unregisterCallback(IPackageInstallerCallback callback) 48 | throws RemoteException; 49 | 50 | // removed from 26 51 | void uninstall(String packageName, String callerPackageName, int flags, 52 | IntentSender statusReceiver, int userId); 53 | 54 | @RequiresApi(26) 55 | void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags, 56 | IntentSender statusReceiver, int userId) 57 | throws RemoteException; 58 | 59 | @RequiresApi(29) 60 | void installExistingPackage(String packageName, int installFlags, int installReason, 61 | IntentSender statusReceiver, int userId, List whiteListedPermissions) 62 | throws RemoteException; 63 | 64 | void setPermissionsResult(int sessionId, boolean accepted) 65 | throws RemoteException; 66 | 67 | abstract class Stub extends Binder implements IPackageInstaller { 68 | 69 | public static IPackageInstaller asInterface(IBinder binder) { 70 | throw new UnsupportedOperationException(); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /app/src/main/java/android/content/pm/IPackageInstallerCallback.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | public interface IPackageInstallerCallback { 4 | } -------------------------------------------------------------------------------- /app/src/main/java/android/content/pm/IPackageInstallerSession.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.content.IntentSender; 4 | import android.os.Binder; 5 | import android.os.IBinder; 6 | import android.os.IInterface; 7 | import android.os.ParcelFileDescriptor; 8 | import android.os.RemoteException; 9 | 10 | import androidx.annotation.RequiresApi; 11 | 12 | public interface IPackageInstallerSession extends IInterface { 13 | 14 | void setClientProgress(float progress) 15 | throws RemoteException; 16 | 17 | void addClientProgress(float progress) 18 | throws RemoteException; 19 | 20 | String[] getNames() 21 | throws RemoteException; 22 | 23 | ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) 24 | throws RemoteException; 25 | 26 | ParcelFileDescriptor openRead(String name) 27 | throws RemoteException; 28 | 29 | @RequiresApi(27) 30 | void write(String name, long offsetBytes, long lengthBytes, ParcelFileDescriptor fd) 31 | throws RemoteException; 32 | 33 | @RequiresApi(24) 34 | void removeSplit(String splitName) 35 | throws RemoteException; 36 | 37 | void close() 38 | throws RemoteException; 39 | 40 | // removed from 28 41 | void commit(IntentSender statusReceiver) 42 | throws RemoteException; 43 | 44 | @RequiresApi(28) 45 | void commit(IntentSender statusReceiver, boolean forTransferred) 46 | throws RemoteException; 47 | 48 | @RequiresApi(28) 49 | void transfer(String packageName) 50 | throws RemoteException; 51 | 52 | void abandon() 53 | throws RemoteException; 54 | 55 | @RequiresApi(29) 56 | boolean isMultiPackage() 57 | throws RemoteException; 58 | 59 | @RequiresApi(29) 60 | int[] getChildSessionIds() 61 | throws RemoteException; 62 | 63 | @RequiresApi(29) 64 | void addChildSessionId(int sessionId) 65 | throws RemoteException; 66 | 67 | @RequiresApi(29) 68 | void removeChildSessionId(int sessionId) 69 | throws RemoteException; 70 | 71 | @RequiresApi(29) 72 | int getParentSessionId() 73 | throws RemoteException; 74 | 75 | @RequiresApi(29) 76 | boolean isStaged() 77 | throws RemoteException; 78 | 79 | abstract class Stub extends Binder implements IPackageInstallerSession { 80 | 81 | public static IPackageInstallerSession asInterface(IBinder binder) { 82 | throw new UnsupportedOperationException(); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/android/content/pm/IPackageManager.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | import android.os.RemoteException; 7 | 8 | public interface IPackageManager extends IInterface { 9 | 10 | IPackageInstaller getPackageInstaller() 11 | throws RemoteException; 12 | 13 | ParceledListSlice getInstalledPackages(int flags, int userId) 14 | throws RemoteException; 15 | 16 | abstract class Stub extends Binder implements IPackageManager { 17 | 18 | public static IPackageManager asInterface(IBinder obj) { 19 | throw new UnsupportedOperationException(); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/android/content/pm/ParceledListSlice.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import java.util.List; 7 | 8 | public class ParceledListSlice { 9 | 10 | public static final Parcelable.ClassLoaderCreator CREATOR = new Parcelable.ClassLoaderCreator() { 11 | public ParceledListSlice createFromParcel(Parcel var1) { 12 | throw new UnsupportedOperationException(); 13 | } 14 | 15 | public ParceledListSlice createFromParcel(Parcel var1, ClassLoader var2) { 16 | throw new UnsupportedOperationException(); 17 | } 18 | 19 | public ParceledListSlice[] newArray(int var1) { 20 | throw new UnsupportedOperationException(); 21 | } 22 | }; 23 | 24 | public List getList() { 25 | throw new UnsupportedOperationException(); 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/android/content/pm/UserInfo.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class UserInfo { 7 | 8 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 9 | 10 | public UserInfo createFromParcel(Parcel var1) { 11 | throw new UnsupportedOperationException(); 12 | } 13 | 14 | public UserInfo[] newArray(int var1) { 15 | throw new UnsupportedOperationException(); 16 | } 17 | }; 18 | } -------------------------------------------------------------------------------- /app/src/main/java/android/os/IUserManager.java: -------------------------------------------------------------------------------- 1 | package android.os; 2 | 3 | import android.content.pm.UserInfo; 4 | 5 | import java.util.List; 6 | 7 | public interface IUserManager extends IInterface { 8 | 9 | List getUsers(boolean excludeDying) 10 | throws RemoteException; 11 | 12 | List getUsers(boolean excludePartial, boolean excludeDying, boolean excludePreCreated) 13 | throws RemoteException; 14 | 15 | abstract class Stub implements IUserManager { 16 | 17 | public static IUserManager asInterface(IBinder obj) { 18 | throw new UnsupportedOperationException(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/App.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.content.Context 6 | import androidx.appcompat.app.AppCompatDelegate 7 | import com.google.android.material.color.DynamicColors 8 | import com.tokyonth.installer.activity.crash.CrashHelper 9 | import com.tokyonth.installer.data.SPDataManager 10 | import me.weishu.reflection.Reflection 11 | 12 | class App : Application() { 13 | 14 | companion object { 15 | 16 | @SuppressLint("StaticFieldLeak") 17 | lateinit var context: Context 18 | private set 19 | 20 | } 21 | 22 | override fun onCreate() { 23 | super.onCreate() 24 | context = applicationContext 25 | 26 | CrashHelper.install(this) 27 | DynamicColors.applyToActivitiesIfAvailable(this) 28 | initNightMode() 29 | } 30 | 31 | private fun initNightMode() { 32 | SPDataManager.instance.run { 33 | if (isFollowSystem()) { 34 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) 35 | } else { 36 | AppCompatDelegate.setDefaultNightMode( 37 | if (isNightMode()) { 38 | AppCompatDelegate.MODE_NIGHT_YES 39 | } else { 40 | AppCompatDelegate.MODE_NIGHT_NO 41 | } 42 | ) 43 | } 44 | } 45 | } 46 | 47 | override fun attachBaseContext(base: Context?) { 48 | super.attachBaseContext(base) 49 | Reflection.unseal(this) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer 2 | 3 | object Constants { 4 | 5 | private const val PKG_NAME = "com.tokyonth.installer" 6 | const val URI_DATA_TYPE = "application/vnd.android.package-archive" 7 | const val DEFAULT_SYS_PKG_NAME = "com.android.packageinstaller" 8 | const val MIUI_SYS_PKG_NAME = "com.miui.packageinstaller" 9 | const val PROVIDER_NAME = "${PKG_NAME}.ApkFileProvider" 10 | const val SE_LINUX_COMMAND = "setenforce permissive" 11 | const val INSTALL_COMMAND = "pm install -r -d --user 0 -i $PKG_NAME " 12 | 13 | const val ICEBOX_PKG_NAME = "com.catchingnow.icebox" 14 | const val SHIZUKU_PKG_NAME = "moe.shizuku.privileged.api" 15 | 16 | const val SP_FILE_NAME = "config" 17 | const val SP_SILENT_KEY = "defaultSilent" 18 | const val SP_INSTALL_MODE_KEY = "installMode" 19 | const val SP_CUSTOM_PKG_KEY = "systemPkgName" 20 | const val SP_IS_SYSTEM_PKG_KEY = "useSystemPkg" 21 | const val SP_AUTO_DELETE_KEY = "autoDelete" 22 | const val SP_SHOW_PERMISSION_KEY = "showPermission" 23 | const val SP_SHOW_ACTIVITY_KEY = "showActivity" 24 | const val SP_NIGHT_MODE_KEY = "nightMode" 25 | const val SP_FOLLOW_SYSTEM_NIGHT_KEY = "nightFollowSystem" 26 | const val SP_NEVER_VERSION_TIP_KEY = "neverVersionTips" 27 | const val SP_NEVER_SYSTEM_PKG_KEY = "neverUseSystemPkg" 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/activity/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.activity 2 | 3 | import android.content.res.Configuration 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.core.view.WindowCompat 7 | import androidx.viewbinding.ViewBinding 8 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 9 | 10 | import com.tokyonth.installer.R 11 | import com.tokyonth.installer.data.SPDataManager 12 | import com.tokyonth.installer.utils.ktx.string 13 | import com.tokyonth.installer.utils.ktx.toast 14 | import com.tokyonth.installer.utils.PermissionHelper 15 | 16 | abstract class BaseActivity : AppCompatActivity() { 17 | 18 | abstract fun setBinding(): ViewBinding? 19 | 20 | abstract fun initView() 21 | 22 | abstract fun initData() 23 | 24 | open var permissionHelper: PermissionHelper? = null 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | WindowCompat.setDecorFitsSystemWindows(window, false) 29 | val uiMode = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) 30 | val isNightMode = uiMode == 0x20 31 | 32 | isNightMode.let { 33 | WindowCompat.getInsetsController(window, window.decorView).apply { 34 | isAppearanceLightStatusBars = !it 35 | } 36 | SPDataManager.instance.setNightMode(it) 37 | } 38 | 39 | permissionHelper = PermissionHelper(this) 40 | setBinding()?.let { 41 | setContentView(it.root) 42 | } 43 | initView() 44 | checkPermission() 45 | } 46 | 47 | private fun checkPermission() { 48 | if (!permissionHelper!!.check()) { 49 | MaterialAlertDialogBuilder(this) 50 | .setTitle(R.string.label_notice) 51 | .setMessage(R.string.use_app_warn) 52 | .setNegativeButton(R.string.exit_app) { _, _ -> 53 | finish() 54 | } 55 | .setPositiveButton(R.string.dialog_btn_ok) { _, _ -> 56 | requestPermission() 57 | } 58 | .setCancelable(false) 59 | .show() 60 | } else { 61 | requestPermission() 62 | } 63 | } 64 | 65 | private fun requestPermission() { 66 | permissionHelper?.registerCallback { all, _ -> 67 | if (all) { 68 | initData() 69 | } else { 70 | toast(string(R.string.no_permissions)) 71 | } 72 | } 73 | permissionHelper?.start() 74 | } 75 | 76 | override fun onDestroy() { 77 | super.onDestroy() 78 | permissionHelper?.dispose() 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/activity/crash/CrashHelper.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.activity.crash 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.os.Build 8 | import android.os.Process 9 | import android.util.Log 10 | import androidx.annotation.RestrictTo 11 | import com.tokyonth.installer.utils.PackageUtils.getVersionName 12 | import java.io.PrintWriter 13 | import java.io.StringWriter 14 | import java.util.* 15 | import kotlin.system.exitProcess 16 | 17 | object CrashHelper { 18 | 19 | private val TAG = CrashHelper::class.java.simpleName 20 | 21 | private const val DEFAULT_HANDLER_PACKAGE_NAME = "com.android.internal.os" 22 | 23 | private const val STACK_TRACE_STRING_INTENT = "stackTraceString" 24 | 25 | private const val MAX_STACK_TRACE_SIZE = 131071 //128 KB 26 | 27 | @RestrictTo(RestrictTo.Scope.LIBRARY) 28 | fun install(application: Application) { 29 | try { 30 | val oldHandler = Thread.getDefaultUncaughtExceptionHandler() 31 | if (oldHandler != null && !oldHandler.javaClass.name.startsWith( 32 | DEFAULT_HANDLER_PACKAGE_NAME 33 | ) 34 | ) { 35 | Log.e(TAG, "IMPORTANT WARNING! You already have an UncaughtExceptionHandler.") 36 | } 37 | Thread.setDefaultUncaughtExceptionHandler { _: Thread?, throwable: Throwable -> 38 | val intent = Intent(application, ErrorActivity::class.java).apply { 39 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 40 | } 41 | val sw = StringWriter() 42 | val pw = PrintWriter(sw) 43 | throwable.printStackTrace(pw) 44 | var stackTraceString = sw.toString() 45 | if (stackTraceString.length > MAX_STACK_TRACE_SIZE) { 46 | val disclaimer = " [stack trace too large]" 47 | stackTraceString = stackTraceString.substring( 48 | 0, 49 | MAX_STACK_TRACE_SIZE - disclaimer.length 50 | ) + disclaimer 51 | } 52 | intent.putExtra(STACK_TRACE_STRING_INTENT, stackTraceString) 53 | application.startActivity(intent) 54 | } 55 | } catch (t: Throwable) { 56 | Log.e(TAG, t.toString()) 57 | } 58 | } 59 | 60 | fun getErrorDetails(context: Context, intent: Intent): String { 61 | return buildString { 62 | append("Build Version: ${getVersionName(context)}") 63 | append("\n") 64 | append("SDK Version: ${Build.VERSION.SDK_INT}") 65 | append("\n") 66 | append("Device: ${getDeviceModelName()}") 67 | append("\n") 68 | append("Stack Trace:") 69 | append("\n") 70 | append(intent.getStringExtra(STACK_TRACE_STRING_INTENT)) 71 | } 72 | } 73 | 74 | fun closeApplication(activity: Activity) { 75 | activity.finish() 76 | Process.killProcess(Process.myPid()) 77 | exitProcess(0) 78 | } 79 | 80 | private fun getDeviceModelName(): String { 81 | val manufacturer = Build.MANUFACTURER 82 | val model = Build.MODEL 83 | return if (model.startsWith(manufacturer)) { 84 | capitalize(model) 85 | } else { 86 | capitalize(manufacturer) + " " + model 87 | } 88 | } 89 | 90 | private fun capitalize(s: String?): String { 91 | if (s == null || s.isEmpty()) { 92 | return "" 93 | } 94 | val first = s[0] 95 | return if (Character.isUpperCase(first)) { 96 | s 97 | } else { 98 | first.uppercaseChar().toString() + s.substring(1) 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/activity/crash/ErrorActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.activity.crash 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | 6 | import com.tokyonth.installer.R 7 | import com.tokyonth.installer.activity.BaseActivity 8 | import com.tokyonth.installer.databinding.ActivityCrashErrorBinding 9 | import com.tokyonth.installer.utils.ktx.lazyBind 10 | import com.tokyonth.installer.utils.ktx.toast 11 | 12 | class ErrorActivity : BaseActivity() { 13 | 14 | private val binding: ActivityCrashErrorBinding by lazyBind() 15 | 16 | override fun setBinding() = binding 17 | 18 | override fun initView() { 19 | val crashInfo = CrashHelper.getErrorDetails(this, intent) 20 | binding.run { 21 | tvCrashInfo.text = crashInfo 22 | btnErrorCopy.setOnClickListener { 23 | copyErrorToClipboard(crashInfo) 24 | toast(getString(R.string.copy_error_log)) 25 | } 26 | btnErrorExit.setOnClickListener { 27 | CrashHelper.closeApplication(this@ErrorActivity) 28 | } 29 | } 30 | } 31 | 32 | override fun initData() { 33 | 34 | } 35 | 36 | private fun copyErrorToClipboard(info: String) { 37 | val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager 38 | val clip = ClipData.newPlainText("apkInstallerError", info) 39 | clipboard.setPrimaryClip(clip) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/activity/model/InstallerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.activity.model 2 | 3 | import android.net.Uri 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | 7 | import com.tokyonth.installer.data.ApkInfoEntity 8 | import com.tokyonth.installer.install.APKCommander 9 | import com.tokyonth.installer.install.InstallCallback 10 | 11 | class InstallerViewModel : ViewModel(), InstallCallback { 12 | 13 | val apkParsedLiveData = MutableLiveData() 14 | 15 | val apkParsedFailedLiveData = MutableLiveData() 16 | 17 | val apkPreInstallLiveData = MutableLiveData() 18 | 19 | val apkInstalledLiveData = MutableLiveData() 20 | 21 | val apkInstallLogLiveData = MutableLiveData() 22 | 23 | private var apkCommander: APKCommander? = null 24 | 25 | fun startParse(uri: Uri, referrer: String) { 26 | apkCommander = APKCommander(uri, referrer, this) 27 | apkCommander?.startParse() 28 | } 29 | 30 | fun startInstall(apkInfo: ApkInfoEntity) { 31 | apkCommander?.startInstall(apkInfo) 32 | } 33 | 34 | override fun onApkParsed(apkInfo: ApkInfoEntity) { 35 | apkParsedLiveData.postValue(apkInfo) 36 | } 37 | 38 | override fun onApkParsedFailed(msg: String) { 39 | apkParsedFailedLiveData.postValue(msg) 40 | } 41 | 42 | override fun onApkPreInstall() { 43 | apkPreInstallLiveData.postValue(true) 44 | } 45 | 46 | override fun onApkInstalled(isInstalled: Boolean) { 47 | apkInstalledLiveData.postValue(isInstalled) 48 | } 49 | 50 | override fun onInstallLog(installLog: String) { 51 | apkInstallLogLiveData.postValue(installLog) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/adapter/ActivityAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.tokyonth.installer.databinding.ItemActivityDetailBinding 7 | 8 | class ActivityAdapter(private val list: MutableList) : 9 | RecyclerView.Adapter() { 10 | 11 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActivityViewHolder { 12 | val binding = 13 | ItemActivityDetailBinding.inflate(LayoutInflater.from(parent.context), parent, false) 14 | return ActivityViewHolder(binding) 15 | } 16 | 17 | override fun onBindViewHolder(holder: ActivityViewHolder, position: Int) { 18 | holder.bind(list[position]) 19 | } 20 | 21 | override fun getItemCount(): Int { 22 | return list.size 23 | } 24 | 25 | inner class ActivityViewHolder(private val binding: ItemActivityDetailBinding) : 26 | RecyclerView.ViewHolder(binding.root) { 27 | 28 | fun bind(name: String) { 29 | binding.tvItemActivityName.text = name 30 | } 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/adapter/PermissionAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | import com.tokyonth.installer.data.PermissionInfoEntity 8 | import com.tokyonth.installer.databinding.ItemPermissionDetailBinding 9 | import com.tokyonth.installer.utils.ktx.click 10 | import com.tokyonth.installer.utils.ktx.visibleOrGone 11 | 12 | class PermissionAdapter(private val list: MutableList) : 13 | RecyclerView.Adapter() { 14 | 15 | private var itemClickListener: ((PermissionInfoEntity) -> Unit)? = null 16 | 17 | fun setItemClickListener(itemClickListener: (PermissionInfoEntity) -> Unit) { 18 | this.itemClickListener = itemClickListener 19 | } 20 | 21 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PermissionViewHolder { 22 | val binding = 23 | ItemPermissionDetailBinding.inflate(LayoutInflater.from(parent.context), parent, false) 24 | return PermissionViewHolder(binding) 25 | } 26 | 27 | override fun onBindViewHolder(holder: PermissionViewHolder, position: Int) { 28 | holder.bind(list[position], itemClickListener) 29 | } 30 | 31 | override fun getItemCount(): Int { 32 | return list.size 33 | } 34 | 35 | inner class PermissionViewHolder(private val binding: ItemPermissionDetailBinding) : 36 | RecyclerView.ViewHolder(binding.root) { 37 | 38 | fun bind( 39 | data: PermissionInfoEntity, 40 | click: ((PermissionInfoEntity) -> Unit)? = null 41 | ) { 42 | binding.tvItemPermissionName.text = data.permissionName 43 | binding.tvItemPermissionDesc.text = data.permissionDesc 44 | binding.tvItemPermissionDesc.visibleOrGone(data.permissionDesc.isNotEmpty()) 45 | binding.root.click { 46 | click?.invoke(data) 47 | } 48 | } 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/adapter/SettingsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.CompoundButton 7 | import androidx.recyclerview.widget.RecyclerView 8 | 9 | import com.tokyonth.installer.R 10 | import com.tokyonth.installer.data.SPDataManager 11 | import com.tokyonth.installer.data.SettingsEntity 12 | import com.tokyonth.installer.databinding.ItemSettingPerfBinding 13 | import com.tokyonth.installer.utils.ktx.click 14 | import com.tokyonth.installer.utils.ktx.string 15 | 16 | import java.util.ArrayList 17 | 18 | class SettingsAdapter : RecyclerView.Adapter() { 19 | 20 | private val list: MutableList = ArrayList() 21 | 22 | init { 23 | list.apply { 24 | add( 25 | SettingsEntity( 26 | string(R.string.title_show_perm), 27 | string(R.string.summary_show_perm), 28 | R.drawable.round_security_24, 29 | SPDataManager.instance.isShowPermission() 30 | ) 31 | ) 32 | add( 33 | SettingsEntity( 34 | string(R.string.title_show_activity), 35 | string(R.string.summary_show_activity), 36 | R.drawable.round_layers_24, 37 | SPDataManager.instance.isShowActivity() 38 | ) 39 | ) 40 | add( 41 | SettingsEntity( 42 | string(R.string.default_silent), 43 | string(R.string.default_silent_sub), 44 | R.drawable.round_silence_24, 45 | SPDataManager.instance.isDefaultSilent() 46 | ) 47 | ) 48 | add( 49 | SettingsEntity( 50 | string(R.string.auto_del_apk_title), 51 | string(R.string.auto_delete_apk), 52 | R.drawable.round_delete_forever_24, 53 | SPDataManager.instance.isAutoDel() 54 | ) 55 | ) 56 | add( 57 | SettingsEntity( 58 | string(R.string.follow_system_night_mode), 59 | string(R.string.follow_system_night_mode_sub), 60 | R.drawable.round_auto_mode_24, 61 | SPDataManager.instance.isFollowSystem() 62 | ) 63 | ) 64 | add( 65 | SettingsEntity( 66 | string(R.string.install_mode), 67 | SPDataManager.instance.getInstallName(), 68 | R.drawable.round_play_for_work_24, 69 | false 70 | ) 71 | ) 72 | } 73 | } 74 | 75 | interface OnItemActionListener { 76 | 77 | fun onSwitch(view: CompoundButton, position: Int, bool: Boolean) 78 | 79 | fun onClick(position: Int) 80 | 81 | } 82 | 83 | private var onItemClickListener: OnItemActionListener? = null 84 | 85 | fun setOnItemActionListener(onItemActionListener: OnItemActionListener) { 86 | this.onItemClickListener = onItemActionListener 87 | } 88 | 89 | fun updateInstallMode() { 90 | list[list.size - 1].sub = SPDataManager.instance.getInstallName() 91 | notifyItemChanged(list.size - 1) 92 | } 93 | 94 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingsViewHolder { 95 | val binding = 96 | ItemSettingPerfBinding.inflate(LayoutInflater.from(parent.context), parent, false) 97 | return SettingsViewHolder(binding) 98 | } 99 | 100 | override fun onBindViewHolder(holder: SettingsViewHolder, position: Int) { 101 | holder.bind(list[position], onItemClickListener) 102 | } 103 | 104 | override fun getItemCount(): Int { 105 | return list.size 106 | } 107 | 108 | inner class SettingsViewHolder(private val binding: ItemSettingPerfBinding) : 109 | RecyclerView.ViewHolder(binding.root) { 110 | 111 | fun bind( 112 | data: SettingsEntity, 113 | action: OnItemActionListener? 114 | ) { 115 | binding.itemSettingTitle.text = data.title 116 | binding.itemSettingSub.text = data.sub 117 | binding.itemSettingIcon.setImageResource(data.icon) 118 | 119 | binding.itemSettingSwitch.run { 120 | isChecked = data.selected 121 | if (bindingAdapterPosition == 5) { 122 | visibility = View.GONE 123 | } 124 | 125 | setOnCheckedChangeListener { view, isChecked -> 126 | action?.onSwitch(view, bindingAdapterPosition, isChecked) 127 | } 128 | } 129 | 130 | binding.root.click { 131 | action?.onClick(bindingAdapterPosition) 132 | } 133 | } 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/data/ApkInfoEntity.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.data 2 | 3 | import android.graphics.Bitmap 4 | 5 | class ApkInfoEntity { 6 | 7 | var icon: Bitmap? = null 8 | var filePath: String = "" 9 | var appName: String = "" 10 | var isArm64: Boolean = false 11 | var isXposed: Boolean = false 12 | var isFakePath: Boolean = false 13 | var versionName: String = "" 14 | var versionCode: Int = 0 15 | var packageName: String = "" 16 | var isHasInstalledApp: Boolean = false 17 | var installedVersionName: String = "" 18 | var installedVersionCode: Int = 0 19 | 20 | var activities: MutableList? = null 21 | var permissions: MutableList? = null 22 | 23 | val version: String 24 | get() = "$versionName($versionCode)" 25 | 26 | val installedVersion: String 27 | get() = if (isHasInstalledApp) "$installedVersionName($installedVersionCode)" else " -- " 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/data/PermissionInfoEntity.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.data 2 | 3 | data class PermissionInfoEntity( 4 | var permissionName: String = "", 5 | var permissionGroup: String = "", 6 | var permissionLabel: String = "", 7 | var permissionDesc: String = "" 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/data/SPDataManager.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.data 2 | 3 | import com.tokyonth.installer.App 4 | import com.tokyonth.installer.BuildConfig 5 | import com.tokyonth.installer.Constants 6 | import com.tokyonth.installer.R 7 | import com.tokyonth.installer.utils.AppHelper 8 | import com.tokyonth.installer.utils.SPUtils.getSP 9 | import com.tokyonth.installer.utils.SPUtils.putSP 10 | 11 | class SPDataManager { 12 | 13 | companion object { 14 | val instance: SPDataManager by lazy { 15 | SPDataManager() 16 | } 17 | } 18 | 19 | fun setDefaultSilent(isDefaultSilent: Boolean) { 20 | putSP(Constants.SP_SILENT_KEY, isDefaultSilent) 21 | } 22 | 23 | fun isDefaultSilent(): Boolean { 24 | return getSP(Constants.SP_SILENT_KEY, false) 25 | } 26 | 27 | fun setInstallMode(installMode: Int) { 28 | putSP(Constants.SP_INSTALL_MODE_KEY, installMode) 29 | } 30 | 31 | fun getInstallMode(): Int { 32 | return getSP(Constants.SP_INSTALL_MODE_KEY, 0) 33 | } 34 | 35 | fun setSystemPkg(pkgName: String) { 36 | putSP(Constants.SP_CUSTOM_PKG_KEY, pkgName) 37 | } 38 | 39 | fun getSystemPkg(): String { 40 | val pkgName = if (AppHelper.isMiuiOS()) { 41 | Constants.MIUI_SYS_PKG_NAME 42 | } else { 43 | Constants.DEFAULT_SYS_PKG_NAME 44 | } 45 | return getSP(Constants.SP_CUSTOM_PKG_KEY, pkgName) 46 | } 47 | 48 | fun setUseSystemPkg(isUse: Boolean) { 49 | putSP(Constants.SP_IS_SYSTEM_PKG_KEY, isUse) 50 | } 51 | 52 | fun isUseSystemPkg(): Boolean { 53 | return getSP(Constants.SP_IS_SYSTEM_PKG_KEY, false) 54 | } 55 | 56 | fun setAutoDel(isAutoDel: Boolean) { 57 | putSP(Constants.SP_AUTO_DELETE_KEY, isAutoDel) 58 | } 59 | 60 | fun isAutoDel(): Boolean { 61 | return getSP(Constants.SP_AUTO_DELETE_KEY, false) 62 | } 63 | 64 | fun setShowPermission(isShow: Boolean) { 65 | putSP(Constants.SP_SHOW_PERMISSION_KEY, isShow) 66 | } 67 | 68 | fun isShowPermission(): Boolean { 69 | return getSP(Constants.SP_SHOW_PERMISSION_KEY, true) 70 | } 71 | 72 | fun setShowActivity(isShow: Boolean) { 73 | putSP(Constants.SP_SHOW_ACTIVITY_KEY, isShow) 74 | } 75 | 76 | fun isShowActivity(): Boolean { 77 | return getSP(Constants.SP_SHOW_ACTIVITY_KEY, true) 78 | } 79 | 80 | fun setNightMode(isNightMode: Boolean) { 81 | putSP(Constants.SP_NIGHT_MODE_KEY, isNightMode) 82 | } 83 | 84 | fun isNightMode(): Boolean { 85 | return getSP(Constants.SP_NIGHT_MODE_KEY, false) 86 | } 87 | 88 | fun setFollowSystem(isFollowSys: Boolean) { 89 | putSP(Constants.SP_FOLLOW_SYSTEM_NIGHT_KEY, isFollowSys) 90 | } 91 | 92 | fun isFollowSystem(): Boolean { 93 | return getSP(Constants.SP_FOLLOW_SYSTEM_NIGHT_KEY, false) 94 | } 95 | 96 | fun setNeverShowTip() { 97 | putSP(Constants.SP_NEVER_VERSION_TIP_KEY, true) 98 | } 99 | 100 | fun isNeverShowTip(): Boolean { 101 | return getSP(Constants.SP_NEVER_VERSION_TIP_KEY, false) 102 | } 103 | 104 | fun setNeverShowUsePkg() { 105 | putSP(Constants.SP_NEVER_SYSTEM_PKG_KEY, true) 106 | } 107 | 108 | fun isNeverShowUsePkg(): Boolean { 109 | return getSP(Constants.SP_NEVER_SYSTEM_PKG_KEY, false) 110 | } 111 | 112 | fun getInstallName(): String { 113 | val arr = App.context.resources.getStringArray(R.array.install_mode_arr) 114 | return arr[getInstallMode()] 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/data/SettingsEntity.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.data 2 | 3 | import androidx.annotation.DrawableRes 4 | 5 | data class SettingsEntity( 6 | var title: String, 7 | var sub: String, 8 | @DrawableRes 9 | var icon: Int, 10 | //var color: Int, 11 | var selected: Boolean 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/APKCommander.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install 2 | 3 | import android.net.Uri 4 | import com.tokyonth.installer.data.ApkInfoEntity 5 | import com.tokyonth.installer.data.SPDataManager 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.launch 9 | import java.util.zip.ZipException 10 | 11 | class APKCommander( 12 | private val uri: Uri, 13 | private val referrer: String, 14 | private val callback: InstallCallback 15 | ) { 16 | 17 | fun startParse() { 18 | try { 19 | ParseApkTask(uri, referrer) { 20 | if (it != null) { 21 | callback.onApkParsed(it) 22 | } else { 23 | callback.onApkParsedFailed("Path is empty!") 24 | } 25 | }.startParseApkTask() 26 | } catch (e: Exception) { 27 | if (e is ZipException) { 28 | callback.onApkParsedFailed("The file is not an apk!") 29 | } else { 30 | callback.onApkParsedFailed(e.message.toString()) 31 | } 32 | } 33 | } 34 | 35 | fun startInstall(apkInfoEntity: ApkInfoEntity) { 36 | val mode = SPDataManager.instance.getInstallMode() 37 | val installer = InstallerFactory.create(mode, apkInfoEntity, callback) 38 | CoroutineScope(Dispatchers.IO).launch { 39 | installer.install() 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/BaseInstaller.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install 2 | 3 | import com.tokyonth.installer.data.ApkInfoEntity 4 | 5 | abstract class BaseInstaller( 6 | val apkInfoEntity: ApkInfoEntity, 7 | val installCallback: InstallCallback 8 | ) { 9 | 10 | init { 11 | installCallback.onApkPreInstall() 12 | } 13 | 14 | abstract suspend fun install() 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/InstallApkIceBoxTask.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install 2 | 3 | import androidx.core.content.FileProvider 4 | import com.catchingnow.icebox.sdk_client.IceBox 5 | import com.tokyonth.installer.App 6 | import com.tokyonth.installer.Constants 7 | import com.tokyonth.installer.R 8 | import com.tokyonth.installer.data.ApkInfoEntity 9 | import com.tokyonth.installer.utils.ktx.string 10 | import java.io.File 11 | 12 | class InstallApkIceBoxTask( 13 | apkInfoEntity: ApkInfoEntity, 14 | installCallback: InstallCallback 15 | ) : BaseInstaller(apkInfoEntity, installCallback) { 16 | 17 | override suspend fun install() { 18 | val iceboxState = when (IceBox.querySupportSilentInstall(App.context)) { 19 | IceBox.SilentInstallSupport.NOT_DEVICE_OWNER -> { 20 | string(R.string.not_icebox_owner) 21 | } 22 | IceBox.SilentInstallSupport.PERMISSION_REQUIRED -> { 23 | string(R.string.not_icebox_perm) 24 | } 25 | IceBox.SilentInstallSupport.SYSTEM_NOT_SUPPORTED -> { 26 | string(R.string.not_support_icebox_system) 27 | } 28 | IceBox.SilentInstallSupport.UPDATE_REQUIRED -> { 29 | string(R.string.not_support_icebox_version) 30 | } 31 | IceBox.SilentInstallSupport.NOT_INSTALLED -> { 32 | string(R.string.not_install_icebox) 33 | } 34 | IceBox.SilentInstallSupport.NOT_RUNNING -> { 35 | string(R.string.icebox_not_running) 36 | } 37 | else -> "" 38 | } 39 | installCallback.onInstallLog(iceboxState) 40 | if (iceboxState.isNotEmpty()) { 41 | installCallback.onApkInstalled(false) 42 | return 43 | } 44 | val authority = Constants.PROVIDER_NAME 45 | val uri = FileProvider.getUriForFile(App.context, authority, File(apkInfoEntity.filePath)) 46 | val status = IceBox.installPackage(App.context, uri) 47 | installCallback.onApkInstalled(status) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/InstallApkShellTask.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install 2 | 3 | import android.os.Build 4 | 5 | import com.tokyonth.installer.Constants 6 | import com.tokyonth.installer.data.ApkInfoEntity 7 | import com.tokyonth.installer.utils.ShellUtils 8 | 9 | class InstallApkShellTask( 10 | apkInfoEntity: ApkInfoEntity, 11 | installCallback: InstallCallback 12 | ) : BaseInstaller(apkInfoEntity, installCallback) { 13 | 14 | override suspend fun install() { 15 | if (Build.VERSION.SDK_INT >= 24) { 16 | ShellUtils.execWithRoot(Constants.SE_LINUX_COMMAND) 17 | } 18 | val retCode = ShellUtils.execWithRoot( 19 | Constants.INSTALL_COMMAND + "\"" + apkInfoEntity.filePath + "\"" + "\n", 20 | object : ShellUtils.Result { 21 | override fun onStdout(text: String) { 22 | installCallback.onInstallLog(text) 23 | } 24 | 25 | override fun onStderr(text: String) { 26 | installCallback.onInstallLog(text) 27 | } 28 | 29 | override fun onCommand(command: String) { 30 | 31 | } 32 | 33 | override fun onFinish(resultCode: Int) { 34 | 35 | } 36 | }) 37 | installCallback.onApkInstalled(retCode == 0) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/InstallApkShizukuTask.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install 2 | 3 | import android.content.Intent 4 | import android.content.IntentSender 5 | import android.content.pm.IPackageInstallerSession 6 | import android.content.pm.PackageInstaller 7 | import android.os.Process 8 | 9 | import com.tokyonth.installer.App 10 | import com.tokyonth.installer.data.ApkInfoEntity 11 | import com.tokyonth.installer.install.shizuku.IIntentSenderAdaptor 12 | import com.tokyonth.installer.install.shizuku.IntentSenderUtils 13 | import com.tokyonth.installer.install.shizuku.PackageInstallerUtils 14 | import com.tokyonth.installer.install.shizuku.ShizukuSystemServerApi 15 | 16 | import rikka.shizuku.Shizuku 17 | import rikka.shizuku.ShizukuBinderWrapper 18 | import java.io.File 19 | import java.util.concurrent.CountDownLatch 20 | 21 | class InstallApkShizukuTask( 22 | apkInfoEntity: ApkInfoEntity, 23 | installCallback: InstallCallback 24 | ) : BaseInstaller(apkInfoEntity, installCallback) { 25 | 26 | private fun getPackageInstaller(): PackageInstaller { 27 | val isRoot = Shizuku.getUid() == 0 28 | // the reason for use "com.android.shell" as installer package under adb is that getMySessions will check installer package's owner 29 | val installerPackageName = if (isRoot) App.context.packageName else "com.android.shell" 30 | val userId = if (isRoot) Process.myUserHandle().hashCode() else 0 31 | return PackageInstallerUtils.createPackageInstaller( 32 | ShizukuSystemServerApi.PackageManager_getPackageInstaller(), 33 | installerPackageName, 34 | userId 35 | ) 36 | } 37 | 38 | private fun getPackageInstallerSession(totalSize: Long? = null): PackageInstaller.Session { 39 | val res = StringBuilder() 40 | res.append("createSession: ") 41 | 42 | val params: PackageInstaller.SessionParams = 43 | PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) 44 | totalSize?.let { 45 | params.setSize(totalSize) 46 | } 47 | var installFlags = PackageInstallerUtils.getInstallFlags(params) 48 | installFlags = 49 | installFlags or (0x00000004 /*PackageManager.INSTALL_ALLOW_TEST*/ or 0x00000002) /*PackageManager.INSTALL_REPLACE_EXISTING*/ 50 | PackageInstallerUtils.setInstallFlags(params, installFlags) 51 | 52 | val packageInstaller = getPackageInstaller() 53 | val sessionId = packageInstaller.createSession(params) 54 | res.append(sessionId) 55 | @Suppress("LocalVariableName") 56 | val _session = IPackageInstallerSession.Stub.asInterface( 57 | ShizukuBinderWrapper( 58 | ShizukuSystemServerApi.PackageManager_getPackageInstaller().openSession(sessionId) 59 | .asBinder() 60 | ) 61 | ) 62 | return PackageInstallerUtils.createSession(_session) 63 | } 64 | 65 | private fun doWriteSession(session: PackageInstaller.Session, apkFile: File) { 66 | val inputStream = apkFile.inputStream() 67 | val outputStream = session.openWrite(apkFile.name, 0, -1) 68 | val buf = ByteArray(8192) 69 | var len: Int 70 | 71 | while (inputStream.read(buf).also { len = it } > 0) { 72 | outputStream.write(buf, 0, len) 73 | outputStream.flush() 74 | session.fsync(outputStream) 75 | } 76 | inputStream.close() 77 | outputStream.close() 78 | } 79 | 80 | private fun doCommitSession(session: PackageInstaller.Session): Intent? { 81 | val results = arrayOf(null) 82 | val countDownLatch = CountDownLatch(1) 83 | val intentSender: IntentSender = 84 | IntentSenderUtils.newInstance(object : IIntentSenderAdaptor() { 85 | override fun send(intent: Intent?) { 86 | results[0] = intent 87 | countDownLatch.countDown() 88 | } 89 | }) 90 | session.commit(intentSender) 91 | countDownLatch.await() 92 | return results[0] 93 | } 94 | 95 | override suspend fun install() { 96 | val apkFile = File(apkInfoEntity.filePath) 97 | var session: PackageInstaller.Session? = null 98 | val res: StringBuilder = StringBuilder() 99 | try { 100 | session = getPackageInstallerSession() 101 | res.append('\n').append("write: ") 102 | doWriteSession(session, apkFile) 103 | 104 | res.append('\n').append("commit: ") 105 | val result = doCommitSession(session) 106 | val status = result!!.getIntExtra( 107 | PackageInstaller.EXTRA_STATUS, 108 | PackageInstaller.STATUS_FAILURE 109 | ) 110 | val message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) 111 | res.append('\n').append("status: ").append(status).append(" (").append(message) 112 | .append(")") 113 | installCallback.onApkInstalled(status == 0) 114 | } catch (tr: Throwable) { 115 | tr.printStackTrace() 116 | res.append(tr) 117 | installCallback.onApkInstalled(false) 118 | } finally { 119 | try { 120 | session?.close() 121 | } catch (tr: Throwable) { 122 | res.append(tr) 123 | } 124 | } 125 | installCallback.onInstallLog(res.toString().trim()) 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/InstallCallback.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install 2 | 3 | import com.tokyonth.installer.data.ApkInfoEntity 4 | 5 | interface InstallCallback { 6 | 7 | fun onApkParsed(apkInfo: ApkInfoEntity) 8 | 9 | fun onApkParsedFailed(msg: String) 10 | 11 | fun onApkPreInstall() 12 | 13 | fun onApkInstalled(isInstalled: Boolean) 14 | 15 | fun onInstallLog(installLog: String) 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/InstallerFactory.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install 2 | 3 | import com.tokyonth.installer.data.ApkInfoEntity 4 | 5 | object InstallerFactory { 6 | 7 | fun create( 8 | installMode: Int, 9 | apkInfoEntity: ApkInfoEntity, 10 | installCallback: InstallCallback 11 | ): BaseInstaller { 12 | return when (installMode) { 13 | 0 -> InstallApkShellTask(apkInfoEntity, installCallback) 14 | 1 -> InstallApkShizukuTask(apkInfoEntity, installCallback) 15 | 2 -> InstallApkIceBoxTask(apkInfoEntity, installCallback) 16 | else -> throw IllegalArgumentException("not found install mode!") 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/InstallerServer.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.core.app.JobIntentService 6 | import com.tokyonth.installer.R 7 | import com.tokyonth.installer.data.ApkInfoEntity 8 | import com.tokyonth.installer.data.SPDataManager 9 | import com.tokyonth.installer.utils.NotificationUtils 10 | import com.tokyonth.installer.utils.ktx.string 11 | import java.io.File 12 | 13 | class InstallerServer : JobIntentService(), InstallCallback { 14 | 15 | companion object { 16 | private const val JOB_ID = 100 17 | const val APK_REFERER = "apkReferer" 18 | 19 | fun enqueueWork(context: Context, work: Intent) { 20 | enqueueWork(context, InstallerServer::class.java, JOB_ID, work) 21 | } 22 | } 23 | 24 | private var apkCommander: APKCommander? = null 25 | 26 | private var apkInfo: ApkInfoEntity? = null 27 | 28 | private var installLog: String = "" 29 | 30 | override fun onHandleWork(intent: Intent) { 31 | intent.data.let { uri -> 32 | if (uri == null) { 33 | sendNotification(string(R.string.unable_install_apk)) 34 | } else { 35 | val ref = intent.getStringExtra(APK_REFERER).orEmpty() 36 | apkCommander = APKCommander(uri, ref, this) 37 | apkCommander?.startParse() 38 | } 39 | } 40 | } 41 | 42 | override fun onApkParsed(apkInfo: ApkInfoEntity) { 43 | if (apkInfo.packageName.isNotEmpty()) { 44 | this.apkInfo = apkInfo 45 | apkCommander?.startInstall(apkInfo) 46 | } else { 47 | sendNotification(string(R.string.unable_install_apk)) 48 | } 49 | } 50 | 51 | override fun onApkParsedFailed(msg: String) { 52 | sendNotification(string(R.string.unable_install_apk)) 53 | } 54 | 55 | override fun onApkPreInstall() { 56 | sendNotification(string(R.string.start_install, apkInfo!!.appName)) 57 | } 58 | 59 | override fun onApkInstalled(isInstalled: Boolean) { 60 | var notificationSub = apkInfo!!.appName 61 | val status = if (isInstalled) { 62 | if (SPDataManager.instance.isAutoDel()) { 63 | File(apkInfo!!.filePath).delete() 64 | notificationSub = string(R.string.auto_del_notification, notificationSub) 65 | } 66 | string(R.string.install_successful) 67 | } else { 68 | notificationSub += " ($installLog)" 69 | string(R.string.install_failed_msg) 70 | } 71 | if (apkInfo!!.isFakePath) { 72 | File(apkInfo!!.filePath).delete() 73 | } 74 | NotificationUtils.sendNotification( 75 | this, 76 | status, 77 | notificationSub, 78 | apkInfo!!.icon!! 79 | ) 80 | } 81 | 82 | private fun sendNotification(msg: String) { 83 | NotificationUtils.sendNotification( 84 | this, 85 | string(R.string.installing), 86 | msg, 87 | apkInfo!!.icon!! 88 | ) 89 | } 90 | 91 | override fun onInstallLog(installLog: String) { 92 | this.installLog = installLog 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/shizuku/IIntentSenderAdaptor.java: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install.shizuku; 2 | 3 | import android.content.IIntentReceiver; 4 | import android.content.IIntentSender; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.os.IBinder; 8 | 9 | public abstract class IIntentSenderAdaptor extends IIntentSender.Stub { 10 | 11 | public abstract void send(Intent intent); 12 | 13 | @Override 14 | public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { 15 | send(intent); 16 | return 0; 17 | } 18 | 19 | @Override 20 | public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { 21 | send(intent); 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/shizuku/IntentSenderUtils.java: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install.shizuku; 2 | 3 | import android.content.IIntentSender; 4 | import android.content.IntentSender; 5 | 6 | import java.lang.reflect.InvocationTargetException; 7 | 8 | public class IntentSenderUtils { 9 | 10 | public static IntentSender newInstance(IIntentSender binder) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 11 | //noinspection JavaReflectionMemberAccess 12 | return IntentSender.class.getConstructor(IIntentSender.class).newInstance(binder); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/shizuku/PackageInstallerUtils.java: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install.shizuku; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.pm.IPackageInstaller; 6 | import android.content.pm.IPackageInstallerSession; 7 | import android.content.pm.PackageInstaller; 8 | import android.content.pm.PackageManager; 9 | import android.os.Build; 10 | 11 | import com.tokyonth.installer.App; 12 | 13 | import java.lang.reflect.InvocationTargetException; 14 | 15 | @SuppressWarnings({"JavaReflectionMemberAccess", "ConstantConditions"}) 16 | public class PackageInstallerUtils { 17 | 18 | public static PackageInstaller createPackageInstaller(IPackageInstaller installer, String installerPackageName, int userId) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 19 | if (Build.VERSION.SDK_INT >= 26) { 20 | if (Build.VERSION.SDK_INT >= 31) { 21 | return PackageInstaller.class.getConstructor(IPackageInstaller.class, String.class, String.class, int.class) 22 | .newInstance(installer, App.Companion.getContext().getAttributionTag(), installerPackageName, userId); 23 | } else { 24 | return PackageInstaller.class.getConstructor(IPackageInstaller.class, String.class, int.class) 25 | .newInstance(installer, installerPackageName, userId); 26 | } 27 | } else { 28 | return PackageInstaller.class.getConstructor(Context.class, PackageManager.class, IPackageInstaller.class, String.class, int.class) 29 | .newInstance(App.Companion.getContext(), App.Companion.getContext().getPackageManager(), installer, installerPackageName, userId); 30 | } 31 | } 32 | 33 | public static PackageInstaller.Session createSession(IPackageInstallerSession session) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 34 | return PackageInstaller.Session.class.getConstructor(IPackageInstallerSession.class) 35 | .newInstance(session); 36 | } 37 | 38 | @SuppressLint("PrivateApi") 39 | public static int getInstallFlags(PackageInstaller.SessionParams params) throws NoSuchFieldException, IllegalAccessException { 40 | return (int) PackageInstaller.SessionParams.class.getDeclaredField("installFlags").get(params); 41 | } 42 | 43 | @SuppressLint("PrivateApi") 44 | public static void setInstallFlags(PackageInstaller.SessionParams params, int newValue) throws NoSuchFieldException, IllegalAccessException { 45 | PackageInstaller.SessionParams.class.getDeclaredField("installFlags").set(params, newValue); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/shizuku/ShizukuSystemServerApi.java: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install.shizuku; 2 | 3 | import android.content.Context; 4 | import android.content.pm.IPackageInstaller; 5 | import android.content.pm.IPackageManager; 6 | import android.content.pm.UserInfo; 7 | import android.os.Build; 8 | import android.os.IUserManager; 9 | import android.os.RemoteException; 10 | 11 | import java.util.List; 12 | 13 | import rikka.shizuku.ShizukuBinderWrapper; 14 | import rikka.shizuku.SystemServiceHelper; 15 | 16 | public class ShizukuSystemServerApi { 17 | 18 | private static final Singleton PACKAGE_MANAGER = new Singleton() { 19 | @Override 20 | protected IPackageManager create() { 21 | return IPackageManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package"))); 22 | } 23 | }; 24 | 25 | private static final Singleton USER_MANAGER = new Singleton() { 26 | @Override 27 | protected IUserManager create() { 28 | return IUserManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService(Context.USER_SERVICE))); 29 | } 30 | }; 31 | 32 | public static IPackageInstaller PackageManager_getPackageInstaller() throws RemoteException { 33 | IPackageInstaller packageInstaller = PACKAGE_MANAGER.get().getPackageInstaller(); 34 | return IPackageInstaller.Stub.asInterface(new ShizukuBinderWrapper(packageInstaller.asBinder())); 35 | } 36 | 37 | public static List UserManager_getUsers(boolean excludePartial, boolean excludeDying, boolean excludePreCreated) throws RemoteException { 38 | if (Build.VERSION.SDK_INT >= 30) { 39 | return USER_MANAGER.get().getUsers(excludePartial, excludeDying, excludePreCreated); 40 | } else { 41 | try { 42 | return USER_MANAGER.get().getUsers(excludeDying); 43 | } catch (NoSuchFieldError e) { 44 | return USER_MANAGER.get().getUsers(excludePartial, excludeDying, excludePreCreated); 45 | } 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/install/shizuku/Singleton.java: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.install.shizuku; 2 | 3 | public abstract class Singleton { 4 | 5 | private T mInstance; 6 | 7 | protected abstract T create(); 8 | 9 | public final T get() { 10 | synchronized (this) { 11 | if (mInstance == null) { 12 | mInstance = create(); 13 | } 14 | return mInstance; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/utils/AppHelper.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.utils 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.graphics.Bitmap 9 | import android.graphics.Canvas 10 | import android.graphics.Color 11 | import android.graphics.PixelFormat 12 | import android.graphics.drawable.BitmapDrawable 13 | import android.graphics.drawable.Drawable 14 | import android.net.Uri 15 | import android.os.Environment 16 | import androidx.core.content.FileProvider 17 | import com.tokyonth.installer.Constants 18 | import com.tokyonth.installer.R 19 | import com.tokyonth.installer.data.SPDataManager 20 | import com.tokyonth.installer.utils.ktx.toast 21 | import java.io.File 22 | import java.io.FileInputStream 23 | import java.io.IOException 24 | import java.util.* 25 | import kotlin.math.floor 26 | 27 | object AppHelper { 28 | 29 | fun toSelfSetting(context: Context, str: String?) { 30 | Intent().apply { 31 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 32 | action = "android.settings.APPLICATION_DETAILS_SETTINGS" 33 | data = Uri.fromParts("package", str, null) 34 | context.startActivity(this) 35 | } 36 | } 37 | 38 | fun colorBurn(RGBValues: Int): Int { 39 | var red = RGBValues shr 16 and 0xFF 40 | var green = RGBValues shr 8 and 0xFF 41 | var blue = RGBValues and 0xFF 42 | red = floor(red * (1 - 0.1)).toInt() 43 | green = floor(green * (1 - 0.1)).toInt() 44 | blue = floor(blue * (1 - 0.1)).toInt() 45 | return Color.rgb(red, green, blue) 46 | } 47 | 48 | fun executeSystemPkgInstall(context: Context, filePath: String) { 49 | val sysPkgName = if (SPDataManager.instance.isUseSystemPkg()) { 50 | SPDataManager.instance.getSystemPkg() 51 | } else { 52 | Constants.DEFAULT_SYS_PKG_NAME 53 | } 54 | try { 55 | val actName = 56 | context.packageManager.getPackageInfo(sysPkgName, PackageManager.GET_ACTIVITIES) 57 | .let { 58 | it.activities[0].name 59 | } 60 | Intent().apply { 61 | val apkUri = 62 | FileProvider.getUriForFile(context, Constants.PROVIDER_NAME, File(filePath)) 63 | component = ComponentName(sysPkgName, actName) 64 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 65 | setDataAndType(apkUri, Constants.URI_DATA_TYPE) 66 | context.startActivity(this) 67 | } 68 | } catch (e: PackageManager.NameNotFoundException) { 69 | toast(context.getString(R.string.open_sys_pkg_failure)) 70 | e.printStackTrace() 71 | } 72 | } 73 | 74 | /** 75 | * 获取准确的Intent Referrer 76 | */ 77 | @SuppressLint("PrivateApi") 78 | fun reflectGetReferrer(context: Context?): String? { 79 | return try { 80 | val activityClass = Class.forName("android.app.Activity") 81 | val refererField = activityClass.getDeclaredField("mReferrer") 82 | refererField.isAccessible = true 83 | refererField[context] as String 84 | } catch (e: Exception) { 85 | e.printStackTrace() 86 | null 87 | } 88 | } 89 | 90 | fun isMiuiOS(): Boolean { 91 | val miuiVersionCode = "ro.miui.ui.version.code" 92 | val miuiVersionName = "ro.miui.ui.version.name" 93 | val miuiInternalStorage = "ro.miui.internal.storage" 94 | val prop = Properties() 95 | try { 96 | prop.load(FileInputStream(File(Environment.getRootDirectory(), "build.prop"))) 97 | } catch (e: IOException) { 98 | e.printStackTrace() 99 | return false 100 | } 101 | return prop.getProperty(miuiVersionCode, null) != null || prop.getProperty( 102 | miuiVersionName, 103 | null 104 | ) != null || prop.getProperty(miuiInternalStorage, null) != null 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/utils/NotificationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.utils 2 | 3 | import android.app.* 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.pm.PackageManager 7 | import android.graphics.Bitmap 8 | import android.os.Build 9 | import androidx.core.app.ActivityCompat 10 | import androidx.core.app.NotificationManagerCompat 11 | import com.tokyonth.installer.R 12 | 13 | import androidx.core.app.NotificationCompat 14 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 15 | import com.tokyonth.installer.utils.ktx.string 16 | 17 | object NotificationUtils { 18 | 19 | private const val POST_NOTIFICATIONS = "android.permission.POST_NOTIFICATIONS" 20 | 21 | fun checkNotification(activity: Activity): Boolean { 22 | val b = checkNotification33(activity) 23 | if (!b) { 24 | MaterialAlertDialogBuilder(activity) 25 | .setMessage(string(R.string.notification_perm)) 26 | .setNegativeButton(string(R.string.dialog_btn_cancel), null) 27 | .setPositiveButton( 28 | string(R.string.dialog_btn_ok) 29 | ) { _, _ -> startNotificationPermission(activity) } 30 | .setCancelable(false) 31 | .show() 32 | } 33 | return b 34 | } 35 | 36 | private fun checkNotification33(activity: Activity): Boolean { 37 | if (Build.VERSION.SDK_INT >= 33) { 38 | if (ActivityCompat.checkSelfPermission( 39 | activity, 40 | POST_NOTIFICATIONS 41 | ) == PackageManager.PERMISSION_DENIED 42 | ) { 43 | return if (!ActivityCompat.shouldShowRequestPermissionRationale( 44 | activity, 45 | POST_NOTIFICATIONS 46 | ) 47 | ) { 48 | false 49 | } else { 50 | ActivityCompat.requestPermissions(activity, arrayOf(POST_NOTIFICATIONS), 100) 51 | false 52 | } 53 | } else { 54 | return true 55 | } 56 | } else { 57 | return NotificationManagerCompat.from(activity).areNotificationsEnabled() 58 | } 59 | } 60 | 61 | private fun startNotificationPermission(context: Context) { 62 | val intent = Intent() 63 | val sdk = Build.VERSION.SDK_INT 64 | when { 65 | sdk >= 26 -> { 66 | intent.action = "android.settings.APP_NOTIFICATION_SETTINGS" 67 | intent.putExtra("android.provider.extra.APP_PACKAGE", context.packageName) 68 | } 69 | (sdk in 21..25) -> { 70 | intent.action = "android.settings.APP_NOTIFICATION_SETTINGS" 71 | intent.putExtra("app_package", context.packageName) 72 | intent.putExtra("app_uid", context.applicationInfo.uid) 73 | } 74 | } 75 | context.startActivity(intent) 76 | } 77 | 78 | fun sendNotification(context: Context, status: String, appName: String, appIcon: Bitmap) { 79 | val notificationManager = 80 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 81 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 82 | val channel = NotificationChannel( 83 | "status", 84 | "apkInstallStatus", 85 | NotificationManager.IMPORTANCE_DEFAULT 86 | ) 87 | channel.enableLights(true) 88 | channel.setShowBadge(true) 89 | notificationManager.createNotificationChannel(channel) 90 | } 91 | val builder = NotificationCompat.Builder(context, "status") 92 | .setContentTitle(status) 93 | .setWhen(System.currentTimeMillis()) 94 | .setSmallIcon(R.drawable.ic_launcher_round) 95 | .setPriority(NotificationCompat.PRIORITY_HIGH) 96 | .setAutoCancel(true) 97 | .setContentText(appName) 98 | .setLargeIcon(appIcon) 99 | notificationManager.notify(System.currentTimeMillis().toInt(), builder.build()) 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/utils/PackageUtils.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.utils 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.Drawable 5 | 6 | import com.tokyonth.installer.Constants 7 | 8 | object PackageUtils { 9 | 10 | fun isShizukuClientAvailable(context: Context): Boolean { 11 | return isAppClientAvailable(context, Constants.SHIZUKU_PKG_NAME) 12 | } 13 | 14 | fun isIceBoxClientAvailable(context: Context): Boolean { 15 | return isAppClientAvailable(context, Constants.ICEBOX_PKG_NAME) 16 | } 17 | 18 | fun isAppClientAvailable(context: Context, pkgName: String): Boolean { 19 | context.packageManager.let { 20 | for (name in it.getInstalledPackages(0)) { 21 | if (name.packageName == pkgName) { 22 | return true 23 | } 24 | } 25 | } 26 | return false 27 | } 28 | 29 | fun getAppNameByPackageName(context: Context, packageName: String?): String { 30 | return context.packageManager.let { 31 | it.getApplicationLabel(it.getApplicationInfo(packageName!!, 0)).toString() 32 | } 33 | } 34 | 35 | fun getAppIconByPackageName(context: Context, packageName: String?): Drawable { 36 | return context.packageManager.let { 37 | it.getApplicationIcon(it.getApplicationInfo(packageName!!, 0)) 38 | } 39 | } 40 | 41 | fun getVersionNameByPackageName(context: Context, packageName: String?): String { 42 | return context.packageManager.let { 43 | val packageInfo = it.getPackageInfo(packageName!!, 0) 44 | packageInfo.versionName 45 | } 46 | } 47 | 48 | fun getVersionName(context: Context): String { 49 | return context.packageManager.let { 50 | val packageInfo = it.getPackageInfo(context.packageName, 0) 51 | packageInfo.versionName 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/utils/PermissionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.utils 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.content.Intent 6 | import android.content.pm.PackageManager 7 | import android.net.Uri 8 | import android.os.Build 9 | import android.os.Environment 10 | import android.provider.DocumentsContract 11 | import android.provider.Settings 12 | import androidx.activity.result.ActivityResultLauncher 13 | import androidx.activity.result.contract.ActivityResultContracts 14 | import androidx.annotation.RequiresApi 15 | import androidx.appcompat.app.AppCompatActivity 16 | import androidx.core.content.ContextCompat 17 | import androidx.documentfile.provider.DocumentFile 18 | import com.tokyonth.installer.utils.path.DATA_TREE_URL 19 | import com.tokyonth.installer.utils.path.isGrantDataDir 20 | 21 | class PermissionHelper( 22 | private val activity: AppCompatActivity 23 | ) { 24 | 25 | private val permissionArray = arrayOf( 26 | Manifest.permission.READ_EXTERNAL_STORAGE, 27 | Manifest.permission.WRITE_EXTERNAL_STORAGE 28 | ) 29 | 30 | private var isGrant: ((Boolean, Int) -> Unit)? = null 31 | 32 | private var requestPermissionLauncher: ActivityResultLauncher>? = null 33 | 34 | private var requestDataLauncher: ActivityResultLauncher? = null 35 | 36 | private var requestResultLauncher: ActivityResultLauncher? = null 37 | 38 | private var requestCode = -1 39 | 40 | init { 41 | val c1 = ActivityResultContracts.StartActivityForResult() 42 | requestResultLauncher = activity.registerForActivityResult(c1) { 43 | isGrant?.invoke(true, requestCode) 44 | } 45 | val c2 = ActivityResultContracts.RequestMultiplePermissions() 46 | requestPermissionLauncher = activity.registerForActivityResult(c2) { map -> 47 | val all = map.filter { !it.value } 48 | isGrant?.invoke(all.isEmpty(), requestCode) 49 | } 50 | val c3 = ActivityResultContracts.StartActivityForResult() 51 | @SuppressLint("WrongConstant") 52 | requestDataLauncher = activity.registerForActivityResult(c3) { 53 | it.data?.let { intent -> 54 | activity.contentResolver.takePersistableUriPermission( 55 | intent.data!!, 56 | intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) 57 | ) 58 | isGrant?.invoke(true, requestCode) 59 | } 60 | } 61 | } 62 | 63 | fun check(): Boolean { 64 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 65 | Environment.isExternalStorageManager() 66 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 67 | ContextCompat.checkSelfPermission( 68 | activity, 69 | permissionArray[0] 70 | ) == PackageManager.PERMISSION_GRANTED 71 | } else { 72 | true 73 | } 74 | } 75 | 76 | @RequiresApi(Build.VERSION_CODES.R) 77 | private fun requestR() { 78 | if (Environment.isExternalStorageManager()) { 79 | isGrant?.invoke(true, requestCode) 80 | } else { 81 | val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply { 82 | data = Uri.parse("package:${activity.packageName}") 83 | } 84 | requestResultLauncher?.launch(intent) 85 | } 86 | } 87 | 88 | private fun requestM(array: Array) { 89 | requestPermissionLauncher?.launch(array) 90 | } 91 | 92 | private fun requestDataDir(pkg: String) { 93 | var uriString = DATA_TREE_URL 94 | if (pkg.isNotEmpty()) { 95 | uriString = uriString.plus("%2F$pkg") 96 | } 97 | val uri = Uri.parse(uriString) 98 | val documentFile = DocumentFile.fromTreeUri(activity, uri) 99 | val intent1 = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) 100 | intent1.flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION 101 | or Intent.FLAG_GRANT_WRITE_URI_PERMISSION 102 | or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION 103 | or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) 104 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 105 | assert(documentFile != null) 106 | intent1.putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentFile!!.uri) 107 | } 108 | requestDataLauncher?.launch(intent1) 109 | } 110 | 111 | fun start() { 112 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 113 | requestR() 114 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 115 | requestM(permissionArray) 116 | } else { 117 | isGrant?.invoke(true, requestCode) 118 | } 119 | } 120 | 121 | fun startData(pkg: String) { 122 | if (!activity.isGrantDataDir(pkg)) { 123 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 124 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 125 | requestDataDir(pkg) 126 | } else { 127 | requestDataDir("") 128 | } 129 | } 130 | } else { 131 | isGrant?.invoke(true, requestCode) 132 | } 133 | } 134 | 135 | fun start(array: Array, requestCode: Int = -1) { 136 | this.requestCode = requestCode 137 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 138 | requestM(array) 139 | } else { 140 | isGrant?.invoke(true, requestCode) 141 | } 142 | } 143 | 144 | fun registerCallback(isGrant: ((Boolean, Int) -> Unit)? = null) { 145 | this.isGrant = isGrant 146 | } 147 | 148 | fun dispose() { 149 | requestPermissionLauncher?.unregister() 150 | requestDataLauncher?.unregister() 151 | requestResultLauncher?.unregister() 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/utils/SPUtils.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.utils 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import androidx.core.content.edit 7 | import androidx.fragment.app.Fragment 8 | import com.tokyonth.installer.App 9 | import com.tokyonth.installer.Constants 10 | 11 | object SPUtils { 12 | 13 | private const val USER = Constants.SP_FILE_NAME 14 | 15 | /** 16 | * 添加 17 | */ 18 | fun Any.putSP(key: String, value: T, commit: Boolean = true) { 19 | putSP(getSharedPreferences(this), key, value, commit = commit) 20 | } 21 | 22 | /** 23 | * 获取 24 | */ 25 | fun Any.getSP(key: String, defValue: T): T { 26 | return getSP(getSharedPreferences(this), key, defValue) 27 | } 28 | 29 | /** 30 | * 删除 31 | */ 32 | fun Any.delSP(key: String) { 33 | getSharedPreferences(this).edit(commit = true) { 34 | remove(key) 35 | } 36 | } 37 | 38 | private fun getSharedPreferences(any: Any): SharedPreferences { 39 | return when (any) { 40 | is Activity -> { 41 | any.getSharedPreferences(USER, Context.MODE_PRIVATE) 42 | } 43 | is Fragment -> { 44 | any.requireActivity().getSharedPreferences(USER, Context.MODE_PRIVATE) 45 | } 46 | is Context -> { 47 | any.applicationContext.getSharedPreferences(USER, Context.MODE_PRIVATE) 48 | } 49 | else -> { 50 | App.context.getSharedPreferences(USER, Context.MODE_PRIVATE) 51 | } 52 | } 53 | } 54 | 55 | private fun putSP( 56 | mShareConfig: SharedPreferences, 57 | key: String, 58 | value: T, 59 | commit: Boolean = true 60 | ) { 61 | mShareConfig.edit(commit = commit) { 62 | when (value) { 63 | is String -> putString(key, value) 64 | is Long -> putLong(key, value) 65 | is Boolean -> putBoolean(key, value) 66 | is Int -> putInt(key, value) 67 | is Float -> putFloat(key, value) 68 | } 69 | } 70 | } 71 | 72 | @Suppress("UNCHECKED_CAST") 73 | private fun getSP(mShareConfig: SharedPreferences, key: String, defValue: T): T { 74 | val value = when (defValue) { 75 | is String -> mShareConfig.getString(key, defValue) 76 | is Long -> 77 | java.lang.Long.valueOf(mShareConfig.getLong(key, defValue)) 78 | is Boolean -> 79 | java.lang.Boolean.valueOf( 80 | mShareConfig.getBoolean( 81 | key, 82 | defValue 83 | ) 84 | ) 85 | is Int -> Integer.valueOf(mShareConfig.getInt(key, defValue)) 86 | is Float -> 87 | java.lang.Float.valueOf(mShareConfig.getFloat(key, defValue)) 88 | else -> { 89 | mShareConfig.getString(key, defValue.toString()) 90 | } 91 | } as T 92 | return value 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/utils/ShellUtils.java: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.utils; 2 | // Copyright (C) 2018 Bave Lee 3 | // This file is part of Quick-Android. 4 | // https://github.com/Crixec/Quick-Android 5 | // 6 | // Quick-Android is free software: you can redistribute it and/or modify 7 | // it under the terms of the GNU General Public License as published by 8 | // the Free Software Foundation, either version 3 of the License, or 9 | // (at your option) any later version. 10 | // 11 | // Quick-Android is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU General Public License 17 | 18 | import java.io.BufferedReader; 19 | import java.io.Closeable; 20 | import java.io.DataOutputStream; 21 | import java.io.IOException; 22 | import java.io.InputStreamReader; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | public class ShellUtils { 27 | 28 | private static int exec(final String sh, final List cmds, final Result result) { 29 | Process process; 30 | DataOutputStream stdin = null; 31 | OutputReader stdout = null; 32 | OutputReader stderr = null; 33 | int resultCode = -1; 34 | try { 35 | process = Runtime.getRuntime().exec(sh); 36 | stdin = new DataOutputStream(process.getOutputStream()); 37 | if (result != null) { 38 | stdout = new OutputReader(new BufferedReader(new InputStreamReader(process.getInputStream())), 39 | result::onStdout); 40 | stderr = new OutputReader(new BufferedReader(new InputStreamReader(process.getErrorStream())), 41 | result::onStderr); 42 | stdout.start(); 43 | stderr.start(); 44 | } 45 | for (String cmd : cmds) { 46 | if (result != null) { 47 | result.onCommand(cmd); 48 | } 49 | stdin.write(cmd.getBytes()); 50 | stdin.writeBytes("\n"); 51 | stdin.flush(); 52 | } 53 | stdin.writeBytes("exit $?\n"); 54 | stdin.flush(); 55 | resultCode = process.waitFor(); 56 | if (result != null) { 57 | result.onFinish(resultCode); 58 | } 59 | } catch (Exception e) { 60 | e.printStackTrace(); 61 | } finally { 62 | safeCancel(stderr); 63 | safeCancel(stdout); 64 | safeClose(stdout); 65 | safeClose(stderr); 66 | safeClose(stdin); 67 | } 68 | return resultCode; 69 | } 70 | 71 | private static void safeCancel(OutputReader reader) { 72 | try { 73 | if (reader != null) reader.cancel(); 74 | } catch (Exception e) { 75 | e.printStackTrace(); 76 | } 77 | } 78 | 79 | private static void safeClose(Closeable closeable) { 80 | try { 81 | if (closeable != null) closeable.close(); 82 | } catch (Exception e) { 83 | e.printStackTrace(); 84 | } 85 | } 86 | 87 | public static int exec(final List cmd_list, final Result result, final boolean isRoot) { 88 | String sh = isRoot ? "su" : "sh"; 89 | return exec(sh, cmd_list, result); 90 | } 91 | 92 | public static int exec(final List cmd_list, final boolean isRoot) { 93 | return exec(cmd_list, null, isRoot); 94 | } 95 | 96 | public static int exec(final String cmd, boolean isRoot) { 97 | return exec(cmd, null, isRoot); 98 | } 99 | 100 | public static int exec(final String cmd, final Result result, boolean isRoot) { 101 | List cmd_list = new ArrayList(); 102 | cmd_list.add(cmd); 103 | return exec(cmd_list, result, isRoot); 104 | } 105 | 106 | public static int exec(final String cmd) { 107 | return exec(cmd, null, false); 108 | } 109 | 110 | public static int execWithRoot(final String cmd) { 111 | return exec(cmd, null, true); 112 | } 113 | 114 | public static int execWithRoot(final String cmd, final Result result) { 115 | return exec(cmd, result, true); 116 | } 117 | 118 | public interface Result { 119 | void onStdout(String text); 120 | 121 | void onStderr(String text); 122 | 123 | void onCommand(String command); 124 | 125 | void onFinish(int resultCode); 126 | } 127 | 128 | private interface Output { 129 | void output(String text); 130 | } 131 | 132 | public static class OutputReader extends Thread implements Closeable { 133 | 134 | private final Output output; 135 | private final BufferedReader reader; 136 | private boolean isRunning; 137 | 138 | private OutputReader(BufferedReader reader, Output output) { 139 | this.output = output; 140 | this.reader = reader; 141 | this.isRunning = true; 142 | } 143 | 144 | @Override 145 | public void close() { 146 | try { 147 | reader.close(); 148 | } catch (IOException e) { 149 | e.printStackTrace(); 150 | } 151 | } 152 | 153 | @Override 154 | public void run() { 155 | super.run(); 156 | String line; 157 | while (isRunning) { 158 | try { 159 | line = reader.readLine(); 160 | if (line != null) 161 | output.output(line); 162 | } catch (IOException e) { 163 | e.printStackTrace(); 164 | } 165 | } 166 | } 167 | 168 | private void cancel() { 169 | synchronized (this) { 170 | isRunning = false; 171 | this.notifyAll(); 172 | } 173 | } 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/utils/ktx/CommonUtils.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.utils.ktx 2 | 3 | import android.content.res.Resources 4 | import android.graphics.Bitmap 5 | import android.graphics.Canvas 6 | import android.graphics.PixelFormat 7 | import android.graphics.drawable.BitmapDrawable 8 | import android.graphics.drawable.Drawable 9 | import android.util.TypedValue 10 | import android.view.View 11 | import android.widget.Toast 12 | import androidx.annotation.ColorRes 13 | import androidx.annotation.StringRes 14 | import androidx.core.content.ContextCompat 15 | import com.tokyonth.installer.App 16 | 17 | object CommonUtils { 18 | 19 | fun getScreenHeight(): Int { 20 | val metrics = App.context.resources.displayMetrics 21 | return metrics.heightPixels 22 | } 23 | 24 | fun getScreenWidth(): Int { 25 | val metrics = App.context.resources.displayMetrics 26 | return metrics.widthPixels 27 | } 28 | 29 | } 30 | 31 | inline fun V.click(interval: Long = 600L, noinline callback: V.() -> Unit) { 32 | 33 | var lastClickTime: Long = 0 34 | 35 | this.setOnClickListener { 36 | val currentTime = System.currentTimeMillis() 37 | if (currentTime - lastClickTime > interval) { 38 | callback.invoke(this) 39 | lastClickTime = currentTime 40 | } 41 | } 42 | } 43 | 44 | inline fun T.visibleOrGone(boolean: Boolean, onVisible: (T.() -> Unit) = {}) { 45 | visibility = if (boolean) { 46 | View.VISIBLE 47 | } else { 48 | View.GONE 49 | } 50 | if (boolean) { 51 | onVisible.invoke(this) 52 | } 53 | } 54 | 55 | fun toast(text: String) { 56 | Toast.makeText(App.context, text, Toast.LENGTH_SHORT).show() 57 | } 58 | 59 | fun color(@ColorRes colorId: Int): Int { 60 | return ContextCompat.getColor(App.context, colorId) 61 | } 62 | 63 | fun string(@StringRes stringId: Int, vararg args: Any?): String { 64 | return App.context.getString(stringId, *args) 65 | } 66 | 67 | fun Float.sp2px(): Float { 68 | return TypedValue.applyDimension( 69 | TypedValue.COMPLEX_UNIT_SP, this, 70 | Resources.getSystem().displayMetrics 71 | ) 72 | } 73 | 74 | fun Float.dp2px(): Float { 75 | return TypedValue.applyDimension( 76 | TypedValue.COMPLEX_UNIT_DIP, this, 77 | Resources.getSystem().displayMetrics 78 | ) 79 | } 80 | 81 | fun Int.sp2px(): Float { 82 | return this.toFloat().sp2px() 83 | } 84 | 85 | fun Int.dp2px(): Float { 86 | return this.toFloat().dp2px() 87 | } 88 | 89 | fun Drawable.drawable2Bitmap(): Bitmap { 90 | return when (this) { 91 | is BitmapDrawable -> { 92 | this.bitmap 93 | } 94 | else -> { 95 | val config = 96 | if (this.opacity != PixelFormat.OPAQUE) { 97 | Bitmap.Config.ARGB_8888 98 | } else { 99 | Bitmap.Config.RGB_565 100 | } 101 | val bitmap = Bitmap.createBitmap( 102 | this.intrinsicWidth, 103 | this.intrinsicHeight, 104 | config 105 | ) 106 | val canvas = Canvas(bitmap) 107 | this.setBounds(0, 0, canvas.width, canvas.height) 108 | this.draw(canvas) 109 | bitmap 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/utils/ktx/FileExt.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.utils.ktx 2 | 3 | import java.io.File 4 | import java.text.DecimalFormat 5 | 6 | fun File.deleteFolderFile(deletePath: Boolean) { 7 | try { 8 | if (this.isDirectory) { 9 | val files = this.listFiles()!! 10 | for (value in files) { 11 | value.deleteFolderFile(true) 12 | } 13 | } 14 | if (deletePath) { 15 | if (!this.isDirectory) { 16 | this.delete() 17 | } 18 | } 19 | } catch (e: Exception) { 20 | e.printStackTrace() 21 | } 22 | } 23 | 24 | fun File.fileOrFolderSize(): Long { 25 | try { 26 | if (!this.exists()) return 0 27 | return if (!this.isDirectory) { 28 | this.length() 29 | } else { 30 | var total: Long = 0 31 | val files = this.listFiles() 32 | if (files == null || files.isEmpty()) { 33 | return 0 34 | } 35 | for (f in files) { 36 | total += f.fileOrFolderSize() 37 | } 38 | total 39 | } 40 | } catch (e: Exception) { 41 | e.printStackTrace() 42 | } 43 | return 0 44 | } 45 | 46 | fun Long.toMemorySize(): String { 47 | val g = 1024 * 1024 * 1024 48 | val m = 1024 * 1024 49 | val k = 1024 50 | val decimalFormat = DecimalFormat("0.00") 51 | return when { 52 | (this / g >= 1) -> { 53 | decimalFormat.format(this / g) + "GB" 54 | } 55 | (this / m >= 1) -> { 56 | decimalFormat.format(this / m) + "MB" 57 | } 58 | (this / k >= 1) -> { 59 | decimalFormat.format(this / k) + "KB" 60 | } 61 | else -> this.toString() + "B" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/utils/ktx/TaskJobKtx.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.utils.ktx 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import java.util.concurrent.Executor 6 | import java.util.concurrent.Executors 7 | 8 | object TaskJobKtx { 9 | 10 | /** 11 | * 异步线程池 12 | */ 13 | private val threadPool: Executor by lazy { 14 | Executors.newScheduledThreadPool(5) 15 | } 16 | 17 | /** 18 | * 主线程的handler 19 | */ 20 | private val mainThread: Handler by lazy { 21 | Handler(Looper.getMainLooper()) 22 | } 23 | 24 | /** 25 | * 异步任务 26 | */ 27 | fun doAsync(task: Task) { 28 | threadPool.execute(task.runnable) 29 | } 30 | 31 | /** 32 | * 主线程 33 | */ 34 | fun onUI(task: Task) { 35 | mainThread.post(task.runnable) 36 | } 37 | 38 | /** 39 | * 延迟任务 40 | */ 41 | fun delay(delay: Long, task: Task) { 42 | mainThread.postDelayed(task.runnable, delay) 43 | } 44 | 45 | /** 46 | * 移除任务 47 | */ 48 | fun remove(task: Task) { 49 | mainThread.removeCallbacks(task.runnable) 50 | } 51 | 52 | /** 53 | * 包装的任务类 54 | * 包装的意义在于复用和移除任务 55 | * 由于Handler任务可能造成内存泄漏,因此在生命周期结束时,有必要移除任务 56 | * 由于主线程的Handler使用了全局的对象,移除不必要的任务显得更为重要 57 | * 因此包装了任务类,以任务类为对象来保留任务和移除任务 58 | */ 59 | class Task( 60 | private val target: T, 61 | private val err: ((Throwable) -> Unit) = {}, 62 | private val run: T.() -> Unit 63 | ) { 64 | 65 | val runnable = Runnable { 66 | try { 67 | run(target) 68 | } catch (e: Throwable) { 69 | err(e) 70 | } 71 | } 72 | 73 | fun cancel() { 74 | remove(this) 75 | } 76 | 77 | fun run() { 78 | doAsync(this) 79 | } 80 | 81 | fun sync() { 82 | onUI(this) 83 | } 84 | 85 | fun delay(time: Long) { 86 | delay(time, this) 87 | } 88 | } 89 | 90 | } 91 | 92 | /** 93 | * 用于创建一个任务对象 94 | */ 95 | inline fun T.task( 96 | noinline err: ((Throwable) -> Unit) = {}, 97 | noinline run: T.() -> Unit 98 | ) = TaskJobKtx.Task(this, err, run) 99 | 100 | /** 101 | * 异步任务 102 | */ 103 | inline fun T.doAsync( 104 | noinline err: ((Throwable) -> Unit) = {}, 105 | noinline run: T.() -> Unit 106 | ): TaskJobKtx.Task { 107 | val task = task(err, run) 108 | TaskJobKtx.doAsync(task) 109 | return task 110 | } 111 | 112 | /** 113 | * 主线程 114 | */ 115 | inline fun T.onUI( 116 | noinline err: ((Throwable) -> Unit) = {}, 117 | noinline run: T.() -> Unit 118 | ): TaskJobKtx.Task { 119 | val task = task(err, run) 120 | TaskJobKtx.onUI(task) 121 | return task 122 | } 123 | 124 | /** 125 | * 延迟任务 126 | */ 127 | inline fun T.delay( 128 | delay: Long, 129 | noinline err: ((Throwable) -> Unit) = {}, 130 | noinline run: T.() -> Unit 131 | ): TaskJobKtx.Task { 132 | val task = task(err, run) 133 | TaskJobKtx.delay(delay, task) 134 | return task 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/utils/ktx/ViewBindingKtx.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.utils.ktx 2 | 3 | import android.app.Activity 4 | import android.view.InflateException 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import androidx.viewbinding.ViewBinding 10 | 11 | inline fun Activity.lazyBind(): Lazy = lazy { bind() } 12 | 13 | inline fun Fragment.lazyBind(): Lazy = lazy { bind() } 14 | 15 | inline fun View.lazyBind(): Lazy = lazy { bind() } 16 | 17 | inline fun Activity.bind(): T { 18 | return this.layoutInflater.bind() 19 | } 20 | 21 | inline fun Fragment.bind(): T { 22 | return this.layoutInflater.bind() 23 | } 24 | 25 | inline fun View.bind(): T { 26 | return LayoutInflater.from(this.context).bind() 27 | } 28 | 29 | inline fun LayoutInflater.bind(): T { 30 | val layoutInflater: LayoutInflater = this 31 | val bindingClass = T::class.java 32 | val inflateMethod = bindingClass.getMethod("inflate", LayoutInflater::class.java) 33 | val invokeObj = inflateMethod.invoke(null, layoutInflater) 34 | if (invokeObj is T) { 35 | return invokeObj 36 | } 37 | throw InflateException("Cant inflate ViewBinding ${bindingClass.name}") 38 | } 39 | 40 | inline fun View.withThis(inflate: Boolean = false): Lazy = lazy { 41 | val bindingClass = T::class.java 42 | val view: View = this 43 | if (view is ViewGroup && inflate) { 44 | val bindMethod = bindingClass.getMethod( 45 | "inflate", 46 | LayoutInflater::class.java, 47 | ViewGroup::class.java, 48 | Boolean::class.javaPrimitiveType 49 | ) 50 | val bindObj = bindMethod.invoke(null, LayoutInflater.from(context), view, true) 51 | if (bindObj is T) { 52 | return@lazy bindObj 53 | } 54 | } else { 55 | val bindMethod = bindingClass.getMethod( 56 | "bind", 57 | View::class.java 58 | ) 59 | val bindObj = bindMethod.invoke(null, view) 60 | if (bindObj is T) { 61 | return@lazy bindObj 62 | } 63 | } 64 | throw InflateException("Cant inflate ViewBinding ${bindingClass.name}") 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/utils/path/FileGlobal.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.utils.path 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.util.Log 9 | import com.tokyonth.installer.App 10 | import java.io.File 11 | import java.io.FileOutputStream 12 | 13 | internal const val AUTHORITY = ".ApkFileProvider" 14 | 15 | const val DATA_TREE_URL = 16 | "content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata" 17 | 18 | fun Context.isGrantDataDir(pkg: String): Boolean { 19 | var uriString = DATA_TREE_URL 20 | if (pkg.isNotEmpty()) { 21 | uriString = uriString.plus("%2F$pkg") 22 | } 23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 24 | for (persistedUriPermission in this.contentResolver.persistedUriPermissions) { 25 | if (persistedUriPermission.isReadPermission 26 | && persistedUriPermission.uri.toString() 27 | == uriString 28 | ) { 29 | return true 30 | } 31 | } 32 | return false 33 | } else { 34 | return true 35 | } 36 | } 37 | 38 | fun Uri.copyFromStreamUri(fileName: String): File { 39 | val targetFile = File(App.context.externalCacheDir, fileName) 40 | if (targetFile.exists()) { 41 | targetFile.delete() 42 | } 43 | var read: Int 44 | val buffer = ByteArray(8 * 1024) 45 | val inputStream = App.context.contentResolver.openInputStream(this) 46 | inputStream?.use { input -> 47 | FileOutputStream(targetFile).use { out -> 48 | while (input.read(buffer).also { read = it } != -1) { 49 | out.write(buffer, 0, read) 50 | } 51 | out.flush() 52 | } 53 | } 54 | return targetFile 55 | } 56 | 57 | //Permission 58 | //---------------------------------------------------------------- 59 | 60 | /** 61 | * @return 传入的Uri是否已具备访问权限 (Whether the incoming Uri has access permission) 62 | */ 63 | fun giveUriPermission(uri: Uri?): Boolean { 64 | return uri?.run { 65 | when (App.context.checkUriPermission( 66 | this, 67 | android.os.Process.myPid(), 68 | android.os.Process.myUid(), 69 | Intent.FLAG_GRANT_READ_URI_PERMISSION 70 | )) { 71 | PackageManager.PERMISSION_GRANTED -> true 72 | PackageManager.PERMISSION_DENIED -> { 73 | App.context.grantUriPermission( 74 | App.context.applicationContext.packageName, 75 | this, 76 | Intent.FLAG_GRANT_READ_URI_PERMISSION 77 | ) 78 | false 79 | } 80 | else -> false 81 | } 82 | } ?: false 83 | } 84 | 85 | fun revokeUriPermission(uri: Uri?) { 86 | App.context.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) 87 | } 88 | 89 | inline fun Uri.use(block: Uri.() -> R): R { 90 | var isAlreadyHavePermission = false 91 | try { 92 | isAlreadyHavePermission = giveUriPermission(this) 93 | return block() 94 | } catch (t: Throwable) { 95 | Log.e("giveUri:", "${t.message}") 96 | } finally { 97 | if (!isAlreadyHavePermission) { 98 | try { 99 | revokeUriPermission(this) 100 | } catch (t: Throwable) { 101 | Log.e("revokeUri", "${t.message}") 102 | } 103 | } 104 | } 105 | return block() 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/utils/path/FileProvider.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.utils.path 2 | 3 | import androidx.core.content.FileProvider 4 | 5 | open class FileProvider : FileProvider() 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/view/BaseListLayout.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.widget.FrameLayout 7 | import androidx.recyclerview.widget.LinearLayoutManager 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 10 | import com.tokyonth.installer.R 11 | import com.tokyonth.installer.adapter.ActivityAdapter 12 | import com.tokyonth.installer.adapter.PermissionAdapter 13 | import com.tokyonth.installer.data.PermissionInfoEntity 14 | import com.tokyonth.installer.databinding.LayoutBaseListBinding 15 | import com.tokyonth.installer.utils.ktx.click 16 | import com.tokyonth.installer.utils.ktx.lazyBind 17 | import com.tokyonth.installer.utils.ktx.string 18 | import com.tokyonth.installer.utils.ktx.visibleOrGone 19 | 20 | class BaseListLayout : FrameLayout { 21 | 22 | constructor(context: Context) : this(context, null) 23 | 24 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 25 | 26 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 27 | context, 28 | attrs, 29 | defStyleAttr 30 | ) { 31 | initView() 32 | } 33 | 34 | private val binding: LayoutBaseListBinding by lazyBind() 35 | 36 | private fun initView() { 37 | binding.rvBaseList.layoutManager = LinearLayoutManager(context) 38 | binding.root.click { 39 | changeView() 40 | } 41 | addView(binding.root) 42 | } 43 | 44 | fun setTitle(title: String) { 45 | binding.tvListTitle.text = title 46 | } 47 | 48 | fun setScrollView(view: View) { 49 | binding.rvBaseList.addOnScrollListener(object : RecyclerView.OnScrollListener() { 50 | override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { 51 | super.onScrollStateChanged(recyclerView, newState) 52 | when (newState) { 53 | RecyclerView.SCROLL_STATE_IDLE -> view.alpha = 1f 54 | RecyclerView.SCROLL_STATE_DRAGGING -> view.alpha = 0.2f 55 | } 56 | } 57 | }) 58 | } 59 | 60 | @Suppress("UNCHECKED_CAST") 61 | fun setData(list: MutableList) { 62 | when (list[0]) { 63 | is String -> { 64 | binding.rvBaseList.adapter = ActivityAdapter(list as MutableList) 65 | } 66 | is PermissionInfoEntity -> { 67 | binding.rvBaseList.adapter = 68 | PermissionAdapter(list as MutableList).apply { 69 | setItemClickListener { 70 | showPermissionDes(it) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | private fun showPermissionDes(permissionInfo: PermissionInfoEntity) { 78 | val lab = permissionInfo.permissionLabel.ifEmpty { 79 | string(R.string.text_no_description) 80 | } 81 | val des = permissionInfo.permissionDesc.ifEmpty { 82 | string(R.string.text_no_description) 83 | } 84 | 85 | MaterialAlertDialogBuilder(context) 86 | .setMessage(permissionInfo.permissionName + "\n\n" + lab + "\n\n" + des) 87 | .setPositiveButton(R.string.dialog_btn_ok, null) 88 | .create() 89 | .show() 90 | } 91 | 92 | private fun changeView() { 93 | binding.rvBaseList.visibility.let { vis -> 94 | (vis == View.GONE).let { 95 | binding.ivListArrow.animate().rotation(if (it) 90F else 0F).start() 96 | binding.rvBaseList.visibleOrGone(it) 97 | } 98 | } 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/view/DataLoadingView.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.FrameLayout 8 | import com.tokyonth.installer.databinding.LayoutErrorBinding 9 | 10 | import com.tokyonth.installer.databinding.LayoutLoadingBinding 11 | import com.tokyonth.installer.utils.ktx.click 12 | import com.tokyonth.installer.utils.ktx.lazyBind 13 | import com.tokyonth.installer.utils.ktx.visibleOrGone 14 | 15 | class DataLoadingView @JvmOverloads constructor( 16 | context: Context, 17 | attrs: AttributeSet? = null, 18 | defStyleAttr: Int = 0 19 | ) : FrameLayout(context, attrs, defStyleAttr) { 20 | 21 | companion object { 22 | private const val CONTENT = "type_content" 23 | private const val LOADING = "type_loading" 24 | private const val ERROR = "type_error" 25 | 26 | private const val TAG_LOADING = "TAG_LOADING" 27 | private const val TAG_ERROR = "TAG_ERROR" 28 | private const val TAG_CUSTOM = "TAG_CUSTOM" 29 | } 30 | 31 | private val lnlLoading: LayoutLoadingBinding by lazyBind() 32 | 33 | private val lnlError: LayoutErrorBinding by lazyBind() 34 | 35 | private val contentViews = mutableListOf() 36 | 37 | init { 38 | lnlLoading.root.tag = TAG_LOADING 39 | addView(lnlLoading.root) 40 | 41 | lnlError.root.tag = TAG_ERROR 42 | addView(lnlError.root) 43 | 44 | showContentView() 45 | } 46 | 47 | fun showLoading() { 48 | switchState(LOADING) 49 | } 50 | 51 | fun showContentView() { 52 | switchState(CONTENT) 53 | } 54 | 55 | fun showErrorView(msg: String) { 56 | lnlError.tvErrorMsg.text = msg 57 | switchState(ERROR) 58 | } 59 | 60 | private fun hideLoadingView() { 61 | lnlLoading.root.visibleOrGone(false) 62 | } 63 | 64 | private fun setLoadingView() { 65 | lnlLoading.root.visibleOrGone(true) 66 | } 67 | 68 | private fun hideErrorView() { 69 | lnlError.root.visibleOrGone(false) 70 | } 71 | 72 | private fun setErrorView() { 73 | lnlError.root.visibleOrGone(true) 74 | } 75 | 76 | private fun switchState(state: String) { 77 | when (state) { 78 | CONTENT -> { 79 | hideLoadingView() 80 | hideErrorView() 81 | setContentVisibility(true) 82 | } 83 | LOADING -> { 84 | hideErrorView() 85 | setLoadingView() 86 | setContentVisibility(false) 87 | } 88 | ERROR -> { 89 | hideLoadingView() 90 | setErrorView() 91 | setContentVisibility(false) 92 | } 93 | } 94 | } 95 | 96 | fun setErrorClickListener(clickListener: OnClickListener) { 97 | lnlError.root.click { 98 | clickListener.onClick(lnlError.root) 99 | } 100 | } 101 | 102 | override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { 103 | super.addView(child, index, params) 104 | if (child.tag == null || child.tag != TAG_LOADING && 105 | child.tag != TAG_ERROR && child.tag != TAG_CUSTOM 106 | ) { 107 | contentViews.add(child) 108 | } 109 | } 110 | 111 | private fun setContentVisibility(visible: Boolean) { 112 | for (v in contentViews) { 113 | v.visibility = if (visible) View.VISIBLE else View.GONE 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/view/InstallHeaderLayout.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.view 2 | 3 | import android.animation.Animator 4 | import android.animation.AnimatorListenerAdapter 5 | import android.annotation.SuppressLint 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.graphics.Bitmap 9 | import android.graphics.drawable.GradientDrawable 10 | import android.util.AttributeSet 11 | import android.view.ViewAnimationUtils 12 | import android.widget.FrameLayout 13 | import androidx.appcompat.app.AppCompatDelegate 14 | import com.google.android.material.resources.MaterialAttributes 15 | import com.tokyonth.installer.R 16 | import com.tokyonth.installer.activity.SettingsActivity 17 | import com.tokyonth.installer.data.ApkInfoEntity 18 | import com.tokyonth.installer.data.SPDataManager 19 | import com.tokyonth.installer.databinding.LayoutInstallHeaderBinding 20 | import com.tokyonth.installer.utils.ktx.* 21 | 22 | class InstallHeaderLayout : FrameLayout { 23 | 24 | constructor(context: Context) : this(context, null) 25 | 26 | constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 27 | 28 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( 29 | context, 30 | attrs, 31 | defStyleAttr 32 | ) { 33 | initView() 34 | } 35 | 36 | private val binding: LayoutInstallHeaderBinding by lazyBind() 37 | 38 | private fun initView() { 39 | changeNightModeStatus() 40 | binding.ibNightMode.click { 41 | changeNightMode() 42 | } 43 | binding.ibSettings.click { 44 | context.startActivity(Intent(context, SettingsActivity::class.java)) 45 | } 46 | addView(binding.root) 47 | } 48 | 49 | fun setTitle(title: String) { 50 | binding.tvAppName.text = title 51 | } 52 | 53 | @SuppressLint("RestrictedApi") 54 | fun setAppInfo(apkInfo: ApkInfoEntity) { 55 | binding.tvAppName.text = apkInfo.appName 56 | binding.tvAppVersion.text = apkInfo.version 57 | 58 | post { 59 | startIconAnimation(apkInfo.icon) 60 | } 61 | val mColor = 62 | MaterialAttributes.resolve(context, com.google.android.material.R.attr.colorSurface) 63 | val surfaceColor = color(mColor!!.resourceId) 64 | val gDrawable = GradientDrawable().apply { 65 | shape = GradientDrawable.RECTANGLE 66 | gradientType = GradientDrawable.LINEAR_GRADIENT 67 | colors = intArrayOf( 68 | surfaceColor and 0x4DFFFFFF, 69 | surfaceColor and 0x99FFFFFF.toInt(), 70 | surfaceColor and 0xE6FFFFFF.toInt(), 71 | surfaceColor 72 | ) 73 | } 74 | binding.viewLayer.background = gDrawable 75 | } 76 | 77 | private fun startIconAnimation(bitmap: Bitmap?) { 78 | binding.ivBigIcon.let { 79 | val width = it.measuredWidth 80 | val height = it.measuredHeight 81 | ViewAnimationUtils.createCircularReveal( 82 | it, 83 | width / 2, 84 | height / 2, 85 | 0f, 86 | height.toFloat() 87 | ).apply { 88 | addListener(object : AnimatorListenerAdapter() { 89 | override fun onAnimationStart(animation: Animator) { 90 | super.onAnimationStart(animation) 91 | it.setImageBitmap(bitmap) 92 | } 93 | }) 94 | duration = 500 95 | start() 96 | } 97 | } 98 | } 99 | 100 | override fun setEnabled(enabled: Boolean) { 101 | super.setEnabled(enabled) 102 | binding.ibNightMode.isEnabled = enabled 103 | binding.ibSettings.isEnabled = enabled 104 | } 105 | 106 | private fun changeNightModeStatus() { 107 | SPDataManager.instance.isNightMode().let { 108 | val res = if (it) { 109 | R.drawable.round_wb_sunny_24 110 | } else { 111 | R.drawable.round_bedtime_24 112 | } 113 | binding.ibNightMode.setImageResource(res) 114 | } 115 | } 116 | 117 | private fun changeNightMode() { 118 | SPDataManager.instance.isNightMode().let { 119 | if (it) { 120 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) 121 | } else { 122 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) 123 | } 124 | SPDataManager.instance.setNightMode(!it) 125 | } 126 | changeNightModeStatus() 127 | } 128 | 129 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 130 | val h = CommonUtils.getScreenHeight() * 0.3 131 | val height = MeasureSpec.makeMeasureSpec(h.toInt(), MeasureSpec.EXACTLY) 132 | super.onMeasure(widthMeasureSpec, height) 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/com/tokyonth/installer/view/ProgressDrawable.kt: -------------------------------------------------------------------------------- 1 | package com.tokyonth.installer.view 2 | 3 | import android.animation.Animator 4 | import android.animation.ValueAnimator 5 | import android.graphics.* 6 | import android.graphics.drawable.Animatable 7 | import android.graphics.drawable.Drawable 8 | import android.view.animation.LinearInterpolator 9 | import kotlin.math.min 10 | 11 | class ProgressDrawable : Drawable(), Animatable, ValueAnimator.AnimatorUpdateListener, 12 | Animator.AnimatorListener { 13 | 14 | private val paint = Paint().apply { 15 | isAntiAlias = true 16 | isDither = true 17 | style = Paint.Style.STROKE 18 | // strokeCap = Paint.Cap.ROUND 19 | } 20 | 21 | private val colorList = ArrayList() 22 | 23 | private var colorIndex = 0 24 | 25 | private var radius = 0F 26 | 27 | private var strokeWidthWeight = 0.25F 28 | 29 | private var startAngle = 0F 30 | 31 | private var sweepAngle = 0F 32 | 33 | private var ovalBounds = RectF() 34 | 35 | private var rotateStep = 360F 36 | 37 | private var sweepStep = 300F 38 | 39 | private var lastAnimationProgress = 0F 40 | 41 | private var lastStep = 0F 42 | 43 | private val animator = ValueAnimator.ofFloat(0F, 2F).apply { 44 | addUpdateListener(this@ProgressDrawable) 45 | addListener(this@ProgressDrawable) 46 | repeatCount = ValueAnimator.INFINITE 47 | repeatMode = ValueAnimator.RESTART 48 | interpolator = LinearInterpolator() 49 | } 50 | 51 | override fun draw(canvas: Canvas) { 52 | if (colorList.isEmpty() || radius < 1) { 53 | return 54 | } 55 | canvas.drawArc(ovalBounds, startAngle, sweepAngle, false, paint) 56 | } 57 | 58 | override fun onBoundsChange(bounds: Rect) { 59 | super.onBoundsChange(bounds) 60 | radius = min(bounds.width(), bounds.height()) * 0.5F 61 | val strokeWidth = radius * strokeWidthWeight 62 | radius -= strokeWidth * 0.5F 63 | paint.strokeWidth = strokeWidth 64 | val top = bounds.exactCenterY() - radius 65 | val left = bounds.exactCenterX() - radius 66 | val right = bounds.exactCenterX() + radius 67 | val bottom = bounds.exactCenterY() + radius 68 | ovalBounds.set(left, top, right, bottom) 69 | } 70 | 71 | override fun setAlpha(alpha: Int) { 72 | paint.alpha = alpha 73 | } 74 | 75 | override fun getOpacity() = PixelFormat.TRANSPARENT 76 | 77 | override fun setColorFilter(colorFilter: ColorFilter?) { 78 | paint.colorFilter = colorFilter 79 | } 80 | 81 | fun putColor(vararg colors: Int) { 82 | colorList.clear() 83 | for (color in colors) { 84 | colorList.add(color) 85 | } 86 | } 87 | 88 | fun progress(progress: Int, max: Int) { 89 | stop() 90 | paint.color = colorList[0] 91 | startAngle = 0F 92 | sweepAngle = 360F * progress / max 93 | invalidateSelf() 94 | } 95 | 96 | fun animatorDuration(value: Long) { 97 | animator.duration = value 98 | } 99 | 100 | override fun isRunning(): Boolean { 101 | return animator.isRunning 102 | } 103 | 104 | override fun start() { 105 | if (colorList.isEmpty()) { 106 | animator.cancel() 107 | return 108 | } 109 | animator.start() 110 | invalidateSelf() 111 | } 112 | 113 | override fun stop() { 114 | animator.cancel() 115 | } 116 | 117 | override fun onAnimationUpdate(animation: ValueAnimator) { 118 | if (animation == animator) { 119 | val value = animator.animatedValue as Float 120 | val isShrink = value > 1 121 | val sweepValue = if (value > 1) { 122 | 2 - value 123 | } else { 124 | value 125 | } 126 | val sweepLength = sweepStep * sweepValue 127 | val startValue = if (value < lastAnimationProgress) { 128 | 2F - lastAnimationProgress + value 129 | } else { 130 | value - lastAnimationProgress 131 | } 132 | lastAnimationProgress = value 133 | 134 | val step = startValue * rotateStep 135 | startAngle += step 136 | if (step < 1) { 137 | startAngle += lastStep 138 | } else { 139 | lastStep = step 140 | } 141 | 142 | if (isShrink) { 143 | val diff = sweepAngle - sweepLength 144 | startAngle += diff 145 | } 146 | sweepAngle = sweepLength 147 | startAngle %= 360 148 | 149 | invalidateSelf() 150 | } 151 | } 152 | 153 | override fun onAnimationRepeat(animation: Animator) { 154 | colorIndex++ 155 | colorIndex %= colorList.size 156 | paint.color = colorList[colorIndex] 157 | } 158 | 159 | override fun onAnimationEnd(animation: Animator) {} 160 | 161 | override fun onAnimationCancel(animation: Animator) {} 162 | 163 | override fun onAnimationStart(animation: Animator) { 164 | colorIndex++ 165 | colorIndex %= colorList.size 166 | paint.color = colorList[colorIndex] 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_version_tips.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_app_icon.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_crash_error.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tokyonth/ApkInstaller/fb5b41ac57a1652a093d5dda1f02a514e3864690/app/src/main/res/drawable/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_attachment_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_auto_mode_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 19 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_bedtime_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_beenhere_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_chevron_right_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_construction_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_delete_forever_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_eject_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_get_app_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_info_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_layers_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_memory_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_play_for_work_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_priority_high_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_security_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_settings_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_silence_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_wb_sunny_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_crash_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | 30 | 31 | 44 | 45 | 50 | 51 | 52 | 53 |