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