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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# 功能

- **通知**(目标 `com.android.systemui` / `com.android.settings` / `system`)— 功能 → 系统界面 → 通知,提供锁屏保留、锁屏显示、悬浮显示、受保护通知修改、重要程度、历史折叠与每应用数量限制七项独立开关,默认均关闭。使用和验证说明见 [通知功能说明](docs/NOTIFICATION_PORT.md)。
- **应用包管理组件净化(实验性)**(目标 `com.miui.packageinstaller`)— 功能 → 实验性功能 → 应用包管理组件净化,提供推广、风险检测、云配置、安全守护提示、频繁安装检查与安装信息上传控制六项独立开关,默认关闭;保留诊断供反馈。功能代码与资源集中管理,使用现有基础扩展接口,可随整项提交撤回。使用范围与状态含义见 [应用包管理组件净化说明](docs/experimental/packageinstaller/EXPERIMENT.md)。

**模块功能**(目标应用 `com.android.systemui` / `com.miui.securitycenter`)

Expand Down
13 changes: 13 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@
android:name=".ui.screen.features.SecurityCenterActivity"
android:exported="false"
android:theme="@style/Theme.HyperRefine" />
<activity
android:name=".experimental.packageinstaller.PackageInstallerActivity"
android:exported="false"
android:theme="@style/Theme.HyperRefine" />
<activity
android:name=".ui.screen.features.DeviceConnectActivity"
android:exported="false"
Expand All @@ -151,6 +155,15 @@
android:name=".ui.screen.safemode.SafeModeActivity"
android:exported="false"
android:theme="@style/Theme.HyperRefine" />
<!-- 安装器专用诊断:Receiver 严格校验已安装安装器 UID、构建、版本和开机号。 -->
<receiver
android:name=".experimental.packageinstaller.InstallerDiagnosticReceiver"
android:exported="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="cn.ianzb.hyperrefine.action.INSTALLER_DIAGNOSTIC" />
</intent-filter>
</receiver>
<!-- 接收目标进程的 Hook 生效回报。来源经发送者 UID / 包名 / 版本校验。 -->
<receiver
android:name=".xposed.HookStatusReceiver"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package cn.ianzb.hyperrefine.experimental.packageinstaller

import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import cn.ianzb.hyperrefine.R
import cn.ianzb.hyperrefine.hook.packageinstaller.InstallerCodeIdentity
import cn.ianzb.hyperrefine.hook.packageinstaller.InstallerDiagnosticContract as Contract
import cn.ianzb.hyperrefine.hook.packageinstaller.PackageInstallerKeys
import cn.ianzb.hyperrefine.prefs.ConfigState
import cn.ianzb.hyperrefine.prefs.PrefsStore
import cn.ianzb.hyperrefine.xposed.XposedServiceManager
import java.text.DateFormat
import java.util.Date
import java.util.zip.ZipFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Text as MiuixText

private data class InstallerLocalProbe(
val checkedAt: Long,
val bootCount: Int,
val moduleVersion: String,
val installerVersion: String,
val localMask: String,
val remoteMask: String,
val identity: String,
val viewHandlers: String,
val installHandlers: String,
)

private fun collectInstallerHandlers(pm: PackageManager, action: String): String = runCatching {
val intent = Intent(action).setDataAndType(
"content://cn.ianzb.hyperrefine.diagnostic/probe.apk".toUri(),
"application/vnd.android.package-archive",
)
val resolved = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)?.activityInfo
val candidates = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
.mapNotNull { it.activityInfo?.let { activity -> "${activity.packageName}/${activity.name}".take(384) } }
.distinct().sorted()
val chosen = resolved?.let { "${it.packageName}/${it.name}".take(384) } ?: "none"
"resolved=$chosen; candidates=${candidates.size}\n${candidates.take(12).joinToString("\n")}"
}.getOrElse { "unavailable (${it.javaClass.simpleName})" }

/** Read installed component metadata and resolve two fixed probe intents. Never launch or open the URI. */
private fun collectInstallerProbe(context: Context): InstallerLocalProbe {
val pm = context.packageManager
val module = runCatching {
val info = pm.getPackageInfo(context.packageName, 0)
"${info.versionName?.take(128)} (${info.longVersionCode})"
}.getOrElse { "unavailable (${it.javaClass.simpleName})" }
val installer = runCatching { pm.getPackageInfo(Contract.TARGET_PACKAGE, 0) }
val version = installer.fold(
{ "${it.versionName?.take(128)} (${it.longVersionCode})" },
{ "unavailable (${it.javaClass.simpleName})" },
)
val identity = runCatching {
val info = pm.getApplicationInfo(Contract.TARGET_PACKAGE, 0)
val result = ZipFile(info.sourceDir).use { InstallerCodeIdentity().inspect(it) }
"readable=${result.readable}; diagnostic_only=true; splits=${info.splitSourceDirs?.size ?: 0}\n${result.diagnostic.take(4_096)}"
}.getOrElse { "unavailable (${it.javaClass.simpleName})" }
val localMask = runCatching {
val mask = PackageInstallerKeys.ALL.mapIndexed { index, key ->
if (PrefsStore.getBoolean(key, false)) 1 shl index else 0
}.fold(0, Int::or)
"0x${mask.toString(16).padStart(2, '0')}"
}.getOrElse { "unavailable (${it.javaClass.simpleName})" }
val remoteMask = runCatching {
val service = XposedServiceManager.getService() ?: return@runCatching "unavailable (service unbound)"
val prefs = service.getRemotePreferences(PrefsStore.REMOTE_GROUP)
val mask = PackageInstallerKeys.ALL.mapIndexed { index, key ->
if (prefs.getBoolean(PrefsStore.key(key), false)) 1 shl index else 0
}.fold(0, Int::or)
val present = PackageInstallerKeys.ALL.count { prefs.contains(PrefsStore.key(it)) }
"0x${mask.toString(16).padStart(2, '0')}; present=$present/6"
}.getOrElse { "unavailable (${it.javaClass.simpleName})" }
return InstallerLocalProbe(
System.currentTimeMillis(), InstallerDiagnosticStore.currentBoot(context), module, version, localMask, remoteMask, identity,
collectInstallerHandlers(pm, Intent.ACTION_VIEW), collectInstallerHandlers(pm, Intent.ACTION_INSTALL_PACKAGE),
)
}

@Composable
internal fun InstallerDiagnosticCard() {
val context = LocalContext.current
val resources = LocalResources.current
var refreshToken by remember { mutableIntStateOf(0) }
var probe by remember { mutableStateOf<InstallerLocalProbe?>(null) }
var loading by remember { mutableStateOf(false) }
val lastReport by InstallerDiagnosticStore.latest.collectAsState()
val serviceBound = XposedServiceManager.isActivated
val inScope = Contract.TARGET_PACKAGE in XposedServiceManager.scope
val remoteAttached = PrefsStore.isRemoteAttached()
val enabled = PackageInstallerKeys.ALL.count { ConfigState.bool(it, false) }

LaunchedEffect(refreshToken) {
loading = true
InstallerDiagnosticStore.reload(context)
XposedServiceManager.refreshScope()
probe = withContext(Dispatchers.IO) { collectInstallerProbe(context.applicationContext) }
loading = false
}

val text = buildString {
appendLine(resources.getString(R.string.installer_diag_build, probe?.moduleVersion ?: "…", Contract.BUILD_ID))
appendLine(resources.getString(R.string.installer_diag_service, serviceBound.toString(), remoteAttached.toString(), inScope.toString()))
appendLine(resources.getString(R.string.installer_diag_settings, enabled, Build.VERSION.SDK_INT))
probe?.let { local ->
appendLine(resources.getString(R.string.installer_diag_checked_at, formatDiagnosticTime(local.checkedAt), local.bootCount))
appendLine(resources.getString(R.string.installer_diag_masks, local.localMask, local.remoteMask))
appendLine(resources.getString(R.string.installer_diag_installer, local.installerVersion))
appendLine(resources.getString(R.string.installer_diag_identity, local.identity))
appendLine(resources.getString(R.string.installer_diag_view_handlers, local.viewHandlers))
appendLine(resources.getString(R.string.installer_diag_install_handlers, local.installHandlers))
}
val report = lastReport
if (report == null) {
appendLine(resources.getString(R.string.installer_diag_no_report))
} else {
appendLine(resources.getString(
R.string.installer_diag_latest_report,
formatDiagnosticTime(report.receivedAtMillis), report.sourcePid,
((System.currentTimeMillis() - report.receivedAtMillis).coerceAtLeast(0) / 1_000L),
))
report.trace.forEach { appendLine(it) }
appendLine(resources.getString(R.string.installer_diag_not_live))
}
append(resources.getString(R.string.installer_diag_boundary))
}.let { if (it.length <= 32_768) it else it.take(32_384) + "\n" + resources.getString(R.string.installer_diag_truncated) }

Card(modifier = Modifier.padding(horizontal = 12.dp).padding(bottom = 12.dp)) {
Column(modifier = Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
MiuixText(text = stringResource(R.string.installer_diag_title), style = MiuixTheme.textStyles.body2)
SelectionContainer {
MiuixText(text = text, style = MiuixTheme.textStyles.footnote2, color = MiuixTheme.colorScheme.onSurfaceVariantSummary)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(
text = stringResource(if (loading) R.string.installer_diag_loading else R.string.installer_diag_refresh),
onClick = { if (!loading) refreshToken++ },
minWidth = 0.dp,
)
TextButton(
text = stringResource(R.string.installer_diag_copy),
onClick = {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("HyperRefine installer diagnostics", text))
Toast.makeText(context, R.string.installer_diag_copied, Toast.LENGTH_SHORT).show()
},
minWidth = 0.dp,
)
TextButton(
text = stringResource(R.string.installer_diag_clear),
onClick = { InstallerDiagnosticStore.clear(context) },
minWidth = 0.dp,
)
}
}
}
}

private fun formatDiagnosticTime(value: Long): String =
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM).format(Date(value))
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cn.ianzb.hyperrefine.experimental.packageinstaller

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import cn.ianzb.hyperrefine.hook.packageinstaller.InstallerDiagnosticContract as Contract

/** Only the installed installer's real UID may submit a current-build, current-boot report. */
class InstallerDiagnosticReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Contract.ACTION) return
val sender = runCatching { sentFromUid }.getOrDefault(-1)
val expectedUid = runCatching {
context.packageManager.getApplicationInfo(Contract.TARGET_PACKAGE, 0).uid
}.getOrNull()
if (sender < 0 || expectedUid == null || sender != expectedUid) return
// Untrusted extras may have wrong parcel/value types. Reject the whole report.
runCatching {
val report = InstallerDiagnosticValidation.Envelope(
buildId = intent.getStringExtra(Contract.EXTRA_BUILD_ID),
moduleVersion = intent.getLongExtra(Contract.EXTRA_MODULE_VERSION, 0L),
bootCount = intent.getIntExtra(Contract.EXTRA_BOOT_COUNT, -1),
sourcePackage = intent.getStringExtra(Contract.EXTRA_SOURCE_PACKAGE),
sourceProcess = intent.getStringExtra(Contract.EXTRA_SOURCE_PROCESS),
sourcePid = intent.getIntExtra(Contract.EXTRA_SOURCE_PID, 0),
generatedElapsed = intent.getLongExtra(Contract.EXTRA_GENERATED_ELAPSED, -1L),
trace = intent.getStringArrayListExtra(Contract.EXTRA_TRACE) ?: return,
)
if (InstallerDiagnosticValidation.accepts(
report, sender, expectedUid, InstallerDiagnosticStore.currentVersion(context),
InstallerDiagnosticStore.currentBoot(context), SystemClock.elapsedRealtime(),
)
) {
InstallerDiagnosticStore.record(context, report)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cn.ianzb.hyperrefine.experimental.packageinstaller

import android.content.Context
import android.provider.Settings
import androidx.core.content.edit
import cn.ianzb.hyperrefine.hook.packageinstaller.InstallerDiagnosticContract as Contract
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.json.JSONArray

/** A single bounded report in module-private data; independent of HookStatusStore. */
internal object InstallerDiagnosticStore {
data class Report(val receivedAtMillis: Long, val sourcePid: Int, val trace: List<String>)

private const val PREFS = "installer_diagnostic_r3"
private val mutableLatest = MutableStateFlow<Report?>(null)
val latest: StateFlow<Report?> = mutableLatest.asStateFlow()

fun currentBoot(context: Context): Int = runCatching {
Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT, -1)
}.getOrDefault(-1)

fun currentVersion(context: Context): Long = runCatching {
context.packageManager.getPackageInfo(context.packageName, 0).longVersionCode
}.getOrDefault(0L)

fun reload(context: Context) {
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
val boot = currentBoot(context)
val version = currentVersion(context)
val scoped = boot >= 0 && version > 0 &&
prefs.getInt("boot", -1) == boot && prefs.getLong("version", 0) == version &&
prefs.getString("build", null) == Contract.BUILD_ID
if (!scoped) {
prefs.edit { clear() }
mutableLatest.value = null
return
}
mutableLatest.value = runCatching {
val json = JSONArray(prefs.getString("trace", "[]"))
check(json.length() in 1..Contract.MAX_TRACE_LINES)
val trace = List(json.length()) { json.getString(it) }
check(InstallerDiagnosticValidation.validTrace(trace))
Report(prefs.getLong("received", 0), prefs.getInt("pid", 0), trace)
}.getOrNull()
if (mutableLatest.value == null) prefs.edit { clear() }
}

fun record(context: Context, report: InstallerDiagnosticValidation.Envelope) {
val received = System.currentTimeMillis()
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
clear()
putInt("boot", report.bootCount)
putLong("version", report.moduleVersion)
putString("build", report.buildId)
putLong("received", received)
putInt("pid", report.sourcePid)
putString("trace", JSONArray(report.trace).toString())
}
mutableLatest.value = Report(received, report.sourcePid, report.trace.toList())
}

fun clear(context: Context) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit { clear() }
mutableLatest.value = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cn.ianzb.hyperrefine.experimental.packageinstaller

import cn.ianzb.hyperrefine.hook.packageinstaller.InstallerDiagnosticContract as Contract

/** Pure validation shared by the receiver and host unit tests. No anonymous-sender fallback. */
internal object InstallerDiagnosticValidation {
data class Envelope(
val buildId: String?,
val moduleVersion: Long,
val bootCount: Int,
val sourcePackage: String?,
val sourceProcess: String?,
val sourcePid: Int,
val generatedElapsed: Long,
val trace: List<String>,
)

fun accepts(
report: Envelope,
senderUid: Int,
installerUid: Int?,
currentVersion: Long,
currentBoot: Int,
nowElapsed: Long,
): Boolean {
if (senderUid < 0 || installerUid == null || senderUid != installerUid) return false
if (report.sourcePackage != Contract.TARGET_PACKAGE || report.sourceProcess != Contract.TARGET_PACKAGE) return false
if (report.buildId != Contract.BUILD_ID || currentVersion <= 0 || report.moduleVersion != currentVersion) return false
if (currentBoot < 0 || report.bootCount != currentBoot || report.sourcePid <= 0) return false
if (report.generatedElapsed < 0 || report.generatedElapsed > nowElapsed + 2_000L) return false
if (nowElapsed - report.generatedElapsed > Contract.MAX_REPORT_AGE_MS) return false
return validTrace(report.trace)
}

fun validTrace(trace: List<String>): Boolean =
trace.isNotEmpty() && trace.size <= Contract.MAX_TRACE_LINES && trace.all { line ->
line.isNotBlank() && line.length <= Contract.MAX_LINE_CHARS &&
line.none { it < ' ' || it == '\u007f' }
}
}
Loading
Loading