Bug: 263554347 Bug: 265281156 Bug: 248409874 Test: manual test Change-Id: Ia252f29ad9dfdc09266e91cc9ecaa32ed8c749c2 Merged-In: Ia252f29ad9dfdc09266e91cc9ecaa32ed8c749c2
510 lines
20 KiB
Java
510 lines
20 KiB
Java
/*
|
|
* Copyright (C) 2022 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.settings.bluetooth;
|
|
|
|
import static android.bluetooth.BluetoothDevice.BOND_NONE;
|
|
import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
|
|
|
|
import android.app.Activity;
|
|
import android.app.AlertDialog;
|
|
import android.app.settings.SettingsEnums;
|
|
import android.bluetooth.BluetoothDevice;
|
|
import android.bluetooth.BluetoothLeBroadcastAssistant;
|
|
import android.bluetooth.BluetoothLeBroadcastMetadata;
|
|
import android.bluetooth.BluetoothLeBroadcastReceiveState;
|
|
import android.bluetooth.le.ScanFilter;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.os.Bundle;
|
|
import android.util.Log;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.WindowManager;
|
|
import android.widget.EditText;
|
|
import android.widget.TextView;
|
|
import android.widget.Toast;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.VisibleForTesting;
|
|
import androidx.preference.PreferenceCategory;
|
|
|
|
import com.android.settings.R;
|
|
import com.android.settings.dashboard.RestrictedDashboardFragment;
|
|
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
|
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
|
|
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata;
|
|
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
|
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
|
|
import com.android.settingslib.core.AbstractPreferenceController;
|
|
import com.android.settingslib.core.lifecycle.Lifecycle;
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.concurrent.Executor;
|
|
import java.util.concurrent.Executors;
|
|
|
|
|
|
/**
|
|
* This fragment allowed users to find the nearby broadcast sources.
|
|
*/
|
|
public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment {
|
|
|
|
private static final String TAG = "BtFindBroadcastsFrg";
|
|
|
|
public static final String KEY_DEVICE_ADDRESS = "device_address";
|
|
public static final String PREF_KEY_BROADCAST_SOURCE_LIST = "broadcast_source_list";
|
|
public static final int REQUEST_SCAN_BT_BROADCAST_QR_CODE = 0;
|
|
|
|
@VisibleForTesting
|
|
String mDeviceAddress;
|
|
@VisibleForTesting
|
|
LocalBluetoothManager mManager;
|
|
@VisibleForTesting
|
|
CachedBluetoothDevice mCachedDevice;
|
|
@VisibleForTesting
|
|
PreferenceCategory mBroadcastSourceListCategory;
|
|
@VisibleForTesting
|
|
BluetoothBroadcastSourcePreference mSelectedPreference;
|
|
BluetoothFindBroadcastsHeaderController mBluetoothFindBroadcastsHeaderController;
|
|
|
|
private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
|
|
private LocalBluetoothLeBroadcastMetadata mLocalBroadcastMetadata;
|
|
private Executor mExecutor;
|
|
private int mSourceId;
|
|
|
|
private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
|
|
new BluetoothLeBroadcastAssistant.Callback() {
|
|
@Override
|
|
public void onSearchStarted(int reason) {
|
|
Log.d(TAG, "onSearchStarted: " + reason);
|
|
getActivity().runOnUiThread(() -> handleSearchStarted());
|
|
}
|
|
|
|
@Override
|
|
public void onSearchStartFailed(int reason) {
|
|
Log.d(TAG, "onSearchStartFailed: " + reason);
|
|
}
|
|
|
|
@Override
|
|
public void onSearchStopped(int reason) {
|
|
Log.d(TAG, "onSearchStopped: " + reason);
|
|
}
|
|
|
|
@Override
|
|
public void onSearchStopFailed(int reason) {
|
|
Log.d(TAG, "onSearchStopFailed: " + reason);
|
|
}
|
|
|
|
@Override
|
|
public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {
|
|
Log.d(TAG, "onSourceFound:");
|
|
getActivity().runOnUiThread(
|
|
() -> updateListCategoryFromBroadcastMetadata(source, false));
|
|
}
|
|
|
|
@Override
|
|
public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
|
|
setSourceId(sourceId);
|
|
if (mSelectedPreference == null) {
|
|
Log.w(TAG, "onSourceAdded: mSelectedPreference == null!");
|
|
return;
|
|
}
|
|
getActivity().runOnUiThread(() -> updateListCategoryFromBroadcastMetadata(
|
|
mSelectedPreference.getBluetoothLeBroadcastMetadata(), true));
|
|
}
|
|
|
|
@Override
|
|
public void onSourceAddFailed(@NonNull BluetoothDevice sink,
|
|
@NonNull BluetoothLeBroadcastMetadata source, int reason) {
|
|
mSelectedPreference = null;
|
|
Log.d(TAG, "onSourceAddFailed: clear the mSelectedPreference.");
|
|
}
|
|
|
|
@Override
|
|
public void onSourceModified(@NonNull BluetoothDevice sink, int sourceId,
|
|
int reason) {
|
|
}
|
|
|
|
@Override
|
|
public void onSourceModifyFailed(@NonNull BluetoothDevice sink, int sourceId,
|
|
int reason) {
|
|
}
|
|
|
|
@Override
|
|
public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId,
|
|
int reason) {
|
|
Log.d(TAG, "onSourceRemoved:");
|
|
getActivity().runOnUiThread(() -> handleSourceRemoved());
|
|
}
|
|
|
|
@Override
|
|
public void onSourceRemoveFailed(@NonNull BluetoothDevice sink, int sourceId,
|
|
int reason) {
|
|
Log.d(TAG, "onSourceRemoveFailed:");
|
|
}
|
|
|
|
@Override
|
|
public void onReceiveStateChanged(@NonNull BluetoothDevice sink, int sourceId,
|
|
@NonNull BluetoothLeBroadcastReceiveState state) {
|
|
Log.d(TAG, "onReceiveStateChanged:");
|
|
}
|
|
};
|
|
|
|
public BluetoothFindBroadcastsFragment() {
|
|
super(DISALLOW_CONFIG_BLUETOOTH);
|
|
}
|
|
|
|
@VisibleForTesting
|
|
LocalBluetoothManager getLocalBluetoothManager(Context context) {
|
|
return Utils.getLocalBtManager(context);
|
|
}
|
|
|
|
@VisibleForTesting
|
|
CachedBluetoothDevice getCachedDevice(String deviceAddress) {
|
|
BluetoothDevice remoteDevice =
|
|
mManager.getBluetoothAdapter().getRemoteDevice(deviceAddress);
|
|
return mManager.getCachedDeviceManager().findDevice(remoteDevice);
|
|
}
|
|
|
|
@Override
|
|
public void onAttach(Context context) {
|
|
mDeviceAddress = getArguments().getString(KEY_DEVICE_ADDRESS);
|
|
mManager = getLocalBluetoothManager(context);
|
|
mCachedDevice = getCachedDevice(mDeviceAddress);
|
|
mLeBroadcastAssistant = getLeBroadcastAssistant();
|
|
mExecutor = Executors.newSingleThreadExecutor();
|
|
mLocalBroadcastMetadata = new LocalBluetoothLeBroadcastMetadata();
|
|
|
|
super.onAttach(context);
|
|
if (mCachedDevice == null || mLeBroadcastAssistant == null) {
|
|
//Close this page if device is null with invalid device mac address
|
|
//or if the device does not have LeBroadcastAssistant profile
|
|
Log.w(TAG, "onAttach() CachedDevice or LeBroadcastAssistant is null!");
|
|
finish();
|
|
return;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(Bundle icicle) {
|
|
super.onCreate(icicle);
|
|
|
|
mBroadcastSourceListCategory = findPreference(PREF_KEY_BROADCAST_SOURCE_LIST);
|
|
}
|
|
|
|
@Override
|
|
public void onStart() {
|
|
super.onStart();
|
|
if (mLeBroadcastAssistant != null) {
|
|
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onResume() {
|
|
super.onResume();
|
|
finishFragmentIfNecessary();
|
|
//check assistant status. Start searching...
|
|
if (mLeBroadcastAssistant != null && !mLeBroadcastAssistant.isSearchInProgress()) {
|
|
mLeBroadcastAssistant.startSearchingForSources(getScanFilter());
|
|
} else {
|
|
addConnectedSourcePreference();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onStop() {
|
|
super.onStop();
|
|
if (mLeBroadcastAssistant != null) {
|
|
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
super.onActivityResult(requestCode, resultCode, data);
|
|
Log.d(TAG, "onActivityResult: " + requestCode + ", resultCode: " + resultCode);
|
|
if (requestCode == REQUEST_SCAN_BT_BROADCAST_QR_CODE) {
|
|
if (resultCode == Activity.RESULT_OK) {
|
|
|
|
//Get BroadcastMetadata
|
|
String broadcastMetadata = data.getStringExtra(
|
|
QrCodeScanModeFragment.KEY_BROADCAST_METADATA);
|
|
BluetoothLeBroadcastMetadata source = convertToBroadcastMetadata(broadcastMetadata);
|
|
|
|
if (source != null) {
|
|
Log.d(TAG, "onActivityResult source Id = " + source.getBroadcastId());
|
|
//Create preference for the broadcast source
|
|
updateListCategoryFromBroadcastMetadata(source, false);
|
|
//Add Source
|
|
addSource(mBroadcastSourceListCategory.findPreference(
|
|
Integer.toString(source.getBroadcastId())));
|
|
} else {
|
|
Toast.makeText(getContext(),
|
|
R.string.find_broadcast_join_broadcast_error, Toast.LENGTH_SHORT).show();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void finishFragmentIfNecessary() {
|
|
if (mCachedDevice.getBondState() == BOND_NONE) {
|
|
finish();
|
|
return;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int getMetricsCategory() {
|
|
//TODO(b/228255796) : add new enum for find broadcast fragment
|
|
return SettingsEnums.PAGE_UNKNOWN;
|
|
}
|
|
|
|
/**
|
|
* Starts to scan broadcast source by the BluetoothLeBroadcastAssistant.
|
|
*/
|
|
public void scanBroadcastSource() {
|
|
if (mLeBroadcastAssistant == null) {
|
|
Log.w(TAG, "scanBroadcastSource: LeBroadcastAssistant is null!");
|
|
return;
|
|
}
|
|
mLeBroadcastAssistant.startSearchingForSources(getScanFilter());
|
|
}
|
|
|
|
/**
|
|
* Leaves the broadcast source by the BluetoothLeBroadcastAssistant.
|
|
*/
|
|
public void leaveBroadcastSession() {
|
|
if (mLeBroadcastAssistant == null || mCachedDevice == null) {
|
|
Log.w(TAG, "leaveBroadcastSession: LeBroadcastAssistant or CachedDevice is null!");
|
|
return;
|
|
}
|
|
mLeBroadcastAssistant.removeSource(mCachedDevice.getDevice(), getSourceId());
|
|
}
|
|
|
|
@Override
|
|
protected String getLogTag() {
|
|
return TAG;
|
|
}
|
|
|
|
@Override
|
|
protected int getPreferenceScreenResId() {
|
|
return R.xml.bluetooth_find_broadcasts_fragment;
|
|
}
|
|
|
|
@Override
|
|
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
|
|
ArrayList<AbstractPreferenceController> controllers = new ArrayList<>();
|
|
|
|
if (mCachedDevice != null) {
|
|
Lifecycle lifecycle = getSettingsLifecycle();
|
|
mBluetoothFindBroadcastsHeaderController = new BluetoothFindBroadcastsHeaderController(
|
|
context, this, mCachedDevice, lifecycle, mManager);
|
|
controllers.add(mBluetoothFindBroadcastsHeaderController);
|
|
}
|
|
return controllers;
|
|
}
|
|
|
|
/**
|
|
* Gets the LocalBluetoothLeBroadcastAssistant
|
|
* @return the LocalBluetoothLeBroadcastAssistant
|
|
*/
|
|
public LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
|
|
if (mManager == null) {
|
|
Log.w(TAG, "getLeBroadcastAssistant: LocalBluetoothManager is null!");
|
|
return null;
|
|
}
|
|
|
|
LocalBluetoothProfileManager profileManager = mManager.getProfileManager();
|
|
if (profileManager == null) {
|
|
Log.w(TAG, "getLeBroadcastAssistant: LocalBluetoothProfileManager is null!");
|
|
return null;
|
|
}
|
|
|
|
return profileManager.getLeAudioBroadcastAssistantProfile();
|
|
}
|
|
|
|
private List<ScanFilter> getScanFilter() {
|
|
// Currently there is no function for setting the ScanFilter. It may have this function
|
|
// in the further.
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
private void updateListCategoryFromBroadcastMetadata(BluetoothLeBroadcastMetadata source,
|
|
boolean isConnected) {
|
|
BluetoothBroadcastSourcePreference item = mBroadcastSourceListCategory.findPreference(
|
|
Integer.toString(source.getBroadcastId()));
|
|
if (item == null) {
|
|
item = createBluetoothBroadcastSourcePreference();
|
|
item.setKey(Integer.toString(source.getBroadcastId()));
|
|
mBroadcastSourceListCategory.addPreference(item);
|
|
}
|
|
item.updateMetadataAndRefreshUi(source, isConnected);
|
|
item.setOrder(isConnected ? 0 : 1);
|
|
|
|
//refresh the header
|
|
if (mBluetoothFindBroadcastsHeaderController != null) {
|
|
mBluetoothFindBroadcastsHeaderController.refreshUi();
|
|
}
|
|
}
|
|
|
|
private void updateListCategoryFromBroadcastReceiveState(
|
|
BluetoothLeBroadcastReceiveState receiveState) {
|
|
BluetoothBroadcastSourcePreference item = mBroadcastSourceListCategory.findPreference(
|
|
Integer.toString(receiveState.getBroadcastId()));
|
|
if (item == null) {
|
|
item = createBluetoothBroadcastSourcePreference();
|
|
item.setKey(Integer.toString(receiveState.getBroadcastId()));
|
|
mBroadcastSourceListCategory.addPreference(item);
|
|
}
|
|
item.updateReceiveStateAndRefreshUi(receiveState);
|
|
item.setOrder(0);
|
|
|
|
setSourceId(receiveState.getSourceId());
|
|
mSelectedPreference = item;
|
|
|
|
//refresh the header
|
|
if (mBluetoothFindBroadcastsHeaderController != null) {
|
|
mBluetoothFindBroadcastsHeaderController.refreshUi();
|
|
}
|
|
}
|
|
|
|
private BluetoothBroadcastSourcePreference createBluetoothBroadcastSourcePreference() {
|
|
BluetoothBroadcastSourcePreference pref = new BluetoothBroadcastSourcePreference(
|
|
getContext());
|
|
pref.setOnPreferenceClickListener(preference -> {
|
|
if (pref.getBluetoothLeBroadcastMetadata() == null) {
|
|
Log.d(TAG, "BluetoothLeBroadcastMetadata is null, do nothing.");
|
|
return false;
|
|
}
|
|
if (pref.isEncrypted()) {
|
|
launchBroadcastCodeDialog(pref);
|
|
} else {
|
|
addSource(pref);
|
|
}
|
|
return true;
|
|
});
|
|
return pref;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void addSource(BluetoothBroadcastSourcePreference pref) {
|
|
if (mLeBroadcastAssistant == null || mCachedDevice == null) {
|
|
Log.w(TAG, "addSource: LeBroadcastAssistant or CachedDevice is null!");
|
|
return;
|
|
}
|
|
if (mSelectedPreference != null) {
|
|
if (mSelectedPreference.isCreatedByReceiveState()) {
|
|
Log.d(TAG, "addSource: Remove preference that created by getAllSources()");
|
|
getActivity().runOnUiThread(() ->
|
|
mBroadcastSourceListCategory.removePreference(mSelectedPreference));
|
|
if (mLeBroadcastAssistant != null && !mLeBroadcastAssistant.isSearchInProgress()) {
|
|
Log.d(TAG, "addSource: Start Searching For Broadcast Sources");
|
|
mLeBroadcastAssistant.startSearchingForSources(getScanFilter());
|
|
}
|
|
} else {
|
|
Log.d(TAG, "addSource: Update preference that created by onSourceFound()");
|
|
// The previous preference status set false after user selects the new Preference.
|
|
getActivity().runOnUiThread(
|
|
() -> {
|
|
mSelectedPreference.updateMetadataAndRefreshUi(
|
|
mSelectedPreference.getBluetoothLeBroadcastMetadata(), false);
|
|
mSelectedPreference.setOrder(1);
|
|
});
|
|
}
|
|
}
|
|
mSelectedPreference = pref;
|
|
mLeBroadcastAssistant.addSource(mCachedDevice.getDevice(),
|
|
pref.getBluetoothLeBroadcastMetadata(), true);
|
|
}
|
|
|
|
private void addBroadcastCodeIntoPreference(BluetoothBroadcastSourcePreference pref,
|
|
String broadcastCode) {
|
|
BluetoothLeBroadcastMetadata metadata =
|
|
new BluetoothLeBroadcastMetadata.Builder(pref.getBluetoothLeBroadcastMetadata())
|
|
.setBroadcastCode(broadcastCode.getBytes(StandardCharsets.UTF_8))
|
|
.build();
|
|
pref.updateMetadataAndRefreshUi(metadata, false);
|
|
}
|
|
|
|
private void launchBroadcastCodeDialog(BluetoothBroadcastSourcePreference pref) {
|
|
final View layout = LayoutInflater.from(getContext()).inflate(
|
|
R.layout.bluetooth_find_broadcast_password_dialog, null);
|
|
final TextView broadcastName = layout.requireViewById(R.id.broadcast_name_text);
|
|
final EditText editText = layout.requireViewById(R.id.broadcast_edit_text);
|
|
broadcastName.setText(pref.getTitle());
|
|
AlertDialog alertDialog = new AlertDialog.Builder(getContext())
|
|
.setTitle(R.string.find_broadcast_password_dialog_title)
|
|
.setView(layout)
|
|
.setNeutralButton(android.R.string.cancel, null)
|
|
.setPositiveButton(R.string.bluetooth_connect_access_dialog_positive,
|
|
(d, w) -> {
|
|
Log.d(TAG, "setPositiveButton: clicked");
|
|
if (pref.getBluetoothLeBroadcastMetadata() == null) {
|
|
Log.d(TAG, "BluetoothLeBroadcastMetadata is null, do nothing.");
|
|
return;
|
|
}
|
|
addBroadcastCodeIntoPreference(pref, editText.getText().toString());
|
|
addSource(pref);
|
|
})
|
|
.create();
|
|
|
|
alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
|
|
alertDialog.show();
|
|
}
|
|
|
|
private void handleSearchStarted() {
|
|
cacheRemoveAllPrefs(mBroadcastSourceListCategory);
|
|
addConnectedSourcePreference();
|
|
}
|
|
|
|
private void handleSourceRemoved() {
|
|
if (mSelectedPreference != null) {
|
|
if (mSelectedPreference.getBluetoothLeBroadcastMetadata() == null) {
|
|
mBroadcastSourceListCategory.removePreference(mSelectedPreference);
|
|
} else {
|
|
mSelectedPreference.clearReceiveState();
|
|
}
|
|
}
|
|
mSelectedPreference = null;
|
|
}
|
|
|
|
private void addConnectedSourcePreference() {
|
|
List<BluetoothLeBroadcastReceiveState> receiveStateList =
|
|
mLeBroadcastAssistant.getAllSources(mCachedDevice.getDevice());
|
|
if (!receiveStateList.isEmpty()) {
|
|
updateListCategoryFromBroadcastReceiveState(receiveStateList.get(0));
|
|
}
|
|
}
|
|
|
|
public int getSourceId() {
|
|
return mSourceId;
|
|
}
|
|
|
|
public void setSourceId(int sourceId) {
|
|
mSourceId = sourceId;
|
|
}
|
|
|
|
private BluetoothLeBroadcastMetadata convertToBroadcastMetadata(String qrCodeString) {
|
|
return mLocalBroadcastMetadata.convertToBroadcastMetadata(qrCodeString);
|
|
}
|
|
}
|