1 /** 2 * Copyright (C) 2022 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.systemui.bluetooth; 18 19 import android.annotation.CallbackExecutor; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.app.Dialog; 23 import android.bluetooth.BluetoothLeBroadcast; 24 import android.bluetooth.BluetoothLeBroadcastMetadata; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.Looper; 30 import android.util.Log; 31 import android.view.View; 32 import android.view.Window; 33 import android.widget.Button; 34 import android.widget.TextView; 35 36 import com.android.internal.annotations.VisibleForTesting; 37 import com.android.internal.logging.UiEvent; 38 import com.android.internal.logging.UiEventLogger; 39 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; 40 import com.android.settingslib.bluetooth.LocalBluetoothManager; 41 import com.android.settingslib.media.MediaOutputConstants; 42 import com.android.systemui.broadcast.BroadcastSender; 43 import com.android.systemui.dagger.qualifiers.Background; 44 import com.android.systemui.media.controls.util.MediaDataUtils; 45 import com.android.systemui.media.dialog.MediaOutputDialogManager; 46 import com.android.systemui.res.R; 47 import com.android.systemui.statusbar.phone.SystemUIDialog; 48 49 import dagger.assisted.Assisted; 50 import dagger.assisted.AssistedFactory; 51 import dagger.assisted.AssistedInject; 52 53 import java.util.HashSet; 54 import java.util.Set; 55 import java.util.concurrent.Executor; 56 57 /** 58 * Dialog for showing le audio broadcasting dialog. 59 */ 60 public class BroadcastDialogDelegate implements SystemUIDialog.Delegate { 61 62 private static final String TAG = "BroadcastDialog"; 63 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 64 private static final int HANDLE_BROADCAST_FAILED_DELAY = 3000; 65 private static final String CURRENT_BROADCAST_APP = "current_broadcast_app"; 66 private static final String OUTPUT_PKG_NAME = "output_pkg_name"; 67 68 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 69 70 private final Context mContext; 71 private final UiEventLogger mUiEventLogger; 72 private final MediaOutputDialogManager mMediaOutputDialogManager; 73 private final LocalBluetoothManager mLocalBluetoothManager; 74 private final BroadcastSender mBroadcastSender; 75 private final SystemUIDialog.Factory mSystemUIDialogFactory; 76 private final String mCurrentBroadcastApp; 77 private final String mOutputPackageName; 78 private final Executor mBgExecutor; 79 private boolean mShouldLaunchLeBroadcastDialog; 80 private Button mSwitchBroadcast; 81 82 private final Set<SystemUIDialog> mDialogs = new HashSet<>(); 83 84 private final BluetoothLeBroadcast.Callback mBroadcastCallback = 85 new BluetoothLeBroadcast.Callback() { 86 @Override 87 public void onBroadcastStarted(int reason, int broadcastId) { 88 if (DEBUG) { 89 Log.d(TAG, "onBroadcastStarted(), reason = " + reason 90 + ", broadcastId = " + broadcastId); 91 } 92 mMainThreadHandler.post(() -> handleLeBroadcastStarted()); 93 } 94 95 @Override 96 public void onBroadcastStartFailed(int reason) { 97 if (DEBUG) { 98 Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason); 99 } 100 mMainThreadHandler.postDelayed(() -> handleLeBroadcastStartFailed(), 101 HANDLE_BROADCAST_FAILED_DELAY); 102 } 103 104 @Override 105 public void onBroadcastMetadataChanged(int broadcastId, 106 @NonNull BluetoothLeBroadcastMetadata metadata) { 107 if (DEBUG) { 108 Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId 109 + ", metadata = " + metadata); 110 } 111 mMainThreadHandler.post(() -> handleLeBroadcastMetadataChanged()); 112 } 113 114 @Override 115 public void onBroadcastStopped(int reason, int broadcastId) { 116 if (DEBUG) { 117 Log.d(TAG, "onBroadcastStopped(), reason = " + reason 118 + ", broadcastId = " + broadcastId); 119 } 120 mMainThreadHandler.post(() -> handleLeBroadcastStopped()); 121 } 122 123 @Override 124 public void onBroadcastStopFailed(int reason) { 125 if (DEBUG) { 126 Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason); 127 } 128 mMainThreadHandler.postDelayed(() -> handleLeBroadcastStopFailed(), 129 HANDLE_BROADCAST_FAILED_DELAY); 130 } 131 132 @Override 133 public void onBroadcastUpdated(int reason, int broadcastId) { 134 } 135 136 @Override 137 public void onBroadcastUpdateFailed(int reason, int broadcastId) { 138 } 139 140 @Override 141 public void onPlaybackStarted(int reason, int broadcastId) { 142 } 143 144 @Override 145 public void onPlaybackStopped(int reason, int broadcastId) { 146 } 147 }; 148 149 @AssistedFactory 150 public interface Factory { create( @ssistedCURRENT_BROADCAST_APP) String currentBroadcastApp, @Assisted(OUTPUT_PKG_NAME) String outputPkgName )151 BroadcastDialogDelegate create( 152 @Assisted(CURRENT_BROADCAST_APP) String currentBroadcastApp, 153 @Assisted(OUTPUT_PKG_NAME) String outputPkgName 154 ); 155 } 156 157 @AssistedInject BroadcastDialogDelegate( Context context, MediaOutputDialogManager mediaOutputDialogManager, @Nullable LocalBluetoothManager localBluetoothManager, UiEventLogger uiEventLogger, @Background Executor bgExecutor, BroadcastSender broadcastSender, SystemUIDialog.Factory systemUIDialogFactory, @Assisted(CURRENT_BROADCAST_APP) String currentBroadcastApp, @Assisted(OUTPUT_PKG_NAME) String outputPkgName)158 BroadcastDialogDelegate( 159 Context context, 160 MediaOutputDialogManager mediaOutputDialogManager, 161 @Nullable LocalBluetoothManager localBluetoothManager, 162 UiEventLogger uiEventLogger, 163 @Background Executor bgExecutor, 164 BroadcastSender broadcastSender, 165 SystemUIDialog.Factory systemUIDialogFactory, 166 @Assisted(CURRENT_BROADCAST_APP) String currentBroadcastApp, 167 @Assisted(OUTPUT_PKG_NAME) String outputPkgName) { 168 mContext = context; 169 mMediaOutputDialogManager = mediaOutputDialogManager; 170 mLocalBluetoothManager = localBluetoothManager; 171 mSystemUIDialogFactory = systemUIDialogFactory; 172 mCurrentBroadcastApp = currentBroadcastApp; 173 mOutputPackageName = outputPkgName; 174 mUiEventLogger = uiEventLogger; 175 mBgExecutor = bgExecutor; 176 mBroadcastSender = broadcastSender; 177 178 if (DEBUG) { 179 Log.d(TAG, "Init BroadcastDialog"); 180 } 181 } 182 183 @Override createDialog()184 public SystemUIDialog createDialog() { 185 return mSystemUIDialogFactory.create(this); 186 } 187 188 @Override onStart(SystemUIDialog dialog)189 public void onStart(SystemUIDialog dialog) { 190 mDialogs.add(dialog); 191 registerBroadcastCallBack(mBgExecutor, mBroadcastCallback); 192 } 193 194 @Override onCreate(SystemUIDialog dialog, Bundle savedInstanceState)195 public void onCreate(SystemUIDialog dialog, Bundle savedInstanceState) { 196 if (DEBUG) { 197 Log.d(TAG, "onCreate"); 198 } 199 200 mUiEventLogger.log(BroadcastDialogEvent.BROADCAST_DIALOG_SHOW); 201 View dialogView = dialog.getLayoutInflater().inflate(R.layout.broadcast_dialog, null); 202 final Window window = dialog.getWindow(); 203 window.setContentView(dialogView); 204 205 TextView title = dialogView.requireViewById(R.id.dialog_title); 206 TextView subTitle = dialogView.requireViewById(R.id.dialog_subtitle); 207 title.setText(mContext.getString( 208 R.string.bt_le_audio_broadcast_dialog_title, mCurrentBroadcastApp)); 209 String switchBroadcastApp = MediaDataUtils.getAppLabel(mContext, mOutputPackageName, 210 mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)); 211 subTitle.setText(mContext.getString( 212 R.string.bt_le_audio_broadcast_dialog_sub_title, switchBroadcastApp)); 213 214 mSwitchBroadcast = dialogView.requireViewById(R.id.switch_broadcast); 215 Button changeOutput = dialogView.requireViewById(R.id.change_output); 216 Button cancelBtn = dialogView.requireViewById(R.id.cancel); 217 mSwitchBroadcast.setText(mContext.getString( 218 R.string.bt_le_audio_broadcast_dialog_switch_app, switchBroadcastApp), null); 219 mSwitchBroadcast.setOnClickListener((view) -> startSwitchBroadcast()); 220 changeOutput.setOnClickListener( 221 (view) -> { 222 // TODO: b/321969740 - Take the userHandle as a parameter and pass it through. 223 // The package name is not sufficient to unambiguously identify an app. 224 mMediaOutputDialogManager.createAndShow( 225 mOutputPackageName, true, null, null, null); 226 dialog.dismiss(); 227 }); 228 cancelBtn.setOnClickListener((view) -> { 229 if (DEBUG) { 230 Log.d(TAG, "BroadcastDialog dismiss."); 231 } 232 dialog.dismiss(); 233 }); 234 } 235 236 @Override onStop(SystemUIDialog dialog)237 public void onStop(SystemUIDialog dialog) { 238 unregisterBroadcastCallBack(mBroadcastCallback); 239 mDialogs.remove(dialog); 240 } 241 refreshSwitchBroadcastButton()242 void refreshSwitchBroadcastButton() { 243 String switchBroadcastApp = MediaDataUtils.getAppLabel(mContext, mOutputPackageName, 244 mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)); 245 mSwitchBroadcast.setText(mContext.getString( 246 R.string.bt_le_audio_broadcast_dialog_switch_app, switchBroadcastApp), null); 247 mSwitchBroadcast.setEnabled(true); 248 } 249 startSwitchBroadcast()250 private void startSwitchBroadcast() { 251 if (DEBUG) { 252 Log.d(TAG, "startSwitchBroadcast"); 253 } 254 mSwitchBroadcast.setText(R.string.media_output_broadcast_starting); 255 mSwitchBroadcast.setEnabled(false); 256 //Stop the current Broadcast 257 if (!stopBluetoothLeBroadcast()) { 258 handleLeBroadcastStopFailed(); 259 return; 260 } 261 } 262 registerBroadcastCallBack( @onNull @allbackExecutor Executor executor, @NonNull BluetoothLeBroadcast.Callback callback)263 private void registerBroadcastCallBack( 264 @NonNull @CallbackExecutor Executor executor, 265 @NonNull BluetoothLeBroadcast.Callback callback) { 266 LocalBluetoothLeBroadcast broadcast = 267 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 268 if (broadcast == null) { 269 Log.d(TAG, "The broadcast profile is null"); 270 return; 271 } 272 broadcast.registerServiceCallBack(executor, callback); 273 } 274 unregisterBroadcastCallBack(@onNull BluetoothLeBroadcast.Callback callback)275 private void unregisterBroadcastCallBack(@NonNull BluetoothLeBroadcast.Callback callback) { 276 LocalBluetoothLeBroadcast broadcast = 277 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 278 if (broadcast == null) { 279 Log.d(TAG, "The broadcast profile is null"); 280 return; 281 } 282 broadcast.unregisterServiceCallBack(callback); 283 } 284 startBluetoothLeBroadcast()285 boolean startBluetoothLeBroadcast() { 286 LocalBluetoothLeBroadcast broadcast = 287 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 288 if (broadcast == null) { 289 Log.d(TAG, "The broadcast profile is null"); 290 return false; 291 } 292 String switchBroadcastApp = MediaDataUtils.getAppLabel(mContext, mOutputPackageName, 293 mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)); 294 broadcast.startBroadcast(switchBroadcastApp, /*language*/ null); 295 return true; 296 } 297 stopBluetoothLeBroadcast()298 boolean stopBluetoothLeBroadcast() { 299 LocalBluetoothLeBroadcast broadcast = 300 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 301 if (broadcast == null) { 302 Log.d(TAG, "The broadcast profile is null"); 303 return false; 304 } 305 broadcast.stopLatestBroadcast(); 306 return true; 307 } 308 309 @Override onWindowFocusChanged(SystemUIDialog dialog, boolean hasFocus)310 public void onWindowFocusChanged(SystemUIDialog dialog, boolean hasFocus) { 311 if (!hasFocus && dialog.isShowing()) { 312 dialog.dismiss(); 313 } 314 } 315 316 public enum BroadcastDialogEvent implements UiEventLogger.UiEventEnum { 317 @UiEvent(doc = "The Broadcast dialog became visible on the screen.") 318 BROADCAST_DIALOG_SHOW(1062); 319 320 private final int mId; 321 BroadcastDialogEvent(int id)322 BroadcastDialogEvent(int id) { 323 mId = id; 324 } 325 326 @Override getId()327 public int getId() { 328 return mId; 329 } 330 } 331 handleLeBroadcastStarted()332 void handleLeBroadcastStarted() { 333 // Waiting for the onBroadcastMetadataChanged. The UI launchs the broadcast dialog when 334 // the metadata is ready. 335 mShouldLaunchLeBroadcastDialog = true; 336 } 337 handleLeBroadcastStartFailed()338 private void handleLeBroadcastStartFailed() { 339 mSwitchBroadcast.setText(R.string.media_output_broadcast_start_failed); 340 mSwitchBroadcast.setEnabled(false); 341 refreshSwitchBroadcastButton(); 342 } 343 handleLeBroadcastMetadataChanged()344 void handleLeBroadcastMetadataChanged() { 345 if (mShouldLaunchLeBroadcastDialog) { 346 startLeBroadcastDialog(); 347 mShouldLaunchLeBroadcastDialog = false; 348 } 349 } 350 351 @VisibleForTesting handleLeBroadcastStopped()352 void handleLeBroadcastStopped() { 353 mShouldLaunchLeBroadcastDialog = false; 354 if (!startBluetoothLeBroadcast()) { 355 handleLeBroadcastStartFailed(); 356 return; 357 } 358 } 359 handleLeBroadcastStopFailed()360 private void handleLeBroadcastStopFailed() { 361 mSwitchBroadcast.setText(R.string.media_output_broadcast_start_failed); 362 mSwitchBroadcast.setEnabled(false); 363 refreshSwitchBroadcastButton(); 364 } 365 startLeBroadcastDialog()366 private void startLeBroadcastDialog() { 367 mBroadcastSender.sendBroadcast(new Intent() 368 .setPackage(mContext.getPackageName()) 369 .setAction(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG) 370 .putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME, mOutputPackageName)); 371 mDialogs.forEach(Dialog::dismiss); 372 } 373 } 374