浏览代码

android: add notification while there is an ongoing meeting

The notification is posted by a foreground service, which also has the nice
side-effect of keeping the app alive for a long time.
j8
Saúl Ibarra Corretgé 5 年前
父节点
当前提交
714e0e045d

二进制
android/app/src/main/res/drawable-hdpi/ic_notification.png 查看文件


二进制
android/app/src/main/res/drawable-mdpi/ic_notification.png 查看文件


二进制
android/app/src/main/res/drawable-xhdpi/ic_notification.png 查看文件


二进制
android/app/src/main/res/drawable-xxhdpi/ic_notification.png 查看文件


二进制
android/app/src/main/res/drawable-xxxhdpi/ic_notification.png 查看文件


+ 3
- 0
android/sdk/src/main/AndroidManifest.xml 查看文件

@@ -12,6 +12,7 @@
12 12
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
13 13
     <uses-permission android:name="android.permission.WAKE_LOCK" />
14 14
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
15
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
15 16
 
16 17
     <uses-feature
17 18
         android:glEsVersion="0x00020000"
@@ -44,6 +45,8 @@
44 45
                 <action android:name="android.telecom.ConnectionService" />
45 46
             </intent-filter>
46 47
         </service>
48
+
49
+        <service android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService" />
47 50
     </application>
48 51
 
49 52
 </manifest>

+ 1
- 1
android/sdk/src/main/java/org/jitsi/meet/sdk/ExternalAPIModule.java 查看文件

@@ -68,7 +68,7 @@ class ExternalAPIModule
68 68
     @ReactMethod
69 69
     public void sendEvent(String name, ReadableMap data, String scope) {
70 70
         // Keep track of the current ongoing conference.
71
-        OngoingConferenceTracker.onExternalAPIEvent(name, data);
71
+        OngoingConferenceTracker.getInstance().onExternalAPIEvent(name, data);
72 72
 
73 73
         // The JavaScript App needs to provide uniquely identifying information
74 74
         // to the native ExternalAPI module so that the latter may match the

+ 1
- 1
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeet.java 查看文件

@@ -42,7 +42,7 @@ public class JitsiMeet {
42 42
      * @return the current conference URL.
43 43
      */
44 44
     public static String getCurrentConference() {
45
-        return OngoingConferenceTracker.getCurrentConference();
45
+        return OngoingConferenceTracker.getInstance().getCurrentConference();
46 46
     }
47 47
 
48 48
     /**

+ 13
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java 查看文件

@@ -73,7 +73,18 @@ public class JitsiMeetActivity extends FragmentActivity
73 73
 
74 74
     @Override
75 75
     public void onDestroy() {
76
+        // Here we are trying to handle the following corner case: an application using the SDK
77
+        // is using this Activity for displaying meetings, but there is another "main" Activity
78
+        // with other content. If this Activity is "swiped out" from the recent list we will get
79
+        // Activity#onDestroy() called without warning. At this point we can try to leave the
80
+        // current meeting, but when our view is detached from React the JS <-> Native bridge won't
81
+        // be operational so the external API won't be able to notify the native side that the
82
+        // conference terminated. Thus, try our best to clean up.
76 83
         leave();
84
+        if (AudioModeModule.useConnectionService()) {
85
+            ConnectionService.abortConnections();
86
+        }
87
+        JitsiMeetOngoingConferenceService.abort(this);
77 88
 
78 89
         super.onDestroy();
79 90
     }
@@ -198,6 +209,8 @@ public class JitsiMeetActivity extends FragmentActivity
198 209
     @Override
199 210
     public void onConferenceJoined(Map<String, Object> data) {
200 211
         Log.d(TAG, "Conference joined: " + data);
212
+        // Launch the service for the ongoing notification.
213
+        JitsiMeetOngoingConferenceService.launch(this);
201 214
     }
202 215
 
203 216
     @Override

+ 109
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java 查看文件

@@ -0,0 +1,109 @@
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
+
17
+package org.jitsi.meet.sdk;
18
+
19
+import android.app.Notification;
20
+import android.app.Service;
21
+import android.content.ComponentName;
22
+import android.content.Context;
23
+import android.content.Intent;
24
+import android.os.IBinder;
25
+import android.util.Log;
26
+
27
+
28
+/**
29
+ * This class implements an Android {@link Service}, a foreground one specifically, and it's
30
+ * responsible for presenting an ongoing notification when a conference is in progress.
31
+ * The service will help keep the app running while in the background.
32
+ *
33
+ * See: https://developer.android.com/guide/components/services
34
+ */
35
+public class JitsiMeetOngoingConferenceService extends Service
36
+        implements OngoingConferenceTracker.OngoingConferenceListener {
37
+    private static final String TAG = JitsiMeetOngoingConferenceService.class.getSimpleName();
38
+
39
+    static final class Actions {
40
+        static final String START = TAG + ":START";
41
+        static final String HANGUP = TAG + ":HANGUP";
42
+    }
43
+
44
+    static void launch(Context context) {
45
+        OngoingNotification.createOngoingConferenceNotificationChannel();
46
+
47
+        Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
48
+        intent.setAction(Actions.START);
49
+
50
+        ComponentName componentName = context.startService(intent);
51
+        if (componentName == null) {
52
+            Log.w(TAG, "Ongoing conference service not started");
53
+        }
54
+    }
55
+
56
+    static void abort(Context context) {
57
+        Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
58
+        context.stopService(intent);
59
+    }
60
+
61
+    @Override
62
+    public void onCreate() {
63
+        super.onCreate();
64
+
65
+        OngoingConferenceTracker.getInstance().addListener(this);
66
+    }
67
+
68
+    @Override
69
+    public void onDestroy() {
70
+        OngoingConferenceTracker.getInstance().removeListener(this);
71
+
72
+        super.onDestroy();
73
+    }
74
+
75
+    @Override
76
+    public IBinder onBind(Intent intent) {
77
+        return null;
78
+    }
79
+
80
+    @Override
81
+    public int onStartCommand(Intent intent, int flags, int startId) {
82
+        final String action = intent.getAction();
83
+        if (action.equals(Actions.START)) {
84
+            Notification notification = OngoingNotification.buildOngoingConferenceNotification();
85
+            startForeground(OngoingNotification.NOTIFICATION_ID, notification);
86
+            Log.i(TAG, "Service started");
87
+        } else if (action.equals(Actions.HANGUP)) {
88
+            Log.i(TAG, "Hangup requested");
89
+            // Abort all ongoing calls
90
+            if (AudioModeModule.useConnectionService()) {
91
+                ConnectionService.abortConnections();
92
+            }
93
+            stopSelf();
94
+        } else {
95
+            Log.w(TAG, "Unknown action received: " + action);
96
+            stopSelf();
97
+        }
98
+
99
+        return START_NOT_STICKY;
100
+    }
101
+
102
+    @Override
103
+    public void onCurrentConferenceChanged(String conferenceUrl) {
104
+        if (conferenceUrl == null) {
105
+            stopSelf();
106
+            Log.i(TAG, "Service stopped");
107
+        }
108
+    }
109
+}

+ 2
- 2
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetView.java 查看文件

@@ -108,12 +108,12 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
108 108
             throw new RuntimeException("Enclosing Activity must implement JitsiMeetActivityInterface");
109 109
         }
110 110
 
111
-        OngoingConferenceTracker.addListener(this);
111
+        OngoingConferenceTracker.getInstance().addListener(this);
112 112
     }
113 113
 
114 114
     @Override
115 115
     public void dispose() {
116
-        OngoingConferenceTracker.removeListener(this);
116
+        OngoingConferenceTracker.getInstance().removeListener(this);
117 117
         super.dispose();
118 118
     }
119 119
 

+ 37
- 13
android/sdk/src/main/java/org/jitsi/meet/sdk/OngoingConferenceTracker.java 查看文件

@@ -22,47 +22,71 @@ import java.util.Collection;
22 22
 import java.util.Collections;
23 23
 import java.util.HashSet;
24 24
 
25
+
26
+/**
27
+ * Helper class to keep track of what the current conference is.
28
+ */
25 29
 class OngoingConferenceTracker {
26
-    private static final Collection<OngoingConferenceListener> listeners =
30
+    private static final OngoingConferenceTracker instance = new OngoingConferenceTracker();
31
+
32
+    private static final String CONFERENCE_WILL_JOIN = "CONFERENCE_WILL_JOIN";
33
+    private static final String CONFERENCE_TERMINATED = "CONFERENCE_TERMINATED";
34
+
35
+    private final Collection<OngoingConferenceListener> listeners =
27 36
         Collections.synchronizedSet(new HashSet<OngoingConferenceListener>());
28
-    private static String currentConference;
37
+    private String currentConference;
38
+
39
+    public OngoingConferenceTracker() {
40
+    }
41
+
42
+    public static OngoingConferenceTracker getInstance() {
43
+        return instance;
44
+    }
29 45
 
30
-    static synchronized String getCurrentConference() {
46
+    /**
47
+     * Gets the current active conference URL.
48
+     *
49
+     * @return - The current conference URL as a String.
50
+     */
51
+    synchronized String getCurrentConference() {
31 52
         return currentConference;
32 53
     }
33 54
 
34
-    static synchronized void onExternalAPIEvent(String name, ReadableMap data) {
55
+    synchronized void onExternalAPIEvent(String name, ReadableMap data) {
35 56
         if (!data.hasKey("url")) {
36 57
             return;
37 58
         }
38 59
 
39 60
         String url = data.getString("url");
61
+        if (url == null) {
62
+            return;
63
+        }
40 64
 
41 65
         switch(name) {
42
-            case "CONFERENCE_WILL_JOIN":
66
+            case CONFERENCE_WILL_JOIN:
43 67
                 currentConference = url;
44
-                updateCurrentConference();
68
+                updateListeners();
45 69
                 break;
46 70
 
47
-            case "CONFERENCE_TERMINATED":
48
-                if (currentConference != null && url.equals(currentConference)) {
71
+            case CONFERENCE_TERMINATED:
72
+                if (url.equals(currentConference)) {
49 73
                     currentConference = null;
50
-                    updateCurrentConference();
74
+                    updateListeners();
51 75
                 }
52 76
                 break;
53 77
         }
54 78
     }
55 79
 
56
-    static void addListener(OngoingConferenceListener listener) {
80
+    void addListener(OngoingConferenceListener listener) {
57 81
         listeners.add(listener);
58 82
     }
59 83
 
60
-    static void removeListener(OngoingConferenceListener listener) {
84
+    void removeListener(OngoingConferenceListener listener) {
61 85
         listeners.remove(listener);
62 86
     }
63 87
 
64
-    private static synchronized void updateCurrentConference() {
65
-        for (OngoingConferenceListener listener: listeners) {
88
+    private void updateListeners() {
89
+        for (OngoingConferenceListener listener : listeners) {
66 90
             listener.onCurrentConferenceChanged(currentConference);
67 91
         }
68 92
     }

+ 118
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/OngoingNotification.java 查看文件

@@ -0,0 +1,118 @@
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
+
17
+package org.jitsi.meet.sdk;
18
+
19
+import android.app.Notification;
20
+import android.app.NotificationChannel;
21
+import android.app.NotificationManager;
22
+import android.app.PendingIntent;
23
+import android.content.Context;
24
+import android.content.Intent;
25
+import android.os.Build;
26
+import android.support.v4.app.NotificationCompat;
27
+import android.util.Log;
28
+
29
+import java.util.Random;
30
+
31
+
32
+/**
33
+ * Helper class for creating the ongoing notification which is used with
34
+ * {@link JitsiMeetOngoingConferenceService}. It allows the user to easily get back to the app
35
+ * and to hangup from within the notification itself.
36
+ */
37
+class OngoingNotification {
38
+    private static final String TAG = OngoingNotification.class.getSimpleName();
39
+
40
+    private static final String CHANNEL_ID = "JitsiNotificationChannel";
41
+    private static final String CHANNEL_NAME = "Ongoing Conference Notifications";
42
+
43
+    static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
44
+
45
+
46
+    static void createOngoingConferenceNotificationChannel() {
47
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
48
+            return;
49
+        }
50
+
51
+        Context context = ReactInstanceManagerHolder.getCurrentActivity();
52
+        if (context == null) {
53
+            Log.w(TAG, "Cannot create notification channel: no current context");
54
+            return;
55
+        }
56
+
57
+        NotificationManager notificationManager
58
+            = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
59
+
60
+        NotificationChannel channel
61
+            = notificationManager.getNotificationChannel(CHANNEL_ID);
62
+        if (channel != null) {
63
+            // The channel was already created, no need to do it again.
64
+            return;
65
+        }
66
+
67
+        channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
68
+        channel.enableLights(false);
69
+        channel.enableVibration(false);
70
+        channel.setShowBadge(false);
71
+
72
+        notificationManager.createNotificationChannel(channel);
73
+    }
74
+
75
+    static Notification buildOngoingConferenceNotification() {
76
+        Context context = ReactInstanceManagerHolder.getCurrentActivity();
77
+        if (context == null) {
78
+            Log.w(TAG, "Cannot create notification: no current context");
79
+            return null;
80
+        }
81
+
82
+        Intent notificationIntent = new Intent(context, context.getClass());
83
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
84
+
85
+        NotificationCompat.Builder builder;
86
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
87
+            builder = new NotificationCompat.Builder(context, CHANNEL_ID);
88
+        } else {
89
+            builder = new NotificationCompat.Builder(context);
90
+        }
91
+
92
+        builder
93
+            .setCategory(NotificationCompat.CATEGORY_CALL)
94
+            .setContentTitle(context.getString(R.string.ongoing_notification_title))
95
+            .setContentText(context.getString(R.string.ongoing_notification_text))
96
+            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
97
+            .setContentIntent(pendingIntent)
98
+            .setOngoing(true)
99
+            .setAutoCancel(false)
100
+            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
101
+            .setUsesChronometer(true)
102
+            .setOnlyAlertOnce(true)
103
+            .setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));
104
+
105
+        // Add a "hang-up" action only if we are using ConnectionService.
106
+        if (AudioModeModule.useConnectionService()) {
107
+            Intent hangupIntent = new Intent(context, JitsiMeetOngoingConferenceService.class);
108
+            hangupIntent.setAction(JitsiMeetOngoingConferenceService.Actions.HANGUP);
109
+            PendingIntent hangupPendingIntent
110
+                = PendingIntent.getService(context, 0, hangupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
111
+            NotificationCompat.Action hangupAction = new NotificationCompat.Action(0, "Hang up", hangupPendingIntent);
112
+
113
+            builder.addAction(hangupAction);
114
+        }
115
+
116
+        return builder.build();
117
+    }
118
+}

+ 2
- 0
android/sdk/src/main/res/values/strings.xml 查看文件

@@ -1,4 +1,6 @@
1 1
 <resources>
2 2
     <string name="app_name">Jitsi Meet SDK</string>
3 3
     <string name="dropbox_app_key"></string>
4
+    <string name="ongoing_notification_title">Ongoing meeting</string>
5
+    <string name="ongoing_notification_text">You are currently in a meeting. Tap to return to it.</string>
4 6
 </resources>

正在加载...
取消
保存