/* * 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 createPreferenceControllers(Context context) { ArrayList 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 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 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); } }