From d46716c6a1b4699c1cdf4e29355b9cbcf604ed77 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Wed, 27 Aug 2025 14:26:17 +0800 Subject: [PATCH 01/25] [skip ci] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a4d079a..b619aeff 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![release](https://img.shields.io/github/v/release/SanmerApps/PI?label=release&color=red)](https://github.com/SanmerApps/PI/releases) [![download](https://shields.io/github/downloads/SanmerApps/PI/total?label=download)](https://github.com/SanmerApps/PI/releases/latest) ## Supported Versions -Android 11 ~ 15 +Android 11 ~ 16 ## Credits - [tabler/tabler-icons](https://github.com/tabler/tabler-icons.git) From 5102ab3419214947b6c5b33f9a92d67dc4d2074a Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Wed, 27 Aug 2025 14:46:08 +0800 Subject: [PATCH 02/25] lite: Fix MainViewModel --- app/src/lite/kotlin/dev/sanmer/pi/di/ViewModelModule.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/lite/kotlin/dev/sanmer/pi/di/ViewModelModule.kt b/app/src/lite/kotlin/dev/sanmer/pi/di/ViewModelModule.kt index 6a721ff5..787bf40e 100644 --- a/app/src/lite/kotlin/dev/sanmer/pi/di/ViewModelModule.kt +++ b/app/src/lite/kotlin/dev/sanmer/pi/di/ViewModelModule.kt @@ -1,9 +1,11 @@ package dev.sanmer.pi.di +import dev.sanmer.pi.ui.main.MainViewModel import dev.sanmer.pi.ui.screens.install.InstallViewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val ViewModel = module { + viewModelOf(::MainViewModel) viewModelOf(::InstallViewModel) } \ No newline at end of file From 3bf59d7afa2d2fb6d6d2cccdfcf58fb38d465cd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:13:19 +0800 Subject: [PATCH 03/25] Bump androidxLifecycle from 2.9.2 to 2.9.3 (#296) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dc6f7d73..1f89bb00 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ androidxCore = "1.17.0" androidxCoreSplashscreen = "1.0.1" androidxDataStore = "1.1.7" androidxDocumentFile = "1.1.0" -androidxLifecycle = "2.9.2" +androidxLifecycle = "2.9.3" androidxNavigation = "2.9.3" appiconloader = "1.5.0" coil = "2.7.0" From 50ccacb778047506383f3b79f4b6ace4edd59e9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:13:30 +0800 Subject: [PATCH 04/25] Bump androidGradlePlugin from 8.12.1 to 8.12.2 (#297) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1f89bb00..e7fa563e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -androidGradlePlugin = "8.12.1" +androidGradlePlugin = "8.12.2" androidxActivity = "1.10.1" androidxAnnotation = "1.9.1" androidxAppCompat = "1.7.1" From 42f03e063cc66df6c770cde361822ce630e1e5ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:24:15 +0800 Subject: [PATCH 05/25] Bump androidGradlePlugin from 8.12.2 to 8.13.0 (#298) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e7fa563e..e6069934 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -androidGradlePlugin = "8.12.2" +androidGradlePlugin = "8.13.0" androidxActivity = "1.10.1" androidxAnnotation = "1.9.1" androidxAppCompat = "1.7.1" From 69314f80578da9dc2da3ba26c72b1485f8a80497 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Thu, 4 Sep 2025 13:42:31 +0800 Subject: [PATCH 06/25] [skip ci] Tidy up code --- app/build.gradle.kts | 17 ++++++++--------- build-logic/src/main/kotlin/ProjectExt.kt | 5 ++--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9fa48e69..f2e8d6fb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,7 +10,7 @@ plugins { val baseVersionName: String by extra val devVersion = exec("git tag --contains HEAD").isEmpty() -val commitShaSuffix = commitSha.let { ".${it.substring(0, 7)}" } +val shaSuffix = gitCommitSha.let { ".${it.substring(0, 7)}" } val devSuffix = if (devVersion) ".dev" else "" android { @@ -18,13 +18,12 @@ android { defaultConfig { applicationId = namespace - versionName = "${baseVersionName}${commitShaSuffix}${devSuffix}" - versionCode = commitCount + versionName = "${baseVersionName}${shaSuffix}${devSuffix}" + versionCode = gitCommitCount ndk.abiFilters += listOf("arm64-v8a", "x86_64") } - @Suppress("UnstableApiUsage") androidResources { generateLocaleConfig = true localeFilters += listOf( @@ -46,12 +45,12 @@ android { ) } - val releaseSigning = if (project.hasReleaseKeyStore) { + val releaseSigning = if (hasReleaseKeyStore) { signingConfigs.create("release") { - storeFile = project.releaseKeyStore - storePassword = project.releaseKeyStorePassword - keyAlias = project.releaseKeyAlias - keyPassword = project.releaseKeyPassword + storeFile = releaseKeyStore + storePassword = releaseKeyStorePassword + keyAlias = releaseKeyAlias + keyPassword = releaseKeyPassword enableV3Signing = true enableV4Signing = true } diff --git a/build-logic/src/main/kotlin/ProjectExt.kt b/build-logic/src/main/kotlin/ProjectExt.kt index 1652bf94..5673b0ad 100644 --- a/build-logic/src/main/kotlin/ProjectExt.kt +++ b/build-logic/src/main/kotlin/ProjectExt.kt @@ -3,10 +3,9 @@ import org.gradle.kotlin.dsl.extra import java.io.File import java.util.Properties -val Project.commitSha: String get() = exec("git rev-parse HEAD") -val Project.commitCount: Int get() = exec("git rev-list --count HEAD").toInt() +val Project.gitCommitSha: String get() = exec("git rev-parse HEAD") +val Project.gitCommitCount: Int get() = exec("git rev-list --count HEAD").toInt() -@Suppress("UnstableApiUsage") fun Project.exec(command: String) = providers.exec { commandLine(command.split(" ")) }.standardOutput.asText.get().trim() From 4c3c7aa2501f454a4a529c4f1fc116d24e8b7bff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:36:14 +0800 Subject: [PATCH 07/25] Bump koin from 4.1.0 to 4.1.1 (#299) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6069934..2035d776 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ appiconloader = "1.5.0" coil = "2.7.0" hiddenApiRefine = "4.4.0" hiddenApiBypass = "6.1" -koin = "4.1.0" +koin = "4.1.1" kotlin = "2.2.10" kotlinxCoroutines = "1.10.2" kotlinxSerialization = "1.9.0" From 3798798cafad49544346c705d7f7ef5b6a741f85 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Fri, 5 Sep 2025 16:27:13 +0800 Subject: [PATCH 08/25] Fix notifyUpdated --- app/src/main/kotlin/dev/sanmer/pi/receiver/Updated.kt | 5 +++-- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/dev/sanmer/pi/receiver/Updated.kt b/app/src/main/kotlin/dev/sanmer/pi/receiver/Updated.kt index 7a7e8ce4..b470c19b 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/receiver/Updated.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/receiver/Updated.kt @@ -66,9 +66,10 @@ class Updated : BroadcastReceiver(), KoinComponent { && !PermissionCompat.checkPermission(this, Manifest.permission.POST_NOTIFICATIONS) ) return - val intent = packageManager.getLaunchIntentForPackage(packageName) ?: return val flag = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - val pending = PendingIntent.getActivity(this, 0, intent, flag) + val pending = packageManager.getLaunchIntentForPackage(packageName)?.let { + PendingIntent.getActivity(this, 0, it, flag) + } val builder = NotificationCompat.Builder(this, Const.CHANNEL_ID_INSTALL) .setSmallIcon(R.drawable.launcher_outline) .setContentIntent(pending) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af568b5f..43d6b91a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,7 +51,7 @@ Installation successful Installation failed Optimizing - PI Updated + PI updated Tap to open app From 5aee35d923cb1abb94e682c0d59c2d8e581698d9 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Mon, 8 Sep 2025 23:34:21 +0800 Subject: [PATCH 09/25] Impl new PackageParser --- core/build.gradle.kts | 2 + .../dev/sanmer/pi/AssetManagerCompat.kt | 22 ++ .../kotlin/dev/sanmer/pi/ContextCompat.kt | 2 + .../kotlin/dev/sanmer/pi/PackageInfoCompat.kt | 32 ++- .../dev/sanmer/pi/PackageParserCompat.kt | 138 ----------- .../kotlin/dev/sanmer/pi/XmlBlockCompat.kt | 21 ++ .../sanmer/pi/appiconloader/AppIconLoader.kt | 52 ++++ .../main/kotlin/dev/sanmer/pi/bundle/ABI.kt | 28 --- .../kotlin/dev/sanmer/pi/bundle/BundleInfo.kt | 13 - .../main/kotlin/dev/sanmer/pi/bundle/DPI.kt | 42 ---- .../dev/sanmer/pi/bundle/SplitConfig.kt | 160 ------------ .../sanmer/pi/delegate/PackageInfoDelegate.kt | 8 +- .../pi/delegate/PackageInstallerDelegate.kt | 40 ++- .../src/main/kotlin/dev/sanmer/pi/ktx/Dp.kt | 0 .../main/kotlin/dev/sanmer/pi/ktx/Drawable.kt | 55 +++++ .../src/main/kotlin/dev/sanmer/pi/ktx/File.kt | 8 + .../dev/sanmer/pi/ktx/FileDescriptor.kt | 24 ++ .../dev/sanmer/pi/ktx/PackageInfoLite.kt | 16 ++ .../sanmer/pi/ktx/ZipArchiveInputStream.kt | 14 ++ .../kotlin/dev/sanmer/pi/parser/BundleInfo.kt | 14 ++ .../dev/sanmer/pi/parser/PackageInfoLite.kt | 42 ++++ .../dev/sanmer/pi/parser/PackageParser.kt | 150 ++++++++++++ .../dev/sanmer/pi/parser/ResourceParser.kt | 142 +++++++++++ .../dev/sanmer/pi/parser/SplitConfig.kt | 230 ++++++++++++++++++ .../dev/sanmer/pi/parser/SplitConfigLite.kt | 13 + .../dev/sanmer/pi/res/ApkAssetsWrapper.kt | 54 ++++ .../main/kotlin/dev/sanmer/pi/res/Wrapper.kt | 5 + gradle/libs.versions.toml | 4 +- .../content/pm/PackageInstallerHidden.java | 9 + .../content/pm/PackageManagerHidden.java | 3 + .../android/content/pm/PackageParser.java | 49 ---- .../android/content/pm/PackageUserState.java | 7 - .../java/android/content/pm/UserInfo.java | 4 + .../pm/pkg/FrameworkPackageUserState.java | 8 - .../pkg/FrameworkPackageUserStateDefault.java | 7 - .../java/android/content/res/ApkAssets.java | 29 +++ .../content/res/AssetManagerHidden.java | 16 ++ .../java/android/content/res/XmlBlock.java | 23 ++ 38 files changed, 1011 insertions(+), 475 deletions(-) create mode 100644 core/src/main/kotlin/dev/sanmer/pi/AssetManagerCompat.kt delete mode 100644 core/src/main/kotlin/dev/sanmer/pi/PackageParserCompat.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/XmlBlockCompat.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/appiconloader/AppIconLoader.kt delete mode 100644 core/src/main/kotlin/dev/sanmer/pi/bundle/ABI.kt delete mode 100644 core/src/main/kotlin/dev/sanmer/pi/bundle/BundleInfo.kt delete mode 100644 core/src/main/kotlin/dev/sanmer/pi/bundle/DPI.kt delete mode 100644 core/src/main/kotlin/dev/sanmer/pi/bundle/SplitConfig.kt rename app/src/main/kotlin/dev/sanmer/pi/ktx/DpExt.kt => core/src/main/kotlin/dev/sanmer/pi/ktx/Dp.kt (100%) create mode 100644 core/src/main/kotlin/dev/sanmer/pi/ktx/Drawable.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/ktx/File.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/ktx/FileDescriptor.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/ktx/PackageInfoLite.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/ktx/ZipArchiveInputStream.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/parser/BundleInfo.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/parser/PackageInfoLite.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/parser/PackageParser.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/parser/ResourceParser.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/parser/SplitConfig.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/parser/SplitConfigLite.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/res/ApkAssetsWrapper.kt create mode 100644 core/src/main/kotlin/dev/sanmer/pi/res/Wrapper.kt delete mode 100644 stub/src/main/java/android/content/pm/PackageParser.java delete mode 100644 stub/src/main/java/android/content/pm/PackageUserState.java delete mode 100644 stub/src/main/java/android/content/pm/pkg/FrameworkPackageUserState.java delete mode 100644 stub/src/main/java/android/content/pm/pkg/FrameworkPackageUserStateDefault.java create mode 100644 stub/src/main/java/android/content/res/ApkAssets.java create mode 100644 stub/src/main/java/android/content/res/AssetManagerHidden.java create mode 100644 stub/src/main/java/android/content/res/XmlBlock.java diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 017febab..2b660842 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -36,5 +36,7 @@ dependencies { implementation(libs.rikka.refine.runtime) implementation(libs.androidx.annotation) + implementation(libs.apache.commons.compress) + implementation(libs.appiconloader.iconloaderlib) implementation(libs.kotlinx.coroutines.android) } \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/AssetManagerCompat.kt b/core/src/main/kotlin/dev/sanmer/pi/AssetManagerCompat.kt new file mode 100644 index 00000000..512d92d2 --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/AssetManagerCompat.kt @@ -0,0 +1,22 @@ +package dev.sanmer.pi + +import android.content.res.ApkAssets +import android.content.res.AssetManager +import android.content.res.AssetManagerHidden +import android.content.res.Resources +import dev.rikka.tools.refine.Refine + +object AssetManagerCompat { + fun new(): AssetManager = Refine.unsafeCast(AssetManagerHidden()) + + fun AssetManager.setApkAssets(apkAssets: Array, invalidateCaches: Boolean) = + Refine.unsafeCast(this) + .setApkAssets(apkAssets, invalidateCaches) + + val AssetManager.resources: Resources + get() { + val sys = Resources.getSystem() + @Suppress("DEPRECATION") + return Resources(this, sys.displayMetrics, sys.configuration) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/ContextCompat.kt b/core/src/main/kotlin/dev/sanmer/pi/ContextCompat.kt index 8ba54a12..6b906936 100644 --- a/core/src/main/kotlin/dev/sanmer/pi/ContextCompat.kt +++ b/core/src/main/kotlin/dev/sanmer/pi/ContextCompat.kt @@ -18,4 +18,6 @@ object ContextCompat { return context } + + internal val context by lazy { getContext() } } \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/PackageInfoCompat.kt b/core/src/main/kotlin/dev/sanmer/pi/PackageInfoCompat.kt index 78bd3852..829a1653 100644 --- a/core/src/main/kotlin/dev/sanmer/pi/PackageInfoCompat.kt +++ b/core/src/main/kotlin/dev/sanmer/pi/PackageInfoCompat.kt @@ -1,16 +1,18 @@ package dev.sanmer.pi +import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageInfoHidden +import android.graphics.drawable.Drawable import android.os.Build import androidx.annotation.RequiresApi import dev.rikka.tools.refine.Refine object PackageInfoCompat { - private inline val PackageInfo.original - get() = Refine.unsafeCast(this) - + private val PackageInfo.original + inline get() = Refine.unsafeCast(this) + var PackageInfo.versionCodeMajor: Int get() = original.versionCodeMajor set(v) { original.versionCodeMajor = v } @@ -65,16 +67,34 @@ object PackageInfoCompat { get() = original.isOverlayPackage val PackageInfo?.isEmpty - get() = this?.packageName == null || applicationInfo == null + inline get() = this?.packageName == null || applicationInfo == null val PackageInfo?.isNotEmpty - get() = !isEmpty + inline get() = !isEmpty val PackageInfo.isSystemApp: Boolean - get() = applicationInfo?.let { + inline get() = applicationInfo?.let { it.flags and (ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 } ?: false fun PackageInfo?.orEmpty() = this ?: PackageInfo() + + val PackageInfo.targetSdkVersion: Int + inline get() = applicationInfo?.targetSdkVersion ?: 0 + + val PackageInfo.minSdkVersion: Int + inline get() = applicationInfo?.minSdkVersion ?: 0 + + fun PackageInfo.loadLabel(context: Context): String? { + return applicationInfo?.loadLabel(context.packageManager)?.toString() + } + + fun PackageInfo.loadUnbadgedIcon(context: Context): Drawable? { + return applicationInfo?.loadUnbadgedIcon(context.packageManager) + } + + fun composeLongVersionCode(major: Int, minor: Int): Long { + return ((major.toLong()) shl 32) or ((minor.toLong()) and 0xffffffffL) + } } \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/PackageParserCompat.kt b/core/src/main/kotlin/dev/sanmer/pi/PackageParserCompat.kt deleted file mode 100644 index fb4a1974..00000000 --- a/core/src/main/kotlin/dev/sanmer/pi/PackageParserCompat.kt +++ /dev/null @@ -1,138 +0,0 @@ -package dev.sanmer.pi - -import android.content.pm.PackageInfo -import android.content.pm.PackageParser -import android.content.pm.PackageUserState -import android.content.pm.pkg.FrameworkPackageUserState -import android.util.Log -import dev.sanmer.pi.bundle.BundleInfo -import dev.sanmer.pi.bundle.SplitConfig -import java.io.File -import java.io.FileNotFoundException -import java.io.IOException -import java.io.InputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream - -object PackageParserCompat { - private const val TAG = "PackageParserCompat" - const val BASE_APK = "base.apk" - - private fun parseApkLite(file: File) = - try { - PackageParser.parseApkLite(file, 0) - } catch (e: PackageParser.PackageParserException) { - null - } catch (e: Throwable) { - Log.w(TAG, "Failed to parse ${file.path}", e) - null - } - - private fun parsePackageInner(file: File, flags: Int): PackageInfo? { - val pkg = PackageParser().parsePackage(file, flags, false) - return generatePackageInfo(pkg, flags)?.also { - it.applicationInfo?.sourceDir = file.path - it.applicationInfo?.publicSourceDir = file.path - } - } - - fun parsePackage(file: File, flags: Int) = - try { - parsePackageInner(file, flags) - } catch (e: PackageParser.PackageParserException) { - null - } catch (e: Throwable) { - Log.w(TAG, "Failed to parse ${file.path}", e) - null - } - - private fun generatePackageInfo( - pkg: PackageParser.Package, - flags: Int, - ): PackageInfo? { - return if (BuildCompat.atLeastT) { - PackageParser.generatePackageInfo( - pkg, - null, - flags, - 0, - 0, - null, - FrameworkPackageUserState.DEFAULT - ) - } else { - PackageParser.generatePackageInfo( - pkg, - null, - flags, - 0, - 0, - null, - PackageUserState() - ) - } - } - - private fun parseAppBundleInner(file: File, flags: Int, cacheDir: File): BundleInfo { - file.unzip(cacheDir) - - val baseFile = File(cacheDir, BASE_APK).apply { - if (!exists()) throw FileNotFoundException(BASE_APK) - } - - val baseInfo = parsePackageInner(baseFile, flags) - ?: throw NullPointerException("Failed to parse $BASE_APK") - - val apkFiles = cacheDir.listFiles { f -> f.extension == "apk" } - ?: throw FileNotFoundException("*.apk") - - val splitConfigs = mutableListOf() - for (apkFile in apkFiles) { - if (apkFile.name == BASE_APK) continue - - val apk = parseApkLite(apkFile) - if (apk != null) { - splitConfigs.add( - SplitConfig.parse(apk, apkFile) - ) - } - } - - return BundleInfo( - baseFile = baseFile, - baseInfo = baseInfo, - splitConfigs = splitConfigs.sortedBy { it.file.name } - ) - } - - fun parseAppBundle(file: File, flags: Int, cacheDir: File): BundleInfo? = - try { - parseAppBundleInner(file, flags, cacheDir) - } catch (e: PackageParser.PackageParserException) { - null - } catch (e: Throwable) { - Log.w(TAG, "Failed to parse ${file.path}", e) - null - } - - private fun File.unzip(folder: File) { - inputStream().buffered().use { - it.unzip(folder) - } - } - - private fun InputStream.unzip(folder: File) { - try { - val zin = ZipInputStream(this) - var entry: ZipEntry - while (true) { - entry = zin.nextEntry ?: break - if (!entry.name.endsWith(".apk") || entry.isDirectory) continue - val dest = File(folder, entry.name) - dest.outputStream().use(zin::copyTo) - } - } catch (e: IllegalArgumentException) { - throw IOException(e) - } - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/XmlBlockCompat.kt b/core/src/main/kotlin/dev/sanmer/pi/XmlBlockCompat.kt new file mode 100644 index 00000000..b17359c3 --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/XmlBlockCompat.kt @@ -0,0 +1,21 @@ +package dev.sanmer.pi + +import android.content.res.XmlBlock +import android.content.res.XmlResourceParser + +object XmlBlockCompat { + fun newParser(data: ByteArray): XmlResourceParser { + return if (BuildCompat.atLeastS) { + XmlBlock(data).use { it.newParser() } + } else { + val cls = Class.forName("android.content.res.XmlBlock") + cls.getConstructor(ByteArray::class.java).run { + isAccessible = true + newInstance(data) as AutoCloseable + }.use { + @Suppress("DiscouragedPrivateApi") + cls.getDeclaredMethod("newParser").invoke(it) as XmlResourceParser + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/appiconloader/AppIconLoader.kt b/core/src/main/kotlin/dev/sanmer/pi/appiconloader/AppIconLoader.kt new file mode 100644 index 00000000..7078931f --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/appiconloader/AppIconLoader.kt @@ -0,0 +1,52 @@ +package dev.sanmer.pi.appiconloader + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.UserHandle +import androidx.annotation.Px +import dev.sanmer.pi.ContextCompat +import dev.sanmer.pi.ContextCompat.userId +import me.zhanghai.android.appiconloader.iconloaderlib.BaseIconFactory +import me.zhanghai.android.appiconloader.iconloaderlib.BitmapInfo + +class AppIconLoader( + @field:Px private val iconSize: Int, + private val shrinkNonAdaptiveIcons: Boolean, + context: Context = ContextCompat.getContext() +) { + private val iconFactory = IconFactory(iconSize, context) + private val user = UserHandle.getUserHandleForUid(context.userId) + + fun loadIcon(unbadgedIcon: Drawable): Bitmap { + return iconFactory.createBadgedIconBitmap( + unbadgedIcon, user, shrinkNonAdaptiveIcons, false + ).icon + } + + private class IconFactory( + @Px iconBitmapSize: Int, + context: Context + ) : BaseIconFactory( + context, + context.resources.configuration.densityDpi, + iconBitmapSize, + true + ) { + private val mTempScale = FloatArray(1) + + init { + disableColorExtraction() + } + + fun createBadgedIconBitmap( + icon: Drawable, user: UserHandle?, + shrinkNonAdaptiveIcons: Boolean, + isInstantApp: Boolean + ): BitmapInfo { + return super.createBadgedIconBitmap( + icon, user, shrinkNonAdaptiveIcons, isInstantApp, mTempScale + ) + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/bundle/ABI.kt b/core/src/main/kotlin/dev/sanmer/pi/bundle/ABI.kt deleted file mode 100644 index 9e109722..00000000 --- a/core/src/main/kotlin/dev/sanmer/pi/bundle/ABI.kt +++ /dev/null @@ -1,28 +0,0 @@ -package dev.sanmer.pi.bundle - -import android.os.Build - -enum class ABI(val value: String) { - ARM64_V8A("arm64-v8a"), - ARMEABI_V7A("armeabi-v7a"), - ARMEABI("armeabi"), - X86("x86"), - X86_64("x86_64"); - - val displayName: String - inline get() = value - - val isRequired: Boolean - inline get() = value == Build.SUPPORTED_ABIS[0] - - val isEnabled: Boolean - inline get() = value in Build.SUPPORTED_ABIS - - companion object Default { - fun valueOfOrNull(value: String): ABI? = try { - valueOf(value) - } catch (_: IllegalArgumentException) { - null - } - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/bundle/BundleInfo.kt b/core/src/main/kotlin/dev/sanmer/pi/bundle/BundleInfo.kt deleted file mode 100644 index d5d3a727..00000000 --- a/core/src/main/kotlin/dev/sanmer/pi/bundle/BundleInfo.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.sanmer.pi.bundle - -import android.content.pm.PackageInfo -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import java.io.File - -@Parcelize -data class BundleInfo( - val baseFile: File, - val baseInfo: PackageInfo, - val splitConfigs: List -) : Parcelable \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/bundle/DPI.kt b/core/src/main/kotlin/dev/sanmer/pi/bundle/DPI.kt deleted file mode 100644 index 34296068..00000000 --- a/core/src/main/kotlin/dev/sanmer/pi/bundle/DPI.kt +++ /dev/null @@ -1,42 +0,0 @@ -package dev.sanmer.pi.bundle - -import android.util.DisplayMetrics -import dev.sanmer.pi.ContextCompat - -enum class DPI(val value: Int) { - LDPI(DisplayMetrics.DENSITY_LOW), - MDPI(DisplayMetrics.DENSITY_MEDIUM), - TVDPI(DisplayMetrics.DENSITY_TV), - HDPI(DisplayMetrics.DENSITY_HIGH), - XHDPI(DisplayMetrics.DENSITY_XHIGH), - XXHDPI(DisplayMetrics.DENSITY_XXHIGH), - XXXHDPI(DisplayMetrics.DENSITY_XXXHIGH); - - val displayName: String - inline get() = "${value}.dpi" - - val isRequired: Boolean - get() { - val context = ContextCompat.getContext() - val densityDpi = context.resources.displayMetrics.densityDpi - return this == densityDpi.asDPI() - } - - companion object Default { - private fun Int.asDPI() = when { - this <= DisplayMetrics.DENSITY_LOW -> LDPI - this <= DisplayMetrics.DENSITY_MEDIUM -> MDPI - this <= DisplayMetrics.DENSITY_TV -> TVDPI - this <= DisplayMetrics.DENSITY_HIGH -> HDPI - this <= DisplayMetrics.DENSITY_XHIGH -> XHDPI - this <= DisplayMetrics.DENSITY_XXHIGH -> XXHDPI - else -> XXXHDPI - } - - fun valueOfOrNull(value: String): DPI? = try { - valueOf(value) - } catch (_: IllegalArgumentException) { - null - } - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/bundle/SplitConfig.kt b/core/src/main/kotlin/dev/sanmer/pi/bundle/SplitConfig.kt deleted file mode 100644 index 0fb442e6..00000000 --- a/core/src/main/kotlin/dev/sanmer/pi/bundle/SplitConfig.kt +++ /dev/null @@ -1,160 +0,0 @@ -package dev.sanmer.pi.bundle - -import android.content.pm.PackageParser -import android.os.Parcelable -import android.text.format.Formatter -import dev.sanmer.pi.ContextCompat -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import java.io.File -import java.util.Locale - -sealed class SplitConfig : Parcelable { - abstract val file: File - abstract val name: String - abstract val configForSplit: String? - abstract val isRequired: Boolean - abstract val isDisabled: Boolean - abstract val isRecommended: Boolean - - val displaySize: String - get() = Formatter.formatFileSize(ContextCompat.getContext(), file.length()) - - val isConfigForSplit: Boolean - inline get() = !configForSplit.isNullOrEmpty() - - override fun toString(): String { - return "name = ${name}, " + - "required = ${isRequired}, " + - "disabled = ${isDisabled}, " + - "recommended = $isRecommended" - } - - @Parcelize - data class Target( - val abi: ABI, - override val configForSplit: String?, - override val file: File - ) : SplitConfig() { - @IgnoredOnParcel - override val name by lazy { abi.displayName } - - @IgnoredOnParcel - override val isRequired by lazy { abi.isRequired } - - @IgnoredOnParcel - override val isDisabled by lazy { !abi.isEnabled } - override val isRecommended get() = isRequired - } - - @Parcelize - data class Density( - val dpi: DPI, - override val configForSplit: String?, - override val file: File - ) : SplitConfig() { - override val name get() = dpi.displayName - - @IgnoredOnParcel - override val isRequired by lazy { dpi.isRequired } - override val isDisabled get() = false - override val isRecommended get() = isRequired - } - - @Parcelize - data class Language( - val locale: Locale, - override val configForSplit: String?, - override val file: File - ) : SplitConfig() { - override val name get() = locale.localizedDisplayName - - @IgnoredOnParcel - override val isRequired by lazy { locale.language == Locale.getDefault().language } - - @IgnoredOnParcel - override val isDisabled by lazy { locale !in Locale.getAvailableLocales() } - override val isRecommended get() = isRequired - } - - @Parcelize - data class Feature( - override val name: String, - override val file: File, - ) : SplitConfig() { - override val configForSplit get() = null - override val isRequired get() = false - override val isDisabled get() = false - override val isRecommended get() = true - } - - @Parcelize - data class Unspecified( - override val configForSplit: String?, - override val file: File, - ) : SplitConfig() { - override val name get() = file.name - override val isRequired get() = false - override val isDisabled get() = false - override val isRecommended get() = true - } - - companion object Default { - private val Locale.localizedDisplayName: String - inline get() = getDisplayName(this) - .replaceFirstChar { - if (it.isLowerCase()) { - it.titlecase(this) - } else { - it.toString() - } - } - - private fun PackageParser.ApkLite.splitName(): String { - val splitName = splitName.removeSurrounding("${configForSplit}.", "") - return splitName.removeSurrounding("config.", "") - } - - fun parse(apk: PackageParser.ApkLite, file: File): SplitConfig { - if (apk.isFeatureSplit) { - return Feature( - name = apk.splitName, - file = file - ) - } - - val value = apk.splitName() - val abi = ABI.valueOfOrNull(value.uppercase()) - if (abi != null) { - return Target( - abi = abi, - configForSplit = apk.configForSplit, - file = file - ) - } - - val dpi = DPI.valueOfOrNull(value.uppercase()) - if (dpi != null) { - return Density( - dpi = dpi, - configForSplit = apk.configForSplit, - file = file - ) - } - - val locale = Locale.forLanguageTag(value) - if (locale.language.isNotEmpty()) { - return Language( - locale = locale, - configForSplit = apk.configForSplit, - file = file - ) - } - - return Unspecified( - configForSplit = apk.configForSplit, - file = file - ) - } - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInfoDelegate.kt b/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInfoDelegate.kt index 55e210f9..89226311 100644 --- a/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInfoDelegate.kt +++ b/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInfoDelegate.kt @@ -8,6 +8,7 @@ import dev.sanmer.pi.PackageInfoCompat.compileSdkVersionCodename import dev.sanmer.pi.PackageInfoCompat.coreApp import dev.sanmer.pi.PackageInfoCompat.isActiveApex import dev.sanmer.pi.PackageInfoCompat.isStub +import dev.sanmer.pi.PackageInfoCompat.loadLabel import dev.sanmer.pi.PackageInfoCompat.overlayCategory import dev.sanmer.pi.PackageInfoCompat.overlayPriority import dev.sanmer.pi.PackageInfoCompat.overlayTarget @@ -67,10 +68,7 @@ abstract class PackageInfoDelegate( } } - val appLabel by lazy { - val context = ContextCompat.getContext() - applicationInfo?.loadLabel( - context.packageManager - ).toString() + val label by lazy { + loadLabel(ContextCompat.context) ?: "" } } \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt b/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt index 2b4aa714..27ef3c15 100644 --- a/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt +++ b/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt @@ -10,6 +10,7 @@ import android.content.pm.PackageManager import android.content.pm.PackageManagerHidden import android.content.pm.VersionedPackage import android.os.IBinder +import android.os.ParcelFileDescriptor import android.os.ServiceManager import androidx.annotation.RequiresApi import dev.rikka.tools.refine.Refine @@ -17,11 +18,11 @@ import dev.sanmer.pi.BuildCompat import dev.sanmer.pi.ContextCompat import dev.sanmer.pi.ContextCompat.userId import dev.sanmer.pi.IntentReceiverCompat +import dev.sanmer.pi.ktx.asZipFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import java.io.File class PackageInstallerDelegate( private val proxy: IBinder.() -> IBinder = { this } @@ -183,6 +184,9 @@ class PackageInstallerDelegate( @get:RequiresApi(34) val INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK get() = PackageManagerHidden.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK + + @get:RequiresApi(34) + val INSTALL_REQUEST_UPDATE_OWNERSHIP get() = PackageManagerHidden.INSTALL_REQUEST_UPDATE_OWNERSHIP } } @@ -191,18 +195,32 @@ class PackageInstallerDelegate( commit(sender) } - suspend fun PackageInstaller.Session.write(file: File) = withContext(Dispatchers.IO) { - openWrite(file.name, 0, file.length()).use { output -> - file.inputStream().buffered().use { input -> - input.copyTo(output) - fsync(output) - } - } + suspend fun PackageInstaller.Session.writeFd( + name: String, + fd: ParcelFileDescriptor + ) = withContext(Dispatchers.IO) { + Refine.unsafeCast(this@writeFd) + .write(name, 0, fd.statSize, fd) } - suspend fun PackageInstaller.Session.write(files: List) = - withContext(Dispatchers.IO) { - files.map { async { write(it) } }.awaitAll() + suspend fun PackageInstaller.Session.writeZip( + names: List, + fd: ParcelFileDescriptor + ) = withContext(Dispatchers.IO) { + fd.fileDescriptor.asZipFile().use { zip -> + zip.entries.toList().map { entry -> + async { + if (entry.name in names) { + zip.getInputStream(entry).use { input -> + openWrite(entry.name, 0, entry.size).use { output -> + input.copyTo(output) + fsync(output) + } + } + } + } + }.awaitAll() } + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/ktx/DpExt.kt b/core/src/main/kotlin/dev/sanmer/pi/ktx/Dp.kt similarity index 100% rename from app/src/main/kotlin/dev/sanmer/pi/ktx/DpExt.kt rename to core/src/main/kotlin/dev/sanmer/pi/ktx/Dp.kt diff --git a/core/src/main/kotlin/dev/sanmer/pi/ktx/Drawable.kt b/core/src/main/kotlin/dev/sanmer/pi/ktx/Drawable.kt new file mode 100644 index 00000000..ca72e225 --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/ktx/Drawable.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.sanmer.pi.ktx + +import android.graphics.Bitmap +import android.graphics.Bitmap.Config +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import androidx.annotation.Px + +internal fun Drawable.toBitmap( + @Px width: Int = intrinsicWidth, + @Px height: Int = intrinsicHeight, + config: Config? = null, +): Bitmap { + if (this is BitmapDrawable) { + if (bitmap == null) { + // This is slightly better than returning an empty, zero-size bitmap. + throw IllegalArgumentException("bitmap is null") + } + if (config == null || bitmap.config == config) { + // Fast-path to return original. Bitmap.createScaledBitmap will do this check, but it + // involves allocation and two jumps into native code so we perform the check ourselves. + if (width == bitmap.width && height == bitmap.height) { + return bitmap + } + + // noinspection UseKtx + return Bitmap.createScaledBitmap(bitmap, width, height, true) + } + } + + // noinspection UseKtx + val bitmap = Bitmap.createBitmap(width, height, config ?: Config.ARGB_8888) + setBounds(0, 0, width, height) + draw(Canvas(bitmap)) + + setBounds(bounds.left, bounds.top, bounds.right, bounds.bottom) + return bitmap +} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/ktx/File.kt b/core/src/main/kotlin/dev/sanmer/pi/ktx/File.kt new file mode 100644 index 00000000..dcd520e6 --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/ktx/File.kt @@ -0,0 +1,8 @@ +package dev.sanmer.pi.ktx + +import java.io.File +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@OptIn(ExperimentalUuidApi::class) +fun File?.temp() = File(this, Uuid.random().toString()) \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/ktx/FileDescriptor.kt b/core/src/main/kotlin/dev/sanmer/pi/ktx/FileDescriptor.kt new file mode 100644 index 00000000..c38f7c65 --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/ktx/FileDescriptor.kt @@ -0,0 +1,24 @@ +package dev.sanmer.pi.ktx + +import android.system.Os +import android.system.OsConstants +import org.apache.commons.compress.archivers.zip.ZipFile +import java.io.FileDescriptor +import java.io.FileInputStream + +internal fun FileDescriptor.asZipFile() = ZipFile.builder() + .setIgnoreLocalFileHeader(true) + .setSeekableByteChannel(FileInputStream(this).channel) + .get() + +val FileDescriptor.statSize: Long + get() { + val st = Os.fstat(this) + return if (OsConstants.S_ISREG(st.st_mode) || + OsConstants.S_ISLNK(st.st_mode) + ) { + st.st_size + } else { + -1 + } + } \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/ktx/PackageInfoLite.kt b/core/src/main/kotlin/dev/sanmer/pi/ktx/PackageInfoLite.kt new file mode 100644 index 00000000..a79378fd --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/ktx/PackageInfoLite.kt @@ -0,0 +1,16 @@ +package dev.sanmer.pi.ktx + +import dev.sanmer.pi.parser.PackageInfoLite + +fun PackageInfoLite?.orEmpty() = PackageInfoLite( + packageName = "", + versionCode = 0, + versionCodeMajor = 0, + versionName = "", + compileSdkVersion = 0, + compileSdkVersionCodename = "", + minSdkVersion = 0, + targetSdkVersion = 0, + label = null, + icon = null +) \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/ktx/ZipArchiveInputStream.kt b/core/src/main/kotlin/dev/sanmer/pi/ktx/ZipArchiveInputStream.kt new file mode 100644 index 00000000..f2bd439d --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/ktx/ZipArchiveInputStream.kt @@ -0,0 +1,14 @@ +package dev.sanmer.pi.ktx + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream +import java.io.FileNotFoundException + +@Throws(FileNotFoundException::class) +internal fun ZipArchiveInputStream.find(name: String): ZipArchiveEntry { + while (true) { + val entry = nextEntry ?: break + if (entry.name == name) return entry + } + throw FileNotFoundException("$name not found") +} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/parser/BundleInfo.kt b/core/src/main/kotlin/dev/sanmer/pi/parser/BundleInfo.kt new file mode 100644 index 00000000..62360a6b --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/parser/BundleInfo.kt @@ -0,0 +1,14 @@ +package dev.sanmer.pi.parser + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class BundleInfo( + val packageInfo: PackageInfoLite, + val fileName: String, + val sizeBytes: Long, + val splitConfigs: List +) : Parcelable { + val isZip inline get() = fileName.isNotEmpty() +} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/parser/PackageInfoLite.kt b/core/src/main/kotlin/dev/sanmer/pi/parser/PackageInfoLite.kt new file mode 100644 index 00000000..2991ece7 --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/parser/PackageInfoLite.kt @@ -0,0 +1,42 @@ +package dev.sanmer.pi.parser + +import android.content.res.Resources +import android.graphics.Bitmap +import android.os.Parcelable +import dev.sanmer.pi.PackageInfoCompat +import dev.sanmer.pi.parser.ResourceParser.toIcon +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +data class PackageInfoLite( + val packageName: String, + val versionCode: Int, + val versionCodeMajor: Int, + val versionName: String, + val compileSdkVersion: Int, + val compileSdkVersionCodename: String, + val minSdkVersion: Int, + val targetSdkVersion: Int, + val label: String?, + val icon: Bitmap? +) : Parcelable { + @IgnoredOnParcel + val longVersionCode by lazy { + PackageInfoCompat.composeLongVersionCode( + versionCodeMajor, versionCode + ) + } + + @IgnoredOnParcel + val labelOrDefault by lazy { + label ?: packageName + } + + @IgnoredOnParcel + val iconOrDefault by lazy { + icon ?: Resources.getSystem() + .getDrawable(android.R.drawable.sym_def_app_icon, null) + .toIcon() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/parser/PackageParser.kt b/core/src/main/kotlin/dev/sanmer/pi/parser/PackageParser.kt new file mode 100644 index 00000000..e734f2ed --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/parser/PackageParser.kt @@ -0,0 +1,150 @@ +package dev.sanmer.pi.parser + +import android.content.pm.PackageInfo +import dev.sanmer.pi.AssetManagerCompat +import dev.sanmer.pi.AssetManagerCompat.resources +import dev.sanmer.pi.AssetManagerCompat.setApkAssets +import dev.sanmer.pi.ContextCompat +import dev.sanmer.pi.PackageInfoCompat.compileSdkVersion +import dev.sanmer.pi.PackageInfoCompat.compileSdkVersionCodename +import dev.sanmer.pi.PackageInfoCompat.loadLabel +import dev.sanmer.pi.PackageInfoCompat.loadUnbadgedIcon +import dev.sanmer.pi.PackageInfoCompat.minSdkVersion +import dev.sanmer.pi.PackageInfoCompat.targetSdkVersion +import dev.sanmer.pi.PackageInfoCompat.versionCodeMajor +import dev.sanmer.pi.XmlBlockCompat +import dev.sanmer.pi.ktx.asZipFile +import dev.sanmer.pi.ktx.find +import dev.sanmer.pi.ktx.statSize +import dev.sanmer.pi.parser.ResourceParser.toIcon +import dev.sanmer.pi.res.ApkAssetsWrapper +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream +import java.io.FileDescriptor +import java.io.InputStream +import kotlin.io.readBytes +import kotlin.use + +object PackageParser { + const val BASE_APK = "base.apk" + + fun setEnableAdaptiveIcons(value: Boolean) { + ResourceParser.enableAdaptiveIcons = value + } + + @Suppress("DEPRECATION") + fun PackageInfo.toLite() = PackageInfoLite( + packageName = packageName, + versionCode = versionCode, + versionCodeMajor = versionCodeMajor, + versionName = versionName.orEmpty(), + compileSdkVersion = compileSdkVersion, + compileSdkVersionCodename = compileSdkVersionCodename.orEmpty(), + minSdkVersion = minSdkVersion, + targetSdkVersion = targetSdkVersion, + label = loadLabel(ContextCompat.context), + icon = loadUnbadgedIcon(ContextCompat.context)?.toIcon() + ) + + fun loadSplitFromFd(fd: FileDescriptor): SplitConfigLite { + val lite = ApkAssetsWrapper.Fd(fd).use { wrapper -> + val asset = wrapper.get() + asset.openXml(ResourceParser.ANDROID_MANIFEST).use { + ResourceParser.parseSplit(it) + } + } + require(lite.versionCode > 0) { "VersionCode not set" } + return lite + } + + fun loadSplitFromStream(stream: InputStream): SplitConfigLite { + val lite = ZipArchiveInputStream(stream).use { zip -> + zip.find(ResourceParser.ANDROID_MANIFEST) + XmlBlockCompat.newParser(zip.readBytes()).use { + ResourceParser.parseSplit(it) + } + } + require(lite.versionCode > 0) { "VersionCode not set" } + return lite + } + + fun loadBaseFromFd(fd: FileDescriptor): PackageInfoLite { + val assets = AssetManagerCompat.new() + val base = ApkAssetsWrapper.Fd(fd).use { wrapper -> + val asset = wrapper.get() + assets.setApkAssets(arrayOf(asset), false) + asset.openXml(ResourceParser.ANDROID_MANIFEST).use { + ResourceParser.parseBase(it, assets.resources) + } + } + require(base.versionCode > 0) { "VersionCode not set" } + return base + } + + fun loadBaseFromStream(stream: InputStream): PackageInfoLite { + val base = ApkAssetsWrapper.Stream(stream).use { wrapper -> + val asset = wrapper.get() + val assets = AssetManagerCompat.new() + assets.setApkAssets(arrayOf(asset), false) + asset.openXml(ResourceParser.ANDROID_MANIFEST).use { + ResourceParser.parseBase(it, assets.resources) + } + } + require(base.versionCode > 0) { "VersionCode not set" } + return base + } + + fun loadBundleFromFd(fd: FileDescriptor): BundleInfo { + return fd.asZipFile().use { zip -> + val xml = zip.getEntry(ResourceParser.ANDROID_MANIFEST) + if (xml != null) { + val packageInfo = loadBaseFromFd(fd) + return BundleInfo( + packageInfo = packageInfo, + fileName = "", + sizeBytes = fd.statSize, + splitConfigs = emptyList() + ) + } + + val base = zip.getEntry(BASE_APK) + if (base != null) { + val stream = zip.getInputStream(base) + val packageInfo = loadBaseFromStream(stream) + + val splitConfigs = mutableListOf() + zip.entries.iterator().forEach { entry -> + if (entry == base || !entry.name.endsWith(".apk")) return@forEach + runCatching { + val stream = zip.getInputStream(entry) + splitConfigs.add( + SplitConfig( + lite = loadSplitFromStream(stream), + fileName = entry.name, + sizeBytes = entry.size + ) + ) + } + } + + BundleInfo( + packageInfo = packageInfo, + fileName = BASE_APK, + sizeBytes = base.size, + splitConfigs = splitConfigs + ) + } else { + val entries = zip.entries.toList().filter { it.name.endsWith(".apk") } + require(entries.size == 1) { "Unsupported ZIP" } + val entry = entries[0] + val stream = zip.getInputStream(entry) + + BundleInfo( + packageInfo = loadBaseFromStream(stream), + fileName = entry.name, + sizeBytes = entry.size, + splitConfigs = emptyList() + ) + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/parser/ResourceParser.kt b/core/src/main/kotlin/dev/sanmer/pi/parser/ResourceParser.kt new file mode 100644 index 00000000..386e9f8c --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/parser/ResourceParser.kt @@ -0,0 +1,142 @@ +package dev.sanmer.pi.parser + +import android.content.res.Resources +import android.content.res.XmlResourceParser +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import dev.sanmer.pi.appiconloader.AppIconLoader +import dev.sanmer.pi.ktx.dp +import dev.sanmer.pi.ktx.toBitmap +import org.xmlpull.v1.XmlPullParser + +internal object ResourceParser { + const val ANDROID_MANIFEST = "AndroidManifest.xml" + const val ANDROID_RESOURCES = "http://schemas.android.com/apk/res/android" + const val TAG_MANIFEST = "manifest" + const val TAG_USES_SDK = "uses-sdk" + const val TAG_APPLICATION = "application" + + val appIconLoader by lazy { AppIconLoader(40.dp, true) } + var enableAdaptiveIcons = true + + fun Drawable.toIcon(): Bitmap { + return if (enableAdaptiveIcons) { + appIconLoader.loadIcon(this) + } else { + toBitmap() + } + } + + fun XmlResourceParser.nextOrNull(): Int? { + return next().takeIf { it != XmlPullParser.END_DOCUMENT } + } + + fun XmlResourceParser.getAttributeValue( + namespace: String?, name: String, defaultValue: String + ): String { + return getAttributeValue(namespace, name) ?: defaultValue + } + + fun XmlResourceParser.getAttributeResStringValue( + res: Resources, name: String + ): String? { + val resId = getAttributeResourceValue(ANDROID_RESOURCES, name, 0) + return runCatching { res.getString(resId) }.getOrNull() + } + + fun XmlResourceParser.getAttributeResDrawableValue( + res: Resources, name: String + ): Drawable? { + val resId = getAttributeResourceValue(ANDROID_RESOURCES, name, 0) + return runCatching { res.getDrawable(resId, null) }.getOrNull() + } + + inline fun P.split( + onManifest: P.() -> Unit, + onUsesSdk: P.() -> Unit, + onApplication: P.() -> Unit + ) { + while (true) { + val eventType = nextOrNull() ?: break + if (eventType != XmlPullParser.START_TAG) continue + when (name) { + TAG_MANIFEST -> onManifest(this) + TAG_USES_SDK -> onUsesSdk(this) + TAG_APPLICATION -> onApplication(this) + } + } + } + + fun parseSplit(parser: XmlResourceParser): SplitConfigLite { + var packageName = "" + var splitName = "" + var configForSplit = "" + var versionCode = -1 + var isFeatureSplit = false + + parser.split( + onManifest = { + packageName = getAttributeValue(null, "package", "") + splitName = getAttributeValue(null, "split", "") + configForSplit = getAttributeValue(null, "configForSplit", "") + versionCode = getAttributeIntValue(ANDROID_RESOURCES, "versionCode", 0) + isFeatureSplit = getAttributeBooleanValue(ANDROID_RESOURCES, "isFeatureSplit", false) + }, + onUsesSdk = {}, + onApplication = {} + ) + + return SplitConfigLite( + packageName = packageName, + splitName = splitName, + configForSplit = configForSplit, + versionCode = versionCode, + isFeatureSplit = isFeatureSplit + ) + } + + fun parseBase(parser: XmlResourceParser, res: Resources): PackageInfoLite { + var packageName = "" + var versionCode = -1 + var versionCodeMajor = -1 + var versionName = "" + var compileSdkVersion = -1 + var compileSdkVersionCodename = "" + var minSdkVersion = -1 + var targetSdkVersion = -1 + var label: String? = null + var icon: Bitmap? = null + + parser.split( + onManifest = { + packageName = getAttributeValue(null, "package", "") + versionCode = getAttributeIntValue(ANDROID_RESOURCES, "versionCode", 0) + versionCodeMajor = getAttributeIntValue(ANDROID_RESOURCES, "versionCodeMajor", 0) + versionName = getAttributeValue(ANDROID_RESOURCES, "versionName", "") + compileSdkVersion = getAttributeIntValue(ANDROID_RESOURCES, "compileSdkVersion", 0) + compileSdkVersionCodename = getAttributeValue(ANDROID_RESOURCES, "compileSdkVersionCodename", "") + }, + onUsesSdk = { + minSdkVersion = getAttributeIntValue(ANDROID_RESOURCES, "minSdkVersion", 0) + targetSdkVersion = getAttributeIntValue(ANDROID_RESOURCES, "targetSdkVersion", 0) + }, + onApplication = { + label = getAttributeResStringValue(res, "label") + icon = getAttributeResDrawableValue(res, "icon")?.toIcon() + } + ) + + return PackageInfoLite( + packageName = packageName, + versionCode = versionCode, + versionCodeMajor = versionCodeMajor, + versionName = versionName, + compileSdkVersion = compileSdkVersion, + compileSdkVersionCodename = compileSdkVersionCodename, + minSdkVersion = minSdkVersion, + targetSdkVersion = targetSdkVersion, + label = label, + icon = icon + ) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/parser/SplitConfig.kt b/core/src/main/kotlin/dev/sanmer/pi/parser/SplitConfig.kt new file mode 100644 index 00000000..a79810a7 --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/parser/SplitConfig.kt @@ -0,0 +1,230 @@ +package dev.sanmer.pi.parser + +import android.os.Build +import android.os.Parcelable +import android.text.format.Formatter +import android.util.DisplayMetrics +import dev.sanmer.pi.ContextCompat +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import java.util.Locale + +sealed class SplitConfig : Parcelable { + abstract val fileName: String + abstract val sizeBytes: Long + + abstract val name: String + abstract val configForSplit: String + abstract val isRequired: Boolean + abstract val isDisabled: Boolean + abstract val isRecommended: Boolean + + val size: String by lazy { + Formatter.formatFileSize(ContextCompat.context, sizeBytes) + } + + @Parcelize + data class Target( + val abi: ABI, + override val configForSplit: String, + override val fileName: String, + override val sizeBytes: Long + ) : SplitConfig() { + @IgnoredOnParcel + override val name by lazy { abi.displayName } + + @IgnoredOnParcel + override val isRequired by lazy { abi.isRequired } + + @IgnoredOnParcel + override val isDisabled by lazy { !abi.isEnabled } + override val isRecommended get() = isRequired + } + + @Parcelize + data class Density( + val dpi: DPI, + override val configForSplit: String, + override val fileName: String, + override val sizeBytes: Long + ) : SplitConfig() { + override val name get() = dpi.displayName + + @IgnoredOnParcel + override val isRequired by lazy { dpi.isRequired } + override val isDisabled get() = false + override val isRecommended get() = isRequired + } + + @Parcelize + data class Language( + val locale: Locale, + override val configForSplit: String, + override val fileName: String, + override val sizeBytes: Long + ) : SplitConfig() { + override val name get() = locale.localizedDisplayName + + @IgnoredOnParcel + override val isRequired by lazy { locale.language == Locale.getDefault().language } + + @IgnoredOnParcel + override val isDisabled by lazy { locale !in Locale.getAvailableLocales() } + override val isRecommended get() = isRequired + } + + @Parcelize + data class Feature( + override val name: String, + override val fileName: String, + override val sizeBytes: Long + ) : SplitConfig() { + override val configForSplit get() = "" + override val isRequired get() = false + override val isDisabled get() = false + override val isRecommended get() = true + } + + @Parcelize + data class Unspecified( + override val configForSplit: String, + override val fileName: String, + override val sizeBytes: Long + ) : SplitConfig() { + override val name get() = fileName + override val isRequired get() = false + override val isDisabled get() = false + override val isRecommended get() = true + } + + enum class ABI(val value: String) { + ARM64_V8A("arm64-v8a"), + ARMEABI_V7A("armeabi-v7a"), + ARMEABI("armeabi"), + X86("x86"), + X86_64("x86_64"); + + val displayName: String + inline get() = value + + val isRequired: Boolean + inline get() = value == Build.SUPPORTED_ABIS[0] + + val isEnabled: Boolean + inline get() = value in Build.SUPPORTED_ABIS + + companion object Default { + fun valueOfOrNull(value: String): ABI? = try { + valueOf(value) + } catch (_: IllegalArgumentException) { + null + } + } + } + + enum class DPI(val value: Int) { + LDPI(DisplayMetrics.DENSITY_LOW), + MDPI(DisplayMetrics.DENSITY_MEDIUM), + TVDPI(DisplayMetrics.DENSITY_TV), + HDPI(DisplayMetrics.DENSITY_HIGH), + XHDPI(DisplayMetrics.DENSITY_XHIGH), + XXHDPI(DisplayMetrics.DENSITY_XXHIGH), + XXXHDPI(DisplayMetrics.DENSITY_XXXHIGH); + + val displayName: String + inline get() = "${value}.dpi" + + val isRequired: Boolean + get() { + val context = ContextCompat.context + val densityDpi = context.resources.displayMetrics.densityDpi + return this == densityDpi.asDPI() + } + + companion object Default { + private fun Int.asDPI() = when { + this <= DisplayMetrics.DENSITY_LOW -> LDPI + this <= DisplayMetrics.DENSITY_MEDIUM -> MDPI + this <= DisplayMetrics.DENSITY_TV -> TVDPI + this <= DisplayMetrics.DENSITY_HIGH -> HDPI + this <= DisplayMetrics.DENSITY_XHIGH -> XHDPI + this <= DisplayMetrics.DENSITY_XXHIGH -> XXHDPI + else -> XXXHDPI + } + + fun valueOfOrNull(value: String): DPI? = try { + valueOf(value) + } catch (_: IllegalArgumentException) { + null + } + } + } + + internal companion object Default { + val Locale.localizedDisplayName: String + inline get() = getDisplayName(this) + .replaceFirstChar { + if (it.isLowerCase()) { + it.titlecase(this) + } else { + it.toString() + } + } + + fun SplitConfigLite.tagValue(): String { + val value = splitName.removeSurrounding("${configForSplit}.", "") + return value.removeSurrounding("config.", "") + } + + operator fun invoke( + lite: SplitConfigLite, + fileName: String, + sizeBytes: Long + ): SplitConfig { + if (lite.isFeatureSplit) { + return Feature( + name = lite.splitName, + fileName = fileName, + sizeBytes = sizeBytes + ) + } + + val value = lite.tagValue() + val abi = ABI.Default.valueOfOrNull(value.uppercase()) + if (abi != null) { + return Target( + abi = abi, + configForSplit = lite.configForSplit, + fileName = fileName, + sizeBytes = sizeBytes + ) + } + + val dpi = DPI.Default.valueOfOrNull(value.uppercase()) + if (dpi != null) { + return Density( + dpi = dpi, + configForSplit = lite.configForSplit, + fileName = fileName, + sizeBytes = sizeBytes + ) + } + + val locale = Locale.forLanguageTag(value) + if (locale.language.isNotEmpty()) { + return Language( + locale = locale, + configForSplit = lite.configForSplit, + fileName = fileName, + sizeBytes = sizeBytes + ) + } + + return Unspecified( + configForSplit = lite.configForSplit, + fileName = fileName, + sizeBytes = sizeBytes + ) + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/parser/SplitConfigLite.kt b/core/src/main/kotlin/dev/sanmer/pi/parser/SplitConfigLite.kt new file mode 100644 index 00000000..1298c0f6 --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/parser/SplitConfigLite.kt @@ -0,0 +1,13 @@ +package dev.sanmer.pi.parser + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class SplitConfigLite( + val packageName: String, + val splitName: String, + val configForSplit: String, + val versionCode: Int, + val isFeatureSplit: Boolean +) : Parcelable diff --git a/core/src/main/kotlin/dev/sanmer/pi/res/ApkAssetsWrapper.kt b/core/src/main/kotlin/dev/sanmer/pi/res/ApkAssetsWrapper.kt new file mode 100644 index 00000000..d929791d --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/res/ApkAssetsWrapper.kt @@ -0,0 +1,54 @@ +package dev.sanmer.pi.res + +import android.content.res.ApkAssets +import android.util.Log +import dev.sanmer.pi.ContextCompat +import dev.sanmer.pi.ktx.statSize +import dev.sanmer.pi.ktx.temp +import java.io.FileDescriptor +import java.io.InputStream + +sealed interface ApkAssetsWrapper : Wrapper { + class Stream( + original: InputStream + ) : ApkAssetsWrapper { + private val temp = ContextCompat.getContext().externalCacheDir.temp() + private val asset by lazy { ApkAssets.loadFromPath(temp.absolutePath, 0, null) } + + init { + original.buffered().use { input -> + temp.outputStream().use { output -> + input.copyTo(output) + } + } + } + + override fun get() = asset + + override fun close() { + asset.close() + if (temp.delete()) { + Log.w("ApkAssetsWrapper.Stream", "Deleted $temp") + } + } + } + + class Fd( + private val original: FileDescriptor + ) : ApkAssetsWrapper { + private val asset by lazy { + ApkAssets.loadFromFd( + original, + original.toString(), + 0, + original.statSize, + 0, + null + ) + } + + override fun get() = asset + + override fun close() = asset.close() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/dev/sanmer/pi/res/Wrapper.kt b/core/src/main/kotlin/dev/sanmer/pi/res/Wrapper.kt new file mode 100644 index 00000000..1bd30574 --- /dev/null +++ b/core/src/main/kotlin/dev/sanmer/pi/res/Wrapper.kt @@ -0,0 +1,5 @@ +package dev.sanmer.pi.res + +interface Wrapper : AutoCloseable { + fun get(): T +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2035d776..45b80425 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ androidxDataStore = "1.1.7" androidxDocumentFile = "1.1.0" androidxLifecycle = "2.9.3" androidxNavigation = "2.9.3" +apacheCompress = "1.28.0" appiconloader = "1.5.0" coil = "2.7.0" hiddenApiRefine = "4.4.0" @@ -45,8 +46,9 @@ androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidxLifecycle" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigation" } -appiconloader = { module = "me.zhanghai.android.appiconloader:appiconloader", version.ref = "appiconloader" } +apache-commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "apacheCompress" } appiconloader-coil = { module = "me.zhanghai.android.appiconloader:appiconloader-coil", version.ref = "appiconloader" } +appiconloader-iconloaderlib = { module = "me.zhanghai.android.appiconloader:appiconloader-iconloaderlib", version.ref = "appiconloader" } coil-kt = { module = "io.coil-kt:coil", version.ref = "coil" } coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } hiddenApiBypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenApiBypass" } diff --git a/stub/src/main/java/android/content/pm/PackageInstallerHidden.java b/stub/src/main/java/android/content/pm/PackageInstallerHidden.java index 51368e5a..69183587 100644 --- a/stub/src/main/java/android/content/pm/PackageInstallerHidden.java +++ b/stub/src/main/java/android/content/pm/PackageInstallerHidden.java @@ -1,5 +1,9 @@ package android.content.pm; +import android.os.ParcelFileDescriptor; + +import androidx.annotation.NonNull; + import dev.rikka.tools.refine.RefineAs; @RefineAs(PackageInstaller.class) @@ -20,5 +24,10 @@ public static class SessionHidden { public SessionHidden(IPackageInstallerSession session) { throw new RuntimeException("Stub!"); } + + public void write(@NonNull String name, long offsetBytes, long lengthBytes, + @NonNull ParcelFileDescriptor fd) { + throw new RuntimeException("Stub!"); + } } } \ No newline at end of file diff --git a/stub/src/main/java/android/content/pm/PackageManagerHidden.java b/stub/src/main/java/android/content/pm/PackageManagerHidden.java index 2a1fb5d7..2541b355 100644 --- a/stub/src/main/java/android/content/pm/PackageManagerHidden.java +++ b/stub/src/main/java/android/content/pm/PackageManagerHidden.java @@ -15,4 +15,7 @@ public class PackageManagerHidden { @RequiresApi(34) public static int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK; + + @RequiresApi(34) + public static int INSTALL_REQUEST_UPDATE_OWNERSHIP; } \ No newline at end of file diff --git a/stub/src/main/java/android/content/pm/PackageParser.java b/stub/src/main/java/android/content/pm/PackageParser.java deleted file mode 100644 index 18a2cf23..00000000 --- a/stub/src/main/java/android/content/pm/PackageParser.java +++ /dev/null @@ -1,49 +0,0 @@ -package android.content.pm; - -import android.content.pm.pkg.FrameworkPackageUserState; - -import androidx.annotation.RequiresApi; - -import java.io.File; -import java.util.Set; - -public class PackageParser { - public PackageParser() { - throw new RuntimeException("Stub!"); - } - - public Package parsePackage(File packageFile, int flags, boolean useCaches) throws PackageParserException { - throw new RuntimeException("Stub!"); - } - - public static ApkLite parseApkLite(File apkFile, int flags) throws PackageParserException { - throw new RuntimeException("Stub!"); - } - - @RequiresApi(33) - public static PackageInfo generatePackageInfo(PackageParser.Package p, - int[] gids, int flags, long firstInstallTime, long lastUpdateTime, - Set grantedPermissions, FrameworkPackageUserState state) { - throw new RuntimeException("Stub!"); - } - - public static PackageInfo generatePackageInfo(PackageParser.Package p, - int[] gids, int flags, long firstInstallTime, long lastUpdateTime, - Set grantedPermissions, PackageUserState state) { - throw new RuntimeException("Stub!"); - } - - public static class PackageParserException extends Exception {} - - public static class Package { - public String packageName; - } - - public static class ApkLite { - public String packageName; - public String splitName; - public boolean isFeatureSplit; - public String configForSplit; - public String usesSplitName; - } -} diff --git a/stub/src/main/java/android/content/pm/PackageUserState.java b/stub/src/main/java/android/content/pm/PackageUserState.java deleted file mode 100644 index e9d46240..00000000 --- a/stub/src/main/java/android/content/pm/PackageUserState.java +++ /dev/null @@ -1,7 +0,0 @@ -package android.content.pm; - -public class PackageUserState { - public PackageUserState() { - throw new RuntimeException("Stub!"); - } -} diff --git a/stub/src/main/java/android/content/pm/UserInfo.java b/stub/src/main/java/android/content/pm/UserInfo.java index 3fcc3412..e247864b 100644 --- a/stub/src/main/java/android/content/pm/UserInfo.java +++ b/stub/src/main/java/android/content/pm/UserInfo.java @@ -10,6 +10,10 @@ public class UserInfo implements Parcelable { public int id; public String name; + public UserInfo(int id, String name, int flags) { + throw new RuntimeException("Stub!"); + } + public boolean isPrimary() { throw new RuntimeException("Stub!"); } diff --git a/stub/src/main/java/android/content/pm/pkg/FrameworkPackageUserState.java b/stub/src/main/java/android/content/pm/pkg/FrameworkPackageUserState.java deleted file mode 100644 index 29715fe5..00000000 --- a/stub/src/main/java/android/content/pm/pkg/FrameworkPackageUserState.java +++ /dev/null @@ -1,8 +0,0 @@ -package android.content.pm.pkg; - -import androidx.annotation.RequiresApi; - -@RequiresApi(33) -public interface FrameworkPackageUserState { - FrameworkPackageUserState DEFAULT = new FrameworkPackageUserStateDefault(); -} diff --git a/stub/src/main/java/android/content/pm/pkg/FrameworkPackageUserStateDefault.java b/stub/src/main/java/android/content/pm/pkg/FrameworkPackageUserStateDefault.java deleted file mode 100644 index 2d221446..00000000 --- a/stub/src/main/java/android/content/pm/pkg/FrameworkPackageUserStateDefault.java +++ /dev/null @@ -1,7 +0,0 @@ -package android.content.pm.pkg; - -import androidx.annotation.RequiresApi; - -@RequiresApi(33) -public class FrameworkPackageUserStateDefault implements FrameworkPackageUserState { -} diff --git a/stub/src/main/java/android/content/res/ApkAssets.java b/stub/src/main/java/android/content/res/ApkAssets.java new file mode 100644 index 00000000..6555ebfd --- /dev/null +++ b/stub/src/main/java/android/content/res/ApkAssets.java @@ -0,0 +1,29 @@ +package android.content.res; + +import android.content.res.loader.AssetsProvider; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.FileDescriptor; + +public class ApkAssets { + public static @NonNull ApkAssets loadFromPath(@NonNull String path, int flags, + @Nullable AssetsProvider assets) { + throw new RuntimeException("Stub!"); + } + + public static @NonNull ApkAssets loadFromFd(@NonNull FileDescriptor fd, + @NonNull String friendlyName, long offset, long length, int flags, + @Nullable AssetsProvider assets) { + throw new RuntimeException("Stub!"); + } + + public @NonNull XmlResourceParser openXml(@NonNull String fileName) { + throw new RuntimeException("Stub!"); + } + + public void close() { + throw new RuntimeException("Stub!"); + } +} diff --git a/stub/src/main/java/android/content/res/AssetManagerHidden.java b/stub/src/main/java/android/content/res/AssetManagerHidden.java new file mode 100644 index 00000000..9afa8956 --- /dev/null +++ b/stub/src/main/java/android/content/res/AssetManagerHidden.java @@ -0,0 +1,16 @@ +package android.content.res; + +import androidx.annotation.NonNull; + +import dev.rikka.tools.refine.RefineAs; + +@RefineAs(AssetManager.class) +public class AssetManagerHidden { + public AssetManagerHidden() { + throw new RuntimeException("Stub!"); + } + + public void setApkAssets(@NonNull ApkAssets[] apkAssets, boolean invalidateCaches) { + throw new RuntimeException("Stub!"); + } +} diff --git a/stub/src/main/java/android/content/res/XmlBlock.java b/stub/src/main/java/android/content/res/XmlBlock.java new file mode 100644 index 00000000..ce2c3972 --- /dev/null +++ b/stub/src/main/java/android/content/res/XmlBlock.java @@ -0,0 +1,23 @@ +package android.content.res; + +import androidx.annotation.RequiresApi; + +@RequiresApi(31) +public final class XmlBlock implements AutoCloseable { + public XmlBlock(byte[] data) { + throw new RuntimeException("Stub!"); + } + + public XmlBlock(byte[] data, int offset, int size) { + throw new RuntimeException("Stub!"); + } + + @Override + public void close() throws Exception { + throw new RuntimeException("Stub!"); + } + + public XmlResourceParser newParser() { + throw new RuntimeException("Stub!"); + } +} From 43fd52a28ea6ac331de5148cb1e0ea8924d365e6 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Mon, 8 Sep 2025 23:42:00 +0800 Subject: [PATCH 10/25] Optimize parsing speed --- app/build.gradle.kts | 9 +- app/src/full/kotlin/dev/sanmer/pi/App.kt | 17 + .../dev/sanmer/pi/model/IPackageInfo.kt | 0 .../pi/ui/screens/apps/AppsViewModel.kt | 2 +- .../pi/ui/screens/apps/component/AppItem.kt | 27 +- .../res/drawable/arrow_left.xml | 0 app/src/{main => full}/res/drawable/bug.xml | 0 .../{main => full}/res/drawable/search.xml | 0 app/src/lite/kotlin/dev/sanmer/pi/App.kt | 3 + app/src/main/AndroidManifest.xml | 5 - .../dev/sanmer/pi/{App.kt => BaseApp.kt} | 25 +- app/src/main/kotlin/dev/sanmer/pi/Const.kt | 5 +- .../dev/sanmer/pi/compat/MediaStoreCompat.kt | 106 ------ .../dev/sanmer/pi/compat/VersionCompat.kt | 87 ----- .../dev/sanmer/pi/di/FactoriesModule.kt | 11 + .../dev/sanmer/pi/factory/BundleFactory.kt | 92 +++++ .../dev/sanmer/pi/factory/VersionFactory.kt | 64 ++++ .../main/kotlin/dev/sanmer/pi/ktx/FileExt.kt | 5 - .../main/kotlin/dev/sanmer/pi/model/Task.kt | 52 --- .../dev/sanmer/pi/service/InstallService.kt | 156 ++++---- .../dev/sanmer/pi/service/ParseService.kt | 261 ------------- .../dev/sanmer/pi/ui/InstallActivity.kt | 79 +--- .../pi/ui/screens/install/InstallScreen.kt | 360 +++++++++++------- .../pi/ui/screens/install/InstallViewModel.kt | 227 +++++------ .../install/component/PackageInfoItem.kt | 45 +-- .../install/component/SelectUserItem.kt | 8 +- .../install/component/SplitConfigItem.kt | 6 +- app/src/main/res/drawable/rotate.xml | 12 + app/src/main/res/values-ar/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 3 - app/src/main/res/values-fa/strings.xml | 3 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-in/strings.xml | 3 - app/src/main/res/values-iw/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 3 - app/src/main/res/values-pt/strings.xml | 3 - app/src/main/res/values-ro/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-su/strings.xml | 3 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 3 - app/src/main/res/values/strings.xml | 3 - 42 files changed, 664 insertions(+), 1030 deletions(-) create mode 100644 app/src/full/kotlin/dev/sanmer/pi/App.kt rename app/src/{main => full}/kotlin/dev/sanmer/pi/model/IPackageInfo.kt (100%) rename app/src/{main => full}/res/drawable/arrow_left.xml (100%) rename app/src/{main => full}/res/drawable/bug.xml (100%) rename app/src/{main => full}/res/drawable/search.xml (100%) create mode 100644 app/src/lite/kotlin/dev/sanmer/pi/App.kt rename app/src/main/kotlin/dev/sanmer/pi/{App.kt => BaseApp.kt} (60%) delete mode 100644 app/src/main/kotlin/dev/sanmer/pi/compat/MediaStoreCompat.kt delete mode 100644 app/src/main/kotlin/dev/sanmer/pi/compat/VersionCompat.kt create mode 100644 app/src/main/kotlin/dev/sanmer/pi/di/FactoriesModule.kt create mode 100644 app/src/main/kotlin/dev/sanmer/pi/factory/BundleFactory.kt create mode 100644 app/src/main/kotlin/dev/sanmer/pi/factory/VersionFactory.kt delete mode 100644 app/src/main/kotlin/dev/sanmer/pi/ktx/FileExt.kt delete mode 100644 app/src/main/kotlin/dev/sanmer/pi/model/Task.kt delete mode 100644 app/src/main/kotlin/dev/sanmer/pi/service/ParseService.kt create mode 100644 app/src/main/res/drawable/rotate.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f2e8d6fb..ecf119d5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -125,18 +125,17 @@ dependencies { implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.viewmodel.savedstate) implementation(libs.androidx.navigation.compose) - implementation(libs.appiconloader) - implementation(libs.appiconloader.coil) - implementation(libs.coil.kt) - implementation(libs.coil.kt.compose) implementation(libs.koin.android) implementation(libs.koin.compose) implementation(libs.kotlinx.coroutines.android) implementation(libs.hiddenApiBypass) - "fullImplementation"(libs.sanmer.su) "fullImplementation"(libs.androidx.datastore.core) + "fullImplementation"(libs.appiconloader.coil) + "fullImplementation"(libs.coil.kt) + "fullImplementation"(libs.coil.kt.compose) "fullImplementation"(libs.kotlinx.serialization.protobuf) + "fullImplementation"(libs.sanmer.su) "liteImplementation"(libs.rikka.shizuku.api) "liteImplementation"(libs.rikka.shizuku.provider) diff --git a/app/src/full/kotlin/dev/sanmer/pi/App.kt b/app/src/full/kotlin/dev/sanmer/pi/App.kt new file mode 100644 index 00000000..799ebae0 --- /dev/null +++ b/app/src/full/kotlin/dev/sanmer/pi/App.kt @@ -0,0 +1,17 @@ +package dev.sanmer.pi + +import coil.ImageLoader +import coil.ImageLoaderFactory +import dev.sanmer.pi.ktx.dp +import me.zhanghai.android.appiconloader.coil.AppIconFetcher +import me.zhanghai.android.appiconloader.coil.AppIconKeyer + +class App : BaseApp(), ImageLoaderFactory { + override fun newImageLoader() = + ImageLoader.Builder(this) + .components { + add(AppIconKeyer()) + add(AppIconFetcher.Factory(40.dp, true, this@App)) + } + .build() +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/model/IPackageInfo.kt b/app/src/full/kotlin/dev/sanmer/pi/model/IPackageInfo.kt similarity index 100% rename from app/src/main/kotlin/dev/sanmer/pi/model/IPackageInfo.kt rename to app/src/full/kotlin/dev/sanmer/pi/model/IPackageInfo.kt diff --git a/app/src/full/kotlin/dev/sanmer/pi/ui/screens/apps/AppsViewModel.kt b/app/src/full/kotlin/dev/sanmer/pi/ui/screens/apps/AppsViewModel.kt index 59e8c0bb..de66528c 100644 --- a/app/src/full/kotlin/dev/sanmer/pi/ui/screens/apps/AppsViewModel.kt +++ b/app/src/full/kotlin/dev/sanmer/pi/ui/screens/apps/AppsViewModel.kt @@ -121,7 +121,7 @@ class AppsViewModel( loadState = LoadState.Ready( source.filter { if (key.isNotBlank()) { - it.appLabel.contains(key, ignoreCase = true) + it.label.contains(key, ignoreCase = true) || it.packageName.contains(key, ignoreCase = true) } else { true diff --git a/app/src/full/kotlin/dev/sanmer/pi/ui/screens/apps/component/AppItem.kt b/app/src/full/kotlin/dev/sanmer/pi/ui/screens/apps/component/AppItem.kt index 667aa822..daabb511 100644 --- a/app/src/full/kotlin/dev/sanmer/pi/ui/screens/apps/component/AppItem.kt +++ b/app/src/full/kotlin/dev/sanmer/pi/ui/screens/apps/component/AppItem.kt @@ -25,9 +25,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest +import dev.sanmer.pi.PackageInfoCompat.compileSdkVersion +import dev.sanmer.pi.PackageInfoCompat.minSdkVersion +import dev.sanmer.pi.PackageInfoCompat.targetSdkVersion import dev.sanmer.pi.R -import dev.sanmer.pi.compat.VersionCompat.getSdkVersion -import dev.sanmer.pi.compat.VersionCompat.versionStr +import dev.sanmer.pi.factory.VersionFactory.Default.orUnknown +import dev.sanmer.pi.factory.VersionFactory.Default.version import dev.sanmer.pi.model.IPackageInfo @Composable @@ -57,7 +60,7 @@ fun AppItem( modifier = Modifier.weight(1f), ) { Text( - text = pi.appLabel, + text = pi.label, style = MaterialTheme.typography.titleMedium ) @@ -66,20 +69,24 @@ fun AppItem( style = MaterialTheme.typography.bodyMedium ) - val versionStr by remember { - derivedStateOf { pi.versionStr } + val version by remember { + derivedStateOf { + (pi.longVersionCode to pi.versionName.orEmpty()).version + } } Text( - text = versionStr, + text = version, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline ) - val sdkVersion by remember { - derivedStateOf { pi.getSdkVersion(context) } - } Text( - text = sdkVersion, + text = stringResource( + R.string.sdk_versions, + pi.targetSdkVersion.orUnknown, + pi.minSdkVersion.orUnknown, + pi.compileSdkVersion.orUnknown + ), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.outline ) diff --git a/app/src/main/res/drawable/arrow_left.xml b/app/src/full/res/drawable/arrow_left.xml similarity index 100% rename from app/src/main/res/drawable/arrow_left.xml rename to app/src/full/res/drawable/arrow_left.xml diff --git a/app/src/main/res/drawable/bug.xml b/app/src/full/res/drawable/bug.xml similarity index 100% rename from app/src/main/res/drawable/bug.xml rename to app/src/full/res/drawable/bug.xml diff --git a/app/src/main/res/drawable/search.xml b/app/src/full/res/drawable/search.xml similarity index 100% rename from app/src/main/res/drawable/search.xml rename to app/src/full/res/drawable/search.xml diff --git a/app/src/lite/kotlin/dev/sanmer/pi/App.kt b/app/src/lite/kotlin/dev/sanmer/pi/App.kt new file mode 100644 index 00000000..c364031e --- /dev/null +++ b/app/src/lite/kotlin/dev/sanmer/pi/App.kt @@ -0,0 +1,3 @@ +package dev.sanmer.pi + +class App : BaseApp() \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2b6af2fd..57743313 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -47,11 +47,6 @@ - - - if (cursor.moveToFirst()) { - return cursor.getString( - cursor.getColumnIndexOrThrow(columnName) - ) - } - } - - return null - } - - fun Context.getOwnerPackageNameForUri(uri: Uri): String? { - require(uri.scheme == "content") { "Expected scheme = content" } - return when { - uri.authority == MediaStore.AUTHORITY -> { - contentResolver.queryString( - uri = uri, - columnName = MediaStore.MediaColumns.OWNER_PACKAGE_NAME - ) - } - - else -> { - uri.authority?.let { - packageManager.resolveContentProvider( - it, 0 - )?.packageName - } - } - } - } - - fun Context.getDisplayNameForUri(uri: Uri): String { - if (uri.scheme == "file") { - return uri.toFile().name - } - - require(uri.scheme == "content") { "Expected scheme = content" } - return contentResolver.queryString( - uri = uri, - columnName = MediaStore.MediaColumns.DISPLAY_NAME - ) ?: uri.toString() - } - - private fun getDocumentUri(context: Context, uri: Uri): Uri { - return when { - DocumentsContract.isTreeUri(uri) -> DocumentFile.fromTreeUri(context, uri)?.uri ?: uri - else -> uri - } - } - - fun Context.getPathForUri(uri: Uri): String { - if (uri.scheme == "file") { - return uri.toFile().path - } - - require(uri.scheme == "content") { "Expected scheme = content" } - contentResolver.openFileDescriptor( - getDocumentUri(this, uri), "r" - )?.use { - return Os.readlink("/proc/self/fd/${it.fd}") - } - - return uri.toString() - } - - fun Context.copyToFile(uri: Uri, file: File): Long? { - return contentResolver.openInputStream(uri)?.buffered()?.use { input -> - file.outputStream().use(input::copyTo) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/compat/VersionCompat.kt b/app/src/main/kotlin/dev/sanmer/pi/compat/VersionCompat.kt deleted file mode 100644 index 4b2e74ad..00000000 --- a/app/src/main/kotlin/dev/sanmer/pi/compat/VersionCompat.kt +++ /dev/null @@ -1,87 +0,0 @@ -package dev.sanmer.pi.compat - -import android.content.Context -import android.content.pm.PackageInfo -import android.text.format.Formatter -import dev.sanmer.pi.PackageInfoCompat.compileSdkVersion -import dev.sanmer.pi.PackageInfoCompat.isEmpty -import dev.sanmer.pi.R - -object VersionCompat { - private fun > Pair.comparator( - ctx: Context, - other: Pair - ): String { - val (value0, text0) = this - val (value1, text1) = other - return when { - value0 == value1 -> text0.ifEmpty { value0.toString() } - else -> ctx.getString( - R.string.comparator, - text0.ifEmpty { value0.toString() }, - text1.ifEmpty { value1.toString() } - ) - } - } - - private fun > T.comparator( - ctx: Context, - other: T, - ) = (this to "").comparator( - ctx = ctx, - other = other to "" - ) - - val PackageInfo.versionStr - inline get() = "$versionName (${longVersionCode})" - - private val PackageInfo.compileSdkDisplay - inline get() = if (compileSdkVersion != 0) compileSdkVersion.toString() else "?" - - fun PackageInfo.getSdkVersion(context: Context): String { - if (isEmpty) return "" - val appInfo = requireNotNull(applicationInfo) - return context.getString( - R.string.sdk_versions, - appInfo.targetSdkVersion.toString(), - appInfo.minSdkVersion.toString(), - compileSdkDisplay - ) - } - - fun PackageInfo.getVersionDiff(context: Context, other: PackageInfo): String { - if (isEmpty) return other.versionStr - if (other.isEmpty) return versionStr - return (longVersionCode to versionStr).comparator( - ctx = context, - other = other.longVersionCode to other.versionStr, - ) - } - - fun PackageInfo.getSdkVersionDiff(context: Context, other: PackageInfo): String { - if (isEmpty) return other.getSdkVersion(context) - if (other.isEmpty) return getSdkVersion(context) - val appInfo0 = requireNotNull(applicationInfo) - val appInfo1 = requireNotNull(other.applicationInfo) - return context.getString( - R.string.sdk_versions, - appInfo0.targetSdkVersion.comparator( - ctx = context, - other = appInfo1.targetSdkVersion - ), - appInfo0.minSdkVersion.comparator( - ctx = context, - other = appInfo1.minSdkVersion - ), - (compileSdkVersion to compileSdkDisplay).comparator( - ctx = context, - other = other.compileSdkVersion to other.compileSdkDisplay - ) - ) - } - - fun Long.fileSize(context: Context): String { - val value = Formatter.formatFileSize(context, this) - return context.getString(R.string.file_size, value) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/di/FactoriesModule.kt b/app/src/main/kotlin/dev/sanmer/pi/di/FactoriesModule.kt new file mode 100644 index 00000000..043c25ea --- /dev/null +++ b/app/src/main/kotlin/dev/sanmer/pi/di/FactoriesModule.kt @@ -0,0 +1,11 @@ +package dev.sanmer.pi.di + +import dev.sanmer.pi.factory.BundleFactory +import dev.sanmer.pi.factory.VersionFactory +import org.koin.core.module.dsl.factoryOf +import org.koin.dsl.module + +val Factories = module { + factoryOf(::VersionFactory) + factoryOf(::BundleFactory) +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/factory/BundleFactory.kt b/app/src/main/kotlin/dev/sanmer/pi/factory/BundleFactory.kt new file mode 100644 index 00000000..1ef7d2e2 --- /dev/null +++ b/app/src/main/kotlin/dev/sanmer/pi/factory/BundleFactory.kt @@ -0,0 +1,92 @@ +package dev.sanmer.pi.factory + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.os.ParcelFileDescriptor +import android.provider.MediaStore +import android.system.Os +import dev.sanmer.pi.Logger +import dev.sanmer.pi.parser.BundleInfo +import dev.sanmer.pi.parser.PackageInfoLite +import dev.sanmer.pi.parser.PackageParser +import dev.sanmer.pi.parser.PackageParser.toLite + +class BundleFactory( + private val context: Context +) { + private fun ContentResolver.queryString(uri: Uri, columnName: String): String? { + query( + uri, + arrayOf(columnName), + null, + null, + null + )?.use { cursor -> + if (cursor.moveToFirst()) { + return cursor.getString(cursor.getColumnIndexOrThrow(columnName)) + } + } + return null + } + + private fun getOwner(uri: Uri): String? { + require(uri.scheme == "content") { "Expected scheme = content" } + return when { + uri.authority == MediaStore.AUTHORITY -> { + context.contentResolver.queryString( + uri = uri, + columnName = MediaStore.MediaColumns.OWNER_PACKAGE_NAME + ) + } + + else -> { + uri.authority?.let { + context.packageManager.resolveContentProvider(it, 0) + ?.packageName + } + } + } + } + + private fun getPackageInfoLite(packageName: String): PackageInfoLite? { + return runCatching { + context.packageManager.getPackageInfo( + packageName, 0 + ) + }.getOrNull()?.toLite() + } + + fun openFd(uri: Uri): ParcelFileDescriptor { + require(uri.scheme == "content") { "Expected scheme = content" } + return requireNotNull(context.contentResolver.openFileDescriptor(uri, "r")) { + "Failed to open $uri" + } + } + + fun load(uri: Uri): Data { + val owner = getOwner(uri) + val bundleInfo = openFd(uri).use { + val path = Os.readlink("/proc/self/fd/${it.fd}") + logger.i("From $owner, Path $path") + PackageParser.loadBundleFromFd(it.fileDescriptor) + } + return Data( + uri = uri, + bundleInfo = bundleInfo, + currentInfo = getPackageInfoLite(bundleInfo.packageInfo.packageName), + sourceInfo = owner?.let(::getPackageInfoLite) + ) + } + + data class Data( + val uri: Uri, + val bundleInfo: BundleInfo, + val currentInfo: PackageInfoLite?, + val sourceInfo: PackageInfoLite?, + ) + + companion object Default { + private val logger = Logger.Android("BundleFactory") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/factory/VersionFactory.kt b/app/src/main/kotlin/dev/sanmer/pi/factory/VersionFactory.kt new file mode 100644 index 00000000..dc337aa9 --- /dev/null +++ b/app/src/main/kotlin/dev/sanmer/pi/factory/VersionFactory.kt @@ -0,0 +1,64 @@ +package dev.sanmer.pi.factory + +import android.content.Context +import android.text.format.Formatter +import dev.sanmer.pi.R + +class VersionFactory( + private val context: Context +) { + private val Pair.value inline get() = second ?: first.toString() + + private infix fun > Pair.compare( + other: Pair + ) = when { + first == other.second -> value + else -> context.getString(R.string.comparator, value, other.value) + } + + private infix fun > T.compare( + other: T + ) = when { + this == other -> toString() + else -> context.getString(R.string.comparator, toString(), other.toString()) + } + + fun sdkVersions(target: Int, min: Int, compile: Int): String { + return context.getString( + R.string.sdk_versions, + target.orUnknown, min.orUnknown, compile.orUnknown + ) + } + + fun versionDiff(that: Pair, other: Pair): String { + if (that.first <= 0) return other.version + if (other.first <= 0) return that.version + return that compare other + } + + fun sdkVersionsDiff( + target: Pair, + min: Pair, + compile: Pair + ): String { + if (min.first <= 0) return sdkVersions(target.second, min.second, compile.second) + if (min.second <= 0) return sdkVersions(target.first, min.first, compile.first) + return context.getString( + R.string.sdk_versions, + target.first compare target.second, + min.first compare min.second, + compile.first compare compile.second + ) + } + + fun fileSize(sizeBytes: Long): String { + val value = Formatter.formatFileSize(context, sizeBytes) + return context.getString(R.string.file_size, value) + } + + companion object Default { + val Pair.version inline get() = "$second ($first)" + + val Int.orUnknown inline get() = if (this > 0) "$this" else "?" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/ktx/FileExt.kt b/app/src/main/kotlin/dev/sanmer/pi/ktx/FileExt.kt deleted file mode 100644 index aac9faae..00000000 --- a/app/src/main/kotlin/dev/sanmer/pi/ktx/FileExt.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.sanmer.pi.ktx - -import java.io.File - -fun File?.temp() = File(this, System.currentTimeMillis().toString()) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/model/Task.kt b/app/src/main/kotlin/dev/sanmer/pi/model/Task.kt deleted file mode 100644 index 59ca3213..00000000 --- a/app/src/main/kotlin/dev/sanmer/pi/model/Task.kt +++ /dev/null @@ -1,52 +0,0 @@ -package dev.sanmer.pi.model - -import android.content.Intent -import android.content.pm.PackageInfo -import android.os.Parcelable -import dev.sanmer.pi.PackageParserCompat -import dev.sanmer.pi.bundle.SplitConfig -import dev.sanmer.pi.ktx.parcelable -import kotlinx.parcelize.Parcelize -import java.io.File - -sealed class Task : Parcelable { - abstract val archivePath: File - abstract val archiveInfo: PackageInfo - abstract val userId: Int - abstract val sourceInfo: PackageInfo - - @Parcelize - data class Apk( - override val archivePath: File, - override val archiveInfo: PackageInfo, - override val userId: Int, - override val sourceInfo: PackageInfo - ) : Task() - - @Parcelize - data class AppBundle( - override val archivePath: File, - override val archiveInfo: PackageInfo, - override val userId: Int, - override val sourceInfo: PackageInfo, - val splitConfigs: List - ) : Task() { - val baseFile get() = File(archivePath, PackageParserCompat.BASE_APK) - - val archiveFiles - get() = splitConfigs.map { it.file } - .toMutableList().apply { - add(0, baseFile) - } - } - - companion object Default { - const val EXTRA_TASK = "dev.sanmer.pi.extra.TASK" - - fun Intent.putTask(value: Task) = - putExtra(EXTRA_TASK, value) - - inline val Intent.taskOrNull: Task? - get() = parcelable(EXTRA_TASK) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/service/InstallService.kt b/app/src/main/kotlin/dev/sanmer/pi/service/InstallService.kt index e4ea294d..3daf0238 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/service/InstallService.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/service/InstallService.kt @@ -5,11 +5,14 @@ import android.app.Notification import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.content.pm.PackageInfo import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller.SessionInfo +import android.content.pm.PackageManager import android.content.pm.ServiceInfo import android.graphics.Bitmap +import android.net.Uri +import android.os.ParcelFileDescriptor +import android.os.Parcelable import android.os.Process import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -20,16 +23,15 @@ import dev.sanmer.pi.Const import dev.sanmer.pi.ContextCompat.userId import dev.sanmer.pi.Logger import dev.sanmer.pi.R -import dev.sanmer.pi.bundle.SplitConfig import dev.sanmer.pi.compat.BuildCompat import dev.sanmer.pi.compat.PermissionCompat import dev.sanmer.pi.delegate.PackageInstallerDelegate import dev.sanmer.pi.delegate.PackageInstallerDelegate.Default.commit -import dev.sanmer.pi.delegate.PackageInstallerDelegate.Default.write -import dev.sanmer.pi.ktx.dp -import dev.sanmer.pi.model.Task -import dev.sanmer.pi.model.Task.Default.putTask -import dev.sanmer.pi.model.Task.Default.taskOrNull +import dev.sanmer.pi.delegate.PackageInstallerDelegate.Default.writeFd +import dev.sanmer.pi.delegate.PackageInstallerDelegate.Default.writeZip +import dev.sanmer.pi.factory.BundleFactory +import dev.sanmer.pi.ktx.parcelable +import dev.sanmer.pi.parser.PackageInfoLite import dev.sanmer.pi.repository.PreferenceRepository import dev.sanmer.pi.repository.ServiceRepository import kotlinx.coroutines.Dispatchers @@ -39,16 +41,15 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import me.zhanghai.android.appiconloader.AppIconLoader +import kotlinx.parcelize.Parcelize import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import java.io.File import kotlin.time.Duration.Companion.seconds class InstallService : LifecycleService(), KoinComponent, PackageInstallerDelegate.SessionCallback { private val preferenceRepository by inject() private val serviceRepository by inject() - private val appIconLoader by lazy { AppIconLoader(45.dp, true, this) } + private val bundleFactory by inject() private val nm by lazy { NotificationManagerCompat.from(this) } private val pm by lazy { serviceRepository.getPackageManager() } private val pi by lazy { serviceRepository.getPackageInstaller() } @@ -58,24 +59,12 @@ class InstallService : LifecycleService(), KoinComponent, PackageInstallerDelega init { lifecycleScope.launch { while (currentCoroutineContext().isActive) { - if (pendingTasks.isEmpty()) stopSelf() + if (pendingUris.isEmpty()) stopSelf() delay(5.seconds) } } } - override fun onCreated(sessionId: Int) { - val session = pi.getSessionInfo(sessionId) - logger.i("onCreated<$sessionId>: ${session?.appPackageName}") - - notifyProgress( - id = sessionId, - appLabel = session?.label ?: sessionId.toString(), - appIcon = session?.appIcon, - progress = 0f - ) - } - override fun onProgressChanged(sessionId: Int, progress: Float) { val session = pi.getSessionInfo(sessionId) @@ -112,41 +101,46 @@ class InstallService : LifecycleService(), KoinComponent, PackageInstallerDelega override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { lifecycleScope.launch(Dispatchers.IO) { val task = intent?.taskOrNull ?: return@launch - - install(task) - task.archivePath.deleteRecursively() - pendingTasks.remove(task.archivePath) + bundleFactory.openFd(task.uri).use { + install(task, it) + } + pendingUris.remove(task.uri) } - return super.onStartCommand(intent, flags, startId) } - private suspend fun install(task: Task) = withContext(Dispatchers.IO) { - val appIcon = task.archiveInfo.applicationInfo?.let(appIconLoader::loadIcon) - val appLabel = task.archiveInfo.applicationInfo?.loadLabel(packageManager) - ?: task.archiveInfo.packageName - + private suspend fun install( + task: Task, + fd: ParcelFileDescriptor + ) = withContext(Dispatchers.IO) { val preference = preferenceRepository.data.first() - val originatingUid = getPackageUid( - preference.requester.ifEmpty { task.sourceInfo.packageName } - ) + val originating = preference.requester.ifEmpty { task.sourceInfo?.packageName } + val originatingUid = originating?.let(::getPackageUid) ?: Process.INVALID_UID + pi.setInstallerPackageName(preference.executor) pi.setUserId(task.userId) val params = createSessionParams() - params.setAppIcon(appIcon) - params.setAppLabel(appLabel) + params.setAppIcon(task.archiveInfo.iconOrDefault) + params.setAppLabel(task.archiveInfo.labelOrDefault) params.setAppPackageName(task.archiveInfo.packageName) if (originatingUid != Process.INVALID_UID) { params.setOriginatingUid(originatingUid) } val sessionId = pi.createSession(params) - val session = pi.openSession(sessionId) + notifyProgress( + id = sessionId, + appLabel = task.archiveInfo.labelOrDefault, + appIcon = task.archiveInfo.iconOrDefault, + progress = 0f + ) - when (task) { - is Task.Apk -> session.write(task.archivePath) - is Task.AppBundle -> session.write(task.archiveFiles) + val session = pi.openSession(sessionId) + if (task.fileNames.isEmpty()) { + session.writeFd(task.archiveInfo.packageName, fd) + } else { + session.writeZip(task.fileNames, fd) } val result = session.commit() @@ -159,27 +153,27 @@ class InstallService : LifecycleService(), KoinComponent, PackageInstallerDelega PackageInstaller.STATUS_SUCCESS -> { notifyOptimizing( id = sessionId, - appLabel = appLabel, - appIcon = appIcon + appLabel = task.archiveInfo.labelOrDefault, + appIcon = task.archiveInfo.iconOrDefault ) optimize(task.archiveInfo.packageName) notifySuccess( id = sessionId, - appLabel = appLabel, - appIcon = appIcon, + appLabel = task.archiveInfo.labelOrDefault, + appIcon = task.archiveInfo.iconOrDefault, packageName = task.archiveInfo.packageName ) } else -> { val msg = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - logger.e("onFailed<${task.archiveInfo.packageName}>: $msg") + logger.e("Failed to install ${task.archiveInfo.packageName}, $msg") notifyFailure( id = sessionId, - appLabel = appLabel, - appIcon = appIcon + appLabel = task.archiveInfo.labelOrDefault, + appIcon = task.archiveInfo.iconOrDefault, ) } } @@ -201,6 +195,7 @@ class InstallService : LifecycleService(), KoinComponent, PackageInstallerDelega PackageInstaller.SessionParams.MODE_FULL_INSTALL ) + params.setInstallReason(PackageManager.INSTALL_REASON_USER) params.installFlags = with(PackageInstallerDelegate.SessionParams) { val flags = params.installFlags or INSTALL_ALLOW_TEST or @@ -208,7 +203,8 @@ class InstallService : LifecycleService(), KoinComponent, PackageInstallerDelega INSTALL_REQUEST_DOWNGRADE if (BuildCompat.atLeastU) { - flags or INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK + flags or INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK or + INSTALL_REQUEST_UPDATE_OWNERSHIP } else { flags } @@ -325,51 +321,39 @@ class InstallService : LifecycleService(), KoinComponent, PackageInstallerDelega ) nm.notify(id, notification) } + @Parcelize + private data class Task( + val uri: Uri, + val archiveInfo: PackageInfoLite, + val fileNames: List, + val sourceInfo: PackageInfoLite?, + val userId: Int + ) : Parcelable + companion object Default { private const val GROUP_KEY = "dev.sanmer.pi.INSTALL_SERVICE_GROUP_KEY" + private const val EXTRA_TASK = "dev.sanmer.pi.extra.TASK" - private val pendingTasks = mutableListOf() + private fun Intent.putTask(value: Task) = + putExtra(EXTRA_TASK, value) - fun apk( - context: Context, - archivePath: File, - archiveInfo: PackageInfo, - sourceInfo: PackageInfo, - userId: Int - ) { - val task = Task.Apk( - archivePath = archivePath, - archiveInfo = archiveInfo, - userId = userId, - sourceInfo = sourceInfo - ) - pendingTasks.add(task.archivePath) - context.startService( - Intent(context, InstallService::class.java).also { - it.putTask(task) - } - ) - } + private inline val Intent.taskOrNull: Task? + get() = parcelable(EXTRA_TASK) - fun appBundle( + private val pendingUris = mutableListOf() + + fun start( context: Context, - archivePath: File, - archiveInfo: PackageInfo, - splitConfigs: List, - userId: Int, - sourceInfo: PackageInfo + uri: Uri, + archiveInfo: PackageInfoLite, + fileNames: List, + sourceInfo: PackageInfoLite? = null, + userId: Int = context.userId ) { - val task = Task.AppBundle( - archivePath = archivePath, - archiveInfo = archiveInfo, - splitConfigs = splitConfigs, - userId = userId, - sourceInfo = sourceInfo - ) - pendingTasks.add(task.archivePath) + pendingUris.add(uri) context.startService( Intent(context, InstallService::class.java).also { - it.putTask(task) + it.putTask(Task(uri, archiveInfo, fileNames, sourceInfo, userId)) } ) } diff --git a/app/src/main/kotlin/dev/sanmer/pi/service/ParseService.kt b/app/src/main/kotlin/dev/sanmer/pi/service/ParseService.kt deleted file mode 100644 index da467b39..00000000 --- a/app/src/main/kotlin/dev/sanmer/pi/service/ParseService.kt +++ /dev/null @@ -1,261 +0,0 @@ -package dev.sanmer.pi.service - -import android.Manifest -import android.app.Notification -import android.content.Context -import android.content.Intent -import android.content.pm.PackageInfo -import android.content.pm.ServiceInfo -import android.net.Uri -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.app.ServiceCompat -import androidx.lifecycle.LifecycleService -import androidx.lifecycle.lifecycleScope -import dev.sanmer.pi.BuildConfig -import dev.sanmer.pi.Const -import dev.sanmer.pi.ContextCompat.userId -import dev.sanmer.pi.Logger -import dev.sanmer.pi.PackageInfoCompat.orEmpty -import dev.sanmer.pi.PackageParserCompat -import dev.sanmer.pi.R -import dev.sanmer.pi.compat.BuildCompat -import dev.sanmer.pi.compat.MediaStoreCompat.copyToFile -import dev.sanmer.pi.compat.MediaStoreCompat.getOwnerPackageNameForUri -import dev.sanmer.pi.compat.MediaStoreCompat.getPathForUri -import dev.sanmer.pi.compat.PermissionCompat -import dev.sanmer.pi.delegate.AppOpsManagerDelegate -import dev.sanmer.pi.ktx.temp -import dev.sanmer.pi.repository.PreferenceRepository -import dev.sanmer.pi.repository.ServiceRepository -import dev.sanmer.pi.ui.InstallActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koin.android.ext.android.inject -import org.koin.core.component.KoinComponent -import java.io.File -import kotlin.time.Duration.Companion.seconds - -class ParseService : LifecycleService(), KoinComponent { - private val serviceRepository by inject() - private val preferenceRepository by inject() - - private val nm by lazy { NotificationManagerCompat.from(this) } - private val pm by lazy { serviceRepository.getPackageManager() } - private val aom by lazy { serviceRepository.getAppOpsManager() } - - private val logger = Logger.Android("ParseService") - - init { - lifecycleScope.launch { - while (currentCoroutineContext().isActive) { - if (pendingUris.isEmpty()) stopSelf() - delay(5.seconds) - } - } - } - - override fun onCreate() { - logger.d("onCreate") - super.onCreate() - setForeground() - } - - override fun onTimeout(startId: Int) { - stopSelf(startId) - super.onTimeout(startId) - } - - override fun onDestroy() { - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - logger.d("onDestroy") - super.onDestroy() - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - lifecycleScope.launch(Dispatchers.IO) { - val uri = intent?.data ?: return@launch - val state = serviceRepository.state.first { !it.isPending } - val preference = preferenceRepository.data.first() - - if (state.isSucceed) { - parse(uri, preference.automatic) - pendingUris.remove(uri) - } else { - notifyFailure( - id = Const.NOTIFICATION_ID_PARSE, - title = getText(R.string.parsing_service), - text = getText(R.string.settings_service_not_running) - ) - pendingUris.clear() - stopSelf() - } - } - return super.onStartCommand(intent, flags, startId) - } - - private suspend fun parse(uri: Uri, auto: Boolean) = withContext(Dispatchers.IO) { - val packageName = getOwnerPackageNameForUri(uri) - val sourceInfo = packageName?.let(::getPackageInfo).orEmpty() - val path = File(getPathForUri(uri)) - logger.i("from: $packageName, path: $path") - - notifyParsing( - id = uri.hashCode(), - filename = path.name - ) - - val archivePath = externalCacheDir.temp() - copyToFile(uri, archivePath) - - val isSucceed = if (parseApk(archivePath, sourceInfo, auto)) true - else parseZip(archivePath, sourceInfo, auto) - - if (isSucceed) { - nm.cancel(uri.hashCode()) - } else { - notifyFailure( - id = uri.hashCode(), - title = path.name, - text = getText(R.string.message_parsing_failed) - ) - } - } - - private fun parseApk(archivePath: File, sourceInfo: PackageInfo, auto: Boolean): Boolean { - return PackageParserCompat.parsePackage(archivePath, 0)?.let { pi -> - val isPIUpdate = pi.packageName == BuildConfig.APPLICATION_ID - && pi.longVersionCode > BuildConfig.VERSION_CODE - val isSelfUpdate = auto && pi.packageName == sourceInfo.packageName - && pi.longVersionCode > sourceInfo.longVersionCode - val isAuthorizedUpdate = auto && sourceInfo.isAuthorized() - && pi.longVersionCode > getPackageInfo(pi.packageName).longVersionCode - - if (isPIUpdate || isSelfUpdate || isAuthorizedUpdate) { - InstallService.apk( - context = applicationContext, - archivePath = archivePath, - archiveInfo = pi, - userId = userId, - sourceInfo = sourceInfo - ) - } else { - InstallActivity.apk( - context = applicationContext, - archivePath = archivePath, - archiveInfo = pi, - sourceInfo = sourceInfo - ) - } - } != null - } - - private fun parseZip(archivePath: File, sourceInfo: PackageInfo, auto: Boolean): Boolean { - val archiveDir = externalCacheDir.temp().apply { mkdirs() } - val isSucceed = PackageParserCompat.parseAppBundle(archivePath, 0, archiveDir)?.let { bi -> - InstallActivity.appBundle( - context = applicationContext, - archivePath = archiveDir, - archiveInfo = bi.baseInfo, - splitConfigs = bi.splitConfigs, - sourceInfo = sourceInfo - ) - } != null - - archivePath.delete() - return if (!isSucceed) { - val isNew = archiveDir.listFiles { f -> f.extension == "apk" }?.firstOrNull() - ?.renameTo(archivePath) == true - if (isNew) parseApk(archivePath, sourceInfo, auto) else false - archiveDir.deleteRecursively() - } else true - } - - private fun getPackageInfo(packageName: String): PackageInfo { - return runCatching { - pm.getPackageInfo(packageName, 0, userId) - }.getOrNull() ?: PackageInfo() - } - - private fun PackageInfo.isAuthorized() = aom.checkOpNoThrow( - op = AppOpsManagerDelegate.OP_REQUEST_INSTALL_PACKAGES, - packageInfo = this - ).isAllowed - - private fun setForeground() { - val notification = newNotificationBuilder() - .setContentTitle(getText(R.string.parsing_service)) - .setSilent(true) - .setOngoing(true) - .setGroup(GROUP_KEY) - .setGroupSummary(true) - .build() - - ServiceCompat.startForeground( - this, - Const.NOTIFICATION_ID_PARSE, - notification, - ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC - ) - } - - private fun notifyParsing( - id: Int, - filename: String - ) { - val notification = newNotificationBuilder() - .setContentTitle(filename) - .setContentText(getString(R.string.message_parsing)) - .setSilent(true) - .setOngoing(true) - .setGroup(GROUP_KEY) - .build() - - notify(id, notification) - } - - private fun notifyFailure( - id: Int, - title: CharSequence?, - text: CharSequence - ) { - val notification = newNotificationBuilder() - .setContentTitle(title) - .setContentText(text) - .build() - - notify(id, notification) - } - - private fun newNotificationBuilder() = - NotificationCompat.Builder(applicationContext, Const.CHANNEL_ID_PARSE) - .setSmallIcon(R.drawable.launcher_outline) - - private fun notify(id: Int, notification: Notification) { - if ( - !BuildCompat.atLeastT - || PermissionCompat.checkPermission(this, Manifest.permission.POST_NOTIFICATIONS) - ) nm.notify(id, notification) - } - - companion object Default { - private const val GROUP_KEY = "dev.sanmer.pi.PARSE_SERVICE_GROUP_KEY" - - private val pendingUris = mutableListOf() - - fun start(context: Context, uri: Uri) { - pendingUris.add(uri) - context.startService( - Intent(context, ParseService::class.java).also { - it.data = uri - it.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION - } - ) - } - } -} diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/InstallActivity.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/InstallActivity.kt index 1e718ce6..35f0f86b 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/InstallActivity.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/InstallActivity.kt @@ -1,29 +1,21 @@ package dev.sanmer.pi.ui import android.Manifest -import android.content.Context -import android.content.Intent -import android.content.pm.PackageInfo import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import dev.sanmer.pi.ContextCompat.userId +import androidx.compose.runtime.CompositionLocalProvider import dev.sanmer.pi.Logger -import dev.sanmer.pi.bundle.SplitConfig import dev.sanmer.pi.compat.BuildCompat import dev.sanmer.pi.compat.PermissionCompat -import dev.sanmer.pi.model.Task -import dev.sanmer.pi.model.Task.Default.putTask -import dev.sanmer.pi.model.Task.Default.taskOrNull -import dev.sanmer.pi.service.ParseService import dev.sanmer.pi.ui.main.MainViewModel import dev.sanmer.pi.ui.main.MainViewModel.LoadState +import dev.sanmer.pi.ui.provider.LocalPreference import dev.sanmer.pi.ui.screens.install.InstallScreen import dev.sanmer.pi.ui.screens.install.InstallViewModel import dev.sanmer.pi.ui.theme.AppTheme import org.koin.androidx.viewmodel.ext.android.viewModel -import java.io.File class InstallActivity : ComponentActivity() { private val main by viewModel() @@ -44,27 +36,24 @@ class InstallActivity : ComponentActivity() { } val uri = intent.data - if (uri != null) { - ParseService.start(this, uri) + if (uri == null) { finish() return - } - - val task = intent.taskOrNull - if (task != null) { - viewModel.load(task) } else { - finish() - return + viewModel.loadFromUri(uri) } setContent { when (main.loadState) { LoadState.Pending -> {} - is LoadState.Ready -> AppTheme( - darkMode = main.preference.darkMode.isDarkTheme + is LoadState.Ready -> CompositionLocalProvider( + LocalPreference provides main.preference ) { - InstallScreen() + AppTheme( + darkMode = main.preference.darkMode.isDarkTheme + ) { + InstallScreen() + } } } } @@ -74,50 +63,4 @@ class InstallActivity : ComponentActivity() { logger.d("onDestroy") super.onDestroy() } - - companion object Default { - fun apk( - context: Context, - archivePath: File, - archiveInfo: PackageInfo, - sourceInfo: PackageInfo, - userId: Int = context.userId - ) { - val task = Task.Apk( - archivePath = archivePath, - archiveInfo = archiveInfo, - userId = userId, - sourceInfo = sourceInfo - ) - context.startActivity( - Intent(context, InstallActivity::class.java).also { - it.putTask(task) - it.flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - ) - } - - fun appBundle( - context: Context, - archivePath: File, - archiveInfo: PackageInfo, - splitConfigs: List, - sourceInfo: PackageInfo, - userId: Int = context.userId - ) { - val task = Task.AppBundle( - archivePath = archivePath, - archiveInfo = archiveInfo, - splitConfigs = splitConfigs, - userId = userId, - sourceInfo = sourceInfo - ) - context.startActivity( - Intent(context, InstallActivity::class.java).also { - it.putTask(task) - it.flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - ) - } - } } \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/InstallScreen.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/InstallScreen.kt index c32336f0..8d154bd5 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/InstallScreen.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/InstallScreen.kt @@ -1,6 +1,6 @@ package dev.sanmer.pi.ui.screens.install -import androidx.activity.compose.BackHandler +import android.content.pm.UserInfo import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -8,14 +8,20 @@ import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -33,6 +39,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext @@ -40,10 +47,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import dev.sanmer.pi.R -import dev.sanmer.pi.bundle.SplitConfig import dev.sanmer.pi.ktx.finishActivity +import dev.sanmer.pi.parser.SplitConfig import dev.sanmer.pi.ui.ktx.isScrollingUp import dev.sanmer.pi.ui.ktx.plus +import dev.sanmer.pi.ui.provider.LocalPreference +import dev.sanmer.pi.ui.screens.install.InstallViewModel.LoadState import dev.sanmer.pi.ui.screens.install.component.PackageInfoItem import dev.sanmer.pi.ui.screens.install.component.SelectUserItem import dev.sanmer.pi.ui.screens.install.component.SplitConfigItem @@ -55,197 +64,268 @@ fun InstallScreen( viewModel: InstallViewModel = koinViewModel() ) { val context = LocalContext.current + val preference = LocalPreference.current val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val listState = rememberLazyListState() val isScrollingUp by listState.isScrollingUp() - val onDeny = { - viewModel.deleteCache() - context.finishActivity() - } - val onStart = { - viewModel.install() - context.finishActivity() - } - - val featureConfigs by remember { - derivedStateOf { viewModel.splitConfigs.filterIsInstance() } - } - val targetConfigs by remember { - derivedStateOf { viewModel.splitConfigs.filterIsInstance() } - } - val densityConfigs by remember { - derivedStateOf { viewModel.splitConfigs.filterIsInstance() } - } - val languageConfigs by remember { - derivedStateOf { viewModel.splitConfigs.filterIsInstance() } - } - val unspecifiedConfigs by remember { - derivedStateOf { viewModel.splitConfigs.filterIsInstance() } - } - var select by remember { mutableStateOf(false) } if (select) SelectUserItem( onDismiss = { select = false }, user = viewModel.user, users = viewModel.users, - onChange = viewModel::updateUser + onChange = viewModel::user::set ) - BackHandler(onBack = onDeny) - Scaffold( topBar = { TopBar( + isReady = viewModel.isReady, user = viewModel.user, onSelectUer = { select = true }, - onDeny = onDeny, scrollBehavior = scrollBehavior ) }, floatingActionButton = { AnimatedVisibility( - visible = isScrollingUp, + visible = isScrollingUp && viewModel.isBundleReady, enter = fadeIn() + scaleIn(), exit = scaleOut() + fadeOut() ) { - ActionButton(onStart = onStart) + FloatingActionButton( + onClick = { + if (viewModel.isServiceReady) { + viewModel.start(context) + context.finishActivity() + } else { + viewModel.recreate(preference.provider) + } + } + ) { + Icon( + painter = painterResource( + if (viewModel.isServiceReady) + R.drawable.player_play + else + R.drawable.rotate + ), + contentDescription = null + ) + } } } ) { contentPadding -> - LazyColumn( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - state = listState, - verticalArrangement = Arrangement.spacedBy(10.dp), - contentPadding = contentPadding + PaddingValues(horizontal = 20.dp, vertical = 10.dp) - ) { - if (viewModel.hasSourceInfo) { - item { - TittleItem(text = stringResource(R.string.install_requester_title)) - } - item { - PackageInfoItem(packageInfo = viewModel.sourceInfo) - } - } + when (val state = viewModel.loadState) { + is LoadState.Failure -> Failed( + error = state.error, + contentPadding = contentPadding, + scrollBehavior = scrollBehavior + ) + + LoadState.Pending -> Loading( + contentPadding = contentPadding + ) + + is LoadState.Success -> InstallContent( + loadState = state, + size = viewModel.size, + splitConfigs = viewModel.splitConfigs, + isRequiredConfig = viewModel::isRequiredConfig, + toggleSplitConfig = viewModel::toggleSplitConfig, + listState = listState, + contentPadding = contentPadding, + scrollBehavior = scrollBehavior + ) + } + } +} + +@Composable +private fun Failed( + error: Throwable, + contentPadding: PaddingValues, + scrollBehavior: TopAppBarScrollBehavior +) { + val text by remember { + derivedStateOf { error.stackTraceToString() } + } + + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .nestedScroll(scrollBehavior.nestedScrollConnection) + .verticalScroll(rememberScrollState()) + .padding(contentPadding + PaddingValues(all = 20.dp)) + ) +} + +@Composable +private fun Loading( + contentPadding: PaddingValues +) { + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(48.dp), + strokeWidth = 5.dp + ) + } +} + +@Composable +private fun InstallContent( + loadState: LoadState.Success, + size: String, + splitConfigs: List, + isRequiredConfig: (SplitConfig) -> Boolean, + toggleSplitConfig: (SplitConfig) -> Unit, + listState: LazyListState, + contentPadding: PaddingValues, + scrollBehavior: TopAppBarScrollBehavior +) { + val featureConfigs by remember { + derivedStateOf { splitConfigs.filterIsInstance() } + } + val targetConfigs by remember { + derivedStateOf { splitConfigs.filterIsInstance() } + } + val densityConfigs by remember { + derivedStateOf { splitConfigs.filterIsInstance() } + } + val languageConfigs by remember { + derivedStateOf { splitConfigs.filterIsInstance() } + } + val unspecifiedConfigs by remember { + derivedStateOf { splitConfigs.filterIsInstance() } + } + LazyColumn( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + state = listState, + verticalArrangement = Arrangement.spacedBy(10.dp), + contentPadding = contentPadding + PaddingValues(horizontal = 20.dp, vertical = 10.dp) + ) { + if (loadState.sourceInfo != null) { item { - TittleItem(text = stringResource(R.string.install_package_title)) + TittleItem(text = stringResource(R.string.install_requester_title)) } item { - PackageInfoItem( - packageInfo = viewModel.archiveInfo, - versionDiff = viewModel.versionDiff, - sdkVersionDiff = viewModel.sdkVersionDiff, - fileSize = viewModel.fileSize, - ) + PackageInfoItem(loadState.sourceInfo) } + } - if (featureConfigs.isNotEmpty()) { - item { - TittleItem(text = stringResource(R.string.install_config_feature_title)) - } - items( - items = featureConfigs, - key = { it.file.name } - ) { - SplitConfigItem( - config = it, - isRequiredConfig = viewModel::isRequiredConfig, - toggleSplitConfig = viewModel::toggleSplitConfig - ) - } + item { + TittleItem(text = stringResource(R.string.install_package_title)) + } + item { + PackageInfoItem( + pi = loadState.archiveInfo, + size = size + ) + } + + if (featureConfigs.isNotEmpty()) { + item { + TittleItem(text = stringResource(R.string.install_config_feature_title)) } + items( + items = featureConfigs, + key = { it.fileName } + ) { + SplitConfigItem( + config = it, + isRequiredConfig = isRequiredConfig, + toggleSplitConfig = toggleSplitConfig + ) + } + } - if (targetConfigs.isNotEmpty()) { - item { - TittleItem(text = stringResource(R.string.install_config_abi_title)) - } - items( - items = targetConfigs, - key = { it.file.name } - ) { - SplitConfigItem( - config = it, - isRequiredConfig = viewModel::isRequiredConfig, - toggleSplitConfig = viewModel::toggleSplitConfig - ) - } + if (targetConfigs.isNotEmpty()) { + item { + TittleItem(text = stringResource(R.string.install_config_abi_title)) } + items( + items = targetConfigs, + key = { it.fileName } + ) { + SplitConfigItem( + config = it, + isRequiredConfig = isRequiredConfig, + toggleSplitConfig = toggleSplitConfig + ) + } + } - if (densityConfigs.isNotEmpty()) { - item { - TittleItem(text = stringResource(R.string.install_config_density_title)) - } - items( - items = densityConfigs, - key = { it.file.name } - ) { - SplitConfigItem( - config = it, - isRequiredConfig = viewModel::isRequiredConfig, - toggleSplitConfig = viewModel::toggleSplitConfig - ) - } + if (densityConfigs.isNotEmpty()) { + item { + TittleItem(text = stringResource(R.string.install_config_density_title)) + } + items( + items = densityConfigs, + key = { it.fileName } + ) { + SplitConfigItem( + config = it, + isRequiredConfig = isRequiredConfig, + toggleSplitConfig = toggleSplitConfig + ) } + } - if (languageConfigs.isNotEmpty()) { - item { - TittleItem(text = stringResource(R.string.install_config_language_title)) - } - items( - items = languageConfigs, - key = { it.file.name } - ) { - SplitConfigItem( - config = it, - isRequiredConfig = viewModel::isRequiredConfig, - toggleSplitConfig = viewModel::toggleSplitConfig - ) - } + if (languageConfigs.isNotEmpty()) { + item { + TittleItem(text = stringResource(R.string.install_config_language_title)) } + items( + items = languageConfigs, + key = { it.fileName } + ) { + SplitConfigItem( + config = it, + isRequiredConfig = isRequiredConfig, + toggleSplitConfig = toggleSplitConfig + ) + } + } - if (unspecifiedConfigs.isNotEmpty()) { - item { - TittleItem(text = stringResource(R.string.install_config_unspecified_title)) - } - items( - items = unspecifiedConfigs, - key = { it.file.name } - ) { - SplitConfigItem( - config = it, - isRequiredConfig = viewModel::isRequiredConfig, - toggleSplitConfig = viewModel::toggleSplitConfig - ) - } + if (unspecifiedConfigs.isNotEmpty()) { + item { + TittleItem(text = stringResource(R.string.install_config_unspecified_title)) + } + items( + items = unspecifiedConfigs, + key = { it.fileName } + ) { + SplitConfigItem( + config = it, + isRequiredConfig = isRequiredConfig, + toggleSplitConfig = toggleSplitConfig + ) } } } } -@Composable -private fun ActionButton( - onStart: () -> Unit -) = FloatingActionButton( - onClick = onStart -) { - Icon( - painter = painterResource(id = R.drawable.player_play), - contentDescription = null - ) -} - @Composable private fun TopBar( - user: InstallViewModel.UserInfoCompat, + isReady: Boolean, + user: UserInfo, onSelectUer: () -> Unit, - onDeny: () -> Unit, scrollBehavior: TopAppBarScrollBehavior ) = TopAppBar( title = { Text(text = stringResource(id = R.string.install_activity_label)) }, navigationIcon = { + val context = LocalContext.current IconButton( - onClick = onDeny + onClick = { + context.finishActivity() + } ) { Icon( painter = painterResource(id = R.drawable.x), @@ -254,7 +334,7 @@ private fun TopBar( } }, actions = { - SuggestionChip( + if (isReady) SuggestionChip( modifier = Modifier .height(SuggestionChipDefaults.Height) .padding(end = 15.dp), diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/InstallViewModel.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/InstallViewModel.kt index 04c8272d..05893bf2 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/InstallViewModel.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/InstallViewModel.kt @@ -1,103 +1,134 @@ package dev.sanmer.pi.ui.screens.install -import android.content.pm.PackageInfo +import android.content.Context import android.content.pm.UserInfo +import android.net.Uri import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel -import dev.sanmer.pi.ContextCompat +import androidx.lifecycle.viewModelScope import dev.sanmer.pi.Logger -import dev.sanmer.pi.PackageInfoCompat.isNotEmpty -import dev.sanmer.pi.bundle.SplitConfig -import dev.sanmer.pi.compat.VersionCompat.fileSize -import dev.sanmer.pi.compat.VersionCompat.getSdkVersionDiff -import dev.sanmer.pi.compat.VersionCompat.getVersionDiff -import dev.sanmer.pi.model.IPackageInfo -import dev.sanmer.pi.model.IPackageInfo.Default.toIPackageInfo -import dev.sanmer.pi.model.Task +import dev.sanmer.pi.UserHandleCompat +import dev.sanmer.pi.datastore.model.Provider +import dev.sanmer.pi.factory.BundleFactory +import dev.sanmer.pi.factory.VersionFactory +import dev.sanmer.pi.factory.VersionFactory.Default.version +import dev.sanmer.pi.ktx.orEmpty +import dev.sanmer.pi.model.ServiceState +import dev.sanmer.pi.parser.PackageInfoLite +import dev.sanmer.pi.parser.SplitConfig import dev.sanmer.pi.repository.ServiceRepository import dev.sanmer.pi.service.InstallService -import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch class InstallViewModel( - private val serviceRepository: ServiceRepository + private val serviceRepository: ServiceRepository, + private val bundleFactory: BundleFactory, + private val versionFactory: VersionFactory ) : ViewModel() { - private val context by lazy { ContextCompat.getContext() } - private val pm by lazy { context.packageManager } - private val um by lazy { serviceRepository.getUserManager() } - - private var archivePath = File(".tmp") - var sourceInfo by mutableStateOf(IPackageInfo.Default.empty()) + var serviceState by mutableStateOf(ServiceState.Pending) private set - var archiveInfo by mutableStateOf(IPackageInfo.Default.empty()) + val isServiceReady inline get() = serviceState.isSucceed + var loadState by mutableStateOf(LoadState.Pending) private set - val hasSourceInfo by lazy { sourceInfo.isNotEmpty } + val isBundleReady inline get() = loadState is LoadState.Success + val isReady inline get() = isServiceReady && isBundleReady - private val currentInfo by lazy { getPackageInfo(archiveInfo.packageName) } - val versionDiff by lazy { currentInfo.getVersionDiff(context, archiveInfo) } - val sdkVersionDiff by lazy { currentInfo.getSdkVersionDiff(context, archiveInfo) } + var users by mutableStateOf(listOf()) + private set + var user by mutableStateOf(UserInfo(-1, "", 0)) - private var baseSize = 0L - val fileSize by derivedStateOf { - (baseSize + requiredConfigs.sumOf { it.file.length() }).fileSize(context) - } + private var data: BundleFactory.Data? = null + private var sizeBytes by mutableLongStateOf(0) + val size by derivedStateOf { versionFactory.fileSize(sizeBytes) } var splitConfigs = listOf() private set private val requiredConfigs = mutableStateListOf() - private var type by mutableStateOf(Type.Apk) - - var users by mutableStateOf(listOf()) - private set - var user by mutableStateOf(UserInfoCompat.Empty) - private set - val logger = Logger.Android("InstallViewModel") init { logger.d("init") - loadUsers() + serviceObserver() } - private fun loadUsers() { - runCatching { - users = um.getUsers().map(::UserInfoCompat) - }.onFailure { - logger.w(it) + private fun serviceObserver() { + viewModelScope.launch { + serviceRepository.state.collectLatest { + serviceState = it + if (it.isSucceed) { + loadUsers() + } + } } } - fun updateUser(userInfo: UserInfoCompat) { - user = userInfo + private fun loadUsers() { + val um = serviceRepository.getUserManager() + users = um.getUsers() + user = um.getUserInfo(UserHandleCompat.myUserId()) } - fun load(task: Task) { - archivePath = task.archivePath - archiveInfo = task.archiveInfo.toIPackageInfo() - sourceInfo = task.sourceInfo.toIPackageInfo() - - runCatching { - user = UserInfoCompat(um.getUserInfo(task.userId)) - }.onFailure { - logger.w(it) + fun recreate(provider: Provider) { + viewModelScope.launch { + serviceRepository.recreate(provider) } + } - when (task) { - is Task.Apk -> { - baseSize = archivePath.length() - } + private fun BundleFactory.Data.toSuccess(): LoadState.Success { + sizeBytes = bundleInfo.sizeBytes + bundleInfo.splitConfigs.sumOf { it.sizeBytes } + splitConfigs = bundleInfo.splitConfigs + requiredConfigs.addAll(splitConfigs.filter { it.isRequired || it.isRecommended }) + + val sourceInfo = sourceInfo?.run { + copy( + versionName = (longVersionCode to versionName).version, + compileSdkVersionCodename = versionFactory.sdkVersions( + target = targetSdkVersion, + min = minSdkVersion, + compile = compileSdkVersion + ) + ) + } - is Task.AppBundle -> { - type = Type.AppBundle - baseSize = task.baseFile.length() - splitConfigs = task.splitConfigs - requiredConfigs.addAll( - task.splitConfigs.filter { it.isRequired || it.isRecommended } + val currentInfo = currentInfo.orEmpty() + val archiveInfo = bundleInfo.packageInfo.run { + copy( + versionName = versionFactory.versionDiff( + that = with(this) { longVersionCode to versionName }, + other = with(currentInfo) { longVersionCode to versionName } + ), + compileSdkVersionCodename = versionFactory.sdkVersionsDiff( + target = targetSdkVersion to currentInfo.targetSdkVersion, + min = minSdkVersion to currentInfo.minSdkVersion, + compile = compileSdkVersion to currentInfo.compileSdkVersion ) + ) + } + + return LoadState.Success( + sourceInfo = sourceInfo, + archiveInfo = archiveInfo + ) + } + + fun loadFromUri(uri: Uri) { + viewModelScope.launch(Dispatchers.IO) { + runCatching { + loadState = bundleFactory.load(uri) + .also { + data = it + }.toSuccess() + }.onFailure { + loadState = LoadState.Failure(it) + logger.e(it) } } } @@ -109,65 +140,39 @@ class InstallViewModel( fun toggleSplitConfig(config: SplitConfig) { if (isRequiredConfig(config)) { requiredConfigs.remove(config) + sizeBytes -= config.sizeBytes } else { requiredConfigs.add(config) + sizeBytes += config.sizeBytes } } - fun install() = when (type) { - Type.Apk -> { - InstallService.Default.apk( - context = context, - archivePath = archivePath, - archiveInfo = archiveInfo, - userId = user.id, - sourceInfo = sourceInfo - ) - } - - Type.AppBundle -> { - InstallService.Default.appBundle( - context = context, - archivePath = archivePath, - archiveInfo = archiveInfo, - splitConfigs = requiredConfigs, - userId = user.id, - sourceInfo = sourceInfo - ) + fun start(context: Context) { + val data = checkNotNull(data) + val fileNames = if (data.bundleInfo.isZip) { + mutableListOf(data.bundleInfo.fileName) + .apply { addAll(requiredConfigs.map { it.fileName }) } + } else { + emptyList() } - } - fun deleteCache() { - archivePath.deleteRecursively() - } - - private fun getPackageInfo(packageName: String): PackageInfo { - return runCatching { - pm.getPackageInfo( - packageName, 0 - ) - }.getOrNull() ?: PackageInfo() - } - - enum class Type { - Apk, - AppBundle + InstallService.start( + context = context, + uri = data.uri, + archiveInfo = data.bundleInfo.packageInfo, + fileNames = fileNames, + sourceInfo = data.sourceInfo, + userId = user.id + ) } - class UserInfoCompat( - val id: Int, - val name: String - ) { - constructor(userInfo: UserInfo) : this( - id = userInfo.id, - name = userInfo.name ?: userInfo.id.toString() - ) + sealed class LoadState { + data object Pending : LoadState() + data class Success( + val sourceInfo: PackageInfoLite?, + val archiveInfo: PackageInfoLite + ) : LoadState() - companion object Default { - val Empty = UserInfoCompat( - id = -1, - name = "" - ) - } + data class Failure(val error: Throwable) : LoadState() } } \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/PackageInfoItem.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/PackageInfoItem.kt index a3c86b28..3fb9f305 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/PackageInfoItem.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/PackageInfoItem.kt @@ -1,5 +1,6 @@ package dev.sanmer.pi.ui.screens.install.component +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -9,25 +10,16 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.request.ImageRequest -import dev.sanmer.pi.compat.VersionCompat.getSdkVersion -import dev.sanmer.pi.compat.VersionCompat.versionStr -import dev.sanmer.pi.model.IPackageInfo +import dev.sanmer.pi.parser.PackageInfoLite @Composable fun PackageInfoItem( - packageInfo: IPackageInfo, - versionDiff: String? = null, - sdkVersionDiff: String? = null, - fileSize: String? = null + pi: PackageInfoLite, + size: String? = null ) = OutlinedCard( shape = MaterialTheme.shapes.large, ) { @@ -37,44 +29,35 @@ fun PackageInfoItem( .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - val context = LocalContext.current - AsyncImage( - modifier = Modifier.size(45.dp), - model = ImageRequest.Builder(context) - .data(packageInfo) - .build(), - contentDescription = null + Image( + bitmap = pi.iconOrDefault.asImageBitmap(), + contentDescription = null, + modifier = Modifier.size(45.dp) ) Column( modifier = Modifier.padding(start = 15.dp) ) { Text( - text = packageInfo.appLabel, + text = pi.labelOrDefault, style = MaterialTheme.typography.bodyLarge ) Text( - text = packageInfo.packageName, + text = pi.packageName, style = MaterialTheme.typography.bodyMedium ) - val versionStr by remember { - derivedStateOf { versionDiff ?: packageInfo.versionStr } - } Text( - text = versionStr, + text = pi.versionName, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline ) - val sdkVersion by remember { - derivedStateOf { sdkVersionDiff ?: packageInfo.getSdkVersion(context) } - } Text( text = buildString { - append(sdkVersion) - fileSize?.let { + append(pi.compileSdkVersionCodename) + size?.let { append(", ") append(it) } diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/SelectUserItem.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/SelectUserItem.kt index d6658540..4a6f5b74 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/SelectUserItem.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/SelectUserItem.kt @@ -1,5 +1,6 @@ package dev.sanmer.pi.ui.screens.install.component +import android.content.pm.UserInfo import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -22,14 +23,13 @@ import androidx.compose.ui.unit.dp import dev.sanmer.pi.R import dev.sanmer.pi.ui.ktx.bottom import dev.sanmer.pi.ui.ktx.surface -import dev.sanmer.pi.ui.screens.install.InstallViewModel.UserInfoCompat @Composable fun SelectUserItem( onDismiss: () -> Unit, - user: UserInfoCompat, - users: List, - onChange: (UserInfoCompat) -> Unit + user: UserInfo, + users: List, + onChange: (UserInfo) -> Unit ) = ModalBottomSheet( onDismissRequest = onDismiss, shape = MaterialTheme.shapes.large.bottom(0.dp) diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/SplitConfigItem.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/SplitConfigItem.kt index 47ccb870..72d7cd9e 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/SplitConfigItem.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/component/SplitConfigItem.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import dev.sanmer.pi.R -import dev.sanmer.pi.bundle.SplitConfig +import dev.sanmer.pi.parser.SplitConfig @Composable fun SplitConfigItem( @@ -61,12 +61,12 @@ fun SplitConfigItem( Text( text = buildString { - if (config.isConfigForSplit) { + if (config.configForSplit.isNotEmpty()) { append(config.configForSplit) append(", ") } - append(config.displaySize) + append(config.size) }, style = MaterialTheme.typography.bodySmall, textDecoration = when { diff --git a/app/src/main/res/drawable/rotate.xml b/app/src/main/res/drawable/rotate.xml new file mode 100644 index 00000000..a664b578 --- /dev/null +++ b/app/src/main/res/drawable/rotate.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 2248061c..51c24492 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -25,7 +25,6 @@ إضغط لمحاولة البدء اللغة إفتراضي تبع النظام - فشل تحليل الحزمة مخول الطالب المنفذ diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b9a34d56..7abe8cc1 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -19,7 +19,6 @@ Densidad de pantalla Idioma Sin especificar - Error al analizar el paquete ABI Lista vacía Instalar servicio @@ -31,6 +30,4 @@ Ejecutor PI Actualizado Toca para abrir la app - Servicio de análisis - Analizando \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 2596933f..47e6c083 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -32,9 +32,6 @@ مشخص نشده انتخاب کاربر - سرویس تجزیه - در حال تجزیه - تجزیه ناموفق بود سرویس نصب نصب موفقیت‌آمیز بود نصب ناموفق بود diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b28de169..b912baed 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -11,7 +11,6 @@ Cliquer pour essayer de démarrer Language Système par défaut - Échec de l\'analyse du paquet Paquet d\'installation Participer à la traduction Aidez nous a traduire PI dans vôtre langue diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index e740973b..ab772a2d 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -19,7 +19,6 @@ Pengaju Bahasa Tidak spesifik - Parsing paket gagal Pemasangan berhasil Pemasangan gagal Cari… @@ -31,6 +30,4 @@ Bantu kami menerjemahkan PI ke dalam bahasa Anda Ketuk untuk membuka apl PI Diperbarui - Penguraian - Layanan penguraian \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 161b40b4..ea7a4986 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -25,7 +25,6 @@ ההתקנה בוצעה בהצלחה לא מוגדר שפה - ניתוח החבילה כשל מורשה מבצע מבקש diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b817428a..6b86fafb 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -25,12 +25,9 @@ Clique para tentar começar Idioma Padrão do sistema - Falha na análise do pacote Autorizar Solicitante Executor PI atualizado Toque para abrir o app - Análise - Serviço de análise \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 24bacc45..636733b4 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -25,12 +25,9 @@ Clique para tentar começar Idioma Padrão do sistema - Falha na análise do pacote Autorizar Solicitante Executor PI atualizado Toque para abrir o app - Serviço de análise - Análise \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 2e684053..7a6fd8f9 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -15,7 +15,6 @@ Ajută-ne să traducem PI în limba ta Apasă pentru a solicita pornirea ABI - Analiza pachetului a eșuat Instalat cu succes Instalează serviciul Instalarea a eșuat diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 00ae32b0..476bef28 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -24,7 +24,6 @@ Запросчик установки ABI Настройки - Сбой синтаксического анализа пакета Поиск… PI Обновлён Нажмите чтоб открыть приложение diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index 35044e0f..581f0870 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -15,7 +15,6 @@ Klik keur ngajalankeun Basa Téang… - Parsing pakét gagal Eusi kosong Pamasangan réngsé Pamasangan gagal @@ -31,6 +30,4 @@ Pasang layanan PI Dianyarkeun Toél pikeun muka apl - Layanan panguraian - Panguraian \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 46387b9f..1cbe684f 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -23,7 +23,6 @@ Ekran yoğunluğu Dil Belirtilmemiş - Paket ayrıştırma başarısız oldu Servisi yükle Yükleme başarılı Yükleme başarısız diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 353f4ee3..27823a31 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -47,9 +47,6 @@ 安装服务 安装成功 安装失败 - 解析服务 - 正在解析 - 解析失败 正在优化 PI 已完成更新 点按即可打开应用 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 43d6b91a..34081018 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,9 +44,6 @@ Select user - Parsing service - Parsing - Parsing failed Installation service Installation successful Installation failed From bb1ffdeebdab29d3f5c3e9654b546b825c85f032 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Tue, 9 Sep 2025 00:18:47 +0800 Subject: [PATCH 11/25] Fix proguard rule --- stub/src/main/java/com/github/luben/zstd/ZstdInputStream.java | 4 ++++ stub/src/main/java/org/tukaani/xz/MemoryLimitException.java | 4 ++++ stub/src/main/java/org/tukaani/xz/SingleXZInputStream.java | 4 ++++ stub/src/main/java/org/tukaani/xz/XZInputStream.java | 4 ++++ 4 files changed, 16 insertions(+) create mode 100644 stub/src/main/java/com/github/luben/zstd/ZstdInputStream.java create mode 100644 stub/src/main/java/org/tukaani/xz/MemoryLimitException.java create mode 100644 stub/src/main/java/org/tukaani/xz/SingleXZInputStream.java create mode 100644 stub/src/main/java/org/tukaani/xz/XZInputStream.java diff --git a/stub/src/main/java/com/github/luben/zstd/ZstdInputStream.java b/stub/src/main/java/com/github/luben/zstd/ZstdInputStream.java new file mode 100644 index 00000000..95c568d4 --- /dev/null +++ b/stub/src/main/java/com/github/luben/zstd/ZstdInputStream.java @@ -0,0 +1,4 @@ +package com.github.luben.zstd; + +public class ZstdInputStream { +} diff --git a/stub/src/main/java/org/tukaani/xz/MemoryLimitException.java b/stub/src/main/java/org/tukaani/xz/MemoryLimitException.java new file mode 100644 index 00000000..6f834224 --- /dev/null +++ b/stub/src/main/java/org/tukaani/xz/MemoryLimitException.java @@ -0,0 +1,4 @@ +package org.tukaani.xz; + +public class MemoryLimitException { +} diff --git a/stub/src/main/java/org/tukaani/xz/SingleXZInputStream.java b/stub/src/main/java/org/tukaani/xz/SingleXZInputStream.java new file mode 100644 index 00000000..941c58b7 --- /dev/null +++ b/stub/src/main/java/org/tukaani/xz/SingleXZInputStream.java @@ -0,0 +1,4 @@ +package org.tukaani.xz; + +public class SingleXZInputStream { +} diff --git a/stub/src/main/java/org/tukaani/xz/XZInputStream.java b/stub/src/main/java/org/tukaani/xz/XZInputStream.java new file mode 100644 index 00000000..655571a1 --- /dev/null +++ b/stub/src/main/java/org/tukaani/xz/XZInputStream.java @@ -0,0 +1,4 @@ +package org.tukaani.xz; + +public class XZInputStream { +} From c7b8c5e14841a61915ef271d071de1fd4adf0c2a Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Tue, 9 Sep 2025 00:58:21 +0800 Subject: [PATCH 12/25] Fix PackageInfoLite?.orEmpty --- core/src/main/kotlin/dev/sanmer/pi/ktx/PackageInfoLite.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/kotlin/dev/sanmer/pi/ktx/PackageInfoLite.kt b/core/src/main/kotlin/dev/sanmer/pi/ktx/PackageInfoLite.kt index a79378fd..9f291aaa 100644 --- a/core/src/main/kotlin/dev/sanmer/pi/ktx/PackageInfoLite.kt +++ b/core/src/main/kotlin/dev/sanmer/pi/ktx/PackageInfoLite.kt @@ -2,7 +2,7 @@ package dev.sanmer.pi.ktx import dev.sanmer.pi.parser.PackageInfoLite -fun PackageInfoLite?.orEmpty() = PackageInfoLite( +fun PackageInfoLite?.orEmpty() = this ?: PackageInfoLite( packageName = "", versionCode = 0, versionCodeMajor = 0, From 8b32be8cc1fb28966a13072a0a1edd35b37d8840 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Tue, 9 Sep 2025 00:58:35 +0800 Subject: [PATCH 13/25] Fix VersionFactory --- .../kotlin/dev/sanmer/pi/factory/VersionFactory.kt | 4 ++-- .../sanmer/pi/ui/screens/install/InstallViewModel.kt | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/dev/sanmer/pi/factory/VersionFactory.kt b/app/src/main/kotlin/dev/sanmer/pi/factory/VersionFactory.kt index dc337aa9..8a5fe273 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/factory/VersionFactory.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/factory/VersionFactory.kt @@ -12,7 +12,7 @@ class VersionFactory( private infix fun > Pair.compare( other: Pair ) = when { - first == other.second -> value + first == other.first -> value else -> context.getString(R.string.comparator, value, other.value) } @@ -33,7 +33,7 @@ class VersionFactory( fun versionDiff(that: Pair, other: Pair): String { if (that.first <= 0) return other.version if (other.first <= 0) return that.version - return that compare other + return (that.first to that.version) compare (other.first to other.version) } fun sdkVersionsDiff( diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/InstallViewModel.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/InstallViewModel.kt index 05893bf2..cf816e41 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/InstallViewModel.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/screens/install/InstallViewModel.kt @@ -102,13 +102,13 @@ class InstallViewModel( val archiveInfo = bundleInfo.packageInfo.run { copy( versionName = versionFactory.versionDiff( - that = with(this) { longVersionCode to versionName }, - other = with(currentInfo) { longVersionCode to versionName } + that = with(currentInfo) { longVersionCode to versionName }, + other = with(this) { longVersionCode to versionName } ), compileSdkVersionCodename = versionFactory.sdkVersionsDiff( - target = targetSdkVersion to currentInfo.targetSdkVersion, - min = minSdkVersion to currentInfo.minSdkVersion, - compile = compileSdkVersion to currentInfo.compileSdkVersion + target = currentInfo.targetSdkVersion to targetSdkVersion, + min = currentInfo.minSdkVersion to minSdkVersion, + compile = currentInfo.compileSdkVersion to compileSdkVersion ) ) } From eddf067d4a6ae3388b86808ab8e0cfbe0ee1d988 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Tue, 9 Sep 2025 01:05:25 +0800 Subject: [PATCH 14/25] Remove unused preference --- .../pi/repository/PreferenceRepositoryImpl.kt | 10 ----- .../pi/ui/screens/settings/SettingsScreen.kt | 9 ---- .../ui/screens/settings/SettingsViewModel.kt | 6 --- app/src/full/res/drawable/hand_finger_off.xml | 42 ------------------- .../pi/repository/PreferenceRepositoryImpl.kt | 2 - .../sanmer/pi/datastore/model/Preference.kt | 2 - .../pi/repository/PreferenceRepository.kt | 1 - app/src/main/res/values-zh-rCN/strings.xml | 2 - app/src/main/res/values/strings.xml | 2 - 9 files changed, 76 deletions(-) delete mode 100644 app/src/full/res/drawable/hand_finger_off.xml diff --git a/app/src/full/kotlin/dev/sanmer/pi/repository/PreferenceRepositoryImpl.kt b/app/src/full/kotlin/dev/sanmer/pi/repository/PreferenceRepositoryImpl.kt index 7c2dbac2..a89832fe 100644 --- a/app/src/full/kotlin/dev/sanmer/pi/repository/PreferenceRepositoryImpl.kt +++ b/app/src/full/kotlin/dev/sanmer/pi/repository/PreferenceRepositoryImpl.kt @@ -22,16 +22,6 @@ class PreferenceRepositoryImpl( } } - override suspend fun setAutomatic(value: Boolean) { - withContext(Dispatchers.IO) { - dataStore.updateData { - it.copy( - automatic = value - ) - } - } - } - override suspend fun setRequester(value: String) { withContext(Dispatchers.IO) { dataStore.updateData { diff --git a/app/src/full/kotlin/dev/sanmer/pi/ui/screens/settings/SettingsScreen.kt b/app/src/full/kotlin/dev/sanmer/pi/ui/screens/settings/SettingsScreen.kt index 2bd3f947..5995f92f 100644 --- a/app/src/full/kotlin/dev/sanmer/pi/ui/screens/settings/SettingsScreen.kt +++ b/app/src/full/kotlin/dev/sanmer/pi/ui/screens/settings/SettingsScreen.kt @@ -41,7 +41,6 @@ import dev.sanmer.pi.datastore.model.Provider import dev.sanmer.pi.ktx.viewUrl import dev.sanmer.pi.ui.component.CheckIcon import dev.sanmer.pi.ui.component.SettingNormalItem -import dev.sanmer.pi.ui.component.SettingSwitchItem import dev.sanmer.pi.ui.ktx.bottom import dev.sanmer.pi.ui.provider.LocalPreference import dev.sanmer.pi.ui.screens.settings.component.LanguageItem @@ -102,14 +101,6 @@ fun SettingsScreen( onClick = { workingMode = true } ) - SettingSwitchItem( - icon = R.drawable.hand_finger_off, - title = stringResource(R.string.settings_automatic_installation), - desc = stringResource(R.string.settings_automatic_installation_desc), - checked = preference.automatic, - onChange = viewModel::setAutomatic - ) - SettingNormalItem( icon = R.drawable.moon, title = stringResource(R.string.settings_dark_mode), diff --git a/app/src/full/kotlin/dev/sanmer/pi/ui/screens/settings/SettingsViewModel.kt b/app/src/full/kotlin/dev/sanmer/pi/ui/screens/settings/SettingsViewModel.kt index 15c5554a..c4f6ff39 100644 --- a/app/src/full/kotlin/dev/sanmer/pi/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/full/kotlin/dev/sanmer/pi/ui/screens/settings/SettingsViewModel.kt @@ -28,12 +28,6 @@ class SettingsViewModel( } } - fun setAutomatic(value: Boolean) { - viewModelScope.launch { - preferenceRepository.setAutomatic(value) - } - } - fun setDarkMode(value: DarkMode) { viewModelScope.launch { preferenceRepository.setDarkMode(value) diff --git a/app/src/full/res/drawable/hand_finger_off.xml b/app/src/full/res/drawable/hand_finger_off.xml deleted file mode 100644 index 080302c1..00000000 --- a/app/src/full/res/drawable/hand_finger_off.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - diff --git a/app/src/lite/kotlin/dev/sanmer/pi/repository/PreferenceRepositoryImpl.kt b/app/src/lite/kotlin/dev/sanmer/pi/repository/PreferenceRepositoryImpl.kt index b0577df3..fa60637d 100644 --- a/app/src/lite/kotlin/dev/sanmer/pi/repository/PreferenceRepositoryImpl.kt +++ b/app/src/lite/kotlin/dev/sanmer/pi/repository/PreferenceRepositoryImpl.kt @@ -7,9 +7,7 @@ import kotlinx.coroutines.flow.flowOf class PreferenceRepositoryImpl() : PreferenceRepository { override val data = flowOf(Preference()) - override suspend fun setProvider(value: Provider) {} - override suspend fun setAutomatic(value: Boolean) {} override suspend fun setRequester(value: String) {} override suspend fun setExecutor(value: String) {} override suspend fun setDarkMode(value: DarkMode) {} diff --git a/app/src/main/kotlin/dev/sanmer/pi/datastore/model/Preference.kt b/app/src/main/kotlin/dev/sanmer/pi/datastore/model/Preference.kt index 44b45fc0..f557cd82 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/datastore/model/Preference.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/datastore/model/Preference.kt @@ -8,8 +8,6 @@ import kotlinx.serialization.protobuf.ProtoNumber data class Preference( @ProtoNumber(1) val provider: Provider = Provider.None, - @ProtoNumber(2) - val automatic: Boolean = true, @ProtoNumber(3) val requester: String = "", @ProtoNumber(4) diff --git a/app/src/main/kotlin/dev/sanmer/pi/repository/PreferenceRepository.kt b/app/src/main/kotlin/dev/sanmer/pi/repository/PreferenceRepository.kt index b8e792ab..3ff1eda1 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/repository/PreferenceRepository.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/repository/PreferenceRepository.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.Flow interface PreferenceRepository { val data: Flow suspend fun setProvider(value: Provider) - suspend fun setAutomatic(value: Boolean) suspend fun setRequester(value: String) suspend fun setExecutor(value: String) suspend fun setDarkMode(value: DarkMode) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 27823a31..a581e1ec 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -22,8 +22,6 @@ 版本 %1$d,%2$s 服务未运行 点击尝试启动 - 自动安装 - 允许应用本身或授权应用发起的更新请求 深色模式 自动 开启 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 34081018..5f8c5993 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,8 +22,6 @@ Version %1$d, %2$s Service is not running Click to try to restart - Automatic installation - Allow update requests from the app itself or authorized apps Dark mode Auto On From b503c5ff27b02f6ba8ff6537e82d592ae343ffd5 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Tue, 9 Sep 2025 01:34:47 +0800 Subject: [PATCH 15/25] Exclude org/** --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ecf119d5..075011fe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -93,8 +93,8 @@ android { packaging.resources.excludes += setOf( "META-INF/**", - "okhttp3/**", "kotlin/**", + "org/**", "**.bin", "**.properties" ) From e647957adcb36cfa15fef7100f9078198a643bc7 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Tue, 9 Sep 2025 01:34:53 +0800 Subject: [PATCH 16/25] Fix PackageInstaller.Session.writeFd --- .../dev/sanmer/pi/delegate/PackageInstallerDelegate.kt | 8 ++++++-- .../java/android/content/pm/PackageInstallerHidden.java | 9 --------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt b/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt index 27ef3c15..4ea93eb0 100644 --- a/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt +++ b/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt @@ -199,8 +199,12 @@ class PackageInstallerDelegate( name: String, fd: ParcelFileDescriptor ) = withContext(Dispatchers.IO) { - Refine.unsafeCast(this@writeFd) - .write(name, 0, fd.statSize, fd) + ParcelFileDescriptor.AutoCloseInputStream(fd).use { input -> + openWrite(name, 0, fd.statSize).use { output -> + input.copyTo(output) + fsync(output) + } + } } suspend fun PackageInstaller.Session.writeZip( diff --git a/stub/src/main/java/android/content/pm/PackageInstallerHidden.java b/stub/src/main/java/android/content/pm/PackageInstallerHidden.java index 69183587..51368e5a 100644 --- a/stub/src/main/java/android/content/pm/PackageInstallerHidden.java +++ b/stub/src/main/java/android/content/pm/PackageInstallerHidden.java @@ -1,9 +1,5 @@ package android.content.pm; -import android.os.ParcelFileDescriptor; - -import androidx.annotation.NonNull; - import dev.rikka.tools.refine.RefineAs; @RefineAs(PackageInstaller.class) @@ -24,10 +20,5 @@ public static class SessionHidden { public SessionHidden(IPackageInstallerSession session) { throw new RuntimeException("Stub!"); } - - public void write(@NonNull String name, long offsetBytes, long lengthBytes, - @NonNull ParcelFileDescriptor fd) { - throw new RuntimeException("Stub!"); - } } } \ No newline at end of file From 69a346de3aa39920146036c89b231f51699e2513 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Tue, 9 Sep 2025 14:33:31 +0800 Subject: [PATCH 17/25] Optimize PackageInstaller.Session.writeZip --- .../dev/sanmer/pi/delegate/PackageInstallerDelegate.kt | 2 +- core/src/main/kotlin/dev/sanmer/pi/ktx/FileDescriptor.kt | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt b/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt index 4ea93eb0..3c9f78bd 100644 --- a/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt +++ b/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt @@ -211,7 +211,7 @@ class PackageInstallerDelegate( names: List, fd: ParcelFileDescriptor ) = withContext(Dispatchers.IO) { - fd.fileDescriptor.asZipFile().use { zip -> + fd.asZipFile().use { zip -> zip.entries.toList().map { entry -> async { if (entry.name in names) { diff --git a/core/src/main/kotlin/dev/sanmer/pi/ktx/FileDescriptor.kt b/core/src/main/kotlin/dev/sanmer/pi/ktx/FileDescriptor.kt index c38f7c65..c72dffd0 100644 --- a/core/src/main/kotlin/dev/sanmer/pi/ktx/FileDescriptor.kt +++ b/core/src/main/kotlin/dev/sanmer/pi/ktx/FileDescriptor.kt @@ -1,11 +1,17 @@ package dev.sanmer.pi.ktx +import android.os.ParcelFileDescriptor import android.system.Os import android.system.OsConstants import org.apache.commons.compress.archivers.zip.ZipFile import java.io.FileDescriptor import java.io.FileInputStream +internal fun ParcelFileDescriptor.asZipFile() = ZipFile.builder() + .setIgnoreLocalFileHeader(true) + .setSeekableByteChannel(ParcelFileDescriptor.AutoCloseInputStream(this).channel) + .get() + internal fun FileDescriptor.asZipFile() = ZipFile.builder() .setIgnoreLocalFileHeader(true) .setSeekableByteChannel(FileInputStream(this).channel) From b1a1eee113c8206a241284fc323bc3d3469b6425 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Tue, 9 Sep 2025 14:58:14 +0800 Subject: [PATCH 18/25] Fix PackageInstaller.Session.writeZip --- .../dev/sanmer/pi/service/InstallService.kt | 13 +++++++++++-- .../pi/delegate/PackageInstallerDelegate.kt | 18 +++++++----------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/dev/sanmer/pi/service/InstallService.kt b/app/src/main/kotlin/dev/sanmer/pi/service/InstallService.kt index 3daf0238..5b75cb44 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/service/InstallService.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/service/InstallService.kt @@ -101,8 +101,17 @@ class InstallService : LifecycleService(), KoinComponent, PackageInstallerDelega override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { lifecycleScope.launch(Dispatchers.IO) { val task = intent?.taskOrNull ?: return@launch - bundleFactory.openFd(task.uri).use { - install(task, it) + bundleFactory.openFd(task.uri).use { fd -> + runCatching { + install(task, fd) + }.onFailure { + logger.e(it) + notifyFailure( + id = task.uri.hashCode(), + appLabel = task.archiveInfo.labelOrDefault, + appIcon = task.archiveInfo.iconOrDefault, + ) + } } pendingUris.remove(task.uri) } diff --git a/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt b/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt index 3c9f78bd..317e9512 100644 --- a/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt +++ b/core/src/main/kotlin/dev/sanmer/pi/delegate/PackageInstallerDelegate.kt @@ -20,8 +20,6 @@ import dev.sanmer.pi.ContextCompat.userId import dev.sanmer.pi.IntentReceiverCompat import dev.sanmer.pi.ktx.asZipFile import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext class PackageInstallerDelegate( @@ -212,18 +210,16 @@ class PackageInstallerDelegate( fd: ParcelFileDescriptor ) = withContext(Dispatchers.IO) { fd.asZipFile().use { zip -> - zip.entries.toList().map { entry -> - async { - if (entry.name in names) { - zip.getInputStream(entry).use { input -> - openWrite(entry.name, 0, entry.size).use { output -> - input.copyTo(output) - fsync(output) - } + zip.entries.asSequence().forEach { entry -> + if (entry.name in names) { + zip.getInputStream(entry).use { input -> + openWrite(entry.name, 0, entry.size).use { output -> + input.copyTo(output) + fsync(output) } } } - }.awaitAll() + } } } } From 090949cede00daff9ee0f4265c63a5a11b9343b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:36:38 +0800 Subject: [PATCH 19/25] Bump the kotlin-ksp group with 8 updates (#301) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 45b80425..7457d94a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,10 +17,10 @@ coil = "2.7.0" hiddenApiRefine = "4.4.0" hiddenApiBypass = "6.1" koin = "4.1.1" -kotlin = "2.2.10" +kotlin = "2.2.20" kotlinxCoroutines = "1.10.2" kotlinxSerialization = "1.9.0" -ksp = "2.2.10-2.0.2" +ksp = "2.2.20-2.0.2" shizuku = "13.1.5" [libraries] From c1488dd4f9eb4b2a428d0fd32e0bbdd0853a8f9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:37:10 +0800 Subject: [PATCH 20/25] Bump androidxCompose from 1.9.0 to 1.9.1 (#303) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7457d94a..8b89d039 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ androidGradlePlugin = "8.13.0" androidxActivity = "1.10.1" androidxAnnotation = "1.9.1" androidxAppCompat = "1.7.1" -androidxCompose = "1.9.0" +androidxCompose = "1.9.1" androidxComposeMaterial3 = "1.3.2" androidxCore = "1.17.0" androidxCoreSplashscreen = "1.0.1" From 878a886a648a132cfd0b920d0a70cb69e5af6eba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:41:32 +0800 Subject: [PATCH 21/25] Bump androidx.navigation:navigation-compose from 2.9.3 to 2.9.4 (#305) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8b89d039..938c1b5a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ androidxCoreSplashscreen = "1.0.1" androidxDataStore = "1.1.7" androidxDocumentFile = "1.1.0" androidxLifecycle = "2.9.3" -androidxNavigation = "2.9.3" +androidxNavigation = "2.9.4" apacheCompress = "1.28.0" appiconloader = "1.5.0" coil = "2.7.0" From 987505026714ca097418c953f827702620626ff7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:51:11 +0800 Subject: [PATCH 22/25] Bump androidx.activity:activity-compose from 1.10.1 to 1.11.0 (#306) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 938c1b5a..c4a48a63 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] androidGradlePlugin = "8.13.0" -androidxActivity = "1.10.1" +androidxActivity = "1.11.0" androidxAnnotation = "1.9.1" androidxAppCompat = "1.7.1" androidxCompose = "1.9.1" From f7405fb1b7bf8434022de14028afb97597386694 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 13:51:09 +0800 Subject: [PATCH 23/25] Bump the kotlin-ksp group with 2 updates (#307) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4a48a63..f421c64e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ koin = "4.1.1" kotlin = "2.2.20" kotlinxCoroutines = "1.10.2" kotlinxSerialization = "1.9.0" -ksp = "2.2.20-2.0.2" +ksp = "2.2.20-2.0.3" shizuku = "13.1.5" [libraries] From 680c3574eb46b94cadde8b5732c992f39e3e36c7 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Sat, 13 Sep 2025 14:30:27 +0800 Subject: [PATCH 24/25] Optimize InstallActivity --- .../dev/sanmer/pi/ui/InstallActivity.kt | 33 +++++++++++++++++++ .../dev/sanmer/pi/ui/main/MainViewModel.kt | 0 .../dev/sanmer/pi/di/ViewModelModule.kt | 2 -- .../dev/sanmer/pi/ui/InstallActivity.kt | 17 ++++++++++ ...tallActivity.kt => BaseInstallActivity.kt} | 25 +------------- 5 files changed, 51 insertions(+), 26 deletions(-) create mode 100644 app/src/full/kotlin/dev/sanmer/pi/ui/InstallActivity.kt rename app/src/{main => full}/kotlin/dev/sanmer/pi/ui/main/MainViewModel.kt (100%) create mode 100644 app/src/lite/kotlin/dev/sanmer/pi/ui/InstallActivity.kt rename app/src/main/kotlin/dev/sanmer/pi/ui/{InstallActivity.kt => BaseInstallActivity.kt} (55%) diff --git a/app/src/full/kotlin/dev/sanmer/pi/ui/InstallActivity.kt b/app/src/full/kotlin/dev/sanmer/pi/ui/InstallActivity.kt new file mode 100644 index 00000000..eb4f2af0 --- /dev/null +++ b/app/src/full/kotlin/dev/sanmer/pi/ui/InstallActivity.kt @@ -0,0 +1,33 @@ +package dev.sanmer.pi.ui + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.compose.runtime.CompositionLocalProvider +import dev.sanmer.pi.ui.main.MainViewModel +import dev.sanmer.pi.ui.main.MainViewModel.LoadState +import dev.sanmer.pi.ui.provider.LocalPreference +import dev.sanmer.pi.ui.screens.install.InstallScreen +import dev.sanmer.pi.ui.theme.AppTheme +import org.koin.androidx.viewmodel.ext.android.viewModel + +class InstallActivity : BaseInstallActivity() { + private val viewModel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + when (viewModel.loadState) { + LoadState.Pending -> {} + is LoadState.Ready -> CompositionLocalProvider( + LocalPreference provides viewModel.preference + ) { + AppTheme( + darkMode = viewModel.preference.darkMode.isDarkTheme + ) { + InstallScreen() + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/main/MainViewModel.kt b/app/src/full/kotlin/dev/sanmer/pi/ui/main/MainViewModel.kt similarity index 100% rename from app/src/main/kotlin/dev/sanmer/pi/ui/main/MainViewModel.kt rename to app/src/full/kotlin/dev/sanmer/pi/ui/main/MainViewModel.kt diff --git a/app/src/lite/kotlin/dev/sanmer/pi/di/ViewModelModule.kt b/app/src/lite/kotlin/dev/sanmer/pi/di/ViewModelModule.kt index 787bf40e..6a721ff5 100644 --- a/app/src/lite/kotlin/dev/sanmer/pi/di/ViewModelModule.kt +++ b/app/src/lite/kotlin/dev/sanmer/pi/di/ViewModelModule.kt @@ -1,11 +1,9 @@ package dev.sanmer.pi.di -import dev.sanmer.pi.ui.main.MainViewModel import dev.sanmer.pi.ui.screens.install.InstallViewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val ViewModel = module { - viewModelOf(::MainViewModel) viewModelOf(::InstallViewModel) } \ No newline at end of file diff --git a/app/src/lite/kotlin/dev/sanmer/pi/ui/InstallActivity.kt b/app/src/lite/kotlin/dev/sanmer/pi/ui/InstallActivity.kt new file mode 100644 index 00000000..f4221513 --- /dev/null +++ b/app/src/lite/kotlin/dev/sanmer/pi/ui/InstallActivity.kt @@ -0,0 +1,17 @@ +package dev.sanmer.pi.ui + +import android.os.Bundle +import androidx.activity.compose.setContent +import dev.sanmer.pi.ui.screens.install.InstallScreen +import dev.sanmer.pi.ui.theme.AppTheme + +class InstallActivity : BaseInstallActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AppTheme { + InstallScreen() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/sanmer/pi/ui/InstallActivity.kt b/app/src/main/kotlin/dev/sanmer/pi/ui/BaseInstallActivity.kt similarity index 55% rename from app/src/main/kotlin/dev/sanmer/pi/ui/InstallActivity.kt rename to app/src/main/kotlin/dev/sanmer/pi/ui/BaseInstallActivity.kt index 35f0f86b..85d83b5d 100644 --- a/app/src/main/kotlin/dev/sanmer/pi/ui/InstallActivity.kt +++ b/app/src/main/kotlin/dev/sanmer/pi/ui/BaseInstallActivity.kt @@ -3,22 +3,14 @@ package dev.sanmer.pi.ui import android.Manifest import android.os.Bundle import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.runtime.CompositionLocalProvider import dev.sanmer.pi.Logger import dev.sanmer.pi.compat.BuildCompat import dev.sanmer.pi.compat.PermissionCompat -import dev.sanmer.pi.ui.main.MainViewModel -import dev.sanmer.pi.ui.main.MainViewModel.LoadState -import dev.sanmer.pi.ui.provider.LocalPreference -import dev.sanmer.pi.ui.screens.install.InstallScreen import dev.sanmer.pi.ui.screens.install.InstallViewModel -import dev.sanmer.pi.ui.theme.AppTheme import org.koin.androidx.viewmodel.ext.android.viewModel -class InstallActivity : ComponentActivity() { - private val main by viewModel() +abstract class BaseInstallActivity : ComponentActivity() { private val viewModel by viewModel() private val logger = Logger.Android("InstallActivity") @@ -42,21 +34,6 @@ class InstallActivity : ComponentActivity() { } else { viewModel.loadFromUri(uri) } - - setContent { - when (main.loadState) { - LoadState.Pending -> {} - is LoadState.Ready -> CompositionLocalProvider( - LocalPreference provides main.preference - ) { - AppTheme( - darkMode = main.preference.darkMode.isDarkTheme - ) { - InstallScreen() - } - } - } - } } override fun onDestroy() { From 5836d38c8104dae5986a0213cf2cebda70dc6558 Mon Sep 17 00:00:00 2001 From: SanmerDev Date: Sat, 13 Sep 2025 14:42:14 +0800 Subject: [PATCH 25/25] Bump version to 1.3.0 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index cb9ae5aa..c9d189da 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ tasks.register("clean") { } subprojects { - val baseVersionName by extra("1.2.2") + val baseVersionName by extra("1.3.0") apply(plugin = "maven-publish") configure {