diff --git a/android/app/build.gradle b/android/app/build.gradle
index 857b7942d4..e25a7d5918 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -15,11 +15,13 @@ def vcode = (int) (((new Date().getTime() / 1000) - 1546297200) / 10)
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
-
packagingOptions {
- exclude 'lib/*/libhermes*.so'
+ jniLibs {
+ excludes += ['lib/*/libhermes*.so']
+ }
}
+
defaultConfig {
applicationId 'org.jitsi.meet'
versionCode vcode
@@ -72,12 +74,13 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+ namespace 'org.jitsi.meet'
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.5.1'
- debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
+ debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13'
if (!rootProject.ext.libreBuild) {
// Sync with react-native-google-signin
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index cb76876615..04114e6fc9 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,6 +1,5 @@
+ xmlns:tools="http://schemas.android.com/tools">
@@ -13,6 +12,8 @@
+
+
+
+
-
\ No newline at end of file
+
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetMediaProjectionModule.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetMediaProjectionModule.java
new file mode 100644
index 0000000000..75af74ac46
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetMediaProjectionModule.java
@@ -0,0 +1,42 @@
+package org.jitsi.meet.sdk;
+
+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 = JitsiMeetMediaProjectionModule.NAME)
+class JitsiMeetMediaProjectionModule
+ extends ReactContextBaseJavaModule {
+
+ public static final String NAME = "JitsiMeetMediaProjectionModule";
+
+ public JitsiMeetMediaProjectionModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @ReactMethod
+ public void launch() {
+ Context context = getReactApplicationContext();
+
+ JitsiMeetMediaProjectionService.launch(context);
+ }
+
+ @ReactMethod
+ public void abort() {
+ Context context = getReactApplicationContext();
+
+ JitsiMeetMediaProjectionService.abort(context);
+ }
+
+ @NonNull
+ @Override
+ public String getName() {
+ return NAME;
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetMediaProjectionService.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetMediaProjectionService.java
new file mode 100644
index 0000000000..e197ff626e
--- /dev/null
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetMediaProjectionService.java
@@ -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.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.IBinder;
+
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
+
+
+/**
+ * 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 JitsiMeetMediaProjectionService extends Service {
+ private static final String TAG = JitsiMeetMediaProjectionService.class.getSimpleName();
+
+ public static void launch(Context context) {
+ OngoingNotification.createOngoingConferenceNotificationChannel();
+
+ Intent intent = new Intent(context, JitsiMeetMediaProjectionService.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, JitsiMeetMediaProjectionService.class);
+ context.stopService(intent);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ Notification notification = OngoingNotification.buildOngoingConferenceNotification(null);
+
+ if (notification == null) {
+ stopSelf();
+ JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ startForeground(OngoingNotification.NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
+ } else {
+ startForeground(OngoingNotification.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;
+ }
+}
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java
index c7d60fd84d..e26eee777c 100644
--- a/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java
@@ -23,6 +23,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
@@ -94,8 +95,11 @@ public class JitsiMeetOngoingConferenceService extends Service
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
- startForeground(OngoingNotification.NOTIFICATION_ID, notification);
- JitsiMeetLogger.i(TAG + " Service started");
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ startForeground(OngoingNotification.NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
+ } else {
+ startForeground(OngoingNotification.NOTIFICATION_ID, notification);
+ }
}
OngoingConferenceTracker.getInstance().addListener(this);
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/OngoingNotification.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/OngoingNotification.java
index ad76d97df1..5ed402457e 100644
--- a/android/sdk/src/main/java/org/jitsi/meet/sdk/OngoingNotification.java
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/OngoingNotification.java
@@ -73,7 +73,7 @@ class OngoingNotification {
notificationManager.createNotificationChannel(channel);
}
- static Notification buildOngoingConferenceNotification(boolean isMuted) {
+ static Notification buildOngoingConferenceNotification(Boolean isMuted) {
Context context = ReactInstanceManagerHolder.getCurrentActivity();
if (context == null) {
JitsiMeetLogger.w(TAG + " Cannot create notification: no current context");
@@ -92,7 +92,7 @@ class OngoingNotification {
builder
.setCategory(NotificationCompat.CATEGORY_CALL)
.setContentTitle(context.getString(R.string.ongoing_notification_title))
- .setContentText(context.getString(R.string.ongoing_notification_text))
+ .setContentText(isMuted != null ? context.getString(R.string.ongoing_notification_text) : context.getString(R.string.ongoing_notification_action_screenshare))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setOngoing(true)
@@ -103,6 +103,10 @@ class OngoingNotification {
.setOnlyAlertOnce(true)
.setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));
+ if (isMuted == null) {
+ return builder.build();
+ }
+
NotificationCompat.Action hangupAction = createAction(context, JitsiMeetOngoingConferenceService.Action.HANGUP, R.string.ongoing_notification_action_hang_up);
JitsiMeetOngoingConferenceService.Action toggleAudioAction = isMuted
diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java
index b7df98735c..d5bb08b579 100644
--- a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java
+++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java
@@ -67,6 +67,7 @@ class ReactInstanceManagerHolder {
new DropboxModule(reactContext),
new ExternalAPIModule(reactContext),
new JavaScriptSandboxModule(reactContext),
+ new JitsiMeetMediaProjectionModule(reactContext),
new LocaleDetector(reactContext),
new LogBridgeModule(reactContext),
new SplashScreenModule(reactContext),
diff --git a/android/sdk/src/main/res/values/strings.xml b/android/sdk/src/main/res/values/strings.xml
index d3ba7915c9..945ac3ad33 100644
--- a/android/sdk/src/main/res/values/strings.xml
+++ b/android/sdk/src/main/res/values/strings.xml
@@ -5,6 +5,7 @@
You are currently in a meeting. Tap to return to it.
Hang up
Mute
+ You are currently screen-sharing. Tap to return to the meeting.
Unmute
Ongoing Conference Notifications
diff --git a/package-lock.json b/package-lock.json
index 33d1b64172..04d692fcd0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -97,7 +97,7 @@
"react-native-svg-transformer": "1.1.0",
"react-native-tab-view": "3.5.2",
"react-native-url-polyfill": "2.0.0",
- "react-native-video": "6.0.0-alpha.7",
+ "react-native-video": "6.0.0-alpha.11",
"react-native-watch-connectivity": "1.1.0",
"react-native-webrtc": "118.0.0",
"react-native-webview": "13.5.1",
@@ -5574,11 +5574,6 @@
}
}
},
- "node_modules/@react-native/normalize-color": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@react-native/normalize-color/-/normalize-color-2.1.0.tgz",
- "integrity": "sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA=="
- },
"node_modules/@react-native/normalize-colors": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.72.0.tgz",
@@ -9335,16 +9330,6 @@
"node": ">= 0.6"
}
},
- "node_modules/deprecated-react-native-prop-types": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz",
- "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==",
- "dependencies": {
- "@react-native/normalize-color": "*",
- "invariant": "*",
- "prop-types": "*"
- }
- },
"node_modules/destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
@@ -12872,11 +12857,6 @@
"rollup": ">= 1.0.0"
}
},
- "node_modules/keymirror": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz",
- "integrity": "sha512-vIkZAFWoDijgQT/Nvl2AHCMmnegN2ehgTPYuyy2hWQkQSntI0S7ESYqdLkoSe1HyEBFHHkCgSIvVdSEiWwKvCg=="
- },
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -16953,13 +16933,12 @@
}
},
"node_modules/react-native-video": {
- "version": "6.0.0-alpha.7",
- "resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-6.0.0-alpha.7.tgz",
- "integrity": "sha512-X/siSaJf0V//IbnozjDm1jAjNaXlFy6Hbr6X8GNFl/ztLvN+Z8R/Quq9Q8o22XVwlPacPQ9VS/G0Stdktn0FEw==",
- "dependencies": {
- "deprecated-react-native-prop-types": "^2.2.0",
- "keymirror": "^0.1.1",
- "prop-types": "^15.7.2"
+ "version": "6.0.0-alpha.11",
+ "resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-6.0.0-alpha.11.tgz",
+ "integrity": "sha512-Z1FqIkNBqQWdBVKoh5WlmM01LIVhxlOsddKVV9IzMJ3EDl8PAU4ln7hdo85RHCHhgWSHzathPDo0UK7gPB48MA==",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
}
},
"node_modules/react-native-watch-connectivity": {
@@ -23970,11 +23949,6 @@
}
}
},
- "@react-native/normalize-color": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@react-native/normalize-color/-/normalize-color-2.1.0.tgz",
- "integrity": "sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA=="
- },
"@react-native/normalize-colors": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.72.0.tgz",
@@ -26841,16 +26815,6 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
- "deprecated-react-native-prop-types": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz",
- "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==",
- "requires": {
- "@react-native/normalize-color": "*",
- "invariant": "*",
- "prop-types": "*"
- }
- },
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
@@ -29474,11 +29438,6 @@
"debounce": "^1.2.0"
}
},
- "keymirror": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz",
- "integrity": "sha512-vIkZAFWoDijgQT/Nvl2AHCMmnegN2ehgTPYuyy2hWQkQSntI0S7ESYqdLkoSe1HyEBFHHkCgSIvVdSEiWwKvCg=="
- },
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -32442,14 +32401,9 @@
}
},
"react-native-video": {
- "version": "6.0.0-alpha.7",
- "resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-6.0.0-alpha.7.tgz",
- "integrity": "sha512-X/siSaJf0V//IbnozjDm1jAjNaXlFy6Hbr6X8GNFl/ztLvN+Z8R/Quq9Q8o22XVwlPacPQ9VS/G0Stdktn0FEw==",
- "requires": {
- "deprecated-react-native-prop-types": "^2.2.0",
- "keymirror": "^0.1.1",
- "prop-types": "^15.7.2"
- }
+ "version": "6.0.0-alpha.11",
+ "resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-6.0.0-alpha.11.tgz",
+ "integrity": "sha512-Z1FqIkNBqQWdBVKoh5WlmM01LIVhxlOsddKVV9IzMJ3EDl8PAU4ln7hdo85RHCHhgWSHzathPDo0UK7gPB48MA=="
},
"react-native-watch-connectivity": {
"version": "1.1.0",
diff --git a/package.json b/package.json
index d3b104e0c9..099424811f 100644
--- a/package.json
+++ b/package.json
@@ -103,7 +103,7 @@
"react-native-svg-transformer": "1.1.0",
"react-native-tab-view": "3.5.2",
"react-native-url-polyfill": "2.0.0",
- "react-native-video": "6.0.0-alpha.7",
+ "react-native-video": "6.0.0-alpha.11",
"react-native-watch-connectivity": "1.1.0",
"react-native-webrtc": "118.0.0",
"react-native-webview": "13.5.1",
diff --git a/react/features/base/tracks/actions.native.ts b/react/features/base/tracks/actions.native.ts
index 8a34955e80..700cc7c6b7 100644
--- a/react/features/base/tracks/actions.native.ts
+++ b/react/features/base/tracks/actions.native.ts
@@ -1,3 +1,5 @@
+import { NativeModules, Platform } from 'react-native';
+
import { IReduxState, IStore } from '../../app/types';
import { setPictureInPictureEnabled } from '../../mobile/picture-in-picture/functions';
import { showNotification } from '../../notifications/actions';
@@ -12,6 +14,8 @@ import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
import { addLocalTrack, replaceLocalTrack } from './actions.any';
import { getLocalDesktopTrack, getTrackState, isLocalVideoTrackDesktop } from './functions.native';
+const { JitsiMeetMediaProjectionModule } = NativeModules;
+
export * from './actions.any';
/**
@@ -31,7 +35,10 @@ export function toggleScreensharing(enabled: boolean, _ignore1?: boolean, _ignor
if (!isSharing) {
_startScreenSharing(dispatch, state);
+ Platform.OS === 'android' && JitsiMeetMediaProjectionModule.launch();
}
+
+ Platform.OS === 'android' && JitsiMeetMediaProjectionModule.abort();
} else {
dispatch(setScreenshareMuted(true));
dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.SCREEN_SHARE));
@@ -77,7 +84,7 @@ async function _startScreenSharing(dispatch: IStore['dispatch'], state: IReduxSt
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
} catch (error: any) {
- console.log('ERROR creating ScreeSharing stream ', error);
+ console.log('ERROR creating screen-sharing stream ', error);
setPictureInPictureEnabled(true);
}
diff --git a/react/features/shared-video/components/native/VideoManager.tsx b/react/features/shared-video/components/native/VideoManager.tsx
index 900bc6c8cd..9b85d76a86 100644
--- a/react/features/shared-video/components/native/VideoManager.tsx
+++ b/react/features/shared-video/components/native/VideoManager.tsx
@@ -19,7 +19,7 @@ interface IState {
* Manager of shared video.
*/
class VideoManager extends AbstractVideoManager {
- playerRef: RefObject