Skip to content

Commit

Permalink
feat(react-native-sdk/android): force permissions to launch RNOngoing…
Browse files Browse the repository at this point in the history
…Notification
  • Loading branch information
Calinteodor committed Dec 16, 2024
1 parent aabc50d commit 7da0850
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ tsconfig.json
react-native-sdk/*.tgz
react-native-sdk/android/src
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JitsiMeetReactNativePackage.java
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JMOngoingConferenceModule.java
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/RNOngoingNotification.java
react-native-sdk/images
react-native-sdk/ios
react-native-sdk/lang
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.jitsi.meet.sdk;
import android.app.Activity;
import android.content.Context;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.module.annotations.ReactModule;

/**
* This class implements a ReactModule and it's
* responsible for launching/aborting a service when a conference is in progress.
*/
@ReactModule(name = JMOngoingConferenceModule.NAME)
class JMOngoingConferenceModule extends ReactContextBaseJavaModule {

public static final String NAME = "JMOngoingConference";

public JMOngoingConferenceModule(ReactApplicationContext reactContext) {
super(reactContext);
}

@ReactMethod
public void launch() {
Context context = getReactApplicationContext();
Activity currentActivity = getCurrentActivity();
JitsiMeetOngoingConferenceService.launch(context, currentActivity);
}

@ReactMethod
public void abort() {
Context context = getReactApplicationContext();
JitsiMeetOngoingConferenceService.abort(context);
}

@NonNull
@Override
public String getName() {
return NAME;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import static android.Manifest.permission.POST_NOTIFICATIONS;
import static android.Manifest.permission.RECORD_AUDIO;

import android.app.Activity;
import android.app.Notification;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.IBinder;

import com.facebook.react.modules.core.PermissionListener;

import org.jitsi.meet.sdk.log.JitsiMeetLogger;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
* This class implements an Android {@link Service}, a foreground one specifically, and it's
* responsible for presenting an ongoing notification when a conference is in progress.
* The service will help keep the app running while in the background.
*
* See: https://developer.android.com/guide/components/services
*/
public class JitsiMeetOngoingConferenceService extends Service {
private static final String TAG = JitsiMeetOngoingConferenceService.class.getSimpleName();

private static final int PERMISSIONS_REQUEST_CODE = (int) (Math.random() * Short.MAX_VALUE);

static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;

private static PermissionListener permissionListener;


public static void doLaunch(Context context, Activity currentActivity) {

RNOngoingNotification.createOngoingConferenceNotificationChannel(currentActivity);

Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);

ComponentName componentName;

try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
componentName = context.startForegroundService(intent);
} else {
componentName = context.startService(intent);
}
} catch (RuntimeException e) {
// Avoid crashing due to ForegroundServiceStartNotAllowedException (API level 31).
// See: https://developer.android.com/guide/components/foreground-services#background-start-restrictions
JitsiMeetLogger.w(TAG + " Ongoing conference service not started", e);
return;
}
if (componentName == null) {
JitsiMeetLogger.w(TAG + " Ongoing conference service not started");
}
}

public static void launch(Context context, Activity currentActivity) {
List<String> permissionsList = new ArrayList<>();

PermissionListener listener = new PermissionListener() {
@Override
public boolean onRequestPermissionsResult(int i, String[] strings, int[] results) {
int counter = 0;

if (results.length > 0) {
for (int result : results) {
if (result == PackageManager.PERMISSION_GRANTED) {
counter++;
}
}

if (counter == results.length){
doLaunch(context, currentActivity);
JitsiMeetLogger.w(TAG + " Service launched, permissions were granted");
} else {
JitsiMeetLogger.w(TAG + " Couldn't launch service, permissions were not granted");
}
}

return true;
}
};

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionsList.add(POST_NOTIFICATIONS);
permissionsList.add(RECORD_AUDIO);
}

String[] permissionsArray = new String[ permissionsList.size() ];
permissionsArray = permissionsList.toArray( permissionsArray );

if (permissionsArray.length > 0) {
try {
currentActivity.requestPermissions(permissionsArray, PERMISSIONS_REQUEST_CODE);
} catch (Exception e) {
JitsiMeetLogger.e(e, "Error requesting permissions");
listener.onRequestPermissionsResult(PERMISSIONS_REQUEST_CODE, permissionsArray, new int[0]);
}
} else {
doLaunch(context, currentActivity);
JitsiMeetLogger.w(TAG + " Service launched");
}
}

public static void abort(Context context) {
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
context.stopService(intent);
}

@Override
public void onCreate() {
super.onCreate();
Notification notification = RNOngoingNotification.buildOngoingConferenceNotification(this);
if (notification == null) {
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
startForeground(NOTIFICATION_ID, notification);
}
}
}

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_NOT_STICKY;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext r
new AndroidSettingsModule(reactContext),
new AppInfoModule(reactContext),
new AudioModeModule(reactContext),
new JMOngoingConferenceModule(reactContext),
new JavaScriptSandboxModule(reactContext),
new LocaleDetector(reactContext),
new LogBridgeModule(reactContext),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.Random;
/**
* Helper class for creating the ongoing notification which is used with
* {@link JitsiMeetOngoingConferenceService}. It allows the user to easily get back to the app
* and to hangup from within the notification itself.
*/
class RNOngoingNotification {
private static final String TAG = RNOngoingNotification.class.getSimpleName();

static void createOngoingConferenceNotificationChannel(Activity currentActivity) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
if (currentActivity == null) {
JitsiMeetLogger.w(TAG + " Cannot create notification channel: no current context");
return;
}

NotificationManager notificationManager
= (NotificationManager) currentActivity.getSystemService(Context.NOTIFICATION_SERVICE);

NotificationChannel channel
= notificationManager.getNotificationChannel("OngoingConferenceChannel");

if (channel != null) {
// The channel was already created, no need to do it again.
return;
}

channel = new NotificationChannel("OngoingConferenceChannel", currentActivity.getString(R.string.ongoing_notification_channel_name), NotificationManager.IMPORTANCE_DEFAULT);
channel.enableLights(false);
channel.enableVibration(false);
channel.setShowBadge(false);

notificationManager.createNotificationChannel(channel);
}
static Notification buildOngoingConferenceNotification(Context context) {
if (context == null) {
JitsiMeetLogger.w(TAG + " Cannot create notification: no current context");
return null;
}

NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "OngoingConferenceChannel");

builder
.setCategory(NotificationCompat.CATEGORY_CALL)
.setContentTitle(context.getString(R.string.ongoing_notification_title))
.setContentText(context.getString(R.string.ongoing_notification_text))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setOngoing(true)
.setWhen(System.currentTimeMillis())
.setUsesChronometer(true)
.setAutoCancel(false)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOnlyAlertOnce(true)
.setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));

return builder.build();
}
}
22 changes: 22 additions & 0 deletions react/features/mobile/react-native-sdk/middleware.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { NativeModules, Platform } from 'react-native';

import { getAppProp } from '../../base/app/functions';
import {
CONFERENCE_BLURRED,
Expand All @@ -10,13 +12,15 @@ import {
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../../base/participants/actionTypes';
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
import { READY_TO_CLOSE } from '../external-api/actionTypes';
import { participantToParticipantInfo } from '../external-api/functions';
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture/actionTypes';

import { isExternalAPIAvailable } from './functions';

const externalAPIEnabled = isExternalAPIAvailable();
const { JMOngoingConference } = NativeModules;


/**
Expand Down Expand Up @@ -84,3 +88,21 @@ const externalAPIEnabled = isExternalAPIAvailable();

return result;
});

/**
* Before enabling media projection service control on Android,
* we need to check if native modules are being used or not.
*/
Platform.OS === 'android' && !externalAPIEnabled && StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, previousConference) => {
if (!conference) {
JMOngoingConference.abort();
} else if (conference && !previousConference) {
JMOngoingConference.launch();
} else if (conference !== previousConference) {
JMOngoingConference.abort();
JMOngoingConference.launch();
}
}
);

0 comments on commit 7da0850

Please sign in to comment.