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>
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:
- Write a small, focused piece of code
- Build it
- Test it (install, run, verify via ADB)
- Review it (is it clean? follows standards? no duplication?)
- 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:
adb shell am broadcast -a com.roundingmobile.sni.test.DUMP_UIadb logcat -d -s CLAUDE_SNI_TEST:V→ parse VIEW lines- Calculate center of target element:
x = (left+right)/2, y = (top+bottom)/2 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.ktswith Kotlin, Hilt, Firebase plugins gradle/libs.versions.tomlversion catalog- App-level
build.gradle.ktswith all 3 flavors configured settings.gradle.kts
Verify: ./gradlew assembleTestingDebug compiles (empty app is fine).
Step 0.2 — Application class + Hilt
App.ktwith@HiltAndroidAppMainActivity.ktwith@AndroidEntryPointand empty Compose contentAndroidManifest.xmlwith 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.ktsealed interface with all routesNavGraph.ktwith 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.ktinterface in mainNoOpTestInstrumentation(used by free/pro flavors)LiveTestInstrumentationwithTestLogEmitter,UIPositionTracker(testing flavor)TestBroadcastReceiverwith DUMP_UI, DUMP_STATEtestTrack()Modifier extension- Hilt modules for each flavor
Verify:
- Build and install testing flavor
adb shell am broadcast -a com.roundingmobile.sni.test.DUMP_UIadb logcat -d -s CLAUDE_SNI_TEST:V→ should see VIEW lines for bottom nav items- Build free flavor → verify it compiles (no-op instrumentation)
- 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.ktwith all entities and DAOsNotificationEntity.kt,NotificationDao.kt(CRUD + Flow queries + FTS)HiddenAppEntity.kt,HiddenAppDao.ktConverters.ktfor JSON fields- Database module for Hilt
Verify:
- Build
- Write a quick unit test or use the testing flavor to insert/query a row via broadcast
- Check logcat for any Room errors
Step 1.2 — Domain models + repository interface
CapturedNotification.ktand other domain modelsNotificationRepository.ktinterface in domainNotificationRepositoryImpl.ktin data with mapperNotificationMapper.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 notificationsNotificationParser.kt— extract all fields from StatusBarNotification- Wire through repository to Room
- Handle hidden apps (skip capture)
Verify:
- Build and install testing flavor
- Grant notification listener permission via adb
- Send a test notification from another app (or use
adb shell amto trigger a system notification) - DUMP_STATE → check
db_countincreased - 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:
- CLEAR_DB
- Inject 5 notifications via broadcast
- DUMP_STATE → db_count=5
- 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:
- Build and install
- CLEAR_DB → inject 10 notifications
- DUMP_UI → verify notif_item_0 through notif_item_9 appear with correct bounds
- Screenshot → verify layout looks good
- Tap sort toggle → DUMP_UI → verify order changed
- CLEAR_DB → verify empty state appears
- 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
keyparameter 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:
- Inject notification, tap it on timeline (using DUMP_UI coordinates)
- DUMP_UI on detail screen → verify all elements present
- Tap copy → verify clipboard (or just verify no crash)
- Tap back → verify return to timeline
- Screenshot detail screen
PHASE 4: Search
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:
- Inject 20 notifications, 3 with "Amazon" in text
- Navigate to search → type "Amazon"
- DUMP_UI → verify 3 results
- Screenshot
- Clear search → verify all results gone
- 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:
- Inject from 4 different apps
- Open filter → DUMP_UI → verify 4 apps listed
- Deselect one → go to timeline → verify filtered
- 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:
- Enable PIN in settings
- Press home → relaunch
- DUMP_UI → verify lock screen shown
- Enter PIN via adb input
- 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.ktin free flavorAdManager.kt— initialize, load bannerAdBanner.ktcomposable — bottom of TimelineScreen only- Wrap in
if (BuildConfig.ADS_ENABLED)
Verify:
- Build free flavor
- Install → verify banner appears on timeline
- Navigate to other screens → verify NO ads anywhere else
- Build pro flavor → verify no ad code compiled
- 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