[Audiosharing] Implement name and password row.

Bug: 308368124
Test: manual
Change-Id: I86d0e771ece0ea7003a50ee0cc9305814d85fecb
This commit is contained in:
chelseahao
2024-01-23 14:58:03 +08:00
committed by Chelsea Hao
parent 317173b7cd
commit 9f23b4e45f
7 changed files with 420 additions and 17 deletions

View File

@@ -45,9 +45,15 @@
<com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreference <com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreference
android:key="audio_sharing_stream_name" android:key="audio_sharing_stream_name"
android:summary="********" android:title="Name"
android:title="Stream name"
settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreferenceController" /> 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 <SwitchPreferenceCompat
android:key="audio_sharing_stream_compatibility" android:key="audio_sharing_stream_compatibility"
android:title="Improve compatibility" android:title="Improve compatibility"

View File

@@ -19,6 +19,8 @@ package com.android.settings.connecteddevice.audiosharing;
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ImageButton; import android.widget.ImageButton;
import androidx.preference.PreferenceViewHolder; import androidx.preference.PreferenceViewHolder;
@@ -30,6 +32,7 @@ import com.android.settings.widget.ValidatedEditTextPreference;
public class AudioSharingNamePreference extends ValidatedEditTextPreference { public class AudioSharingNamePreference extends ValidatedEditTextPreference {
private static final String TAG = "AudioSharingNamePreference"; private static final String TAG = "AudioSharingNamePreference";
private boolean mShowQrCodeIcon = false;
public AudioSharingNamePreference( public AudioSharingNamePreference(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@@ -58,17 +61,50 @@ public class AudioSharingNamePreference extends ValidatedEditTextPreference {
setWidgetLayoutResource(R.layout.preference_widget_qrcode); setWidgetLayoutResource(R.layout.preference_widget_qrcode);
} }
void setShowQrCodeIcon(boolean show) {
mShowQrCodeIcon = show;
notifyChanged();
}
@Override @Override
public void onBindViewHolder(PreferenceViewHolder holder) { public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(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.setImageDrawable(getContext().getDrawable(R.drawable.ic_qrcode_24dp));
shareButton.setOnClickListener( shareButton.setOnClickListener(unused -> launchAudioSharingQrCodeFragment());
unused -> }
new SubSettingLauncher(getContext())
.setTitleText("Audio sharing QR code") private void configureInvisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
.setDestination(AudioStreamsQrCodeFragment.class.getName()) divider.setVisibility(View.INVISIBLE);
.setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS) shareButton.setVisibility(View.INVISIBLE);
.launch()); shareButton.setOnClickListener(null);
}
private void launchAudioSharingQrCodeFragment() {
new SubSettingLauncher(getContext())
.setTitleText("Audio sharing QR code")
.setDestination(AudioStreamsQrCodeFragment.class.getName())
.setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
.launch();
} }
} }

View File

@@ -16,25 +16,128 @@
package com.android.settings.connecteddevice.audiosharing; 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.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.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.bluetooth.Utils;
import com.android.settings.core.BasePreferenceController; import com.android.settings.core.BasePreferenceController;
import com.android.settings.widget.ValidatedEditTextPreference; 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 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 String TAG = "AudioSharingNamePreferenceController";
private static final boolean DEBUG = BluetoothUtils.D;
private static final String PREF_KEY = "audio_sharing_stream_name"; 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) { public AudioSharingNamePreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey); super(context, preferenceKey);
mLocalBtManager = Utils.getLocalBluetoothManager(context);
mBroadcast =
(mLocalBtManager != null)
? mLocalBtManager.getProfileManager().getLeAudioBroadcastProfile()
: null;
mAudioSharingNameTextValidator = new AudioSharingNameTextValidator(); 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 @Override
@@ -42,6 +145,17 @@ public class AudioSharingNamePreferenceController extends BasePreferenceControll
return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; 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);
updateBroadcastName();
updateQrCodeIcon(isBroadcasting(mLocalBtManager));
}
}
@Override @Override
public String getPreferenceKey() { public String getPreferenceKey() {
return PREF_KEY; return PREF_KEY;
@@ -49,10 +163,59 @@ public class AudioSharingNamePreferenceController extends BasePreferenceControll
@Override @Override
public boolean onPreferenceChange(Preference preference, Object newValue) { 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; 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 @Override
public boolean isTextValid(String value) { public boolean isTextValid(String value) {
return mAudioSharingNameTextValidator.isTextValid(value); return mAudioSharingNameTextValidator.isTextValid(value);

View File

@@ -18,10 +18,27 @@ package com.android.settings.connecteddevice.audiosharing;
import com.android.settings.widget.ValidatedEditTextPreference; 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 { public class AudioSharingNameTextValidator implements ValidatedEditTextPreference.Validator {
private static final int MIN_LENGTH = 4;
private static final int MAX_LENGTH = 32;
@Override @Override
public boolean isTextValid(String value) { public boolean isTextValid(String value) {
// TODO: Add validate rule if applicable. if (value == null || value.length() < MIN_LENGTH || value.length() > MAX_LENGTH) {
return true; 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);
} }
} }

View File

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

View File

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

View File

@@ -336,7 +336,7 @@ public class AudioSharingUtils {
} }
/** Returns if the broadcast is on-going. */ /** 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; if (manager == null) return false;
LocalBluetoothLeBroadcast broadcast = LocalBluetoothLeBroadcast broadcast =
manager.getProfileManager().getLeAudioBroadcastProfile(); manager.getProfileManager().getLeAudioBroadcastProfile();