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