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