rmt-notisaver/CLAUDE.md
jima a0fc459e7a v1.0.0-beta06: Auto-action rules, deletion tracking, test notifications
Auto-action filter rules (EXECUTE_ACTION):
- New filter action that automatically taps notification action buttons
- Finds target button by label (e.g. "Delete", "Archive") on incoming notifications
- Executes PendingIntent if found, logs failure note if button missing
- "Also delete from list" option when action succeeds
- Full UI in filter rule editor with button label field and checkbox

Deletion reason tracking:
- New deletion_reason column (DB v6→v7 migration)
- Tracks why each notification was deleted: USER, USER_BULK, AUTO_ACTION, RETENTION
- Auto-action success/failure details stored with rule name and action label
- Detail screen shows colored status banner for auto-action results
- Deletion reason visible in Info for Nerds section

Test notification broadcast (dev flavor only):
- SEND_NOTIFICATION posts real Android notifications via NotificationManager
- Supports action buttons, reply actions with RemoteInput
- Notification styles: messaging, bigpicture, bigtext, inbox
- Generated test bitmaps for sticker/image testing
- Action confirmation notifications show reply text in timeline
- POST_NOTIFICATIONS permission added to dev manifest only

Detail screen improvements:
- Icons on live action chips (Delete→trash, Archive→box, Like→heart, etc.)
- Reply dialog auto-focuses text field and opens keyboard
- Taller reply text field (3 lines)

Code quality:
- Renamed CLAUDE_TEST tag to CLAUDE_SNI_TEST across all files
- Fixed deprecated Icons.Default.OpenInNew → AutoMirrored in DetailScreen + SwipeableNotificationItem
- Fixed deprecated Icons.Default.Backspace → AutoMirrored in LockScreen
- Fixed deprecated fallbackToDestructiveMigration() → added dropAllTables param
- Wrapped BitmapFactory.decodeFile() in runCatching to prevent crashes on corrupt files
- Removed unused Notifications icon import
- Added translations for all new strings (ES, FR, DE, CA, SV)

Published as internal test on Google Play Store.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 14:20:49 +01:00

19 KiB

SNI — Claude Code Instructions

READ FIRST

Read SNI-ARCHITECTURE.md before doing anything. It contains the full architecture, module structure, database schema, all features, and the test instrumentation protocol.


Project Philosophy

This project follows an iterative build-test-verify methodology. You never write a large chunk of code and hope it works. Instead:

  1. Write a small, focused piece of code
  2. Build it
  3. Test it (install, run, verify via ADB)
  4. Review it (is it clean? follows standards? no duplication?)
  5. Only then move to the next piece

Quality over speed. Every file you create must follow Google's Android coding standards, use proper Hilt injection, have no duplicate logic, and be production-ready.


Development Standards

Mandatory for ALL code

  • Kotlin coding conventions — official Kotlin style guide
  • Jetpack Compose best practices — stable types, skip-friendly composables, no side effects in composition
  • Clean Architecture — domain layer has ZERO Android imports, presentation never touches data layer directly
  • Hilt/Dagger DI — no manual instantiation of repositories, use cases, or ViewModels. Everything injected.
  • No duplicate code — if two screens share logic, extract to a use case or shared composable
  • Repository pattern — interfaces in domain, implementations in data
  • Coroutines + Flow — suspend for one-shot, Flow for streams. viewModelScope in ViewModels. IO dispatcher for DB.
  • Single Activity — Compose Navigation for all screens
  • Material 3 — dynamic color, proper theming, dark mode
  • Edge-to-edge — handle insets properly with WindowInsets
  • Proper error handling — try/catch in data layer, sealed Result types, never crash on data errors
  • No hardcoded strings — all user-visible text in strings.xml
  • Content descriptions — on all icons and interactive elements for accessibility
  • ProGuard-safe — keep annotations where needed, test release builds

Before committing any file, ask yourself:

  • Does this class have a single responsibility?
  • Is there another class that already does something similar?
  • Are all dependencies injected via Hilt?
  • Would a senior Android developer approve this code?
  • Is the domain layer still free of Android imports?
  • Are Compose previews provided for screens?

Build Flavors

Three flavors exist: free, pro, testing.

# Build commands
./gradlew assembleFreeDebug         # Free flavor, debug
./gradlew assembleProDebug          # Pro flavor, debug
./gradlew assembleTestingDebug      # Testing flavor, debug (this is your main one)
./gradlew assembleProRelease        # Pro release (minified)
./gradlew assembleFreeRelease       # Free release (minified + ads)

# Install testing flavor
adb install -r app/build/outputs/apk/testing/debug/app-testing-debug.apk

# Launch testing flavor
adb shell am start -n com.roundingmobile.sni.testing.debug/com.roundingmobile.sni.presentation.MainActivity

# Grant notification listener
adb shell cmd notification allow_listener \
  com.roundingmobile.sni.testing.debug/com.roundingmobile.sni.service.NotificationCaptureService

The testing flavor is your primary development target. It has all pro features plus the test instrumentation layer. Build and test against it. Periodically also build the free and pro flavors to verify they compile and the flavor-specific code (ads, feature gates) works.


ADB Testing Protocol

Reading app state

# Dump all visible UI element positions → logcat
adb shell am broadcast -a com.roundingmobile.sni.test.DUMP_UI
adb logcat -d -s CLAUDE_SNI_TEST:V

# Dump app state → logcat
adb shell am broadcast -a com.roundingmobile.sni.test.DUMP_STATE
adb logcat -d -s CLAUDE_SNI_TEST:V

# Stream test logs live
adb logcat -c && adb logcat -s CLAUDE_SNI_TEST:V

# Check for crashes
adb logcat -d | grep -E "(CLAUDE_SNI_TEST|AndroidRuntime|FATAL)"

Interacting

# Tap (always DUMP_UI first to get coordinates)
adb shell input tap <x> <y>

# Type text (%s = space)
adb shell input text "hello%sworld"

# Keys
adb shell input keyevent KEYCODE_BACK
adb shell input keyevent KEYCODE_HOME
adb shell input keyevent KEYCODE_ENTER
adb shell input keyevent KEYCODE_DEL        # backspace

# Swipe (scroll)
adb shell input swipe 540 1800 540 600 300  # scroll down

# Screenshot
adb shell screencap -p /sdcard/screen.png && adb pull /sdcard/screen.png ./screenshot.png

# UI automator fallback
adb shell uiautomator dump && adb pull /sdcard/window_dump.xml

Injecting test data

# Single notification
adb shell am broadcast -a com.roundingmobile.sni.test.INJECT_NOTIFICATION \
  --es package "com.whatsapp" --es app_name "WhatsApp" \
  --es title "John" --es text "See you later" \
  --es category "message" --ei priority 2

# Batch
adb shell am broadcast -a com.roundingmobile.sni.test.INJECT_BATCH \
  --es json '[{"package":"com.whatsapp","title":"John","text":"Hi"},{"package":"com.google.android.gm","app_name":"Gmail","title":"Anna","text":"Meeting"}]'

# Remove notification
adb shell am broadcast -a com.roundingmobile.sni.test.REMOVE_NOTIFICATION --ei notification_id 123

# Navigate directly
adb shell am broadcast -a com.roundingmobile.sni.test.NAVIGATE --es screen "settings"

# Set preference
adb shell am broadcast -a com.roundingmobile.sni.test.SET_STATE --es key "retention_days" --es value "7"

# Clear DB
adb shell am broadcast -a com.roundingmobile.sni.test.CLEAR_DB

# Trigger workers
adb shell am broadcast -a com.roundingmobile.sni.test.TRIGGER_CLEANUP
adb shell am broadcast -a com.roundingmobile.sni.test.TRIGGER_DIGEST

CRITICAL RULE: Always DUMP_UI before tapping

Never guess coordinates. Always:

  1. adb shell am broadcast -a com.roundingmobile.sni.test.DUMP_UI
  2. adb logcat -d -s CLAUDE_SNI_TEST:V → parse VIEW lines
  3. Calculate center of target element: x = (left+right)/2, y = (top+bottom)/2
  4. adb shell input tap x y

Step-by-Step Implementation Plan

Follow these phases IN ORDER. Each phase builds on the previous one. Do NOT skip ahead. Each step within a phase has a verification checkpoint — complete it before moving on.


PHASE 0: Project Skeleton

Step 0.1 — Gradle setup

Create the project structure with:

  • Project-level build.gradle.kts with Kotlin, Hilt, Firebase plugins
  • gradle/libs.versions.toml version catalog
  • App-level build.gradle.kts with all 3 flavors configured
  • settings.gradle.kts

Verify: ./gradlew assembleTestingDebug compiles (empty app is fine).

Step 0.2 — Application class + Hilt

  • App.kt with @HiltAndroidApp
  • MainActivity.kt with @AndroidEntryPoint and empty Compose content
  • AndroidManifest.xml with application, activity, theme

Verify: Build, install on emulator, app launches showing blank screen. No crash in logcat.

Step 0.3 — Theme

  • Material 3 theme with dynamic color
  • Dark/light mode support
  • Color.kt, Type.kt, Theme.kt

Verify: Build, install, verify dark mode toggle works via adb shell cmd uimode night yes / adb shell cmd uimode night no. Screenshot both modes.

Step 0.4 — Navigation skeleton

  • Route.kt sealed interface with all routes
  • NavGraph.kt with empty placeholder screens
  • Bottom navigation bar: Timeline, Stats, Settings
  • Wire up in MainActivity

Verify: Build, install, tap each bottom nav item. DUMP_UI to confirm navigation works. Screenshot each tab.

Step 0.5 — Test instrumentation layer

  • TestInstrumentation.kt interface in main
  • NoOpTestInstrumentation (used by free/pro flavors)
  • LiveTestInstrumentation with TestLogEmitter, UIPositionTracker (testing flavor)
  • TestBroadcastReceiver with DUMP_UI, DUMP_STATE
  • testTrack() Modifier extension
  • Hilt modules for each flavor

Verify:

  1. Build and install testing flavor
  2. adb shell am broadcast -a com.roundingmobile.sni.test.DUMP_UI
  3. adb logcat -d -s CLAUDE_SNI_TEST:V → should see VIEW lines for bottom nav items
  4. Build free flavor → verify it compiles (no-op instrumentation)
  5. Build pro flavor → verify it compiles

Step 0.6 — Code review checkpoint

Review ALL code written so far:

  • Is Hilt injection correct everywhere?
  • No Android imports in domain layer?
  • No duplicate code?
  • Naming conventions followed?
  • ProGuard rules in place? Fix any issues before proceeding.

PHASE 1: Database + Capture Service

Step 1.1 — Room database

  • AppDatabase.kt with all entities and DAOs
  • NotificationEntity.kt, NotificationDao.kt (CRUD + Flow queries + FTS)
  • HiddenAppEntity.kt, HiddenAppDao.kt
  • Converters.kt for JSON fields
  • Database module for Hilt

Verify:

  1. Build
  2. Write a quick unit test or use the testing flavor to insert/query a row via broadcast
  3. Check logcat for any Room errors

Step 1.2 — Domain models + repository interface

  • CapturedNotification.kt and other domain models
  • NotificationRepository.kt interface in domain
  • NotificationRepositoryImpl.kt in data with mapper
  • NotificationMapper.kt (entity ↔ domain)

Verify: Build compiles. Domain models have no Android imports. Repository interface is clean.

Step 1.3 — NotificationListenerService

  • NotificationCaptureService.kt — capture posted/removed notifications
  • NotificationParser.kt — extract all fields from StatusBarNotification
  • Wire through repository to Room
  • Handle hidden apps (skip capture)

Verify:

  1. Build and install testing flavor
  2. Grant notification listener permission via adb
  3. Send a test notification from another app (or use adb shell am to trigger a system notification)
  4. DUMP_STATE → check db_count increased
  5. Check logcat for EVENT notification_captured

Step 1.4 — FakeNotificationInjector (testing flavor)

  • Implement INJECT_NOTIFICATION and INJECT_BATCH broadcast handlers
  • Insert directly into Room via repository (bypass service)
  • Wire up CLEAR_DB, DUMP_STATE with real db counts

Verify:

  1. CLEAR_DB
  2. Inject 5 notifications via broadcast
  3. DUMP_STATE → db_count=5
  4. CLEAR_DB → DUMP_STATE → db_count=0

Step 1.5 — Code review checkpoint

Review database layer, service, repository, mapper. Check:

  • Proper indices on Room entities?
  • Mapper handles nulls correctly?
  • Service properly handles edge cases (null title, missing extras)?
  • No memory leaks in service?

PHASE 2: Timeline Screen

Step 2.1 — GetTimelineUseCase

  • Use case that returns Flow<List<CapturedNotification>> from repository
  • Supports sort order parameter
  • Filters hidden apps

Verify: Build compiles. Use case has no Android imports.

Step 2.2 — TimelineViewModel

  • Hilt ViewModel
  • Exposes StateFlow of timeline data
  • Sort order state
  • Loading/error/empty states

Verify: Build compiles. ViewModel uses viewModelScope correctly.

Step 2.3 — TimelineScreen composable

  • LazyColumn with NotificationItem composables
  • Time block headers (Today, Yesterday, etc.)
  • Sort toggle in top bar
  • Pull-to-refresh (if needed) or auto-update via Flow
  • Empty state when no notifications
  • testTrack() on every interactive element

Verify:

  1. Build and install
  2. CLEAR_DB → inject 10 notifications
  3. DUMP_UI → verify notif_item_0 through notif_item_9 appear with correct bounds
  4. Screenshot → verify layout looks good
  5. Tap sort toggle → DUMP_UI → verify order changed
  6. CLEAR_DB → verify empty state appears
  7. Screenshot empty state

Step 2.4 — NotificationItem shared composable

  • App icon, app name, title, text preview, timestamp
  • Compact and detailed layout variants
  • Deleted message badge
  • Bookmark indicator
  • Shared across timeline, search results, filter results

Verify: Visual inspection via screenshots in both compact and detailed modes.

Step 2.5 — Code review checkpoint

  • Is LazyColumn using key parameter for stable recomposition?
  • Are composables skip-friendly (stable parameters)?
  • Is the shared NotificationItem truly reusable?
  • No business logic in composables?

PHASE 3: Detail Screen

Step 3.1 — GetNotificationDetailUseCase

Verify: Build compiles. Pure domain.

Step 3.2 — DetailScreen + ViewModel

  • Full notification content display
  • Actions: copy, share, open source app, bookmark toggle
  • Extras section (collapsible)
  • Navigation from timeline item tap

Verify:

  1. Inject notification, tap it on timeline (using DUMP_UI coordinates)
  2. DUMP_UI on detail screen → verify all elements present
  3. Tap copy → verify clipboard (or just verify no crash)
  4. Tap back → verify return to timeline
  5. Screenshot detail screen

Step 4.1 — Room FTS setup

  • Add FTS virtual table for notifications
  • SearchNotificationsUseCase

Verify: Build compiles. Test search query returns correct results via DUMP_STATE.

Step 4.2 — SearchScreen + ViewModel

  • Search bar with debounced input
  • Results list using shared NotificationItem
  • Empty results state
  • Recent searches (optional)

Verify:

  1. Inject 20 notifications, 3 with "Amazon" in text
  2. Navigate to search → type "Amazon"
  3. DUMP_UI → verify 3 results
  4. Screenshot
  5. Clear search → verify all results gone
  6. Type gibberish → verify empty state

PHASE 5: Filter

Step 5.1 — FilterByAppUseCase + FilterScreen

  • Checkbox list of all apps with notification counts
  • Select all / deselect all
  • Persist filter in app_filter_prefs

Verify:

  1. Inject from 4 different apps
  2. Open filter → DUMP_UI → verify 4 apps listed
  3. Deselect one → go to timeline → verify filtered
  4. Go back to filter → reselect → verify restored

PHASE 6: Settings + Security

Step 6.1 — SettingsScreen

  • Retention period selector
  • Hidden apps management
  • App lock toggle
  • Theme mode toggle
  • Developer mode toggle (shows raw data viewer)
  • About section

Verify: Build, install, navigate to settings, toggle options, DUMP_STATE to confirm persisted.

Step 6.2 — AppLockManager + LockScreen

  • PIN setup/verification with EncryptedSharedPreferences
  • BiometricPrompt integration
  • Lock on app background

Verify:

  1. Enable PIN in settings
  2. Press home → relaunch
  3. DUMP_UI → verify lock screen shown
  4. Enter PIN via adb input
  5. DUMP_UI → verify timeline shown

Step 6.3 — Onboarding flow

  • Welcome → permission grant → optional lock setup → done

Verify: Clear app data, relaunch, walk through onboarding via ADB taps. Verify permission prompt appears.


PHASE 7: Deleted Message Detection + Conversation View

Step 7.1 — DetectDeletedMessageUseCase

  • Logic for flagging rapid removals

Verify: Inject notification → wait 1s → remove → check flag in DB.

Step 7.2 — ConversationScreen

  • Group by sender, chat-bubble layout

Verify: Inject 10 messages between 2 senders. Open conversation view. Screenshot.


PHASE 8: Statistics

Step 8.1 — ComputeStatisticsUseCase + StatsScreen

  • Per-app counts, frequency graph, noisy apps ranking

Verify: Inject 50 varied notifications. Navigate to stats. Screenshot. Verify numbers match.


PHASE 9: Export

Step 9.1 — ExportNotificationsUseCase + ExportScreen

  • CSV, JSON, TXT, PDF with date/app filters
  • Save to Downloads

Verify: Export CSV → pull via adb → check content matches DB data.


PHASE 10: Firebase + Crashlytics

Step 10.1 — Firebase setup

  • Add google-services.json per flavor
  • Initialize Crashlytics
  • Add custom keys (flavor, version)
  • Add non-fatal exception logging in catch blocks

Verify: Build all 3 flavors. Force a crash in testing flavor → check Crashlytics dashboard (or just verify no build errors).

Step 10.2 — Analytics events

  • Screen views, feature usage events (aggregate only, no PII)

Verify: Build free + pro. Verify events fire (logcat Firebase debug).


PHASE 11: Ads (free flavor)

Step 11.1 — AdMob integration

  • AdsModule.kt in free flavor
  • AdManager.kt — initialize, load banner
  • AdBanner.kt composable — bottom of TimelineScreen only
  • Wrap in if (BuildConfig.ADS_ENABLED)

Verify:

  1. Build free flavor
  2. Install → verify banner appears on timeline
  3. Navigate to other screens → verify NO ads anywhere else
  4. Build pro flavor → verify no ad code compiled
  5. Build testing flavor → verify no ad code compiled

Step 11.2 — PremiumGate composable

  • Shown when free users tap pro features
  • "Upgrade to Pro" with feature list
  • Link to Play Store listing

Verify: Build free flavor, tap export (pro feature) → verify gate shown instead.


PHASE 12: Polish + Release Prep

Step 12.1 — Widget

  • Home screen widget showing last N notifications

Step 12.2 — Regex filters + keyword alerts (pro)

Step 12.3 — Notification diffing UI (pro)

Step 12.4 — Backup/restore (pro)

Step 12.5 — Full regression test

Run all test scenarios from the architecture doc (Scenarios 1-10) against the testing flavor. Fix anything broken.

Step 12.6 — Build release APKs

./gradlew assembleFreeRelease
./gradlew assembleProRelease

Verify both build with ProGuard enabled. Install release builds on emulator and smoke test.

Step 12.7 — Final code review

Walk through every file. Check:

  • No TODO comments left
  • No hardcoded strings
  • No unused imports
  • No duplicate logic
  • All Hilt modules correct
  • All content descriptions present
  • ProGuard rules complete
  • Crashlytics not logging PII
  • Ads only in free flavor
  • Test instrumentation only in testing flavor

Troubleshooting

App won't install

adb shell pm list packages | grep sni
adb uninstall com.roundingmobile.sni.testing.debug
adb install -r app/build/outputs/apk/testing/debug/app-testing-debug.apk

Notification listener not working

adb shell cmd notification list_listeners
adb shell cmd notification allow_listener \
  com.roundingmobile.sni.testing.debug/com.roundingmobile.sni.service.NotificationCaptureService

No CLAUDE_SNI_TEST output

  • Verify testing flavor installed: adb shell pm list packages | grep testing
  • App running: adb shell pidof com.roundingmobile.sni.testing.debug
  • Try DUMP_STATE and wait before reading logcat

Build failures

./gradlew clean assembleTestingDebug --stacktrace
java -version   # must be 17+

Firebase issues

  • Verify google-services.json exists for the flavor being built
  • Check app/src/<flavor>/google-services.json

Emulator

adb devices                     # list connected
emulator -list-avds             # list available
emulator -avd <name> &          # start

Editor

Use vim for all terminal text editing. Never use nano.


Decision Authority

You have full authority to make UI/UX decisions. When choosing between approaches:

  • Pick the one that's most user-friendly
  • Pick the one that follows Material 3 guidelines
  • Pick the one that requires less code (simpler is better)
  • Pick the one a senior Android developer would choose

If a decision is genuinely ambiguous (e.g., bottom sheet vs full screen for filters), just pick one, implement it, test it, and note your reasoning in a code comment. We can always change it later.

You also decide:

  • Exact color schemes (within Material 3 dynamic color system)
  • Icon choices (Material Icons)
  • Animation details (keep subtle and functional)
  • Spacing and padding values (follow Material 3 specs)
  • Error message wording
  • Empty state illustrations or text
  • Settings organization and grouping