From e1b21155c9e2076145a40800a85d4c8a7d73b874 Mon Sep 17 00:00:00 2001 From: wxxsfxyzm <65166044+wxxsfxyzm@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:34:47 +0800 Subject: [PATCH 1/2] feat: Intercept session install and fix permissions on Android 14+ - Implement `intercept_session_install` to intercept `CONFIRM_INSTALL` requests - Add "Fix Permissions" feature for Android 14+ to bypass `READ_INSTALLED_SESSION_PATHS` checks - Hook `PackageInstallerSession.generateInfoInternal` to manually restore `resolvedBaseCodePath` when necessary - Update UI with new toggles and animations for session install and permission fix settings - Add logging for Intent extras to improve debugging - Update internationalization strings for new features --- .../github/chimio/inxlocker/hook/HookEntry.kt | 63 ++++- .../inxlocker/ui/activity/MainActivity.kt | 219 +++++++++++++----- .../chimio/inxlocker/util/IntentAnalyzer.kt | 17 ++ .../chimio/inxlocker/util/IntentRedirector.kt | 1 + app/src/main/res/values-zh/strings.xml | 8 + app/src/main/res/values/strings.xml | 8 + 6 files changed, 263 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/io/github/chimio/inxlocker/hook/HookEntry.kt b/app/src/main/java/io/github/chimio/inxlocker/hook/HookEntry.kt index 09ef25e..aeafa75 100644 --- a/app/src/main/java/io/github/chimio/inxlocker/hook/HookEntry.kt +++ b/app/src/main/java/io/github/chimio/inxlocker/hook/HookEntry.kt @@ -4,12 +4,15 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.os.Build import androidx.core.content.ContextCompat import com.highcapable.kavaref.KavaRef.Companion.resolve import com.highcapable.yukihookapi.annotation.xposed.InjectYukiHookWithXposed import com.highcapable.yukihookapi.hook.core.annotation.LegacyHookApi import com.highcapable.yukihookapi.hook.factory.configs import com.highcapable.yukihookapi.hook.factory.encase +import com.highcapable.yukihookapi.hook.factory.field +import com.highcapable.yukihookapi.hook.factory.method import com.highcapable.yukihookapi.hook.log.YLog import com.highcapable.yukihookapi.hook.param.PackageParam import com.highcapable.yukihookapi.hook.xposed.proxy.IYukiHookXposedInit @@ -24,8 +27,8 @@ import io.github.chimio.inxlocker.util.i class HookEntry : IYukiHookXposedInit { companion object { - private const val TAG = "InstallerRedirect" + private val isFixingPermissions = ThreadLocal() } private fun registerPrefsUpdateReceiver(context: Context?) { @@ -57,6 +60,7 @@ class HookEntry : IYukiHookXposedInit { } loadSystem { hookActivityStarterExecute() + hookPackageInstallerSession() } } @@ -177,4 +181,61 @@ class HookEntry : IYukiHookXposedInit { } } } + + private fun PackageParam.hookPackageInstallerSession() { + if (Build.VERSION.SDK_INT < 34) return + + "com.android.server.pm.PackageInstallerSession".toClassOrNull()?.method { + name = "generateInfoInternal" + }?.hook { + before { + try { + PrefsProvider.reload() + if (PrefsProvider.getBoolean("fix_permissions", false)) { + isFixingPermissions.set(true) + } + } catch (e: Exception) { + YLog.e(TAG, "generateInfoInternal Hook before 错误: ${e.message}", e) + } + } + after { + if (isFixingPermissions.get() == true) { + isFixingPermissions.set(false) + try { + val info = result // SessionInfo 对象 + if (info != null) { + val infoClass = info.javaClass + // 检查 resolvedBaseCodePath 是否为空 + val currentPath = infoClass.field { name = "resolvedBaseCodePath" }.get(info).string() + if (currentPath.isEmpty()) { + // 显式使用 instanceClass (即 PackageInstallerSession.class) 来调用 field + val mResolvedBaseFile = instanceClass?.field { name = "mResolvedBaseFile" }?.get(instance)?.any() as? java.io.File + if (mResolvedBaseFile != null) { + infoClass.field { name = "resolvedBaseCodePath" }.get(info).set(mResolvedBaseFile.absolutePath) + YLog.i(TAG, "权限绕过可能失败,已手动补全路径: ${mResolvedBaseFile.absolutePath}") + } + } + } + } catch (e: Exception) { + YLog.e(TAG, "generateInfoInternal Hook after 修复失败: ${e.message}") + } + } + } + } + + "android.app.ContextImpl".toClassOrNull()?.method { + name = "checkCallingOrSelfPermission" + param(String::class.java) + }?.hook { + after { + try { + if (isFixingPermissions.get() == true && args(0).string() == "android.permission.READ_INSTALLED_SESSION_PATHS") { + result = 0 // PackageManager.PERMISSION_GRANTED + } + } catch (e: Exception) { + // Ignore + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/github/chimio/inxlocker/ui/activity/MainActivity.kt b/app/src/main/java/io/github/chimio/inxlocker/ui/activity/MainActivity.kt index 235439b..4091c5c 100644 --- a/app/src/main/java/io/github/chimio/inxlocker/ui/activity/MainActivity.kt +++ b/app/src/main/java/io/github/chimio/inxlocker/ui/activity/MainActivity.kt @@ -5,11 +5,17 @@ import android.content.Intent import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.graphics.drawable.Drawable +import android.os.Build import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -31,6 +37,8 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.DateRange import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -65,6 +73,7 @@ import io.github.chimio.inxlocker.ui.widget.SettingsGroup import io.github.chimio.inxlocker.ui.widget.SettingsItem import io.github.chimio.inxlocker.ui.widget.SwitchGroup import io.github.chimio.inxlocker.ui.widget.SwitchItem +import io.github.chimio.inxlocker.ui.widget.SettingsSwitchRow import io.github.chimio.inxlocker.util.Broadcasts data class InstallerApp( @@ -190,6 +199,42 @@ class MainActivity : ComponentActivity() { } } + private fun saveInterceptSessionInstallEnabled(enabled: Boolean) { + try { + prefs("selected_installer_package").edit { + putBoolean("intercept_session_install", enabled) + } + sendBroadcast(Intent(Broadcasts.ACTION_PREFS_UPDATED)) + } catch (_: Exception) { + } + } + + private fun getInterceptSessionInstallEnabled(): Boolean { + return try { + prefs("selected_installer_package").getBoolean("intercept_session_install", false) + } catch (_: Exception) { + false + } + } + + private fun saveFixPermissionsEnabled(enabled: Boolean) { + try { + prefs("selected_installer_package").edit { + putBoolean("fix_permissions", enabled) + } + sendBroadcast(Intent(Broadcasts.ACTION_PREFS_UPDATED)) + } catch (_: Exception) { + } + } + + private fun getFixPermissionsEnabled(): Boolean { + return try { + prefs("selected_installer_package").getBoolean("fix_permissions", false) + } catch (_: Exception) { + false + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -211,6 +256,9 @@ class MainActivity : ComponentActivity() { var hideIcon by remember { mutableStateOf(getHideIconState()) } var debugLogEnabled by remember { mutableStateOf(getDebugLogEnabled()) } var interceptUninstallEnabled by remember { mutableStateOf(getInterceptUninstallEnabled()) } + var interceptSessionInstallEnabled by remember { mutableStateOf(getInterceptSessionInstallEnabled()) } + var fixPermissionsEnabled by remember { mutableStateOf(getFixPermissionsEnabled()) } + val selectedInstaller = installerList.find { it.packageName == selectedPackage } Scaffold( @@ -248,62 +296,129 @@ class MainActivity : ComponentActivity() { item { SwitchGroup( title = stringResource(R.string.settings), - items = listOf( - SwitchItem( - icon = Icons.Default.Info, - title = stringResource(R.string.hide_icon_title), - subtitle = stringResource(R.string.hide_icon_desc), - isChecked = hideIcon, - onCheckedChange = { newState -> - hideIcon = newState - saveHideIconState(newState) - Toast.makeText( - context, - if (newState) context.getString(R.string.hide_icon_enabled_toast) else context.getString( - R.string.hide_icon_disabled_toast - ), - Toast.LENGTH_SHORT - ).show() - } - ), - SwitchItem( - icon = Icons.Default.DateRange, - title = stringResource(R.string.debug_log_title), - subtitle = stringResource(R.string.debug_log_desc), - isChecked = debugLogEnabled, - onCheckedChange = { newState -> - debugLogEnabled = newState - saveDebugLogEnabled(newState) - Toast.makeText( - context, - if (newState) context.getString(R.string.debug_log_enabled_toast) else context.getString( - R.string.debug_log_disabled_toast - ), - Toast.LENGTH_SHORT - ).show() - } - ), - SwitchItem( - icon = Icons.Default.Delete, - title = stringResource(R.string.intercept_uninstall_title), - subtitle = stringResource(R.string.intercept_uninstall_desc), - isChecked = interceptUninstallEnabled, - onCheckedChange = { newState -> - interceptUninstallEnabled = newState - saveInterceptUninstallEnabled(newState) - Toast.makeText( - context, - if (newState) context.getString(R.string.intercept_uninstall_enabled_toast) else context.getString( - R.string.intercept_uninstall_disabled_toast - ), - Toast.LENGTH_SHORT - ).show() - } + items = buildList { + add( + SwitchItem( + icon = Icons.Default.Info, + title = stringResource(R.string.hide_icon_title), + subtitle = stringResource(R.string.hide_icon_desc), + isChecked = hideIcon, + onCheckedChange = { newState -> + hideIcon = newState + saveHideIconState(newState) + Toast.makeText( + context, + if (newState) context.getString(R.string.hide_icon_enabled_toast) else context.getString( + R.string.hide_icon_disabled_toast + ), + Toast.LENGTH_SHORT + ).show() + } + ) ) - ) + add( + SwitchItem( + icon = Icons.Default.DateRange, + title = stringResource(R.string.debug_log_title), + subtitle = stringResource(R.string.debug_log_desc), + isChecked = debugLogEnabled, + onCheckedChange = { newState -> + debugLogEnabled = newState + saveDebugLogEnabled(newState) + Toast.makeText( + context, + if (newState) context.getString(R.string.debug_log_enabled_toast) else context.getString( + R.string.debug_log_disabled_toast + ), + Toast.LENGTH_SHORT + ).show() + } + ) + ) + add( + SwitchItem( + icon = Icons.Default.Delete, + title = stringResource(R.string.intercept_uninstall_title), + subtitle = stringResource(R.string.intercept_uninstall_desc), + isChecked = interceptUninstallEnabled, + onCheckedChange = { newState -> + interceptUninstallEnabled = newState + saveInterceptUninstallEnabled(newState) + Toast.makeText( + context, + if (newState) context.getString(R.string.intercept_uninstall_enabled_toast) else context.getString( + R.string.intercept_uninstall_disabled_toast + ), + Toast.LENGTH_SHORT + ).show() + } + ) + ) + } ) } + item { + Card( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.medium, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) + ), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + Column { + SettingsSwitchRow( + item = SwitchItem( + icon = Icons.Default.Notifications, + title = stringResource(R.string.intercept_session_install_title), + subtitle = stringResource(R.string.intercept_session_install_desc), + isChecked = interceptSessionInstallEnabled, + onCheckedChange = { newState -> + interceptSessionInstallEnabled = newState + saveInterceptSessionInstallEnabled(newState) + Toast.makeText( + context, + if (newState) context.getString(R.string.intercept_session_install_enabled_toast) else context.getString( + R.string.intercept_session_install_disabled_toast + ), + Toast.LENGTH_SHORT + ).show() + } + ), + showDivider = Build.VERSION.SDK_INT >= 34 && interceptSessionInstallEnabled + ) + + AnimatedVisibility( + visible = Build.VERSION.SDK_INT >= 34 && interceptSessionInstallEnabled, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + SettingsSwitchRow( + item = SwitchItem( + icon = Icons.Default.Lock, + title = stringResource(R.string.fix_permissions_title), + subtitle = stringResource(R.string.fix_permissions_desc), + isChecked = fixPermissionsEnabled, + onCheckedChange = { newState -> + fixPermissionsEnabled = newState + saveFixPermissionsEnabled(newState) + Toast.makeText( + context, + if (newState) context.getString(R.string.fix_permissions_enabled_toast) else context.getString( + R.string.fix_permissions_disabled_toast + ), + Toast.LENGTH_SHORT + ).show() + } + ), + showDivider = false + ) + } + } + } + } + item { InstructionCard() } diff --git a/app/src/main/java/io/github/chimio/inxlocker/util/IntentAnalyzer.kt b/app/src/main/java/io/github/chimio/inxlocker/util/IntentAnalyzer.kt index 76ee63f..08b8fad 100644 --- a/app/src/main/java/io/github/chimio/inxlocker/util/IntentAnalyzer.kt +++ b/app/src/main/java/io/github/chimio/inxlocker/util/IntentAnalyzer.kt @@ -18,6 +18,7 @@ object IntentAnalyzer { YLog.d(TAG, "Intent type: ${intent.type}") YLog.d(TAG, "Intent data: ${intent.data}") YLog.d(TAG, "Intent clipData: ${intent.clipData}") + YLog.d(TAG, "Intent extras: ${formatExtras(intent)}") if (intent.action !in allowedActions) return Result.ShouldNotRedirect @@ -35,6 +36,13 @@ object IntentAnalyzer { Result.ShouldNotRedirect } } + "android.content.pm.action.CONFIRM_INSTALL" -> { + if (PrefsProvider.getBoolean("intercept_session_install", false)) { + Result.ShouldRedirect + } else { + Result.ShouldNotRedirect + } + } else -> Result.ShouldRedirect } } ?: Result.ShouldNotRedirect @@ -42,6 +50,15 @@ object IntentAnalyzer { Result.ShouldNotRedirect } + fun formatExtras(intent: Intent): String { + return intent.extras?.let { bundle -> + if (bundle.isEmpty) "{}" + else bundle.keySet().joinToString(", ", "{", "}") { key -> + "$key=${bundle.get(key)}" + } + } ?: "null" + } + private fun hasValidAction(intent: Intent): Boolean { return intent.action in listOf( Intent.ACTION_INSTALL_PACKAGE, diff --git a/app/src/main/java/io/github/chimio/inxlocker/util/IntentRedirector.kt b/app/src/main/java/io/github/chimio/inxlocker/util/IntentRedirector.kt index c3b0888..8013a84 100644 --- a/app/src/main/java/io/github/chimio/inxlocker/util/IntentRedirector.kt +++ b/app/src/main/java/io/github/chimio/inxlocker/util/IntentRedirector.kt @@ -52,6 +52,7 @@ object IntentRedirector { YLog.i(tag, "Intent重定向:") YLog.i(tag, "- 目标 package: ${current.`package` ?: "<系统默认>"}") YLog.i(tag, "- Intent action: ${current.action}") + YLog.i(tag, "- Intent extras: ${IntentAnalyzer.formatExtras(current)}") if (current.action == ACTION_DELETE || current.action == ACTION_UNINSTALL_PACKAGE) { YLog.i(tag, "- 拦截卸载Intent,重定向到指定安装器") } diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 7cd4d78..4d91b07 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -25,6 +25,14 @@ 仅拦截Intent,需要安装器支持卸载 拦截卸载已开启 拦截卸载已关闭 + 拦截会话安装 + 拦截 CONFIRM_INSTALL 请求 + 拦截会话安装已开启 + 拦截会话安装已关闭 + 修复权限 + 在 Android 14+ 上强制通过会话路径权限检查 + 权限修复已开启 + 权限修复已关闭 使用说明 确保模块已在 Xposed 框架中激活\n选择默认安装器后,系统会优先使用该应用安装 APK\n开启拦截卸载后,卸载操作会重定向到指定安装器\n可在 Xposed 管理器中找到应用设置 关闭 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64688a2..1272424 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,14 @@ Only intercept Intent, requires installer to support uninstall Uninstall interception enabled Uninstall interception disabled + Intercept Session Install + Intercept CONFIRM_INSTALL requests + Session install interception enabled + Session install interception disabled + Fix Permissions + Force permission check to pass on Android 14+ for session paths + Permission fix enabled + Permission fix disabled Instructions Ensure the module is activated in Xposed framework\nAfter selecting default installer, system will prioritize using that app to install APKs\nWhen uninstall interception is enabled, uninstall operations will be redirected to the specified installer\nFind app settings in Xposed Manager Close From 11df42ea2993d47a18f1b1ef63c9c718a97eec69 Mon Sep 17 00:00:00 2001 From: wxxsfxyzm <65166044+wxxsfxyzm@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:43:12 +0800 Subject: [PATCH 2/2] fix: automatically disable fixPermissions when interceptSessionInstall is disabled - Update `MainActivity.kt` to turn off and save `fixPermissionsEnabled` state if `interceptSessionInstallEnabled` is toggled off. --- .../io/github/chimio/inxlocker/ui/activity/MainActivity.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/io/github/chimio/inxlocker/ui/activity/MainActivity.kt b/app/src/main/java/io/github/chimio/inxlocker/ui/activity/MainActivity.kt index 4091c5c..1711790 100644 --- a/app/src/main/java/io/github/chimio/inxlocker/ui/activity/MainActivity.kt +++ b/app/src/main/java/io/github/chimio/inxlocker/ui/activity/MainActivity.kt @@ -377,6 +377,10 @@ class MainActivity : ComponentActivity() { onCheckedChange = { newState -> interceptSessionInstallEnabled = newState saveInterceptSessionInstallEnabled(newState) + if (!newState) { + fixPermissionsEnabled = false + saveFixPermissionsEnabled(false) + } Toast.makeText( context, if (newState) context.getString(R.string.intercept_session_install_enabled_toast) else context.getString(