Skip to content
This repository has been archived by the owner on Jun 16, 2023. It is now read-only.

Commit

Permalink
feat: allow camera scene when audio permissions are denied (#2048), F…
Browse files Browse the repository at this point in the history
…ixes #2047, Fixes #2051
  • Loading branch information
n1ru4l committed Jan 18, 2019
1 parent d93a6c7 commit 22533ed
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 70 deletions.
29 changes: 25 additions & 4 deletions android/src/main/java/org/reactnative/camera/CameraModule.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
package org.reactnative.camera;

import android.graphics.Bitmap;
import android.os.Build;
import android.Manifest;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;
import android.widget.Toast;

import com.facebook.react.bridge.*;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.UIManagerModule;
import com.google.android.cameraview.AspectRatio;
import com.google.zxing.BarcodeFormat;
import org.reactnative.barcodedetector.BarcodeFormatUtils;
import org.reactnative.camera.tasks.ResolveTakenPictureAsyncTask;
import org.reactnative.camera.utils.ScopedContext;
import org.reactnative.facedetector.RNFaceDetector;
import com.google.android.cameraview.Size;

import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -369,4 +372,22 @@ public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
}
});
}

@ReactMethod
public void checkIfRecordAudioPermissionsAreDefined(final Promise promise) {
try {
PackageInfo info = getCurrentActivity().getPackageManager().getPackageInfo(getReactApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS);
if (info.requestedPermissions != null) {
for (String p : info.requestedPermissions) {
if (p.equals(Manifest.permission.RECORD_AUDIO)) {
promise.resolve(true);
return;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
promise.resolve(false);
}
}
8 changes: 8 additions & 0 deletions docs/RNCamera.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,16 @@ _It's the RNCamera's reference_

#### `status`

One of `RNCamera.Constants.CameraStatus`

'READY' | 'PENDING_AUTHORIZATION' | 'NOT_AUTHORIZED'

#### `recordAudioPermissionStatus`

One of `RNCamera.Constants.RecordAudioPermissionStatus`.

`'AUTHORIZED'` | `'NOT_AUTHORIZED'` | `'PENDING_AUTHORIZATION'`

## Properties

#### `autoFocus`
Expand Down
9 changes: 4 additions & 5 deletions examples/basic/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,8 @@ export default class CameraScreen extends React.Component {

takePicture = async function() {
if (this.camera) {
this.camera.takePictureAsync().then(data => {
console.log('data: ', data);
});
const data = await this.camera.takePictureAsync();
console.warn('takePicture ', data);
}
};

Expand All @@ -96,10 +95,10 @@ export default class CameraScreen extends React.Component {
this.setState({ isRecording: true });
const data = await promise;
this.setState({ isRecording: false });
console.warn(data);
console.warn('takeVideo', data);
}
} catch (e) {
console.warn(e);
console.error(e);
}
}
};
Expand Down
5 changes: 0 additions & 5 deletions ios/RN/RNCamera.m
Original file line number Diff line number Diff line change
Expand Up @@ -607,11 +607,6 @@ - (void)startSession
[self onReady:nil];
return;
#endif
// NSDictionary *cameraPermissions = [EXCameraPermissionRequester permissions];
// if (![cameraPermissions[@"status"] isEqualToString:@"granted"]) {
// [self onMountingError:@{@"message": @"Camera permissions not granted - component could not be rendered."}];
// return;
// }
dispatch_async(self.sessionQueue, ^{
if (self.presetCamera == AVCaptureDevicePositionUnspecified) {
return;
Expand Down
30 changes: 25 additions & 5 deletions ios/RN/RNCameraManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -371,11 +371,31 @@ + (NSDictionary *)faceDetectorConstants

RCT_EXPORT_METHOD(checkVideoAuthorizationStatus:(RCTPromiseResolveBlock)resolve
reject:(__unused RCTPromiseRejectBlock)reject) {
__block NSString *mediaType = AVMediaTypeVideo;

[AVCaptureDevice requestAccessForMediaType:mediaType completionHandler:^(BOOL granted) {
resolve(@(granted));
}];
if ([[NSBundle mainBundle].infoDictionary objectForKey:@"NSCameraUsageDescription"] != nil) {
__block NSString *mediaType = AVMediaTypeVideo;
[AVCaptureDevice requestAccessForMediaType:mediaType completionHandler:^(BOOL granted) {
resolve(@(granted));
}];
} else {
#ifdef DEBUG
RCTLogWarn(@"Checking video permissions without having key 'NSCameraUsageDescription' defined in your Info.plist. You will have to add it to your Info.plist file, otherwise RNCamera is not allowed to use the camera. You can learn more about adding permissions here: https://stackoverflow.com/a/38498347/4202031");
#endif
resolve(@(NO));
}
}

RCT_EXPORT_METHOD(checkRecordAudioAuthorizationStatus:(RCTPromiseResolveBlock)resolve
reject:(__unused RCTPromiseRejectBlock)reject) {
if ([[NSBundle mainBundle].infoDictionary objectForKey:@"NSMicrophoneUsageDescription"] != nil) {
[[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
resolve(@(granted));
}];
} else {
#ifdef DEBUG
RCTLogWarn(@"Checking audio permissions without having key 'NSMicrophoneUsageDescription' defined in your Info.plist. Audio Recording for your video files is therefore disabled. If you do not need audio on your videos is is recommended to set the 'captureAudio' property on your component instance to 'false', otherwise you will have to add the key 'NSMicrophoneUsageDescription' to your Info.plist. You can learn more about adding permissions here: https://stackoverflow.com/a/38498347/4202031");
#endif
resolve(@(NO));
}
}

RCT_REMAP_METHOD(getAvailablePictureSizes,
Expand Down
32 changes: 31 additions & 1 deletion src/Camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,39 @@ import {
View,
Text,
UIManager,
PermissionsAndroid,
} from 'react-native';

import { requestPermissions } from './handlePermissions';
const requestPermissions = async (
hasVideoAndAudio,
CameraManager,
permissionDialogTitle,
permissionDialogMessage,
): Promise<boolean> => {
if (Platform.OS === 'ios') {
let check = hasVideoAndAudio
? CameraManager.checkDeviceAuthorizationStatus
: CameraManager.checkVideoAuthorizationStatus;

if (check) return await check();
} else if (Platform.OS === 'android') {
let params = undefined;
if (permissionDialogTitle || permissionDialogMessage)
params = { title: permissionDialogTitle, message: permissionDialogMessage };
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA, params);
if (!hasVideoAndAudio)
return granted === PermissionsAndroid.RESULTS.GRANTED || granted === true;
const grantedAudio = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
params,
);
return (
(granted === PermissionsAndroid.RESULTS.GRANTED || granted === true) &&
(grantedAudio === PermissionsAndroid.RESULTS.GRANTED || grantedAudio === true)
);
}
return true;
};

const styles = StyleSheet.create({
base: {},
Expand Down
124 changes: 109 additions & 15 deletions src/RNCamera.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,61 @@ import {
ActivityIndicator,
Text,
StyleSheet,
PermissionsAndroid,
} from 'react-native';

import type { FaceFeature } from './FaceDetector';

import { requestPermissions } from './handlePermissions';
const requestPermissions = async (
captureAudio: boolean,
CameraManager: any,
permissionDialogTitle?: string,
permissionDialogMessage?: string,
): Promise<{ hasCameraPermissions: boolean, hasRecordAudioPermissions: boolean }> => {
let hasCameraPermissions = false;
let hasRecordAudioPermissions = false;

let params = undefined;
if (permissionDialogTitle || permissionDialogMessage) {
params = { title: permissionDialogTitle, message: permissionDialogMessage };
}

if (Platform.OS === 'ios') {
hasCameraPermissions = await CameraManager.checkVideoAuthorizationStatus();
} else if (Platform.OS === 'android') {
const cameraPermissionResult = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.CAMERA,
params,
);
hasCameraPermissions = cameraPermissionResult === PermissionsAndroid.RESULTS.GRANTED;
}

if (captureAudio) {
if (Platform.OS === 'ios') {
hasRecordAudioPermissions = await CameraManager.checkRecordAudioAuthorizationStatus();
} else if (Platform.OS === 'android') {
if (await CameraManager.checkIfRecordAudioPermissionsAreDefined()) {

This comment has been minimized.

Copy link
@bcalik

bcalik Feb 5, 2019

@n1ru4l This line caused many crashes for hundreds of users on our app. Error says undefined is not a function (evaluating 't.checkIfRecordAudioPermissionsAreDefined()'). We are downgrading right now.

This comment has been minimized.

Copy link
@n1ru4l

n1ru4l Feb 6, 2019

Author Collaborator

Could you please open an issue and provide more context?

const audioPermissionResult = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
params,
);
hasRecordAudioPermissions = audioPermissionResult === PermissionsAndroid.RESULTS.GRANTED;
} else if (__DEV__) {
// eslint-disable-next-line no-console
console.warn(
`The 'captureAudio' property set on RNCamera instance but 'RECORD_AUDIO' permissions not defined in the applications 'AndroidManifest.xml'. ` +
`If you want to record audio you will have to add '<uses-permission android:name="android.permission.RECORD_AUDIO"/>' to your 'AndroidManifest.xml'. ` +
`Otherwise you should set the 'captureAudio' property on the component instance to 'false'.`,
);
}
}
}

return {
hasCameraPermissions,
hasRecordAudioPermissions,
};
};

const styles = StyleSheet.create({
authorizationContainer: {
Expand Down Expand Up @@ -110,6 +160,7 @@ type PropsType = typeof View.props & {
type StateType = {
isAuthorized: boolean,
isAuthorizationChecked: boolean,
recordAudioPermissionStatus: RecordAudioPermissionStatus,
};

export type Status = 'READY' | 'PENDING_AUTHORIZATION' | 'NOT_AUTHORIZED';
Expand All @@ -120,6 +171,16 @@ const CameraStatus: { [key: Status]: Status } = {
NOT_AUTHORIZED: 'NOT_AUTHORIZED',
};

export type RecordAudioPermissionStatus = 'AUTHORIZED' | 'NOT_AUTHORIZED' | 'PENDING_AUTHORIZATION';

const RecordAudioPermissionStatusEnum: {
[key: RecordAudioPermissionStatus]: RecordAudioPermissionStatus,
} = {
AUTHORIZED: 'AUTHORIZED',
PENDING_AUTHORIZATION: 'PENDING_AUTHORIZATION',
NOT_AUTHORIZED: 'NOT_AUTHORIZED',
};

const CameraManager: Object = NativeModules.RNCameraManager ||
NativeModules.RNCameraModule || {
stubbed: true,
Expand Down Expand Up @@ -172,6 +233,7 @@ export default class Camera extends React.Component<PropsType, StateType> {
GoogleVisionBarcodeDetection: CameraManager.GoogleVisionBarcodeDetection,
FaceDetection: CameraManager.FaceDetection,
CameraStatus,
RecordAudioPermissionStatus: RecordAudioPermissionStatusEnum,
VideoStabilization: CameraManager.VideoStabilization,
Orientation: {
auto: 'auto',
Expand Down Expand Up @@ -281,6 +343,7 @@ export default class Camera extends React.Component<PropsType, StateType> {
this.state = {
isAuthorized: false,
isAuthorizationChecked: false,
recordAudioPermissionStatus: RecordAudioPermissionStatusEnum.PENDING_AUTHORIZATION,
};
}

Expand All @@ -296,9 +359,11 @@ export default class Camera extends React.Component<PropsType, StateType> {
if (typeof options.orientation !== 'number') {
const { orientation } = options;
options.orientation = CameraManager.Orientation[orientation];
if (typeof options.orientation !== 'number') {
// eslint-disable-next-line no-console
console.warn(`Orientation '${orientation}' is invalid.`);
if (__DEV__) {
if (typeof options.orientation !== 'number') {
// eslint-disable-next-line no-console
console.warn(`Orientation '${orientation}' is invalid.`);
}
}
}
}
Expand Down Expand Up @@ -333,9 +398,11 @@ export default class Camera extends React.Component<PropsType, StateType> {
if (typeof options.orientation !== 'number') {
const { orientation } = options;
options.orientation = CameraManager.Orientation[orientation];
if (typeof options.orientation !== 'number') {
// eslint-disable-next-line no-console
console.warn(`Orientation '${orientation}' is invalid.`);
if (__DEV__) {
if (typeof options.orientation !== 'number') {
// eslint-disable-next-line no-console
console.warn(`Orientation '${orientation}' is invalid.`);
}
}
}
}
Expand All @@ -347,11 +414,26 @@ export default class Camera extends React.Component<PropsType, StateType> {
}
}

const { captureAudio } = this.props
const { recordAudioPermissionStatus } = this.state;
const { captureAudio } = this.props;

if (
!captureAudio ||
recordAudioPermissionStatus !== RecordAudioPermissionStatusEnum.AUTHORIZED
) {
options.mute = true;
}

if (!captureAudio) {
options.mute = true
if (__DEV__) {
if (
(options.mute || captureAudio) &&
recordAudioPermissionStatus !== RecordAudioPermissionStatusEnum.AUTHORIZED
) {
// eslint-disable-next-line no-console
console.warn('Recording with audio not possible. Permissions are missing.');
}
}

return await CameraManager.record(options, this._cameraHandle);
}

Expand Down Expand Up @@ -423,17 +505,25 @@ export default class Camera extends React.Component<PropsType, StateType> {
}

async componentDidMount() {
const hasVideoAndAudio = this.props.captureAudio;
const isAuthorized = await requestPermissions(
hasVideoAndAudio,
const { hasCameraPermissions, hasRecordAudioPermissions } = await requestPermissions(
this.props.captureAudio,
CameraManager,
this.props.permissionDialogTitle,
this.props.permissionDialogMessage,
);
if (this._isMounted === false) {
return;
}
this.setState({ isAuthorized, isAuthorizationChecked: true });

const recordAudioPermissionStatus = hasRecordAudioPermissions
? RecordAudioPermissionStatusEnum.AUTHORIZED
: RecordAudioPermissionStatusEnum.NOT_AUTHORIZED;

this.setState({
isAuthorized: hasCameraPermissions,
isAuthorizationChecked: true,
recordAudioPermissionStatus,
});
}

getStatus = (): Status => {
Expand All @@ -449,7 +539,11 @@ export default class Camera extends React.Component<PropsType, StateType> {

renderChildren = (): * => {
if (this.hasFaCC()) {
return this.props.children({ camera: this, status: this.getStatus() });
return this.props.children({
camera: this,
status: this.getStatus(),
recordAudioPermissionStatus: this.state.recordAudioPermissionStatus,
});
}
return this.props.children;
};
Expand Down
Loading

0 comments on commit 22533ed

Please sign in to comment.