Add notification visibility policy bridge
This commit is contained in:
@@ -53,6 +53,10 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".module.VisibilityPolicyRequestReceiver"
|
||||
android:exported="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -3,6 +3,7 @@ package se.ajpanton.notificationsmaster
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager
|
||||
import se.ajpanton.notificationsmaster.data.EncryptedNotificationLogStore
|
||||
import se.ajpanton.notificationsmaster.module.VisibilityPolicySync
|
||||
import se.ajpanton.notificationsmaster.settings.AppFilterSettingsStore
|
||||
import se.ajpanton.notificationsmaster.settings.NotificationListenerComponentController
|
||||
import se.ajpanton.notificationsmaster.settings.PerAppEventSettingsStore
|
||||
@@ -11,6 +12,7 @@ class NotificationLogApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
synchronizeListenerComponent()
|
||||
VisibilityPolicySync.publishStored(this)
|
||||
Thread(::removeStaleAppStorage, "notification-log-storage-cleanup").start()
|
||||
}
|
||||
|
||||
|
||||
@@ -46,10 +46,11 @@ internal object AlertPolicySync {
|
||||
private const val TAG = "NotificationsMaster"
|
||||
}
|
||||
|
||||
/** Re-publishes active alert policy after boot, without starting a service. */
|
||||
/** Re-publishes process-local policies after boot, without starting a service. */
|
||||
class AlertPolicyBootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
|
||||
AlertPolicySync.publish(context, se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore(context).load())
|
||||
VisibilityPolicySync.publishStored(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ import io.github.libxposed.api.XposedModuleInterface
|
||||
* in a compatible framework and grants it a system-server scope.
|
||||
*/
|
||||
class NotificationsMasterModule : XposedModule() {
|
||||
override fun onPackageReady(param: XposedModuleInterface.PackageReadyParam) {
|
||||
if (param.packageName in VisibilityPolicySync.TARGET_PACKAGES) {
|
||||
ProcessVisibilityPolicyCache.installWhenReady()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSystemServerStarting(param: XposedModuleInterface.SystemServerStartingParam) {
|
||||
SystemAlertPolicyCache.installWhenReady()
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package se.ajpanton.notificationsmaster.module
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import se.ajpanton.notificationsmaster.visibility.CompiledVisibilityPolicy
|
||||
import se.ajpanton.notificationsmaster.visibility.NotificationSurface
|
||||
import se.ajpanton.notificationsmaster.visibility.VisibilityNotification
|
||||
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicyJson
|
||||
|
||||
/** Process-local policy used by SystemUI/AOD hooks without entering the app process. */
|
||||
internal object ProcessVisibilityPolicyCache {
|
||||
@Volatile
|
||||
private var policy = CompiledVisibilityPolicy.ALLOW_ALL
|
||||
private var installed = false
|
||||
private var attempts = 0
|
||||
private var onChanged: () -> Unit = {}
|
||||
|
||||
fun installWhenReady(onChanged: () -> Unit = {}) {
|
||||
this.onChanged = onChanged
|
||||
Handler(Looper.getMainLooper()).post(::tryInstall)
|
||||
}
|
||||
|
||||
fun isBlocked(notification: VisibilityNotification, surface: NotificationSurface) =
|
||||
policy.isBlocked(notification, surface)
|
||||
|
||||
val unlockedIconLimit get() = policy.unlockedIconLimit
|
||||
|
||||
private fun tryInstall() {
|
||||
if (installed) return
|
||||
val context = currentApplicationContext()
|
||||
if (context == null || context.packageName !in VisibilityPolicySync.TARGET_PACKAGES) {
|
||||
retryOrGiveUp()
|
||||
return
|
||||
}
|
||||
runCatching {
|
||||
context.registerReceiver(
|
||||
PolicyReceiver(),
|
||||
IntentFilter(VisibilityPolicySync.UPDATE_ACTION),
|
||||
VisibilityPolicySync.PERMISSION,
|
||||
null,
|
||||
Context.RECEIVER_EXPORTED,
|
||||
)
|
||||
context.sendBroadcast(
|
||||
Intent(VisibilityPolicySync.REQUEST_ACTION).setComponent(
|
||||
ComponentName(VisibilityPolicySync.APP_PACKAGE, VisibilityPolicySync.REQUEST_RECEIVER),
|
||||
).putExtra(
|
||||
VisibilityPolicySync.EXTRA_REQUESTER,
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
Intent(REQUESTER_IDENTITY_ACTION).setPackage(context.packageName),
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.onSuccess {
|
||||
installed = true
|
||||
Log.i(TAG, "Installed visibility-policy cache in ${context.packageName}; requested replay")
|
||||
}.onFailure {
|
||||
retryOrGiveUp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentApplicationContext(): Context? = runCatching {
|
||||
Class.forName("android.app.ActivityThread")
|
||||
.getMethod("currentApplication")
|
||||
.invoke(null) as? Context
|
||||
}.getOrNull()
|
||||
|
||||
private fun retryOrGiveUp() {
|
||||
if (++attempts < MAX_ATTEMPTS) {
|
||||
Handler(Looper.getMainLooper()).postDelayed(::tryInstall, RETRY_DELAY_MILLIS)
|
||||
} else {
|
||||
Log.w(TAG, "Visibility-policy cache unavailable; allowing all notifications")
|
||||
}
|
||||
}
|
||||
|
||||
private class PolicyReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val payload = intent.getByteArrayExtra(VisibilityPolicySync.EXTRA_POLICY)
|
||||
val next = if (payload == null || payload.size > VisibilityPolicySync.MAX_POLICY_BYTES) null else {
|
||||
runCatching { CompiledVisibilityPolicy(VisibilityPolicyJson.decode(payload)) }
|
||||
.onFailure { Log.w(TAG, "Rejected malformed visibility policy; allowing all", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
if (next != null && next.generation < policy.generation) return
|
||||
policy = next ?: CompiledVisibilityPolicy.ALLOW_ALL
|
||||
Log.i(TAG, "Visibility policy cache generation ${policy.generation}")
|
||||
onChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private const val TAG = "NotificationsMaster"
|
||||
private const val REQUESTER_IDENTITY_ACTION = "se.ajpanton.notificationsmaster.VISIBILITY_REQUESTER_IDENTITY"
|
||||
private const val MAX_ATTEMPTS = 10
|
||||
private const val RETRY_DELAY_MILLIS = 1_000L
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package se.ajpanton.notificationsmaster.module
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicy
|
||||
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicyJson
|
||||
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicyStore
|
||||
|
||||
internal object VisibilityPolicySync {
|
||||
const val UPDATE_ACTION = "se.ajpanton.notificationsmaster.UPDATE_VISIBILITY_POLICY"
|
||||
const val REQUEST_ACTION = "se.ajpanton.notificationsmaster.REQUEST_VISIBILITY_POLICY"
|
||||
const val EXTRA_POLICY = "policy"
|
||||
const val EXTRA_REQUESTER = "requester"
|
||||
const val PERMISSION = AlertPolicySync.PERMISSION
|
||||
const val APP_PACKAGE = "se.ajpanton.notificationsmaster"
|
||||
const val REQUEST_RECEIVER = "$APP_PACKAGE.module.VisibilityPolicyRequestReceiver"
|
||||
const val MAX_POLICY_BYTES = 128 * 1024
|
||||
val TARGET_PACKAGES = setOf("com.android.systemui", "com.samsung.android.app.aodservice")
|
||||
|
||||
fun publish(context: Context, policy: VisibilityPolicy) {
|
||||
val payload = VisibilityPolicyJson.encode(policy)
|
||||
if (payload.size > MAX_POLICY_BYTES) {
|
||||
Log.w(TAG, "Visibility policy exceeds the process-cache limit; allowing all notifications")
|
||||
return
|
||||
}
|
||||
TARGET_PACKAGES.forEach { target ->
|
||||
context.sendBroadcast(Intent(UPDATE_ACTION).setPackage(target).putExtra(EXTRA_POLICY, payload))
|
||||
}
|
||||
Log.i(TAG, "Published visibility policy generation ${policy.generation}")
|
||||
}
|
||||
|
||||
fun publishStored(context: Context) {
|
||||
val policy = runCatching { VisibilityPolicyStore(context).load() }
|
||||
.onFailure { Log.e(TAG, "Stored visibility policy is unreadable; allowing all notifications", it) }
|
||||
.getOrElse { VisibilityPolicy(enabled = false) }
|
||||
publish(context, policy)
|
||||
}
|
||||
|
||||
private const val TAG = "NotificationsMaster"
|
||||
}
|
||||
|
||||
class VisibilityPolicyRequestReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != VisibilityPolicySync.REQUEST_ACTION) return
|
||||
val requester = intent.getParcelableExtra(VisibilityPolicySync.EXTRA_REQUESTER, PendingIntent::class.java)
|
||||
val creatorPackages = requester?.let {
|
||||
context.packageManager.getPackagesForUid(it.creatorUid).orEmpty().toSet()
|
||||
}.orEmpty()
|
||||
val creatorPackage = requester?.creatorPackage
|
||||
if (creatorPackage !in VisibilityPolicySync.TARGET_PACKAGES ||
|
||||
creatorPackage !in creatorPackages
|
||||
) {
|
||||
Log.w(TAG, "Rejected visibility-policy request without a trusted identity")
|
||||
return
|
||||
}
|
||||
VisibilityPolicySync.publishStored(context)
|
||||
}
|
||||
|
||||
private companion object { const val TAG = "NotificationsMaster" }
|
||||
}
|
||||
+10
-2
@@ -8,6 +8,7 @@ import android.util.Log
|
||||
import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore
|
||||
import se.ajpanton.notificationsmaster.capture.NotificationCaptureService
|
||||
import se.ajpanton.notificationsmaster.module.AlertPolicyBootReceiver
|
||||
import se.ajpanton.notificationsmaster.visibility.VisibilityPolicyStore
|
||||
|
||||
/**
|
||||
* Notification listeners are re-bound by Android after boot when their component
|
||||
@@ -23,6 +24,7 @@ internal object NotificationListenerComponentController {
|
||||
LoggingType.entries.associateWith(rules::ruleFor),
|
||||
PerAppEventSettingsStore(applicationContext).hasEnabledEventOverride(),
|
||||
AlertConfigurationStore(applicationContext).load().needsListener,
|
||||
VisibilityPolicyStore(applicationContext).load().hasRules,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,11 +33,17 @@ internal object NotificationListenerComponentController {
|
||||
rules: Map<LoggingType, LoggingRule>,
|
||||
hasEnabledEventOverride: Boolean = false,
|
||||
hasEnabledAlertRule: Boolean = false,
|
||||
hasVisibilityRules: Boolean = false,
|
||||
) {
|
||||
val applicationContext = context.applicationContext
|
||||
val component = ComponentName(applicationContext, NotificationCaptureService::class.java)
|
||||
val packageManager = applicationContext.packageManager
|
||||
val desired = if (NotificationListenerPolicy.shouldRun(rules, hasEnabledEventOverride, hasEnabledAlertRule)) {
|
||||
val desired = if (NotificationListenerPolicy.shouldRun(
|
||||
rules,
|
||||
hasEnabledEventOverride,
|
||||
hasEnabledAlertRule,
|
||||
hasVisibilityRules,
|
||||
)) {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
} else {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
||||
@@ -44,7 +52,7 @@ internal object NotificationListenerComponentController {
|
||||
packageManager.setComponentEnabledSetting(component, desired, PackageManager.DONT_KILL_APP)
|
||||
}
|
||||
val bootReceiver = ComponentName(applicationContext, AlertPolicyBootReceiver::class.java)
|
||||
val bootReceiverState = if (hasEnabledAlertRule) {
|
||||
val bootReceiverState = if (hasEnabledAlertRule || hasVisibilityRules) {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
} else {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
||||
|
||||
+3
-1
@@ -6,7 +6,9 @@ object NotificationListenerPolicy {
|
||||
rules: Map<LoggingType, LoggingRule>,
|
||||
hasEnabledEventOverride: Boolean = false,
|
||||
hasEnabledAlertRule: Boolean = false,
|
||||
hasVisibilityRules: Boolean = false,
|
||||
): Boolean = LoggingType.eventTypes.any { rules.getValue(it).enabled } ||
|
||||
hasEnabledEventOverride ||
|
||||
hasEnabledAlertRule
|
||||
hasEnabledAlertRule ||
|
||||
hasVisibilityRules
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package se.ajpanton.notificationsmaster.visibility
|
||||
|
||||
import com.google.re2j.Pattern
|
||||
|
||||
enum class NotificationSurface { AOD, LOCKSCREEN_COLLAPSED, UNLOCKED_STATUSBAR }
|
||||
|
||||
data class VisibilityExceptionRule(
|
||||
val pattern: String,
|
||||
val blockedSurfaces: Set<NotificationSurface>,
|
||||
) {
|
||||
init { require(pattern.isNotBlank()) }
|
||||
|
||||
fun hasValidPattern() = runCatching { Pattern.compile(pattern) }.isSuccess
|
||||
}
|
||||
|
||||
data class AppVisibilityPolicy(
|
||||
val packageName: String,
|
||||
val blockedSurfaces: Set<NotificationSurface> = emptySet(),
|
||||
val exceptions: List<VisibilityExceptionRule> = emptyList(),
|
||||
) {
|
||||
init { require(packageName.isNotBlank()) }
|
||||
}
|
||||
|
||||
data class VisibilityPolicy(
|
||||
val generation: Long = 0,
|
||||
val enabled: Boolean = true,
|
||||
val apps: List<AppVisibilityPolicy> = emptyList(),
|
||||
val unlockedIconLimit: Int? = null,
|
||||
) {
|
||||
init {
|
||||
require(generation >= 0)
|
||||
require(apps.map { it.packageName }.distinct().size == apps.size)
|
||||
require(unlockedIconLimit == null || unlockedIconLimit in 1..1000)
|
||||
}
|
||||
|
||||
val hasRules get() = enabled && (unlockedIconLimit != null ||
|
||||
apps.any { it.blockedSurfaces.isNotEmpty() || it.exceptions.isNotEmpty() })
|
||||
}
|
||||
|
||||
data class VisibilityNotification(
|
||||
val packageName: String,
|
||||
val key: String,
|
||||
val title: String? = null,
|
||||
val text: String? = null,
|
||||
val bigText: String? = null,
|
||||
val subtext: String? = null,
|
||||
val messages: List<String> = emptyList(),
|
||||
val channelId: String? = null,
|
||||
)
|
||||
|
||||
class CompiledVisibilityPolicy(policy: VisibilityPolicy) {
|
||||
val generation = policy.generation
|
||||
val unlockedIconLimit = policy.unlockedIconLimit
|
||||
private val enabled = policy.enabled
|
||||
private val apps = policy.apps.associate { app ->
|
||||
app.packageName to CompiledAppPolicy(
|
||||
app.blockedSurfaces,
|
||||
app.exceptions.map { CompiledRule(Pattern.compile(it.pattern), it.blockedSurfaces) },
|
||||
)
|
||||
}
|
||||
|
||||
fun isBlocked(notification: VisibilityNotification, surface: NotificationSurface): Boolean {
|
||||
if (!enabled) return false
|
||||
val policy = apps[notification.packageName] ?: return false
|
||||
val matchText = notification.matchText()
|
||||
val exception = policy.exceptions.firstOrNull { it.pattern.matcher(matchText).find() }
|
||||
return surface in (exception?.blockedSurfaces ?: policy.blockedSurfaces)
|
||||
}
|
||||
|
||||
private fun VisibilityNotification.matchText() = buildList {
|
||||
add(packageName)
|
||||
add(key)
|
||||
listOf(title, text, bigText, subtext, channelId).filterNotNullTo(this)
|
||||
addAll(messages)
|
||||
}.joinToString("\n")
|
||||
|
||||
private data class CompiledAppPolicy(
|
||||
val blockedSurfaces: Set<NotificationSurface>,
|
||||
val exceptions: List<CompiledRule>,
|
||||
)
|
||||
|
||||
private data class CompiledRule(
|
||||
val pattern: Pattern,
|
||||
val blockedSurfaces: Set<NotificationSurface>,
|
||||
)
|
||||
|
||||
companion object {
|
||||
val ALLOW_ALL = CompiledVisibilityPolicy(VisibilityPolicy(enabled = false))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package se.ajpanton.notificationsmaster.visibility
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
internal object VisibilityPolicyJson {
|
||||
const val SCHEMA_VERSION = 1
|
||||
|
||||
fun encode(policy: VisibilityPolicy): ByteArray = JSONObject()
|
||||
.put("version", SCHEMA_VERSION)
|
||||
.put("generation", policy.generation)
|
||||
.put("enabled", policy.enabled)
|
||||
.put("unlockedIconLimit", policy.unlockedIconLimit ?: JSONObject.NULL)
|
||||
.put("apps", JSONArray().apply { policy.apps.forEach { put(it.toJson()) } })
|
||||
.toString().encodeToByteArray()
|
||||
|
||||
fun decode(bytes: ByteArray): VisibilityPolicy {
|
||||
val root = JSONObject(bytes.decodeToString())
|
||||
require(root.getInt("version") == SCHEMA_VERSION)
|
||||
return VisibilityPolicy(
|
||||
generation = root.getLong("generation"),
|
||||
enabled = root.getBoolean("enabled"),
|
||||
apps = root.getJSONArray("apps").map { (it as JSONObject).toAppPolicy() },
|
||||
unlockedIconLimit = if (root.isNull("unlockedIconLimit")) null else root.getInt("unlockedIconLimit"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun AppVisibilityPolicy.toJson() = JSONObject()
|
||||
.put("packageName", packageName)
|
||||
.put("blockedSurfaces", JSONArray(blockedSurfaces.map { it.name }))
|
||||
.put("exceptions", JSONArray().apply { exceptions.forEach { put(it.toJson()) } })
|
||||
|
||||
private fun VisibilityExceptionRule.toJson() = JSONObject()
|
||||
.put("pattern", pattern)
|
||||
.put("blockedSurfaces", JSONArray(blockedSurfaces.map { it.name }))
|
||||
|
||||
private fun JSONObject.toAppPolicy() = AppVisibilityPolicy(
|
||||
packageName = getString("packageName"),
|
||||
blockedSurfaces = getJSONArray("blockedSurfaces").surfaces(),
|
||||
exceptions = getJSONArray("exceptions").map { (it as JSONObject).toException() },
|
||||
)
|
||||
|
||||
private fun JSONObject.toException() = VisibilityExceptionRule(
|
||||
pattern = getString("pattern"),
|
||||
blockedSurfaces = getJSONArray("blockedSurfaces").surfaces(),
|
||||
)
|
||||
|
||||
private fun JSONArray.surfaces() = map { NotificationSurface.valueOf(it as String) }.toSet()
|
||||
private fun <T> JSONArray.map(transform: (Any) -> T) = List(length()) { transform(get(it)) }
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package se.ajpanton.notificationsmaster.visibility
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AtomicFile
|
||||
import se.ajpanton.notificationsmaster.data.AesGcmCipher
|
||||
import se.ajpanton.notificationsmaster.data.EncryptedPayload
|
||||
import se.ajpanton.notificationsmaster.data.LogEncryptionKeyProvider
|
||||
import se.ajpanton.notificationsmaster.module.VisibilityPolicySync
|
||||
import se.ajpanton.notificationsmaster.settings.NotificationListenerComponentController
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class VisibilityPolicyStore(context: Context) {
|
||||
private val context = context.applicationContext
|
||||
private val file = AtomicFile(File(this.context.filesDir, FILE_NAME))
|
||||
private val cipher = AesGcmCipher(LogEncryptionKeyProvider().getOrCreate())
|
||||
|
||||
@Synchronized
|
||||
fun load(): VisibilityPolicy = try {
|
||||
DataInputStream(BufferedInputStream(file.openRead())).use { input ->
|
||||
val iv = ByteArray(input.readInt().also { require(it in 1..32) })
|
||||
input.readFully(iv)
|
||||
val encrypted = ByteArray(input.readInt().also { require(it in 1..MAX_CIPHER_TEXT_BYTES) })
|
||||
input.readFully(encrypted)
|
||||
VisibilityPolicyJson.decode(cipher.decrypt(EncryptedPayload(iv, encrypted)))
|
||||
}
|
||||
} catch (_: FileNotFoundException) {
|
||||
VisibilityPolicy()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun save(policy: VisibilityPolicy): VisibilityPolicy {
|
||||
require(policy.apps.flatMap { it.exceptions }.all { it.hasValidPattern() })
|
||||
val stored = policy.copy(generation = load().generation + 1)
|
||||
val encoded = VisibilityPolicyJson.encode(stored)
|
||||
require(encoded.size <= VisibilityPolicySync.MAX_POLICY_BYTES) { "Visibility policy is too large." }
|
||||
val payload = cipher.encrypt(encoded)
|
||||
val output = file.startWrite()
|
||||
try {
|
||||
DataOutputStream(BufferedOutputStream(output)).apply {
|
||||
writeInt(payload.initializationVector.size)
|
||||
write(payload.initializationVector)
|
||||
writeInt(payload.cipherText.size)
|
||||
write(payload.cipherText)
|
||||
flush()
|
||||
}
|
||||
file.finishWrite(output)
|
||||
} catch (error: Exception) {
|
||||
file.failWrite(output)
|
||||
throw error
|
||||
}
|
||||
NotificationListenerComponentController.synchronize(context)
|
||||
VisibilityPolicySync.publish(context, stored)
|
||||
return stored
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FILE_NAME = "visibility-policy.bin"
|
||||
const val MAX_CIPHER_TEXT_BYTES = 1024 * 1024
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
android
|
||||
system
|
||||
com.android.systemui
|
||||
com.samsung.android.app.aodservice
|
||||
|
||||
Reference in New Issue
Block a user