diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiUnlockedIconBackend.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiUnlockedIconBackend.kt index fc9a7f5..fe5a5ad 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiUnlockedIconBackend.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/OneUiUnlockedIconBackend.kt @@ -4,6 +4,8 @@ import android.os.Handler import android.os.Looper import android.service.notification.StatusBarNotification import android.util.Log +import android.view.View +import android.view.ViewGroup import io.github.libxposed.api.XposedInterface import se.ajpanton.notificationsmaster.visibility.NotificationSurface import java.lang.reflect.Method @@ -14,7 +16,9 @@ import java.util.function.Function /** Filters only OneUI's native unlocked status-bar container. */ internal class OneUiUnlockedIconBackend(private val framework: XposedInterface) : VisibilitySurfaceBackend { private val controllers = Collections.newSetFromMap(WeakHashMap()) + private val iconContainers = Collections.newSetFromMap(WeakHashMap()) private lateinit var updateStatusBarIcons: Method + private lateinit var maxStaticIcons: java.lang.reflect.Field @Volatile private var blockedObserved = false override fun install(classLoader: ClassLoader): Boolean = runCatching { @@ -25,6 +29,10 @@ internal class OneUiUnlockedIconBackend(private val framework: XposedInterface) require(it.type == container) it.isAccessible = true } + maxStaticIcons = container.getDeclaredField("mMaxStaticIcons").also { + require(it.type == Int::class.javaPrimitiveType) + it.isAccessible = true + } val sbn = entry.getDeclaredField("mSbn").also { require(it.type == StatusBarNotification::class.java) it.isAccessible = true @@ -50,10 +58,13 @@ internal class OneUiUnlockedIconBackend(private val framework: XposedInterface) updateStatusBarIcons.isAccessible = true framework.hook(updateStatusBarIcons).intercept { chain -> synchronized(controllers) { controllers += chain.thisObject } + (notificationIcons.get(chain.thisObject) as? View)?.let(::manage) chain.proceed() } framework.hook(update).intercept { chain -> - if (chain.args[1] !== notificationIcons.get(chain.thisObject)) return@intercept chain.proceed() + val icons = notificationIcons.get(chain.thisObject) + if (chain.args[1] !== icons) return@intercept chain.proceed() + (icons as? View)?.let(::manage) @Suppress("UNCHECKED_CAST") val original = chain.args[0] as? Function ?: return@intercept chain.proceed() val wrapped = Function { item -> @@ -77,6 +88,28 @@ internal class OneUiUnlockedIconBackend(private val framework: XposedInterface) args[0] = wrapped chain.proceed(args) } + container.declaredMethods.filter { it.name == "calculateIconXTranslations" }.forEach { method -> + method.isAccessible = true + framework.hook(method).intercept { chain -> + (chain.thisObject as? View)?.takeIf(::isManaged)?.let(::applyLimit) + chain.proceed() + } + } + container.declaredMethods.filter { it.name == "shouldForceOverflow" }.forEach { method -> + method.isAccessible = true + framework.hook(method).intercept { chain -> + val view = chain.thisObject as? View + if (view == null || !isManaged(view) || chain.args.size < 4) { + return@intercept chain.proceed() + } + val limit = unlockedLimit() + applyLimit(view) + val index = chain.args[0] as? Int ?: return@intercept chain.proceed() + val speedBump = chain.args[1] as? Int ?: -1 + val appear = chain.args[2] as? Float ?: 0f + (speedBump != -1 && index >= speedBump && appear > 0f) || index >= limit + } + } }.onSuccess { Log.i(TAG, "Installed OneUI unlocked notification-icon backend") }.onFailure { @@ -94,10 +127,41 @@ internal class OneUiUnlockedIconBackend(private val framework: XposedInterface) } } + private fun manage(view: View) { + synchronized(iconContainers) { iconContainers += view } + applyLimit(view) + widenParent(view) + } + + private fun isManaged(view: View) = synchronized(iconContainers) { view in iconContainers } + + private fun applyLimit(view: View) { + maxStaticIcons.setInt(view, unlockedLimit()) + } + + private fun unlockedLimit() = ProcessVisibilityPolicyCache.unlockedIconLimit ?: UNLIMITED_ICONS + + private fun widenParent(view: View) { + var parent = view.parent as? View + repeat(3) { + val current = parent ?: return + if (current.javaClass.name.contains("AlphaOptimized")) { + current.layoutParams?.takeIf { + it.width == ViewGroup.LayoutParams.WRAP_CONTENT || it.width == 0 + }?.let { + it.width = ViewGroup.LayoutParams.MATCH_PARENT + current.layoutParams = it + } + } + parent = current.parent as? View + } + } + private companion object { const val TAG = "NotificationsMaster" const val CONTROLLER = "com.android.systemui.statusbar.phone.LegacyNotificationIconAreaControllerImpl" const val ICON_CONTAINER = "com.android.systemui.statusbar.phone.NotificationIconContainer" const val NOTIFICATION_ENTRY = "com.android.systemui.statusbar.notification.collection.NotificationEntry" + const val UNLIMITED_ICONS = 1000 } }