Skip to content

Lower minSdk to 29 (Android 10) — closes #2#4

Open
ShubhamJ010 wants to merge 12 commits into
mainfrom
feature/issue-2-lower-minsdk-api29
Open

Lower minSdk to 29 (Android 10) — closes #2#4
ShubhamJ010 wants to merge 12 commits into
mainfrom
feature/issue-2-lower-minsdk-api29

Conversation

@ShubhamJ010

@ShubhamJ010 ShubhamJ010 commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

Implements issue #2: lower minSdk from 34 (Android 14) to 29 (Android 10) so ssJanitor supports older devices, including custom ROMs / hardware stuck on Android 10–12.

Backward-compatible screenshot detection already used MediaStore + ContentObserver (the API-29 columns RELATIVE_PATH / IS_PENDING), so the real work was making the foreground service, permissions, and deletion model valid across the API 29–36 range.

Changes

  • BuildminSdk 34 → 29 (app/build.gradle.kts).
  • Manifest — Add READ_EXTERNAL_STORAGE / WRITE_EXTERNAL_STORAGE (maxSdk=32); keep specialUse + subtype (tolerated on older platforms, only used at runtime on API 34+).
  • Foreground serviceScreenshotDetectionService now passes FOREGROUND_SERVICE_TYPE_SPECIAL_USE only on API 34+ (was >= Q, which crashed the FGS on 29–33).
  • Permissions — New StoragePermissions helper: SDK-aware storage check and request (uses READ_EXTERNAL_STORAGE below 33, READ_MEDIA_IMAGES on 33+).
  • CleanupScreenshotCleanupWorker: silent background deletion gated to API 30+ (needs All-Files); manual/notification deletes via createDeleteRequest still work on 29+.
  • UIHomeScreen: uses the StoragePermissions helper; isAllFilesManager is now honest below API 30.
  • Docs — README, docs/features.md, docs/development.md, fastlane listing, and CHANGELOG updated to Android 10+ with the API-29 auto-cleanup caveat.

Bugs fixed

  • Permanent "Permissions Required" trap on API 29–32 — the old code always checked/requested READ_MEDIA_IMAGES (which doesn't exist below 33), so those devices could never grant storage access and detection never started.
  • Foreground-service crash on API 29–33 — passing SPECIAL_USE to startForeground while the manifest didn't declare it threw SecurityException in onCreate.
  • isAllFilesManager lie on API 29 — it was hardcoded true below API 30 even though All-Files doesn't exist there, masking that silent auto-cleanup can't run.

Verification

  • ./gradlew assembleDebug and assembleRelease both BUILD SUCCESSFUL (including R8 minification and lintVital).
  • Merged manifests confirm minSdkVersion="29", READ/WRITE_EXTERNAL_STORAGE, and retained specialUse.
  • ⚠️ Runtime behavior across API 29–34 (detection, permission prompts, auto-cleanup on 30+) should still be verified on the emulator matrix per docs/development.md before merge.

Closes #2

…p/build.gradle.kts\n- Manifest: add READ_EXTERNAL_STORAGE / WRITE_EXTERNAL_STORAGE (maxSdk 32); keep specialUse (tolerated on older platforms)\n- ScreenshotDetectionService: only pass FOREGROUND_SERVICE_TYPE_SPECIAL_USE on API 34+ (was >= Q, which crashed on 29-33)\n- New StoragePermissions helper: SDK-aware storage-permission check + request (fixes permanent 'Permissions Required' trap on 29-32)\n- ScreenshotCleanupWorker: gate silent background deletion to API 30+ (All-Files); manual/notification deletes still work on 29+\n- HomeScreen: use StoragePermissions helper; isAllFilesManager honest on < R\n- Docs/CHANGELOG: reflect Android 10+ support and the 29 auto-cleanup limitation
@ShubhamJ010 ShubhamJ010 added the feature Feature work / new capabilities label Jul 9, 2026
@ShubhamJ010 ShubhamJ010 self-assigned this Jul 9, 2026
…minSdk to 29 exposed 10 NewApi lint errors (previously hidden at minSdk 34):\n- MainActivity: guard high-refresh-rate block (getDisplay/supportedModes/preferredDisplayModeId need API 30)\n- ScreenshotRepository: MediaStore.createDeleteRequest Collection overload is API 30; add API-29 fallback using direct delete + RecoverableSecurityException consent\n- avd_auto_delete.xml: system_accent1_* colors only exist on API 31+; move original to drawable-v31/ and add an API-29-safe hex default\n- themes.xml: windowSplashScreenBackground is compat-lib backed; suppress the lint false-positive with tools:targetApi=31\n\nVerified: ./gradlew lint test assembleDebug BUILD SUCCESSFUL (0 errors).
ShubhamJ010 and others added 4 commits July 9, 2026 23:52
… system_accent1_100/_300 (tones 100/300 are near-white),\nso against the icon background it read as colorless and did not visibly follow\nthe wallpaper. Drive the icon from saturated dynamic Material You tones\n(system_accent1_200 fill, pulsing to system_accent1_500) and use the public,\nwallpaper-derived android:attr/colorBackground for the icon circle so the\nsplash follows Material You on Android 12+. The drawable-v31 version is used\non API 31+; the default hex drawable remains the <31 fallback.\n\nVerified: ./gradlew lint assembleRelease BUILD SUCCESSFUL (0 errors).
Automate pre-release test-APK flow (Closes #5)
…r\n\nThe reminder was a periodic WorkManager job whose initial delay clamped to 0\nwhen cleanup was <30 min away and whose inexact periodic scheduling let the\nheads-up be deferred/coalesced (worse after minSdk lowered to 29).\n\n- Add CleanupReminderReceiver (broadcast) that shows the heads-up when there\n are archived screenshots pending and re-arms the next day's alarm.\n- CleanupScheduler.setReminderAlarm uses AlarmManager.setExactAndAllowWhileIdle\n (RTC_WAKEUP) with a setAndAllowWhileIdle fallback on API 31+ without\n SCHEDULE_EXACT_ALARM; computeReminderTimeMillis clamps sub-lead-time cleanups\n to now instead of colliding with cleanup.\n- BootReceiver re-arms the reminder alarm after reboot.\n- Manifest: register CleanupReminderReceiver, add SCHEDULE_EXACT_ALARM.\n- Remove obsolete CleanupReminderWorker + unused WORK_REMINDER_NAME.\n- Add CleanupSchedulerReminderTest for the reminder timing math.\n\nVerified: ./gradlew lint test assembleDebug BUILD SUCCESSFUL.
…- Remove the push trigger so branch pushes no longer create a test-<branch>\n pre-release (only pull_request to main does, keyed to pr-<number>).\n- Simplify the tag step to always use pr-<number>.\n- Rename workflow file build-apk.yml -> build-pr-test-apk.yml and update\n its name/comments to reflect PR-only scope.
@ShubhamJ010

Copy link
Copy Markdown
Owner Author

Review of PR #4 (Lower minSdk to 29). Overall the build/manifest/foreground-service changes are solid and CI is green. One blocking bug re-introduces the exact permission trap this PR targets, plus minor nits.\n\n## ⛔ Blocking: storage-permission result handling still hardcoded to READ_MEDIA_IMAGES (API 29-32)\n\napp/src/main/java/dev/sj010/ssjanitor/ui/screens/home/HomeScreen.kt:150-151\nkotlin\nhasStoragePermission =\n permissions[Manifest.permission.READ_MEDIA_IMAGES] ?: hasStoragePermission\n\nThe request path correctly uses StoragePermissions.requiredStoragePermission() (so on API 29-32 it asks for READ_EXTERNAL_STORAGE), but the result reader only looks up READ_MEDIA_IMAGES. RequestMultiplePermissions returns results keyed by the permissions actually requested, so on API 29-32 the map contains READ_EXTERNAL_STORAGE, not READ_MEDIA_IMAGES. The lookup returns null and falls back to the stale hasStoragePermission (still false).\n\nNet effect: on Android 10-12 a user can grant storage access, but the UI never leaves the "Permissions Required" state and keeps re-prompting — the exact trap the PR claims to fix. (hasStoragePermission is also not re-read on ON_RESUME, so returning to the app won't recover.)\n\nSuggested fix — re-query the helper instead of peeking the map:\nkotlin\nhasStoragePermission = StoragePermissions.hasStoragePermission(context)\n\n(or read permissions[StoragePermissions.requiredStoragePermission()]).\n\n## 🔸 Minor nits (non-blocking)\n1. isAllFilesManager returns true below API R (HomeScreen.kt:83-87). It hides the non-existent All-Files UI on API 29, which is the right UX, but the name now means "no grant needed" rather than "has grant". Add a comment/name tweak so future readers don't invert it.\n2. BootReceiver.goAsync() (BootReceiver.kt:13-20) calls finish() at the end without try/finally. If startDetectionService()/setReminderAlarm() throws, the pending result is never finished and the broadcast can be killed. Wrap in try/finally.\n3. deleteScreenshots API 29 path returns at the first RecoverableSecurityException (ScreenshotRepository.kt:118-120) and abandons remaining URIs. Fine for the single-screenshot notification delete, but a batch call on API 29 would stop early; consider collecting the intent sender and deleting the rest.\n\n## ✅ Looks good\n- minSdk 34→29, manifest maxSdk=32 storage perms, retained specialUse subtype — correct.\n- Foreground service passes type 0 below API 34 (was the crash source) and SPECIAL_USE only on 34+ — correct.\n- ScreenshotCleanupWorker gating silent deletion to API 30+ (All-Files) while manual/notification deletes work on 29+ — correct and well documented.\n- AlarmManager reminder with setExactAndAllowWhileIdle + canScheduleExactAlarms() fallback, re-armed on boot — correct.\n- CleanupSchedulerReminderTest covers lead-time clamping and past-time cases.\n\nPlease fix the result-handling bug and this is good to merge.

…rmManager\n\nThe reminder was a periodic WorkManager job whose initial delay clamped to 0\nwhen cleanup was <30 min away and whose inexact periodic scheduling let the\nheads-up be deferred/coalesced (worse after minSdk lowered to 29).\n\n- Add CleanupReminderReceiver (broadcast) that shows the heads-up when there\n are archived screenshots pending and re-arms the next day's alarm.\n- CleanupScheduler.setReminderAlarm uses AlarmManager.setExactAndAllowWhileIdle\n (RTC_WAKEUP) with a setAndAllowWhileIdle fallback on API 31+ without\n SCHEDULE_EXACT_ALARM; computeReminderTimeMillis clamps sub-lead-time cleanups\n to now instead of colliding with cleanup.\n- BootReceiver re-arms the reminder alarm after reboot.\n- Manifest: register CleanupReminderReceiver, add SCHEDULE_EXACT_ALARM.\n- Remove obsolete CleanupReminderWorker + unused WORK_REMINDER_NAME.\n- Add CleanupSchedulerReminderTest for the reminder timing math.\n\nVerified: ./gradlew lint test assembleDebug BUILD SUCCESSFUL."

This reverts commit ae4f689.
…th)\n\nThe permission-launcher result callback read permissions[READ_MEDIA_IMAGES]\nto update hasStoragePermission, but on API 29-32 the app requests\nREAD_EXTERNAL_STORAGE (via StoragePermissions.requiredStoragePermission()),\nso READ_MEDIA_IMAGES is absent from the result map and the ?: fallback kept\nthe stale un-granted value — stranding those devices in 'Permissions Required'\neven after granting.\n\nRe-evaluate from StoragePermissions.hasStoragePermission(context) so the state\nis correct on every API level, consistent with the initial-state computation and\nthe single source of truth.\n\nVerified: ./gradlew compileDebugKotlin BUILD SUCCESSFUL; release APK installed\non OnePlus 8T (API 30).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Feature work / new capabilities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Lower minSdkVersion to support older Android versions (Android 10 / 12)

1 participant