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.media.dialog; 18 19 import static com.android.settingslib.flags.Flags.legacyLeAudioSharing; 20 21 import android.app.AlertDialog; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothLeBroadcastAssistant; 24 import android.bluetooth.BluetoothLeBroadcastMetadata; 25 import android.bluetooth.BluetoothLeBroadcastReceiveState; 26 import android.content.Context; 27 import android.graphics.Bitmap; 28 import android.os.Bundle; 29 import android.text.Editable; 30 import android.text.TextWatcher; 31 import android.text.method.HideReturnsTransformationMethod; 32 import android.text.method.PasswordTransformationMethod; 33 import android.util.Log; 34 import android.view.LayoutInflater; 35 import android.view.View; 36 import android.view.ViewStub; 37 import android.view.WindowManager; 38 import android.widget.Button; 39 import android.widget.EditText; 40 import android.widget.ImageView; 41 import android.widget.TextView; 42 43 import androidx.annotation.NonNull; 44 import androidx.core.graphics.drawable.IconCompat; 45 46 import com.android.internal.annotations.VisibleForTesting; 47 import com.android.settingslib.qrcode.QrCodeGenerator; 48 import com.android.systemui.broadcast.BroadcastSender; 49 import com.android.systemui.dagger.SysUISingleton; 50 import com.android.systemui.res.R; 51 import com.android.systemui.statusbar.phone.SystemUIDialog; 52 53 import com.google.zxing.WriterException; 54 55 /** 56 * Dialog for media output broadcast. 57 */ 58 @SysUISingleton 59 public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { 60 private static final String TAG = "MediaOutputBroadcastDialog"; 61 62 static final int METADATA_BROADCAST_NAME = 0; 63 static final int METADATA_BROADCAST_CODE = 1; 64 65 private static final int MAX_BROADCAST_INFO_UPDATE = 3; 66 @VisibleForTesting 67 static final int BROADCAST_CODE_MAX_LENGTH = 16; 68 @VisibleForTesting 69 static final int BROADCAST_CODE_MIN_LENGTH = 4; 70 @VisibleForTesting 71 static final int BROADCAST_NAME_MAX_LENGTH = 254; 72 73 private ViewStub mBroadcastInfoArea; 74 private ImageView mBroadcastQrCodeView; 75 private ImageView mBroadcastNotify; 76 private TextView mBroadcastName; 77 private ImageView mBroadcastNameEdit; 78 private TextView mBroadcastCode; 79 private ImageView mBroadcastCodeEye; 80 private Boolean mIsPasswordHide = true; 81 private ImageView mBroadcastCodeEdit; 82 @VisibleForTesting 83 AlertDialog mAlertDialog; 84 private TextView mBroadcastErrorMessage; 85 private int mRetryCount = 0; 86 private String mCurrentBroadcastName; 87 private String mCurrentBroadcastCode; 88 private boolean mIsStopbyUpdateBroadcastCode = false; 89 private boolean mIsLeBroadcastAssistantCallbackRegistered; 90 91 private TextWatcher mBroadcastCodeTextWatcher = new TextWatcher() { 92 @Override 93 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 94 // Do nothing 95 } 96 97 @Override 98 public void onTextChanged(CharSequence s, int start, int before, int count) { 99 // Do nothing 100 } 101 102 @Override 103 public void afterTextChanged(Editable s) { 104 if (mAlertDialog == null || mBroadcastErrorMessage == null) { 105 return; 106 } 107 boolean breakBroadcastCodeRuleTextLengthLessThanMin = 108 s.length() > 0 && s.length() < BROADCAST_CODE_MIN_LENGTH; 109 boolean breakBroadcastCodeRuleTextLengthMoreThanMax = 110 s.length() > BROADCAST_CODE_MAX_LENGTH; 111 boolean breakRule = breakBroadcastCodeRuleTextLengthLessThanMin 112 || breakBroadcastCodeRuleTextLengthMoreThanMax; 113 114 if (breakBroadcastCodeRuleTextLengthLessThanMin) { 115 mBroadcastErrorMessage.setText( 116 R.string.media_output_broadcast_code_hint_no_less_than_min); 117 } else if (breakBroadcastCodeRuleTextLengthMoreThanMax) { 118 mBroadcastErrorMessage.setText( 119 mContext.getResources().getString( 120 R.string.media_output_broadcast_edit_hint_no_more_than_max, 121 BROADCAST_CODE_MAX_LENGTH)); 122 } 123 124 mBroadcastErrorMessage.setVisibility(breakRule ? View.VISIBLE : View.INVISIBLE); 125 Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE); 126 if (positiveBtn != null) { 127 positiveBtn.setEnabled(breakRule ? false : true); 128 } 129 } 130 }; 131 132 private TextWatcher mBroadcastNameTextWatcher = new TextWatcher() { 133 @Override 134 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 135 // Do nothing 136 } 137 138 @Override 139 public void onTextChanged(CharSequence s, int start, int before, int count) { 140 // Do nothing 141 } 142 143 @Override 144 public void afterTextChanged(Editable s) { 145 if (mAlertDialog == null || mBroadcastErrorMessage == null) { 146 return; 147 } 148 boolean breakBroadcastNameRuleTextLengthMoreThanMax = 149 s.length() > BROADCAST_NAME_MAX_LENGTH; 150 boolean breakRule = breakBroadcastNameRuleTextLengthMoreThanMax || (s.length() == 0); 151 152 if (breakBroadcastNameRuleTextLengthMoreThanMax) { 153 mBroadcastErrorMessage.setText( 154 mContext.getResources().getString( 155 R.string.media_output_broadcast_edit_hint_no_more_than_max, 156 BROADCAST_NAME_MAX_LENGTH)); 157 } 158 mBroadcastErrorMessage.setVisibility( 159 breakBroadcastNameRuleTextLengthMoreThanMax ? View.VISIBLE : View.INVISIBLE); 160 Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE); 161 if (positiveBtn != null) { 162 positiveBtn.setEnabled(breakRule ? false : true); 163 } 164 } 165 }; 166 167 private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = 168 new BluetoothLeBroadcastAssistant.Callback() { 169 @Override 170 public void onSearchStarted(int reason) { 171 Log.d(TAG, "Assistant-onSearchStarted: " + reason); 172 } 173 174 @Override 175 public void onSearchStartFailed(int reason) { 176 Log.d(TAG, "Assistant-onSearchStartFailed: " + reason); 177 } 178 179 @Override 180 public void onSearchStopped(int reason) { 181 Log.d(TAG, "Assistant-onSearchStopped: " + reason); 182 } 183 184 @Override 185 public void onSearchStopFailed(int reason) { 186 Log.d(TAG, "Assistant-onSearchStopFailed: " + reason); 187 } 188 189 @Override 190 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) { 191 Log.d(TAG, "Assistant-onSourceFound:"); 192 } 193 194 @Override 195 public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) { 196 Log.d(TAG, "Assistant-onSourceAdded: Device: " + sink 197 + ", sourceId: " + sourceId); 198 mMainThreadHandler.post(() -> refreshUi()); 199 } 200 201 @Override 202 public void onSourceAddFailed(@NonNull BluetoothDevice sink, 203 @NonNull BluetoothLeBroadcastMetadata source, int reason) { 204 Log.d(TAG, "Assistant-onSourceAddFailed: Device: " + sink); 205 } 206 207 @Override 208 public void onSourceModified(@NonNull BluetoothDevice sink, int sourceId, 209 int reason) { 210 Log.d(TAG, "Assistant-onSourceModified:"); 211 } 212 213 @Override 214 public void onSourceModifyFailed(@NonNull BluetoothDevice sink, int sourceId, 215 int reason) { 216 Log.d(TAG, "Assistant-onSourceModifyFailed:"); 217 } 218 219 @Override 220 public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId, 221 int reason) { 222 Log.d(TAG, "Assistant-onSourceRemoved:"); 223 } 224 225 @Override 226 public void onSourceRemoveFailed(@NonNull BluetoothDevice sink, int sourceId, 227 int reason) { 228 Log.d(TAG, "Assistant-onSourceRemoveFailed:"); 229 } 230 231 @Override 232 public void onReceiveStateChanged(@NonNull BluetoothDevice sink, int sourceId, 233 @NonNull BluetoothLeBroadcastReceiveState state) { 234 Log.d(TAG, "Assistant-onReceiveStateChanged:"); 235 } 236 }; 237 MediaOutputBroadcastDialog(Context context, boolean aboveStatusbar, BroadcastSender broadcastSender, MediaOutputController mediaOutputController)238 MediaOutputBroadcastDialog(Context context, boolean aboveStatusbar, 239 BroadcastSender broadcastSender, MediaOutputController mediaOutputController) { 240 super( 241 context, 242 broadcastSender, 243 mediaOutputController, /* includePlaybackAndAppMetadata */ 244 true); 245 mAdapter = new MediaOutputAdapter(mMediaOutputController); 246 // TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class 247 // that extends MediaOutputBaseDialog 248 if (!aboveStatusbar) { 249 getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); 250 } 251 } 252 253 @Override onCreate(Bundle savedInstanceState)254 public void onCreate(Bundle savedInstanceState) { 255 super.onCreate(savedInstanceState); 256 257 initBtQrCodeUI(); 258 } 259 260 @Override start()261 public void start() { 262 super.start(); 263 if (!mIsLeBroadcastAssistantCallbackRegistered) { 264 mIsLeBroadcastAssistantCallbackRegistered = true; 265 mMediaOutputController.registerLeBroadcastAssistantServiceCallback(mExecutor, 266 mBroadcastAssistantCallback); 267 } 268 /* Add local source broadcast to connected capable devices that may be possible receivers 269 * of stream. 270 */ 271 startBroadcastWithConnectedDevices(); 272 } 273 274 @Override stop()275 public void stop() { 276 super.stop(); 277 if (mIsLeBroadcastAssistantCallbackRegistered) { 278 mIsLeBroadcastAssistantCallbackRegistered = false; 279 mMediaOutputController.unregisterLeBroadcastAssistantServiceCallback( 280 mBroadcastAssistantCallback); 281 } 282 } 283 284 @Override getHeaderIconRes()285 int getHeaderIconRes() { 286 return 0; 287 } 288 289 @Override getHeaderIcon()290 IconCompat getHeaderIcon() { 291 return mMediaOutputController.getHeaderIcon(); 292 } 293 294 @Override getHeaderIconSize()295 int getHeaderIconSize() { 296 return mContext.getResources().getDimensionPixelSize( 297 R.dimen.media_output_dialog_header_album_icon_size); 298 } 299 300 @Override getHeaderText()301 CharSequence getHeaderText() { 302 return mMediaOutputController.getHeaderTitle(); 303 } 304 305 @Override getHeaderSubtitle()306 CharSequence getHeaderSubtitle() { 307 return mMediaOutputController.getHeaderSubTitle(); 308 } 309 310 @Override getAppSourceIcon()311 IconCompat getAppSourceIcon() { 312 return mMediaOutputController.getNotificationSmallIcon(); 313 } 314 315 @Override getStopButtonVisibility()316 int getStopButtonVisibility() { 317 return View.VISIBLE; 318 } 319 320 @Override onStopButtonClick()321 public void onStopButtonClick() { 322 mMediaOutputController.stopBluetoothLeBroadcast(); 323 dismiss(); 324 } 325 getBroadcastMetadataInfo(int metadata)326 private String getBroadcastMetadataInfo(int metadata) { 327 switch (metadata) { 328 case METADATA_BROADCAST_NAME: 329 return mMediaOutputController.getBroadcastName(); 330 case METADATA_BROADCAST_CODE: 331 return mMediaOutputController.getBroadcastCode(); 332 default: 333 return ""; 334 } 335 } 336 initBtQrCodeUI()337 private void initBtQrCodeUI() { 338 //add the view to xml 339 inflateBroadcastInfoArea(); 340 341 //init UI component 342 mBroadcastQrCodeView = getDialogView().requireViewById(R.id.qrcode_view); 343 344 mBroadcastNotify = getDialogView().requireViewById(R.id.broadcast_info); 345 mBroadcastNotify.setOnClickListener(v -> { 346 mMediaOutputController.launchLeBroadcastNotifyDialog( 347 /* view= */ null, 348 /* broadcastSender= */ null, 349 MediaOutputController.BroadcastNotifyDialog.ACTION_BROADCAST_INFO_ICON, 350 /* onClickListener= */ null); 351 }); 352 mBroadcastName = getDialogView().requireViewById(R.id.broadcast_name_summary); 353 mBroadcastNameEdit = getDialogView().requireViewById(R.id.broadcast_name_edit); 354 mBroadcastNameEdit.setOnClickListener(v -> { 355 launchBroadcastUpdatedDialog(false, mBroadcastName.getText().toString()); 356 }); 357 mBroadcastCode = getDialogView().requireViewById(R.id.broadcast_code_summary); 358 mBroadcastCode.setTransformationMethod(PasswordTransformationMethod.getInstance()); 359 mBroadcastCodeEye = getDialogView().requireViewById(R.id.broadcast_code_eye); 360 mBroadcastCodeEye.setOnClickListener(v -> { 361 updateBroadcastCodeVisibility(); 362 }); 363 mBroadcastCodeEdit = getDialogView().requireViewById(R.id.broadcast_code_edit); 364 mBroadcastCodeEdit.setOnClickListener(v -> { 365 launchBroadcastUpdatedDialog(true, mBroadcastCode.getText().toString()); 366 }); 367 368 refreshUi(); 369 } 370 refreshUi()371 private void refreshUi() { 372 setQrCodeView(); 373 374 mCurrentBroadcastName = getBroadcastMetadataInfo(METADATA_BROADCAST_NAME); 375 mCurrentBroadcastCode = getBroadcastMetadataInfo(METADATA_BROADCAST_CODE); 376 mBroadcastName.setText(mCurrentBroadcastName); 377 mBroadcastCode.setText(mCurrentBroadcastCode); 378 refresh(false); 379 } 380 inflateBroadcastInfoArea()381 private void inflateBroadcastInfoArea() { 382 mBroadcastInfoArea = getDialogView().requireViewById(R.id.broadcast_qrcode); 383 mBroadcastInfoArea.inflate(); 384 } 385 setQrCodeView()386 private void setQrCodeView() { 387 //get the Metadata, and convert to BT QR code format. 388 String broadcastMetadata = getLocalBroadcastMetadataQrCodeString(); 389 if (broadcastMetadata.isEmpty()) { 390 //TDOD(b/226708424) Error handling for unable to generate the QR code bitmap 391 return; 392 } 393 try { 394 final int qrcodeSize = getContext().getResources().getDimensionPixelSize( 395 R.dimen.media_output_qrcode_size); 396 final Bitmap bmp = QrCodeGenerator.encodeQrCode(broadcastMetadata, qrcodeSize); 397 mBroadcastQrCodeView.setImageBitmap(bmp); 398 } catch (WriterException e) { 399 //TDOD(b/226708424) Error handling for unable to generate the QR code bitmap 400 Log.e(TAG, "Error generatirng QR code bitmap " + e); 401 } 402 } 403 startBroadcastWithConnectedDevices()404 void startBroadcastWithConnectedDevices() { 405 //get the Metadata, and convert to BT QR code format. 406 BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata(); 407 if (broadcastMetadata == null) { 408 Log.e(TAG, "Error: There is no broadcastMetadata."); 409 return; 410 } 411 412 for (BluetoothDevice sink : mMediaOutputController.getConnectedBroadcastSinkDevices()) { 413 Log.d(TAG, "The broadcastMetadata broadcastId: " + broadcastMetadata.getBroadcastId() 414 + ", the device: " + sink.getAnonymizedAddress()); 415 416 if (mMediaOutputController.isThereAnyBroadcastSourceIntoSinkDevice(sink)) { 417 Log.d(TAG, "The sink device has the broadcast source now."); 418 return; 419 } 420 if (!mMediaOutputController.addSourceIntoSinkDeviceWithBluetoothLeAssistant(sink, 421 broadcastMetadata, /*isGroupOp=*/ false)) { 422 Log.e(TAG, "Error: Source add failed"); 423 } 424 } 425 } 426 updateBroadcastCodeVisibility()427 private void updateBroadcastCodeVisibility() { 428 mBroadcastCode.setTransformationMethod( 429 mIsPasswordHide ? HideReturnsTransformationMethod.getInstance() 430 : PasswordTransformationMethod.getInstance()); 431 mIsPasswordHide = !mIsPasswordHide; 432 } 433 launchBroadcastUpdatedDialog(boolean isBroadcastCode, String editString)434 private void launchBroadcastUpdatedDialog(boolean isBroadcastCode, String editString) { 435 final View layout = LayoutInflater.from(mContext).inflate( 436 R.layout.media_output_broadcast_update_dialog, null); 437 final EditText editText = layout.requireViewById(R.id.broadcast_edit_text); 438 editText.setText(editString); 439 editText.addTextChangedListener( 440 isBroadcastCode ? mBroadcastCodeTextWatcher : mBroadcastNameTextWatcher); 441 mBroadcastErrorMessage = layout.requireViewById(R.id.broadcast_error_message); 442 mAlertDialog = new Builder(mContext) 443 .setTitle(isBroadcastCode ? R.string.media_output_broadcast_code 444 : R.string.media_output_broadcast_name) 445 .setView(layout) 446 .setNegativeButton(android.R.string.cancel, null) 447 .setPositiveButton(R.string.media_output_broadcast_dialog_save, 448 (d, w) -> { 449 updateBroadcastInfo(isBroadcastCode, editText.getText().toString()); 450 }) 451 .create(); 452 453 mAlertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); 454 SystemUIDialog.setShowForAllUsers(mAlertDialog, true); 455 SystemUIDialog.registerDismissListener(mAlertDialog); 456 mAlertDialog.show(); 457 } 458 getLocalBroadcastMetadataQrCodeString()459 private String getLocalBroadcastMetadataQrCodeString() { 460 return mMediaOutputController.getLocalBroadcastMetadataQrCodeString(); 461 } 462 getBroadcastMetadata()463 private BluetoothLeBroadcastMetadata getBroadcastMetadata() { 464 return mMediaOutputController.getBroadcastMetadata(); 465 } 466 467 @VisibleForTesting updateBroadcastInfo(boolean isBroadcastCode, String updatedString)468 void updateBroadcastInfo(boolean isBroadcastCode, String updatedString) { 469 Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE); 470 if (positiveBtn != null) { 471 positiveBtn.setEnabled(false); 472 } 473 474 if (isBroadcastCode) { 475 /* If the user wants to update the Broadcast Code, the Broadcast session should be 476 * stopped then used the new Broadcast code to start the Broadcast. 477 */ 478 mIsStopbyUpdateBroadcastCode = true; 479 mMediaOutputController.setBroadcastCode(updatedString); 480 if (!mMediaOutputController.stopBluetoothLeBroadcast()) { 481 handleLeBroadcastStopFailed(); 482 return; 483 } 484 } else { 485 /* If the user wants to update the Broadcast Name, we don't need to stop the Broadcast 486 * session. Only use the new Broadcast name to update the broadcast session. 487 */ 488 mMediaOutputController.setBroadcastName(updatedString); 489 if (!mMediaOutputController.updateBluetoothLeBroadcast()) { 490 handleLeBroadcastUpdateFailed(); 491 } 492 } 493 } 494 495 @Override isBroadcastSupported()496 public boolean isBroadcastSupported() { 497 if (!legacyLeAudioSharing()) return false; 498 boolean isBluetoothLeDevice = false; 499 if (mMediaOutputController.getCurrentConnectedMediaDevice() != null) { 500 isBluetoothLeDevice = mMediaOutputController.isBluetoothLeDevice( 501 mMediaOutputController.getCurrentConnectedMediaDevice()); 502 } 503 504 return mMediaOutputController.isBroadcastSupported() && isBluetoothLeDevice; 505 } 506 507 @Override handleLeBroadcastStarted()508 public void handleLeBroadcastStarted() { 509 mRetryCount = 0; 510 if (mAlertDialog != null) { 511 mAlertDialog.dismiss(); 512 } 513 refreshUi(); 514 } 515 516 @Override handleLeBroadcastStartFailed()517 public void handleLeBroadcastStartFailed() { 518 mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode); 519 mRetryCount++; 520 521 handleUpdateFailedUi(); 522 } 523 524 @Override handleLeBroadcastMetadataChanged()525 public void handleLeBroadcastMetadataChanged() { 526 Log.d(TAG, "handleLeBroadcastMetadataChanged:"); 527 refreshUi(); 528 } 529 530 @Override handleLeBroadcastUpdated()531 public void handleLeBroadcastUpdated() { 532 mRetryCount = 0; 533 if (mAlertDialog != null) { 534 mAlertDialog.dismiss(); 535 } 536 refreshUi(); 537 } 538 539 @Override handleLeBroadcastUpdateFailed()540 public void handleLeBroadcastUpdateFailed() { 541 //Change the value in shared preferences back to it original value 542 mMediaOutputController.setBroadcastName(mCurrentBroadcastName); 543 mRetryCount++; 544 545 handleUpdateFailedUi(); 546 } 547 548 @Override handleLeBroadcastStopped()549 public void handleLeBroadcastStopped() { 550 if (mIsStopbyUpdateBroadcastCode) { 551 mIsStopbyUpdateBroadcastCode = false; 552 mRetryCount = 0; 553 if (!mMediaOutputController.startBluetoothLeBroadcast()) { 554 handleLeBroadcastStartFailed(); 555 return; 556 } 557 } else { 558 dismiss(); 559 } 560 } 561 562 @Override handleLeBroadcastStopFailed()563 public void handleLeBroadcastStopFailed() { 564 //Change the value in shared preferences back to it original value 565 mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode); 566 mRetryCount++; 567 568 handleUpdateFailedUi(); 569 } 570 handleUpdateFailedUi()571 private void handleUpdateFailedUi() { 572 if (mAlertDialog == null) { 573 Log.d(TAG, "handleUpdateFailedUi: mAlertDialog is null"); 574 return; 575 } 576 int errorMessageStringId = -1; 577 boolean enablePositiveBtn = false; 578 if (mRetryCount < MAX_BROADCAST_INFO_UPDATE) { 579 enablePositiveBtn = true; 580 errorMessageStringId = R.string.media_output_broadcast_update_error; 581 } else { 582 mRetryCount = 0; 583 errorMessageStringId = R.string.media_output_broadcast_last_update_error; 584 } 585 586 // update UI 587 final Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE); 588 if (positiveBtn != null && enablePositiveBtn) { 589 positiveBtn.setEnabled(true); 590 } 591 if (mBroadcastErrorMessage != null) { 592 mBroadcastErrorMessage.setVisibility(View.VISIBLE); 593 mBroadcastErrorMessage.setText(errorMessageStringId); 594 } 595 } 596 597 @VisibleForTesting getRetryCount()598 int getRetryCount() { 599 return mRetryCount; 600 } 601 } 602