Skip to content

Conversation

@woozer
Copy link

@woozer woozer commented Jan 26, 2026

  • Raise targetSdk to 31 (Android 12).
  • Centralize permissions in PermissionManager (storage + notifications).
  • Clean up unused file and imports.
  • Minor rework due to Android Studio recommendations.

Summary by cubic

Raised minSdk to 28 and targetSdk to 31 (Android 12), and centralized storage and notification permissions in a new PermissionManager. Updated screens to request All Files Access when needed and cleaned up legacy permission code.

  • Refactors

    • Added PermissionManager for storage and notification permission checks and requests.
    • Replaced inline permission logic in MainActivity, Library/Container dialogs, and SteamAppScreen.
    • Switched to All Files Access flow on Android 11+; simplified legacy storage permission handling.
    • Removed CustomGameScanner permission helpers and the folder picker’s requestPermissionsForPath.
    • Minor code cleanups and small API adjustments (e.g., download progress checks).
  • Migration

    • Use PermissionManager for all storage/notification permission handling.
    • On Android 11+, external folders trigger the system “All files access” settings flow.
    • minSdk is now 28; older devices are no longer supported.

Written for commit c9157c1. Summary will update on new commits.

Summary by CodeRabbit

  • Chores

    • Increased minimum Android version to 28 and target to 31
    • Centralized and refactored permission handling for storage and notifications
  • New Features

    • Added a unified permission manager to streamline storage/all-files and notification permission requests
    • Improved storage access flows to better handle modern Android all-files access and reduce redundant prompts
    • Minor UI/flow improvements around install/download progress handling

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

Centralizes Android storage and notification permission handling into a new PermissionManager, updates multiple UI components to use it, removes legacy permission helpers, and bumps minSdk 26→28 and targetSdk 28→31.

Changes

Cohort / File(s) Summary
SDK Version Updates
app/build.gradle.kts
minSdk increased 26 → 28; targetSdk increased 28 → 31
New Permission Manager
app/src/main/java/app/gamenative/utils/PermissionManager.kt
Added singleton encapsulating storage & notification checks and request flows (hasStorageAccess, hasStorageAccessForPath, requestStorageAccess, requestAllFilesAccess, hasNotificationPermission, requestNotificationPermission) with launchers and fallbacks
Permission Flow Refactor — Activity / Entry
app/src/main/java/app/gamenative/MainActivity.kt
Replaced inline POST_NOTIFICATIONS checks with PermissionManager-based request flow and permission launcher integration
Permission Flow Refactor — Dialogs / Components
app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt, app/src/main/java/app/gamenative/ui/components/CustomGameFolderPicker.kt
Replaced per-component storage permission logic with PermissionManager calls; removed requestPermissionsForPath helper; added all-files access launcher usage
Permission Flow Refactor — Library Screens
app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt, app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
Replaced CustomGameScanner permission probes with PermissionManager.hasStorageAccess/hasStorageAccessForPath; added allFilesAccessLauncher and centralized requestStorageAccessIfNeeded; adjusted observeGameState signature and some progress/state checks
CustomGameScanner updates
app/src/main/java/app/gamenative/utils/CustomGameScanner.kt
Removed legacy permission helper methods; added context-aware findIconFileForCustomGame and ensureDefaultFolderExists

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as Composable / Activity
    participant PM as PermissionManager
    participant Launcher as ActivityResultLauncher
    participant Android as Android System

    User->>UI: Trigger action requiring storage/notification
    UI->>PM: hasStorageAccess(context) / hasNotificationPermission(context)
    alt Already granted
        PM-->>UI: true
        UI->>UI: Proceed with operation (scan/install/display)
    else Not granted
        PM-->>UI: false
        UI->>PM: requestStorageAccess(context, legacyLauncher, allFilesLauncher) or requestNotificationPermission(context, launcher)
        PM->>Launcher: launch appropriate intent/permission request
        Launcher->>Android: show permission UI
        Android-->>Launcher: user decision
        Launcher-->>UI: result
        UI->>PM: (optional) re-check hasStorageAccess / hasNotificationPermission
        UI->>UI: Proceed or handle denial
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • phobos665

Poem

🐰 I hopped through code with eager paws,
Centralized permissions—no more straws.
One manager calls, the launchers sing,
SDKs rose, and the app took wing.
A floppy-eared cheer for cleaner laws! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Feat/increase target sdk' is only partially related to the changeset. While increasing the target SDK is one change, the PR's primary focus is centralizing permission handling through a new PermissionManager system. Revise the title to reflect the main objective: consider 'Refactor: centralize permission handling with PermissionManager' or 'Feat: increase target SDK and centralize permission management' to accurately represent the significant architectural changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (2)

892-912: Missing permission verification before game migration.

The permissionMovingInternalLauncher callback ignores the permission result map and immediately proceeds with file migration. If the user denies the permissions, the migration will still attempt to run, likely causing failures.

🐛 Proposed fix
         val permissionMovingInternalLauncher = rememberLauncherForActivityResult(
             contract = ActivityResultContracts.RequestMultiplePermissions(),
-            onResult = { permission ->
-                scope.launch {
-                    showMoveDialog = true
-                    StorageUtils.moveGamesFromOldPath(
-                        Paths.get(Environment.getExternalStorageDirectory().absolutePath, "GameNative", "Steam").pathString,
-                        oldGamesDirectory,
-                        onProgressUpdate = { currentFile, fileProgress, movedFiles, totalFiles ->
-                            current = currentFile
-                            progress = fileProgress
-                            moved = movedFiles
-                            total = totalFiles
-                        },
-                        onComplete = {
-                            showMoveDialog = false
-                        },
-                    )
+            onResult = { permissions ->
+                val allGranted = permissions.values.all { it }
+                if (allGranted) {
+                    scope.launch {
+                        showMoveDialog = true
+                        StorageUtils.moveGamesFromOldPath(
+                            Paths.get(Environment.getExternalStorageDirectory().absolutePath, "GameNative", "Steam").pathString,
+                            oldGamesDirectory,
+                            onProgressUpdate = { currentFile, fileProgress, movedFiles, totalFiles ->
+                                current = currentFile
+                                progress = fileProgress
+                                moved = movedFiles
+                                total = totalFiles
+                            },
+                            onComplete = {
+                                showMoveDialog = false
+                            },
+                        )
+                    }
+                } else {
+                    Toast.makeText(
+                        context,
+                        context.getString(R.string.steam_storage_permission_required),
+                        Toast.LENGTH_SHORT
+                    ).show()
                 }
             },
         )

482-494: Use safe call operator for downloadInfo.cancel().

Kotlin's smart cast does not propagate through the intermediate isDownloading boolean variable. Since SteamService.getAppDownloadInfo() returns DownloadInfo?, the direct call at line 488 is unsafe. Use the safe call operator for consistency with line 1039 and other similar usages:

 if (isDownloading) {
-    downloadInfo.cancel()
+    downloadInfo?.cancel()
 }
🤖 Fix all issues with AI agents
In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt`:
- Around line 930-934: The allFilesAccessLauncher callback
(rememberLauncherForActivityResult assigned to allFilesAccessLauncher) only
updates hasStoragePermission via PermissionManager.hasStorageAccess(context) but
does not mirror the denied-case handling performed in permissionLauncher (lines
around permissionLauncher) — you should add the same denial-handling logic:
detect when PermissionManager.hasStorageAccess(context) is false and then show
the same toast feedback and update/close any related dialog state variables
exactly as permissionLauncher does so dialogs aren't left visible; ensure you
reuse the same UI state setters and toast message used by permissionLauncher to
keep behavior consistent.
🧹 Nitpick comments (3)
app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt (1)

156-163: Folder is added before permission is confirmed.

The current flow adds the folder via onAddCustomGameFolder(path) at line 163 regardless of whether the permission request succeeds. If the user denies the permission in settings, the folder will still be added but won't be accessible.

Consider either:

  1. Only adding the folder after confirming access, or
  2. Accepting this UX trade-off since OpenDocumentTree usually grants URI permissions for the selected folder anyway.

If the current behavior is intentional (since OpenDocumentTree grants URI-specific access), the comment at lines 144-146 suggests this is expected. However, the !canAccess check at line 156 implies the folder isn't accessible, yet it's still added.

Consider restructuring the flow
             // Only request permissions if we can't access the folder AND it's outside the sandbox
             // (folders selected via OpenDocumentTree should already be accessible)
             if (!canAccess && !PermissionManager.hasStorageAccessForPath(context, path)) {
                 PermissionManager.requestStorageAccess(
                     context,
                     storagePermissionLauncher,
                     allFilesAccessLauncher,
                 )
+                // Note: Folder is added optimistically; if permission is denied, 
+                // scanning will fail gracefully at runtime
             }
             onAddCustomGameFolder(path)
app/src/main/java/app/gamenative/MainActivity.kt (1)

155-159: Redundant permission check - PermissionManager handles this internally.

The condition if (!hasNotificationPermission) is unnecessary since PermissionManager.requestNotificationPermission() already returns early if permission is granted (see line 108 in PermissionManager.kt). The hasNotificationPermission state is also only set to true after the launcher callback, not before the initial check.

Simplify by removing redundant check
             LaunchedEffect(Unit) {
-                if (!hasNotificationPermission) {
-                    PermissionManager.requestNotificationPermission(context, permissionLauncher)
-                }
+                PermissionManager.requestNotificationPermission(context, permissionLauncher)
             }
app/src/main/java/app/gamenative/utils/PermissionManager.kt (1)

39-46: Path check may have edge cases with normalization.

The string-based path check at lines 40-41 could miss edge cases:

  • Paths with trailing slashes: /Android/data/app.gamenative/ vs /Android/data/app.gamenative
  • Symbolic links or mount points that resolve to the sandbox
  • Case sensitivity on some file systems

Consider normalizing the path before comparison.

Normalize paths for more robust comparison
     fun hasStorageAccessForPath(context: Context, path: String): Boolean {
-        val isOutsideSandbox = !path.contains("/Android/data/${context.packageName}") &&
-            !path.contains(context.dataDir.path)
+        val normalizedPath = java.io.File(path).canonicalPath
+        val externalSandbox = context.getExternalFilesDir(null)?.parentFile?.canonicalPath
+        val internalSandbox = context.dataDir.canonicalPath
+        
+        val isOutsideSandbox = (externalSandbox == null || !normalizedPath.startsWith(externalSandbox)) &&
+            !normalizedPath.startsWith(internalSandbox)
         if (!isOutsideSandbox) {
             return true
         }
         return hasStorageAccess(context)
     }

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 8 files

@woozer woozer marked this pull request as draft January 26, 2026 11:04
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 8 files

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant