Avoid inactive listener work and eager image encoding
This commit is contained in:
@@ -25,7 +25,7 @@ class NotificationLogApplication : Application() {
|
||||
val rules = LoggingRuleStore(this)
|
||||
NotificationListenerComponentController.update(
|
||||
this,
|
||||
LoggingType.entries.map(rules::ruleFor),
|
||||
LoggingType.entries.associateWith(rules::ruleFor),
|
||||
PerAppEventSettingsStore(this).hasEnabledEventOverride(),
|
||||
)
|
||||
}
|
||||
|
||||
+32
-12
@@ -44,22 +44,41 @@ class NotificationCaptureService : NotificationListenerService() {
|
||||
override fun onListenerConnected() {
|
||||
super.onListenerConnected()
|
||||
getActiveNotifications()?.forEach { sbn ->
|
||||
val snapshot = NotificationContents.snapshot(sbn, this)
|
||||
val snapshot = NotificationContents.snapshot(sbn)
|
||||
SeenApps.markSeen(snapshot.packageName)
|
||||
activeNotifications[snapshot.key] = snapshot
|
||||
record(snapshot, NotificationAction.ALREADY_ACTIVE, LoggingType.APPEARING, includeContents = true)
|
||||
record(
|
||||
snapshot,
|
||||
NotificationAction.ALREADY_ACTIVE,
|
||||
LoggingType.APPEARING,
|
||||
includeContents = true,
|
||||
imageBytes = { NotificationContents.extractImageBytes(sbn.notification, this) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||
val snapshot = NotificationContents.snapshot(sbn, this)
|
||||
val snapshot = NotificationContents.snapshot(sbn)
|
||||
SeenApps.markSeen(snapshot.packageName)
|
||||
val previous = activeNotifications.put(snapshot.key, snapshot)
|
||||
when {
|
||||
previous == null -> record(snapshot, NotificationAction.APPEARED, LoggingType.APPEARING, includeContents = true)
|
||||
previous == null -> record(
|
||||
snapshot,
|
||||
NotificationAction.APPEARED,
|
||||
LoggingType.APPEARING,
|
||||
includeContents = true,
|
||||
imageBytes = { NotificationContents.extractImageBytes(sbn.notification, this) },
|
||||
)
|
||||
NotificationChangeClassifier.isMeaningfulEdit(previous, snapshot) &&
|
||||
!NotificationChangeClassifier.shouldIgnoreEdit(previous, snapshot, ruleStore.ruleFor(LoggingType.EDITS).ignoreRoutineUpdates) ->
|
||||
record(snapshot, NotificationAction.EDITED, LoggingType.EDITS, includeContents = true, previousSnapshot = previous)
|
||||
record(
|
||||
snapshot,
|
||||
NotificationAction.EDITED,
|
||||
LoggingType.EDITS,
|
||||
includeContents = true,
|
||||
previousSnapshot = previous,
|
||||
imageBytes = { NotificationContents.extractImageBytes(sbn.notification, this) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +87,7 @@ class NotificationCaptureService : NotificationListenerService() {
|
||||
rankingMap: RankingMap,
|
||||
reason: Int,
|
||||
) {
|
||||
val snapshot = activeNotifications.remove(sbn.key) ?: NotificationContents.snapshot(sbn, this)
|
||||
val snapshot = activeNotifications.remove(sbn.key) ?: NotificationContents.snapshot(sbn)
|
||||
SeenApps.markSeen(snapshot.packageName)
|
||||
record(snapshot, actionForRemoval(reason), LoggingType.DISAPPEARING, includeContents = false)
|
||||
}
|
||||
@@ -84,11 +103,13 @@ class NotificationCaptureService : NotificationListenerService() {
|
||||
loggingType: LoggingType,
|
||||
includeContents: Boolean,
|
||||
previousSnapshot: NotificationSnapshot? = null,
|
||||
imageBytes: (() -> ByteArray?)? = null,
|
||||
) {
|
||||
if (!GroupSummaryPolicy.shouldLog(snapshot, action, captureSettings.logGroupSummaries)) return
|
||||
if (!allows(loggingType, snapshot.packageName)) return
|
||||
val appName = appName(snapshot.packageName)
|
||||
val retainImage = includeContents && snapshot.imageBytes != null && allows(LoggingType.IMAGE_CONTENT, snapshot.packageName)
|
||||
val retainImage = includeContents && snapshot.hasImage && allows(LoggingType.IMAGE_CONTENT, snapshot.packageName)
|
||||
val retainedImageBytes = if (retainImage) imageBytes?.invoke() else null
|
||||
val entry = NotificationLogEntry(
|
||||
recordedAtEpochMillis = System.currentTimeMillis(),
|
||||
eventTimeZoneId = java.util.TimeZone.getDefault().id,
|
||||
@@ -97,13 +118,13 @@ class NotificationCaptureService : NotificationListenerService() {
|
||||
action = action,
|
||||
contents = if (includeContents) visibleContents(snapshot) else null,
|
||||
previousContents = previousSnapshot?.let(::visibleContents),
|
||||
imageId = if (retainImage) java.util.UUID.randomUUID().toString() else null,
|
||||
imageId = if (retainedImageBytes != null) java.util.UUID.randomUUID().toString() else null,
|
||||
)
|
||||
writeExecutor.execute {
|
||||
try {
|
||||
if (retainImage) {
|
||||
if (retainedImageBytes != null) {
|
||||
try {
|
||||
imageStore.save(entry.imageId!!, snapshot.imageBytes!!)
|
||||
imageStore.save(entry.imageId!!, retainedImageBytes)
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "Could not retain notification image; keeping the text event", error)
|
||||
logStore.append(entry.copy(imageId = null))
|
||||
@@ -131,7 +152,7 @@ class NotificationCaptureService : NotificationListenerService() {
|
||||
|
||||
private fun allows(type: LoggingType, packageName: String): Boolean {
|
||||
val globalEnabled = ruleStore.ruleFor(type).enabled
|
||||
val eventEnabled = if (type in EVENT_TYPES) {
|
||||
val eventEnabled = if (type in LoggingType.eventTypes) {
|
||||
perAppEventSettings.isEnabled(packageName, type, globalEnabled)
|
||||
} else {
|
||||
globalEnabled
|
||||
@@ -177,6 +198,5 @@ class NotificationCaptureService : NotificationListenerService() {
|
||||
private companion object {
|
||||
const val TAG = "NotificationCapture"
|
||||
const val MAX_CONTENT_CHARACTERS = 16_000
|
||||
val EVENT_TYPES = setOf(LoggingType.APPEARING, LoggingType.DISAPPEARING, LoggingType.EDITS)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ data class NotificationSnapshot(
|
||||
val hasImage: Boolean,
|
||||
val isGroupSummary: Boolean,
|
||||
val isRoutine: Boolean,
|
||||
val imageBytes: ByteArray?,
|
||||
)
|
||||
|
||||
object NotificationContents {
|
||||
@@ -63,11 +62,9 @@ object NotificationContents {
|
||||
return parts.takeIf { it.isNotEmpty() }?.joinToString("\n")
|
||||
}
|
||||
|
||||
fun snapshot(sbn: StatusBarNotification, context: Context): NotificationSnapshot {
|
||||
fun snapshot(sbn: StatusBarNotification): NotificationSnapshot {
|
||||
val notification = sbn.notification
|
||||
val extras = notification.extras
|
||||
val picture = extras?.getParcelable(Notification.EXTRA_PICTURE, Bitmap::class.java)
|
||||
val pictureIcon = extras?.getParcelable(Notification.EXTRA_PICTURE_ICON, Icon::class.java)
|
||||
return NotificationSnapshot(
|
||||
key = sbn.key,
|
||||
packageName = sbn.packageName,
|
||||
@@ -78,10 +75,17 @@ object NotificationContents {
|
||||
isGroupSummary = notification.flags and Notification.FLAG_GROUP_SUMMARY != 0,
|
||||
isRoutine = extras?.getBoolean(Notification.EXTRA_SHOW_CHRONOMETER, false) == true ||
|
||||
extras?.containsKey(Notification.EXTRA_PROGRESS) == true,
|
||||
imageBytes = picture?.toPng() ?: pictureIcon?.let { icon -> icon.loadDrawable(context)?.toBitmap()?.toPng() },
|
||||
)
|
||||
}
|
||||
|
||||
/** Called only after the event has passed all capture filters. */
|
||||
fun extractImageBytes(notification: Notification, context: Context): ByteArray? {
|
||||
val extras = notification.extras ?: return null
|
||||
val picture = extras.getParcelable(Notification.EXTRA_PICTURE, Bitmap::class.java)
|
||||
val pictureIcon = extras.getParcelable(Notification.EXTRA_PICTURE_ICON, Icon::class.java)
|
||||
return picture?.toPng() ?: pictureIcon?.let { icon -> icon.loadDrawable(context)?.toBitmap()?.toPng() }
|
||||
}
|
||||
|
||||
private fun Drawable.toBitmap(): Bitmap? = when (this) {
|
||||
is BitmapDrawable -> bitmap
|
||||
else -> runCatching {
|
||||
|
||||
@@ -30,7 +30,7 @@ class LoggingRuleStore(context: Context) {
|
||||
private fun updateListenerComponent() {
|
||||
NotificationListenerComponentController.update(
|
||||
appContext,
|
||||
LoggingType.entries.map(::ruleFor),
|
||||
LoggingType.entries.associateWith(::ruleFor),
|
||||
PerAppEventSettingsStore(appContext).hasEnabledEventOverride(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@ enum class LoggingType {
|
||||
TEXT_CONTENT,
|
||||
IMAGE_CONTENT,
|
||||
EDITS,
|
||||
;
|
||||
|
||||
companion object {
|
||||
val eventTypes = listOf(APPEARING, DISAPPEARING, EDITS)
|
||||
val contentTypes = listOf(TEXT_CONTENT, IMAGE_CONTENT)
|
||||
}
|
||||
}
|
||||
|
||||
enum class AppRuleMode {
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import se.ajpanton.notificationlog.capture.NotificationCaptureService
|
||||
* is therefore both the no-work battery mode and the no-start-on-boot mode.
|
||||
*/
|
||||
internal object NotificationListenerComponentController {
|
||||
fun update(context: Context, rules: Collection<LoggingRule>, hasEnabledEventOverride: Boolean = false) {
|
||||
fun update(context: Context, rules: Map<LoggingType, LoggingRule>, hasEnabledEventOverride: Boolean = false) {
|
||||
val applicationContext = context.applicationContext
|
||||
val component = ComponentName(applicationContext, NotificationCaptureService::class.java)
|
||||
val packageManager = applicationContext.packageManager
|
||||
|
||||
+4
-2
@@ -2,6 +2,8 @@ package se.ajpanton.notificationlog.settings
|
||||
|
||||
/** The listener is useful only when at least one event type is enabled. */
|
||||
object NotificationListenerPolicy {
|
||||
fun shouldRun(rules: Collection<LoggingRule>, hasEnabledEventOverride: Boolean = false): Boolean =
|
||||
rules.any { it.enabled } || hasEnabledEventOverride
|
||||
fun shouldRun(
|
||||
rules: Map<LoggingType, LoggingRule>,
|
||||
hasEnabledEventOverride: Boolean = false,
|
||||
): Boolean = LoggingType.eventTypes.any { rules.getValue(it).enabled } || hasEnabledEventOverride
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class PerAppEventSettingsStore(context: Context) {
|
||||
}
|
||||
|
||||
private fun updateListenerComponent() {
|
||||
val rules = LoggingType.entries.map { LoggingRuleStore(appContext).ruleFor(it) }
|
||||
val rules = LoggingType.entries.associateWith { LoggingRuleStore(appContext).ruleFor(it) }
|
||||
NotificationListenerComponentController.update(appContext, rules, hasEnabledEventOverride())
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ class GroupSummaryPolicyTest {
|
||||
hasImage = false,
|
||||
isGroupSummary = isGroupSummary,
|
||||
isRoutine = false,
|
||||
imageBytes = null,
|
||||
)
|
||||
|
||||
@Test fun `enabled group-summary logging retains summaries`() {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import org.junit.Test
|
||||
class NotificationChangeClassifierTest {
|
||||
private fun snapshot(text: String, routine: Boolean = false) = NotificationSnapshot(
|
||||
key = "key", packageName = "example.app", textContents = text, hasImage = false,
|
||||
isGroupSummary = false, isRoutine = routine, imageBytes = null,
|
||||
isGroupSummary = false, isRoutine = routine,
|
||||
)
|
||||
|
||||
@Test fun `text change is an edit`() {
|
||||
|
||||
+12
-4
@@ -5,20 +5,28 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class NotificationListenerPolicyTest {
|
||||
@Test fun `listener runs when any logging type is enabled`() {
|
||||
assertTrue(NotificationListenerPolicy.shouldRun(listOf(LoggingRule(enabled = false), LoggingRule(enabled = true))))
|
||||
@Test fun `listener runs when any event logging type is enabled`() {
|
||||
assertTrue(NotificationListenerPolicy.shouldRun(rules(LoggingType.EDITS to true)))
|
||||
}
|
||||
|
||||
@Test fun `listener stays off when every logging type is disabled`() {
|
||||
assertFalse(NotificationListenerPolicy.shouldRun(LoggingType.entries.map { LoggingRule(enabled = false) }))
|
||||
assertFalse(NotificationListenerPolicy.shouldRun(rules()))
|
||||
}
|
||||
|
||||
@Test fun `content settings alone do not keep the listener active`() {
|
||||
assertFalse(NotificationListenerPolicy.shouldRun(rules(LoggingType.TEXT_CONTENT to true, LoggingType.IMAGE_CONTENT to true)))
|
||||
}
|
||||
|
||||
@Test fun `listener runs for an enabled per-app event override`() {
|
||||
assertTrue(
|
||||
NotificationListenerPolicy.shouldRun(
|
||||
LoggingType.entries.map { LoggingRule(enabled = false) },
|
||||
rules(),
|
||||
hasEnabledEventOverride = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun rules(vararg enabled: Pair<LoggingType, Boolean>) = LoggingType.entries.associateWith { type ->
|
||||
LoggingRule(enabled = enabled.firstOrNull { it.first == type }?.second ?: false)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user