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.settings.bluetooth; 18 19 import static android.bluetooth.BluetoothDevice.BOND_NONE; 20 import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; 21 22 import android.app.Activity; 23 import android.app.AlertDialog; 24 import android.app.settings.SettingsEnums; 25 import android.bluetooth.BluetoothDevice; 26 import android.bluetooth.BluetoothLeBroadcastAssistant; 27 import android.bluetooth.BluetoothLeBroadcastMetadata; 28 import android.bluetooth.BluetoothLeBroadcastReceiveState; 29 import android.bluetooth.le.ScanFilter; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.os.Bundle; 33 import android.text.Editable; 34 import android.text.InputFilter; 35 import android.text.InputType; 36 import android.text.Spanned; 37 import android.text.TextWatcher; 38 import android.util.Log; 39 import android.view.LayoutInflater; 40 import android.view.View; 41 import android.view.WindowManager; 42 import android.widget.Button; 43 import android.widget.EditText; 44 import android.widget.TextView; 45 import android.widget.Toast; 46 47 import androidx.annotation.NonNull; 48 import androidx.annotation.VisibleForTesting; 49 import androidx.preference.PreferenceCategory; 50 51 import com.android.settings.R; 52 import com.android.settings.dashboard.RestrictedDashboardFragment; 53 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 54 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; 55 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata; 56 import com.android.settingslib.bluetooth.LocalBluetoothManager; 57 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 58 import com.android.settingslib.core.AbstractPreferenceController; 59 import com.android.settingslib.core.lifecycle.Lifecycle; 60 61 import java.nio.charset.StandardCharsets; 62 import java.util.ArrayList; 63 import java.util.Collections; 64 import java.util.List; 65 import java.util.concurrent.Executor; 66 import java.util.concurrent.Executors; 67 68 69 /** 70 * This fragment allowed users to find the nearby broadcast sources. 71 */ 72 public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment { 73 74 private static final String TAG = "BtFindBroadcastsFrg"; 75 76 public static final String KEY_DEVICE_ADDRESS = "device_address"; 77 public static final String PREF_KEY_BROADCAST_SOURCE_LIST = "broadcast_source_list"; 78 public static final int REQUEST_SCAN_BT_BROADCAST_QR_CODE = 0; 79 80 @VisibleForTesting 81 String mDeviceAddress; 82 @VisibleForTesting 83 LocalBluetoothManager mManager; 84 @VisibleForTesting 85 CachedBluetoothDevice mCachedDevice; 86 @VisibleForTesting 87 PreferenceCategory mBroadcastSourceListCategory; 88 @VisibleForTesting 89 BluetoothBroadcastSourcePreference mSelectedPreference; 90 BluetoothFindBroadcastsHeaderController mBluetoothFindBroadcastsHeaderController; 91 92 private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; 93 private LocalBluetoothLeBroadcastMetadata mLocalBroadcastMetadata; 94 private Executor mExecutor; 95 private int mSourceId; 96 97 private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = 98 new BluetoothLeBroadcastAssistant.Callback() { 99 @Override 100 public void onSearchStarted(int reason) { 101 Log.d(TAG, "onSearchStarted: " + reason); 102 getActivity().runOnUiThread(() -> handleSearchStarted()); 103 } 104 105 @Override 106 public void onSearchStartFailed(int reason) { 107 Log.d(TAG, "onSearchStartFailed: " + reason); 108 } 109 110 @Override 111 public void onSearchStopped(int reason) { 112 Log.d(TAG, "onSearchStopped: " + reason); 113 } 114 115 @Override 116 public void onSearchStopFailed(int reason) { 117 Log.d(TAG, "onSearchStopFailed: " + reason); 118 } 119 120 @Override 121 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) { 122 Log.d(TAG, "onSourceFound:"); 123 getActivity().runOnUiThread( 124 () -> updateListCategoryFromBroadcastMetadata(source, false)); 125 } 126 127 @Override 128 public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) { 129 setSourceId(sourceId); 130 if (mSelectedPreference == null) { 131 Log.w(TAG, "onSourceAdded: mSelectedPreference == null!"); 132 return; 133 } 134 if (mLeBroadcastAssistant != null 135 && mLeBroadcastAssistant.isSearchInProgress()) { 136 mLeBroadcastAssistant.stopSearchingForSources(); 137 } 138 getActivity().runOnUiThread(() -> updateListCategoryFromBroadcastMetadata( 139 mSelectedPreference.getBluetoothLeBroadcastMetadata(), true)); 140 } 141 142 @Override 143 public void onSourceAddFailed(@NonNull BluetoothDevice sink, 144 @NonNull BluetoothLeBroadcastMetadata source, int reason) { 145 mSelectedPreference = null; 146 Log.d(TAG, "onSourceAddFailed: clear the mSelectedPreference."); 147 } 148 149 @Override 150 public void onSourceModified(@NonNull BluetoothDevice sink, int sourceId, 151 int reason) { 152 } 153 154 @Override 155 public void onSourceModifyFailed(@NonNull BluetoothDevice sink, int sourceId, 156 int reason) { 157 } 158 159 @Override 160 public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId, 161 int reason) { 162 Log.d(TAG, "onSourceRemoved:"); 163 getActivity().runOnUiThread(() -> handleSourceRemoved()); 164 } 165 166 @Override 167 public void onSourceRemoveFailed(@NonNull BluetoothDevice sink, int sourceId, 168 int reason) { 169 Log.d(TAG, "onSourceRemoveFailed:"); 170 } 171 172 @Override 173 public void onReceiveStateChanged(@NonNull BluetoothDevice sink, int sourceId, 174 @NonNull BluetoothLeBroadcastReceiveState state) { 175 Log.d(TAG, "onReceiveStateChanged:"); 176 } 177 }; 178 BluetoothFindBroadcastsFragment()179 public BluetoothFindBroadcastsFragment() { 180 super(DISALLOW_CONFIG_BLUETOOTH); 181 } 182 183 @VisibleForTesting getLocalBluetoothManager(Context context)184 LocalBluetoothManager getLocalBluetoothManager(Context context) { 185 return Utils.getLocalBtManager(context); 186 } 187 188 @VisibleForTesting getCachedDevice(String deviceAddress)189 CachedBluetoothDevice getCachedDevice(String deviceAddress) { 190 BluetoothDevice remoteDevice = 191 mManager.getBluetoothAdapter().getRemoteDevice(deviceAddress); 192 return mManager.getCachedDeviceManager().findDevice(remoteDevice); 193 } 194 195 @Override onAttach(Context context)196 public void onAttach(Context context) { 197 mDeviceAddress = getArguments().getString(KEY_DEVICE_ADDRESS); 198 mManager = getLocalBluetoothManager(context); 199 mCachedDevice = getCachedDevice(mDeviceAddress); 200 mLeBroadcastAssistant = getLeBroadcastAssistant(); 201 mExecutor = Executors.newSingleThreadExecutor(); 202 mLocalBroadcastMetadata = new LocalBluetoothLeBroadcastMetadata(); 203 204 super.onAttach(context); 205 if (mCachedDevice == null || mLeBroadcastAssistant == null) { 206 //Close this page if device is null with invalid device mac address 207 //or if the device does not have LeBroadcastAssistant profile 208 Log.w(TAG, "onAttach() CachedDevice or LeBroadcastAssistant is null!"); 209 finish(); 210 return; 211 } 212 } 213 214 @Override onCreate(Bundle icicle)215 public void onCreate(Bundle icicle) { 216 super.onCreate(icicle); 217 218 mBroadcastSourceListCategory = findPreference(PREF_KEY_BROADCAST_SOURCE_LIST); 219 } 220 221 @Override onStart()222 public void onStart() { 223 super.onStart(); 224 if (mLeBroadcastAssistant != null) { 225 mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); 226 } 227 } 228 229 @Override onResume()230 public void onResume() { 231 super.onResume(); 232 finishFragmentIfNecessary(); 233 //check assistant status. Start searching... 234 if (mLeBroadcastAssistant != null && !mLeBroadcastAssistant.isSearchInProgress()) { 235 mLeBroadcastAssistant.startSearchingForSources(getScanFilter()); 236 } else { 237 addConnectedSourcePreference(); 238 } 239 } 240 241 @Override onStop()242 public void onStop() { 243 super.onStop(); 244 if (mLeBroadcastAssistant != null) { 245 if (mLeBroadcastAssistant.isSearchInProgress()) { 246 mLeBroadcastAssistant.stopSearchingForSources(); 247 } 248 mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); 249 } 250 } 251 252 @Override onActivityResult(int requestCode, int resultCode, Intent data)253 public void onActivityResult(int requestCode, int resultCode, Intent data) { 254 super.onActivityResult(requestCode, resultCode, data); 255 Log.d(TAG, "onActivityResult: " + requestCode + ", resultCode: " + resultCode); 256 if (requestCode == REQUEST_SCAN_BT_BROADCAST_QR_CODE) { 257 if (resultCode == Activity.RESULT_OK) { 258 259 //Get BroadcastMetadata 260 String broadcastMetadata = data.getStringExtra( 261 QrCodeScanModeFragment.KEY_BROADCAST_METADATA); 262 BluetoothLeBroadcastMetadata source = convertToBroadcastMetadata(broadcastMetadata); 263 264 if (source != null) { 265 Log.d(TAG, "onActivityResult source Id = " + source.getBroadcastId()); 266 //Create preference for the broadcast source 267 updateListCategoryFromBroadcastMetadata(source, false); 268 //Add Source 269 addSource(mBroadcastSourceListCategory.findPreference( 270 Integer.toString(source.getBroadcastId()))); 271 } else { 272 Toast.makeText(getContext(), 273 R.string.find_broadcast_join_broadcast_error, Toast.LENGTH_SHORT).show(); 274 return; 275 } 276 } 277 } 278 } 279 280 @VisibleForTesting finishFragmentIfNecessary()281 void finishFragmentIfNecessary() { 282 if (mCachedDevice.getBondState() == BOND_NONE) { 283 finish(); 284 return; 285 } 286 } 287 288 @Override getMetricsCategory()289 public int getMetricsCategory() { 290 return SettingsEnums.LE_AUDIO_BROADCAST_FIND_BROADCAST; 291 } 292 293 /** 294 * Starts to scan broadcast source by the BluetoothLeBroadcastAssistant. 295 */ scanBroadcastSource()296 public void scanBroadcastSource() { 297 if (mLeBroadcastAssistant == null) { 298 Log.w(TAG, "scanBroadcastSource: LeBroadcastAssistant is null!"); 299 return; 300 } 301 mLeBroadcastAssistant.startSearchingForSources(getScanFilter()); 302 } 303 304 /** 305 * Leaves the broadcast source by the BluetoothLeBroadcastAssistant. 306 */ leaveBroadcastSession()307 public void leaveBroadcastSession() { 308 if (mLeBroadcastAssistant == null || mCachedDevice == null) { 309 Log.w(TAG, "leaveBroadcastSession: LeBroadcastAssistant or CachedDevice is null!"); 310 return; 311 } 312 mLeBroadcastAssistant.removeSource(mCachedDevice.getDevice(), getSourceId()); 313 } 314 315 @Override getLogTag()316 protected String getLogTag() { 317 return TAG; 318 } 319 320 @Override getPreferenceScreenResId()321 protected int getPreferenceScreenResId() { 322 return R.xml.bluetooth_find_broadcasts_fragment; 323 } 324 325 @Override createPreferenceControllers(Context context)326 protected List<AbstractPreferenceController> createPreferenceControllers(Context context) { 327 ArrayList<AbstractPreferenceController> controllers = new ArrayList<>(); 328 329 if (mCachedDevice != null) { 330 Lifecycle lifecycle = getSettingsLifecycle(); 331 mBluetoothFindBroadcastsHeaderController = new BluetoothFindBroadcastsHeaderController( 332 context, this, mCachedDevice, lifecycle, mManager); 333 controllers.add(mBluetoothFindBroadcastsHeaderController); 334 } 335 return controllers; 336 } 337 338 /** 339 * Gets the LocalBluetoothLeBroadcastAssistant 340 * @return the LocalBluetoothLeBroadcastAssistant 341 */ getLeBroadcastAssistant()342 public LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() { 343 if (mManager == null) { 344 Log.w(TAG, "getLeBroadcastAssistant: LocalBluetoothManager is null!"); 345 return null; 346 } 347 348 LocalBluetoothProfileManager profileManager = mManager.getProfileManager(); 349 if (profileManager == null) { 350 Log.w(TAG, "getLeBroadcastAssistant: LocalBluetoothProfileManager is null!"); 351 return null; 352 } 353 354 return profileManager.getLeAudioBroadcastAssistantProfile(); 355 } 356 getScanFilter()357 private List<ScanFilter> getScanFilter() { 358 // Currently there is no function for setting the ScanFilter. It may have this function 359 // in the further. 360 return Collections.emptyList(); 361 } 362 updateListCategoryFromBroadcastMetadata(BluetoothLeBroadcastMetadata source, boolean isConnected)363 private void updateListCategoryFromBroadcastMetadata(BluetoothLeBroadcastMetadata source, 364 boolean isConnected) { 365 BluetoothBroadcastSourcePreference item = mBroadcastSourceListCategory.findPreference( 366 Integer.toString(source.getBroadcastId())); 367 if (item == null) { 368 item = createBluetoothBroadcastSourcePreference(); 369 item.setKey(Integer.toString(source.getBroadcastId())); 370 mBroadcastSourceListCategory.addPreference(item); 371 } 372 item.updateMetadataAndRefreshUi(source, isConnected); 373 item.setOrder(isConnected ? 0 : 1); 374 375 //refresh the header 376 if (mBluetoothFindBroadcastsHeaderController != null) { 377 mBluetoothFindBroadcastsHeaderController.refreshUi(); 378 } 379 } 380 updateListCategoryFromBroadcastReceiveState( BluetoothLeBroadcastReceiveState receiveState)381 private void updateListCategoryFromBroadcastReceiveState( 382 BluetoothLeBroadcastReceiveState receiveState) { 383 BluetoothBroadcastSourcePreference item = mBroadcastSourceListCategory.findPreference( 384 Integer.toString(receiveState.getBroadcastId())); 385 if (item == null) { 386 item = createBluetoothBroadcastSourcePreference(); 387 item.setKey(Integer.toString(receiveState.getBroadcastId())); 388 mBroadcastSourceListCategory.addPreference(item); 389 } 390 item.updateReceiveStateAndRefreshUi(receiveState); 391 item.setOrder(0); 392 393 setSourceId(receiveState.getSourceId()); 394 mSelectedPreference = item; 395 396 //refresh the header 397 if (mBluetoothFindBroadcastsHeaderController != null) { 398 mBluetoothFindBroadcastsHeaderController.refreshUi(); 399 } 400 } 401 createBluetoothBroadcastSourcePreference()402 private BluetoothBroadcastSourcePreference createBluetoothBroadcastSourcePreference() { 403 BluetoothBroadcastSourcePreference pref = new BluetoothBroadcastSourcePreference( 404 getContext()); 405 pref.setOnPreferenceClickListener(preference -> { 406 if (pref.getBluetoothLeBroadcastMetadata() == null) { 407 Log.d(TAG, "BluetoothLeBroadcastMetadata is null, do nothing."); 408 return false; 409 } 410 if (pref.isEncrypted()) { 411 launchBroadcastCodeDialog(pref); 412 } else { 413 addSource(pref); 414 } 415 return true; 416 }); 417 return pref; 418 } 419 420 @VisibleForTesting addSource(BluetoothBroadcastSourcePreference pref)421 void addSource(BluetoothBroadcastSourcePreference pref) { 422 if (mLeBroadcastAssistant == null || mCachedDevice == null) { 423 Log.w(TAG, "addSource: LeBroadcastAssistant or CachedDevice is null!"); 424 return; 425 } 426 if (mSelectedPreference != null) { 427 if (mSelectedPreference.isCreatedByReceiveState()) { 428 Log.d(TAG, "addSource: Remove preference that created by getAllSources()"); 429 getActivity().runOnUiThread(() -> 430 mBroadcastSourceListCategory.removePreference(mSelectedPreference)); 431 if (mLeBroadcastAssistant != null && !mLeBroadcastAssistant.isSearchInProgress()) { 432 Log.d(TAG, "addSource: Start Searching For Broadcast Sources"); 433 mLeBroadcastAssistant.startSearchingForSources(getScanFilter()); 434 } 435 } else { 436 Log.d(TAG, "addSource: Update preference that created by onSourceFound()"); 437 // The previous preference status set false after user selects the new Preference. 438 getActivity().runOnUiThread( 439 () -> { 440 mSelectedPreference.updateMetadataAndRefreshUi( 441 mSelectedPreference.getBluetoothLeBroadcastMetadata(), false); 442 mSelectedPreference.setOrder(1); 443 }); 444 } 445 } 446 mSelectedPreference = pref; 447 mLeBroadcastAssistant.addSource(mCachedDevice.getDevice(), 448 pref.getBluetoothLeBroadcastMetadata(), true); 449 } 450 addBroadcastCodeIntoPreference(BluetoothBroadcastSourcePreference pref, String broadcastCode)451 private void addBroadcastCodeIntoPreference(BluetoothBroadcastSourcePreference pref, 452 String broadcastCode) { 453 BluetoothLeBroadcastMetadata metadata = 454 new BluetoothLeBroadcastMetadata.Builder(pref.getBluetoothLeBroadcastMetadata()) 455 .setBroadcastCode(broadcastCode.getBytes(StandardCharsets.UTF_8)) 456 .build(); 457 pref.updateMetadataAndRefreshUi(metadata, false); 458 } 459 launchBroadcastCodeDialog(BluetoothBroadcastSourcePreference pref)460 private void launchBroadcastCodeDialog(BluetoothBroadcastSourcePreference pref) { 461 final View layout = LayoutInflater.from(getContext()).inflate( 462 R.layout.bluetooth_find_broadcast_password_dialog, null); 463 final TextView broadcastName = layout.requireViewById(R.id.broadcast_name_text); 464 final EditText editText = layout.requireViewById(R.id.broadcast_edit_text); 465 broadcastName.setText(pref.getTitle()); 466 AlertDialog alertDialog = new AlertDialog.Builder(getContext()) 467 .setTitle(R.string.find_broadcast_password_dialog_title) 468 .setView(layout) 469 .setNeutralButton(android.R.string.cancel, null) 470 .setPositiveButton(R.string.bluetooth_connect_access_dialog_positive, 471 (d, w) -> { 472 Log.d(TAG, "setPositiveButton: clicked"); 473 if (pref.getBluetoothLeBroadcastMetadata() == null) { 474 Log.d(TAG, "BluetoothLeBroadcastMetadata is null, do nothing."); 475 return; 476 } 477 addBroadcastCodeIntoPreference(pref, editText.getText().toString()); 478 addSource(pref); 479 }) 480 .create(); 481 482 alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); 483 addTextWatcher(alertDialog, editText); 484 alertDialog.show(); 485 updateBtnState(alertDialog, false); 486 } 487 addTextWatcher(AlertDialog alertDialog, EditText editText)488 private void addTextWatcher(AlertDialog alertDialog, EditText editText) { 489 if (alertDialog == null || editText == null) { 490 return; 491 } 492 final InputFilter[] filter = new InputFilter[] {mInputFilter}; 493 editText.setFilters(filter); 494 editText.setInputType(InputType.TYPE_CLASS_TEXT 495 | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); 496 TextWatcher bCodeTextWatcher = new TextWatcher() { 497 @Override 498 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 499 // Do nothing 500 } 501 502 @Override 503 public void onTextChanged(CharSequence s, int start, int before, int count) { 504 // Do nothing 505 } 506 507 @Override 508 public void afterTextChanged(Editable s) { 509 boolean breakBroadcastCodeRuleTextLengthLessThanMin = 510 s.length() > 0 && s.toString().getBytes().length < 4; 511 boolean breakBroadcastCodeRuleTextLengthMoreThanMax = 512 s.toString().getBytes().length > 16; 513 boolean breakRule = breakBroadcastCodeRuleTextLengthLessThanMin 514 || breakBroadcastCodeRuleTextLengthMoreThanMax; 515 updateBtnState(alertDialog, !breakRule); 516 } 517 }; 518 editText.addTextChangedListener(bCodeTextWatcher); 519 } 520 updateBtnState(AlertDialog alertDialog, boolean isEnable)521 private void updateBtnState(AlertDialog alertDialog, boolean isEnable) { 522 Button positiveBtn = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); 523 if (positiveBtn != null) { 524 positiveBtn.setEnabled(isEnable ? true : false); 525 } 526 } 527 528 private InputFilter mInputFilter = new InputFilter() { 529 @Override 530 public CharSequence filter(CharSequence source, int start, int end, 531 Spanned dest, int dstart, int dend) { 532 byte[] bytes = source.toString().getBytes(StandardCharsets.UTF_8); 533 if (bytes.length == source.length()) { 534 return source; 535 } else { 536 return ""; 537 } 538 } 539 }; 540 handleSearchStarted()541 private void handleSearchStarted() { 542 cacheRemoveAllPrefs(mBroadcastSourceListCategory); 543 addConnectedSourcePreference(); 544 } 545 handleSourceRemoved()546 private void handleSourceRemoved() { 547 if (mSelectedPreference != null) { 548 if (mSelectedPreference.getBluetoothLeBroadcastMetadata() == null) { 549 mBroadcastSourceListCategory.removePreference(mSelectedPreference); 550 } else { 551 mSelectedPreference.clearReceiveState(); 552 } 553 } 554 mSelectedPreference = null; 555 } 556 addConnectedSourcePreference()557 private void addConnectedSourcePreference() { 558 List<BluetoothLeBroadcastReceiveState> receiveStateList = 559 mLeBroadcastAssistant.getAllSources(mCachedDevice.getDevice()); 560 if (!receiveStateList.isEmpty()) { 561 updateListCategoryFromBroadcastReceiveState(receiveStateList.get(0)); 562 } 563 } 564 getSourceId()565 public int getSourceId() { 566 return mSourceId; 567 } 568 setSourceId(int sourceId)569 public void setSourceId(int sourceId) { 570 mSourceId = sourceId; 571 } 572 convertToBroadcastMetadata(String qrCodeString)573 private BluetoothLeBroadcastMetadata convertToBroadcastMetadata(String qrCodeString) { 574 return mLocalBroadcastMetadata.convertToBroadcastMetadata(qrCodeString); 575 } 576 } 577