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.audiostreams; 18 19 import android.app.Notification; 20 import android.app.NotificationChannel; 21 import android.app.NotificationManager; 22 import android.app.Service; 23 import android.app.settings.SettingsEnums; 24 import android.bluetooth.BluetoothAdapter; 25 import android.bluetooth.BluetoothDevice; 26 import android.bluetooth.BluetoothLeBroadcastReceiveState; 27 import android.bluetooth.BluetoothProfile; 28 import android.bluetooth.BluetoothVolumeControl; 29 import android.content.Intent; 30 import android.media.MediaMetadata; 31 import android.media.session.MediaSession; 32 import android.media.session.PlaybackState; 33 import android.os.Bundle; 34 import android.os.IBinder; 35 import android.util.Log; 36 37 import androidx.annotation.IntRange; 38 import androidx.annotation.NonNull; 39 import androidx.annotation.Nullable; 40 import androidx.annotation.VisibleForTesting; 41 42 import com.android.settings.R; 43 import com.android.settings.bluetooth.Utils; 44 import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils; 45 import com.android.settings.overlay.FeatureFactory; 46 import com.android.settingslib.bluetooth.BluetoothCallback; 47 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 48 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 49 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; 50 import com.android.settingslib.bluetooth.LocalBluetoothManager; 51 import com.android.settingslib.bluetooth.VolumeControlProfile; 52 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 53 54 import java.util.ArrayList; 55 import java.util.concurrent.ExecutorService; 56 import java.util.concurrent.Executors; 57 58 public class AudioStreamMediaService extends Service { 59 static final String BROADCAST_ID = "audio_stream_media_service_broadcast_id"; 60 static final String BROADCAST_TITLE = "audio_stream_media_service_broadcast_title"; 61 static final String DEVICES = "audio_stream_media_service_devices"; 62 private static final String TAG = "AudioStreamMediaService"; 63 private static final int NOTIFICATION_ID = 1; 64 private static final int BROADCAST_CONTENT_TEXT = R.string.audio_streams_listening_now; 65 private static final String LEAVE_BROADCAST_ACTION = "leave_broadcast_action"; 66 private static final String LEAVE_BROADCAST_TEXT = "Leave Broadcast"; 67 private static final String CHANNEL_ID = "bluetooth_notification_channel"; 68 private static final String DEFAULT_DEVICE_NAME = ""; 69 private static final int STATIC_PLAYBACK_DURATION = 100; 70 private static final int STATIC_PLAYBACK_POSITION = 30; 71 private static final int ZERO_PLAYBACK_SPEED = 0; 72 private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback = 73 new AudioStreamsBroadcastAssistantCallback() { 74 @Override 75 public void onSourceLost(int broadcastId) { 76 super.onSourceLost(broadcastId); 77 if (broadcastId == mBroadcastId) { 78 stopSelf(); 79 } 80 } 81 82 @Override 83 public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { 84 super.onSourceRemoved(sink, sourceId, reason); 85 if (mAudioStreamsHelper != null 86 && mAudioStreamsHelper.getAllConnectedSources().stream() 87 .map(BluetoothLeBroadcastReceiveState::getBroadcastId) 88 .noneMatch(id -> id == mBroadcastId)) { 89 stopSelf(); 90 } 91 } 92 }; 93 94 private final BluetoothCallback mBluetoothCallback = 95 new BluetoothCallback() { 96 @Override 97 public void onBluetoothStateChanged(int bluetoothState) { 98 if (BluetoothAdapter.STATE_OFF == bluetoothState) { 99 stopSelf(); 100 } 101 } 102 103 @Override 104 public void onProfileConnectionStateChanged( 105 @NonNull CachedBluetoothDevice cachedDevice, 106 @ConnectionState int state, 107 int bluetoothProfile) { 108 if (state == BluetoothAdapter.STATE_DISCONNECTED 109 && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT 110 && mDevices != null) { 111 mDevices.remove(cachedDevice.getDevice()); 112 cachedDevice 113 .getMemberDevice() 114 .forEach( 115 m -> { 116 // Check nullability to pass NullAway check 117 if (mDevices != null) { 118 mDevices.remove(m.getDevice()); 119 } 120 }); 121 } 122 if (mDevices == null || mDevices.isEmpty()) { 123 stopSelf(); 124 } 125 } 126 }; 127 128 private final BluetoothVolumeControl.Callback mVolumeControlCallback = 129 new BluetoothVolumeControl.Callback() { 130 @Override 131 public void onDeviceVolumeChanged( 132 @NonNull BluetoothDevice device, 133 @IntRange(from = -255, to = 255) int volume) { 134 if (mDevices == null || mDevices.isEmpty()) { 135 Log.w(TAG, "active device or device has source is null!"); 136 return; 137 } 138 if (mDevices.contains(device)) { 139 Log.d( 140 TAG, 141 "onDeviceVolumeChanged() bluetoothDevice : " 142 + device 143 + " volume: " 144 + volume); 145 if (volume == 0) { 146 mIsMuted = true; 147 } else { 148 mIsMuted = false; 149 mLatestPositiveVolume = volume; 150 } 151 if (mLocalSession != null) { 152 mLocalSession.setPlaybackState(getPlaybackState()); 153 if (mNotificationManager != null) { 154 mNotificationManager.notify(NOTIFICATION_ID, buildNotification()); 155 } 156 } 157 } 158 } 159 }; 160 161 private final PlaybackState.Builder mPlayStatePlayingBuilder = 162 new PlaybackState.Builder() 163 .setActions(PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_SEEK_TO) 164 .setState( 165 PlaybackState.STATE_PLAYING, 166 STATIC_PLAYBACK_POSITION, 167 ZERO_PLAYBACK_SPEED) 168 .addCustomAction( 169 LEAVE_BROADCAST_ACTION, 170 LEAVE_BROADCAST_TEXT, 171 com.android.settings.R.drawable.ic_clear); 172 private final PlaybackState.Builder mPlayStatePausingBuilder = 173 new PlaybackState.Builder() 174 .setActions(PlaybackState.ACTION_PLAY | PlaybackState.ACTION_SEEK_TO) 175 .setState( 176 PlaybackState.STATE_PAUSED, 177 STATIC_PLAYBACK_POSITION, 178 ZERO_PLAYBACK_SPEED) 179 .addCustomAction( 180 LEAVE_BROADCAST_ACTION, 181 LEAVE_BROADCAST_TEXT, 182 com.android.settings.R.drawable.ic_clear); 183 184 private final MetricsFeatureProvider mMetricsFeatureProvider = 185 FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); 186 private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); 187 private int mBroadcastId; 188 @Nullable private ArrayList<BluetoothDevice> mDevices; 189 @Nullable private LocalBluetoothManager mLocalBtManager; 190 @Nullable private AudioStreamsHelper mAudioStreamsHelper; 191 @Nullable private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; 192 @Nullable private VolumeControlProfile mVolumeControl; 193 @Nullable private NotificationManager mNotificationManager; 194 195 // Set 25 as default as the volume range from `VolumeControlProfile` is from 0 to 255. 196 // If the initial volume from `onDeviceVolumeChanged` is larger than zero (not muted), we will 197 // override this value. Otherwise, we raise the volume to 25 when the play button is clicked. 198 private int mLatestPositiveVolume = 25; 199 private boolean mIsMuted = false; 200 @VisibleForTesting @Nullable MediaSession mLocalSession; 201 202 @Override onCreate()203 public void onCreate() { 204 if (!AudioSharingUtils.isFeatureEnabled()) { 205 return; 206 } 207 208 super.onCreate(); 209 mLocalBtManager = Utils.getLocalBtManager(this); 210 if (mLocalBtManager == null) { 211 Log.w(TAG, "onCreate() : mLocalBtManager is null!"); 212 return; 213 } 214 215 mAudioStreamsHelper = new AudioStreamsHelper(mLocalBtManager); 216 mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); 217 if (mLeBroadcastAssistant == null) { 218 Log.w(TAG, "onCreate() : mLeBroadcastAssistant is null!"); 219 return; 220 } 221 222 mNotificationManager = getSystemService(NotificationManager.class); 223 if (mNotificationManager == null) { 224 Log.w(TAG, "onCreate() : notificationManager is null!"); 225 return; 226 } 227 228 if (mNotificationManager.getNotificationChannel(CHANNEL_ID) == null) { 229 NotificationChannel notificationChannel = 230 new NotificationChannel( 231 CHANNEL_ID, 232 getString(com.android.settings.R.string.bluetooth), 233 NotificationManager.IMPORTANCE_HIGH); 234 mNotificationManager.createNotificationChannel(notificationChannel); 235 } 236 237 mLocalBtManager.getEventManager().registerCallback(mBluetoothCallback); 238 239 mVolumeControl = mLocalBtManager.getProfileManager().getVolumeControlProfile(); 240 if (mVolumeControl != null) { 241 mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback); 242 } 243 244 mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); 245 } 246 247 @Override onDestroy()248 public void onDestroy() { 249 super.onDestroy(); 250 251 if (!AudioSharingUtils.isFeatureEnabled()) { 252 return; 253 } 254 if (mLocalBtManager != null) { 255 mLocalBtManager.getEventManager().unregisterCallback(mBluetoothCallback); 256 } 257 if (mLeBroadcastAssistant != null) { 258 mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); 259 } 260 if (mVolumeControl != null) { 261 mVolumeControl.unregisterCallback(mVolumeControlCallback); 262 } 263 if (mLocalSession != null) { 264 mLocalSession.release(); 265 mLocalSession = null; 266 } 267 } 268 269 @Override onStartCommand(@ullable Intent intent, int flags, int startId)270 public int onStartCommand(@Nullable Intent intent, int flags, int startId) { 271 Log.d(TAG, "onStartCommand()"); 272 273 mBroadcastId = intent != null ? intent.getIntExtra(BROADCAST_ID, -1) : -1; 274 if (mBroadcastId == -1) { 275 Log.w(TAG, "Invalid broadcast ID. Service will not start."); 276 stopSelf(); 277 return START_NOT_STICKY; 278 } 279 280 if (intent != null) { 281 mDevices = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class); 282 } 283 if (mDevices == null || mDevices.isEmpty()) { 284 Log.w(TAG, "No device. Service will not start."); 285 stopSelf(); 286 return START_NOT_STICKY; 287 } 288 if (intent != null) { 289 createLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE)); 290 startForeground(NOTIFICATION_ID, buildNotification()); 291 } 292 293 return START_NOT_STICKY; 294 } 295 createLocalMediaSession(String title)296 private void createLocalMediaSession(String title) { 297 mLocalSession = new MediaSession(this, TAG); 298 mLocalSession.setMetadata( 299 new MediaMetadata.Builder() 300 .putString(MediaMetadata.METADATA_KEY_TITLE, title) 301 .putLong(MediaMetadata.METADATA_KEY_DURATION, STATIC_PLAYBACK_DURATION) 302 .build()); 303 mLocalSession.setActive(true); 304 mLocalSession.setPlaybackState(getPlaybackState()); 305 mLocalSession.setCallback( 306 new MediaSession.Callback() { 307 public void onSeekTo(long pos) { 308 Log.d(TAG, "onSeekTo: " + pos); 309 if (mLocalSession != null) { 310 mLocalSession.setPlaybackState(getPlaybackState()); 311 if (mNotificationManager != null) { 312 mNotificationManager.notify(NOTIFICATION_ID, buildNotification()); 313 } 314 } 315 } 316 317 @Override 318 public void onPause() { 319 if (mDevices == null || mDevices.isEmpty()) { 320 Log.w(TAG, "active device or device has source is null!"); 321 return; 322 } 323 Log.d( 324 TAG, 325 "onPause() setting volume for device : " 326 + mDevices.get(0) 327 + " volume: " 328 + 0); 329 if (mVolumeControl != null) { 330 mVolumeControl.setDeviceVolume(mDevices.get(0), 0, true); 331 mMetricsFeatureProvider.action( 332 getApplicationContext(), 333 SettingsEnums 334 .ACTION_AUDIO_STREAM_NOTIFICATION_MUTE_BUTTON_CLICK, 335 1); 336 } 337 } 338 339 @Override 340 public void onPlay() { 341 if (mDevices == null || mDevices.isEmpty()) { 342 Log.w(TAG, "active device or device has source is null!"); 343 return; 344 } 345 Log.d( 346 TAG, 347 "onPlay() setting volume for device : " 348 + mDevices.get(0) 349 + " volume: " 350 + mLatestPositiveVolume); 351 if (mVolumeControl != null) { 352 mVolumeControl.setDeviceVolume( 353 mDevices.get(0), mLatestPositiveVolume, true); 354 } 355 mMetricsFeatureProvider.action( 356 getApplicationContext(), 357 SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_MUTE_BUTTON_CLICK, 358 0); 359 } 360 361 @Override 362 public void onCustomAction(@NonNull String action, Bundle extras) { 363 Log.d(TAG, "onCustomAction: " + action); 364 if (action.equals(LEAVE_BROADCAST_ACTION) && mAudioStreamsHelper != null) { 365 mAudioStreamsHelper.removeSource(mBroadcastId); 366 mMetricsFeatureProvider.action( 367 getApplicationContext(), 368 SettingsEnums 369 .ACTION_AUDIO_STREAM_NOTIFICATION_LEAVE_BUTTON_CLICK); 370 } 371 } 372 }); 373 } 374 getPlaybackState()375 private PlaybackState getPlaybackState() { 376 return mIsMuted ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build(); 377 } 378 getDeviceName()379 private String getDeviceName() { 380 if (mDevices == null || mDevices.isEmpty() || mLocalBtManager == null) { 381 return DEFAULT_DEVICE_NAME; 382 } 383 384 CachedBluetoothDeviceManager manager = mLocalBtManager.getCachedDeviceManager(); 385 if (manager == null) { 386 return DEFAULT_DEVICE_NAME; 387 } 388 389 CachedBluetoothDevice device = manager.findDevice(mDevices.get(0)); 390 return device != null ? device.getName() : DEFAULT_DEVICE_NAME; 391 } 392 buildNotification()393 private Notification buildNotification() { 394 String deviceName = getDeviceName(); 395 Notification.MediaStyle mediaStyle = 396 new Notification.MediaStyle() 397 .setMediaSession( 398 mLocalSession != null ? mLocalSession.getSessionToken() : null); 399 if (deviceName != null && !deviceName.isEmpty()) { 400 mediaStyle.setRemotePlaybackInfo( 401 deviceName, com.android.settingslib.R.drawable.ic_bt_le_audio, null); 402 } 403 Notification.Builder notificationBuilder = 404 new Notification.Builder(this, CHANNEL_ID) 405 .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing) 406 .setStyle(mediaStyle) 407 .setContentText(getString(BROADCAST_CONTENT_TEXT)) 408 .setSilent(true); 409 return notificationBuilder.build(); 410 } 411 412 @Nullable 413 @Override onBind(Intent intent)414 public IBinder onBind(Intent intent) { 415 return null; 416 } 417 } 418