[Audiosharing] Implement name and password row.
Bug: 308368124
Test: manual
Change-Id: I86d0e771ece0ea7003a50ee0cc9305814d85fecb
diff --git a/res/xml/bluetooth_audio_sharing.xml b/res/xml/bluetooth_audio_sharing.xml
index d3aad22..5de8dfc 100644
--- a/res/xml/bluetooth_audio_sharing.xml
+++ b/res/xml/bluetooth_audio_sharing.xml
@@ -45,9 +45,15 @@
<com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreference
android:key="audio_sharing_stream_name"
- android:summary="********"
- android:title="Stream name"
+ android:title="Name"
settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreferenceController" />
+
+ <com.android.settings.widget.ValidatedEditTextPreference
+ android:key="audio_sharing_stream_password"
+ android:summary="********"
+ android:title="Password"
+ settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPasswordPreferenceController" />
+
<SwitchPreferenceCompat
android:key="audio_sharing_stream_compatibility"
android:title="Improve compatibility"
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
index 81465ed..44c947d 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
@@ -19,6 +19,8 @@
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
import android.widget.ImageButton;
import androidx.preference.PreferenceViewHolder;
@@ -30,6 +32,7 @@
public class AudioSharingNamePreference extends ValidatedEditTextPreference {
private static final String TAG = "AudioSharingNamePreference";
+ private boolean mShowQrCodeIcon = false;
public AudioSharingNamePreference(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@@ -58,17 +61,50 @@
setWidgetLayoutResource(R.layout.preference_widget_qrcode);
}
+ void setShowQrCodeIcon(boolean show) {
+ mShowQrCodeIcon = show;
+ notifyChanged();
+ }
+
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
- final ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
+
+ ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
+ View divider =
+ holder.findViewById(
+ com.android.settingslib.widget.preference.twotarget.R.id
+ .two_target_divider);
+
+ if (shareButton != null && divider != null) {
+ if (mShowQrCodeIcon) {
+ configureVisibleStateForQrCodeIcon(shareButton, divider);
+ } else {
+ configureInvisibleStateForQrCodeIcon(shareButton, divider);
+ }
+ } else {
+ Log.w(TAG, "onBindViewHolder() : shareButton or divider is null!");
+ }
+ }
+
+ private void configureVisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
+ divider.setVisibility(View.VISIBLE);
+ shareButton.setVisibility(View.VISIBLE);
shareButton.setImageDrawable(getContext().getDrawable(R.drawable.ic_qrcode_24dp));
- shareButton.setOnClickListener(
- unused ->
- new SubSettingLauncher(getContext())
- .setTitleText("Audio sharing QR code")
- .setDestination(AudioStreamsQrCodeFragment.class.getName())
- .setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
- .launch());
+ shareButton.setOnClickListener(unused -> launchAudioSharingQrCodeFragment());
+ }
+
+ private void configureInvisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
+ divider.setVisibility(View.INVISIBLE);
+ shareButton.setVisibility(View.INVISIBLE);
+ shareButton.setOnClickListener(null);
+ }
+
+ private void launchAudioSharingQrCodeFragment() {
+ new SubSettingLauncher(getContext())
+ .setTitleText("Audio sharing QR code")
+ .setDestination(AudioStreamsQrCodeFragment.class.getName())
+ .setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
+ .launch();
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
index a3eb188..644e05e 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
@@ -16,25 +16,128 @@
package com.android.settings.connecteddevice.audiosharing;
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.isBroadcasting;
+
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.content.Context;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.widget.ValidatedEditTextPreference;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
public class AudioSharingNamePreferenceController extends BasePreferenceController
- implements ValidatedEditTextPreference.Validator, Preference.OnPreferenceChangeListener {
+ implements ValidatedEditTextPreference.Validator,
+ Preference.OnPreferenceChangeListener,
+ DefaultLifecycleObserver {
private static final String TAG = "AudioSharingNamePreferenceController";
-
+ private static final boolean DEBUG = BluetoothUtils.D;
private static final String PREF_KEY = "audio_sharing_stream_name";
- private AudioSharingNameTextValidator mAudioSharingNameTextValidator;
+ private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+ new BluetoothLeBroadcast.Callback() {
+ @Override
+ public void onBroadcastMetadataChanged(
+ int broadcastId, BluetoothLeBroadcastMetadata metadata) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onBroadcastMetadataChanged() broadcastId : "
+ + broadcastId
+ + " metadata: "
+ + metadata);
+ }
+ updateQrCodeIcon(true);
+ }
+
+ @Override
+ public void onBroadcastStartFailed(int reason) {}
+
+ @Override
+ public void onBroadcastStarted(int reason, int broadcastId) {}
+
+ @Override
+ public void onBroadcastStopFailed(int reason) {}
+
+ @Override
+ public void onBroadcastStopped(int reason, int broadcastId) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onBroadcastStopped() reason : "
+ + reason
+ + " broadcastId: "
+ + broadcastId);
+ }
+ updateQrCodeIcon(false);
+ }
+
+ @Override
+ public void onBroadcastUpdateFailed(int reason, int broadcastId) {
+ Log.w(TAG, "onBroadcastUpdateFailed() reason : " + reason);
+ // Do nothing if update failed.
+ }
+
+ @Override
+ public void onBroadcastUpdated(int reason, int broadcastId) {
+ if (DEBUG) {
+ Log.d(TAG, "onBroadcastUpdated() reason : " + reason);
+ }
+ updateBroadcastName();
+ }
+
+ @Override
+ public void onPlaybackStarted(int reason, int broadcastId) {}
+
+ @Override
+ public void onPlaybackStopped(int reason, int broadcastId) {}
+ };
+
+ @Nullable private final LocalBluetoothManager mLocalBtManager;
+ @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
+ private final Executor mExecutor;
+ private final AudioSharingNameTextValidator mAudioSharingNameTextValidator;
+ @Nullable private AudioSharingNamePreference mPreference;
public AudioSharingNamePreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
+ mLocalBtManager = Utils.getLocalBluetoothManager(context);
+ mBroadcast =
+ (mLocalBtManager != null)
+ ? mLocalBtManager.getProfileManager().getLeAudioBroadcastProfile()
+ : null;
mAudioSharingNameTextValidator = new AudioSharingNameTextValidator();
+ mExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {
+ if (mBroadcast != null) {
+ mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
+ }
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ if (mBroadcast != null) {
+ mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+ }
}
@Override
@@ -43,16 +146,76 @@
}
@Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPreference = screen.findPreference(getPreferenceKey());
+ if (mPreference != null) {
+ mPreference.setValidator(this);
+ updateBroadcastName();
+ updateQrCodeIcon(isBroadcasting(mLocalBtManager));
+ }
+ }
+
+ @Override
public String getPreferenceKey() {
return PREF_KEY;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
- // TODO: update broadcast when name is changed.
+ if (mPreference != null
+ && mPreference.getSummary() != null
+ && ((String) newValue).contentEquals(mPreference.getSummary())) {
+ return false;
+ }
+
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ if (mBroadcast != null) {
+ mBroadcast.setProgramInfo((String) newValue);
+ if (isBroadcasting(mLocalBtManager)) {
+ // Update broadcast, UI update will be handled after callback
+ mBroadcast.updateBroadcast();
+ } else {
+ // Directly update UI if no ongoing broadcast
+ updateBroadcastName();
+ }
+ }
+ });
return true;
}
+ private void updateBroadcastName() {
+ if (mPreference != null) {
+ var unused =
+ ThreadUtils.postOnBackgroundThread(
+ () -> {
+ if (mBroadcast != null) {
+ String name = mBroadcast.getProgramInfo();
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mPreference != null) {
+ mPreference.setText(name);
+ mPreference.setSummary(name);
+ }
+ });
+ }
+ });
+ }
+ }
+
+ private void updateQrCodeIcon(boolean show) {
+ if (mPreference != null) {
+ ThreadUtils.postOnMainThread(
+ () -> {
+ if (mPreference != null) {
+ mPreference.setShowQrCodeIcon(show);
+ }
+ });
+ }
+ }
+
@Override
public boolean isTextValid(String value) {
return mAudioSharingNameTextValidator.isTextValid(value);
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
index 9492961..2022eb2 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
@@ -18,10 +18,27 @@
import com.android.settings.widget.ValidatedEditTextPreference;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Validator for Audio Sharing Name, which should be a UTF-8 encoded string containing a minimum of
+ * 4 characters and a maximum of 32 human-readable characters.
+ */
public class AudioSharingNameTextValidator implements ValidatedEditTextPreference.Validator {
+ private static final int MIN_LENGTH = 4;
+ private static final int MAX_LENGTH = 32;
+
@Override
public boolean isTextValid(String value) {
- // TODO: Add validate rule if applicable.
- return true;
+ if (value == null || value.length() < MIN_LENGTH || value.length() > MAX_LENGTH) {
+ return false;
+ }
+ return isValidUTF8(value);
+ }
+
+ private static boolean isValidUTF8(String value) {
+ byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
+ String reconstructedString = new String(bytes, StandardCharsets.UTF_8);
+ return value.equals(reconstructedString);
}
}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
new file mode 100644
index 0000000..da0eb2e
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2023 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.connecteddevice.audiosharing;
+
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.widget.ValidatedEditTextPreference;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class AudioSharingPasswordPreferenceController extends BasePreferenceController
+ implements ValidatedEditTextPreference.Validator,
+ Preference.OnPreferenceChangeListener,
+ DefaultLifecycleObserver {
+ private static final String PREF_KEY = "audio_sharing_stream_password";
+
+ private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+ new BluetoothLeBroadcast.Callback() {
+ @Override
+ public void onBroadcastMetadataChanged(
+ int broadcastId, BluetoothLeBroadcastMetadata metadata) {}
+
+ @Override
+ public void onBroadcastStartFailed(int reason) {}
+
+ @Override
+ public void onBroadcastStarted(int reason, int broadcastId) {}
+
+ @Override
+ public void onBroadcastStopFailed(int reason) {}
+
+ @Override
+ public void onBroadcastStopped(int reason, int broadcastId) {}
+
+ @Override
+ public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
+
+ @Override
+ public void onBroadcastUpdated(int reason, int broadcastId) {}
+
+ @Override
+ public void onPlaybackStarted(int reason, int broadcastId) {}
+
+ @Override
+ public void onPlaybackStopped(int reason, int broadcastId) {}
+ };
+ @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
+ private final Executor mExecutor;
+ private final AudioSharingPasswordValidator mAudioSharingPasswordValidator;
+ @Nullable private ValidatedEditTextPreference mPreference;
+
+ public AudioSharingPasswordPreferenceController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
+ mBroadcast =
+ Utils.getLocalBtManager(context).getProfileManager().getLeAudioBroadcastProfile();
+ mAudioSharingPasswordValidator = new AudioSharingPasswordValidator();
+ mExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {
+ if (mBroadcast != null) {
+ mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
+ }
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ if (mBroadcast != null) {
+ mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+ }
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPreference = screen.findPreference(getPreferenceKey());
+ if (mPreference != null) {
+ mPreference.setValidator(this);
+ }
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return PREF_KEY;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ // TODO(chelseahao): implement
+ return true;
+ }
+
+ @Override
+ public boolean isTextValid(String value) {
+ return mAudioSharingPasswordValidator.isTextValid(value);
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java
new file mode 100644
index 0000000..dbb40ec
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 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.connecteddevice.audiosharing;
+
+import com.android.settings.widget.ValidatedEditTextPreference;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Validator for Audio Sharing Password, which should be a UTF-8 string that has at least 4 octets
+ * and should not exceed 16 octets.
+ */
+public class AudioSharingPasswordValidator implements ValidatedEditTextPreference.Validator {
+ private static final int MIN_OCTETS = 4;
+ private static final int MAX_OCTETS = 16;
+
+ @Override
+ public boolean isTextValid(String value) {
+ if (value == null
+ || getOctetsCount(value) < MIN_OCTETS
+ || getOctetsCount(value) > MAX_OCTETS) {
+ return false;
+ }
+
+ return isValidUTF8(value);
+ }
+
+ private static int getOctetsCount(String value) {
+ return value.getBytes(StandardCharsets.UTF_8).length;
+ }
+
+ private static boolean isValidUTF8(String value) {
+ byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
+ String reconstructedString = new String(bytes, StandardCharsets.UTF_8);
+ return value.equals(reconstructedString);
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
index 924b04d..242ce20 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
@@ -336,7 +336,7 @@
}
/** Returns if the broadcast is on-going. */
- public static boolean isBroadcasting(LocalBluetoothManager manager) {
+ public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
if (manager == null) return false;
LocalBluetoothLeBroadcast broadcast =
manager.getProfileManager().getLeAudioBroadcastProfile();