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