From dcca40e6b94a7dddea2836d2b6461e45b274b8db Mon Sep 17 00:00:00 2001
From: Ashurinnn <77261372+Ashurinnn@users.noreply.github.com>
Date: Fri, 16 Oct 2026 01:59:05 +0100
Subject: [PATCH] feat(systemui): enable stacked dual-SIM signals with stable
transitions
Restore the dormant dual-SIM hooks with fixed normal-width artwork,
uniform geometry and frame-safe refreshes. Preserve host animation
properties, keep existing visibility hooks frozen, and document the
confirmed device behavior and remaining coverage limits.
---
README.md | 2 +-
.../screen/features/NetworkIconsActivity.kt | 3 +-
.../ui/screen/features/StatusBarOptions.kt | 13 +
app/src/main/res/values-en/strings.xml | 9 +
app/src/main/res/values/strings.xml | 9 +
changelog.md | 2 +-
docs/CUSTOMIZE.md | 2 +-
docs/STATUSBAR_PORT.md | 19 +-
.../hyperrefine/hook/systemui/SystemUiLoad.kt | 2 +
.../statusbar/DualSimArtworkPolicy.kt | 127 ++--
.../statusbar/DualSimRuntimeSupport.kt | 129 ++--
.../systemui/statusbar/DualSimSignalHook.kt | 713 +++++++++---------
.../statusbar/StackedSignalDrawable.kt | 142 ++--
.../hook/systemui/statusbar/StatusBarKeys.kt | 17 +-
.../systemui/statusbar/StatusBarPolicy.kt | 56 +-
.../systemui/statusbar/StatusBarSupport.kt | 17 +-
.../hook/systemui/statusbar/StatusBarViews.kt | 40 +-
.../statusbar/DualSimArtworkPolicyTest.kt | 190 ++---
.../statusbar/DualSimRuntimeSupportTest.kt | 231 +++---
.../systemui/statusbar/StatusBarPolicyTest.kt | 35 +-
20 files changed, 950 insertions(+), 808 deletions(-)
diff --git a/README.md b/README.md
index d128b82..0ca5cf3 100644
--- a/README.md
+++ b/README.md
@@ -170,7 +170,7 @@ HyperRefine 基于个人 Android 模块开发脚手架 MiuixGuiTemplate 创建
通知功能独立实现,未引入参考模块的代码、资源或原生库;参考项目的来源与许可证已登记到应用内参考项目分组。
-状态栏的 Wi-Fi 活动指示器隐藏与移动网络类型隐藏同样独立实现,参考来源统一登记在应用内致谢页;功能范围、验证和暂未启用的双卡双行源码见 [状态栏功能说明](docs/STATUSBAR_PORT.md)。
+状态栏的 Wi-Fi 活动指示器隐藏、移动网络类型隐藏与双排堆叠信号同样独立实现,参考来源统一登记在应用内致谢页;功能范围与验证见 [状态栏功能说明](docs/STATUSBAR_PORT.md)。
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
index b12749a..bd4b0b2 100644
--- 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
@@ -20,7 +20,8 @@ class NetworkIconsActivity : BaseSubPageActivity() {
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)),
+ HookSection(R.string.sb_mobile_type_section, specs = specs.slice(1..1)),
+ HookSection(R.string.sb_mobile_dual_section, specs = specs.drop(2)),
), contentPadding)
}
}
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
index 6cca6fb..1835330 100644
--- 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
@@ -12,10 +12,23 @@ internal fun statusBarFeatureSpecs(): List {
key = key, type = OptionType.SWITCH, titleRes = title,
defaultBoolean = false, targetPackages = listOf(Keys.TARGET), showStatus = true,
)
+ fun slider(key: String, title: Int, value: Float, min: Float, max: Float, step: Float = 0.5f) = OptionSpec(
+ key = key, type = OptionType.SLIDER, titleRes = title, defaultFloat = value,
+ sliderMin = min, sliderMax = max, sliderStep = step, sliderDecimals = 1,
+ sliderUnitRes = if (key == Keys.DUAL_SCALE) R.string.sb_unit_scale else R.string.sb_unit_dp,
+ targetPackages = listOf(Keys.TARGET), dependsOn = Keys.DUAL_ROWS,
+ )
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),
+ OptionSpec(key = Keys.DUAL_ROWS, type = OptionType.SWITCH, titleRes = R.string.sb_dual_rows,
+ summaryRes = R.string.sb_dual_rows_summary, defaultBoolean = false,
+ targetPackages = listOf(Keys.TARGET), showStatus = true),
+ slider(Keys.DUAL_SCALE, R.string.sb_icon_scale, 1f, 0.6f, 1.6f, 0.1f),
+ slider(Keys.DUAL_LEFT_GAP, R.string.sb_left_gap, 0f, 0f, 8f),
+ slider(Keys.DUAL_RIGHT_GAP, R.string.sb_right_gap, 0f, 0f, 8f),
+ slider(Keys.DUAL_OFFSET, R.string.sb_vertical_offset, 0f, -4f, 4f),
)
}
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
index b08f9d2..76302d4 100644
--- a/app/src/main/res/values-en/strings.xml
+++ b/app/src/main/res/values-en/strings.xml
@@ -499,4 +499,13 @@
Hide Wi-Fi activity indicators
Mobile network type
Hide network type completely
+ Stacked signals
+ Show two rows of stacked signals
+ When enabled, mobile signal indicators use a stacked layout. Adjust the scale and position offsets as needed. Restart SystemUI after making changes.
+ Icon scale
+ Left spacing
+ Right spacing
+ Vertical offset
+ dp
+ ×
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fe47590..a6cc9c8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -499,4 +499,13 @@
隐藏 Wi-Fi 网络活动指示器
移动网络类型
完全隐藏网络类型
+ 堆叠信号
+ 显示双排堆叠信号
+ 开启后移动信号将更改为堆叠显示,请自行调整缩放和位置偏移量;修改完成后需要重启系统界面。
+ 图标缩放
+ 左侧间距
+ 右侧间距
+ 垂直偏移
+ dp
+ ×
diff --git a/changelog.md b/changelog.md
index 8cb4793..0b69b4d 100644
--- a/changelog.md
+++ b/changelog.md
@@ -6,7 +6,7 @@
### 新增
-- **状态栏 · 网络图标**(目标 `com.android.systemui`):新增「系统界面 → 状态栏」页,提供「隐藏 Wi-Fi 活动指示器」「完全隐藏网络类型」两项开关,默认关闭;双卡双行实现暂以注释源码保留,等待后续修复。
+- **状态栏 · 网络图标**(目标 `com.android.systemui`):新增「系统界面 → 状态栏」页,提供「隐藏 Wi-Fi 活动指示器」「完全隐藏网络类型」与「显示双排堆叠信号」开关,默认关闭。双卡固定为上排 SIM 1、下排 SIM 2,使用普通宽度四格图形;移除加粗样式,统一图形比例并在绘制前完成更新,避免过渡中切回原生形状。下拉面板和返回桌面时的双排过渡已通过真机验证。
- **负一屏 · 过渡动画优化**(目标 `com.miui.personalassistant` / system_server):新增「负一屏」功能页。**过渡闪黑修复**消除打开 / 返回快速切换时的纯色闪黑;**上滑合并强制反向**修正返回动画被系统合并时先反向播一半的问题;**放行远程动画权限**(系统服务侧,需重启设备)让负一屏的远程过渡动画完整生效。思路参考 HyperAssistant(非开源,独立实现)。
- **负一屏 · 打开应用动画与过渡遮罩重绘**(目标 `com.miui.personalassistant`):**应用打开动画**让负一屏点开的应用从图标位置缩放展开(options 逐级剥离重试,失败回退框架动画);**过渡遮罩重绘**修复过渡中遮罩显示异常内容。
- **负一屏 · 返回缩回动画**(目标 `com.miui.personalassistant`):关闭应用回到负一屏时,部件快照从全屏缩回原位,接近末尾渐隐;需先开启「应用打开动画」。
diff --git a/docs/CUSTOMIZE.md b/docs/CUSTOMIZE.md
index 6c63986..77c2f08 100644
--- a/docs/CUSTOMIZE.md
+++ b/docs/CUSTOMIZE.md
@@ -413,7 +413,7 @@ HookOptionsPage(
通知功能的参考来源已登记到 `LicensePage.kt` 的参考项目分组,注明 GPL-3.0、仅思路参考、未复用代码或资源。新增七项配置和进程范围见 [通知功能说明](NOTIFICATION_PORT.md)。
-状态栏功能的参考来源也在该分组登记;仅借鉴思路,未引入参考模块或小米 SystemUI 的代码、图片及原生库。两项开关的配置、目标进程和注释中的双卡双行实现见 [状态栏功能说明](STATUSBAR_PORT.md)。
+状态栏功能的参考来源也在该分组登记;仅借鉴思路,未引入参考模块或小米 SystemUI 的代码、图片及原生库。各项开关的配置、目标进程和双排堆叠信号实现见 [状态栏功能说明](STATUSBAR_PORT.md)。
本模板的定位是**个人 Android 模块项目的脚手架模板**。具体参考与致谢清单统一维护在应用内「第三方许可证与致谢」页面(`LicensePage.kt` → `licenses_section_refs` 分组),衍生项目**可增不可删**。
diff --git a/docs/STATUSBAR_PORT.md b/docs/STATUSBAR_PORT.md
index 899c3eb..a492b93 100644
--- a/docs/STATUSBAR_PORT.md
+++ b/docs/STATUSBAR_PORT.md
@@ -1,11 +1,12 @@
# 状态栏功能说明
-入口:**功能 → 系统界面 → 状态栏**。当前仅开放以下两项开关,默认关闭,目标均为 `com.android.systemui`;修改后重新加载系统界面进程生效。
+入口:**功能 → 系统界面 → 状态栏**。以下开关默认关闭,目标均为 `com.android.systemui`;修改后重新加载系统界面进程生效。
| 选项 | 配置键 | 效果 |
|---|---|---|
| 隐藏 Wi-Fi 网络活动指示器 | `statusbar_wifi_hide_activity` | 隐藏 Wi-Fi 的收发活动图标,保留信号与连接显示 |
| 完全隐藏网络类型 | `statusbar_mobile_hide_type` | 隐藏移动网络类型图片、独立文字和特殊 5G 标识,保留原生信号布局 |
+| 显示双排堆叠信号 | `statusbar_mobile_dual_rows` | 将两个蜂窝信号合并为上下各四格,上排 SIM 1、下排 SIM 2 |
## 实现与验证
@@ -13,18 +14,22 @@ Wi-Fi Hook 在原生绑定与活动更新之后将对应活动视图隐藏,避
配置通过项目既有 `OptionSpec`、偏好与 Hook 状态机制接入;状态栏子页面继承 `BaseSubPageActivity`,登记到父页搜索,并使用 `NetworkIconsActivity`。没有新增权限、作用域或原生依赖。
-接口静态核对基于提供的 HyperOS `OS4.0.22.0.XPNCNXM` ROM,SystemUI `17.03.260226.r`;其他 ROM 的私有接口兼容性尚未验证。用户已确认本次精简测试 APK 的两个开关均生效、双 SIM 保持原生布局,从状态栏页下拉通知中心/控制中心时背景显示正常。单元测试覆盖配置键、反射成员解析和两种只读 Flow 构造签名;没有新增临时诊断日志。
+接口静态核对基于提供的 HyperOS `OS4.0.22.0.XPNCNXM` ROM,SystemUI `17.03.260226.r`;其他 ROM 的私有接口兼容性尚未验证。用户先前已确认两个隐藏功能生效、关闭双排时保持原生布局,从状态栏页下拉通知中心/控制中心时背景显示正常。本轮恢复双排后,用户确认下拉通知中心、控制中心和返回桌面时双排无跳变,最终文案与三个功能均无异常。单元测试覆盖配置键、反射成员解析、只读 Flow 构造、图形比例、模型回退和绘制前事件处理;没有新增临时诊断日志。
-## 暂不启用双卡双行
+## 双排堆叠信号
-`hook/.../systemui/statusbar/` 中的 `DualSimSignalHook.kt`、`DualSimRuntimeSupport.kt`、`DualSimArtworkPolicy.kt`、`StackedSignalDrawable.kt`、`StatusBarPolicy.kt`、`StatusBarViews.kt` 及对应双排测试以逐行注释保留,供后续修复参考。相关偏好键和宿主 Pair 解析辅助也已注释。
+此前注释保留的双排实现和对应测试现已恢复。双排使用审阅通过的普通宽度、紧凑圆角图形;移除加粗样式和整个样式下拉框,旧 `statusbar_mobile_dual_style` 偏好不再读取。缩放、左右间距和垂直偏移继续提供,仅依赖双排开关;左右间距为原生间距的增量。
-这些文件不生成运行时类;当前没有双排 Hook 注册、配置入口或搜索项,不读取该功能的旧偏好。系统继续使用原生 SIM 信号布局。以后恢复时须重新接入配置与注册,验证各卡信号强度、单双卡切换、数据卡切换、锁屏及主题/明暗表现;不能只取消某个文件的注释就视为可用。
+图形在固定 16×20 的逻辑坐标中定义,随宿主 bounds 等比缩放,不再依赖原生主题图片的固有尺寸。主题、明暗和信号变化更新同一 Drawable;原生收集器返回前立即刷新,并在绘制前清理尚未合并的更新。临时隐藏、圆点及透明度动画不当作 SIM 消失,不因这些展示状态恢复原生单排图形。用户缩放与偏移在 Drawable 内完成,不覆盖 ImageView 的缩放或父容器的平移动画。
-此前电脑端导出的 PNG/SVG 不是运行时依赖,未进入仓库或 APK。当前构建没有图标解包、图片落盘或系统资源替换。
+仅对同一宿主中两张可见蜂窝 SIM 合并;单卡、无可识别强度、卫星或仅 VoWiFi 时恢复原生显示。灰格与亮格保留宿主着色和透明度。双排的模型订阅继续使用原生 JavaAdapter,不新增轮询。开关及四个布局偏好沿用旧键,保留已有配置;关闭双排并重新加载系统界面可恢复原生布局。
+
+用户已在 `1.13.0-dualsim.1`(versionCode 30)确认下拉 Control center / notification shade、返回桌面时双排无跳变,并在 `1.13.0-dualsim.2-ui`(versionCode 31)确认最终文案和三个功能正常。两包全部 DEX 和原生库逐字节一致;后一包仅修改文案资源和测试版本号。单双卡/数据卡切换、卫星、仅 VoWiFi、锁屏及主题/明暗切换尚未收到本轮逐项真机记录,不能用已有反馈代替这些覆盖。已合并的两个隐藏 Hook 保持原样。
+
+电脑端素材包已删除加粗 PNG/SVG 与包含加粗示例的旧预览,保留 50 张普通 PNG 和 2 张普通 SVG;这 52 个文件的内容与此前审阅版本一致。它们不是运行时依赖,未进入仓库或 APK。当前构建没有图标解包、图片落盘或系统资源替换。
## 参考与安全边界
功能思路参考 Pengeek(米客)`OS4-26.10.13-test`,来源与许可在应用内「关于 → 第三方许可证与致谢」统一登记。实现独立编写,未复用参考模块或小米专有代码/资源;逆向材料仅保留在本地忽略的 `reverse_report/`。项目继续以 AGPL-3.0 分发。
-新增 Hook 仅修改 SystemUI 进程中的视图可见性与显示 Flow,不请求 Root、不执行 shell、不挂载或重新挂载分区、不操作块设备/AVB,也不向受保护系统分区写文件。安装 APK、项目既有偏好及系统/框架日志仍可能保存普通应用数据,因此此说明不承诺整个模块或临时 Root 环境的所有组件均无写入。
+新增 Hook 仅修改 SystemUI 进程中的视图、显示 Flow 与内存 Drawable,不请求 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 a0fcfaf..c7176a5 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
@@ -12,6 +12,7 @@ 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.DualSimSignalHook
import cn.ianzb.hyperrefine.hook.systemui.statusbar.MobileIndicatorVisibilityHook
import cn.ianzb.hyperrefine.hook.systemui.statusbar.StatusBarKeys
@@ -28,6 +29,7 @@ class SystemUiLoad : BaseLoad() {
override fun onPackageLoaded(target: PackageTarget) {
initHook(HideWifiActivityHook(), HookPrefs.getBoolean(StatusBarKeys.WIFI_ACTIVITY))
initHook(MobileIndicatorVisibilityHook(), HookPrefs.getBoolean(StatusBarKeys.HIDE_TYPE))
+ initHook(DualSimSignalHook(), HookPrefs.getBoolean(StatusBarKeys.DUAL_ROWS))
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
index 9da9b0e..5fc82c1 100644
--- 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
@@ -1,61 +1,66 @@
-// 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)
-// }
-// }
+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
+ const val VIEWPORT_WIDTH = 16f
+ const val VIEWPORT_HEIGHT = 20f
+ 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.
+ }
+ }
+
+ /** Keep the approved normal artwork's proportions even if host bounds change aspect ratio. */
+ fun cells(width: Float, height: Float): List| {
+ if (!width.isFinite() || !height.isFinite() || width <= 0f || height <= 0f) return emptyList()
+ val scale = min(width / VIEWPORT_WIDTH, height / VIEWPORT_HEIGHT)
+ val offsetX = (width - VIEWPORT_WIDTH * scale) / 2f
+ val offsetY = height - VIEWPORT_HEIGHT * scale
+ val step = VIEWPORT_WIDTH / CELL_COUNT
+ val barWidth = step * 0.64f
+ val artworkHeight = min(VIEWPORT_HEIGHT * 0.72f, VIEWPORT_WIDTH * 0.80f)
+ val artworkTop = VIEWPORT_HEIGHT - artworkHeight
+ val bottomHeight = min(artworkHeight * 0.24f, barWidth * 0.92f)
+ val bottomTop = VIEWPORT_HEIGHT - bottomHeight
+ val upperBaseline = bottomTop - artworkHeight * 0.09f
+ val upperHeight = upperBaseline - artworkTop
+ val cells = (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, VIEWPORT_HEIGHT, radius)
+ }
+ return cells.map { it.copy(left = offsetX + it.left * scale, top = offsetY + it.top * scale,
+ right = offsetX + it.right * scale, bottom = offsetY + it.bottom * scale, radius = it.radius * scale) }
+ }
+
+ /** 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
index 08785ad..5f2cf1b 100644
--- 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
@@ -1,58 +1,71 @@
-// 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)
-// }
-// }
+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
+import java.util.concurrent.atomic.AtomicBoolean
+
+/** One refresh per event batch; an immediate/pre-draw flush makes the queued callback a no-op. */
+internal class DualSimRefreshGate(
+ private val post: (() -> Unit) -> Unit,
+ private val refresh: () -> Unit,
+) {
+ private val pending = AtomicBoolean()
+ fun request() {
+ if (pending.compareAndSet(false, true)) post { flush() }
+ }
+ fun flush() {
+ if (pending.compareAndSet(true, false)) refresh()
+ }
+}
+
+/** 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
index f9fe916..1c36fea 100644
--- 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
@@ -1,363 +1,350 @@
-// 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)
-// }
-// }
+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 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 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)
+ } } } }
+ 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 nativeColor = Color.WHITE
+ var stacked: StackedSignalDrawable? = null
+ val parent = WeakReference(image.parent as? ViewGroup)
+ 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 guard = StatusBarGuard("DualSimSignalHook")
+ private val refreshGate = DualSimRefreshGate({ callback -> handler.post { callback() } }, {
+ guard.run { refresh() }
+ })
+
+ 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.OnPreDrawListener {
+ // Final barrier: native visibility/resource updates must not leak an intermediate frame.
+ if (row.styled && image.drawable !is StackedSignalDrawable) {
+ capture(row, image)
+ schedule()
+ }
+ if (row.hiddenByModule && root.visibility != View.GONE) schedule()
+ flushPending()
+ true
+ }
+ var listening = false
+ fun attachListener() {
+ if (!listening && root.viewTreeObserver.isAlive) {
+ root.viewTreeObserver.addOnPreDrawListener(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.removeOnPreDrawListener(listener)
+ listening = false
+ schedule()
+ }
+ })
+ if (root.isAttachedToWindow) attachListener()
+ schedule()
+ }
+
+ fun emitted(image: ImageView) {
+ val affected = rows.values.filter { it.image.get() === image }
+ if (affected.isEmpty()) return
+ affected.forEach { row -> capture(row, image) }
+ schedule()
+ // Native collectors run on the UI thread. Restore composition before returning to drawing.
+ if (Looper.myLooper() == handler.looper) flushPending()
+ }
+
+ 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
+ } ?: native
+ 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() {
+ refreshGate.request()
+ }
+
+ private fun flushPending() {
+ refreshGate.flush()
+ }
+
+ 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 ->
+ // Host icon/dot/alpha transitions are presentation, not a SIM disappearing.
+ // Real visibility, provider and satellite state still come from the model below.
+ val eligible = group.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, 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) {
+ row.parent.get()?.let { parent ->
+ 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
+ }
+ }
+
+ private fun render(first: Row, levels: DualSimArtworkPolicy.Levels) {
+ val image = first.image.get() ?: return
+ val root = first.root.get() ?: return
+ first.styled = true
+ 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 density = image.resources.displayMetrics.density
+ val offset = setting(StatusBarKeys.DUAL_OFFSET, 0f, -4f, 4f) * density
+ val drawable = first.stacked ?: StackedSignalDrawable(density, levels.top, levels.bottom, color)
+ .also { first.stacked = it }
+ drawable.update(levels.top, levels.bottom, color, scale, offset)
+ if (image.drawable !== drawable) image.setImageDrawable(drawable)
+ (image.parent as? ViewGroup)?.let { parent ->
+ parent.clipChildren = if (scale > 1f || offset != 0f) false else first.originalClipChildren
+ parent.clipToPadding = if (scale > 1f || offset != 0f) false else first.originalClipPadding
+ }
+ val group = StatusBarViews.find(root, "mobile_group") as? ViewGroup ?: return
+ val left = first.originalLeft + (setting(StatusBarKeys.DUAL_LEFT_GAP, 0f, 0f, 8f) * density).roundToInt()
+ val right = first.originalRight + (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/StackedSignalDrawable.kt b/hook/src/main/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/StackedSignalDrawable.kt
index 6b7f8ce..c65d365 100644
--- 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
@@ -1,65 +1,77 @@
-// 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
-// }
+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
+import kotlin.math.roundToInt
+
+/** Four independently drawn cells per SIM; SystemUI supplies the current light/dark tint. */
+internal class StackedSignalDrawable(
+ private val density: Float,
+ private var topLevel: Int,
+ private var bottomLevel: Int,
+ private var 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
+ private var artworkScale = 1f
+ private var verticalOffset = 0f
+
+ /** Update the same drawable: a tint or signal event must not swap its shape or intrinsic size. */
+ fun update(top: Int, bottom: Int, color: Int, scale: Float, offset: Float) {
+ if (topLevel == top && bottomLevel == bottom && baseColor == color &&
+ artworkScale == scale && verticalOffset == offset) return
+ topLevel = top; bottomLevel = bottom; baseColor = color
+ artworkScale = scale; verticalOffset = offset
+ foreground = tint?.getColorForState(state, tint!!.defaultColor) ?: color
+ invalidateSelf()
+ }
+
+ 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())
+ }
+ val saved = canvas.save()
+ canvas.translate(bounds.left.toFloat(), bounds.top + verticalOffset)
+ // User sizing stays inside the drawable; do not overwrite native ImageView animations.
+ canvas.scale(artworkScale, artworkScale, bounds.width() / 2f, bounds.height().toFloat())
+ geometry.forEach { cell ->
+ val level = if (cell.row == 0) topLevel else bottomLevel
+ paint.color = DualSimArtworkPolicy.cellColor(foreground, opacity, cell.column < level)
+ canvas.drawRoundRect(cell.left, cell.top, cell.right, cell.bottom, cell.radius, cell.radius, paint)
+ }
+ canvas.restoreToCount(saved)
+ }
+
+ override fun setAlpha(alpha: Int) {
+ opacity = alpha.coerceIn(0, 255); invalidateSelf()
+ }
+ override fun getAlpha() = opacity
+ override fun setColorFilter(filter: ColorFilter?) {
+ paint.colorFilter = filter; invalidateSelf()
+ }
+ override fun setTintList(tint: ColorStateList?) {
+ this.tint = tint
+ foreground = tint?.getColorForState(state, tint.defaultColor) ?: baseColor
+ invalidateSelf()
+ }
+ override fun isStateful() = tint?.isStateful == true
+ override fun onStateChange(state: IntArray): Boolean {
+ val next = tint?.let { it.getColorForState(state, it.defaultColor) } ?: baseColor
+ if (foreground == next) return false
+ foreground = next; invalidateSelf(); return true
+ }
+ @Deprecated("Drawable opacity is retained for Android compatibility")
+ override fun getOpacity() = PixelFormat.TRANSLUCENT
+ override fun getIntrinsicWidth() = (DualSimArtworkPolicy.VIEWPORT_WIDTH * density).roundToInt().coerceAtLeast(1)
+ override fun getIntrinsicHeight() = (DualSimArtworkPolicy.VIEWPORT_HEIGHT * density).roundToInt().coerceAtLeast(1)
+}
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
index 7ece9bd..69b3b51 100644
--- 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
@@ -4,13 +4,12 @@ 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"
+ const val DUAL_ROWS = "statusbar_mobile_dual_rows"
+ 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"
+ // Deliberately do not read the retired style/bold preference, even from an older installation.
+ val DUAL_KEYS = listOf(DUAL_ROWS, DUAL_SCALE, DUAL_LEFT_GAP, DUAL_RIGHT_GAP, DUAL_OFFSET)
+ val ALL = listOf(WIFI_ACTIVITY, HIDE_TYPE) + DUAL_KEYS
}
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
index 94af558..9a4e9aa 100644
--- 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
@@ -1,29 +1,27 @@
-// 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
-// }
+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
index d41b2a5..c56b661 100644
--- 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
@@ -60,15 +60,14 @@ internal object StatusBarMembers {
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)
- // }
+ /** The host Pair comes from its own Kotlin loader; do not cast it 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) {
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
index ffdb6b8..d26f328 100644
--- 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
@@ -1,21 +1,19 @@
-// 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"))
-// }
+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/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimArtworkPolicyTest.kt b/hook/src/test/java/cn/ianzb/hyperrefine/hook/systemui/statusbar/DualSimArtworkPolicyTest.kt
index c623a62..29da707 100644
--- 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
@@ -1,84 +1,106 @@
-// 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)
-// }
-// }
+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 normalArtworkHasFourAlignedCellsPerRowInsideTheDrawable() {
+ for ((width, height) in listOf(16f to 20f, 32f to 28f, 8f to 8f)) {
+ val cells = DualSimArtworkPolicy.cells(width, height)
+ 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)
+ 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).isEmpty())
+ assertTrue(DualSimArtworkPolicy.cells(16f, -1f).isEmpty())
+ assertTrue(DualSimArtworkPolicy.cells(Float.NaN, 20f).isEmpty())
+ assertTrue(DualSimArtworkPolicy.cells(16f, Float.POSITIVE_INFINITY).isEmpty())
+ }
+
+ @Test fun canonicalViewportMatchesTheApprovedNormalSvg() {
+ val cells = DualSimArtworkPolicy.cells(16f, 20f)
+ assertEquals(0.72f, cells.first().left, 0.00001f)
+ assertEquals(2.56f, cells.first().right - cells.first().left, 0.00001f)
+ assertEquals(7.2f, cells[3].top, 0.00001f)
+ assertEquals(17.6448f, cells[4].top, 0.00001f)
+ assertEquals(20f, cells.last().bottom, 0f)
+ }
+
+ @Test fun aspectChangesScaleUniformlyWithoutMorphingTheBarsOrCorners() {
+ val template = DualSimArtworkPolicy.cells(16f, 20f)
+ for ((width, height) in listOf(32f to 28f, 20f to 20f, 16f to 48f, 64f to 16f)) {
+ val scale = minOf(width / 16f, height / 20f)
+ val cells = DualSimArtworkPolicy.cells(width, height)
+ template.zip(cells).forEach { (before, after) ->
+ assertEquals((before.right - before.left) * scale, after.right - after.left, 0.00001f)
+ assertEquals((before.bottom - before.top) * scale, after.bottom - after.top, 0.00001f)
+ assertEquals(before.radius * scale, after.radius, 0.00001f)
+ }
+ assertEquals(height, cells.last().bottom, 0.00001f)
+ }
+ }
+
+ @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
index 966f020..d2dae24 100644
--- 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
@@ -1,96 +1,135 @@
-// 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 {})
-// }
-// }
+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 {
+ @Test fun immediateNativeEmissionFlushDoesNotRunAgainOnTheNextFrame() {
+ val queued = mutableListOf<() -> Unit>()
+ var refreshes = 0
+ val gate = DualSimRefreshGate({ queued += it }, { refreshes++ })
+ gate.request()
+ gate.flush()
+ assertEquals(1, refreshes)
+ queued.single().invoke()
+ assertEquals(1, refreshes)
+ }
+
+ @Test fun multipleSignalEventsCoalesceAtThePreDrawBarrier() {
+ val queued = mutableListOf<() -> Unit>()
+ var latest = 0
+ val drawn = mutableListOf()
+ val gate = DualSimRefreshGate({ queued += it }, { drawn += latest })
+ latest = 1; gate.request()
+ latest = 4; gate.request()
+ assertEquals(1, queued.size)
+ gate.flush()
+ queued.single().invoke()
+ assertEquals(listOf(4), drawn)
+ }
+
+ @Test fun anEventDuringRefreshIsNotLostWhenThePreviousBatchFinishes() {
+ val queued = mutableListOf<() -> Unit>()
+ var refreshes = 0
+ lateinit var gate: DualSimRefreshGate
+ gate = DualSimRefreshGate({ queued += it }, {
+ refreshes++
+ if (refreshes == 1) gate.request()
+ })
+ gate.request()
+ queued.removeAt(0).invoke()
+ assertEquals(1, refreshes)
+ queued.removeAt(0).invoke()
+ assertEquals(2, refreshes)
+ gate.flush()
+ assertEquals(2, refreshes)
+ }
+
+ 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
index 1b28cbb..fffde6e 100644
--- 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
@@ -5,8 +5,9 @@ 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(listOf(StatusBarKeys.WIFI_ACTIVITY, StatusBarKeys.HIDE_TYPE) + StatusBarKeys.DUAL_KEYS, StatusBarKeys.ALL)
+ assertEquals(7, StatusBarKeys.ALL.distinct().size)
+ assertFalse(StatusBarKeys.ALL.any { it.contains("style") || it.contains("bold") })
assertEquals("com.android.systemui", StatusBarKeys.TARGET)
}
@@ -32,6 +33,36 @@ class StatusBarPolicyTest {
class UnoptimizedReadonly(val flow: MutableFlow, val job: Any?)
class InvalidReadonly(val value: Int)
+ @Test fun singleSimUnknownStrengthAndInvisibleCellKeepNativeLayout() {
+ val first = StatusBarPolicy.Signal(10, 101, true)
+ assertNull(StatusBarPolicy.pair(listOf(first), 10))
+ assertNull(StatusBarPolicy.pair(listOf(first, StatusBarPolicy.Signal(20, 0, true)), 10))
+ assertNull(StatusBarPolicy.pair(listOf(first, StatusBarPolicy.Signal(20, 102, false)), 10))
+ }
+
+ @Test fun satelliteAndVoWifiOnlyRowsDoNotBecomeCellularBars() {
+ val first = StatusBarPolicy.Signal(10, 101, true)
+ val second = StatusBarPolicy.Signal(20, 102, true)
+ assertNull(StatusBarPolicy.pair(listOf(first, second.copy(satellite = true)), 10))
+ assertNull(StatusBarPolicy.pair(listOf(first, second.copy(cellularMode = false)), 10))
+ assertNull(StatusBarPolicy.pair(listOf(first, second.copy(visible = false, rootVisible = true)), 10))
+ }
+
+ @Test fun dataSimChangesTheNativeCarrierButPhysicalSimStrengthOrderStaysFixed() {
+ val first = StatusBarPolicy.Signal(10, 101, true)
+ val second = StatusBarPolicy.Signal(20, 102, true)
+ assertEquals(first, StatusBarPolicy.pair(listOf(first, second), 10)?.primary)
+ assertEquals(second, StatusBarPolicy.pair(listOf(first, second), 20)?.primary)
+ assertEquals(DualSimArtworkPolicy.levels(0, 1, 5, 2), DualSimArtworkPolicy.levels(1, 0, 2, 5))
+ }
+
+ @Test fun hostPairVisibilityDistinguishesCellularSignalFromTheRoot() {
+ assertEquals(StatusBarPolicy.Visibility(true, false), StatusBarMembers.mobileVisibility(true to false))
+ assertEquals(StatusBarPolicy.Visibility(false, false), StatusBarMembers.mobileVisibility(false to true))
+ assertEquals(StatusBarPolicy.Visibility(true, true), StatusBarMembers.mobileVisibility(true))
+ assertNull(StatusBarMembers.mobileVisibility(true to "unknown"))
+ }
+
@Test fun romOptimizedReadonlyFlowConstructorRetainsTheReplacementFlow() {
val source = MutableFlow(false)
val wrapped = StatusBarMembers.readonlyFlow(OptimizedReadonly::class.java, source) as OptimizedReadonly
| |