Skip to content

[Feat]: Automatically map 'photos' permission to 'READ_EXTERNAL_STORAGE' on Android <= 12 (API <= 32) #57

Description

@luisbytes

Description

There is a critical inconsistency in how @capacitor/camera handles gallery permissions on older Android devices (Android 12 or lower / API $\le$ 32), leading to production crashes when using Camera.chooseFromGallery().

  1. The False Positive Bug: When calling Camera.checkPermissions() on Android 12 or lower, the plugin resolves the 'photos' alias state as "granted". However, this is a false positive. Because API $\le$ 32 strictly requires READ_EXTERNAL_STORAGE, the internal native library (ioncameralib) fails to read the selected file metadata later on, throwing an unhandled EACCES (Permission denied) exception.

  2. Type Definition Inconsistency:
    When inspecting the raw JSON payload returned by the native layer on Android 12, the object actually contains the legacy fields:

   {
     "saveGallery": "prompt",
     "readExternalStorage": "prompt",
     "camera": "granted",
     "photos": "granted"
   }

However, the TypeScript interface completely hides these properties, only exposing:

export type CameraPermissionType = 'camera' | 'photos';
export interface PermissionStatus {
    camera: CameraPermissionState;
    photos: CameraPermissionState;
}

This leaves developers blind to the true state of readExternalStorage unless they explicitly cast the result or bypass the types.

Crash Log (Android <= 12)

Fatal Exception: java.io.FileNotFoundException: /storage/emulated/0/Pictures/xxx.png: open failed: EACCES (Permission denied)
       at libcore.io.IoBridge.open(IoBridge.java:496)
       at java.io.FileInputStream.<init>(FileInputStream.java:159)
       at android.media.ExifInterface.initForFilename(ExifInterface.java:2361)
       at io.ionic.libs.ioncameralib.processor.IONCAMRMediaProcessor.createImageMediaResult
       at io.ionic.libs.ioncameralib.manager.IONCAMRGalleryManager.onChooseFromGalleryResult

Proposed Solution

The plugin should internalize this platform retrocompatibility logic so that 'photos' behaves consistently across all OS versions (matching iOS DX):

  • Internal Mapping: When a developer requests or checks 'photos', the native Android implementation should check the device's SDK_INT. If SDK_INT <= 32, it should bind that status to the actual state of READ_EXTERNAL_STORAGE instead of blindly returning "granted".
  • Keep Types Clean: By fixing this under the hood, the TypeScript interface can remain clean (only exposing 'camera' and 'photos'), avoiding the need to leak Android-specific legacy strings like readExternalStorage into the cross-platform API types.

Alternatives Considered (Current Boilerplate Workaround)

To prevent crashes right now, developers are forced to install an extra plugin (@capacitor/device) to manually check the API level and bypass the plugin types using type casting to push 'readExternalStorage' into the flow:

import { Device } from '@capacitor/device';
import { Camera, CameraPermissionType } from '@capacitor/camera';

private readonly MAX_ANDROID_LEGACY_STORAGE_API = 32;

private async getRequiredPermissions(): Promise<CameraPermissionType[]> {
  const permissions = ['camera', 'photos'];
  const { androidSDKVersion } = await Device.getInfo();

  if (androidSDKVersion && androidSDKVersion <= this.MAX_ANDROID_LEGACY_STORAGE_API) {
    // Forced to cast as CameraPermissionType because the type definition excludes it
    permissions.push('readExternalStorage' as CameraPermissionType);
  }

  return permissions;
}

Additional Information

@andredestro @OS-pedrogustavobilro I am not sure if this should be considered a bug or an undocumented limitation

The fact that checkPermissions() returns "granted" for the 'photos' alias on Android 12 and below, while the native layer silently requires READ_EXTERNAL_STORAGE to actually read the files, creates an unexpected false positive that breaks runtime execution. Whether this is handled via an internal mapping fix or explicit documentation updates, improving this behavior would vastly enhance the Developer Experience (DX).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions