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().
-
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.
-
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).
Description
There is a critical inconsistency in how$\le$ 32), leading to production crashes when using
@capacitor/camerahandles gallery permissions on older Android devices (Android 12 or lower / APICamera.chooseFromGallery().The False Positive Bug: When calling$\le$ 32 strictly requires
Camera.checkPermissions()on Android 12 or lower, the plugin resolves the'photos'alias state as"granted". However, this is a false positive. Because APIREAD_EXTERNAL_STORAGE, the internal native library (ioncameralib) fails to read the selected file metadata later on, throwing an unhandledEACCES (Permission denied)exception.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:
This leaves developers blind to the true state of readExternalStorage unless they explicitly cast the result or bypass the types.
Crash Log (Android <= 12)
Proposed Solution
The plugin should internalize this platform retrocompatibility logic so that
'photos'behaves consistently across all OS versions (matching iOS DX):'photos', the native Android implementation should check the device'sSDK_INT. IfSDK_INT <= 32, it should bind that status to the actual state ofREAD_EXTERNAL_STORAGEinstead of blindly returning"granted".'camera'and'photos'), avoiding the need to leak Android-specific legacy strings likereadExternalStorageinto 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: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 requiresREAD_EXTERNAL_STORAGEto 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).