Skip to content

Commit

Permalink
sdk(react-native-sdk): rnsdk screenshare android fix (jitsi#13884)
Browse files Browse the repository at this point in the history
sdk(react-native-sdk): rnsdk screenshare android fix
  • Loading branch information
Calinteodor authored Nov 7, 2023
1 parent 0e55cbb commit 8a4990d
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ tsconfig.json
#
react-native-sdk/*.tgz
react-native-sdk/android/src
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JitsiMeetReactNativePackage.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
7 changes: 7 additions & 0 deletions react-native-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ cd ios && pod install && cd ..
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
```
- In `android/app/src/main/AndroidManifest.xml`, under the `</application>` tag, include
```xml
<service
android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService"
android:foregroundServiceType="mediaProjection" />
```
This will take care of the screen share feature.

If you want to test all the steps before applying them to your app, you can check our React Native SDK sample app here:
https://github.com/jitsi/jitsi-meet-sdk-samples/tree/master/react-native
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.jitsi.meet.sdk;

import android.app.Notification;
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;


@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();
Context activityContext = context.getCurrentActivity();

JitsiMeetOngoingConferenceService.launch(context, activityContext);
}

@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,100 @@
/*
* 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.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;

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

import java.util.HashMap;

/**
* 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();

public static void launch(Context context, Context activityContext) {

RNOngoingNotification.createOngoingConferenceNotificationChannel(activityContext);

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 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 {
startForeground(RNOngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
}
}

@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,97 @@
/*
* 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.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 final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;

static void createOngoingConferenceNotificationChannel(Context activityContext) {

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}

if (activityContext == null) {
JitsiMeetLogger.w(TAG + " Cannot create notification channel: no current context");
return;
}

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

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

channel = new NotificationChannel("JitsiOngoingConferenceChannel", activityContext.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, "JitsiOngoingConferenceChannel");

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();
}
}
26 changes: 26 additions & 0 deletions react-native-sdk/prepare_sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ const packageJSON = require('../package.json');
const SDKPackageJSON = require('./package.json');

const androidSourcePath = '../android/sdk/src/main/java/org/jitsi/meet/sdk';
const androidMainSourcePath = '../android/sdk/src/main/res';
const androidTargetPath = './android/src/main/java/org/jitsi/meet/sdk';
const androidMainTargetPath = './android/src/main/res';
const iosSrcPath = '../ios/sdk/src';
const iosDestPath = './ios/src';

Expand Down Expand Up @@ -169,6 +171,30 @@ copyFolderRecursiveSync(
`${androidSourcePath}/log`,
`${androidTargetPath}/log`
);
copyFolderRecursiveSync(
`${androidMainSourcePath}/values`,
`${androidMainTargetPath}`
);
copyFolderRecursiveSync(
`${androidMainSourcePath}/drawable-hdpi`,
`${androidMainTargetPath}`
);
copyFolderRecursiveSync(
`${androidMainSourcePath}/drawable-mdpi`,
`${androidMainTargetPath}`
);
copyFolderRecursiveSync(
`${androidMainSourcePath}/drawable-xhdpi`,
`${androidMainTargetPath}`
);
copyFolderRecursiveSync(
`${androidMainSourcePath}/drawable-xxhdpi`,
`${androidMainTargetPath}`
);
copyFolderRecursiveSync(
`${androidMainSourcePath}/drawable-xxxhdpi`,
`${androidMainTargetPath}`
);
copyFolderRecursiveSync(
`${androidSourcePath}/net`,
`${androidTargetPath}/log`
Expand Down
2 changes: 1 addition & 1 deletion react/features/mobile/react-native-sdk/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NativeModules } from 'react-native';


/**
* Determimes if the ExternalAPI native module is available.
* Determines if the ExternalAPI native module is available.
*
* @returns {boolean} If yes {@code true} otherwise {@code false}.
*/
Expand Down
26 changes: 24 additions & 2 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 } from 'react-native';

import { getAppProp } from '../../base/app/functions';
import {
CONFERENCE_BLURRED,
Expand All @@ -8,16 +10,20 @@ import {
} from '../../base/conference/actionTypes';
import { PARTICIPANT_JOINED } 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;


/**
* Check if native modules are being used or not. If not then the init of middleware doesn't happen.
* Check if native modules are being used or not.
* If not, then the init of middleware doesn't happen.
*/
!externalAPIEnabled && MiddlewareRegistry.register(store => next => action => {
const result = next(action);
Expand Down Expand Up @@ -56,5 +62,21 @@ const externalAPIEnabled = isExternalAPIAvailable();
}

return result;
}
});

/**
* Check if native modules are being used or not.
*/
!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 8a4990d

Please sign in to comment.