From 3ce5ca2b56b084b60cb1e2f6ef99098d2d0e0fe1 Mon Sep 17 00:00:00 2001 From: Jatin Nagar Date: Tue, 16 Jul 2024 17:03:26 +0530 Subject: [PATCH 1/3] completed android runtime permissions flow --- .../java/com/reactnativehmssdk/HMSManager.kt | 17 +++++ .../java/com/reactnativehmssdk/HMSRNSDK.kt | 29 +++++++++ .../react-native-hms/src/classes/HMSSDK.tsx | 65 ++++++++++++++++++- .../src/classes/HMSUpdateListenerActions.ts | 1 + .../example/src/utils/functions.ts | 62 +++++++----------- .../src/HMSInstanceSetup.tsx | 1 + .../src/HMSRoomSetup.tsx | 36 +++++++++- 7 files changed, 172 insertions(+), 39 deletions(-) diff --git a/packages/react-native-hms/android/src/main/java/com/reactnativehmssdk/HMSManager.kt b/packages/react-native-hms/android/src/main/java/com/reactnativehmssdk/HMSManager.kt index e9b6e359c..8c06e6b2d 100644 --- a/packages/react-native-hms/android/src/main/java/com/reactnativehmssdk/HMSManager.kt +++ b/packages/react-native-hms/android/src/main/java/com/reactnativehmssdk/HMSManager.kt @@ -1646,6 +1646,23 @@ class HMSManager( } // endregion + @ReactMethod + fun setPermissionsAccepted( + data: ReadableMap, + promise: Promise?, + ) { + val rnSDK = + HMSHelper.getHms(data, hmsCollection) ?: run { + promise?.reject( + "6004", + "RN HMS SDK not initialized", + ) + return + } + rnSDK.hmsSDK?.setPermissionsAccepted() + promise?.resolve(null) + } + // region Warning on JS side @ReactMethod fun addListener(eventName: String) { diff --git a/packages/react-native-hms/android/src/main/java/com/reactnativehmssdk/HMSRNSDK.kt b/packages/react-native-hms/android/src/main/java/com/reactnativehmssdk/HMSRNSDK.kt index 3765916a8..85b6afa8e 100644 --- a/packages/react-native-hms/android/src/main/java/com/reactnativehmssdk/HMSRNSDK.kt +++ b/packages/react-native-hms/android/src/main/java/com/reactnativehmssdk/HMSRNSDK.kt @@ -87,6 +87,13 @@ class HMSRNSDK( } } + if (data?.hasKey("haltPreviewJoinForPermissionsRequest") == true) { + val halt = data?.getBoolean("haltPreviewJoinForPermissionsRequest") + if (halt != null) { + builder.haltPreviewJoinForPermissionsRequest(halt) + } + } + this.hmsSDK = builder.build() hmsSDK?.let { @@ -286,6 +293,17 @@ class HMSRNSDK( data.putArray("removedPeers", removedPeersArray) delegate.emitEvent("ON_PEER_LIST_UPDATED", data) } + + override fun onPermissionsRequested(permissions: List) { + if (eventsEnableStatus["ON_PERMISSIONS_REQUESTED"] != true) { + return + } + val data: WritableMap = Arguments.createMap() + + data.putArray("permissions", Arguments.fromList(permissions)) + data.putString("id", id) + delegate.emitEvent("ON_PERMISSIONS_REQUESTED", data) + } }, ) } else { @@ -557,6 +575,17 @@ class HMSRNSDK( data.putString("id", id) delegate.emitEvent("ON_TRANSCRIPTS", data) } + + override fun onPermissionsRequested(permissions: List) { + if (eventsEnableStatus["ON_PERMISSIONS_REQUESTED"] != true) { + return + } + val data: WritableMap = Arguments.createMap() + + data.putArray("permissions", Arguments.fromList(permissions)) + data.putString("id", id) + delegate.emitEvent("ON_PERMISSIONS_REQUESTED", data) + } }, ) diff --git a/packages/react-native-hms/src/classes/HMSSDK.tsx b/packages/react-native-hms/src/classes/HMSSDK.tsx index 4ceb58add..f8f32dad9 100644 --- a/packages/react-native-hms/src/classes/HMSSDK.tsx +++ b/packages/react-native-hms/src/classes/HMSSDK.tsx @@ -18,7 +18,7 @@ import type { HMSRole } from './HMSRole'; import type { HMSTrack } from './HMSTrack'; import type { HMSLogger } from './HMSLogger'; import type { HMSPeer } from './HMSPeer'; -import { HMSVideoViewMode } from './HMSVideoViewMode'; +import type { HMSVideoViewMode } from './HMSVideoViewMode'; import type { HMSTrackSettings } from './HMSTrackSettings'; import type { HMSRTMPConfig } from './HMSRTMPConfig'; import type { HMSHLSConfig } from './HMSHLSConfig'; @@ -63,6 +63,7 @@ export class HMSSDK { private appStateSubscription?: any; private onPreviewDelegate?: any; private onJoinDelegate?: any; + private onPermissionsRequestedDelegate?: any; private onRoomDelegate?: any; private onTranscriptsDelegate?: any; private onPeerDelegate?: any; @@ -106,6 +107,7 @@ export class HMSSDK { * const hmsInstance = await HMSSDK.build(); * * For Advanced Use-Cases: + * @param {haltPreviewJoinForPermissionsRequest} boolean is an optional value only required to enable Android SDK to request required permissions when needed, user doesn't have to allow all permissions in advance. * @param {trackSettings} trackSettings is an optional value only required to enable features like iOS Screen/Audio Share, Android Software Echo Cancellation, etc * @param {appGroup} appGroup is an optional value only required for implementing Screen & Audio Share on iOS. They are not required for Android. DO NOT USE if your app does not implements Screen or Audio Share on iOS. * @param {preferredExtension} preferredExtension is an optional value only required for implementing Screen & Audio Share on iOS. They are not required for Android. DO NOT USE if your app does not implements Screen or Audio Share on iOS. @@ -116,6 +118,7 @@ export class HMSSDK { */ static async build(params?: { trackSettings?: HMSTrackSettings; + haltPreviewJoinForPermissionsRequest?: boolean; appGroup?: String; preferredExtension?: String; logSettings?: HMSLogSettings; @@ -125,6 +128,8 @@ export class HMSSDK { const { major, minor, patch } = ReactNativeVersion.version; let id = await HMSManager.build({ trackSettings: params?.trackSettings, + haltPreviewJoinForPermissionsRequest: + params?.haltPreviewJoinForPermissionsRequest, // required for Android Permissions, not required for iOS appGroup: params?.appGroup, // required for iOS Screenshare, not required for Android preferredExtension: params?.preferredExtension, // required for iOS Screenshare, not required for Android frameworkInfo: { @@ -1224,6 +1229,12 @@ export class HMSSDK { return HMSManager.setAlwaysScreenOn({ id: this.id, enabled }); }; + setPermissionsAccepted = async () => { + if (Platform.OS === 'ios') return; + logger?.verbose('#Function setPermissionsAccepted', { id: this.id }); + return HMSManager.setPermissionsAccepted({ id: this.id }); + }; + /** * - This is a prototype event listener that takes action and listens for updates related to that particular action * @@ -1272,6 +1283,28 @@ export class HMSSDK { this.onJoinDelegate = callback; break; } + case HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED: { + // Checking if we already have ON_PERMISSIONS_REQUESTED subscription + if ( + !this.emitterSubscriptions[ + HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED + ] + ) { + // Adding ON_PERMISSIONS_REQUESTED native listener + const permissionsRequestedSubscription = + HMSNativeEventListener.addListener( + this.id, + HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED, + this.onPermissionsRequestedListener + ); + this.emitterSubscriptions[ + HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED + ] = permissionsRequestedSubscription; + } + // Adding App Delegate listener + this.onPermissionsRequestedDelegate = callback; + break; + } case HMSUpdateListenerActions.ON_ROOM_UPDATE: { // Checking if we already have ON_ROOM_UPDATE subscription if ( @@ -1735,6 +1768,23 @@ export class HMSSDK { this.onJoinDelegate = null; break; } + case HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED: { + const subscription = + this.emitterSubscriptions[ + HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED + ]; + // Removing ON_PERMISSIONS_REQUESTED native listener + if (subscription) { + subscription.remove(); + + this.emitterSubscriptions[ + HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED + ] = undefined; + } + // Removing App Delegate listener + this.onPermissionsRequestedDelegate = null; + break; + } case HMSUpdateListenerActions.ON_ROOM_UPDATE: { const subscription = this.emitterSubscriptions[HMSUpdateListenerActions.ON_ROOM_UPDATE]; @@ -2150,6 +2200,19 @@ export class HMSSDK { } }; + onPermissionsRequestedListener = (data: { + id: string; + permissions: Array; + }) => { + if (data.id !== this.id) { + return; + } + if (this.onPermissionsRequestedDelegate) { + logger?.verbose('#Listener ON_PERMISSIONS_REQUESTED_LISTENER_CALL', data); + this.onPermissionsRequestedDelegate({ ...data }); + } + }; + onRoomListener = (data: any) => { if (data.id !== this.id) { return; diff --git a/packages/react-native-hms/src/classes/HMSUpdateListenerActions.ts b/packages/react-native-hms/src/classes/HMSUpdateListenerActions.ts index b899b631d..788221e71 100644 --- a/packages/react-native-hms/src/classes/HMSUpdateListenerActions.ts +++ b/packages/react-native-hms/src/classes/HMSUpdateListenerActions.ts @@ -28,4 +28,5 @@ export enum HMSUpdateListenerActions { ON_SESSION_STORE_CHANGED = 'ON_SESSION_STORE_CHANGED', ON_PEER_LIST_UPDATED = 'ON_PEER_LIST_UPDATED', ON_TRANSCRIPTS = 'ON_TRANSCRIPTS', + ON_PERMISSIONS_REQUESTED = 'ON_PERMISSIONS_REQUESTED', } diff --git a/packages/react-native-room-kit/example/src/utils/functions.ts b/packages/react-native-room-kit/example/src/utils/functions.ts index a57d8086a..cd725611b 100644 --- a/packages/react-native-room-kit/example/src/utils/functions.ts +++ b/packages/react-native-room-kit/example/src/utils/functions.ts @@ -1,4 +1,4 @@ -import { Platform } from 'react-native'; +import {Platform} from 'react-native'; import { PERMISSIONS, @@ -6,7 +6,7 @@ import { requestMultiple, RESULTS, } from 'react-native-permissions'; -import { getRoomLinkDetails } from './getRoomLinkDetails'; +import {getRoomLinkDetails} from './getRoomLinkDetails'; export const getMeetingUrl = () => 'https://reactnative.app.100ms.live/meeting/rlk-lsml-aiy'; @@ -14,13 +14,13 @@ export const getMeetingUrl = () => export const callService = async ( roomID: string, success: Function, - failure: Function + failure: Function, ) => { let roomCode; let subdomain; try { if (validateUrl(roomID)) { - const { roomCode: code, roomDomain: domain } = getRoomLinkDetails(roomID); + const {roomCode: code, roomDomain: domain} = getRoomLinkDetails(roomID); roomCode = code; subdomain = domain; @@ -33,29 +33,17 @@ export const callService = async ( return; } - const permissions = await checkPermissions([ - PERMISSIONS.ANDROID.CAMERA, - PERMISSIONS.ANDROID.RECORD_AUDIO, - PERMISSIONS.ANDROID.BLUETOOTH_CONNECT, - ]); - - if (permissions) { - const userId = getRandomUserId(6); - const isQARoom = subdomain && subdomain.search('.qa-') >= 0; - success( - roomCode, - userId, - isQARoom - ? `https://auth-nonprod.100ms.live${Platform.OS === 'ios' ? '/' : ''}` - : undefined, // Auth Endpoint - isQARoom ? 'https://qa-init.100ms.live/init' : undefined, // HMSConfig Endpoint - isQARoom ? 'https://api-nonprod.100ms.live' : undefined // Room Layout endpoint - ); - return; - } else { - failure('permission not granted'); - return; - } + const userId = getRandomUserId(6); + const isQARoom = subdomain && subdomain.search('.qa-') >= 0; + success( + roomCode, + userId, + isQARoom + ? `https://auth-nonprod.100ms.live${Platform.OS === 'ios' ? '/' : ''}` + : undefined, // Auth Endpoint + isQARoom ? 'https://qa-init.100ms.live/init' : undefined, // HMSConfig Endpoint + isQARoom ? 'https://api-nonprod.100ms.live' : undefined, // Room Layout endpoint + ); } catch (error) { console.log(error); failure('error in call service'); @@ -73,7 +61,7 @@ export const getRandomNumberInRange = (min: number, max: number) => { }; export const getRandomUserId = (length: number) => { - return Array.from({ length }, () => { + return Array.from({length}, () => { const randomAlphaAsciiCode = getRandomNumberInRange(97, 123); // 97 - 122 is the ascii code range for a-z chars const alphaCharacter = String.fromCharCode(randomAlphaAsciiCode); return alphaCharacter; @@ -89,7 +77,7 @@ export const validateUrl = (url?: string): boolean => { '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + '(\\?[;&a-z\\d%_.~+=-]*)?' + '(\\#[-a-z\\d_]*)?$', - 'i' + 'i', ); return pattern.test(url); } @@ -99,7 +87,7 @@ export const validateUrl = (url?: string): boolean => { export const checkPermissions = async ( permissions: Array< (typeof PERMISSIONS.ANDROID)[keyof typeof PERMISSIONS.ANDROID] - > + >, ): Promise => { if (Platform.OS === 'ios') { return true; @@ -107,8 +95,8 @@ export const checkPermissions = async ( try { const requiredPermissions = permissions.filter( - (permission) => - permission.toString() !== PERMISSIONS.ANDROID.BLUETOOTH_CONNECT + permission => + permission.toString() !== PERMISSIONS.ANDROID.BLUETOOTH_CONNECT, ); const results = await requestMultiple(requiredPermissions); @@ -121,22 +109,22 @@ export const checkPermissions = async ( console.log( requiredPermissions[permission], ':', - results[requiredPermissions[permission]] + results[requiredPermissions[permission]], ); } // Bluetooth Connect Permission handling if ( permissions.findIndex( - (permission) => - permission.toString() === PERMISSIONS.ANDROID.BLUETOOTH_CONNECT + permission => + permission.toString() === PERMISSIONS.ANDROID.BLUETOOTH_CONNECT, ) >= 0 ) { const bleConnectResult = await request( - PERMISSIONS.ANDROID.BLUETOOTH_CONNECT + PERMISSIONS.ANDROID.BLUETOOTH_CONNECT, ); console.log( - `${PERMISSIONS.ANDROID.BLUETOOTH_CONNECT} : ${bleConnectResult}` + `${PERMISSIONS.ANDROID.BLUETOOTH_CONNECT} : ${bleConnectResult}`, ); } diff --git a/packages/react-native-room-kit/src/HMSInstanceSetup.tsx b/packages/react-native-room-kit/src/HMSInstanceSetup.tsx index 908d5cca6..edba4a49f 100644 --- a/packages/react-native-room-kit/src/HMSInstanceSetup.tsx +++ b/packages/react-native-room-kit/src/HMSInstanceSetup.tsx @@ -141,6 +141,7 @@ const getHmsInstance = async ( const hmsInstance = await HMSSDK.build({ logSettings, trackSettings, + haltPreviewJoinForPermissionsRequest: true, appGroup, preferredExtension, isPrebuilt: true, diff --git a/packages/react-native-room-kit/src/HMSRoomSetup.tsx b/packages/react-native-room-kit/src/HMSRoomSetup.tsx index dc99bbba3..acfbb8120 100644 --- a/packages/react-native-room-kit/src/HMSRoomSetup.tsx +++ b/packages/react-native-room-kit/src/HMSRoomSetup.tsx @@ -10,7 +10,16 @@ import { HMSWhiteboardUpdateType, } from '@100mslive/react-native-hms'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { Alert, Keyboard, StatusBar, StyleSheet, View } from 'react-native'; +import { + Alert, + Keyboard, + PermissionsAndroid, + Platform, + StatusBar, + StyleSheet, + View, +} from 'react-native'; +import type { Permission } from 'react-native'; import Toast from 'react-native-simple-toast'; import { batch, useDispatch, useSelector, useStore } from 'react-redux'; @@ -375,6 +384,31 @@ export const HMSRoomSetup = () => { }; }, [startHLSStreaming, hmsInstance]); + if (Platform.OS === 'android') { + // HMS Android Permissions Listener + useEffect(() => { + const onPermissionsRequested = async ({ + permissions, + }: { + permissions: Array; + }) => { + await PermissionsAndroid.requestMultiple(permissions as Permission[]); + await hmsInstance.setPermissionsAccepted(); + }; + + hmsInstance.addEventListener( + HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED, + onPermissionsRequested + ); + + return () => { + hmsInstance.removeEventListener( + HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED + ); + }; + }, [hmsInstance]); + } + // HMS Active Speaker Listener // dev-note: This is added here because we have `setPeerTrackNodes` here useHMSActiveSpeakerUpdates(setPeerTrackNodes, meetingJoined); From 89548768dae6cf9563d3939e21d601d17f2618f3 Mon Sep 17 00:00:00 2001 From: Yogesh Singh Date: Wed, 17 Jul 2024 20:27:26 +0530 Subject: [PATCH 2/3] updated Example app changelog --- .../example/ExampleAppChangelog.txt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/react-native-room-kit/example/ExampleAppChangelog.txt b/packages/react-native-room-kit/example/ExampleAppChangelog.txt index d6d5a6ad9..8db428b14 100644 --- a/packages/react-native-room-kit/example/ExampleAppChangelog.txt +++ b/packages/react-native-room-kit/example/ExampleAppChangelog.txt @@ -1,6 +1,15 @@ Board: https://app.devrev.ai/100ms/vistas/vista-254 +- Add runtime permission flow on Android +https://app.devrev.ai/100ms/works/ISS-22855 + +- Resolve app not running on Android 14 issue +https://app.devrev.ai/100ms/works/ISS-22860 + +- Resolve warning about React State update +https://app.devrev.ai/100ms/works/ISS-22807 + Room Kit: 1.2.2 React Native SDK: 1.10.9 -Android SDK: 2.9.63 +Android SDK: 2.9.62 iOS SDK: 1.14.1 From 4b3c95d2b9314bfefc5f2e10c52a210e82864895 Mon Sep 17 00:00:00 2001 From: Yogesh Singh Date: Wed, 17 Jul 2024 20:36:33 +0530 Subject: [PATCH 3/3] updated docs --- .../react-native-hms/src/classes/HMSSDK.tsx | 4 +++- .../src/classes/HMSUpdateListenerActions.ts | 24 ++++++++++++++++++- .../src/HMSRoomSetup.tsx | 15 +++++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/react-native-hms/src/classes/HMSSDK.tsx b/packages/react-native-hms/src/classes/HMSSDK.tsx index e8fc2557f..f308ef592 100644 --- a/packages/react-native-hms/src/classes/HMSSDK.tsx +++ b/packages/react-native-hms/src/classes/HMSSDK.tsx @@ -142,7 +142,9 @@ export class HMSSDK { * }); * * @see https://www.100ms.live/docs/react-native/v2/how-to-guides/install-the-sdk/hmssdk - * @static async build - Asynchronously builds and returns an instance of the HMSSDK class. + * @static + * @async + * @function build * @memberof HMSSDK */ static async build(params?: { diff --git a/packages/react-native-hms/src/classes/HMSUpdateListenerActions.ts b/packages/react-native-hms/src/classes/HMSUpdateListenerActions.ts index 8da4ed6d3..14f1de126 100644 --- a/packages/react-native-hms/src/classes/HMSUpdateListenerActions.ts +++ b/packages/react-native-hms/src/classes/HMSUpdateListenerActions.ts @@ -32,7 +32,7 @@ * @property {string} ON_SESSION_STORE_CHANGED - Emitted when the session store has changed. * @property {string} ON_PEER_LIST_UPDATED - Emitted when the list of peers is updated. * @property {string} ON_TRANSCRIPTS - Emitted when transcripts are available. - * + * @property {string} ON_PERMISSIONS_REQUESTED - Emitted when permissions are requested. */ export enum HMSUpdateListenerActions { ON_PREVIEW = 'ON_PREVIEW', @@ -57,6 +57,28 @@ export enum HMSUpdateListenerActions { ON_SESSION_STORE_AVAILABLE = 'ON_SESSION_STORE_AVAILABLE', ON_SESSION_STORE_CHANGED = 'ON_SESSION_STORE_CHANGED', ON_PEER_LIST_UPDATED = 'ON_PEER_LIST_UPDATED', + + /** + * Event emitted when transcripts are available. + * + * This event is triggered when the HMS SDK has generated transcripts from the audio streams in the room. + * It allows the application to receive real-time or post-processed text versions of spoken content, which can be used for + * accessibility features, content analysis, or storing meeting minutes. The availability of this feature depends on the + * HMS service configuration and may require additional setup or permissions. + * + * @type {string} + * @see https://www.100ms.live/docs/react-native/v2/how-to-guides/extend-capabilities/live-captions + */ ON_TRANSCRIPTS = 'ON_TRANSCRIPTS', + + /** + * Event emitted when the HMS SDK requests permissions from the user. Android only. + * + * This event is triggered whenever the application needs to request permissions from the user, such as access to the camera or microphone. + * It is used in conjunction with the platform's permissions API to prompt the user for the necessary permissions and to inform the HMS SDK + * of the user's response. This is crucial for features that require explicit user consent before they can be used. + * + * @type {string} + */ ON_PERMISSIONS_REQUESTED = 'ON_PERMISSIONS_REQUESTED', } diff --git a/packages/react-native-room-kit/src/HMSRoomSetup.tsx b/packages/react-native-room-kit/src/HMSRoomSetup.tsx index a470345df..585bd5012 100644 --- a/packages/react-native-room-kit/src/HMSRoomSetup.tsx +++ b/packages/react-native-room-kit/src/HMSRoomSetup.tsx @@ -376,22 +376,35 @@ export const HMSRoomSetup = () => { }, [startHLSStreaming, hmsInstance]); if (Platform.OS === 'android') { - // HMS Android Permissions Listener + /** + * Sets up a listener for permissions requests on Android devices. + * + * This listener is activated when the HMS SDK requests permissions, such as camera or microphone access. + * It uses the `PermissionsAndroid` API to request these permissions from the user asynchronously. + * Upon receiving the permissions, it notifies the HMS SDK that the permissions have been accepted, + * allowing the SDK to proceed with operations that require these permissions. + * + * Note: This listener is only set up and functional on Android devices, as indicated by the `Platform.OS` check. + */ useEffect(() => { const onPermissionsRequested = async ({ permissions, }: { permissions: Array; }) => { + // Requests multiple permissions using the PermissionsAndroid API. await PermissionsAndroid.requestMultiple(permissions as Permission[]); + // Notifies the HMS SDK that the permissions have been accepted. await hmsInstance.setPermissionsAccepted(); }; + // Adds the permissions requested listener to the HMS SDK. hmsInstance.addEventListener( HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED, onPermissionsRequested ); + // Cleanup function to remove the listener when the component unmounts or dependencies change. return () => { hmsInstance.removeEventListener( HMSUpdateListenerActions.ON_PERMISSIONS_REQUESTED