ionic-team / ionic-team/capacitor-camera

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

Open
#57 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
4
Forks
1
Avg merge
2d 1h
Merged PRs (30d)
2

Description

### 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:

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

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

```typescript
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)
```java
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.(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:

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

private readonly MAX_ANDROID_LEGACY_STORAGE_API = 32;

private async getRequiredPermissions(): Promise {
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).

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.