[LE broadcast sink] Add the source list in boradcast sink UI.
Add the source list in boradcast sink UI.
Add the password dialog.
hsv link1: https://hsv.googleplex.com/6256032201310208
hsv link2: https://hsv.googleplex.com/5934966820044800
hsv link3: https://hsv.googleplex.com/6238095344140288
Bug: 228258236
Test: manual test
Change-Id: I698c2f7aba9baa9f143a98629b8796eda57fb379
diff --git a/res/drawable/bluetooth_broadcast_dialog_done.xml b/res/drawable/bluetooth_broadcast_dialog_done.xml
new file mode 100644
index 0000000..b2a5cc6
--- /dev/null
+++ b/res/drawable/bluetooth_broadcast_dialog_done.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M9.55,18 L3.85,12.3 5.275,10.875 9.55,15.15 18.725,5.975 20.15,7.4Z"/>
+</vector>
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 0000000..f9df3f5
--- /dev/null
+++ b/res/layout/bluetooth_find_broadcast_password_dialog.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="?android:attr/dialogPreferredPadding"
+ android:paddingRight="?android:attr/dialogPreferredPadding"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/broadcast_name_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp"
+ android:textAlignment="viewStart"/>
+ <EditText
+ android:id="@+id/broadcast_edit_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp"
+ android:textAlignment="viewStart"/>
+ <TextView
+ android:id="@+id/broadcast_error_message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ style="@style/TextAppearance.ErrorText"
+ android:visibility="invisible"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 63db309..27e449f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -14139,4 +14139,10 @@
<string name="bluetooth_find_broadcast_button_leave">Leave broadcast</string>
<!-- The Button of the action to scan QR code [CHAR LIMIT=none] -->
<string name="bluetooth_find_broadcast_button_scan">Scan QR code</string>
+ <!-- The title of enter password dialog in bluetooth find broadcast page. [CHAR LIMIT=none] -->
+ <string name="find_broadcast_password_dialog_title">Enter password</string>
+ <!-- The error message of enter password dialog in bluetooth find broadcast page [CHAR LIMIT=none] -->
+ <string name="find_broadcast_password_dialog_connection_error">Can\u2019t connect. Try again.</string>
+ <!-- The error message of enter password dialog in bluetooth find broadcast page [CHAR LIMIT=none] -->
+ <string name="find_broadcast_password_dialog_password_error">Wrong password</string>
</resources>
diff --git a/src/com/android/settings/bluetooth/BluetoothBroadcastSourcePreference.java b/src/com/android/settings/bluetooth/BluetoothBroadcastSourcePreference.java
new file mode 100644
index 0000000..17b604c
--- /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<BluetoothLeBroadcastSubgroup> 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 f251db5..9a26a57 100644
--- a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
@@ -19,33 +19,53 @@
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 @@
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 @@
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 @@
}
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<ScanFilter> 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;
+ }
}