[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
This commit is contained in:
SongFerngWang
2022-05-03 08:27:07 +08:00
committed by SongFerng Wang
parent 74b1027fe2
commit 1709c80eff
5 changed files with 463 additions and 5 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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));
}
}

View File

@@ -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<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;
}
}