feat: allow camera scene when audio permissions are denied (#2048), Fixes #2047, Fixes #2051

This commit is contained in:
Laurin Quast 2019-01-18 15:58:48 +01:00 committed by GitHub
parent d93a6c7e11
commit 22533ed8e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 211 additions and 70 deletions

View File

@ -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;
@ -369,4 +372,22 @@ public class CameraModule extends ReactContextBaseJavaModule {
}
});
}
@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);
}
}

View File

@ -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`

View File

@ -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);
}
};
@ -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);
}
}
};

View File

@ -607,11 +607,6 @@ static NSDictionary *defaultFaceDetectorOptions = nil;
[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;

View File

@ -371,11 +371,31 @@ RCT_EXPORT_METHOD(checkDeviceAuthorizationStatus:(RCTPromiseResolveBlock)resolve
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,

View File

@ -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: {},

View File

@ -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()) {
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: {
@ -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';
@ -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,
@ -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',
@ -281,6 +343,7 @@ export default class Camera extends React.Component<PropsType, StateType> {
this.state = {
isAuthorized: false,
isAuthorizationChecked: false,
recordAudioPermissionStatus: RecordAudioPermissionStatusEnum.PENDING_AUTHORIZATION,
};
}
@ -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.`);
}
}
}
}
@ -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.`);
}
}
}
}
@ -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) {
options.mute = true
if (
!captureAudio ||
recordAudioPermissionStatus !== RecordAudioPermissionStatusEnum.AUTHORIZED
) {
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);
}
@ -423,9 +505,8 @@ 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,
@ -433,7 +514,16 @@ export default class Camera extends React.Component<PropsType, StateType> {
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 => {
@ -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;
};

View File

@ -1,32 +0,0 @@
import { PermissionsAndroid, Platform } from 'react-native';
export const requestPermissions = async (
hasVideoAndAudio,
CameraManager,
permissionDialogTitle,
permissionDialogMessage,
) => {
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;
};

12
types/index.d.ts vendored
View File

@ -86,10 +86,18 @@ type GoogleVisionBarcodeMode = Readonly<{ NORMAL: any; ALTERNATE: any; INVERTED:
// FaCC (Function as Child Components)
type Self<T> = { [P in keyof T]: P };
type CameraStatus = Readonly<Self<{ READY: any; PENDING_AUTHORIZATION: any; NOT_AUTHORIZED: any }>>;
type RecordAudioPermissionStatus = Readonly<
Self<{
AUTHORIZED: 'AUTHORIZED';
PENDING_AUTHORIZATION: 'PENDING_AUTHORIZATION';
NOT_AUTHORIZED: 'NOT_AUTHORIZED';
}>
>;
type FaCC = (
params: {
camera: RNCamera;
status: keyof CameraStatus;
recordAudioPermissionStatus: keyof RecordAudioPermissionStatus;
},
) => JSX.Element;
@ -130,6 +138,7 @@ export interface RNCameraProps {
pendingAuthorizationView?: JSX.Element;
useCamera2Api?: boolean;
whiteBalance?: keyof WhiteBalance;
captureAudio?: boolean;
onCameraReady?(): void;
onMountError?(error: { message: string }): void;
@ -175,9 +184,6 @@ export interface RNCameraProps {
playSoundOnCapture?: boolean;
// -- IOS ONLY PROPS
/** iOS Only */
captureAudio?: boolean;
defaultVideoQuality?: keyof VideoQuality;
}