Support unlocked icon filtering across Android variants
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.module
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import android.util.Log
|
||||||
|
import io.github.libxposed.api.XposedInterface
|
||||||
|
import se.ajpanton.notificationsmaster.visibility.NotificationSurface
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.WeakHashMap
|
||||||
|
|
||||||
|
/** API 34's class-id 1 mapper is used exclusively by the unlocked status-bar container. */
|
||||||
|
internal class Aosp34UnlockedIconBackend(private val framework: XposedInterface) : UnlockedIconBackend {
|
||||||
|
private val controllers = Collections.newSetFromMap(WeakHashMap<Any, Boolean>())
|
||||||
|
private lateinit var updateStatusBarIcons: Method
|
||||||
|
@Volatile private var blockedObserved = false
|
||||||
|
|
||||||
|
override fun install(classLoader: ClassLoader) {
|
||||||
|
if (Build.VERSION.SDK_INT != 34) return
|
||||||
|
runCatching {
|
||||||
|
val controller = Class.forName(CONTROLLER, false, classLoader)
|
||||||
|
val mapper = Class.forName(ICON_MAPPER, false, classLoader)
|
||||||
|
val entry = Class.forName(NOTIFICATION_ENTRY, false, classLoader)
|
||||||
|
val classId = mapper.getDeclaredField("\$r8\$classId").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
|
||||||
|
}
|
||||||
|
val apply = mapper.declaredMethods.singleOrNull {
|
||||||
|
it.name == "apply" && it.parameterTypes.contentEquals(arrayOf(Any::class.java))
|
||||||
|
} ?: error("Unlocked icon mapper method not found")
|
||||||
|
updateStatusBarIcons = controller.declaredMethods.singleOrNull {
|
||||||
|
it.name == "updateStatusBarIcons" && it.parameterTypes.isEmpty() && it.returnType == Void.TYPE
|
||||||
|
} ?: error("Unlocked icon refresh method not found")
|
||||||
|
apply.isAccessible = true
|
||||||
|
updateStatusBarIcons.isAccessible = true
|
||||||
|
framework.hook(updateStatusBarIcons).intercept { chain ->
|
||||||
|
synchronized(controllers) { controllers += chain.thisObject }
|
||||||
|
chain.proceed()
|
||||||
|
}
|
||||||
|
framework.hook(apply).intercept { chain ->
|
||||||
|
if (classId.getInt(chain.thisObject) != STATUS_BAR_CLASS_ID) return@intercept chain.proceed()
|
||||||
|
val notification = runCatching {
|
||||||
|
VisibilityNotificationExtractor.from(sbn.get(chain.args[0]) as StatusBarNotification)
|
||||||
|
}.getOrNull() ?: return@intercept chain.proceed()
|
||||||
|
if (!ProcessVisibilityPolicyCache.isBlocked(
|
||||||
|
notification,
|
||||||
|
NotificationSurface.UNLOCKED_STATUSBAR,
|
||||||
|
)) return@intercept chain.proceed()
|
||||||
|
if (!blockedObserved) {
|
||||||
|
blockedObserved = true
|
||||||
|
Log.i(TAG, "Filtered an API 34 unlocked notification icon")
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
Log.i(TAG, "Installed AOSP API 34 unlocked notification-icon backend")
|
||||||
|
}.onFailure {
|
||||||
|
Log.w(TAG, "AOSP API 34 unlocked icon shape is unsupported; leaving icons unchanged", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPolicyChanged() {
|
||||||
|
blockedObserved = false
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
val current = synchronized(controllers) { controllers.toList() }
|
||||||
|
current.forEach { controller ->
|
||||||
|
runCatching { updateStatusBarIcons.invoke(controller) }
|
||||||
|
.onFailure { Log.w(TAG, "Could not refresh API 34 unlocked icons", it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TAG = "NotificationsMaster"
|
||||||
|
const val STATUS_BAR_CLASS_ID = 1
|
||||||
|
const val CONTROLLER = "com.android.systemui.statusbar.phone.NotificationIconAreaController"
|
||||||
|
const val ICON_MAPPER = "$CONTROLLER\$\$ExternalSyntheticLambda2"
|
||||||
|
const val NOTIFICATION_ENTRY = "com.android.systemui.statusbar.notification.collection.NotificationEntry"
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
-63
@@ -1,8 +1,6 @@
|
|||||||
package se.ajpanton.notificationsmaster.module
|
package se.ajpanton.notificationsmaster.module
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
|
||||||
import android.service.notification.StatusBarNotification
|
import android.service.notification.StatusBarNotification
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import io.github.libxposed.api.XposedInterface
|
import io.github.libxposed.api.XposedInterface
|
||||||
@@ -14,34 +12,41 @@ import java.lang.reflect.Proxy
|
|||||||
import java.util.LinkedHashSet
|
import java.util.LinkedHashSet
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/** Filters only API 36's status-bar icon flow; the notification/shade model is untouched. */
|
/** Filters only the modern status-bar icon flow; the notification/shade model is untouched. */
|
||||||
internal class Aosp36UnlockedIconBackend(private val framework: XposedInterface) {
|
internal class AospModernUnlockedIconBackend(private val framework: XposedInterface) : UnlockedIconBackend {
|
||||||
private val metadata = ConcurrentHashMap<String, VisibilityNotification>()
|
private val metadata = ConcurrentHashMap<String, VisibilityNotification>()
|
||||||
@Volatile private var policyChanges: Any? = null
|
@Volatile private var policyChanges: Any? = null
|
||||||
private var policyRevision = 0L
|
private var policyRevision = 0L
|
||||||
@Volatile private var lastFilterSignature = -1L
|
@Volatile private var lastFilterSignature = -1L
|
||||||
@Volatile private var captureFailureLogged = false
|
@Volatile private var captureFailureLogged = false
|
||||||
|
|
||||||
fun install(classLoader: ClassLoader) {
|
override fun install(classLoader: ClassLoader) {
|
||||||
if (Build.VERSION.SDK_INT != 36) return
|
if (Build.VERSION.SDK_INT !in 35..36) return
|
||||||
runCatching {
|
runCatching {
|
||||||
val entryClass = Class.forName(NOTIFICATION_ENTRY, false, classLoader)
|
val entryClass = Class.forName(NOTIFICATION_ENTRY, false, classLoader)
|
||||||
val modelClass = Class.forName(ACTIVE_NOTIFICATION_MODEL, false, classLoader)
|
val modelClass = Class.forName(ACTIVE_NOTIFICATION_MODEL, false, classLoader)
|
||||||
val modelAccess = ModelAccess(
|
val modelAccesses = buildList {
|
||||||
modelClass.requiredField("key", String::class.java),
|
add(ModelAccess(
|
||||||
modelClass.requiredField("packageName", String::class.java),
|
modelClass,
|
||||||
)
|
modelClass.requiredField("key", String::class.java),
|
||||||
|
modelClass.requiredField("packageName", String::class.java),
|
||||||
|
))
|
||||||
|
runCatching { Class.forName(ACTIVE_NOTIFICATION_ICON_MODEL, false, classLoader) }
|
||||||
|
.getOrNull()?.let { iconModel ->
|
||||||
|
add(ModelAccess(iconModel, iconModel.requiredField("notifKey", String::class.java), null))
|
||||||
|
}
|
||||||
|
}
|
||||||
installMetadataCapture(classLoader, entryClass, modelClass)
|
installMetadataCapture(classLoader, entryClass, modelClass)
|
||||||
installFlowRefresh(classLoader)
|
installFlowRefresh(classLoader)
|
||||||
installStatusBarFilter(classLoader, modelClass, modelAccess)
|
installStatusBarFilter(classLoader, modelAccesses)
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
Log.i(TAG, "Installed AOSP 36 unlocked notification-icon backend")
|
Log.i(TAG, "Installed AOSP API ${Build.VERSION.SDK_INT} unlocked notification-icon backend")
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Log.w(TAG, "AOSP 36 unlocked icon shape is unsupported; leaving icons unchanged", it)
|
Log.w(TAG, "AOSP API ${Build.VERSION.SDK_INT} unlocked icon shape is unsupported; leaving icons unchanged", it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized fun onPolicyChanged() {
|
@Synchronized override fun onPolicyChanged() {
|
||||||
val signal = policyChanges ?: return
|
val signal = policyChanges ?: return
|
||||||
policyRevision += 1
|
policyRevision += 1
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -60,7 +65,7 @@ internal class Aosp36UnlockedIconBackend(private val framework: XposedInterface)
|
|||||||
framework.hook(method).intercept { chain ->
|
framework.hook(method).intercept { chain ->
|
||||||
val result = chain.proceed()
|
val result = chain.proceed()
|
||||||
runCatching { sbnField.get(chain.args[0]) as StatusBarNotification }
|
runCatching { sbnField.get(chain.args[0]) as StatusBarNotification }
|
||||||
.map(::visibilityNotification)
|
.map(VisibilityNotificationExtractor::from)
|
||||||
.onSuccess { metadata[it.key] = it }
|
.onSuccess { metadata[it.key] = it }
|
||||||
.onFailure {
|
.onFailure {
|
||||||
if (!captureFailureLogged) {
|
if (!captureFailureLogged) {
|
||||||
@@ -81,15 +86,53 @@ internal class Aosp36UnlockedIconBackend(private val framework: XposedInterface)
|
|||||||
val signal = Class.forName("kotlinx.coroutines.flow.StateFlowKt", false, classLoader)
|
val signal = Class.forName("kotlinx.coroutines.flow.StateFlowKt", false, classLoader)
|
||||||
.declaredMethods.single { it.name == "MutableStateFlow" && it.parameterTypes.size == 1 }
|
.declaredMethods.single { it.name == "MutableStateFlow" && it.parameterTypes.size == 1 }
|
||||||
.invoke(null, policyRevision)
|
.invoke(null, policyRevision)
|
||||||
val combine = Class.forName(TWO_FLOW_COMBINE, false, classLoader)
|
val combineMethod = Class.forName("kotlinx.coroutines.flow.FlowKt", false, classLoader)
|
||||||
.declaredConstructors.single {
|
.methods.singleOrNull {
|
||||||
it.parameterTypes.map { type -> type.name } == listOf(
|
it.name == "combine" && it.parameterTypes.map(Class<*>::getName) == listOf(
|
||||||
flowClass.name,
|
flowClass.name,
|
||||||
flowClass.name,
|
flowClass.name,
|
||||||
functionClass.name,
|
functionClass.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.also { it.isAccessible = true }
|
val transformMethod = if (combineMethod == null) {
|
||||||
|
val function4 = Class.forName("kotlin.jvm.functions.Function4", false, classLoader)
|
||||||
|
Class.forName("kotlinx.coroutines.flow.FlowKt", false, classLoader).methods.singleOrNull {
|
||||||
|
it.name == "combineTransform" && it.parameterTypes.map(Class<*>::getName) == listOf(
|
||||||
|
flowClass.name,
|
||||||
|
flowClass.name,
|
||||||
|
function4.name,
|
||||||
|
)
|
||||||
|
}?.let { method ->
|
||||||
|
val collector = Class.forName("kotlinx.coroutines.flow.FlowCollector", false, classLoader)
|
||||||
|
val continuation = Class.forName("kotlin.coroutines.Continuation", false, classLoader)
|
||||||
|
val emit = collector.getMethod("emit", Any::class.java, continuation)
|
||||||
|
method to Proxy.newProxyInstance(classLoader, arrayOf(function4)) { proxy, called, args ->
|
||||||
|
when (called.name) {
|
||||||
|
"invoke" -> emit.invoke(args!![0], args[1], args[3])
|
||||||
|
"toString" -> "NotificationsMasterVisibilityTransform"
|
||||||
|
"hashCode" -> System.identityHashCode(proxy)
|
||||||
|
"equals" -> proxy === args?.get(0)
|
||||||
|
else -> error("Unexpected Function4 method ${called.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val combineConstructor = if (combineMethod == null && transformMethod == null) {
|
||||||
|
Class.forName(TWO_FLOW_COMBINE, false, classLoader).declaredConstructors.singleOrNull {
|
||||||
|
it.parameterTypes.map(Class<*>::getName) == listOf(
|
||||||
|
flowClass.name,
|
||||||
|
flowClass.name,
|
||||||
|
functionClass.name,
|
||||||
|
)
|
||||||
|
}?.also { it.isAccessible = true }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
require(combineMethod != null || transformMethod != null || combineConstructor != null) {
|
||||||
|
"Two-flow combine implementation not found"
|
||||||
|
}
|
||||||
val keepUpstreamValue = Proxy.newProxyInstance(classLoader, arrayOf(functionClass)) { proxy, method, args ->
|
val keepUpstreamValue = Proxy.newProxyInstance(classLoader, arrayOf(functionClass)) { proxy, method, args ->
|
||||||
when (method.name) {
|
when (method.name) {
|
||||||
"invoke" -> args?.get(0)
|
"invoke" -> args?.get(0)
|
||||||
@@ -107,46 +150,69 @@ internal class Aosp36UnlockedIconBackend(private val framework: XposedInterface)
|
|||||||
"com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository",
|
"com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
?: error("Status-bar notification interactor constructor not found")
|
val (field, constructors) = if (constructor != null) {
|
||||||
constructor.isAccessible = true
|
flowField to listOf(constructor)
|
||||||
framework.hook(constructor).intercept { chain ->
|
} else {
|
||||||
val result = chain.proceed()
|
val viewModel = Class.forName(STATUS_BAR_VIEW_MODEL, false, classLoader)
|
||||||
runCatching {
|
viewModel.requiredField("icons", null).also {
|
||||||
val upstream = flowField.get(chain.thisObject)
|
require(flowClass.isAssignableFrom(it.type))
|
||||||
flowField.set(chain.thisObject, combine.newInstance(upstream, signal, keepUpstreamValue))
|
} to viewModel.declaredConstructors.toList()
|
||||||
}.onFailure { Log.w(TAG, "Could not attach visibility refresh flow; leaving icons unchanged", it) }
|
}
|
||||||
result
|
require(constructors.isNotEmpty()) { "Status-bar flow owner has no constructors" }
|
||||||
|
constructors.forEach { target ->
|
||||||
|
target.isAccessible = true
|
||||||
|
framework.hook(target).intercept { chain ->
|
||||||
|
val result = chain.proceed()
|
||||||
|
runCatching {
|
||||||
|
val upstream = field.get(chain.thisObject)
|
||||||
|
val combined = combineMethod?.invoke(null, upstream, signal, keepUpstreamValue)
|
||||||
|
?: transformMethod?.let { (method, transform) ->
|
||||||
|
method.invoke(null, upstream, signal, transform)
|
||||||
|
}
|
||||||
|
?: combineConstructor!!.newInstance(upstream, signal, keepUpstreamValue)
|
||||||
|
field.set(chain.thisObject, combined)
|
||||||
|
}.onFailure { Log.w(TAG, "Could not attach visibility refresh flow; leaving icons unchanged", it) }
|
||||||
|
result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun installStatusBarFilter(
|
private fun installStatusBarFilter(
|
||||||
classLoader: ClassLoader,
|
classLoader: ClassLoader,
|
||||||
modelClass: Class<*>,
|
modelAccesses: List<ModelAccess>,
|
||||||
access: ModelAccess,
|
|
||||||
) {
|
) {
|
||||||
val collector = Class.forName(STATUS_BAR_MAP_COLLECTOR, false, classLoader)
|
val collector = statusBarMapCollectors().firstNotNullOfOrNull { name ->
|
||||||
|
runCatching { Class.forName(name, false, classLoader) }.getOrNull()
|
||||||
|
} ?: error("Status-bar icon collector class not found")
|
||||||
val emit = collector.declaredMethods.singleOrNull(::isEmitMethod)
|
val emit = collector.declaredMethods.singleOrNull(::isEmitMethod)
|
||||||
?: error("Status-bar icon collector method not found")
|
?: error("Status-bar icon collector method not found")
|
||||||
emit.isAccessible = true
|
emit.isAccessible = true
|
||||||
framework.hook(emit).intercept { chain ->
|
framework.hook(emit).intercept { chain ->
|
||||||
val original = chain.args[0]
|
val original = chain.args[0]
|
||||||
val filtered = filter(original, modelClass, access)
|
val filtered = filter(original, modelAccesses)
|
||||||
if (filtered === original) chain.proceed() else chain.proceed(arrayOf(filtered, chain.args[1]))
|
if (filtered === original) chain.proceed() else chain.proceed(arrayOf(filtered, chain.args[1]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filter(value: Any?, modelClass: Class<*>, access: ModelAccess): Any? {
|
private fun filter(value: Any?, modelAccesses: List<ModelAccess>): Any? {
|
||||||
val entries = value as? Set<*> ?: return value
|
val entries = value as? Set<*> ?: return value
|
||||||
if (entries.any { it != null && !modelClass.isInstance(it) }) return value
|
if (entries.any { model -> model != null && modelAccesses.none { it.type.isInstance(model) } }) return value
|
||||||
val activeKeys = entries.mapNotNull { access.key.get(it) as? String }.toSet()
|
fun access(model: Any) = modelAccesses.first { it.type.isInstance(model) }
|
||||||
|
val activeKeys = entries.mapNotNull { model ->
|
||||||
|
model?.let { access(it).key.get(it) as? String }
|
||||||
|
}.toSet()
|
||||||
metadata.keys.retainAll(activeKeys)
|
metadata.keys.retainAll(activeKeys)
|
||||||
var changed = false
|
var changed = false
|
||||||
var missingMetadata = 0
|
var missingMetadata = 0
|
||||||
val filtered = entries.filterTo(LinkedHashSet(entries.size)) { model ->
|
val filtered = entries.filterTo(LinkedHashSet(entries.size)) { model ->
|
||||||
if (model == null) return@filterTo true
|
if (model == null) return@filterTo true
|
||||||
val key = access.key.get(model) as? String ?: return@filterTo true
|
val fields = access(model)
|
||||||
val packageName = access.packageName.get(model) as? String ?: return@filterTo true
|
val key = fields.key.get(model) as? String ?: return@filterTo true
|
||||||
val captured = metadata[key]
|
val captured = metadata[key]
|
||||||
|
val packageName = fields.packageName?.get(model) as? String
|
||||||
|
?: captured?.packageName
|
||||||
|
?: key.split('|', limit = 3).getOrNull(1)
|
||||||
|
?: return@filterTo true
|
||||||
val hasExceptions = ProcessVisibilityPolicyCache.hasExceptions(packageName)
|
val hasExceptions = ProcessVisibilityPolicyCache.hasExceptions(packageName)
|
||||||
if (hasExceptions && captured == null) missingMetadata += 1
|
if (hasExceptions && captured == null) missingMetadata += 1
|
||||||
val notification = captured ?: VisibilityNotification(packageName, key)
|
val notification = captured ?: VisibilityNotification(packageName, key)
|
||||||
@@ -168,7 +234,7 @@ internal class Aosp36UnlockedIconBackend(private val framework: XposedInterface)
|
|||||||
return if (changed) filtered else value
|
return if (changed) filtered else value
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class ModelAccess(val key: Field, val packageName: Field)
|
private data class ModelAccess(val type: Class<*>, val key: Field, val packageName: Field?)
|
||||||
|
|
||||||
private fun Class<*>.requiredField(name: String, type: Class<*>?): Field =
|
private fun Class<*>.requiredField(name: String, type: Class<*>?): Field =
|
||||||
getDeclaredField(name).also {
|
getDeclaredField(name).also {
|
||||||
@@ -179,42 +245,26 @@ internal class Aosp36UnlockedIconBackend(private val framework: XposedInterface)
|
|||||||
private fun isEmitMethod(method: Method) = method.name == "emit" &&
|
private fun isEmitMethod(method: Method) = method.name == "emit" &&
|
||||||
method.parameterTypes.size == 2 && method.parameterTypes[1].name == "kotlin.coroutines.Continuation"
|
method.parameterTypes.size == 2 && method.parameterTypes[1].name == "kotlin.coroutines.Continuation"
|
||||||
|
|
||||||
private fun visibilityNotification(sbn: StatusBarNotification): VisibilityNotification {
|
private fun statusBarMapCollectors() =
|
||||||
val notification = sbn.notification
|
(if (Build.VERSION.SDK_INT == 35) listOf("2\$2", "1\$2") else listOf("1\$2"))
|
||||||
val extras = notification.extras ?: Bundle.EMPTY
|
.map { STATUS_BAR_MAP_COLLECTOR_PREFIX + it }
|
||||||
val messages = listOf(Notification.EXTRA_HISTORIC_MESSAGES, Notification.EXTRA_MESSAGES).flatMap { key ->
|
|
||||||
Notification.MessagingStyle.Message.getMessagesFromBundleArray(
|
|
||||||
extras.getParcelableArray(key, Bundle::class.java),
|
|
||||||
).map { message ->
|
|
||||||
listOfNotNull(message.senderPerson?.name, message.text).joinToString(": ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return VisibilityNotification(
|
|
||||||
packageName = sbn.packageName,
|
|
||||||
key = sbn.key,
|
|
||||||
title = extras.text(Notification.EXTRA_TITLE),
|
|
||||||
text = extras.text(Notification.EXTRA_TEXT),
|
|
||||||
bigText = extras.text(Notification.EXTRA_BIG_TEXT),
|
|
||||||
subtext = extras.text(Notification.EXTRA_SUB_TEXT),
|
|
||||||
messages = messages,
|
|
||||||
channelId = notification.channelId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Bundle.text(key: String) = getCharSequence(key)?.toString()
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val TAG = "NotificationsMaster"
|
const val TAG = "NotificationsMaster"
|
||||||
const val NOTIFICATION_ENTRY = "com.android.systemui.statusbar.notification.collection.NotificationEntry"
|
const val NOTIFICATION_ENTRY = "com.android.systemui.statusbar.notification.collection.NotificationEntry"
|
||||||
const val ACTIVE_NOTIFICATION_MODEL =
|
const val ACTIVE_NOTIFICATION_MODEL =
|
||||||
"com.android.systemui.statusbar.notification.shared.ActiveNotificationModel"
|
"com.android.systemui.statusbar.notification.shared.ActiveNotificationModel"
|
||||||
|
const val ACTIVE_NOTIFICATION_ICON_MODEL =
|
||||||
|
"com.android.systemui.statusbar.notification.icon.domain.interactor.ActiveNotificationIconModel"
|
||||||
const val ACTIVE_STORE_BUILDER =
|
const val ACTIVE_STORE_BUILDER =
|
||||||
"com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsStoreBuilder"
|
"com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsStoreBuilder"
|
||||||
const val STATUS_BAR_INTERACTOR =
|
const val STATUS_BAR_INTERACTOR =
|
||||||
"com.android.systemui.statusbar.notification.icon.domain.interactor.StatusBarNotificationIconsInteractor"
|
"com.android.systemui.statusbar.notification.icon.domain.interactor.StatusBarNotificationIconsInteractor"
|
||||||
const val STATUS_BAR_MAP_COLLECTOR =
|
const val STATUS_BAR_VIEW_MODEL =
|
||||||
|
"com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel"
|
||||||
|
const val STATUS_BAR_MAP_COLLECTOR_PREFIX =
|
||||||
"com.android.systemui.statusbar.notification.icon.ui.viewmodel." +
|
"com.android.systemui.statusbar.notification.icon.ui.viewmodel." +
|
||||||
"NotificationIconContainerStatusBarViewModel\$special\$\$inlined\$map\$1\$2"
|
"NotificationIconContainerStatusBarViewModel\$special\$\$inlined\$map\$"
|
||||||
const val TWO_FLOW_COMBINE =
|
const val TWO_FLOW_COMBINE =
|
||||||
"kotlinx.coroutines.flow.FlowKt__ZipKt\$combine\$\$inlined\$unsafeFlow\$1"
|
"kotlinx.coroutines.flow.FlowKt__ZipKt\$combine\$\$inlined\$unsafeFlow\$1"
|
||||||
}
|
}
|
||||||
+4
-2
@@ -11,13 +11,15 @@ import io.github.libxposed.api.XposedModuleInterface
|
|||||||
* in a compatible framework and grants it a system-server scope.
|
* in a compatible framework and grants it a system-server scope.
|
||||||
*/
|
*/
|
||||||
class NotificationsMasterModule : XposedModule() {
|
class NotificationsMasterModule : XposedModule() {
|
||||||
private var systemUiBackend: Aosp36UnlockedIconBackend? = null
|
private var systemUiBackend: UnlockedIconBackend? = null
|
||||||
|
|
||||||
override fun onPackageReady(param: XposedModuleInterface.PackageReadyParam) {
|
override fun onPackageReady(param: XposedModuleInterface.PackageReadyParam) {
|
||||||
if (param.packageName !in VisibilityPolicySync.TARGET_PACKAGES) return
|
if (param.packageName !in VisibilityPolicySync.TARGET_PACKAGES) return
|
||||||
if (param.packageName == SYSTEM_UI_PACKAGE) {
|
if (param.packageName == SYSTEM_UI_PACKAGE) {
|
||||||
if (systemUiBackend != null) return
|
if (systemUiBackend != null) return
|
||||||
val backend = Aosp36UnlockedIconBackend(this)
|
val backend = if (android.os.Build.VERSION.SDK_INT == 34) {
|
||||||
|
Aosp34UnlockedIconBackend(this)
|
||||||
|
} else AospModernUnlockedIconBackend(this)
|
||||||
backend.install(param.classLoader)
|
backend.install(param.classLoader)
|
||||||
systemUiBackend = backend
|
systemUiBackend = backend
|
||||||
ProcessVisibilityPolicyCache.installWhenReady(backend::onPolicyChanged)
|
ProcessVisibilityPolicyCache.installWhenReady(backend::onPolicyChanged)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.module
|
||||||
|
|
||||||
|
internal interface UnlockedIconBackend {
|
||||||
|
fun install(classLoader: ClassLoader)
|
||||||
|
fun onPolicyChanged()
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.module
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import se.ajpanton.notificationsmaster.visibility.VisibilityNotification
|
||||||
|
|
||||||
|
internal object VisibilityNotificationExtractor {
|
||||||
|
fun from(sbn: StatusBarNotification): VisibilityNotification {
|
||||||
|
val notification = sbn.notification
|
||||||
|
val extras = notification.extras ?: Bundle.EMPTY
|
||||||
|
val messages = listOf(Notification.EXTRA_HISTORIC_MESSAGES, Notification.EXTRA_MESSAGES).flatMap { key ->
|
||||||
|
Notification.MessagingStyle.Message.getMessagesFromBundleArray(
|
||||||
|
extras.getParcelableArray(key, Bundle::class.java),
|
||||||
|
).map { message ->
|
||||||
|
listOfNotNull(message.senderPerson?.name, message.text).joinToString(": ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return VisibilityNotification(
|
||||||
|
packageName = sbn.packageName,
|
||||||
|
key = sbn.key,
|
||||||
|
title = extras.text(Notification.EXTRA_TITLE),
|
||||||
|
text = extras.text(Notification.EXTRA_TEXT),
|
||||||
|
bigText = extras.text(Notification.EXTRA_BIG_TEXT),
|
||||||
|
subtext = extras.text(Notification.EXTRA_SUB_TEXT),
|
||||||
|
messages = messages,
|
||||||
|
channelId = notification.channelId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Bundle.text(key: String) = getCharSequence(key)?.toString()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user