• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
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 com.android.settings.connecteddevice.audiosharing;
18 
19 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
20 
21 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE;
22 
23 import android.Manifest;
24 import android.app.ActivityManager;
25 import android.app.Notification;
26 import android.app.NotificationChannel;
27 import android.app.NotificationManager;
28 import android.app.PendingIntent;
29 import android.app.settings.SettingsEnums;
30 import android.bluetooth.BluetoothCsipSetCoordinator;
31 import android.bluetooth.BluetoothDevice;
32 import android.content.BroadcastReceiver;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.pm.PackageManager;
36 import android.os.Bundle;
37 import android.text.TextUtils;
38 import android.util.Log;
39 
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 import androidx.core.app.NotificationCompat;
43 
44 import com.android.settings.R;
45 import com.android.settings.bluetooth.Utils;
46 import com.android.settings.overlay.FeatureFactory;
47 import com.android.settingslib.bluetooth.BluetoothUtils;
48 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
49 import com.android.settingslib.bluetooth.LocalBluetoothManager;
50 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
51 import com.android.settingslib.flags.Flags;
52 
53 import com.google.common.collect.ImmutableList;
54 
55 import java.util.List;
56 import java.util.Map;
57 
58 public class AudioSharingReceiver extends BroadcastReceiver {
59     private static final String TAG = "AudioSharingReceiver";
60     private static final String ACTION_LE_AUDIO_SHARING_SETTINGS =
61             "com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS";
62     private static final String ACTION_LE_AUDIO_SHARING_STOP =
63             "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STOP";
64     private static final String ACTION_LE_AUDIO_SHARING_ADD_SOURCE =
65             "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_ADD_SOURCE";
66     private static final String ACTION_LE_AUDIO_SHARING_CANCEL_NOTIF =
67             "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_CANCEL_NOTIF";
68     private static final String EXTRA_NOTIF_ID = "NOTIF_ID";
69     private static final String CHANNEL_ID = "bluetooth_notification_channel";
70     private static final int AUDIO_SHARING_NOTIFICATION_ID =
71             com.android.settingslib.R.drawable.ic_bt_le_audio_sharing;
72     private static final int ADD_SOURCE_NOTIFICATION_ID = R.string.share_audio_notification_title;
73     private static final int NOTIF_AUTO_DISMISS_MILLIS = 300000; //5mins
74 
75     @Override
onReceive(@onNull Context context, @NonNull Intent intent)76     public void onReceive(@NonNull Context context, @NonNull Intent intent) {
77         String action = intent.getAction();
78         if (action == null) {
79             Log.w(TAG, "Received unexpected intent with null action.");
80             return;
81         }
82         MetricsFeatureProvider metricsFeatureProvider =
83                 FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
84         switch (action) {
85             case LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_STATE_CHANGE:
86                 int state =
87                         intent.getIntExtra(
88                                 LocalBluetoothLeBroadcast.EXTRA_LE_AUDIO_SHARING_STATE, -1);
89                 if (state == LocalBluetoothLeBroadcast.BROADCAST_STATE_ON) {
90                     if (!BluetoothUtils.isAudioSharingUIAvailable(context)) {
91                         Log.w(TAG, "Skip showSharingNotification, feature disabled.");
92                         return;
93                     }
94                     showSharingNotification(context);
95                     metricsFeatureProvider.action(
96                             context, SettingsEnums.ACTION_SHOW_AUDIO_SHARING_NOTIFICATION);
97                 } else if (state == LocalBluetoothLeBroadcast.BROADCAST_STATE_OFF) {
98                     // TODO: check BluetoothUtils#isAudioSharingEnabled() till BluetoothAdapter#
99                     //       isLeAudioBroadcastSourceSupported() and BluetoothAdapter#
100                     //       isLeAudioBroadcastAssistantSupported() always return FEATURE_SUPPORTED
101                     //       or FEATURE_NOT_SUPPORTED when BT and BLE off
102                     cancelSharingNotification(context, AUDIO_SHARING_NOTIFICATION_ID);
103                     metricsFeatureProvider.action(
104                             context, SettingsEnums.ACTION_CANCEL_AUDIO_SHARING_NOTIFICATION,
105                             LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_STATE_CHANGE);
106                     cancelSharingNotification(context, ADD_SOURCE_NOTIFICATION_ID);
107                     // TODO: add metric
108                 } else {
109                     Log.w(
110                             TAG,
111                             "Skip handling ACTION_LE_AUDIO_SHARING_STATE_CHANGE, invalid extras.");
112                 }
113                 break;
114             case ACTION_LE_AUDIO_SHARING_STOP:
115                 if (BluetoothUtils.isAudioSharingUIAvailable(context)) {
116                     LocalBluetoothManager manager = Utils.getLocalBtManager(context);
117                     if (BluetoothUtils.isBroadcasting(manager)) {
118                         AudioSharingUtils.stopBroadcasting(manager);
119                         metricsFeatureProvider.action(
120                                 context, SettingsEnums.ACTION_STOP_AUDIO_SHARING_FROM_NOTIFICATION);
121                         return;
122                     }
123                 }
124                 Log.w(TAG, "cancelSharingNotification, feature disabled or not in broadcast.");
125                 // TODO: check BluetoothUtils#isAudioSharingEnabled() till BluetoothAdapter#
126                 //       isLeAudioBroadcastSourceSupported() and BluetoothAdapter#
127                 //       isLeAudioBroadcastAssistantSupported() always return FEATURE_SUPPORTED
128                 //       or FEATURE_NOT_SUPPORTED when BT and BLE off
129                 cancelSharingNotification(context, AUDIO_SHARING_NOTIFICATION_ID);
130                 metricsFeatureProvider.action(
131                         context, SettingsEnums.ACTION_CANCEL_AUDIO_SHARING_NOTIFICATION,
132                         ACTION_LE_AUDIO_SHARING_STOP);
133                 cancelSharingNotification(context, ADD_SOURCE_NOTIFICATION_ID);
134                 break;
135             case LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED:
136                 if (!Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()
137                         || !BluetoothUtils.isAudioSharingUIAvailable(context)) {
138                     Log.d(TAG, "Skip ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED, flag/feature off");
139                     return;
140                 }
141                 BluetoothDevice device = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE,
142                         BluetoothDevice.class);
143                 if (device == null) {
144                     Log.d(TAG, "Skip ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED, null device");
145                     return;
146                 }
147                 if (isAppInForeground(context)) {
148                     Log.d(TAG, "App in foreground, show share audio dialog");
149                     Intent dialogIntent = new Intent();
150                     dialogIntent.setClass(context, AudioSharingJoinHandlerActivity.class);
151                     dialogIntent.addFlags(FLAG_ACTIVITY_NEW_TASK);
152                     dialogIntent.putExtra(EXTRA_BLUETOOTH_DEVICE, device);
153                     context.startActivity(dialogIntent);
154                 } else {
155                     Log.d(TAG, "App not in foreground, show share audio notification");
156                     LocalBluetoothManager manager = Utils.getLocalBtManager(context);
157                     if (!validToAddSource(device, action, manager).isEmpty()) {
158                         showAddSourceNotification(context, device);
159                     }
160                 }
161                 break;
162             case ACTION_LE_AUDIO_SHARING_ADD_SOURCE:
163                 if (!Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()
164                         || !BluetoothUtils.isAudioSharingUIAvailable(context)) {
165                     Log.d(TAG, "Skip ACTION_LE_AUDIO_SHARING_ADD_SOURCE, flag/feature off");
166                     cancelSharingNotification(context, ADD_SOURCE_NOTIFICATION_ID);
167                     return;
168                 }
169                 BluetoothDevice sink = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE,
170                         BluetoothDevice.class);
171                 LocalBluetoothManager manager = Utils.getLocalBtManager(context);
172                 ImmutableList<BluetoothDevice> sinksToAdd = validToAddSource(sink, action, manager);
173                 AudioSharingUtils.addSourceToTargetSinks(sinksToAdd, manager);
174                 cancelSharingNotification(context, ADD_SOURCE_NOTIFICATION_ID);
175                 break;
176             case ACTION_LE_AUDIO_SHARING_CANCEL_NOTIF:
177                 int notifId = intent.getIntExtra(EXTRA_NOTIF_ID, -1);
178                 if (notifId != -1) {
179                     cancelSharingNotification(context, notifId);
180                 }
181                 break;
182             default:
183                 Log.w(TAG, "Received unexpected intent " + action);
184         }
185     }
186 
validToAddSource(@ullable BluetoothDevice sink, @NonNull String action, @Nullable LocalBluetoothManager btManager)187     private ImmutableList<BluetoothDevice> validToAddSource(@Nullable BluetoothDevice sink,
188             @NonNull String action, @Nullable LocalBluetoothManager btManager) {
189         if (sink == null) {
190             Log.d(TAG, "Skip " + action + ", null device");
191             return ImmutableList.of();
192         }
193         boolean isBroadcasting = BluetoothUtils.isBroadcasting(btManager);
194         if (!isBroadcasting) {
195             Log.d(TAG, "Skip " + action + ", not broadcasting");
196             return ImmutableList.of();
197         }
198         Map<Integer, List<BluetoothDevice>> groupedDevices =
199                 AudioSharingUtils.fetchConnectedDevicesByGroupId(btManager);
200         int groupId = groupedDevices.entrySet().stream().filter(
201                 entry -> entry.getValue().contains(sink)).findFirst().map(
202                 Map.Entry::getKey).orElse(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
203         if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
204             Log.d(TAG, "Skip " + action + ", no valid group id");
205             return ImmutableList.of();
206         }
207         List<BluetoothDevice> sinksToAdd = groupedDevices.getOrDefault(groupId,
208                 ImmutableList.of()).stream().filter(
209                     d -> !BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(d,
210                         btManager)).toList();
211         if (sinksToAdd.isEmpty()) {
212             Log.d(TAG, "Skip " + action + ", already has source");
213             return ImmutableList.of();
214         } else if (groupedDevices.entrySet().stream().filter(
215                 entry -> entry.getKey() != groupId && entry.getValue().stream().anyMatch(
216                         d -> BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(d,
217                                 btManager))).toList().size() >= 2) {
218             Log.d(TAG, "Skip " + action + ", already 2 sinks");
219             return ImmutableList.of();
220         }
221         return ImmutableList.copyOf(sinksToAdd);
222     }
223 
showSharingNotification(@onNull Context context)224     private void showSharingNotification(@NonNull Context context) {
225         NotificationManager nm = context.getSystemService(NotificationManager.class);
226         if (nm == null) return;
227         createNotificationChannelIfNeeded(nm, context);
228         Intent stopIntent =
229                 new Intent(ACTION_LE_AUDIO_SHARING_STOP).setPackage(context.getPackageName());
230         PendingIntent stopPendingIntent =
231                 PendingIntent.getBroadcast(
232                         context,
233                         R.string.audio_sharing_stop_button_label,
234                         stopIntent,
235                         PendingIntent.FLAG_IMMUTABLE);
236         Intent settingsIntent =
237                 new Intent(ACTION_LE_AUDIO_SHARING_SETTINGS)
238                         .setPackage(context.getPackageName())
239                         .putExtra(
240                                 MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,
241                                 SettingsEnums.NOTIFICATION_AUDIO_SHARING);
242         PendingIntent settingsPendingIntent =
243                 PendingIntent.getActivity(
244                         context,
245                         R.string.audio_sharing_settings_button_label,
246                         settingsIntent,
247                         PendingIntent.FLAG_IMMUTABLE);
248         NotificationCompat.Action stopAction =
249                 new NotificationCompat.Action.Builder(
250                         0,
251                         context.getString(R.string.audio_sharing_stop_button_label),
252                         stopPendingIntent)
253                         .build();
254         NotificationCompat.Action settingsAction =
255                 new NotificationCompat.Action.Builder(
256                         0,
257                         context.getString(R.string.audio_sharing_settings_button_label),
258                         settingsPendingIntent)
259                         .build();
260         final Bundle extras = new Bundle();
261         extras.putString(
262                 Notification.EXTRA_SUBSTITUTE_APP_NAME,
263                 context.getString(R.string.audio_sharing_title));
264         NotificationCompat.Builder builder =
265                 new NotificationCompat.Builder(context, CHANNEL_ID)
266                         .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
267                         .setLocalOnly(true)
268                         .setContentTitle(
269                                 context.getString(R.string.audio_sharing_notification_title))
270                         .setContentText(
271                                 context.getString(R.string.audio_sharing_notification_content))
272                         .setOngoing(true)
273                         .setSilent(true)
274                         .setColor(
275                                 context.getColor(
276                                         com.android.internal.R.color
277                                                 .system_notification_accent_color))
278                         .setContentIntent(settingsPendingIntent)
279                         .addAction(stopAction)
280                         .addAction(settingsAction)
281                         .addExtras(extras);
282         nm.notify(AUDIO_SHARING_NOTIFICATION_ID, builder.build());
283     }
284 
showAddSourceNotification(@onNull Context context, @NonNull BluetoothDevice device)285     private void showAddSourceNotification(@NonNull Context context,
286             @NonNull BluetoothDevice device) {
287         NotificationManager nm = context.getSystemService(NotificationManager.class);
288         if (nm == null) return;
289         createNotificationChannelIfNeeded(nm, context);
290         Intent addSourceIntent =
291                 new Intent(ACTION_LE_AUDIO_SHARING_ADD_SOURCE).setPackage(context.getPackageName())
292                         .putExtra(EXTRA_BLUETOOTH_DEVICE, device);
293         // Use PendingIntent.FLAG_UPDATE_CURRENT here because intent extra (device) could be updated
294         PendingIntent addSourcePendingIntent =
295                 PendingIntent.getBroadcast(
296                         context,
297                         R.string.audio_sharing_share_button_label,
298                         addSourceIntent,
299                         PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT
300                                 | PendingIntent.FLAG_IMMUTABLE);
301         NotificationCompat.Action addSourceAction =
302                 new NotificationCompat.Action.Builder(
303                         0,
304                         context.getString(R.string.audio_sharing_share_button_label),
305                         addSourcePendingIntent)
306                         .build();
307         Intent cancelIntent = new Intent(ACTION_LE_AUDIO_SHARING_CANCEL_NOTIF).setPackage(
308                         context.getPackageName())
309                 .putExtra(EXTRA_NOTIF_ID, ADD_SOURCE_NOTIFICATION_ID);
310         PendingIntent cancelPendingIntent =
311                 PendingIntent.getBroadcast(
312                         context,
313                         R.string.cancel,
314                         cancelIntent,
315                         PendingIntent.FLAG_IMMUTABLE);
316         NotificationCompat.Action cancelAction =
317                 new NotificationCompat.Action.Builder(
318                         0,
319                         context.getString(R.string.cancel),
320                         cancelPendingIntent)
321                         .build();
322         final Bundle extras = new Bundle();
323         extras.putString(
324                 Notification.EXTRA_SUBSTITUTE_APP_NAME,
325                 context.getString(R.string.audio_sharing_title));
326         String deviceName = device.getAlias();
327         if (TextUtils.isEmpty(deviceName)) {
328             deviceName = device.getAddress();
329         }
330         NotificationCompat.Builder builder =
331                 new NotificationCompat.Builder(context, CHANNEL_ID)
332                         .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
333                         .setLocalOnly(true)
334                         .setContentTitle(context.getString(R.string.share_audio_notification_title,
335                                 deviceName))
336                         .setContentText(
337                                 context.getString(R.string.audio_sharing_notification_content))
338                         .setOngoing(true)
339                         .setSilent(true)
340                         .setColor(
341                                 context.getColor(
342                                         com.android.internal.R.color
343                                                 .system_notification_accent_color))
344                         .addAction(addSourceAction)
345                         .addAction(cancelAction)
346                         .setTimeoutAfter(NOTIF_AUTO_DISMISS_MILLIS)
347                         .addExtras(extras);
348         nm.notify(ADD_SOURCE_NOTIFICATION_ID, builder.build());
349     }
350 
cancelSharingNotification(@onNull Context context, int notifId)351     private void cancelSharingNotification(@NonNull Context context, int notifId) {
352         NotificationManager nm = context.getSystemService(NotificationManager.class);
353         if (nm != null) {
354             nm.cancel(notifId);
355         }
356     }
357 
createNotificationChannelIfNeeded(@onNull NotificationManager nm, @NonNull Context context)358     private void createNotificationChannelIfNeeded(@NonNull NotificationManager nm,
359             @NonNull Context context) {
360         if (nm.getNotificationChannel(CHANNEL_ID) == null) {
361             Log.d(TAG, "Create bluetooth notification channel");
362             NotificationChannel notificationChannel =
363                     new NotificationChannel(
364                             CHANNEL_ID,
365                             context.getString(com.android.settings.R.string.bluetooth),
366                             NotificationManager.IMPORTANCE_HIGH);
367             nm.createNotificationChannel(notificationChannel);
368         }
369     }
370 
isAppInForeground(@onNull Context context)371     private boolean isAppInForeground(@NonNull Context context) {
372         try {
373             ActivityManager activityManager = context.getSystemService(ActivityManager.class);
374             String packageName = context.getPackageName();
375             if (context.getPackageManager().checkPermission(Manifest.permission.PACKAGE_USAGE_STATS,
376                     packageName) != PackageManager.PERMISSION_GRANTED) {
377                 Log.d(TAG, "check isAppInForeground, returns false due to no permission");
378                 return false;
379             }
380             if (packageName != null && activityManager.getPackageImportance(packageName)
381                     == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
382                 Log.d(TAG, "check isAppInForeground, returns true");
383                 return true;
384             }
385         } catch (RuntimeException e) {
386             Log.d(TAG, "check isAppInForeground, error = " + e.getMessage());
387         }
388         return false;
389     }
390 }
391