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" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".NotificationLogApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
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) {
|
fun replaceAll(entries: List<NotificationLogEntry>) = synchronized(lock) {
|
||||||
|
val existing = readPayloadOrNull()?.let { NotificationLogEntryJson.decode(cipher.decrypt(it)) } ?: emptyList()
|
||||||
write(entries)
|
write(entries)
|
||||||
|
removeUnreferencedImages(entries, existing)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() = synchronized(lock) {
|
fun clear() = synchronized(lock) {
|
||||||
@@ -49,6 +51,12 @@ class EncryptedNotificationLogStore(context: Context) {
|
|||||||
true
|
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>) {
|
private fun write(entries: List<NotificationLogEntry>) {
|
||||||
val payload = cipher.encrypt(NotificationLogEntryJson.encode(entries))
|
val payload = cipher.encrypt(NotificationLogEntryJson.encode(entries))
|
||||||
val output = file.startWrite()
|
val output = file.startWrite()
|
||||||
@@ -82,6 +90,26 @@ class EncryptedNotificationLogStore(context: Context) {
|
|||||||
if (id.matches(IMAGE_ID_PATTERN)) File(imageDirectory, "$id.bin").delete()
|
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? {
|
private fun readPayloadOrNull(): EncryptedPayload? {
|
||||||
val input = try {
|
val input = try {
|
||||||
file.openRead()
|
file.openRead()
|
||||||
|
|||||||
@@ -11,18 +11,14 @@ data class AppFilterSettings(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class AppFilterSettingsStore(context: Context) {
|
class AppFilterSettingsStore(context: Context) {
|
||||||
private val appContext = context.applicationContext
|
private val preferences = context.applicationContext.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
|
||||||
private val preferences = appContext.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
fun load(): AppFilterSettings {
|
fun load(): AppFilterSettings = AppFilterSettings(
|
||||||
migrateLegacyFiltersIfNeeded()
|
|
||||||
return AppFilterSettings(
|
|
||||||
mode = AppRuleMode.valueOf(preferences.getString(MODE, AppRuleMode.BLACKLIST.name)!!),
|
mode = AppRuleMode.valueOf(preferences.getString(MODE, AppRuleMode.BLACKLIST.name)!!),
|
||||||
selectedPackages = preferences.getStringSet(SELECTED_PACKAGES, emptySet())?.toSet() ?: emptySet(),
|
selectedPackages = preferences.getStringSet(SELECTED_PACKAGES, emptySet())?.toSet() ?: emptySet(),
|
||||||
onlySeenApps = preferences.getBoolean(ONLY_SEEN_APPS, false),
|
onlySeenApps = preferences.getBoolean(ONLY_SEEN_APPS, false),
|
||||||
seenAppsFirst = preferences.getBoolean(SEEN_APPS_FIRST, true),
|
seenAppsFirst = preferences.getBoolean(SEEN_APPS_FIRST, true),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
fun save(settings: AppFilterSettings) = preferences.edit {
|
fun save(settings: AppFilterSettings) = preferences.edit {
|
||||||
putString(MODE, settings.mode.name)
|
putString(MODE, settings.mode.name)
|
||||||
@@ -31,21 +27,16 @@ class AppFilterSettingsStore(context: Context) {
|
|||||||
putBoolean(SEEN_APPS_FIRST, settings.seenAppsFirst)
|
putBoolean(SEEN_APPS_FIRST, settings.seenAppsFirst)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateLegacyFiltersIfNeeded() {
|
fun removeUninstalledPackages(installedPackages: Set<String>) {
|
||||||
if (preferences.getBoolean(MIGRATED, false)) return
|
val settings = load()
|
||||||
val legacy = appContext.getSharedPreferences("logging-rules", Context.MODE_PRIVATE)
|
val retainedPackages = settings.selectedPackages.intersect(installedPackages)
|
||||||
val blacklistedPackages = LoggingType.entries.flatMap { type ->
|
if (retainedPackages != settings.selectedPackages) {
|
||||||
legacy.getStringSet("${type.name.lowercase()}.selected_packages", emptySet()).orEmpty()
|
save(settings.copy(selectedPackages = retainedPackages))
|
||||||
.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) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val FILE_NAME = "app-filter-settings"
|
const val FILE_NAME = "app-filter-settings"
|
||||||
const val MIGRATED = "migrated_from_per_event_filters"
|
|
||||||
const val MODE = "mode"
|
const val MODE = "mode"
|
||||||
const val SELECTED_PACKAGES = "selected_packages"
|
const val SELECTED_PACKAGES = "selected_packages"
|
||||||
const val ONLY_SEEN_APPS = "only_seen_apps"
|
const val ONLY_SEEN_APPS = "only_seen_apps"
|
||||||
|
|||||||
@@ -36,6 +36,16 @@ class PerAppEventSettingsStore(context: Context) {
|
|||||||
.values
|
.values
|
||||||
.any { value -> (value as? Set<*>)?.isNotEmpty() == true }
|
.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>) {
|
private fun saveEvents(packageName: String, enabledEvents: Set<LoggingType>) {
|
||||||
preferences.edit { putStringSet(key(packageName), enabledEvents.mapTo(mutableSetOf()) { it.name }) }
|
preferences.edit { putStringSet(key(packageName), enabledEvents.mapTo(mutableSetOf()) { it.name }) }
|
||||||
updateListenerComponent()
|
updateListenerComponent()
|
||||||
|
|||||||
Reference in New Issue
Block a user