Clean stale per-app storage on startup
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:name=".NotificationLogApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package se.ajpanton.notificationlog
|
||||
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager
|
||||
import se.ajpanton.notificationlog.data.EncryptedNotificationLogStore
|
||||
import se.ajpanton.notificationlog.settings.AppFilterSettingsStore
|
||||
import se.ajpanton.notificationlog.settings.PerAppEventSettingsStore
|
||||
|
||||
class NotificationLogApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Thread(::removeStaleAppStorage, "notification-log-storage-cleanup").start()
|
||||
}
|
||||
|
||||
private fun removeStaleAppStorage() {
|
||||
val installedPackages = packageManager.getInstalledApplications(
|
||||
PackageManager.ApplicationInfoFlags.of(0),
|
||||
).mapTo(mutableSetOf()) { it.packageName }
|
||||
AppFilterSettingsStore(this).removeUninstalledPackages(installedPackages)
|
||||
PerAppEventSettingsStore(this).removeUninstalledPackages(installedPackages)
|
||||
EncryptedNotificationLogStore(this).removeOrphanedImages()
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,9 @@ class EncryptedNotificationLogStore(context: Context) {
|
||||
}
|
||||
|
||||
fun replaceAll(entries: List<NotificationLogEntry>) = synchronized(lock) {
|
||||
val existing = readPayloadOrNull()?.let { NotificationLogEntryJson.decode(cipher.decrypt(it)) } ?: emptyList()
|
||||
write(entries)
|
||||
removeUnreferencedImages(entries, existing)
|
||||
}
|
||||
|
||||
fun clear() = synchronized(lock) {
|
||||
@@ -49,6 +51,12 @@ class EncryptedNotificationLogStore(context: Context) {
|
||||
true
|
||||
}
|
||||
|
||||
/** Removes failed-write remnants and images no remaining log entry references. */
|
||||
fun removeOrphanedImages() = synchronized(lock) {
|
||||
val entries = readPayloadOrNull()?.let { NotificationLogEntryJson.decode(cipher.decrypt(it)) } ?: emptyList()
|
||||
removeUnreferencedImages(entries)
|
||||
}
|
||||
|
||||
private fun write(entries: List<NotificationLogEntry>) {
|
||||
val payload = cipher.encrypt(NotificationLogEntryJson.encode(entries))
|
||||
val output = file.startWrite()
|
||||
@@ -82,6 +90,26 @@ class EncryptedNotificationLogStore(context: Context) {
|
||||
if (id.matches(IMAGE_ID_PATTERN)) File(imageDirectory, "$id.bin").delete()
|
||||
}
|
||||
|
||||
private fun removeUnreferencedImages(
|
||||
entries: List<NotificationLogEntry>,
|
||||
previousEntries: List<NotificationLogEntry> = emptyList(),
|
||||
) {
|
||||
val referenced = entries.mapNotNull { it.imageId }.toSet()
|
||||
previousEntries.mapNotNull { it.imageId }
|
||||
.filterNot(referenced::contains)
|
||||
.forEach(::deleteImage)
|
||||
imageDirectory.listFiles()?.forEach { image ->
|
||||
val imageId = image.name.removeSuffix(".bin")
|
||||
if (image.name.endsWith(".new") ||
|
||||
!image.name.endsWith(".bin") ||
|
||||
!imageId.matches(IMAGE_ID_PATTERN) ||
|
||||
imageId !in referenced
|
||||
) {
|
||||
image.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readPayloadOrNull(): EncryptedPayload? {
|
||||
val input = try {
|
||||
file.openRead()
|
||||
|
||||
@@ -11,18 +11,14 @@ data class AppFilterSettings(
|
||||
)
|
||||
|
||||
class AppFilterSettingsStore(context: Context) {
|
||||
private val appContext = context.applicationContext
|
||||
private val preferences = appContext.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
|
||||
private val preferences = context.applicationContext.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
fun load(): AppFilterSettings {
|
||||
migrateLegacyFiltersIfNeeded()
|
||||
return AppFilterSettings(
|
||||
fun load(): AppFilterSettings = AppFilterSettings(
|
||||
mode = AppRuleMode.valueOf(preferences.getString(MODE, AppRuleMode.BLACKLIST.name)!!),
|
||||
selectedPackages = preferences.getStringSet(SELECTED_PACKAGES, emptySet())?.toSet() ?: emptySet(),
|
||||
onlySeenApps = preferences.getBoolean(ONLY_SEEN_APPS, false),
|
||||
seenAppsFirst = preferences.getBoolean(SEEN_APPS_FIRST, true),
|
||||
)
|
||||
}
|
||||
|
||||
fun save(settings: AppFilterSettings) = preferences.edit {
|
||||
putString(MODE, settings.mode.name)
|
||||
@@ -31,21 +27,16 @@ class AppFilterSettingsStore(context: Context) {
|
||||
putBoolean(SEEN_APPS_FIRST, settings.seenAppsFirst)
|
||||
}
|
||||
|
||||
private fun migrateLegacyFiltersIfNeeded() {
|
||||
if (preferences.getBoolean(MIGRATED, false)) return
|
||||
val legacy = appContext.getSharedPreferences("logging-rules", Context.MODE_PRIVATE)
|
||||
val blacklistedPackages = LoggingType.entries.flatMap { type ->
|
||||
legacy.getStringSet("${type.name.lowercase()}.selected_packages", emptySet()).orEmpty()
|
||||
.takeIf { legacy.getString("${type.name.lowercase()}.app_rule_mode", AppRuleMode.BLACKLIST.name) == AppRuleMode.BLACKLIST.name }
|
||||
.orEmpty()
|
||||
}.toSet()
|
||||
save(AppFilterSettings(selectedPackages = blacklistedPackages))
|
||||
preferences.edit { putBoolean(MIGRATED, true) }
|
||||
fun removeUninstalledPackages(installedPackages: Set<String>) {
|
||||
val settings = load()
|
||||
val retainedPackages = settings.selectedPackages.intersect(installedPackages)
|
||||
if (retainedPackages != settings.selectedPackages) {
|
||||
save(settings.copy(selectedPackages = retainedPackages))
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FILE_NAME = "app-filter-settings"
|
||||
const val MIGRATED = "migrated_from_per_event_filters"
|
||||
const val MODE = "mode"
|
||||
const val SELECTED_PACKAGES = "selected_packages"
|
||||
const val ONLY_SEEN_APPS = "only_seen_apps"
|
||||
|
||||
@@ -36,6 +36,16 @@ class PerAppEventSettingsStore(context: Context) {
|
||||
.values
|
||||
.any { value -> (value as? Set<*>)?.isNotEmpty() == true }
|
||||
|
||||
fun removeUninstalledPackages(installedPackages: Set<String>) {
|
||||
val removed = preferences.all.keys
|
||||
.filter { it.startsWith(KEY_PREFIX) }
|
||||
.filter { it.removePrefix(KEY_PREFIX) !in installedPackages }
|
||||
if (removed.isNotEmpty()) {
|
||||
preferences.edit { removed.forEach(::remove) }
|
||||
updateListenerComponent()
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveEvents(packageName: String, enabledEvents: Set<LoggingType>) {
|
||||
preferences.edit { putStringSet(key(packageName), enabledEvents.mapTo(mutableSetOf()) { it.name }) }
|
||||
updateListenerComponent()
|
||||
|
||||
Reference in New Issue
Block a user