From 9f23b4e45fd9bf5090131407d92616d2ff3a56cb Mon Sep 17 00:00:00 2001 From: chelseahao Date: Tue, 23 Jan 2024 14:58:03 +0800 Subject: [PATCH] [Audiosharing] Implement name and password row. Bug: 308368124 Test: manual Change-Id: I86d0e771ece0ea7003a50ee0cc9305814d85fecb --- res/xml/bluetooth_audio_sharing.xml | 10 +- .../AudioSharingNamePreference.java | 52 +++++- .../AudioSharingNamePreferenceController.java | 171 +++++++++++++++++- .../AudioSharingNameTextValidator.java | 21 ++- ...ioSharingPasswordPreferenceController.java | 130 +++++++++++++ .../AudioSharingPasswordValidator.java | 51 ++++++ .../audiosharing/AudioSharingUtils.java | 2 +- 7 files changed, 420 insertions(+), 17 deletions(-) create mode 100644 src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java create mode 100644 src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java diff --git a/res/xml/bluetooth_audio_sharing.xml b/res/xml/bluetooth_audio_sharing.xml index d3aad228b07..5de8dfcf0d7 100644 --- a/res/xml/bluetooth_audio_sharing.xml +++ b/res/xml/bluetooth_audio_sharing.xml @@ -45,9 +45,15 @@ + + + - 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 a3eb188fa2a..644e05eb86e 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 @@ -42,6 +145,17 @@ public class AudioSharingNamePreferenceController extends BasePreferenceControll 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 public String getPreferenceKey() { return PREF_KEY; @@ -49,10 +163,59 @@ public class AudioSharingNamePreferenceController extends BasePreferenceControll @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 94929614230..2022eb260d6 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java @@ -18,10 +18,27 @@ package com.android.settings.connecteddevice.audiosharing; 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 00000000000..da0eb2eb78e --- /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 00000000000..dbb40ece2b0 --- /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 924b04d3da2..242ce20efe9 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java @@ -336,7 +336,7 @@ public class AudioSharingUtils { } /** 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();