From 99c93a36ca8cb06ce0f439130b152ce8c5837272 Mon Sep 17 00:00:00 2001 From: hxreborn <32096750+hxreborn@users.noreply.github.com> Date: Sun, 7 Dec 2025 19:52:46 +0100 Subject: [PATCH 1/2] feat: add Android 16 QPR 2 support --- .../nextalone/pkginstallerplus/HookEntry.java | 13 +- .../hook/InstallerHookBaklava.kt | 194 ++++++++++++++++++ .../pkginstallerplus/utils/HookUtils.kt | 15 ++ 3 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/ltd/nextalone/pkginstallerplus/hook/InstallerHookBaklava.kt diff --git a/app/src/main/java/ltd/nextalone/pkginstallerplus/HookEntry.java b/app/src/main/java/ltd/nextalone/pkginstallerplus/HookEntry.java index c044e12..1a59867 100644 --- a/app/src/main/java/ltd/nextalone/pkginstallerplus/HookEntry.java +++ b/app/src/main/java/ltd/nextalone/pkginstallerplus/HookEntry.java @@ -13,9 +13,11 @@ import de.robv.android.xposed.IXposedHookZygoteInit; import de.robv.android.xposed.callbacks.XC_LoadPackage; import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam; +import ltd.nextalone.pkginstallerplus.hook.InstallerHookBaklava; import ltd.nextalone.pkginstallerplus.hook.InstallerHookN; import ltd.nextalone.pkginstallerplus.hook.InstallerHookQ; +import static ltd.nextalone.pkginstallerplus.utils.HookUtilsKt.isV2InstallerAvailable; import static ltd.nextalone.pkginstallerplus.utils.LogUtilsKt.logDebug; import static ltd.nextalone.pkginstallerplus.utils.LogUtilsKt.logDetail; import static ltd.nextalone.pkginstallerplus.utils.LogUtilsKt.logError; @@ -32,8 +34,17 @@ public class HookEntry implements IXposedHookLoadPackage, IXposedHookZygoteInit private static void initializeHookInternal(LoadPackageParam lpparam) { logDebug("initializeHookInternal start"); + lpClassLoader = lpparam.classLoader; + if (isV2InstallerAvailable()) { + // Android 16 QPR2 + try { + logDebug("initializeHook: Baklava QPR2"); + InstallerHookBaklava.INSTANCE.initOnce(); + } catch (Exception e) { + logThrowable("initializeHook(Baklava QPR2): ", e); + } + } try { - lpClassLoader = lpparam.classLoader; if (VERSION.SDK_INT >= VERSION_CODES.Q) { //Android Q -- Android T logDebug("initializeHook: Q"); diff --git a/app/src/main/java/ltd/nextalone/pkginstallerplus/hook/InstallerHookBaklava.kt b/app/src/main/java/ltd/nextalone/pkginstallerplus/hook/InstallerHookBaklava.kt new file mode 100644 index 0000000..1d23c2c --- /dev/null +++ b/app/src/main/java/ltd/nextalone/pkginstallerplus/hook/InstallerHookBaklava.kt @@ -0,0 +1,194 @@ +package ltd.nextalone.pkginstallerplus.hook + +import android.app.Activity +import android.app.Dialog +import android.content.pm.PackageInfo +import android.graphics.Typeface +import android.os.Build +import android.os.UserManager +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import ltd.nextalone.pkginstallerplus.HookEntry.injectModuleResources +import ltd.nextalone.pkginstallerplus.R +import ltd.nextalone.pkginstallerplus.utils.* + +private const val TAG_INSTALL_DETAILS = "IPP_install_details" +private const val TAG_UNINSTALL_DETAILS = "IPP_uninstall_details" + +object InstallerHookBaklava { + fun initOnce() { + "$INSTALLER_V2_PKG.fragments.InstallationFragment".clazz?.method("updateUI")?.hookAfter { + val fragment = it.thisObject + val dialog = fragment.get("mDialog") as? Dialog ?: return@hookAfter + + val activity = + fragment.javaClass.getMethod("requireActivity").invoke(fragment) as? Activity + ?: return@hookAfter + + val isConfirmation = runCatching { + val viewModel = activity.get("installViewModel") ?: return@hookAfter + val liveData = listOf("_currentInstallStage", "currentInstallStage") + .firstNotNullOfOrNull { key -> viewModel.get(key) } + + liveData?.get("mData")?.javaClass?.simpleName == "InstallUserActionRequired" + }.getOrElse { e -> + logThrowable(msg = "Baklava: stage detection failed", t = e) + false + } + + if (isConfirmation) { + injectModuleResources(activity.resources) + addInstallDetails(activity, dialog) + } else { + removeInstallDetails(dialog) + } + } + + "$INSTALLER_V2_PKG.fragments.UninstallationFragment".clazz?.method("updateUI")?.hookAfter { + val fragment = it.thisObject + val dialog = fragment.get("mDialog") as? Dialog ?: return@hookAfter + + val activity = + fragment.javaClass.getMethod("requireActivity").invoke(fragment) as? Activity + ?: return@hookAfter + injectModuleResources(activity.resources) + addUninstallDetails(activity, dialog) + } + } + + private fun addInstallDetails( + activity: Activity, + dialog: Dialog, + ) { + val appSnippet: ViewGroup = dialog.findHostView("app_snippet") ?: return + val parent = appSnippet.parent as? ViewGroup ?: return + + if (parent.findViewWithTag(TAG_INSTALL_DETAILS) != null) return + + val viewModel = activity.get("installViewModel") ?: return + val repository = viewModel.get("repository") ?: return + val newPkgInfo = repository.get("newPackageInfo") as? PackageInfo ?: return + val usrManager = repository.get("userManager") as? UserManager ?: return + val oldPkgInfo = activity.packageManager.getPackageInfoOrNull(newPkgInfo.packageName) + + val sb = SpannableStringBuilder() + sb + .append(activity.getString(R.string.IPP_info_user) + ": ") + .append(usrManager.userName) + .append('\n') + .append(activity.getString(R.string.IPP_info_package) + ": ") + .append( + newPkgInfo.packageName, + ForegroundColorSpan(ThemeUtil.colorGreen), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ).append('\n') + + if (oldPkgInfo == null) { + sb + .append(activity.getString(R.string.IPP_info_version) + ": ") + .append( + "${newPkgInfo.versionName ?: "N/A"}(${newPkgInfo.compatLongVersionCode()})", + ForegroundColorSpan(ThemeUtil.colorGreen), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ).append('\n') + .append(activity.getString(R.string.IPP_info_sdk) + ": ") + .append( + newPkgInfo.applicationInfo?.targetSdkVersion?.toString() ?: "N/A", + ForegroundColorSpan(ThemeUtil.colorGreen), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + } else { + sb + .append(activity.getString(R.string.IPP_info_version) + ": ") + .append( + "${oldPkgInfo.versionName ?: "N/A"}(${oldPkgInfo.compatLongVersionCode()})", + ForegroundColorSpan(ThemeUtil.colorRed), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ).append(" ➞ ") + .append( + "${newPkgInfo.versionName ?: "N/A"}(${newPkgInfo.compatLongVersionCode()})", + ForegroundColorSpan(ThemeUtil.colorGreen), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ).append('\n') + .append(activity.getString(R.string.IPP_info_sdk) + ": ") + .append( + oldPkgInfo.applicationInfo?.targetSdkVersion?.toString() ?: "N/A", + ForegroundColorSpan(ThemeUtil.colorRed), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ).append(" ➞ ") + .append( + newPkgInfo.applicationInfo?.targetSdkVersion?.toString() ?: "N/A", + ForegroundColorSpan(ThemeUtil.colorGreen), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + } + + parent.addView( + TextView(activity).apply { + tag = TAG_INSTALL_DETAILS + setTextIsSelectable(true) + typeface = Typeface.MONOSPACE + text = sb + }, + ) + } + + private fun removeInstallDetails(dialog: Dialog) { + val appSnippet: ViewGroup = dialog.findHostView("app_snippet") ?: return + val parent = appSnippet.parent as? ViewGroup ?: return + parent.findViewWithTag(TAG_INSTALL_DETAILS)?.let { parent.removeView(it) } + } + + private fun addUninstallDetails( + activity: Activity, + dialog: Dialog, + ) { + val appSnippet: ViewGroup = dialog.findHostView("app_snippet") ?: return + val parent = appSnippet.parent as? ViewGroup ?: return + + if (parent.findViewWithTag(TAG_UNINSTALL_DETAILS) != null) return + + val viewModel = activity.get("uninstallViewModel") ?: return + val repository = viewModel.get("repository") ?: return + val packageName = repository.get("targetPackageName") as? String ?: return + val pkgInfo = activity.packageManager.getPackageInfoOrNull(packageName) ?: return + + val sb = SpannableStringBuilder() + sb + .append(activity.getString(R.string.IPP_info_package) + ": ") + .append( + packageName, + ForegroundColorSpan(ThemeUtil.colorRed), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ).append('\n') + .append(activity.getString(R.string.IPP_info_version) + ": ") + .append( + "${pkgInfo.versionName ?: "N/A"}(${pkgInfo.compatLongVersionCode()})", + ForegroundColorSpan(ThemeUtil.colorRed), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ).append('\n') + .append(activity.getString(R.string.IPP_info_sdk) + ": ") + .append( + pkgInfo.applicationInfo?.targetSdkVersion?.toString() ?: "N/A", + ForegroundColorSpan(ThemeUtil.colorRed), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + + parent.addView( + TextView(activity).apply { + tag = TAG_UNINSTALL_DETAILS + setTextIsSelectable(true) + typeface = Typeface.MONOSPACE + text = sb + }, + ) + } +} + +@Suppress("DEPRECATION") +private fun PackageInfo.compatLongVersionCode(): Long = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) longVersionCode else versionCode.toLong() diff --git a/app/src/main/java/ltd/nextalone/pkginstallerplus/utils/HookUtils.kt b/app/src/main/java/ltd/nextalone/pkginstallerplus/utils/HookUtils.kt index e73b088..319a2d8 100644 --- a/app/src/main/java/ltd/nextalone/pkginstallerplus/utils/HookUtils.kt +++ b/app/src/main/java/ltd/nextalone/pkginstallerplus/utils/HookUtils.kt @@ -1,5 +1,7 @@ package ltd.nextalone.pkginstallerplus.utils +import android.content.pm.PackageInfo +import android.content.pm.PackageManager import android.util.Log import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XC_MethodReplacement @@ -8,6 +10,8 @@ import ltd.nextalone.pkginstallerplus.HookEntry import java.lang.reflect.Member import java.lang.reflect.Method +const val INSTALLER_V2_PKG = "com.android.packageinstaller.v2.ui" + internal val String.clazz: Class<*>? get() = try { HookEntry.lpClassLoader.loadClass(this) @@ -123,3 +127,14 @@ internal fun Class<*>.method(name: String, size: Int, returnType: Class<*>, cond } return null } + +internal val isV2InstallerAvailable: Boolean + get() = "$INSTALLER_V2_PKG.InstallLaunch".clazz != null + +internal fun PackageManager.getPackageInfoOrNull(pkgName: String): PackageInfo? = + try { + @Suppress("DEPRECATION", "InlinedApi") + getPackageInfo(pkgName, PackageManager.MATCH_UNINSTALLED_PACKAGES) + } catch (e: PackageManager.NameNotFoundException) { + null + } From 82c42552e6692a361ed5ed58bf964cfb25ecb675 Mon Sep 17 00:00:00 2001 From: hxreborn <32096750+hxreborn@users.noreply.github.com> Date: Mon, 8 Dec 2025 01:16:36 +0100 Subject: [PATCH 2/2] ci: update GitHub Actions to v4 --- .github/workflows/autoci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/autoci.yml b/.github/workflows/autoci.yml index c7d082b..b3753d6 100644 --- a/.github/workflows/autoci.yml +++ b/.github/workflows/autoci.yml @@ -23,10 +23,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '17' @@ -47,7 +47,7 @@ jobs: run: echo "SHA7=$(echo ${GITHUB_SHA} | cut -c1-7)" >> $GITHUB_ENV - name: Upload Packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: success() with: name: InstallerPlus-debug-${{ env.SHA7 }}