Skip to content

fix: resolve silent document upload failures on Android 13+ by implem…#1059

Open
vibhutomer wants to merge 1 commit into
mosip:masterfrom
vibhutomer:fix/android13-storage-permissions
Open

fix: resolve silent document upload failures on Android 13+ by implem…#1059
vibhutomer wants to merge 1 commit into
mosip:masterfrom
vibhutomer:fix/android13-storage-permissions

Conversation

@vibhutomer
Copy link
Copy Markdown

@vibhutomer vibhutomer commented May 14, 2026

Description

This PR resolves a critical device-fragmentation bug where the document upload feature silently failed on modern Android tablets (Android 13 / API 33+).

Previously, the application exclusively requested Permission.storage (READ_EXTERNAL_STORAGE) before opening the file picker. On Android 13+, this legacy permission is completely deprecated and is silently denied by the OS without prompting the user, entirely blocking the document registration workflow.

Changes Made

  • AndroidManifest.xml: Added granular media permissions (READ_MEDIA_IMAGES and READ_MEDIA_VIDEO) to support modern Android API levels.
  • document_upload_control.dart: Implemented device_info_plus to conditionally check the Android SDK version at runtime. The app now correctly requests Permission.photos for Android 13+ devices, while maintaining legacy Permission.storage support for older devices.

Related Issue

Closes #1058

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Device Compatibility (Android 13+ SDK support)

Checklist

  • My code follows the style guidelines of this project.
  • I have performed a self-review of my own code.
  • My changes generate no new warnings or exceptions.

Summary by CodeRabbit

  • Bug Fixes
    • Updated storage and media permissions to comply with Android 13+ requirements. The app now requests appropriate photo or storage permissions when accessing media files, ensuring compatibility with newer Android versions.

Review Change Stack

…enting granular media permissions

Signed-off-by: vibhutomer <vibhutomer25@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Walkthrough

This PR updates the Android registration app to support Android 13+ scoped media permissions for document upload. The manifest declares new granular READ_MEDIA_IMAGES and READ_MEDIA_VIDEO permissions, and the Dart code adds runtime SDK-level detection to request the appropriate permission before launching the scanner on both mobile and responsive layouts.

Changes

Android 13+ Media Permission Compatibility

Layer / File(s) Summary
Permission declarations and imports
android/app/src/main/AndroidManifest.xml, lib/ui/process_ui/widgets/document_upload_control.dart
AndroidManifest replaces legacy READ_EXTERNAL_STORAGE with READ_MEDIA_IMAGES and READ_MEDIA_VIDEO. DocumentUploadControl imports device_info_plus and permission_handler.
Permission-gated scanner launch
lib/ui/process_ui/widgets/document_upload_control.dart
Both mobile and responsive scan button handlers detect Android SDK level, request Permission.photos on Android 13+ or Permission.storage on older versions, display error and return early on denial, then proceed to CustomScanner and continue document flow.

Sequence Diagram

sequenceDiagram
  participant User
  participant ScanButton
  participant DeviceInfo
  participant PermissionHandler
  participant CustomScanner
  User->>ScanButton: tap scan button
  ScanButton->>DeviceInfo: query androidInfo.version.sdkInt
  alt SDK >= 33 (Android 13+)
    ScanButton->>PermissionHandler: request Permission.photos
  else SDK < 33 (Android 12 and below)
    ScanButton->>PermissionHandler: request Permission.storage
  end
  alt permission granted
    PermissionHandler-->>ScanButton: isGranted
    ScanButton->>CustomScanner: navigate
  else permission denied
    PermissionHandler-->>ScanButton: isDenied
    ScanButton->>User: show error SnackBar
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A hop and a skip through permissions new,
From legacy storage to granular too!
Android thirteen now stops the silent fall,
With photos and videos, we grant them all.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately identifies the primary fix for silent document upload failures on Android 13+ devices.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements: AndroidManifest.xml adds granular media permissions [#1058], document_upload_control.dart implements SDK version detection [#1058], and requests Permission.photos on Android 13+ and Permission.storage on older versions [#1058].
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the Android 13+ document upload issue; no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Copy Markdown

@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: 3

🧹 Nitpick comments (2)
android/app/src/main/AndroidManifest.xml (1)

4-9: ⚡ Quick win

Adjust comment placement for clarity.

The comment on line 9 ("Storage - Android 13+") appears below WRITE_EXTERNAL_STORAGE and above MANAGE_EXTERNAL_STORAGE, but the actual Android 13+ permissions (READ_MEDIA_IMAGES and READ_MEDIA_VIDEO) are declared at lines 6-7. Consider moving the comment to line 6 to improve readability.

♻️ Suggested comment reorganization
     <!-- Storage - older Android -->
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
+    <!-- Storage - Android 13+ -->
     <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
     <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/>
-    <!-- Storage - Android 13+ -->
     <!-- Full storage access for documents/PDFs (all Android versions) -->
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/app/src/main/AndroidManifest.xml` around lines 4 - 9, Move the
"Storage - Android 13+" comment so it sits immediately above the Android 13+
permissions (the uses-permission entries with
android:name="android.permission.READ_MEDIA_IMAGES" and
android:name="android.permission.READ_MEDIA_VIDEO") instead of above/after
WRITE_EXTERNAL_STORAGE; update the comment placement in AndroidManifest.xml to
group the older storage permissions (READ_EXTERNAL_STORAGE,
WRITE_EXTERNAL_STORAGE) under the "Storage - older Android" comment and the
READ_MEDIA_* entries under the "Storage - Android 13+" comment for clearer
organization.
lib/ui/process_ui/widgets/document_upload_control.dart (1)

565-589: ⚡ Quick win

Consider handling permanently denied permissions.

When users permanently deny permissions (by selecting "Don't ask again"), the current implementation shows the same error SnackBar without guiding them to Settings. Consider detecting isDenied vs isPermanentlyDenied and offering to open app settings when appropriate.

💡 Example implementation
if (!isGranted) {
  if (!mounted) return;
  final photosStatus = Platform.isAndroid && (await DeviceInfoPlugin().androidInfo).version.sdkInt >= 33
      ? await Permission.photos.status
      : await Permission.storage.status;
  
  if (photosStatus.isPermanentlyDenied) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: const Text('Storage permission is required. Please enable it in Settings.'),
        backgroundColor: Colors.red,
        action: SnackBarAction(
          label: 'Settings',
          textColor: Colors.white,
          onPressed: () => openAppSettings(),
        ),
      ),
    );
  } else {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Storage permission is required to select documents.'),
        backgroundColor: Colors.red,
      ),
    );
  }
  return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/ui/process_ui/widgets/document_upload_control.dart` around lines 565 -
589, The permission failure branch in the document selection flow (the
Platform.isAndroid / DeviceInfoPlugin check using Permission.photos and
Permission.storage) currently only shows a generic SnackBar; update the code in
the same method/block to re-check the Permission.status (use the same
Permission.photos or Permission.storage chosen by SDK version) and test for
isPermanentlyDenied versus isDenied, call openAppSettings() via a SnackBarAction
when isPermanentlyDenied, and preserve the existing SnackBar for non-permanent
denials; also ensure you check mounted before showing the SnackBar to avoid
context issues (refer to Permission.photos, Permission.storage,
photosStatus/storageStatus, openAppSettings, Platform.isAndroid,
DeviceInfoPlugin, and ScaffoldMessenger.of(context).showSnackBar).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/ui/process_ui/widgets/document_upload_control.dart`:
- Around line 567-568: Wrap the DeviceInfoPlugin().androidInfo call and the
surrounding platform/permission logic inside a try-catch to handle exceptions
from device info retrieval; in the catch block log the error (e.g., debugPrint
or process logger) and fall back to requesting legacy storage permission so the
flow still proceeds. Update the block around DeviceInfoPlugin/androidInfo (and
related permission requests like Permission.photos.request and
Permission.storage.request) to use the try-catch, ensure
_documentScanClickedAudit() still runs before the try, and set the isGranted
boolean based on the fallback storage permission when an exception occurs.
- Around line 565-589: Duplicate permission-request logic in the mobile and
responsive handlers should be extracted into a single helper on the
_DocumentUploadControlState; add a private method (e.g. Future<bool>
_requestStoragePermission()) that contains the Platform.isAndroid/device SDK
check and returns whether permission was granted, then replace both duplicated
blocks with a call to _documentScanClickedAudit(); final isGranted = await
_requestStoragePermission(); and keep the existing SnackBar/return behavior if
false, followed by the existing navigator/addDocument/getScannedDocuments flow
(references: _DocumentUploadControlState, _requestStoragePermission,
_documentScanClickedAudit, addDocument, getScannedDocuments).
- Around line 565-589: This code calls ScaffoldMessenger.of(context) after
several awaits; add a mounted check in the surrounding State method (the async
handler that contains this permission/request logic, e.g., the
onPressed/_pickDocument method) immediately after the awaited permission calls
and before using context—i.e., after the final await(s) insert "if (!mounted)
return;" so the method exits if the widget was disposed, preventing use of
context when unmounted; ensure the check is placed before the
ScaffoldMessenger.of(context).showSnackBar(...) call and any other context
usage.

---

Nitpick comments:
In `@android/app/src/main/AndroidManifest.xml`:
- Around line 4-9: Move the "Storage - Android 13+" comment so it sits
immediately above the Android 13+ permissions (the uses-permission entries with
android:name="android.permission.READ_MEDIA_IMAGES" and
android:name="android.permission.READ_MEDIA_VIDEO") instead of above/after
WRITE_EXTERNAL_STORAGE; update the comment placement in AndroidManifest.xml to
group the older storage permissions (READ_EXTERNAL_STORAGE,
WRITE_EXTERNAL_STORAGE) under the "Storage - older Android" comment and the
READ_MEDIA_* entries under the "Storage - Android 13+" comment for clearer
organization.

In `@lib/ui/process_ui/widgets/document_upload_control.dart`:
- Around line 565-589: The permission failure branch in the document selection
flow (the Platform.isAndroid / DeviceInfoPlugin check using Permission.photos
and Permission.storage) currently only shows a generic SnackBar; update the code
in the same method/block to re-check the Permission.status (use the same
Permission.photos or Permission.storage chosen by SDK version) and test for
isPermanentlyDenied versus isDenied, call openAppSettings() via a SnackBarAction
when isPermanentlyDenied, and preserve the existing SnackBar for non-permanent
denials; also ensure you check mounted before showing the SnackBar to avoid
context issues (refer to Permission.photos, Permission.storage,
photosStatus/storageStatus, openAppSettings, Platform.isAndroid,
DeviceInfoPlugin, and ScaffoldMessenger.of(context).showSnackBar).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d59b914d-1230-47a3-9a6d-813ce3eb2ca5

📥 Commits

Reviewing files that changed from the base of the PR and between aef4fb6 and aebd173.

📒 Files selected for processing (2)
  • android/app/src/main/AndroidManifest.xml
  • lib/ui/process_ui/widgets/document_upload_control.dart

Comment on lines +565 to +589
bool isGranted = false;
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt >= 33) {
// Android 13+: Granular media permission
final photosStatus = await Permission.photos.request();
isGranted = photosStatus.isGranted;
} else {
// Android 12 and below: Legacy storage permission
final storageStatus = await Permission.storage.request();
isGranted = storageStatus.isGranted;
}
} else {
final storageStatus = await Permission.storage.request();
isGranted = storageStatus.isGranted;
}
if (!isGranted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Storage permission is required to select documents.'),
backgroundColor: Colors.red,
),
);
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Extract duplicate permission logic into a helper method.

The identical permission-checking logic appears in both mobile (lines 565-589) and responsive (lines 860-895) layout handlers, violating the DRY principle. Extract this into a reusable helper method to improve maintainability.

♻️ Proposed refactor to eliminate duplication

Add this helper method to the _DocumentUploadControlState class:

Future<bool> _requestStoragePermission() async {
  if (Platform.isAndroid) {
    final androidInfo = await DeviceInfoPlugin().androidInfo;
    if (androidInfo.version.sdkInt >= 33) {
      // Android 13+: Granular media permission
      final photosStatus = await Permission.photos.request();
      return photosStatus.isGranted;
    } else {
      // Android 12 and below: Legacy storage permission
      final storageStatus = await Permission.storage.request();
      return storageStatus.isGranted;
    }
  } else {
    final storageStatus = await Permission.storage.request();
    return storageStatus.isGranted;
  }
}

Then replace both occurrences with:

_documentScanClickedAudit();
final isGranted = await _requestStoragePermission();
if (!isGranted) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Storage permission is required to select documents.'),
      backgroundColor: Colors.red,
    ),
  );
  return;
}
var doc = await Navigator.push(
  context,
  MaterialPageRoute(
      builder: (context) =>
          CustomScanner(field: widget.field)),
);
await addDocument(doc, widget.field, referenceNumber);
await getScannedDocuments(widget.field);

Also applies to: 860-895

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/ui/process_ui/widgets/document_upload_control.dart` around lines 565 -
589, Duplicate permission-request logic in the mobile and responsive handlers
should be extracted into a single helper on the _DocumentUploadControlState; add
a private method (e.g. Future<bool> _requestStoragePermission()) that contains
the Platform.isAndroid/device SDK check and returns whether permission was
granted, then replace both duplicated blocks with a call to
_documentScanClickedAudit(); final isGranted = await
_requestStoragePermission(); and keep the existing SnackBar/return behavior if
false, followed by the existing navigator/addDocument/getScannedDocuments flow
(references: _DocumentUploadControlState, _requestStoragePermission,
_documentScanClickedAudit, addDocument, getScannedDocuments).

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add mounted check before using context after async operations.

The code uses context in ScaffoldMessenger.of(context) (line 582) after multiple await calls without verifying the widget is still mounted. This can cause exceptions if the widget is disposed during the async operations.

🛡️ Proposed fix to add mounted check
                                   if (!isGranted) {
+                                    if (!mounted) return;
                                     ScaffoldMessenger.of(context).showSnackBar(
                                       const SnackBar(
                                         content: Text('Storage permission is required to select documents.'),
                                         backgroundColor: Colors.red,
                                       ),
                                     );
                                     return;
                                   }
+                                  if (!mounted) return;
                                   var doc = await Navigator.push(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/ui/process_ui/widgets/document_upload_control.dart` around lines 565 -
589, This code calls ScaffoldMessenger.of(context) after several awaits; add a
mounted check in the surrounding State method (the async handler that contains
this permission/request logic, e.g., the onPressed/_pickDocument method)
immediately after the awaited permission calls and before using context—i.e.,
after the final await(s) insert "if (!mounted) return;" so the method exits if
the widget was disposed, preventing use of context when unmounted; ensure the
check is placed before the ScaffoldMessenger.of(context).showSnackBar(...) call
and any other context usage.

Comment on lines +567 to +568
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt >= 33) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add error handling for device info retrieval.

The call to DeviceInfoPlugin().androidInfo can throw an exception (e.g., on unsupported platforms or plugin initialization failures), but there's no try-catch block. Consider adding error handling to gracefully fallback or show a user-friendly message.

🛡️ Proposed fix with error handling
_documentScanClickedAudit();
bool isGranted = false;
try {
  if (Platform.isAndroid) {
    final androidInfo = await DeviceInfoPlugin().androidInfo;
    if (androidInfo.version.sdkInt >= 33) {
      final photosStatus = await Permission.photos.request();
      isGranted = photosStatus.isGranted;
    } else {
      final storageStatus = await Permission.storage.request();
      isGranted = storageStatus.isGranted;
    }
  } else {
    final storageStatus = await Permission.storage.request();
    isGranted = storageStatus.isGranted;
  }
} catch (e) {
  debugPrint("Error checking device info or permissions: $e");
  // Fallback to legacy permission on error
  final storageStatus = await Permission.storage.request();
  isGranted = storageStatus.isGranted;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/ui/process_ui/widgets/document_upload_control.dart` around lines 567 -
568, Wrap the DeviceInfoPlugin().androidInfo call and the surrounding
platform/permission logic inside a try-catch to handle exceptions from device
info retrieval; in the catch block log the error (e.g., debugPrint or process
logger) and fall back to requesting legacy storage permission so the flow still
proceeds. Update the block around DeviceInfoPlugin/androidInfo (and related
permission requests like Permission.photos.request and
Permission.storage.request) to use the try-catch, ensure
_documentScanClickedAudit() still runs before the try, and set the isGranted
boolean based on the fallback storage permission when an exception occurs.

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.

Document Upload feature silently fails on modern Android 13+ tablets due to deprecated Storage Permissions

1 participant