Skip to content

fix(camera): handle permissions in the same way in iOS and Android #613

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 35 additions & 10 deletions packages/camera/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@

A plugin that allows you to take a picture and optionally save it on the device storage.

**Note: Version 7 contains breaking changes:**
* New behavior on requesting permissions, detailed in [Request for user permissions](#request-for-user-permissions).


## Installation

To install the plugin, run the following command in the root directory of your project:
Expand Down Expand Up @@ -54,17 +58,38 @@ To prompt the user to grant or deny your app access to their camera and photo ga
```TypeScript
import { requestPermissions } from '@nativescript/camera';

requestPermissions().then(
function success() {
// permission request accepted or already granted
// ... call camera.takePicture here ...
},
function failure() {
// permission request rejected
// ... tell the user ...
}
);
const perms = await camera.requestPermissions();

if (perms.Success) {
// permission request accepted or already granted
// ... call camera.takePicture here ...
} else {
// permission request rejected
// ... tell the user ...
const cameraPermissionSuccess = perms.Details.Camera.Success;
const photoPermissionSuccess = perms.Details.Photo.Success
}

```

If specifying the `saveToGallery = false` option, you can call the `requestCameraPermissions` method.

```TypeScript
import { requestPermissions } from '@nativescript/camera';

const perms = await camera.requestCameraPermissions();

if (perms.Success) {
// permission request accepted or already granted
// ... call camera.takePicture here ...
} else {
// permission request rejected
// ... tell the user ...

}

```

> **Note:** (**for Android**) Older versions of Android that don't use a request permissions popup won't be affected by the usage of the `requestPermissions()` method.

> **Note**: (**for iOS**) If the user rejects permissions from the iOS popup, the app is not allowed to ask again. You can instruct the user to go to app settings and enable the camera permission manually from there.
Expand Down
86 changes: 78 additions & 8 deletions packages/camera/common.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,81 @@
import * as permissions from '@nativescript-community/perms';
export function getAspectSafeDimensions(sourceWidth, sourceHeight, reqWidth, reqHeight) {
let widthCoef = sourceWidth / reqWidth;
let heightCoef = sourceHeight / reqHeight;
let widthCoef = sourceWidth / reqWidth;
let heightCoef = sourceHeight / reqHeight;

let aspectCoef = widthCoef > heightCoef ? widthCoef : heightCoef;
let aspectCoef = widthCoef > heightCoef ? widthCoef : heightCoef;

return {
width: Math.floor(sourceWidth / aspectCoef),
height: Math.floor(sourceHeight / aspectCoef)
};
}
return {
width: Math.floor(sourceWidth / aspectCoef),
height: Math.floor(sourceHeight / aspectCoef),
};
}
function mapStatus(result: permissions.Result): Status {
let status = Status.unknown;
if (result && result.length > 1) {
if (Object.keys(Status).findIndex((k) => k === result[0]) >= 0) {
status = Status[result[0]];
}
}
return status;
}
export function mapError(e): PermissionResult {
return {
Success: false,
Details: Status.error,
Error: e,
};
}
export function mapCameraPermissionStatus(permission: permissions.Result): PermissionResult {
const status = mapStatus(permission);
const result = {
Success: status === Status.authorized,
Details: status,
};
return result;
}

export function mapPhotoPermissionStatus(permission: permissions.Result): PermissionResult {
const status = mapStatus(permission);
const result = {
Success: status === Status.authorized || status === Status.limited,
Details: status,
};
return result;
}

export function combineCamerPhotoPermissions(cameraPermissions: PermissionResult, photoPermissions: PermissionResult): PermissionsResult {
const result = {
Success: cameraPermissions.Success && photoPermissions.Success,
Details: {
Camera: cameraPermissions,
Photo: photoPermissions,
},
};
return result;
}

export enum Status {
authorized = 'authorized',
denied = 'denied',
limited = 'limited',
restricted = 'restricted',
undetermined = 'undetermined',
never_ask_again = 'never_ask_again',
unknown = 'unknown',
error = 'error',
}

export interface PermissionsResult {
Success: boolean;
Details: {
Camera?: PermissionResult;
Photo?: PermissionResult;
};
}

export interface PermissionResult {
Success: boolean;
Details: Status;
Error?: any;
}
28 changes: 14 additions & 14 deletions packages/camera/index.android.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Utils, Application, Device, Trace, ImageAsset } from '@nativescript/core';
import * as permissions from '@nativescript-community/perms';
import { CameraOptions } from '.';
import { combineCamerPhotoPermissions, mapCameraPermissionStatus, mapPhotoPermissionStatus, PermissionResult, PermissionsResult, Status } from './common';

const REQUEST_IMAGE_CAPTURE = 3453;
declare let global: any;
Expand Down Expand Up @@ -32,7 +33,6 @@ export const takePicture = function (options?: CameraOptions): Promise<any> {
reqHeight = options.height ? options.height * density : reqWidth;
shouldKeepAspectRatio = Utils.isNullOrUndefined(options.keepAspectRatio) ? shouldKeepAspectRatio : options.keepAspectRatio;
}

const hasStoragePerm = await permissions.check(android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (!api30() && !hasStoragePerm[1]) {
saveToGallery = false;
Expand Down Expand Up @@ -174,25 +174,25 @@ function api30(): boolean {
return (<any>android).os.Build.VERSION.SDK_INT >= 30 && Utils.ad.getApplicationContext().getApplicationInfo().targetSdkVersion >= 30;
}

export async function requestPermissions() {
export async function requestPermissions(): Promise<PermissionsResult> {
return requestCameraPermissions().then((cameraPermissions) => requestPhotosPermissions().then((photoPermissions) => combineCamerPhotoPermissions(cameraPermissions, photoPermissions)));
}

export async function requestPhotosPermissions(): Promise<PermissionResult> {
if (api30()) {
const hasPerm = await permissions.request('android.permission.CAMERA');
return hasPerm[1];
return {
Success: true,
Details: Status.authorized,
};
} else {
const hasPerm1 = await permissions.request('android.permission.WRITE_EXTERNAL_STORAGE');
const hasPerm2 = await permissions.request('android.permission.CAMERA');
return hasPerm1[1] && hasPerm2[1];
const hasPerm = await permissions.request('android.permission.WRITE_EXTERNAL_STORAGE');
return mapPhotoPermissionStatus(hasPerm);
}
}

export async function requestPhotosPermissions() {
const hasPerm = await permissions.request('android.permission.WRITE_EXTERNAL_STORAGE');
return hasPerm[1];
}

export async function requestCameraPermissions() {
export async function requestCameraPermissions(): Promise<PermissionResult> {
const hasPerm = await permissions.request('android.permission.CAMERA');
return hasPerm[1];
return mapCameraPermissionStatus(hasPerm);
}

const createDateTimeStamp = function () {
Expand Down
91 changes: 45 additions & 46 deletions packages/camera/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ImageAsset } from '@nativescript/core';

import { PermissionsResult, PermissionResult } from './common';
/**
* Take a photo using the camera.
* @param options - Optional parameter for setting different camera options.
Expand All @@ -9,56 +9,55 @@ export function takePicture(options?: CameraOptions): Promise<ImageAsset>;
/**
* Check required permissions for using device camera.
*/
export function requestPermissions(): Promise<any>;
export function requestCameraPermissions(): Promise<any>;
export function requestPhotosPermissions(): Promise<any>;
export function requestPermissions(): Promise<PermissionsResult>;
export function requestCameraPermissions(): Promise<PermissionResult>;
export function requestPhotosPermissions(): Promise<PermissionResult>;

/**
* Is the camera available to use
*/
export function isAvailable(): Boolean;

export interface CameraOptions {
/**
* Defines the desired width (in device independent pixels) of the taken image. It should be used with height property.
* If `keepAspectRatio` actual image width could be different in order to keep the aspect ratio of the original camera image.
* The actual image width will be greater than requested if the display density of the device is higher (than 1) (full HD+ resolutions).
*/
width?: number;

/**
* Defines the desired height (in device independent pixels) of the taken image. It should be used with width property.
* If `keepAspectRatio` actual image width could be different in order to keep the aspect ratio of the original camera image.
* The actual image height will be greater than requested if the display density of the device is higher (than 1) (full HD+ resolutions).
*/
height?: number;

/**
* Defines if camera picture aspect ratio should be kept during picture resizing.
* This property could affect width or height return values.
*/
keepAspectRatio?: boolean;

/**
* Defines if camera picture should be copied to photo Gallery (Android) or Photos (iOS)
*/
saveToGallery?: boolean;

/**
* iOS Only
* Defines if camera "Retake" or "Use Photo" screen forces user to crop camera picture to a square and optionally lets them zoom in.
*/
allowsEditing?: boolean;

/**
* The initial camera. Default "rear".
* The current implementation doesn't work on all Android devices, in which case it falls back to the default behavior.
*/
cameraFacing?: "front" | "rear";


/**
* (iOS Only) Specify a custom UIModalPresentationStyle (Defaults to UIModalPresentationStyle.FullScreen)
*/
modalPresentationStyle?: number;
/**
* Defines the desired width (in device independent pixels) of the taken image. It should be used with height property.
* If `keepAspectRatio` actual image width could be different in order to keep the aspect ratio of the original camera image.
* The actual image width will be greater than requested if the display density of the device is higher (than 1) (full HD+ resolutions).
*/
width?: number;

/**
* Defines the desired height (in device independent pixels) of the taken image. It should be used with width property.
* If `keepAspectRatio` actual image width could be different in order to keep the aspect ratio of the original camera image.
* The actual image height will be greater than requested if the display density of the device is higher (than 1) (full HD+ resolutions).
*/
height?: number;

/**
* Defines if camera picture aspect ratio should be kept during picture resizing.
* This property could affect width or height return values.
*/
keepAspectRatio?: boolean;

/**
* Defines if camera picture should be copied to photo Gallery (Android) or Photos (iOS)
*/
saveToGallery?: boolean;

/**
* iOS Only
* Defines if camera "Retake" or "Use Photo" screen forces user to crop camera picture to a square and optionally lets them zoom in.
*/
allowsEditing?: boolean;

/**
* The initial camera. Default "rear".
* The current implementation doesn't work on all Android devices, in which case it falls back to the default behavior.
*/
cameraFacing?: 'front' | 'rear';

/**
* (iOS Only) Specify a custom UIModalPresentationStyle (Defaults to UIModalPresentationStyle.FullScreen)
*/
modalPresentationStyle?: number;
}
Loading