From 2424481f182961f2d04997ddb233223e7e83c27e Mon Sep 17 00:00:00 2001 From: Ashurinnn <77261372+Ashurinnn@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:28:08 +0100 Subject: [PATCH] feat(systemui): add Wi-Fi activity and mobile network type hiding Add two opt-in status-bar controls with searchable settings and native HyperOS presentation hooks. Keep the dual-SIM implementation and tests commented out for later repair, and document the verified feature scope. --- README.md | 2 + app/src/main/AndroidManifest.xml | 2 + .../ui/screen/about/LicensePage.kt | 2 +- .../ui/screen/features/FeaturesPage.kt | 7 + .../screen/features/NetworkIconsActivity.kt | 38 ++ .../ui/screen/features/StatusBarOptions.kt | 23 ++ .../ui/screen/features/SystemUiActivity.kt | 7 +- app/src/main/res/values-en/strings.xml | 5 + app/src/main/res/values/strings.xml | 5 + changelog.md | 1 + docs/CUSTOMIZE.md | 2 + docs/STATUSBAR_PORT.md | 30 ++ .../hyperrefine/hook/systemui/SystemUiLoad.kt | 5 + .../statusbar/DualSimArtworkPolicy.kt | 61 +++ .../statusbar/DualSimRuntimeSupport.kt | 58 +++ .../systemui/statusbar/DualSimSignalHook.kt | 363 ++++++++++++++++++ .../systemui/statusbar/MobileTypeHooks.kt | 28 ++ .../statusbar/StackedSignalDrawable.kt | 65 ++++ .../hook/systemui/statusbar/StatusBarKeys.kt | 16 + .../systemui/statusbar/StatusBarPolicy.kt | 29 ++ .../systemui/statusbar/StatusBarSupport.kt | 99 +++++ .../hook/systemui/statusbar/StatusBarViews.kt | 21 + .../systemui/statusbar/WifiIndicatorHooks.kt | 39 ++ .../statusbar/DualSimArtworkPolicyTest.kt | 84 ++++ .../statusbar/DualSimRuntimeSupportTest.kt | 96 +++++ .../systemui/statusbar/StatusBarPolicyTest.kt | 53 +++ 26 files changed, 1139 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/NetworkIconsActivity.kt create mode 100644 app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/StatusBarOptions.kt create mode 100644 docs/STATUSBAR_PORT.md create mode 100644 hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimArtworkPolicy.kt create mode 100644 hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimRuntimeSupport.kt create mode 100644 hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimSignalHook.kt create mode 100644 hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/MobileTypeHooks.kt create mode 100644 hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StackedSignalDrawable.kt create mode 100644 hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarKeys.kt create mode 100644 hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarPolicy.kt create mode 100644 hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarSupport.kt create mode 100644 hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarViews.kt create mode 100644 hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/WifiIndicatorHooks.kt create mode 100644 hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimArtworkPolicyTest.kt create mode 100644 hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimRuntimeSupportTest.kt create mode 100644 hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarPolicyTest.kt diff --git a/README.md b/README.md index 30ad26a..d128b82 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,8 @@ HyperRefine 基于个人 Android 模块开发脚手架 MiuixGuiTemplate 创建 通知功能独立实现,未引入参考模块的代码、资源或原生库;参考项目的来源与许可证已登记到应用内参考项目分组。 +状态栏的 Wi-Fi 活动指示器隐藏与移动网络类型隐藏同样独立实现,参考来源统一登记在应用内致谢页;功能范围、验证和暂未启用的双卡双行源码见 [状态栏功能说明](docs/STATUSBAR_PORT.md)。 +
# 赞助支持 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4c5cce9..5d87c8b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -73,6 +73,8 @@ android:name=".ui.screen.features.NotificationsActivity" android:exported="false" android:theme="@style/Theme.HyperRefine" /> + = listOf( LibraryInfo( "Pengeek(米客)", "OS4-26.10.13-test", - "GPL-3.0,仅通知功能思路参考(独立实现,未复用其代码或资源)", + "GPL-3.0,通知与状态栏功能思路参考(独立实现,未复用其代码或资源)", "https://github.com/MonwF/customiuizer", ), LibraryInfo( diff --git a/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/FeaturesPage.kt b/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/FeaturesPage.kt index ead76e8..17eee87 100644 --- a/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/FeaturesPage.kt +++ b/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/FeaturesPage.kt @@ -72,6 +72,7 @@ fun FeaturesPageView( HookSubPage( titleRes = R.string.feature_system_ui, specs = listOf( + specByKey(specs, KEY_STATUS_BAR), specByKey(specs, KEY_GLASS), specByKey(specs, KEY_CC_RADIUS), specByKey(specs, PercentLocation.entryKey(PercentLocation.CC_BRIGHTNESS)), @@ -88,6 +89,11 @@ fun FeaturesPageView( ), onOpen = { context.startActivity(Intent(context, SystemUiActivity::class.java)) }, subPages = listOf( + HookSubPage( + titleRes = R.string.sb_title, + specs = statusBarSpecs(specs), + onOpen = { context.startActivity(Intent(context, NetworkIconsActivity::class.java)) }, + ), HookSubPage( titleRes = R.string.notifications_title, specs = notificationSpecs(specs), @@ -1223,6 +1229,7 @@ internal fun featureSpecs(): List { specs += buildAutoBalanceSpecs(systemUi) specs += ccRadiusSpecs(systemUi) specs += notificationFeatureSpecs() + specs += statusBarFeatureSpecs() return specs } diff --git a/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/NetworkIconsActivity.kt b/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/NetworkIconsActivity.kt new file mode 100644 index 0000000..b12749a --- /dev/null +++ b/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/NetworkIconsActivity.kt @@ -0,0 +1,38 @@ +package cn.ianzb.hyperrefine.ui.screen.features + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import cn.ianzb.hyperrefine.R +import cn.ianzb.hyperrefine.ui.component.pref.HookOptionView +import cn.ianzb.hyperrefine.ui.component.pref.HookSection +import cn.ianzb.hyperrefine.ui.component.pref.HookSectionCard +import cn.ianzb.hyperrefine.ui.screen.subpage.BaseSubPageActivity +import cn.ianzb.hyperrefine.ui.util.LocalSubPageScrollBehavior +import cn.ianzb.hyperrefine.ui.util.pageScrollModifiers + +class NetworkIconsActivity : BaseSubPageActivity() { + override val titleRes = R.string.sb_title + @Composable + override fun SubPageContent(isBlurEnabled: Boolean, contentPadding: PaddingValues) { + val specs = statusBarSpecs(featureSpecs()) + StatusBarSections(listOf( + HookSection(R.string.sb_network_section, specs = specs.take(1)), + HookSection(R.string.sb_mobile_type_section, specs = specs.drop(1)), + ), contentPadding) + } +} + +@Composable +private fun StatusBarSections(sections: List, padding: PaddingValues) { + val scroll = LocalSubPageScrollBehavior.current + LazyColumn(modifier = Modifier.fillMaxSize().then( + if (scroll != null) Modifier.pageScrollModifiers(showTopAppBar = true, topAppBarScrollBehavior = scroll) else Modifier, + ), contentPadding = padding) { + sections.forEach { section -> item { + HookSectionCard(section) { section.specs.forEach { spec -> HookOptionView(spec) } } + } } + } +} diff --git a/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/StatusBarOptions.kt b/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/StatusBarOptions.kt new file mode 100644 index 0000000..6cca6fb --- /dev/null +++ b/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/StatusBarOptions.kt @@ -0,0 +1,23 @@ +package cn.ianzb.hyperrefine.ui.screen.features + +import cn.ianzb.hyperrefine.R +import cn.ianzb.hyperrefine.hook.systemui.statusbar.StatusBarKeys as Keys +import cn.ianzb.hyperrefine.prefs.OptionSpec +import cn.ianzb.hyperrefine.prefs.OptionType + +const val KEY_STATUS_BAR = "feature_statusbar" + +internal fun statusBarFeatureSpecs(): List { + fun toggle(key: String, title: Int) = OptionSpec( + key = key, type = OptionType.SWITCH, titleRes = title, + defaultBoolean = false, targetPackages = listOf(Keys.TARGET), showStatus = true, + ) + return listOf( + OptionSpec(key = KEY_STATUS_BAR, type = OptionType.ARROW, titleRes = R.string.sb_title), + toggle(Keys.WIFI_ACTIVITY, R.string.sb_wifi_hide_activity), + toggle(Keys.HIDE_TYPE, R.string.sb_mobile_hide_type), + ) +} + +internal fun statusBarSpecs(specs: List) = + Keys.ALL.map { key -> specs.first { it.key == key } } diff --git a/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/SystemUiActivity.kt b/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/SystemUiActivity.kt index 144a271..264a28f 100644 --- a/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/SystemUiActivity.kt +++ b/app/src/main/java/cn/ianzb/hyperrefine/ui/screen/features/SystemUiActivity.kt @@ -39,6 +39,10 @@ class SystemUiActivity : BaseSubPageActivity() { ) { val context = LocalContext.current val scrollBehavior = LocalSubPageScrollBehavior.current + val statusBarSection = HookSection( + titleRes = R.string.sb_title, + specs = listOf(featureSpec(KEY_STATUS_BAR)), + ) val controlCenterSection = HookSection( titleRes = R.string.feature_control_center, specs = listOf( @@ -81,6 +85,7 @@ class SystemUiActivity : BaseSubPageActivity() { val openSubPage: (String) -> Unit = { key -> when (key) { + KEY_STATUS_BAR -> context.startActivity(Intent(context, NetworkIconsActivity::class.java)) KEY_GLASS -> context.startActivity(Intent(context, GlassActivity::class.java)) KEY_CC_RADIUS -> @@ -125,7 +130,7 @@ class SystemUiActivity : BaseSubPageActivity() { ), contentPadding = contentPadding, ) { - listOf(controlCenterSection, volumeSection, lockAodSection, wallpaperSection, windowSection).forEach { section -> + listOf(statusBarSection, controlCenterSection, volumeSection, lockAodSection, wallpaperSection, windowSection).forEach { section -> item { HookSectionCard(section) { section.specs.forEach { spec -> diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 54e965b..9342725 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -494,4 +494,9 @@ Passkey Browser + Status bar + Network icons + Hide Wi-Fi activity indicators + Mobile network type + Hide network type completely diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 93eb815..fc2a6bb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -494,4 +494,9 @@ 通行密钥 浏览器 + 状态栏 + 网络图标 + 隐藏 Wi-Fi 网络活动指示器 + 移动网络类型 + 完全隐藏网络类型 diff --git a/changelog.md b/changelog.md index 0232d73..d3f18eb 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ ### 新增 +- **状态栏 · 网络图标**(目标 `com.android.systemui`):新增「系统界面 → 状态栏」页,提供「隐藏 Wi-Fi 活动指示器」「完全隐藏网络类型」两项开关,默认关闭;双卡双行实现暂以注释源码保留,等待后续修复。 - **负一屏 · 过渡动画优化**(目标 `com.miui.personalassistant` / system_server):新增「负一屏」功能页。**过渡闪黑修复**消除打开 / 返回快速切换时的纯色闪黑;**上滑合并强制反向**修正返回动画被系统合并时先反向播一半的问题;**放行远程动画权限**(系统服务侧,需重启设备)让负一屏的远程过渡动画完整生效。思路参考 HyperAssistant(非开源,独立实现)。 - **负一屏 · 打开应用动画与过渡遮罩重绘**(目标 `com.miui.personalassistant`):**应用打开动画**让负一屏点开的应用从图标位置缩放展开(options 逐级剥离重试,失败回退框架动画);**过渡遮罩重绘**修复过渡中遮罩显示异常内容。 - **负一屏 · 返回缩回动画**(目标 `com.miui.personalassistant`):关闭应用回到负一屏时,部件快照从全屏缩回原位,接近末尾渐隐;需先开启「应用打开动画」。 diff --git a/docs/CUSTOMIZE.md b/docs/CUSTOMIZE.md index 3543a57..6c63986 100644 --- a/docs/CUSTOMIZE.md +++ b/docs/CUSTOMIZE.md @@ -413,6 +413,8 @@ HookOptionsPage( 通知功能的参考来源已登记到 `LicensePage.kt` 的参考项目分组,注明 GPL-3.0、仅思路参考、未复用代码或资源。新增七项配置和进程范围见 [通知功能说明](NOTIFICATION_PORT.md)。 +状态栏功能的参考来源也在该分组登记;仅借鉴思路,未引入参考模块或小米 SystemUI 的代码、图片及原生库。两项开关的配置、目标进程和注释中的双卡双行实现见 [状态栏功能说明](STATUSBAR_PORT.md)。 + 本模板的定位是**个人 Android 模块项目的脚手架模板**。具体参考与致谢清单统一维护在应用内「第三方许可证与致谢」页面(`LicensePage.kt` → `licenses_section_refs` 分组),衍生项目**可增不可删**。 > 文档不展开对具体模块的借鉴说明;复用任何第三方代码前须先核对许可证,无许可证 / 非开源项目只能借鉴思路并致谢,完整流程见 [模块开发工作流](WORKFLOW.md)。 diff --git a/docs/STATUSBAR_PORT.md b/docs/STATUSBAR_PORT.md new file mode 100644 index 0000000..899c3eb --- /dev/null +++ b/docs/STATUSBAR_PORT.md @@ -0,0 +1,30 @@ +# 状态栏功能说明 + +入口:**功能 → 系统界面 → 状态栏**。当前仅开放以下两项开关,默认关闭,目标均为 `com.android.systemui`;修改后重新加载系统界面进程生效。 + +| 选项 | 配置键 | 效果 | +|---|---|---| +| 隐藏 Wi-Fi 网络活动指示器 | `statusbar_wifi_hide_activity` | 隐藏 Wi-Fi 的收发活动图标,保留信号与连接显示 | +| 完全隐藏网络类型 | `statusbar_mobile_hide_type` | 隐藏移动网络类型图片、独立文字和特殊 5G 标识,保留原生信号布局 | + +## 实现与验证 + +Wi-Fi Hook 在原生绑定与活动更新之后将对应活动视图隐藏,避免后续收发更新再次显示。移动网络类型 Hook 在原生蜂窝模型创建后,将四个类型显示 Flow 替换为宿主自身的只读 false Flow;兼容宿主经过优化的一参数构造与普通二参数构造。 + +配置通过项目既有 `OptionSpec`、偏好与 Hook 状态机制接入;状态栏子页面继承 `BaseSubPageActivity`,登记到父页搜索,并使用 `NetworkIconsActivity`。没有新增权限、作用域或原生依赖。 + +接口静态核对基于提供的 HyperOS `OS4.0.22.0.XPNCNXM` ROM,SystemUI `17.03.260226.r`;其他 ROM 的私有接口兼容性尚未验证。用户已确认本次精简测试 APK 的两个开关均生效、双 SIM 保持原生布局,从状态栏页下拉通知中心/控制中心时背景显示正常。单元测试覆盖配置键、反射成员解析和两种只读 Flow 构造签名;没有新增临时诊断日志。 + +## 暂不启用双卡双行 + +`hook/.../systemui/statusbar/` 中的 `DualSimSignalHook.kt`、`DualSimRuntimeSupport.kt`、`DualSimArtworkPolicy.kt`、`StackedSignalDrawable.kt`、`StatusBarPolicy.kt`、`StatusBarViews.kt` 及对应双排测试以逐行注释保留,供后续修复参考。相关偏好键和宿主 Pair 解析辅助也已注释。 + +这些文件不生成运行时类;当前没有双排 Hook 注册、配置入口或搜索项,不读取该功能的旧偏好。系统继续使用原生 SIM 信号布局。以后恢复时须重新接入配置与注册,验证各卡信号强度、单双卡切换、数据卡切换、锁屏及主题/明暗表现;不能只取消某个文件的注释就视为可用。 + +此前电脑端导出的 PNG/SVG 不是运行时依赖,未进入仓库或 APK。当前构建没有图标解包、图片落盘或系统资源替换。 + +## 参考与安全边界 + +功能思路参考 Pengeek(米客)`OS4-26.10.13-test`,来源与许可在应用内「关于 → 第三方许可证与致谢」统一登记。实现独立编写,未复用参考模块或小米专有代码/资源;逆向材料仅保留在本地忽略的 `reverse_report/`。项目继续以 AGPL-3.0 分发。 + +新增 Hook 仅修改 SystemUI 进程中的视图可见性与显示 Flow,不请求 Root、不执行 shell、不挂载或重新挂载分区、不操作块设备/AVB,也不向受保护系统分区写文件。安装 APK、项目既有偏好及系统/框架日志仍可能保存普通应用数据,因此此说明不承诺整个模块或临时 Root 环境的所有组件均无写入。 diff --git a/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/SystemUiLoad.kt b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/SystemUiLoad.kt index e18d986..a0fcfaf 100644 --- a/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/SystemUiLoad.kt +++ b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/SystemUiLoad.kt @@ -11,6 +11,9 @@ import cn.ianzb.hyperrefine.hook.systemui.radius.CcRadiusHook import cn.ianzb.hyperrefine.hook.systemui.radius.CcRadiusKeys import cn.ianzb.hyperrefine.hook.systemui.wallpaper.WallpaperDarkDimHook import cn.ianzb.hyperrefine.hook.systemui.wallpaper.WallpaperDimKeys +import cn.ianzb.hyperrefine.hook.systemui.statusbar.HideWifiActivityHook +import cn.ianzb.hyperrefine.hook.systemui.statusbar.MobileIndicatorVisibilityHook +import cn.ianzb.hyperrefine.hook.systemui.statusbar.StatusBarKeys /** * 系统界面(com.android.systemui)目标 Load: @@ -23,6 +26,8 @@ class SystemUiLoad : BaseLoad() { override val targetPackages: List = listOf(TARGET_PACKAGE) override fun onPackageLoaded(target: PackageTarget) { + initHook(HideWifiActivityHook(), HookPrefs.getBoolean(StatusBarKeys.WIFI_ACTIVITY)) + initHook(MobileIndicatorVisibilityHook(), HookPrefs.getBoolean(StatusBarKeys.HIDE_TYPE)) initHook(CcVolumePercentHook(), HookPrefs.getBoolean(CcVolumePercentHook.KEY, false)) initHook(CcBrightnessPercentHook(), HookPrefs.getBoolean(CcBrightnessPercentHook.KEY, false)) initHook(SideVolumePercentHook(), HookPrefs.getBoolean(SideVolumePercentHook.KEY, false)) diff --git a/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimArtworkPolicy.kt b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimArtworkPolicy.kt new file mode 100644 index 0000000..9da9b0e --- /dev/null +++ b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimArtworkPolicy.kt @@ -0,0 +1,61 @@ +// DORMANT: preserved from statusbar.6 (ddb0e59); not compiled or registered. +// Restore only after artwork and device regression testing. Runtime diagnostics removed. +// package cn.ianzb.hyperrefine.hook.systemui.statusbar +// +// import kotlin.math.min +// import kotlin.math.roundToInt +// +// /** Original four-cell artwork geometry and slot ordering, independent of the data SIM. */ +// internal object DualSimArtworkPolicy { +// const val CELL_COUNT = 4 +// private const val INACTIVE_ALPHA = 0.28f +// +// data class Levels(val top: Int, val bottom: Int) +// data class Cell(val row: Int, val column: Int, val left: Float, val top: Float, +// val right: Float, val bottom: Float, val radius: Float) +// +// /** HyperOS has six native levels (0..5); round proportionally to four visible cells. */ +// fun fourCellLevel(nativeLevel: Int?): Int? = nativeLevel?.takeIf { it in 0..5 } +// ?.let { (it * CELL_COUNT / 5f).roundToInt() } +// +// fun levels(primarySlot: Int, secondarySlot: Int, primaryLevel: Int?, secondaryLevel: Int?): Levels? { +// val first = fourCellLevel(primaryLevel) ?: return null +// val second = fourCellLevel(secondaryLevel) ?: return null +// return when { +// primarySlot == 0 && secondarySlot == 1 -> Levels(first, second) +// primarySlot == 1 && secondarySlot == 0 -> Levels(second, first) +// else -> null // Unknown/duplicate slots must not invent a SIM 1 / SIM 2 assignment. +// } +// } +// +// fun cells(width: Float, height: Float, thick: Boolean): List { +// if (!width.isFinite() || !height.isFinite() || width <= 0f || height <= 0f) return emptyList() +// val step = width / CELL_COUNT +// val barWidth = step * if (thick) 0.80f else 0.64f +// // Compact proportions independent of a tall Android signal drawable's intrinsic bounds. +// val artworkHeight = min(height * 0.72f, width * 0.80f) +// val artworkTop = height - artworkHeight +// val bottomHeight = min(artworkHeight * 0.24f, barWidth * 0.92f) +// val bottomTop = height - bottomHeight +// val upperBaseline = bottomTop - artworkHeight * 0.09f +// val upperHeight = upperBaseline - artworkTop +// return (0 until CELL_COUNT).map { column -> +// val left = column * step + (step - barWidth) / 2f +// val barHeight = upperHeight * (0.4f + column * 0.2f) +// val radius = min(barWidth, barHeight) * 0.48f +// Cell(0, column, left, upperBaseline - barHeight, left + barWidth, upperBaseline, radius) +// } + (0 until CELL_COUNT).map { column -> +// val left = column * step + (step - barWidth) / 2f +// val radius = min(barWidth, bottomHeight) * 0.48f +// Cell(1, column, left, bottomTop, left + barWidth, height, radius) +// } +// } +// +// /** Preserve the host's RGB and alpha; inactive cells use the same hue at lower opacity. */ +// fun cellColor(foreground: Int, drawableAlpha: Int, filled: Boolean): Int { +// val hostAlpha = foreground ushr 24 +// val strengthAlpha = if (filled) 1f else INACTIVE_ALPHA +// val alpha = (hostAlpha * drawableAlpha.coerceIn(0, 255) / 255f * strengthAlpha).roundToInt() +// return (foreground and 0x00ffffff) or (alpha shl 24) +// } +// } diff --git a/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimRuntimeSupport.kt b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimRuntimeSupport.kt new file mode 100644 index 0000000..08785ad --- /dev/null +++ b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimRuntimeSupport.kt @@ -0,0 +1,58 @@ +// DORMANT: preserved from statusbar.6 (ddb0e59); not compiled or registered. +// Restore only after artwork and device regression testing. Runtime diagnostics removed. +// package cn.ianzb.hyperrefine.hook.systemui.statusbar +// +// import cn.ianzb.hyperrefine.hook.xposed.Reflect +// import java.lang.reflect.Method +// import java.lang.reflect.Modifier +// import java.util.function.Consumer +// +// /** A later LoadedApk callback must not reinstall the same process's dual-SIM hooks. */ +// internal class DualSimInstallation { +// private var installed = false +// +// @Synchronized +// fun installOnce(install: () -> Unit): Boolean { +// if (installed) return false +// install() // A failed/rolled-back installation remains retryable. +// installed = true +// return true +// } +// } +// +// /** Collect with MobileUiAdapter's actual JavaAdapter, without the removed Dependency.get API. */ +// internal class DualSimFlowObserver { +// private data class Collector(val owner: Any, val method: Method) +// private data class Subscription(val flow: Any, val consumer: Consumer) +// private var collector: Collector? = null +// private val waiting = ArrayDeque() +// +// fun value(flow: Any): Any? = Reflect.callMethod(flow, "getValue") +// +// @Synchronized +// fun attach(adapter: Any): Boolean { +// if (collector?.owner === adapter) return false +// check(collector == null) { "Dual-SIM JavaAdapter changed within one process" } +// val method = StatusBarMembers.methods(adapter.javaClass, "alwaysCollectFlow") { +// !Modifier.isStatic(it.modifiers) && it.parameterCount == 2 && +// !it.parameterTypes[0].isPrimitive && it.parameterTypes[1] == Consumer::class.java +// }.single() +// // Binding can precede MobileUiAdapter.start. Drain only after the real adapter is ready. +// // Remove each subscription after success, so a failed drain does not lose pending work. +// while (waiting.isNotEmpty()) { +// val subscription = waiting.first() +// method.invoke(adapter, subscription.flow, subscription.consumer) +// waiting.removeFirst() +// } +// collector = Collector(adapter, method) +// return true +// } +// +// @Synchronized +// fun observe(flow: Any, onValue: (Any?) -> Unit) { +// val consumer = Consumer { onValue(it) } +// val active = collector +// if (active == null) waiting.addLast(Subscription(flow, consumer)) +// else active.method.invoke(active.owner, flow, consumer) +// } +// } diff --git a/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimSignalHook.kt b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimSignalHook.kt new file mode 100644 index 0000000..f9fe916 --- /dev/null +++ b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimSignalHook.kt @@ -0,0 +1,363 @@ +// DORMANT: preserved from statusbar.6 (ddb0e59); not compiled or registered. +// Restore only after artwork and device regression testing. Runtime diagnostics removed. +// package cn.ianzb.hyperrefine.hook.systemui.statusbar +// +// import android.graphics.Color +// import android.graphics.Bitmap +// import android.graphics.Canvas +// import android.graphics.Rect +// import android.graphics.drawable.Drawable +// import android.os.Handler +// import android.os.Looper +// import android.view.View +// import android.view.ViewGroup +// import android.widget.ImageView +// import cn.ianzb.hyperrefine.hook.base.BaseHook +// import cn.ianzb.hyperrefine.hook.prefs.HookPrefs +// import cn.ianzb.hyperrefine.hook.xposed.Reflect +// import io.github.libxposed.api.XposedInterface.HookHandle +// import java.lang.ref.WeakReference +// import java.util.WeakHashMap +// import java.util.concurrent.atomic.AtomicBoolean +// import kotlin.math.roundToInt +// +// /** Event-driven native-view composition; no polling, telephony changes, or copied signal assets. */ +// class DualSimSignalHook : BaseHook() { +// override val key = StatusBarKeys.DUAL_ROWS +// override fun init() { +// installation.installOnce { installFor(target.classLoader) } +// } +// +// private fun installFor(loader: ClassLoader?) { +// val binder = Reflect.findClass(StatusBarViews.MOBILE_BINDER, loader) +// val binds = StatusBarMembers.methods(binder, "bind") { +// it.parameterCount == 4 && ViewGroup::class.java.isAssignableFrom(it.parameterTypes[0]) && +// it.parameterTypes[1].name == "com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel" && +// it.parameterTypes[2].name == "com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MiuiMobileIconViewModel" && +// it.parameterTypes[3].name == "com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger" +// } +// val adapter = Reflect.findClass("com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter", loader) +// val starts = StatusBarMembers.methods(adapter, "start") { it.parameterCount == 0 } +// val collector = Reflect.findClass(StatusBarViews.MOBILE_BINDER + "\$bind\$1\$1\$4\$2", loader) +// val icon = StatusBarMembers.field(collector, "\$mobile") +// require(ImageView::class.java.isAssignableFrom(icon.type)) +// val emits = StatusBarMembers.methods(collector, "emit") { +// it.parameterCount == 2 && it.parameterTypes[0] == Any::class.java && +// it.parameterTypes[1].name == "kotlin.coroutines.Continuation" +// } +// val themeCollector = Reflect.findClass(StatusBarViews.MOBILE_BINDER + "\$bind\$1\$1\$26\$2", loader) +// val themeIcon = StatusBarMembers.field(themeCollector, "\$mobile") +// require(ImageView::class.java.isAssignableFrom(themeIcon.type)) +// val themeEmits = StatusBarMembers.methods(themeCollector, "emit") { +// it.parameterCount == 2 && it.parameterTypes[0] == Any::class.java && +// it.parameterTypes[1].name == "kotlin.coroutines.Continuation" +// } +// val tintCollector = Reflect.findClass(StatusBarViews.MOBILE_BINDER + "\$bind\$1\$1\$5\$1", loader) +// val tintViews = StatusBarMembers.field(tintCollector, "\$tintViewList") +// require(List::class.java.isAssignableFrom(tintViews.type)) +// val tintUpdates = StatusBarMembers.methods(tintCollector, "invokeSuspend") { +// it.parameterTypes.contentEquals(arrayOf(Any::class.java)) && it.returnType == Any::class.java +// } +// val mobileView = Reflect.findClass("com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView", loader) +// val states = StatusBarMembers.methods(mobileView, "setVisibleState") { +// it.parameterTypes.contentEquals(arrayOf(Int::class.javaPrimitiveType, Boolean::class.javaPrimitiveType)) +// } +// val runtime = DualSignalRuntime(DualSimFlowObserver()) +// val guard = StatusBarGuard(tag) +// val installs = mutableListOf<() -> HookHandle>() +// binds.forEach { method -> installs += { StatusBarMembers.after(method) { param -> guard.run { +// val root = param.args.firstOrNull() as? ViewGroup ?: return@run +// // bind(root, locationModel, miuiModel, logger): the location wrapper has no cell flows. +// val model = param.args.getOrNull(2) ?: return@run +// if (StatusBarViews.isStatusBarMobile(root, model)) runtime.bind(root, model) +// } } } } +// starts.forEach { method -> installs += { StatusBarMembers.after(method) { param -> guard.run { +// param.thisObject?.let(runtime::adapter) +// } } } } +// emits.forEach { method -> installs += { StatusBarMembers.after(method) { param -> guard.run { +// val instance = param.thisObject ?: return@run +// val image = icon.get(instance) as? ImageView ?: return@run +// runtime.emitted(image) +// } } } } +// themeEmits.forEach { method -> installs += { StatusBarMembers.after(method) { param -> guard.run { +// val instance = param.thisObject ?: return@run +// (themeIcon.get(instance) as? ImageView)?.let(runtime::emitted) +// } } } } +// tintUpdates.forEach { method -> installs += { StatusBarMembers.after(method) { param -> guard.run { +// val instance = param.thisObject ?: return@run +// (tintViews.get(instance) as? List<*>)?.filterIsInstance()?.forEach(runtime::emitted) +// } } } } +// states.forEach { method -> installs += { StatusBarMembers.after(method) { param -> +// guard.run { (param.thisObject as? ViewGroup)?.let { runtime.visibleState(it, param.args[0] as Int) } } +// } } } +// StatusBarMembers.install(installs) +// } +// +// private companion object { +// val installation = DualSimInstallation() +// } +// } +// +// private class DualSignalRuntime(private val flows: DualSimFlowObserver) { +// private class Row(root: ViewGroup, image: ImageView, val subId: Int) { +// val root = WeakReference(root) +// val image = WeakReference(image) +// var native: Drawable? = null +// var hiddenByModule = false +// var nativeVisibility = root.visibility +// var iconState = 0 +// var nativeColor = Color.WHITE +// var renderKey: List? = null +// val originalScaleX = image.scaleX +// val originalScaleY = image.scaleY +// val parent = WeakReference(image.parent as? ViewGroup) +// val originalTranslation = (image.parent as? View)?.translationY ?: 0f +// val originalClipChildren = (image.parent as? ViewGroup)?.clipChildren ?: true +// val originalClipPadding = (image.parent as? ViewGroup)?.clipToPadding ?: true +// val group = WeakReference(StatusBarViews.find(root, "mobile_group") as? ViewGroup) +// val originalLeft = group.get()?.paddingLeft ?: 0 +// val originalRight = group.get()?.paddingRight ?: 0 +// var styled = false +// } +// +// private val rows = WeakHashMap() // Accessed on the UI thread only. +// private val observed = WeakHashMap() +// private val signals = mutableMapOf() +// private val slotIndexes = mutableMapOf() +// private val signalLevels = mutableMapOf() +// private var dataSubId = -1 +// private val handler = Handler(Looper.getMainLooper()) +// private val pending = AtomicBoolean() +// private val guard = StatusBarGuard("DualSimSignalHook") +// +// fun adapter(adapter: Any) { +// val hdController = Reflect.getObjectField(adapter, "hdController") ?: error("No mobile HD controller") +// val javaAdapter = Reflect.getObjectField(hdController, "javaAdapter") ?: error("No mobile JavaAdapter") +// flows.attach(javaAdapter) +// val provider = Reflect.getObjectField(adapter, "mobileIconsViewModel") ?: error("No mobile model provider") +// val model = Reflect.callMethod(provider, "get") ?: error("No mobile icons model") +// val interactor = runCatching { +// Reflect.callMethod(Reflect.getObjectField(model, "miuiIntsLazy")!!, "get") +// }.getOrNull() ?: Reflect.getObjectField(model, "miuiInt") ?: error("No mobile interactor") +// val repository = Reflect.getObjectField(interactor, "mobileConnectionsRepo") ?: error("No mobile repository") +// val source = Reflect.getObjectField(repository, "activeMobileDataSubscriptionId") ?: error("No active SIM flow") +// val initial = flows.value(source) as? Int ?: -1 +// synchronized(signals) { dataSubId = initial } +// flows.observe(source) { value -> +// (value as? Int)?.let { synchronized(signals) { dataSubId = it }; schedule() } +// } +// } +// +// fun bind(root: ViewGroup, model: Any) { +// if (rows.containsKey(root)) return +// val image = StatusBarViews.find(root, "mobile_signal") as? ImageView ?: error("No native signal image") +// val subId = Reflect.getObjectField(root, "subId") as? Int ?: error("No SIM subscription id") +// val cell = runCatching { Reflect.callMethod(model, "getCellProvider") }.getOrNull() ?: model +// val signalFlow = Reflect.getObjectField(cell, "signalIconId") ?: error("No native signal flow") +// val visibleFlow = runCatching { Reflect.getObjectField(model, "isVisible") }.getOrNull() +// ?: Reflect.getObjectField(cell, "isVisible") ?: error("No native signal visibility") +// val providerFlow = Reflect.getObjectField(model, "vmProvider") ?: error("No active mobile model flow") +// val satelliteFlow = Reflect.getObjectField(cell, "satelliteVisible") ?: error("No satellite visibility flow") +// val interactor = Reflect.getObjectField(model, "iconInteractor") ?: error("No SIM slot interactor") +// val slotFlow = Reflect.getObjectField(interactor, "phoneId") ?: error("No SIM slot flow") +// synchronized(signals) { slotIndexes.putIfAbsent(subId, flows.value(slotFlow) as? Int ?: -1) } +// val resource = flows.value(signalFlow) as? Int ?: error("Unexpected signal flow value") +// val visibility = StatusBarMembers.mobileVisibility(flows.value(visibleFlow)) ?: error("Unexpected visibility flow value") +// val initial = StatusBarPolicy.Signal(subId, resource, visibility.signal, visibility.root, +// flows.value(providerFlow) === cell, +// flows.value(satelliteFlow) as? Boolean ?: error("Unexpected satellite visibility")) +// val row = Row(root, image, subId) +// capture(row, image) +// if (!observed.containsKey(model)) { +// flows.observe(signalFlow) { value -> guard.run { +// val res = value as? Int ?: return@run +// synchronized(signals) { +// signals[subId] = (signals[subId] ?: initial).copy(resourceId = res) +// } +// schedule() +// } } +// flows.observe(visibleFlow) { value -> guard.run { +// val shown = StatusBarMembers.mobileVisibility(value) ?: return@run +// synchronized(signals) { +// signals[subId] = (signals[subId] ?: initial).copy(visible = shown.signal, rootVisible = shown.root) +// } +// schedule() +// } } +// flows.observe(providerFlow) { value -> guard.run { +// synchronized(signals) { signals[subId] = (signals[subId] ?: initial).copy(cellularMode = value === cell) } +// schedule() +// } } +// flows.observe(satelliteFlow) { value -> guard.run { +// val satellite = value as? Boolean ?: return@run +// synchronized(signals) { signals[subId] = (signals[subId] ?: initial).copy(satellite = satellite) } +// schedule() +// } } +// // A subscription ID is not a SIM slot; observe the host's actual slot mapping once per model. +// flows.observe(slotFlow) { value -> +// synchronized(signals) { slotIndexes[subId] = value as? Int ?: -1 } +// schedule() +// } +// observed[model] = true +// } +// synchronized(signals) { signals.putIfAbsent(subId, initial) } +// rows[root] = row +// val listener = android.view.ViewTreeObserver.OnGlobalLayoutListener { schedule() } +// var listening = false +// fun attachListener() { +// if (!listening && root.viewTreeObserver.isAlive) { +// root.viewTreeObserver.addOnGlobalLayoutListener(listener); listening = true +// } +// } +// root.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { +// override fun onViewAttachedToWindow(v: View) { attachListener(); schedule() } +// override fun onViewDetachedFromWindow(v: View) { +// if (v.viewTreeObserver.isAlive) v.viewTreeObserver.removeOnGlobalLayoutListener(listener) +// listening = false +// schedule() +// } +// }) +// if (root.isAttachedToWindow) attachListener() +// schedule() +// } +// +// fun visibleState(root: ViewGroup, state: Int) { +// rows[root]?.let { it.iconState = state; schedule() } +// } +// +// fun emitted(image: ImageView) { +// val affected = rows.values.filter { it.image.get() === image } +// if (affected.isEmpty()) return +// affected.forEach { row -> capture(row, image) } +// // A native emission may update the secondary theme drawable without changing its resource id. +// rows.values.forEach { it.renderKey = null } +// schedule() +// } +// +// private fun capture(row: Row, image: ImageView) { +// val native = image.drawable ?: return +// if (native is StackedSignalDrawable) return +// row.native = native.constantState?.newDrawable(image.resources)?.mutate()?.also { +// it.colorFilter = native.colorFilter; it.state = native.state +// } +// row.native?.let { drawable -> row.nativeColor = sampleColor(drawable) } +// } +// +// /** Read a tiny in-memory rendering, including the native tint, when a signal emission changes. */ +// private fun sampleColor(drawable: Drawable): Int { +// val bitmap = Bitmap.createBitmap(24, 24, Bitmap.Config.ARGB_8888) +// val original = Rect(drawable.bounds) +// try { +// drawable.setBounds(0, 0, 24, 24) +// drawable.draw(Canvas(bitmap)) +// var best = -1f +// var color = Color.WHITE +// for (y in 0..23) for (x in 0..23) { +// val pixel = bitmap.getPixel(x, y) +// val brightness = (Color.red(pixel) + Color.green(pixel) + Color.blue(pixel)) / 3f +// val score = Color.alpha(pixel) * kotlin.math.abs(brightness - 127.5f) +// if (Color.alpha(pixel) > 0 && score > best) { best = score; color = pixel or (0xff shl 24) } +// } +// return color +// } finally { drawable.bounds = original; bitmap.recycle() } +// } +// +// private fun schedule() { +// if (pending.compareAndSet(false, true)) handler.post { +// pending.set(false) +// guard.run { refresh() } +// } +// } +// +// private fun refresh() { +// val snapshot = synchronized(signals) { Triple(signals.values.toList(), dataSubId, slotIndexes.toMap()) } +// // Separate hosts (home / lock screen / another display) never share a composed view. +// rows.values.toList().filter { it.root.get()?.isAttachedToWindow == true } +// .groupBy { it.root.get()?.parent }.values.forEach { group -> +// val eligible = group.filter { +// it.iconState == 0 && it.image.get()?.visibility == View.VISIBLE && +// it.parent.get()?.visibility == View.VISIBLE +// }.map { it.subId }.toSet() +// val pairing = StatusBarPolicy.pair(snapshot.first.filter { it.subId in eligible }, snapshot.second) +// val first = pairing?.let { pair -> group.firstOrNull { it.subId == pair.primary.subId } } +// val second = pairing?.let { pair -> group.firstOrNull { it.subId == pair.secondary.subId } } +// val levels = if (pairing != null && first != null && second != null) { +// fun nativeLevel(row: Row, resource: Int): Int? { +// if (!signalLevels.containsKey(resource)) signalLevels[resource] = runCatching { +// StatusBarPolicy.signalLevel(row.image.get()!!.resources.getResourceEntryName(resource)) +// }.getOrNull() +// return signalLevels[resource] +// } +// DualSimArtworkPolicy.levels(snapshot.third[first.subId] ?: -1, snapshot.third[second.subId] ?: -1, +// nativeLevel(first, pairing.primary.resourceId), nativeLevel(second, pairing.secondary.resourceId)) +// } else null +// if (pairing == null || first?.native == null || second?.native == null || levels == null) { +// group.forEach { restore(it, snapshot.first.firstOrNull { state -> state.subId == it.subId }?.rootVisible) } +// return@forEach +// } +// group.forEach { row -> +// val root = row.root.get() ?: return@forEach +// if (row === second) { +// if (!row.hiddenByModule) { row.nativeVisibility = root.visibility; row.hiddenByModule = true } +// if (root.visibility != View.GONE) root.visibility = View.GONE +// } else if (row === first) { +// restoreVisibility(row, true) +// render(row, pairing, levels) +// } else restore(row, snapshot.first.firstOrNull { it.subId == row.subId }?.rootVisible) +// } +// } +// } +// +// private fun restoreVisibility(row: Row, visible: Boolean?) { +// if (row.hiddenByModule) { +// row.root.get()?.visibility = when (visible) { +// true -> View.VISIBLE +// false -> View.GONE +// null -> row.nativeVisibility +// } +// row.hiddenByModule = false +// } +// } +// +// private fun restore(row: Row, visible: Boolean?) { +// restoreVisibility(row, visible) +// row.image.get()?.let { image -> +// if (image.drawable is StackedSignalDrawable) row.native?.let(image::setImageDrawable) +// if (row.styled) { image.scaleX = row.originalScaleX; image.scaleY = row.originalScaleY } +// } +// if (row.styled) { +// row.parent.get()?.let { parent -> +// parent.translationY = row.originalTranslation +// parent.clipChildren = row.originalClipChildren; parent.clipToPadding = row.originalClipPadding +// } +// row.group.get()?.let { it.setPadding(row.originalLeft, it.paddingTop, row.originalRight, it.paddingBottom) } +// row.styled = false +// } +// row.renderKey = null +// } +// +// private fun render(first: Row, pair: StatusBarPolicy.Pairing, levels: DualSimArtworkPolicy.Levels) { +// val image = first.image.get() ?: return +// val root = first.root.get() ?: return +// first.styled = true +// val style = HookPrefs.getString(StatusBarKeys.DUAL_STYLE, "standard").takeIf { it in listOf("standard", "thick", "theme") } ?: "standard" +// fun setting(key: String, default: Float, min: Float, max: Float) = StatusBarPolicy.bounded(HookPrefs.getFloat(key, default), default, min, max) +// val scale = setting(StatusBarKeys.DUAL_SCALE, 1f, 0.6f, 1.6f) +// val color = image.imageTintList?.getColorForState(image.drawableState, first.nativeColor) ?: first.nativeColor +// val key = listOf(pair.primary.resourceId, pair.secondary.resourceId, levels.top, levels.bottom, style, color) +// if (first.renderKey != key || image.drawable !is StackedSignalDrawable) { +// image.setImageDrawable(StackedSignalDrawable(first.native!!, levels.top, levels.bottom, style, color)) +// first.renderKey = key +// } +// if (image.scaleX != scale) { image.scaleX = scale; image.scaleY = scale } +// (image.parent as? ViewGroup)?.let { parent -> +// parent.clipChildren = false; parent.clipToPadding = false +// val offset = setting(StatusBarKeys.DUAL_OFFSET, 0f, -4f, 4f) * image.resources.displayMetrics.density +// if (parent.translationY != offset) parent.translationY = offset +// } +// val group = StatusBarViews.find(root, "mobile_group") as? ViewGroup ?: return +// val density = image.resources.displayMetrics.density +// val left = (setting(StatusBarKeys.DUAL_LEFT_GAP, 0f, 0f, 8f) * density).roundToInt() +// val right = (setting(StatusBarKeys.DUAL_RIGHT_GAP, 0f, 0f, 8f) * density).roundToInt() +// if (group.paddingLeft != left || group.paddingRight != right) group.setPadding(left, group.paddingTop, right, group.paddingBottom) +// } +// } diff --git a/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/MobileTypeHooks.kt b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/MobileTypeHooks.kt new file mode 100644 index 0000000..b84eb36 --- /dev/null +++ b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/MobileTypeHooks.kt @@ -0,0 +1,28 @@ +package cn.ianzb.hyperrefine.hook.systemui.statusbar + +import cn.ianzb.hyperrefine.hook.base.BaseHook +import cn.ianzb.hyperrefine.hook.xposed.Reflect + +class MobileIndicatorVisibilityHook : BaseHook() { + override val key = StatusBarKeys.HIDE_TYPE + override fun init() { + val loader = target.classLoader + val factory = Reflect.findClass("com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MiuiMobileIconVMImpl\$\$ExternalSyntheticLambda0", loader) + val cell = Reflect.findClass("com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MiuiCellularIconVM", loader) + val create = StatusBarMembers.methods(factory, "invoke") { + it.parameterCount == 0 && it.returnType == Any::class.java + } + // Native image, separate text, and special 5G visibility have independent collectors. + val visibility = listOf("mobileTypeVisible", "mobileTypeImageVisible", "mobileTypeSingleVisible", "showSpecial5GIcon") + .map { StatusBarMembers.field(cell, it) } + require(visibility.all { it.type.name == "kotlinx.coroutines.flow.ReadonlyStateFlow" }) + val flows = StatusBarFlows(loader) + val hidden = flows.mutable(false) + val guard = StatusBarGuard(tag) + StatusBarMembers.install(create.map { method -> { StatusBarMembers.after(method) { param -> guard.run { + val model = param.result ?: return@run + require(cell.isInstance(model)) { "Unexpected cellular model" } + visibility.forEach { flows.replace(model, it, hidden) } + } } } }) + } +} diff --git a/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StackedSignalDrawable.kt b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StackedSignalDrawable.kt new file mode 100644 index 0000000..6b7f8ce --- /dev/null +++ b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StackedSignalDrawable.kt @@ -0,0 +1,65 @@ +// DORMANT: preserved from statusbar.6 (ddb0e59); not compiled or registered. +// Restore only after artwork and device regression testing. Runtime diagnostics removed. +// package cn.ianzb.hyperrefine.hook.systemui.statusbar +// +// import android.content.res.ColorStateList +// import android.graphics.Canvas +// import android.graphics.ColorFilter +// import android.graphics.Paint +// import android.graphics.PixelFormat +// import android.graphics.drawable.Drawable +// +// /** Four independently drawn cells per SIM; SystemUI supplies the current light/dark tint. */ +// internal class StackedSignalDrawable( +// private val nativeSizing: Drawable, +// private val topLevel: Int, +// private val bottomLevel: Int, +// private val style: String, +// private val baseColor: Int, +// ) : Drawable() { +// private val paint = Paint(Paint.ANTI_ALIAS_FLAG) +// private var tint: ColorStateList? = null +// private var foreground = baseColor +// private var opacity = 255 +// private var geometry: List = emptyList() +// private var geometryWidth = -1 +// private var geometryHeight = -1 +// +// override fun draw(canvas: Canvas) { +// if (bounds.width() != geometryWidth || bounds.height() != geometryHeight) { +// geometryWidth = bounds.width(); geometryHeight = bounds.height() +// geometry = DualSimArtworkPolicy.cells(geometryWidth.toFloat(), geometryHeight.toFloat(), style == "thick") +// } +// geometry.forEach { cell -> +// val level = if (cell.row == 0) topLevel else bottomLevel +// paint.color = DualSimArtworkPolicy.cellColor(foreground, opacity, cell.column < level) +// canvas.drawRoundRect(bounds.left + cell.left, bounds.top + cell.top, +// bounds.left + cell.right, bounds.top + cell.bottom, cell.radius, cell.radius, paint) +// } +// } +// +// override fun setAlpha(alpha: Int) { +// opacity = alpha.coerceIn(0, 255); nativeSizing.alpha = opacity; invalidateSelf() +// } +// override fun getAlpha() = opacity +// override fun setColorFilter(filter: ColorFilter?) { +// paint.colorFilter = filter; nativeSizing.colorFilter = filter; invalidateSelf() +// } +// override fun setTintList(tint: ColorStateList?) { +// this.tint = tint +// nativeSizing.setTintList(tint) // Keep the private native clone ready for single-SIM restoration. +// foreground = tint?.getColorForState(state, tint.defaultColor) ?: baseColor +// invalidateSelf() +// } +// override fun isStateful() = tint?.isStateful == true || nativeSizing.isStateful +// override fun onStateChange(state: IntArray): Boolean { +// val next = tint?.let { it.getColorForState(state, it.defaultColor) } ?: baseColor +// val nativeChanged = nativeSizing.setState(state) +// if (foreground == next) return nativeChanged +// foreground = next; invalidateSelf(); return true +// } +// @Deprecated("Drawable opacity is retained for Android compatibility") +// override fun getOpacity() = PixelFormat.TRANSLUCENT +// override fun getIntrinsicWidth() = nativeSizing.intrinsicWidth +// override fun getIntrinsicHeight() = nativeSizing.intrinsicHeight +// } diff --git a/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarKeys.kt b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarKeys.kt new file mode 100644 index 0000000..7ece9bd --- /dev/null +++ b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarKeys.kt @@ -0,0 +1,16 @@ +package cn.ianzb.hyperrefine.hook.systemui.statusbar + +object StatusBarKeys { + const val TARGET = "com.android.systemui" + const val WIFI_ACTIVITY = "statusbar_wifi_hide_activity" + const val HIDE_TYPE = "statusbar_mobile_hide_type" + val ALL = listOf(WIFI_ACTIVITY, HIDE_TYPE) + + // DORMANT: restore these only with the commented dual-SIM implementation and its UI. + // const val DUAL_ROWS = "statusbar_mobile_dual_rows" + // const val DUAL_STYLE = "statusbar_mobile_dual_style" + // const val DUAL_SCALE = "statusbar_mobile_dual_scale" + // const val DUAL_LEFT_GAP = "statusbar_mobile_dual_left_gap" + // const val DUAL_RIGHT_GAP = "statusbar_mobile_dual_right_gap" + // const val DUAL_OFFSET = "statusbar_mobile_dual_offset" +} diff --git a/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarPolicy.kt b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarPolicy.kt new file mode 100644 index 0000000..94af558 --- /dev/null +++ b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarPolicy.kt @@ -0,0 +1,29 @@ +// DORMANT: preserved from statusbar.6 (ddb0e59); not compiled or registered. +// Restore only after artwork and device regression testing. Runtime diagnostics removed. +// package cn.ianzb.hyperrefine.hook.systemui.statusbar +// +// internal object StatusBarPolicy { +// +// data class Visibility(val root: Boolean, val signal: Boolean) +// data class Signal( +// val subId: Int, val resourceId: Int, val visible: Boolean, val rootVisible: Boolean = visible, +// val cellularMode: Boolean = true, val satellite: Boolean = false, +// ) +// data class Pairing(val primary: Signal, val secondary: Signal) +// +// /** Only combine two real visible subscriptions. Unknown / single-SIM state keeps native layout. */ +// fun pair(signals: List, dataSubId: Int): Pairing? { +// val visible = signals.filter { it.visible && it.cellularMode && !it.satellite && it.subId >= 0 && it.resourceId > 0 } +// .distinctBy { it.subId }.sortedBy { it.subId } +// if (visible.size != 2) return null +// val primary = visible.firstOrNull { it.subId == dataSubId } ?: visible.first() +// return Pairing(primary, visible.first { it.subId != primary.subId }) +// } +// +// private val levelName = Regex("^stat_sys_signal_([0-5])(?:_.*)?$") +// fun signalLevel(resourceName: String): Int? = +// if (resourceName == "stat_sys_signal_null") 0 else levelName.matchEntire(resourceName)?.groupValues?.get(1)?.toInt() +// +// fun bounded(value: Float, default: Float, min: Float, max: Float): Float = +// if (value.isFinite()) value.coerceIn(min, max) else default +// } diff --git a/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarSupport.kt b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarSupport.kt new file mode 100644 index 0000000..d41b2a5 --- /dev/null +++ b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarSupport.kt @@ -0,0 +1,99 @@ +package cn.ianzb.hyperrefine.hook.systemui.statusbar + +import cn.ianzb.hyperrefine.hook.xposed.HookHelper +import cn.ianzb.hyperrefine.hook.xposed.Reflect +import cn.ianzb.hyperrefine.hook.xposed.HookParam +import io.github.libxposed.api.XposedInterface.HookHandle +import java.lang.reflect.Field +import java.lang.reflect.Executable +import java.lang.reflect.Method +import java.util.concurrent.atomic.AtomicBoolean + +internal object StatusBarMembers { + fun after(executable: Executable, callback: (HookParam) -> Unit): HookHandle { + HookHelper.deoptimize(executable) + return HookHelper.hookAfter(executable, callback = callback) + } + fun field(type: Class<*>, name: String): Field { + var current: Class<*>? = type + while (current != null) { + current.declaredFields.firstOrNull { it.name == name }?.let { + return it.apply { isAccessible = true } + } + current = current.superclass + } + throw NoSuchFieldException(type.name + "#" + name) + } + + fun methods(type: Class<*>, name: String, predicate: (Method) -> Boolean): List { + val result = linkedMapOf>, Method>() + var current: Class<*>? = type + while (current != null) { + current.declaredMethods.filter { it.name == name && predicate(it) } + .forEach { result.putIfAbsent(it.parameterTypes.toList(), it.apply { isAccessible = true }) } + current = current.superclass + } + check(result.isNotEmpty()) { type.name + "#" + name + ": no compatible method" } + return result.values.toList() + } + + fun install(installers: List<() -> HookHandle>) { + val handles = mutableListOf() + try { installers.forEach { handles += it() } } + catch (error: Throwable) { + handles.asReversed().forEach { runCatching { it.unhook() } } + throw error + } + } + + /** R8 can remove ReadonlyStateFlow's unused Job argument in the host APK. */ + fun readonlyFlow(wrapper: Class<*>, flow: Any): Any { + val constructors = wrapper.declaredConstructors.filter { + it.parameterCount in 1..2 && it.parameterTypes[0].isInstance(flow) && + (it.parameterCount == 1 || !it.parameterTypes[1].isPrimitive) + } + val constructor = constructors.singleOrNull { it.parameterCount == 1 } + ?: constructors.singleOrNull { it.parameterCount == 2 } + ?: throw NoSuchMethodException(wrapper.name + ": no compatible read-only flow constructor") + constructor.isAccessible = true + return if (constructor.parameterCount == 1) constructor.newInstance(flow) + else constructor.newInstance(flow, null) + } + + // DORMANT: helper required when restoring DualSimSignalHook and StatusBarPolicy. + // The host Pair comes from its own Kotlin class loader; do not cast to the module's Pair. + // fun mobileVisibility(value: Any?): StatusBarPolicy.Visibility? { + // if (value is Boolean) return StatusBarPolicy.Visibility(value, value) + // if (value?.javaClass?.name != "kotlin.Pair") return null + // val root = Reflect.callMethod(value, "getFirst") as? Boolean ?: return null + // val signal = Reflect.callMethod(value, "getSecond") as? Boolean ?: return null + // return StatusBarPolicy.Visibility(root, root && signal) + // } +} + +internal class StatusBarGuard(private val label: String) { + private val warned = AtomicBoolean() + fun run(block: () -> Unit) { + try { block() } catch (error: Throwable) { + if (warned.compareAndSet(false, true)) { + HookHelper.log(label + ": unsupported ROM member", error) + } + } + } +} + +/** Use the host's Kotlin flow classes; no bundled coroutine dependency or polling. */ +internal class StatusBarFlows(private val loader: ClassLoader?) { + private val factory = Reflect.findClass("kotlinx.coroutines.flow.StateFlowKt", loader) + .getDeclaredMethod("MutableStateFlow", Any::class.java).apply { isAccessible = true } + + fun mutable(value: Any): Any = checkNotNull(factory.invoke(null, value)) + fun replace(owner: Any, field: Field, flow: Any) { + val assigned = if (field.type.isInstance(flow)) flow else { + val wrapper = Reflect.findClass("kotlinx.coroutines.flow.ReadonlyStateFlow", loader) + StatusBarMembers.readonlyFlow(wrapper, flow) + } + require(field.type.isInstance(assigned)) { field.name + ": unexpected flow type" } + field.set(owner, assigned) + } +} diff --git a/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarViews.kt b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarViews.kt new file mode 100644 index 0000000..ffdb6b8 --- /dev/null +++ b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarViews.kt @@ -0,0 +1,21 @@ +// DORMANT: preserved from statusbar.6 (ddb0e59); not compiled or registered. +// Restore only after artwork and device regression testing. Runtime diagnostics removed. +// package cn.ianzb.hyperrefine.hook.systemui.statusbar +// +// import android.view.View +// import android.view.ViewGroup +// import cn.ianzb.hyperrefine.hook.xposed.Reflect +// +// /** Native status-bar lookup only; no network-type styling or view reparenting. */ +// internal object StatusBarViews { +// const val MOBILE_BINDER = "com.android.systemui.statusbar.pipeline.mobile.ui.binder.MiuiMobileIconBinder" +// +// fun find(root: View, name: String): View? { +// val id = root.resources.getIdentifier(name, "id", StatusBarKeys.TARGET) +// return if (id == 0) null else root.findViewById(id) +// } +// +// fun isStatusBarMobile(root: ViewGroup, model: Any?): Boolean = +// model?.javaClass?.name?.contains("ShadeCarrierGroup") != true && +// (runCatching { Reflect.getObjectField(root, "slot") }.getOrNull() in listOf("mobile", "demo_mobile")) +// } diff --git a/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/WifiIndicatorHooks.kt b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/WifiIndicatorHooks.kt new file mode 100644 index 0000000..cba6a4a --- /dev/null +++ b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/WifiIndicatorHooks.kt @@ -0,0 +1,39 @@ +package cn.ianzb.hyperrefine.hook.systemui.statusbar + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import cn.ianzb.hyperrefine.hook.base.BaseHook +import cn.ianzb.hyperrefine.hook.xposed.Reflect + +// Presentation-only override verified against HyperOS 4.0.22.0's native Wi-Fi binder. +class HideWifiActivityHook : BaseHook() { + override val key = StatusBarKeys.WIFI_ACTIVITY + override fun init() { + val loader = target.classLoader + val name = "com.android.systemui.statusbar.pipeline.wifi.ui.binder.MiuiWifiViewBinder" + val binder = Reflect.findClass(name, loader) + val binding = Reflect.findClass(name + "\$bind\$2", loader) + val bindingActivity = StatusBarMembers.field(binding, "\$wifiActivity") + require(ImageView::class.java.isAssignableFrom(bindingActivity.type)) + val bind = StatusBarMembers.methods(binder, "bind") { + it.parameterCount == 2 && it.parameterTypes[0] == ViewGroup::class.java && + it.parameterTypes[1].name == "com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel" && + binding.isAssignableFrom(it.returnType) + } + val collector = Reflect.findClass(name + "\$bind\$1\$1\$4\$1", loader) + val collectorActivity = StatusBarMembers.field(collector, "\$wifiActivity") + require(ImageView::class.java.isAssignableFrom(collectorActivity.type)) + val update = StatusBarMembers.methods(collector, "invokeSuspend") { + it.parameterTypes.contentEquals(arrayOf(Any::class.java)) && it.returnType == Any::class.java + } + val guard = StatusBarGuard(tag) + val installs = bind.map { method -> { StatusBarMembers.after(method) { param -> guard.run { + param.result?.let { (bindingActivity.get(it) as ImageView).visibility = View.GONE } + } } } } + update.map { method -> { StatusBarMembers.after(method) { param -> guard.run { + // Native activity updates force this view visible even for the no-traffic drawable. + param.thisObject?.let { (collectorActivity.get(it) as ImageView).visibility = View.GONE } + } } } } + StatusBarMembers.install(installs) + } +} diff --git a/hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimArtworkPolicyTest.kt b/hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimArtworkPolicyTest.kt new file mode 100644 index 0000000..c623a62 --- /dev/null +++ b/hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimArtworkPolicyTest.kt @@ -0,0 +1,84 @@ +// DORMANT: preserved from statusbar.6 (ddb0e59); not compiled or registered. +// Restore only after artwork and device regression testing. Runtime diagnostics removed. +// package cn.ianzb.hyperrefine.hook.systemui.statusbar +// +// import org.junit.Assert.* +// import org.junit.Test +// +// class DualSimArtworkPolicyTest { +// @Test fun sixNativeStrengthLevelsRemainMonotonicWithinFourCells() { +// assertEquals(listOf(0, 1, 2, 2, 3, 4), (0..5).map(DualSimArtworkPolicy::fourCellLevel)) +// assertNull(DualSimArtworkPolicy.fourCellLevel(null)) +// assertNull(DualSimArtworkPolicy.fourCellLevel(-1)) +// assertNull(DualSimArtworkPolicy.fourCellLevel(6)) +// } +// +// @Test fun dataSimSwitchDoesNotSwapTheDisplayedPhysicalSimRows() { +// val fromSim1 = DualSimArtworkPolicy.levels(0, 1, 5, 2) +// val fromSim2 = DualSimArtworkPolicy.levels(1, 0, 2, 5) +// assertEquals(DualSimArtworkPolicy.Levels(4, 2), fromSim1) +// assertEquals(fromSim1, fromSim2) +// } +// +// @Test fun duplicateOrUnknownSlotsAndUnknownStrengthKeepNativeLayout() { +// assertNull(DualSimArtworkPolicy.levels(0, 0, 3, 4)) +// assertNull(DualSimArtworkPolicy.levels(-1, 1, 3, 4)) +// assertNull(DualSimArtworkPolicy.levels(0, 2, 3, 4)) +// assertNull(DualSimArtworkPolicy.levels(0, 1, null, 4)) +// } +// +// @Test fun bothWidthsHaveFourAlignedCellsPerRowInsideTheDrawable() { +// for (thick in listOf(false, true)) for ((width, height) in listOf(16f to 20f, 32f to 28f, 8f to 8f)) { +// val cells = DualSimArtworkPolicy.cells(width, height, thick) +// assertEquals(8, cells.size) +// assertEquals(listOf(0, 1, 2, 3), cells.filter { it.row == 0 }.map { it.column }) +// cells.forEach { assertTrue(it.left >= 0 && it.top >= -0.0001f && it.right <= width && it.bottom <= height) } +// cells.take(4).zip(cells.drop(4)).forEach { (upper, lower) -> +// assertEquals(upper.left, lower.left, 0f) +// assertEquals(upper.right, lower.right, 0f) +// assertTrue(upper.bottom < lower.top) +// } +// } +// } +// +// @Test fun upperRowRisesAndLowerRowHasEqualShortCells() { +// val cells = DualSimArtworkPolicy.cells(16f, 20f, false) +// val upper = cells.take(4) +// val lower = cells.drop(4) +// assertTrue(upper.zipWithNext().all { (a, b) -> a.top > b.top }) +// assertEquals(1, upper.map { it.bottom }.distinct().size) +// assertEquals(1, lower.map { it.top }.distinct().size) +// assertEquals(1, lower.map { it.bottom }.distinct().size) +// assertTrue(lower.first().bottom - lower.first().top < upper.first().bottom - upper.first().top) +// // Both rows fit a compact silhouette rather than filling the host's tall drawable bounds. +// assertTrue(lower.last().bottom - upper.last().top < 16f) +// cells.forEach { +// val shortSide = minOf(it.right - it.left, it.bottom - it.top) +// assertTrue(it.radius > shortSide * 0.4f) +// assertTrue(it.radius <= shortSide / 2f) +// } +// } +// +// @Test fun invalidBoundsDoNotProduceInvalidCoordinates() { +// assertTrue(DualSimArtworkPolicy.cells(0f, 20f, false).isEmpty()) +// assertTrue(DualSimArtworkPolicy.cells(16f, -1f, false).isEmpty()) +// assertTrue(DualSimArtworkPolicy.cells(Float.NaN, 20f, false).isEmpty()) +// } +// +// @Test fun whiteAndBlackHostPalettesRetainRgbAndDimOnlyInactiveCells() { +// for (color in listOf(0xffffffff.toInt(), 0xff000000.toInt(), 0xff75aaff.toInt())) { +// assertEquals(color, DualSimArtworkPolicy.cellColor(color, 255, true)) +// val inactive = DualSimArtworkPolicy.cellColor(color, 255, false) +// assertEquals(color and 0x00ffffff, inactive and 0x00ffffff) +// assertEquals(71, inactive ushr 24) +// } +// } +// +// @Test fun hostTransparencyAndDrawableFadeMultiplyForBothSignalStates() { +// val color = 0x80ffffff.toInt() +// assertEquals(64, DualSimArtworkPolicy.cellColor(color, 128, true) ushr 24) +// assertEquals(18, DualSimArtworkPolicy.cellColor(color, 128, false) ushr 24) +// assertEquals(0, DualSimArtworkPolicy.cellColor(color, 0, true) ushr 24) +// assertEquals(128, DualSimArtworkPolicy.cellColor(color, 999, true) ushr 24) +// } +// } diff --git a/hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimRuntimeSupportTest.kt b/hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimRuntimeSupportTest.kt new file mode 100644 index 0000000..966f020 --- /dev/null +++ b/hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimRuntimeSupportTest.kt @@ -0,0 +1,96 @@ +// DORMANT: preserved from statusbar.6 (ddb0e59); not compiled or registered. +// Restore only after artwork and device regression testing. Runtime diagnostics removed. +// package cn.ianzb.hyperrefine.hook.systemui.statusbar +// +// import org.junit.Assert.* +// import org.junit.Test +// import java.lang.reflect.InvocationTargetException +// import java.util.function.Consumer +// +// class DualSimRuntimeSupportTest { +// class Flow(var current: Any?) { +// val listeners = mutableListOf>() +// fun getValue(): Any? = current +// fun emit(value: Any?) { current = value; listeners.forEach { it.accept(value) } } +// } +// +// class Adapter { +// var failNext = false +// fun alwaysCollectFlow(flow: Flow, consumer: Consumer) { +// if (failNext) { failNext = false; error("collector not ready") } +// flow.listeners += consumer +// consumer.accept(flow.current) +// } +// } +// +// @Test fun bindBeforeAdapterPreservesInitialAndLaterSignalEvents() { +// val observer = DualSimFlowObserver() +// val signal = Flow(2) +// val seen = mutableListOf() +// observer.observe(signal) { seen += it } +// signal.emit(3) +// assertTrue(seen.isEmpty()) +// assertTrue(observer.attach(Adapter())) +// signal.emit(4) +// assertEquals(listOf(3, 4), seen) +// assertEquals(4, observer.value(signal)) +// } +// +// @Test fun adapterBeforeBindCollectsIndependentFlowsAndDoesNotSubscribeTwice() { +// val observer = DualSimFlowObserver() +// val adapter = Adapter() +// assertTrue(observer.attach(adapter)) +// val first = Flow(1) +// val second = Flow(2) +// val firstSeen = mutableListOf() +// val secondSeen = mutableListOf() +// observer.observe(first) { firstSeen += it } +// observer.observe(second) { secondSeen += it } +// assertFalse(observer.attach(adapter)) +// first.emit(3) +// second.emit(4) +// assertEquals(listOf(1, 3), firstSeen) +// assertEquals(listOf(2, 4), secondSeen) +// assertEquals(1, first.listeners.size) +// assertEquals(1, second.listeners.size) +// } +// +// @Test fun failedDrainRetainsPendingSubscriptionForRetry() { +// val observer = DualSimFlowObserver() +// val adapter = Adapter().apply { failNext = true } +// val signal = Flow(5) +// val seen = mutableListOf() +// observer.observe(signal) { seen += it } +// try { observer.attach(adapter); fail("Must expose collection failure") } +// catch (error: InvocationTargetException) { assertEquals("collector not ready", error.cause?.message) } +// assertTrue(observer.attach(adapter)) +// assertEquals(listOf(5), seen) +// assertEquals(1, signal.listeners.size) +// } +// +// @Test fun unrelatedAdapterCannotConsumeOrDropPendingSignals() { +// val observer = DualSimFlowObserver() +// val seen = mutableListOf() +// observer.observe(Flow(1)) { seen += it } +// try { observer.attach(Any()); fail("Must reject an unsupported adapter") } +// catch (_: IllegalStateException) { assertTrue(seen.isEmpty()) } +// observer.attach(Adapter()) +// assertEquals(listOf(1), seen) +// } +// +// @Test fun laterLoadedApkCallbacksReuseOnlyCompletedInstallation() { +// val installation = DualSimInstallation() +// var calls = 0 +// assertTrue(installation.installOnce { calls++ }) +// assertFalse(installation.installOnce { error("Should not resolve classes from another loader") }) +// assertEquals(1, calls) +// } +// +// @Test fun failedInstallationKeepsOriginalErrorAndAllowsRealRetry() { +// val installation = DualSimInstallation() +// val expected = ClassNotFoundException("binder") +// try { installation.installOnce { throw expected }; fail("Must not record failed installation") } +// catch (actual: ClassNotFoundException) { assertSame(expected, actual) } +// assertTrue(installation.installOnce {}) +// } +// } diff --git a/hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarPolicyTest.kt b/hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarPolicyTest.kt new file mode 100644 index 0000000..1b28cbb --- /dev/null +++ b/hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StatusBarPolicyTest.kt @@ -0,0 +1,53 @@ +package cn.ianzb.hyperrefine.hook.systemui.statusbar + +import org.junit.Assert.* +import org.junit.Test + +class StatusBarPolicyTest { + @Test fun retainedOptionsHaveUniqueKeysAndOnlySystemUiIsTargeted() { + assertEquals(listOf(StatusBarKeys.WIFI_ACTIVITY, StatusBarKeys.HIDE_TYPE), StatusBarKeys.ALL) + assertEquals(2, StatusBarKeys.ALL.distinct().size) + assertEquals("com.android.systemui", StatusBarKeys.TARGET) + } + + open class Parent { private var field = 1; open fun build(value: String) {}; fun build(value: Int) {} } + class Child : Parent() { override fun build(value: String) {} } + @Test fun memberResolutionValidatesSignaturesAndSupportsInheritedPrivateFields() { + val child = Child() + assertEquals(1, StatusBarMembers.field(Child::class.java, "field").getInt(child)) + val methods = StatusBarMembers.methods(Child::class.java, "build") { + it.parameterTypes.contentEquals(arrayOf(String::class.java)) && it.returnType == Void.TYPE + } + assertEquals(1, methods.size) + assertEquals(Child::class.java, methods.single().declaringClass) + } + + @Test(expected = IllegalStateException::class) + fun missingCompatibleHookMethodIsRejected() { + StatusBarMembers.methods(Child::class.java, "build") { it.parameterTypes.contentEquals(arrayOf(Boolean::class.javaPrimitiveType)) } + } + + class MutableFlow(val value: Boolean) + class OptimizedReadonly private constructor(val flow: MutableFlow) + class UnoptimizedReadonly(val flow: MutableFlow, val job: Any?) + class InvalidReadonly(val value: Int) + + @Test fun romOptimizedReadonlyFlowConstructorRetainsTheReplacementFlow() { + val source = MutableFlow(false) + val wrapped = StatusBarMembers.readonlyFlow(OptimizedReadonly::class.java, source) as OptimizedReadonly + assertSame(source, wrapped.flow) + assertFalse(wrapped.flow.value) + } + + @Test fun unoptimizedReadonlyFlowConstructorRemainsSupported() { + val source = MutableFlow(false) + val wrapped = StatusBarMembers.readonlyFlow(UnoptimizedReadonly::class.java, source) as UnoptimizedReadonly + assertSame(source, wrapped.flow) + assertNull(wrapped.job) + } + + @Test(expected = NoSuchMethodException::class) + fun unknownReadonlyFlowSignatureIsRejectedInsteadOfConstructingInvalidState() { + StatusBarMembers.readonlyFlow(InvalidReadonly::class.java, MutableFlow(false)) + } +}