Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/autoci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 }}
Expand Down
13 changes: 12 additions & 1 deletion app/src/main/java/ltd/nextalone/pkginstallerplus/HookEntry.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TextView>(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<View>(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<TextView>(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()
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Loading