Skip to content

Commit 635ba10

Browse files
authored
fix(camera): handle permissions in the same way in iOS and Android (#613)
1 parent ead4beb commit 635ba10

File tree

7 files changed

+207
-177
lines changed

7 files changed

+207
-177
lines changed

packages/camera/README.md

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818

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

21+
**Note: Version 7 contains breaking changes:**
22+
* New behavior on requesting permissions, detailed in [Request for user permissions](#request-for-user-permissions).
23+
24+
2125
## Installation
2226

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

57-
requestPermissions().then(
58-
function success() {
59-
// permission request accepted or already granted
60-
// ... call camera.takePicture here ...
61-
},
62-
function failure() {
63-
// permission request rejected
64-
// ... tell the user ...
65-
}
66-
);
61+
const perms = await camera.requestPermissions();
62+
63+
if (perms.Success) {
64+
// permission request accepted or already granted
65+
// ... call camera.takePicture here ...
66+
} else {
67+
// permission request rejected
68+
// ... tell the user ...
69+
const cameraPermissionSuccess = perms.Details.Camera.Success;
70+
const photoPermissionSuccess = perms.Details.Photo.Success
71+
}
72+
6773
```
74+
75+
If specifying the `saveToGallery = false` option, you can call the `requestCameraPermissions` method.
76+
77+
```TypeScript
78+
import { requestPermissions } from '@nativescript/camera';
79+
80+
const perms = await camera.requestCameraPermissions();
81+
82+
if (perms.Success) {
83+
// permission request accepted or already granted
84+
// ... call camera.takePicture here ...
85+
} else {
86+
// permission request rejected
87+
// ... tell the user ...
88+
89+
}
90+
91+
```
92+
6893
> **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.
6994
7095
> **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.

packages/camera/common.ts

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,81 @@
1+
import * as permissions from '@nativescript-community/perms';
12
export function getAspectSafeDimensions(sourceWidth, sourceHeight, reqWidth, reqHeight) {
2-
let widthCoef = sourceWidth / reqWidth;
3-
let heightCoef = sourceHeight / reqHeight;
3+
let widthCoef = sourceWidth / reqWidth;
4+
let heightCoef = sourceHeight / reqHeight;
45

5-
let aspectCoef = widthCoef > heightCoef ? widthCoef : heightCoef;
6+
let aspectCoef = widthCoef > heightCoef ? widthCoef : heightCoef;
67

7-
return {
8-
width: Math.floor(sourceWidth / aspectCoef),
9-
height: Math.floor(sourceHeight / aspectCoef)
10-
};
11-
}
8+
return {
9+
width: Math.floor(sourceWidth / aspectCoef),
10+
height: Math.floor(sourceHeight / aspectCoef),
11+
};
12+
}
13+
function mapStatus(result: permissions.Result): Status {
14+
let status = Status.unknown;
15+
if (result && result.length > 1) {
16+
if (Object.keys(Status).findIndex((k) => k === result[0]) >= 0) {
17+
status = Status[result[0]];
18+
}
19+
}
20+
return status;
21+
}
22+
export function mapError(e): PermissionResult {
23+
return {
24+
Success: false,
25+
Details: Status.error,
26+
Error: e,
27+
};
28+
}
29+
export function mapCameraPermissionStatus(permission: permissions.Result): PermissionResult {
30+
const status = mapStatus(permission);
31+
const result = {
32+
Success: status === Status.authorized,
33+
Details: status,
34+
};
35+
return result;
36+
}
37+
38+
export function mapPhotoPermissionStatus(permission: permissions.Result): PermissionResult {
39+
const status = mapStatus(permission);
40+
const result = {
41+
Success: status === Status.authorized || status === Status.limited,
42+
Details: status,
43+
};
44+
return result;
45+
}
46+
47+
export function combineCamerPhotoPermissions(cameraPermissions: PermissionResult, photoPermissions: PermissionResult): PermissionsResult {
48+
const result = {
49+
Success: cameraPermissions.Success && photoPermissions.Success,
50+
Details: {
51+
Camera: cameraPermissions,
52+
Photo: photoPermissions,
53+
},
54+
};
55+
return result;
56+
}
57+
58+
export enum Status {
59+
authorized = 'authorized',
60+
denied = 'denied',
61+
limited = 'limited',
62+
restricted = 'restricted',
63+
undetermined = 'undetermined',
64+
never_ask_again = 'never_ask_again',
65+
unknown = 'unknown',
66+
error = 'error',
67+
}
68+
69+
export interface PermissionsResult {
70+
Success: boolean;
71+
Details: {
72+
Camera?: PermissionResult;
73+
Photo?: PermissionResult;
74+
};
75+
}
76+
77+
export interface PermissionResult {
78+
Success: boolean;
79+
Details: Status;
80+
Error?: any;
81+
}

packages/camera/index.android.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Utils, Application, Device, Trace, ImageAsset } from '@nativescript/core';
22
import * as permissions from '@nativescript-community/perms';
33
import { CameraOptions } from '.';
4+
import { combineCamerPhotoPermissions, mapCameraPermissionStatus, mapPhotoPermissionStatus, PermissionResult, PermissionsResult, Status } from './common';
45

56
const REQUEST_IMAGE_CAPTURE = 3453;
67
declare let global: any;
@@ -32,7 +33,6 @@ export const takePicture = function (options?: CameraOptions): Promise<any> {
3233
reqHeight = options.height ? options.height * density : reqWidth;
3334
shouldKeepAspectRatio = Utils.isNullOrUndefined(options.keepAspectRatio) ? shouldKeepAspectRatio : options.keepAspectRatio;
3435
}
35-
3636
const hasStoragePerm = await permissions.check(android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
3737
if (!api30() && !hasStoragePerm[1]) {
3838
saveToGallery = false;
@@ -174,25 +174,25 @@ function api30(): boolean {
174174
return (<any>android).os.Build.VERSION.SDK_INT >= 30 && Utils.ad.getApplicationContext().getApplicationInfo().targetSdkVersion >= 30;
175175
}
176176

177-
export async function requestPermissions() {
177+
export async function requestPermissions(): Promise<PermissionsResult> {
178+
return requestCameraPermissions().then((cameraPermissions) => requestPhotosPermissions().then((photoPermissions) => combineCamerPhotoPermissions(cameraPermissions, photoPermissions)));
179+
}
180+
181+
export async function requestPhotosPermissions(): Promise<PermissionResult> {
178182
if (api30()) {
179-
const hasPerm = await permissions.request('android.permission.CAMERA');
180-
return hasPerm[1];
183+
return {
184+
Success: true,
185+
Details: Status.authorized,
186+
};
181187
} else {
182-
const hasPerm1 = await permissions.request('android.permission.WRITE_EXTERNAL_STORAGE');
183-
const hasPerm2 = await permissions.request('android.permission.CAMERA');
184-
return hasPerm1[1] && hasPerm2[1];
188+
const hasPerm = await permissions.request('android.permission.WRITE_EXTERNAL_STORAGE');
189+
return mapPhotoPermissionStatus(hasPerm);
185190
}
186191
}
187192

188-
export async function requestPhotosPermissions() {
189-
const hasPerm = await permissions.request('android.permission.WRITE_EXTERNAL_STORAGE');
190-
return hasPerm[1];
191-
}
192-
193-
export async function requestCameraPermissions() {
193+
export async function requestCameraPermissions(): Promise<PermissionResult> {
194194
const hasPerm = await permissions.request('android.permission.CAMERA');
195-
return hasPerm[1];
195+
return mapCameraPermissionStatus(hasPerm);
196196
}
197197

198198
const createDateTimeStamp = function () {

packages/camera/index.d.ts

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ImageAsset } from '@nativescript/core';
2-
2+
import { PermissionsResult, PermissionResult } from './common';
33
/**
44
* Take a photo using the camera.
55
* @param options - Optional parameter for setting different camera options.
@@ -9,56 +9,55 @@ export function takePicture(options?: CameraOptions): Promise<ImageAsset>;
99
/**
1010
* Check required permissions for using device camera.
1111
*/
12-
export function requestPermissions(): Promise<any>;
13-
export function requestCameraPermissions(): Promise<any>;
14-
export function requestPhotosPermissions(): Promise<any>;
12+
export function requestPermissions(): Promise<PermissionsResult>;
13+
export function requestCameraPermissions(): Promise<PermissionResult>;
14+
export function requestPhotosPermissions(): Promise<PermissionResult>;
1515

1616
/**
1717
* Is the camera available to use
1818
*/
1919
export function isAvailable(): Boolean;
2020

2121
export interface CameraOptions {
22-
/**
23-
* Defines the desired width (in device independent pixels) of the taken image. It should be used with height property.
24-
* If `keepAspectRatio` actual image width could be different in order to keep the aspect ratio of the original camera image.
25-
* The actual image width will be greater than requested if the display density of the device is higher (than 1) (full HD+ resolutions).
26-
*/
27-
width?: number;
28-
29-
/**
30-
* Defines the desired height (in device independent pixels) of the taken image. It should be used with width property.
31-
* If `keepAspectRatio` actual image width could be different in order to keep the aspect ratio of the original camera image.
32-
* The actual image height will be greater than requested if the display density of the device is higher (than 1) (full HD+ resolutions).
33-
*/
34-
height?: number;
35-
36-
/**
37-
* Defines if camera picture aspect ratio should be kept during picture resizing.
38-
* This property could affect width or height return values.
39-
*/
40-
keepAspectRatio?: boolean;
41-
42-
/**
43-
* Defines if camera picture should be copied to photo Gallery (Android) or Photos (iOS)
44-
*/
45-
saveToGallery?: boolean;
46-
47-
/**
48-
* iOS Only
49-
* Defines if camera "Retake" or "Use Photo" screen forces user to crop camera picture to a square and optionally lets them zoom in.
50-
*/
51-
allowsEditing?: boolean;
52-
53-
/**
54-
* The initial camera. Default "rear".
55-
* The current implementation doesn't work on all Android devices, in which case it falls back to the default behavior.
56-
*/
57-
cameraFacing?: "front" | "rear";
58-
59-
60-
/**
61-
* (iOS Only) Specify a custom UIModalPresentationStyle (Defaults to UIModalPresentationStyle.FullScreen)
62-
*/
63-
modalPresentationStyle?: number;
22+
/**
23+
* Defines the desired width (in device independent pixels) of the taken image. It should be used with height property.
24+
* If `keepAspectRatio` actual image width could be different in order to keep the aspect ratio of the original camera image.
25+
* The actual image width will be greater than requested if the display density of the device is higher (than 1) (full HD+ resolutions).
26+
*/
27+
width?: number;
28+
29+
/**
30+
* Defines the desired height (in device independent pixels) of the taken image. It should be used with width property.
31+
* If `keepAspectRatio` actual image width could be different in order to keep the aspect ratio of the original camera image.
32+
* The actual image height will be greater than requested if the display density of the device is higher (than 1) (full HD+ resolutions).
33+
*/
34+
height?: number;
35+
36+
/**
37+
* Defines if camera picture aspect ratio should be kept during picture resizing.
38+
* This property could affect width or height return values.
39+
*/
40+
keepAspectRatio?: boolean;
41+
42+
/**
43+
* Defines if camera picture should be copied to photo Gallery (Android) or Photos (iOS)
44+
*/
45+
saveToGallery?: boolean;
46+
47+
/**
48+
* iOS Only
49+
* Defines if camera "Retake" or "Use Photo" screen forces user to crop camera picture to a square and optionally lets them zoom in.
50+
*/
51+
allowsEditing?: boolean;
52+
53+
/**
54+
* The initial camera. Default "rear".
55+
* The current implementation doesn't work on all Android devices, in which case it falls back to the default behavior.
56+
*/
57+
cameraFacing?: 'front' | 'rear';
58+
59+
/**
60+
* (iOS Only) Specify a custom UIModalPresentationStyle (Defaults to UIModalPresentationStyle.FullScreen)
61+
*/
62+
modalPresentationStyle?: number;
6463
}

0 commit comments

Comments
 (0)