From 48e61e3be7d0c2663a05b6f5fa83c5f60b48fb02 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Sat, 25 Jul 2026 04:09:04 +0000 Subject: [PATCH] Prepare public project for distribution --- .gitignore | 3 ++ README.md | 74 +++++++++++++++++--------------------------- app/build.gradle.kts | 51 ++++++++++++++++++++++++++++-- 3 files changed, 80 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index 599e407..6bacd9b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ /project.md /local/ /local.properties +*.jks +*.keystore +keystore.properties # Gradle and Android build output /.gradle/ diff --git a/README.md b/README.md index df767fd..11ffd6f 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,43 @@ # Notification Log -Notification Log is a private, non-root Android notification listener for API -34–36. It keeps an encrypted local history of notification appearances, edits, -and removal reasons, including a distinction between an app cancelling its own -notification and a user dismissing it. +Notification Log keeps a private history of the notifications that appear on +your Android device. It works as a standard Android notification listener; it +does not need root or an Xposed module. -## Privacy and capture limits +## What it records -The event log and copied image files are encrypted at rest with an Android -Keystore-backed AES-GCM key. Export is deliberately unencrypted only after the -user acknowledges a warning and chooses a destination through Android's Storage -Access Framework. +- Notifications appearing, changing, and disappearing. +- Whether a notification was dismissed by you or cancelled by its app. +- Available text, messaging-style content, and readable notification images. +- Common removal reasons such as timeouts and channel changes. -Android exposes standard notification extras such as title, text, big text, -inbox lines, messaging-style messages, and readable big-picture content. This -app copies a readable bitmap at post time when Android provides one. It cannot -reconstruct opaque custom `RemoteViews`, OTP content Android redacts, or image -URIs/Icons which Android does not allow it to load. Such images remain marked -as `[image]` without causing the text event to be lost. +You can choose which event types to record, set app-specific allow/block lists, +and suppress routine progress or timer updates. The log view can be tailored, +expanded for long entries, and exported as CSV, aligned text, or an HTML ZIP. -The app declares `QUERY_ALL_PACKAGES` solely to implement the settings pages' -“all installed apps” list. A Play-distributed build must meet Google Play's -restricted package-visibility policy and provide the required declaration; a -future distribution variant may need a narrower app-selection flow. +## Privacy -## Background behavior +Logs and retained notification images are encrypted on the device with an +Android Keystore key. Exporting is an explicit action and produces an +unencrypted file at the location you choose. Android may withhold sensitive or +custom notification content; this app records only what Android makes available +to a notification listener. -Android owns the notification-listener connection; this app has no polling, -scheduled job, alarm, or boot receiver. When one or more logging types are -enabled and notification access has been granted, Android binds the listener -again after device boot. When every logging type is disabled, the app disables -that listener component: it has no background service and does not start at -boot. Re-enabling any logging type re-enables and asks Android to rebind it. +When all logging types are disabled, the notification listener is disabled too: +the app has no polling, scheduled job, or background service to run. -## Notification update policy +## Requirements -Each platform notification key is tracked independently. Group summaries are -logged as their own platform notifications; the app does not invent extra group -events. Ongoing notifications are recorded when Android posts, updates, or -removes them, but are never treated as user-dismissible. Identical reposts are -ignored. With the default **Ignore routine updates** setting, standard -progress/chronometer notifications do not create edit rows; non-routine content -changes do. Disabling that setting records the updates as edits. +Notification Log supports Android 14 through Android 16 (API levels 34–36). +After installation, grant **Notification access** when prompted. The optional +app lock uses your device's biometric or credential prompt. -## Development checks +## Development + +Build a debug APK with: ```bash -./gradlew test assembleDebug -./scripts/run-aosp-api36-notification-smoke.sh +./gradlew assembleDebug ``` -The AOSP API 36 smoke scenario starts from a clean emulator snapshot, grants -listener access, drives all helper notification variants, verifies encrypted -event and image persistence, and verifies that the listener records another -event after an emulator reboot. It deliberately leaves user swipe dismissal, -biometric lock behavior, and OEM/foldable rendering to device testing. - -The local Android test lab has been checked on AOSP API 34/35/36, Google APIs -API 35/36, and LineageOS API 35/36 using clean LSPosed snapshots. Root and -Xposed are not used by this application. +This project has been developed with substantial assistance from AI tools. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cd76ee1..0c252a7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.io.File +import java.util.Properties import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -5,6 +7,32 @@ plugins { alias(libs.plugins.kotlin.android) } +val releaseSigningProperties = Properties() +val releaseSigningPropertiesFile = File( + System.getProperty("user.home"), + ".config/android/release-signing.properties", +) +if (releaseSigningPropertiesFile.isFile) { + releaseSigningPropertiesFile.inputStream().use(releaseSigningProperties::load) +} + +val releaseKeystorePath = releaseSigningProperties.getProperty("storeFile") +val releaseKeyAlias = releaseSigningProperties.getProperty("keyAlias") +val releaseStorePassword = providers.gradleProperty("androidReleaseStorePassword").orNull +val releaseKeyPassword = providers.gradleProperty("androidReleaseKeyPassword").orNull +val hasReleaseSigning = !releaseKeystorePath.isNullOrBlank() && + !releaseKeyAlias.isNullOrBlank() && + !releaseStorePassword.isNullOrBlank() && + !releaseKeyPassword.isNullOrBlank() +val releaseBuildRequested = gradle.startParameter.taskNames.any { it.contains("release", ignoreCase = true) } + +if (releaseBuildRequested && !hasReleaseSigning) { + error( + "Release signing is not configured. Configure storeFile and keyAlias in " + + "~/.config/android/release-signing.properties and passwords in ~/.gradle/gradle.properties.", + ) +} + android { namespace = "se.ajpanton.notificationlog" compileSdk = 36 @@ -13,8 +41,8 @@ android { applicationId = "se.ajpanton.notificationlog" minSdk = 34 targetSdk = 36 - versionCode = 1 - versionName = "0.1.0" + versionCode = project.findProperty("notificationLogVersionCode")?.toString()?.toIntOrNull() ?: 1 + versionName = project.findProperty("notificationLogVersionName")?.toString() ?: "0.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -23,6 +51,25 @@ android { viewBinding = true } + signingConfigs { + if (hasReleaseSigning) { + create("release") { + storeFile = file(releaseKeystorePath!!) + storePassword = releaseStorePassword + keyAlias = releaseKeyAlias + keyPassword = releaseKeyPassword + } + } + } + + buildTypes { + getByName("release") { + if (hasReleaseSigning) { + signingConfig = signingConfigs.getByName("release") + } + } + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17