diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/AndroidSettingsModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/AndroidSettingsModule.java new file mode 100644 index 000000000..d14b9828d --- /dev/null +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/AndroidSettingsModule.java @@ -0,0 +1,44 @@ +/** + * Adapted from + * {@link https://github.com/Aleksandern/react-native-android-settings-library}. + */ + +package org.jitsi.meet.sdk; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.Settings; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +class AndroidSettingsModule extends ReactContextBaseJavaModule { + /** + * React Native module name. + */ + private static final String MODULE_NAME = "AndroidSettings"; + + public AndroidSettingsModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return MODULE_NAME; + } + + @ReactMethod + public void open() { + Context context = getReactApplicationContext(); + Intent intent = new Intent(); + + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData( + Uri.fromParts("package", context.getPackageName(), null)); + + context.startActivity(intent); + } +} diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java index 1ecff365b..b6fe2af11 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java @@ -59,6 +59,7 @@ public class JitsiMeetView extends FrameLayout { private static List createNativeModules( ReactApplicationContext reactContext) { return Arrays.asList( + new AndroidSettingsModule(reactContext), new AudioModeModule(reactContext), new ExternalAPIModule(reactContext), new ProximityModule(reactContext) diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js index 3b2e9bc0b..8c9d6e2a0 100644 --- a/react/features/app/components/App.native.js +++ b/react/features/app/components/App.native.js @@ -8,6 +8,7 @@ import '../../mobile/audio-mode'; import '../../mobile/background'; import '../../mobile/external-api'; import '../../mobile/full-screen'; +import '../../mobile/permissions'; import '../../mobile/proximity'; import '../../mobile/wake-lock'; diff --git a/react/features/base/tracks/actionTypes.js b/react/features/base/tracks/actionTypes.js index 163a261ad..35f346ae7 100644 --- a/react/features/base/tracks/actionTypes.js +++ b/react/features/base/tracks/actionTypes.js @@ -9,6 +9,17 @@ */ export const TRACK_ADDED = Symbol('TRACK_ADDED'); +/** + * Action for when a track cannot be created because permissions have not been + * granted. + * + * { + * type: TRACK_PERMISSION_ERROR, + * trackType: string + * } + */ +export const TRACK_PERMISSION_ERROR = Symbol('TRACK_PERMISSION_ERROR'); + /** * Action for when a track has been removed from the conference, * local or remote. diff --git a/react/features/base/tracks/actions.js b/react/features/base/tracks/actions.js index 6c7f4007e..4b123ed84 100644 --- a/react/features/base/tracks/actions.js +++ b/react/features/base/tracks/actions.js @@ -7,7 +7,12 @@ import { } from '../media'; import { getLocalParticipant } from '../participants'; -import { TRACK_ADDED, TRACK_REMOVED, TRACK_UPDATED } from './actionTypes'; +import { + TRACK_ADDED, + TRACK_PERMISSION_ERROR, + TRACK_REMOVED, + TRACK_UPDATED +} from './actionTypes'; import { createLocalTracksF } from './functions'; /** @@ -78,14 +83,19 @@ export function createLocalTracksA(options = {}) { }, /* firePermissionPromptIsShownEvent */ false, store) - .then(localTracks => dispatch(_updateLocalTracks(localTracks))); - - // TODO The function createLocalTracksF logs the rejection reason of - // JitsiMeetJS.createLocalTracks so there is no real benefit to - // logging it here as well. Technically though, - // _updateLocalTracks may cause a rejection so it may be nice to log - // it. It's not too big of a concern at the time of this writing - // because React Native warns on unhandled Promise rejections. + .then(localTracks => dispatch(_updateLocalTracks(localTracks))) + .catch(({ gum }) => { + // If permissions are not allowed, alert the user. + if (gum + && gum.error + && gum.error.name === 'DOMException' + && gum.error.message === 'NotAllowedError') { + dispatch({ + type: TRACK_PERMISSION_ERROR, + trackType: device + }); + } + }); } }; } diff --git a/react/features/mobile/permissions/functions.js b/react/features/mobile/permissions/functions.js new file mode 100644 index 000000000..e9fa54693 --- /dev/null +++ b/react/features/mobile/permissions/functions.js @@ -0,0 +1,47 @@ +import { Alert, Linking, NativeModules } from 'react-native'; + +import { Platform } from '../../base/react'; + +/** + * Shows an alert panel which tells the user they have to manually grant some + * permissions by opening Settings. A button which opens Settings is provided. + * + * FIXME: translate. + * + * @param {string} trackType - Type of track that failed with a permission + * error. + * @returns {void} + */ +export function alertPermissionErrorWithSettings(trackType) { + const type = trackType === 'video' ? 'Camera' : 'Microphone'; + + Alert.alert( + 'Permissions Error', + `${type} permission is required, please enable it in Settings.`, + [ + { text: 'Cancel' }, + { + onPress: _openSettings, + text: 'Settings' + } + ], + { cancelable: false }); +} + +/** + * Opens the settings panel for the current platform. + * + * @private + * @returns {void} + */ +function _openSettings() { + switch (Platform.OS) { + case 'android': + NativeModules.AndroidSettings.open(); + break; + + case 'ios': + Linking.openURL('app-settings:'); + break; + } +} diff --git a/react/features/mobile/permissions/index.js b/react/features/mobile/permissions/index.js new file mode 100644 index 000000000..d43689289 --- /dev/null +++ b/react/features/mobile/permissions/index.js @@ -0,0 +1 @@ +import './middleware'; diff --git a/react/features/mobile/permissions/middleware.js b/react/features/mobile/permissions/middleware.js new file mode 100644 index 000000000..c9dfcd528 --- /dev/null +++ b/react/features/mobile/permissions/middleware.js @@ -0,0 +1,25 @@ +/* @flow */ + +import { MiddlewareRegistry } from '../../base/redux'; +import { TRACK_PERMISSION_ERROR } from '../../base/tracks'; + +import { alertPermissionErrorWithSettings } from './functions'; + +/** + * Middleware that captures track permission errors and alerts the user so they + * can enable the permission themselves. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(() => next => action => { + const result = next(action); + + switch (action.type) { + case TRACK_PERMISSION_ERROR: + alertPermissionErrorWithSettings(action.trackType); + break; + } + + return result; +});