diff --git a/res/drawable/bluetooth_broadcast_dialog_done.xml b/res/drawable/bluetooth_broadcast_dialog_done.xml new file mode 100644 index 00000000000..b2a5cc62681 --- /dev/null +++ b/res/drawable/bluetooth_broadcast_dialog_done.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/res/layout/bluetooth_find_broadcast_password_dialog.xml b/res/layout/bluetooth_find_broadcast_password_dialog.xml new file mode 100644 index 00000000000..f9df3f51a8a --- /dev/null +++ b/res/layout/bluetooth_find_broadcast_password_dialog.xml @@ -0,0 +1,43 @@ + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 63db309b17f..27e449fe5ae 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -14139,4 +14139,10 @@ Leave broadcast Scan QR code + + Enter password + + Can\u2019t connect. Try again. + + Wrong password diff --git a/src/com/android/settings/bluetooth/BluetoothBroadcastSourcePreference.java b/src/com/android/settings/bluetooth/BluetoothBroadcastSourcePreference.java new file mode 100644 index 00000000000..17b604c58ff --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothBroadcastSourcePreference.java @@ -0,0 +1,141 @@ +/* + * 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 android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastSubgroup; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; +import com.android.settingslib.Utils; + +import java.util.List; + +/** + * Preference to display a broadcast source in the Broadcast Source List. + */ +class BluetoothBroadcastSourcePreference extends Preference { + + private static final int RESOURCE_ID_UNKNOWN_PROGRAM_INFO = R.string.device_info_default; + private static final int RESOURCE_ID_ICON = R.drawable.settings_input_antenna; + + private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata; + private ImageView mFrictionImageView; + private String mTitle; + private boolean mStatus; + private boolean mIsEncrypted; + + BluetoothBroadcastSourcePreference(@NonNull Context context, + @NonNull BluetoothLeBroadcastMetadata source) { + super(context); + initUi(); + updateMetadataAndRefreshUi(source, false); + } + + @Override + public void onBindViewHolder(final PreferenceViewHolder view) { + super.onBindViewHolder(view); + view.findViewById(R.id.two_target_divider).setVisibility(View.INVISIBLE); + final ImageButton imageButton = (ImageButton) view.findViewById(R.id.icon_button); + imageButton.setVisibility(View.GONE); + mFrictionImageView = (ImageView) view.findViewById(R.id.friction_icon); + updateStatusButton(); + } + + private void initUi() { + setLayoutResource(R.layout.preference_access_point); + setWidgetLayoutResource(R.layout.access_point_friction_widget); + + mStatus = false; + final Drawable drawable = getContext().getDrawable(RESOURCE_ID_ICON); + if (drawable != null) { + drawable.setTint(Utils.getColorAttrDefaultColor(getContext(), + android.R.attr.colorControlNormal)); + setIcon(drawable); + } + } + + private void updateStatusButton() { + if (mFrictionImageView == null) { + return; + } + if (mStatus || mIsEncrypted) { + Drawable drawable; + if (mStatus) { + drawable = getContext().getDrawable(R.drawable.bluetooth_broadcast_dialog_done); + } else { + drawable = getContext().getDrawable(R.drawable.ic_friction_lock_closed); + } + if (drawable != null) { + drawable.setTint(Utils.getColorAttrDefaultColor(getContext(), + android.R.attr.colorControlNormal)); + mFrictionImageView.setImageDrawable(drawable); + } + mFrictionImageView.setVisibility(View.VISIBLE); + } else { + mFrictionImageView.setVisibility(View.GONE); + } + } + + /** + * Updates the title and status from BluetoothLeBroadcastMetadata. + */ + public void updateMetadataAndRefreshUi(BluetoothLeBroadcastMetadata source, boolean status) { + mBluetoothLeBroadcastMetadata = source; + mTitle = getBroadcastMetadataProgramInfo(); + mIsEncrypted = mBluetoothLeBroadcastMetadata.isEncrypted(); + mStatus = status; + + refresh(); + } + + /** + * Gets the BluetoothLeBroadcastMetadata. + */ + public BluetoothLeBroadcastMetadata getBluetoothLeBroadcastMetadata() { + return mBluetoothLeBroadcastMetadata; + } + + private void refresh() { + setTitle(mTitle); + updateStatusButton(); + } + + private String getBroadcastMetadataProgramInfo() { + if (mBluetoothLeBroadcastMetadata == null) { + return getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO); + } + final List subgroups = + mBluetoothLeBroadcastMetadata.getSubgroups(); + if (subgroups.isEmpty()) { + return getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO); + } + return subgroups.stream() + .map(i -> i.getContentMetadata().getProgramInfo()) + .filter(i -> !TextUtils.isEmpty(i)) + .findFirst().orElse(getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO)); + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java index f251db50ab9..9a26a57f0fc 100644 --- a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java @@ -19,33 +19,53 @@ package com.android.settings.bluetooth; import static android.bluetooth.BluetoothDevice.BOND_NONE; import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; +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.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 androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +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.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"; + private static final String TAG = "BtFindBroadcastsFrg"; public static final String KEY_DEVICE_ADDRESS = "device_address"; - - public static final String PREF_KEY_BROADCAST_SOURCE = "broadcast_source"; + public static final String PREF_KEY_BROADCAST_SOURCE_LIST = "broadcast_source_list"; @VisibleForTesting String mDeviceAddress; @@ -53,6 +73,91 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment LocalBluetoothManager mManager; @VisibleForTesting CachedBluetoothDevice mCachedDevice; + @VisibleForTesting + PreferenceCategory mBroadcastSourceListCategory; + private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; + private BluetoothBroadcastSourcePreference mSelectedPreference; + 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( + () -> cacheRemoveAllPrefs(mBroadcastSourceListCategory)); + } + + @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(() -> updateListCategory(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(() -> updateListCategory( + 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:"); + } + + @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); @@ -75,19 +180,50 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment mDeviceAddress = getArguments().getString(KEY_DEVICE_ADDRESS); mManager = getLocalBluetoothManager(context); mCachedDevice = getCachedDevice(mDeviceAddress); + mLeBroadcastAssistant = getLeBroadcastAssistant(); + mExecutor = Executors.newSingleThreadExecutor(); + super.onAttach(context); - if (mCachedDevice == null) { + if (mCachedDevice == null || mLeBroadcastAssistant == null) { //Close this page if device is null with invalid device mac address - Log.w(TAG, "onAttach() CachedDevice is null!"); + //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()); + } + } + + @Override + public void onStop() { + super.onStop(); + if (mLeBroadcastAssistant != null) { + mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); + } } @VisibleForTesting @@ -125,4 +261,110 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment } return controllers; } + + private 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 updateListCategory(BluetoothLeBroadcastMetadata source, boolean isConnected) { + BluetoothBroadcastSourcePreference item = mBroadcastSourceListCategory.findPreference( + Integer.toString(source.getBroadcastId())); + if (item == null) { + item = createBluetoothBroadcastSourcePreference(source); + mBroadcastSourceListCategory.addPreference(item); + } + item.updateMetadataAndRefreshUi(source, isConnected); + item.setOrder(isConnected ? 0 : 1); + } + + private BluetoothBroadcastSourcePreference createBluetoothBroadcastSourcePreference( + BluetoothLeBroadcastMetadata source) { + BluetoothBroadcastSourcePreference pref = new BluetoothBroadcastSourcePreference( + getContext(), source); + pref.setKey(Integer.toString(source.getBroadcastId())); + pref.setOnPreferenceClickListener(preference -> { + if (source.isEncrypted()) { + launchBroadcastCodeDialog(pref); + } else { + addSource(pref); + } + return true; + }); + return pref; + } + + private void addSource(BluetoothBroadcastSourcePreference pref) { + if (mLeBroadcastAssistant == null || mCachedDevice == null) { + Log.w(TAG, "addSource: LeBroadcastAssistant or CachedDevice is null!"); + return; + } + if (mSelectedPreference != null) { + // 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"); + addBroadcastCodeIntoPreference(pref, editText.getText().toString()); + addSource(pref); + }) + .create(); + + alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + alertDialog.show(); + } + + public int getSourceId() { + return mSourceId; + } + + public void setSourceId(int sourceId) { + mSourceId = sourceId; + } }