Procházet zdrojové kódy

feat(react-native-sdk/android): force permissions approval in order to launch RNOngoingNotification (#15400)

Re-added visibility control for ongoing conference and media projection notifications on our React Native SDK.
factor2
Calinteodor před 5 měsíci
rodič
revize
b890aa33c3
Žádný účet není propojen s e-mailovou adresou tvůrce revize

+ 3
- 0
.gitignore Zobrazit soubor

@@ -101,6 +101,9 @@ tsconfig.json
101 101
 react-native-sdk/*.tgz
102 102
 react-native-sdk/android/src
103 103
 !react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JitsiMeetReactNativePackage.java
104
+!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java
105
+!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JMOngoingConferenceModule.java
106
+!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/RNOngoingNotification.java
104 107
 react-native-sdk/images
105 108
 react-native-sdk/ios
106 109
 react-native-sdk/lang

+ 117
- 0
react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JMOngoingConferenceModule.java Zobrazit soubor

@@ -0,0 +1,117 @@
1
+package org.jitsi.meet.sdk;
2
+
3
+import static android.Manifest.permission.POST_NOTIFICATIONS;
4
+import static android.Manifest.permission.RECORD_AUDIO;
5
+
6
+import android.content.Context;
7
+import android.content.pm.PackageManager;
8
+import android.os.Build;
9
+
10
+import androidx.annotation.NonNull;
11
+import androidx.annotation.RequiresApi;
12
+
13
+import com.facebook.react.ReactActivity;
14
+
15
+import com.facebook.react.bridge.ReactApplicationContext;
16
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
17
+import com.facebook.react.bridge.ReactMethod;
18
+import com.facebook.react.module.annotations.ReactModule;
19
+import com.facebook.react.modules.core.PermissionListener;
20
+
21
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
22
+
23
+import java.util.ArrayList;
24
+import java.util.Arrays;
25
+import java.util.List;
26
+
27
+
28
+/**
29
+ * This class implements a ReactModule and it's
30
+ * responsible for launching/aborting a service when a conference is in progress.
31
+ */
32
+@ReactModule(name = JMOngoingConferenceModule.NAME)
33
+class JMOngoingConferenceModule extends ReactContextBaseJavaModule {
34
+
35
+    public static final String NAME = "JMOngoingConference";
36
+
37
+    private static final int PERMISSIONS_REQUEST_CODE = (int) (Math.random() * Short.MAX_VALUE);
38
+
39
+    public JMOngoingConferenceModule(ReactApplicationContext reactContext) {
40
+        super(reactContext);
41
+    }
42
+
43
+    @ReactMethod
44
+    public void launch() {
45
+        Context context = getReactApplicationContext();
46
+        ReactActivity reactActivity = (ReactActivity) getCurrentActivity();
47
+
48
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
49
+            JMOngoingConferenceService.launch(context);
50
+
51
+            JitsiMeetLogger.i(NAME + " launch");
52
+
53
+            return;
54
+        }
55
+
56
+        PermissionListener listener = new PermissionListener() {
57
+            @Override
58
+            public boolean onRequestPermissionsResult(int i, String[] strings, int[] results) {
59
+                JitsiMeetLogger.i(NAME + " Permission callback received");
60
+
61
+                if (results == null || results.length == 0) {
62
+                    JitsiMeetLogger.w(NAME + " Permission results are null or empty");
63
+                    return true;
64
+                }
65
+
66
+                int counter = 0;
67
+                for (int result : results) {
68
+                    if (result == PackageManager.PERMISSION_GRANTED) {
69
+                        counter++;
70
+                    }
71
+                }
72
+
73
+                JitsiMeetLogger.i(NAME + " Permissions granted: " + counter + "/" + results.length);
74
+
75
+                if (counter == results.length) {
76
+                    JitsiMeetLogger.i(NAME + " All permissions granted, launching service");
77
+                    JMOngoingConferenceService.launch(context);
78
+                } else {
79
+                    JitsiMeetLogger.w(NAME + " Not all permissions were granted");
80
+                }
81
+
82
+                return true;
83
+            }
84
+        };
85
+
86
+        JitsiMeetLogger.i(NAME + " Checking Tiramisu permissions");
87
+
88
+        List<String> permissionsList = new ArrayList<>();
89
+
90
+        permissionsList.add(POST_NOTIFICATIONS);
91
+        permissionsList.add(RECORD_AUDIO);
92
+
93
+        String[] permissionsArray = new String[ permissionsList.size() ];
94
+        permissionsArray = permissionsList.toArray( permissionsArray );
95
+
96
+        try {
97
+            JitsiMeetLogger.i(NAME + " Requesting permissions: " + Arrays.toString(permissionsArray));
98
+            reactActivity.requestPermissions(permissionsArray, PERMISSIONS_REQUEST_CODE, listener);
99
+        } catch (Exception e) {
100
+            JitsiMeetLogger.e(e, NAME + " Error requesting permissions");
101
+        }
102
+    }
103
+
104
+    @ReactMethod
105
+    public void abort() {
106
+        Context context = getReactApplicationContext();
107
+        JMOngoingConferenceService.abort(context);
108
+
109
+        JitsiMeetLogger.i(NAME + " abort");
110
+    }
111
+
112
+    @NonNull
113
+    @Override
114
+    public String getName() {
115
+        return NAME;
116
+    }
117
+}

+ 107
- 0
react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JMOngoingConferenceService.java Zobrazit soubor

@@ -0,0 +1,107 @@
1
+/*
2
+ * Copyright @ 2019-present 8x8, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *     http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+package org.jitsi.meet.sdk;
17
+
18
+import android.app.Notification;
19
+import android.app.Service;
20
+
21
+import android.content.Context;
22
+import android.content.Intent;
23
+
24
+import android.content.pm.ServiceInfo;
25
+
26
+import android.os.Build;
27
+import android.os.IBinder;
28
+
29
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
30
+
31
+import java.util.Random;
32
+
33
+/**
34
+ * This class implements an Android {@link Service}, a foreground one specifically, and it's
35
+ * responsible for presenting an ongoing notification when a conference is in progress.
36
+ * The service will help keep the app running while in the background.
37
+ *
38
+ * See: https://developer.android.com/guide/components/services
39
+ */
40
+public class JMOngoingConferenceService extends Service {
41
+    private static final String TAG = JMOngoingConferenceService.class.getSimpleName();
42
+
43
+    static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
44
+
45
+    public static void launch(Context context) {
46
+        try {
47
+            RNOngoingNotification.createOngoingConferenceNotificationChannel(context);
48
+            JitsiMeetLogger.i(TAG + " Notification channel creation completed");
49
+        } catch (Exception e) {
50
+            JitsiMeetLogger.e(e, TAG + " Error creating notification channel");
51
+        }
52
+
53
+        Intent intent = new Intent(context, JMOngoingConferenceService.class);
54
+
55
+        try {
56
+            context.startForegroundService(intent);
57
+            JitsiMeetLogger.i(TAG + " Starting foreground service");
58
+        } catch (RuntimeException e) {
59
+            // Avoid crashing due to ForegroundServiceStartNotAllowedException (API level 31).
60
+            // See: https://developer.android.com/guide/components/foreground-services#background-start-restrictions
61
+            JitsiMeetLogger.w(TAG + " Ongoing conference service not started", e);
62
+        }
63
+    }
64
+
65
+    public static void abort(Context context) {
66
+        Intent intent = new Intent(context, JMOngoingConferenceService.class);
67
+        context.stopService(intent);
68
+    }
69
+
70
+    @Override
71
+    public void onCreate() {
72
+        super.onCreate();
73
+        JitsiMeetLogger.i(TAG + " onCreate called");
74
+
75
+        try {
76
+            Notification notification = RNOngoingNotification.buildOngoingConferenceNotification(this);
77
+            JitsiMeetLogger.i(TAG + " Notification build result: " + (notification != null));
78
+
79
+            if (notification == null) {
80
+                stopSelf();
81
+                JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
82
+            } else {
83
+                JitsiMeetLogger.i(TAG + " Starting service in foreground with notification");
84
+
85
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
86
+                    startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
87
+                } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
88
+                    startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
89
+                } else {
90
+                    startForeground(NOTIFICATION_ID, notification);
91
+                }
92
+            }
93
+        } catch (Exception e) {
94
+            JitsiMeetLogger.e(e, TAG + " Error in onCreate");
95
+        }
96
+    }
97
+
98
+    @Override
99
+    public IBinder onBind(Intent intent) {
100
+        return null;
101
+    }
102
+
103
+    @Override
104
+    public int onStartCommand(Intent intent, int flags, int startId) {
105
+        return START_NOT_STICKY;
106
+    }
107
+}

+ 1
- 0
react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JitsiMeetReactNativePackage.java Zobrazit soubor

@@ -21,6 +21,7 @@ public class JitsiMeetReactNativePackage implements ReactPackage {
21 21
                 new AndroidSettingsModule(reactContext),
22 22
                 new AppInfoModule(reactContext),
23 23
                 new AudioModeModule(reactContext),
24
+                new JMOngoingConferenceModule(reactContext),
24 25
                 new JavaScriptSandboxModule(reactContext),
25 26
                 new LocaleDetector(reactContext),
26 27
                 new LogBridgeModule(reactContext),

+ 108
- 0
react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/RNOngoingNotification.java Zobrazit soubor

@@ -0,0 +1,108 @@
1
+/*
2
+ * Copyright @ 2019-present 8x8, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *     http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+package org.jitsi.meet.sdk;
17
+
18
+import android.app.Notification;
19
+import android.app.NotificationChannel;
20
+import android.app.NotificationManager;
21
+
22
+import android.app.PendingIntent;
23
+import android.app.Service;
24
+import android.content.Context;
25
+
26
+import android.content.Intent;
27
+import android.os.Build;
28
+
29
+import androidx.core.app.NotificationCompat;
30
+
31
+import org.jitsi.meet.sdk.log.JitsiMeetLogger;
32
+
33
+
34
+/**
35
+ * Helper class for creating the ongoing notification which is used with
36
+ * {@link JMOngoingConferenceService}. It allows the user to easily get back to the app
37
+ * and to hangup from within the notification itself.
38
+ */
39
+class RNOngoingNotification {
40
+    private static final String TAG = RNOngoingNotification.class.getSimpleName();
41
+
42
+    static final String RN_ONGOING_CONFERENCE_CHANNEL_ID = "OngoingConferenceChannel";
43
+
44
+    static void createOngoingConferenceNotificationChannel(Context context) {
45
+        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Service.NOTIFICATION_SERVICE);
46
+        NotificationChannel channel = notificationManager.getNotificationChannel(RN_ONGOING_CONFERENCE_CHANNEL_ID);
47
+
48
+        if (channel != null) {
49
+            JitsiMeetLogger.i(TAG + " Notification channel already exists");
50
+            return;
51
+        }
52
+
53
+        channel = new NotificationChannel(
54
+            RN_ONGOING_CONFERENCE_CHANNEL_ID,
55
+            context.getString(R.string.ongoing_notification_channel_name),
56
+            NotificationManager.IMPORTANCE_DEFAULT
57
+        );
58
+
59
+        channel.enableVibration(true);
60
+        channel.setShowBadge(true);
61
+        channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
62
+
63
+        notificationManager.createNotificationChannel(channel);
64
+        JitsiMeetLogger.i(TAG + " Notification channel created with importance: " + channel.getImportance());
65
+    }
66
+
67
+    static Notification buildOngoingConferenceNotification(Context context) {
68
+        if (context == null) {
69
+            JitsiMeetLogger.w(TAG + " Cannot create notification: no current context");
70
+            return null;
71
+        }
72
+
73
+        JitsiMeetLogger.i(TAG + " Creating notification with context: " + context);
74
+
75
+        // Creating an intent to launch app's main activity
76
+        Intent intent = context.getPackageManager()
77
+            .getLaunchIntentForPackage(context.getPackageName());
78
+        assert intent != null;
79
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
80
+
81
+        // Creating PendingIntent
82
+        PendingIntent pendingIntent = PendingIntent.getActivity(
83
+            context,
84
+            0,
85
+            intent,
86
+            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
87
+        );
88
+
89
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, RN_ONGOING_CONFERENCE_CHANNEL_ID);
90
+
91
+        builder
92
+            .setCategory(NotificationCompat.CATEGORY_CALL)
93
+            .setContentTitle(context.getString(R.string.ongoing_notification_title))
94
+            .setContentText(context.getString(R.string.ongoing_notification_text))
95
+            .setPriority(NotificationCompat.PRIORITY_HIGH)
96
+            .setOngoing(true)
97
+            .setWhen(System.currentTimeMillis())
98
+            .setUsesChronometer(true)
99
+            .setAutoCancel(false)
100
+            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
101
+            .setOnlyAlertOnce(true)
102
+            .setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()))
103
+            .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
104
+            .setContentIntent(pendingIntent);
105
+
106
+        return builder.build();
107
+    }
108
+}

+ 1
- 0
react-native-sdk/package.json Zobrazit soubor

@@ -36,6 +36,7 @@
36 36
         "moment-duration-format": "0.0.0",
37 37
         "optional-require": "0.0.0",
38 38
         "promise.allsettled": "0.0.0",
39
+        "promise.withresolvers": "0.0.0",
39 40
         "punycode": "0.0.0",
40 41
         "react-emoji-render": "0.0.0",
41 42
         "react-i18next": "0.0.0",

+ 22
- 0
react/features/mobile/react-native-sdk/middleware.js Zobrazit soubor

@@ -1,3 +1,5 @@
1
+import { NativeModules } from 'react-native';
2
+
1 3
 import { getAppProp } from '../../base/app/functions';
2 4
 import {
3 5
     CONFERENCE_BLURRED,
@@ -10,6 +12,7 @@ import {
10 12
 import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
11 13
 import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../../base/participants/actionTypes';
12 14
 import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
15
+import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
13 16
 import { READY_TO_CLOSE } from '../external-api/actionTypes';
14 17
 import { participantToParticipantInfo } from '../external-api/functions';
15 18
 import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture/actionTypes';
@@ -17,6 +20,7 @@ import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture/actionTypes';
17 20
 import { isExternalAPIAvailable } from './functions';
18 21
 
19 22
 const externalAPIEnabled = isExternalAPIAvailable();
23
+const { JMOngoingConference } = NativeModules;
20 24
 
21 25
 
22 26
 /**
@@ -84,3 +88,21 @@ const externalAPIEnabled = isExternalAPIAvailable();
84 88
 
85 89
     return result;
86 90
 });
91
+
92
+/**
93
+ * Before enabling media projection service control on Android,
94
+ * we need to check if native modules are being used or not.
95
+ */
96
+JMOngoingConference && !externalAPIEnabled && StateListenerRegistry.register(
97
+    state => state['features/base/conference'].conference,
98
+    (conference, previousConference) => {
99
+        if (!conference) {
100
+            JMOngoingConference.abort();
101
+        } else if (conference && !previousConference) {
102
+            JMOngoingConference.launch();
103
+        } else if (conference !== previousConference) {
104
+            JMOngoingConference.abort();
105
+            JMOngoingConference.launch();
106
+        }
107
+    }
108
+);

Načítá se…
Zrušit
Uložit