From c2d7daf645ff49d6ec19f029ad7179817677d9e9 Mon Sep 17 00:00:00 2001 From: Chun-Ku Lin Date: Sat, 20 Jan 2024 05:40:52 +0000 Subject: [PATCH 01/15] Replace ShortcutType with UserShortcutType to reduce duplicate declaration. Bug: 317424693 Test: atest Flag: N/A. Not able to flag the refactor Change-Id: I20aea442cb08f047f98422176058acf4b344d2f1 --- .../settings/gestures/SystemNavigationGestureSettings.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java index c40212b7e01..c6b1bdb6ada 100644 --- a/src/com/android/settings/gestures/SystemNavigationGestureSettings.java +++ b/src/com/android/settings/gestures/SystemNavigationGestureSettings.java @@ -41,6 +41,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceScreen; +import com.android.internal.accessibility.common.ShortcutConstants; import com.android.settings.R; import com.android.settings.accessibility.AccessibilityGestureNavigationTutorial; import com.android.settings.core.SubSettingLauncher; @@ -353,7 +354,7 @@ public class SystemNavigationGestureSettings extends RadioButtonPickerFragment i private boolean isAnyServiceSupportAccessibilityButton() { final AccessibilityManager ams = getContext().getSystemService(AccessibilityManager.class); final List targets = ams.getAccessibilityShortcutTargets( - AccessibilityManager.ACCESSIBILITY_BUTTON); + ShortcutConstants.UserShortcutType.SOFTWARE); return !targets.isEmpty(); } From 647c381848c78fd15b236adb7d50a27770056904 Mon Sep 17 00:00:00 2001 From: Fan Wu Date: Wed, 24 Jan 2024 10:46:32 +0800 Subject: [PATCH 02/15] Fix ProfileSelectLocationFragmentTest The test environment is not properly setup Bug: 313569889 Test: atest Change-Id: I5424e998cc22bf7bbddff6f69bfc7c4e56ea870c --- .../ProfileSelectLocationFragmentTest.java | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectLocationFragmentTest.java b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectLocationFragmentTest.java index e30759af8c1..22fec8f058e 100644 --- a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectLocationFragmentTest.java +++ b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectLocationFragmentTest.java @@ -16,27 +16,58 @@ package com.android.settings.dashboard.profileselector; +import static android.os.UserManager.USER_TYPE_FULL_SYSTEM; +import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; +import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; + import static com.android.settings.dashboard.profileselector.ProfileSelectFragment.EXTRA_PROFILE; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.pm.UserInfo; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.testutils.shadow.ShadowUserManager; + import org.junit.Before; -import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; -@Ignore("b/313569889") +@Config(shadows = { + ShadowUserManager.class, +}) @RunWith(RobolectricTestRunner.class) public class ProfileSelectLocationFragmentTest { + private static final String PERSONAL_PROFILE_NAME = "personal"; + private static final String WORK_PROFILE_NAME = "work"; + private static final String PRIVATE_PROFILE_NAME = "private"; + @Rule + public final MockitoRule rule = MockitoJUnit.rule(); + private ShadowUserManager mUserManager; private ProfileSelectLocationFragment mProfileSelectLocationFragment; @Before public void setUp() { - MockitoAnnotations.initMocks(this); - mProfileSelectLocationFragment = new ProfileSelectLocationFragment(); + mUserManager = ShadowUserManager.getShadow(); + mUserManager.addProfile( + new UserInfo(0, PERSONAL_PROFILE_NAME, null, 0, USER_TYPE_FULL_SYSTEM)); + mUserManager.addProfile( + new UserInfo(1, WORK_PROFILE_NAME, null, 0, USER_TYPE_PROFILE_MANAGED)); + mUserManager.addProfile( + new UserInfo(11, PRIVATE_PROFILE_NAME, null, 0, USER_TYPE_PROFILE_PRIVATE)); + mProfileSelectLocationFragment = spy(new ProfileSelectLocationFragment()); + when(mProfileSelectLocationFragment.getContext()).thenReturn( + ApplicationProvider.getApplicationContext()); } @Test @@ -46,7 +77,7 @@ public class ProfileSelectLocationFragmentTest { EXTRA_PROFILE, -1)).isEqualTo(ProfileSelectFragment.ProfileType.PERSONAL); assertThat(mProfileSelectLocationFragment.getFragments()[1].getArguments().getInt( EXTRA_PROFILE, -1)).isEqualTo(ProfileSelectFragment.ProfileType.WORK); - assertThat(mProfileSelectLocationFragment.getFragments()[1].getArguments().getInt( + assertThat(mProfileSelectLocationFragment.getFragments()[2].getArguments().getInt( EXTRA_PROFILE, -1)).isEqualTo(ProfileSelectFragment.ProfileType.PRIVATE); } } From 3a86c8e77f0702b03a2517f52e76704bb951e6d6 Mon Sep 17 00:00:00 2001 From: Bill Yi Date: Wed, 24 Jan 2024 21:39:03 -0800 Subject: [PATCH 03/15] Import translations. DO NOT MERGE ANYWHERE Auto-generated-cl: translation import Change-Id: Ia0ae85fde8ff629355d440e59dd1bfd22e9d544d --- res/values-eu/arrays.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/values-eu/arrays.xml b/res/values-eu/arrays.xml index dcb153c2620..700229c4c2b 100644 --- a/res/values-eu/arrays.xml +++ b/res/values-eu/arrays.xml @@ -196,7 +196,7 @@ "bolumen nagusia" "ahotsaren bolumena" "tonuaren bolumena" - "multimedia-elementuen bolumena" + "multimedia-edukiaren bolumena" "alarmaren bolumena" "jakinarazpenen bolumena" "Bluetooth bidezko audioaren bolumena" @@ -263,7 +263,7 @@ "Bolumen nagusia" "Ahotsaren bolumena" "Tonuaren bolumena" - "Multimedia-elementuen bolumena" + "Multimedia-edukiaren bolumena" "Alarmaren bolumena" "Jakinarazpenen bolumena" "Bluetooth bidezko audioaren bolumena" From 745c69b76adbe363353ca9f90dc0926f4644d236 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Thu, 25 Jan 2024 20:22:45 +0000 Subject: [PATCH 04/15] Make MoreOptionsScope abstract class Bug: 321724969 Test: m Settings Test: unit test Merged-In: Iced9df83f600c86cc409abc040fb9ace0dcedf1e Change-Id: Iced9df83f600c86cc409abc040fb9ace0dcedf1e --- .../src/com/android/settings/spa/app/ResetAppPreferencesTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/spa_unit/src/com/android/settings/spa/app/ResetAppPreferencesTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/ResetAppPreferencesTest.kt index ffd58313f1b..b63c28184c8 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/ResetAppPreferencesTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/ResetAppPreferencesTest.kt @@ -45,7 +45,7 @@ class ResetAppPreferencesTest { } private fun setResetAppPreferences() { - val fakeMoreOptionsScope = object : MoreOptionsScope { + val fakeMoreOptionsScope = object : MoreOptionsScope() { override fun dismiss() {} } composeTestRule.setContent { From 9f23b4e45fd9bf5090131407d92616d2ff3a56cb Mon Sep 17 00:00:00 2001 From: chelseahao Date: Tue, 23 Jan 2024 14:58:03 +0800 Subject: [PATCH 05/15] [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(); From 685befa86a4ce98eacc90d84063b0b40f2fcbdc8 Mon Sep 17 00:00:00 2001 From: SongFerngWang Date: Thu, 18 Jan 2024 14:21:48 +0800 Subject: [PATCH 06/15] SIMs page enhancement This is for the Sim onboarding UI enhancement - create new SIMs spa UI - hide the Call & SMS when flag is on Bug: 318310357 Bug: 298898436 Bug: 298891941 Test: munally test Change-Id: Iaecb8fe435b26f2952263024c93c8719feda96a4 --- res/xml/network_provider_internet.xml | 1 - .../network/MobileNetworkListFragment.kt | 12 + .../network/SubscriptionInfoListViewModel.kt | 12 + .../settings/spa/SettingsSpaEnvironment.kt | 2 + .../network/NetworkCellularGroupProvider.kt | 465 ++++++++++++++++++ .../spa/network/SimOnboardingPrimarySim.kt | 26 +- 6 files changed, 505 insertions(+), 13 deletions(-) create mode 100644 src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt diff --git a/res/xml/network_provider_internet.xml b/res/xml/network_provider_internet.xml index 1a8ee0872cc..04f248ed745 100644 --- a/res/xml/network_provider_internet.xml +++ b/res/xml/network_provider_internet.xml @@ -52,7 +52,6 @@ settings:keywords="@string/keywords_more_mobile_networks" settings:userRestriction="no_config_mobile_networks" settings:isPreferenceVisible="@bool/config_show_sim_info" - settings:allowDividerAbove="true" settings:useAdminDisabledSummary="true" settings:searchable="@bool/config_show_sim_info"/> diff --git a/src/com/android/settings/network/MobileNetworkListFragment.kt b/src/com/android/settings/network/MobileNetworkListFragment.kt index 09b1150d196..e7228666da7 100644 --- a/src/com/android/settings/network/MobileNetworkListFragment.kt +++ b/src/com/android/settings/network/MobileNetworkListFragment.kt @@ -26,8 +26,11 @@ import androidx.preference.Preference import com.android.settings.R import com.android.settings.SettingsPreferenceFragment import com.android.settings.dashboard.DashboardFragment +import com.android.settings.flags.Flags import com.android.settings.network.telephony.MobileNetworkUtils import com.android.settings.search.BaseSearchIndexProvider +import com.android.settings.spa.SpaActivity.Companion.startSpaActivity +import com.android.settings.spa.network.NetworkCellularGroupProvider import com.android.settingslib.search.SearchIndexable import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle import com.android.settingslib.spaprivileged.framework.common.userManager @@ -40,6 +43,15 @@ class MobileNetworkListFragment : DashboardFragment() { collectAirplaneModeAndFinishIfOn() } + override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + + if (Flags.isDualSimOnboardingEnabled()) { + context?.startSpaActivity(NetworkCellularGroupProvider.name); + finish() + } + } + override fun onResume() { super.onResume() // Disable the animation of the preference list diff --git a/src/com/android/settings/network/SubscriptionInfoListViewModel.kt b/src/com/android/settings/network/SubscriptionInfoListViewModel.kt index ed930d4c8db..f6820023c92 100644 --- a/src/com/android/settings/network/SubscriptionInfoListViewModel.kt +++ b/src/com/android/settings/network/SubscriptionInfoListViewModel.kt @@ -32,7 +32,19 @@ class SubscriptionInfoListViewModel(application: Application) : AndroidViewModel application.getSystemService(SubscriptionManager::class.java)!! private val scope = viewModelScope + Dispatchers.Default + /** + * Getting the active Subscription list + */ + //ToDo: renaming the function name val subscriptionInfoListFlow = application.subscriptionsChangedFlow().map { SubscriptionUtil.getActiveSubscriptions(subscriptionManager) }.stateIn(scope, SharingStarted.Eagerly, initialValue = emptyList()) + + /** + * Getting the Selectable SubscriptionInfo List from the SubscriptionManager's + * getAvailableSubscriptionInfoList + */ + val selectableSubscriptionInfoListFlow = application.subscriptionsChangedFlow().map { + SubscriptionUtil.getSelectableSubscriptionInfoList(application) + }.stateIn(scope, SharingStarted.Eagerly, initialValue = emptyList()) } diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt index ac1af804549..41852e55dfe 100644 --- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt +++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt @@ -48,6 +48,7 @@ import com.android.settings.spa.development.UsageStatsPageProvider import com.android.settings.spa.development.compat.PlatformCompatAppListPageProvider import com.android.settings.spa.home.HomePageProvider import com.android.settings.spa.network.NetworkAndInternetPageProvider +import com.android.settings.spa.network.NetworkCellularGroupProvider import com.android.settings.spa.network.SimOnboardingPageProvider import com.android.settings.spa.notification.AppListNotificationsPageProvider import com.android.settings.spa.notification.NotificationMainPageProvider @@ -118,6 +119,7 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) { ApnEditPageProvider, SimOnboardingPageProvider, BatteryOptimizationModeAppListPageProvider, + NetworkCellularGroupProvider, ) override val logger = if (FeatureFlagUtils.isEnabled( diff --git a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt new file mode 100644 index 00000000000..e746d4a79a0 --- /dev/null +++ b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2024 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.spa.network + +import android.app.Application +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import android.os.UserManager +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.telephony.euicc.EuiccManager +import android.util.Log +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Message +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.DataUsage +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableIntState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import com.android.settings.R +import com.android.settings.network.SubscriptionInfoListViewModel +import com.android.settings.network.SubscriptionUtil +import com.android.settings.network.telephony.MobileNetworkUtils +import com.android.settings.wifi.WifiPickerTrackerHelper +import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle +import com.android.settingslib.spa.widget.preference.ListPreferenceOption +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.preference.SwitchPreference +import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference +import com.android.settingslib.spa.widget.scaffold.RegularScaffold +import com.android.settingslib.spa.widget.ui.Category +import com.android.settingslib.spa.widget.ui.SettingsIcon +import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow + +import com.android.settingslib.spaprivileged.model.enterprise.Restrictions +import com.android.settingslib.spaprivileged.template.preference.RestrictedPreference +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Showing the sim onboarding which is the process flow of sim switching on. + */ +object NetworkCellularGroupProvider : SettingsPageProvider { + override val name = "NetworkCellularGroupProvider" + + private lateinit var subscriptionViewModel: SubscriptionInfoListViewModel + private val owner = createSettingsPage() + + var selectableSubscriptionInfoList: List = listOf() + var defaultVoiceSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID + var defaultSmsSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID + var defaultDataSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID + var nonDds: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID + + fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner) + .setUiLayoutFn { + // never using + Preference(object : PreferenceModel { + override val title = name + override val onClick = navigator(name) + }) + } + + @Composable + override fun Page(arguments: Bundle?) { + val context = LocalContext.current + var selectableSubscriptionInfoListRemember = remember { + mutableListOf().toMutableStateList() + } + var callsSelectedId = rememberSaveable { + mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + } + var textsSelectedId = rememberSaveable { + mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + } + var mobileDataSelectedId = rememberSaveable { + mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + } + var nonDdsRemember = rememberSaveable { + mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + } + + subscriptionViewModel = SubscriptionInfoListViewModel( + context.applicationContext as Application) + + allOfFlows(context, subscriptionViewModel.selectableSubscriptionInfoListFlow) + .collectLatestWithLifecycle(LocalLifecycleOwner.current) { + selectableSubscriptionInfoListRemember.clear() + selectableSubscriptionInfoListRemember.addAll(selectableSubscriptionInfoList) + callsSelectedId.intValue = defaultVoiceSubId + textsSelectedId.intValue = defaultSmsSubId + mobileDataSelectedId.intValue = defaultDataSubId + nonDdsRemember.intValue = nonDds + } + + PageImpl(selectableSubscriptionInfoListRemember, + callsSelectedId, + textsSelectedId, + mobileDataSelectedId, + nonDdsRemember) + } + + private fun allOfFlows(context: Context, + selectableSubscriptionInfoListFlow: Flow>) = + combine( + selectableSubscriptionInfoListFlow, + context.defaultVoiceSubscriptionFlow(), + context.defaultSmsSubscriptionFlow(), + context.defaultDefaultDataSubscriptionFlow(), + NetworkCellularGroupProvider::refreshUiStates, + ).flowOn(Dispatchers.Default) + + fun refreshUiStates( + inputSelectableSubscriptionInfoList: List, + inputDefaultVoiceSubId: Int, + inputDefaultSmsSubId: Int, + inputDefaultDateSubId: Int + ): Unit { + selectableSubscriptionInfoList = inputSelectableSubscriptionInfoList + defaultVoiceSubId = inputDefaultVoiceSubId + defaultSmsSubId = inputDefaultSmsSubId + defaultDataSubId = inputDefaultDateSubId + nonDds = if (defaultDataSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + SubscriptionManager.INVALID_SUBSCRIPTION_ID + } else { + selectableSubscriptionInfoList + .filter { info -> + (info.simSlotIndex != -1) && (info.subscriptionId != defaultDataSubId) + } + .map { it.subscriptionId } + .firstOrNull() ?: SubscriptionManager.INVALID_SUBSCRIPTION_ID + } + } +} + +@Composable +fun PageImpl(selectableSubscriptionInfoList: List, + defaultVoiceSubId: MutableIntState, + defaultSmsSubId: MutableIntState, + defaultDataSubId: MutableIntState, + nonDds: MutableIntState) { + val context = LocalContext.current + var activeSubscriptionInfoList: List = + selectableSubscriptionInfoList.filter { subscriptionInfo -> + subscriptionInfo.simSlotIndex != -1 + } + var subscriptionManager = context.getSystemService(SubscriptionManager::class.java) + + val stringSims = stringResource(R.string.provider_network_settings_title) + RegularScaffold(title = stringSims) { + SimsSectionImpl( + context, + subscriptionManager, + selectableSubscriptionInfoList + ) + PrimarySimSectionImpl( + subscriptionManager, + activeSubscriptionInfoList, + defaultVoiceSubId, + defaultSmsSubId, + defaultDataSubId, + nonDds + ) + } +} + +@Composable +fun SimsSectionImpl( + context: Context, + subscriptionManager: SubscriptionManager?, + subscriptionInfoList: List +) { + val coroutineScope = rememberCoroutineScope() + for (subInfo in subscriptionInfoList) { + val checked = rememberSaveable() { + mutableStateOf(false) + } + //TODO: Add the Restricted TwoTargetSwitchPreference in SPA + TwoTargetSwitchPreference(remember { + object : SwitchPreferenceModel { + override val title = subInfo.displayName.toString() + override val summary = { subInfo.number } + override val checked = { + coroutineScope.launch { + withContext(Dispatchers.Default) { + checked.value = subscriptionManager?.isSubscriptionEnabled( + subInfo.subscriptionId)?:false + } + } + checked.value + } + override val onCheckedChange = { newChecked: Boolean -> + startToggleSubscriptionDialog(context, subInfo, newChecked) + } + } + }) { + startMobileNetworkSettings(context, subInfo) + } + } + + // + add sim + if (showEuiccSettings(context)) { + RestrictedPreference( + model = object : PreferenceModel { + override val title = stringResource(id = R.string.mobile_network_list_add_more) + override val icon = @Composable { SettingsIcon(Icons.Outlined.Add) } + override val onClick = { + startAddSimFlow(context) + } + }, + restrictions = Restrictions(keys = + listOf(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)), + ) + } +} + +@Composable +fun PrimarySimSectionImpl( + subscriptionManager: SubscriptionManager?, + activeSubscriptionInfoList: List, + callsSelectedId: MutableIntState, + textsSelectedId: MutableIntState, + mobileDataSelectedId: MutableIntState, + nonDds: MutableIntState +) { + var state = rememberSaveable { mutableStateOf(false) } + var callsAndSmsList = remember { + mutableListOf(ListPreferenceOption(id = -1, text = "Loading")) + } + var dataList = remember { + mutableListOf(ListPreferenceOption(id = -1, text = "Loading")) + } + + if (activeSubscriptionInfoList.size >= 2) { + state.value = true + callsAndSmsList.clear() + dataList.clear() + for (info in activeSubscriptionInfoList) { + var item = ListPreferenceOption( + id = info.subscriptionId, + text = "${info.displayName}" + ) + callsAndSmsList.add(item) + dataList.add(item) + } + callsAndSmsList.add(ListPreferenceOption( + id = SubscriptionManager.INVALID_SUBSCRIPTION_ID, + text = stringResource(id = R.string.sim_calls_ask_first_prefs_title) + )) + } else { + // hide the primary sim + state.value = false + Log.d("NetworkCellularGroupProvider", "Hide primary sim") + } + + if (state.value) { + val coroutineScope = rememberCoroutineScope() + var context = LocalContext.current + val telephonyManagerForNonDds: TelephonyManager? = + context.getSystemService(TelephonyManager::class.java) + ?.createForSubscriptionId(nonDds.intValue) + val automaticDataChecked = rememberSaveable() { + mutableStateOf(false) + } + + Category(title = stringResource(id = R.string.primary_sim_title)) { + createPrimarySimListPreference( + stringResource(id = R.string.primary_sim_calls_title), + callsAndSmsList, + callsSelectedId, + ImageVector.vectorResource(R.drawable.ic_phone), + ) { + callsSelectedId.intValue = it + coroutineScope.launch { + setDefaultVoice(subscriptionManager, it) + } + } + createPrimarySimListPreference( + stringResource(id = R.string.primary_sim_texts_title), + callsAndSmsList, + textsSelectedId, + Icons.AutoMirrored.Outlined.Message, + ) { + textsSelectedId.intValue = it + coroutineScope.launch { + setDefaultSms(subscriptionManager, it) + } + } + createPrimarySimListPreference( + stringResource(id = R.string.mobile_data_settings_title), + dataList, + mobileDataSelectedId, + Icons.Outlined.DataUsage, + ) { + mobileDataSelectedId.intValue = it + coroutineScope.launch { + // TODO: to fix the WifiPickerTracker crash when create + // the wifiPickerTrackerHelper + setDefaultData(context, + subscriptionManager, + null/*wifiPickerTrackerHelper*/, + it) + } + } + } + + val autoDataTitle = stringResource(id = R.string.primary_sim_automatic_data_title) + val autoDataSummary = stringResource(id = R.string.primary_sim_automatic_data_msg) + SwitchPreference(remember { + object : SwitchPreferenceModel { + override val title = autoDataTitle + override val summary = { autoDataSummary } + override val changeable: () -> Boolean = { + nonDds.intValue != SubscriptionManager.INVALID_SUBSCRIPTION_ID + } + override val checked = { + coroutineScope.launch { + withContext(Dispatchers.Default) { + automaticDataChecked.value = telephonyManagerForNonDds != null + && telephonyManagerForNonDds.isMobileDataPolicyEnabled( + TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH) + } + } + automaticDataChecked.value + } + override val onCheckedChange: ((Boolean) -> Unit)? = + { newChecked: Boolean -> + coroutineScope.launch { + setAutomaticData(telephonyManagerForNonDds, newChecked) + } + } + } + }) + } +} + +private fun Context.defaultVoiceSubscriptionFlow(): Flow = + merge( + flowOf(null), // kick an initial value + broadcastReceiverFlow( + IntentFilter(TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED) + ), + ).map { SubscriptionManager.getDefaultVoiceSubscriptionId() } + .conflate().flowOn(Dispatchers.Default) + +private fun Context.defaultSmsSubscriptionFlow(): Flow = + merge( + flowOf(null), // kick an initial value + broadcastReceiverFlow( + IntentFilter(SubscriptionManager.ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED) + ), + ).map { SubscriptionManager.getDefaultSmsSubscriptionId() } + .conflate().flowOn(Dispatchers.Default) + +private fun Context.defaultDefaultDataSubscriptionFlow(): Flow = + merge( + flowOf(null), // kick an initial value + broadcastReceiverFlow( + IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + ), + ).map { SubscriptionManager.getDefaultDataSubscriptionId() } + .conflate().flowOn(Dispatchers.Default) + +private fun startToggleSubscriptionDialog( + context: Context, + subInfo: SubscriptionInfo, + newStatus: Boolean +) { + SubscriptionUtil.startToggleSubscriptionDialogActivity( + context, + subInfo.subscriptionId, + newStatus + ) +} + +private fun startMobileNetworkSettings(context: Context, subInfo: SubscriptionInfo) { + MobileNetworkUtils.launchMobileNetworkSettings(context, subInfo) +} + +private fun startAddSimFlow(context: Context) { + val intent = Intent(EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION) + intent.putExtra(EuiccManager.EXTRA_FORCE_PROVISION, true) + context.startActivity(intent) +} + +private fun showEuiccSettings(context: Context): Boolean { + return MobileNetworkUtils.showEuiccSettings(context) +} + +private suspend fun setDefaultVoice( + subscriptionManager: SubscriptionManager?, + subId: Int): Unit = withContext(Dispatchers.Default) { + subscriptionManager?.setDefaultVoiceSubscriptionId(subId) +} + +private suspend fun setDefaultSms( + subscriptionManager: SubscriptionManager?, + subId: Int): Unit = withContext(Dispatchers.Default) { + subscriptionManager?.setDefaultSmsSubId(subId) +} + +private suspend fun setDefaultData(context: Context, + subscriptionManager: SubscriptionManager?, + wifiPickerTrackerHelper: WifiPickerTrackerHelper?, + subId: Int): Unit = withContext(Dispatchers.Default) { + subscriptionManager?.setDefaultDataSubId(subId) + MobileNetworkUtils.setMobileDataEnabled( + context, + subId, + true /* enabled */, + true /* disableOtherSubscriptions */) + if (wifiPickerTrackerHelper != null + && !wifiPickerTrackerHelper.isCarrierNetworkProvisionEnabled(subId)) { + wifiPickerTrackerHelper.setCarrierNetworkEnabled(true) + } +} + +private suspend fun setAutomaticData(telephonyManager: TelephonyManager?, newState: Boolean): Unit = + withContext(Dispatchers.Default) { + telephonyManager?.setMobileDataPolicyEnabled( + TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH, + newState) + //TODO: setup backup calling + } \ No newline at end of file diff --git a/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt b/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt index 7704f84ec40..5752a4f4f02 100644 --- a/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt +++ b/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt @@ -108,21 +108,22 @@ private fun primarySimBody(onboardingService: SimOnboardingService) { list, callsSelectedId, ImageVector.vectorResource(R.drawable.ic_phone), - true + onIdSelected = { callsSelectedId.intValue = it } ) createPrimarySimListPreference( stringResource(id = R.string.primary_sim_texts_title), list, textsSelectedId, Icons.AutoMirrored.Outlined.Message, - true + onIdSelected = { textsSelectedId.intValue = it } ) + createPrimarySimListPreference( - stringResource(id = R.string.mobile_data_settings_title), - list, - mobileDataSelectedId, + stringResource(id = R.string.mobile_data_settings_title), + list, + mobileDataSelectedId, Icons.Outlined.DataUsage, - true + onIdSelected = { mobileDataSelectedId.intValue = it } ) val autoDataTitle = stringResource(id = R.string.primary_sim_automatic_data_title) @@ -140,17 +141,18 @@ private fun primarySimBody(onboardingService: SimOnboardingService) { @Composable fun createPrimarySimListPreference( - title: String, - list: List, - selectedId: MutableIntState, - icon: ImageVector, - enable: Boolean + title: String, + list: List, + selectedId: MutableIntState, + icon: ImageVector, + enable: Boolean = true, + onIdSelected: (id: Int) -> Unit ) = ListPreference(remember { object : ListPreferenceModel { override val title = title override val options = list override val selectedId = selectedId - override val onIdSelected: (id: Int) -> Unit = { selectedId.intValue = it } + override val onIdSelected = onIdSelected override val icon = @Composable { SettingsIcon(icon) } From 19a0292230947269991da4be0707039d0a708396 Mon Sep 17 00:00:00 2001 From: Faye Yan Date: Fri, 26 Jan 2024 04:23:30 +0000 Subject: [PATCH 07/15] Revert "Fix the op mode dependency for the second toggle:" This reverts commit 78f99c6a67079eda69b6eb569ad39f16bf3717eb. Reason for revert: The Fedhot team cancel the egress data permission Change-Id: I57c2fad0450f5f5ad36a5ff8a39ed42603b09d49 --- .../spa/app/specialaccess/VoiceActivationApps.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt b/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt index aafe49382db..a7f971418cd 100644 --- a/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt +++ b/src/com/android/settings/spa/app/specialaccess/VoiceActivationApps.kt @@ -56,12 +56,9 @@ class VoiceActivationAppsListModel(context: Context) : AppOpPermissionListModel( override val appOp = AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO override val permission = Manifest.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO override val setModeByUid = true - private var receiveDetectionTrainingDataOpController:AppOpsController? = null + override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) { super.setAllowed(record, newAllowed) - if (!newAllowed && receiveDetectionTrainingDataOpController != null) { - receiveDetectionTrainingDataOpController!!.setAllowed(false) - } logPermissionChange(newAllowed) } @@ -82,21 +79,20 @@ class VoiceActivationAppsListModel(context: Context) : AppOpPermissionListModel( isReceiveSandBoxTriggerAudioOpAllowed: () -> Boolean? ): ReceiveDetectionTrainingDataOpSwitchModel { val context = LocalContext.current - receiveDetectionTrainingDataOpController = remember { + val ReceiveDetectionTrainingDataOpController = remember { AppOpsController( context = context, app = record.app, op = AppOpsManager.OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA, ) } - val isReceiveDetectionTrainingDataOpAllowed = isReceiveDetectionTrainingDataOpAllowed(record, receiveDetectionTrainingDataOpController!!) - + val isReceiveDetectionTrainingDataOpAllowed = isReceiveDetectionTrainingDataOpAllowed(record, ReceiveDetectionTrainingDataOpController) return remember(record) { ReceiveDetectionTrainingDataOpSwitchModel( context, record, isReceiveSandBoxTriggerAudioOpAllowed, - receiveDetectionTrainingDataOpController!!, + ReceiveDetectionTrainingDataOpController, isReceiveDetectionTrainingDataOpAllowed, ) }.also { model -> LaunchedEffect(model, Dispatchers.Default) { model.initState() } } From 8a3ebe25e0da2363786de61b0b0b2ed8da938f42 Mon Sep 17 00:00:00 2001 From: chelseahao Date: Fri, 19 Jan 2024 15:43:17 +0800 Subject: [PATCH 08/15] [Audiosharing] Add button action in detail page. Bug: 308368124 Test: manual Change-Id: I44e631cb75af432965d2221e71676146ea1537b6 --- .../AudioStreamButtonController.java | 145 +++++++++++++++- .../AudioStreamHeaderController.java | 92 +++++++++- ...udioStreamsBroadcastAssistantCallback.java | 41 ----- .../AudioStreamsProgressCategoryCallback.java | 119 +++++++++++++ ...udioStreamsProgressCategoryController.java | 26 ++- .../audiostreams/AudioStreamsRepository.java | 160 ++++++++++++++++++ .../AudioStreamsScanQrCodeController.java | 5 - 7 files changed, 528 insertions(+), 60 deletions(-) create mode 100644 src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java create mode 100644 src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java index bb729d67ec3..47597cf370f 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java @@ -16,39 +16,170 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastAssistant; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; +import android.util.Log; +import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.PreferenceScreen; import com.android.settings.R; +import com.android.settings.bluetooth.Utils; import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.ActionButtonsPreference; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + public class AudioStreamButtonController extends BasePreferenceController implements DefaultLifecycleObserver { + private static final String TAG = "AudioStreamButtonController"; private static final String KEY = "audio_stream_button"; + private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = + new AudioStreamsBroadcastAssistantCallback() { + @Override + public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { + super.onSourceRemoved(sink, sourceId, reason); + updateButton(); + } + + @Override + public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) { + super.onSourceRemoveFailed(sink, sourceId, reason); + updateButton(); + } + + @Override + public void onReceiveStateChanged( + BluetoothDevice sink, + int sourceId, + BluetoothLeBroadcastReceiveState state) { + super.onReceiveStateChanged(sink, sourceId, state); + if (mAudioStreamsHelper.isConnected(state)) { + updateButton(); + } + } + + @Override + public void onSourceAddFailed( + BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) { + super.onSourceAddFailed(sink, source, reason); + updateButton(); + } + + @Override + public void onSourceLost(int broadcastId) { + super.onSourceLost(broadcastId); + updateButton(); + } + }; + + private final AudioStreamsRepository mAudioStreamsRepository = + AudioStreamsRepository.getInstance(); + private final Executor mExecutor; + private final AudioStreamsHelper mAudioStreamsHelper; + private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; private @Nullable ActionButtonsPreference mPreference; private int mBroadcastId = -1; public AudioStreamButtonController(Context context, String preferenceKey) { super(context, preferenceKey); + mExecutor = Executors.newSingleThreadExecutor(); + mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context)); + mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); + } + + @Override + public void onStart(@NonNull LifecycleOwner owner) { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "onStart(): LeBroadcastAssistant is null!"); + return; + } + mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "onStop(): LeBroadcastAssistant is null!"); + return; + } + mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); } @Override public final void displayPreference(PreferenceScreen screen) { mPreference = screen.findPreference(getPreferenceKey()); - if (mPreference != null) { - mPreference.setButton1Enabled(true); - // TODO(chelseahao): update this based on stream connection state - mPreference - .setButton1Text(R.string.bluetooth_device_context_disconnect) - .setButton1Icon(R.drawable.ic_settings_close); - } + updateButton(); super.displayPreference(screen); } + private void updateButton() { + if (mPreference != null) { + if (mAudioStreamsHelper.getAllConnectedSources().stream() + .map(BluetoothLeBroadcastReceiveState::getBroadcastId) + .anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId)) { + ThreadUtils.postOnMainThread( + () -> { + if (mPreference != null) { + mPreference.setButton1Enabled(true); + mPreference + .setButton1Text( + R.string.bluetooth_device_context_disconnect) + .setButton1Icon(R.drawable.ic_settings_close) + .setButton1OnClickListener( + unused -> { + if (mPreference != null) { + mPreference.setButton1Enabled(false); + } + mAudioStreamsHelper.removeSource(mBroadcastId); + }); + } + }); + } else { + View.OnClickListener clickToRejoin = + unused -> + ThreadUtils.postOnBackgroundThread( + () -> { + var metadata = + mAudioStreamsRepository.getSavedMetadata( + mContext, mBroadcastId); + if (metadata != null) { + mAudioStreamsHelper.addSource(metadata); + ThreadUtils.postOnMainThread( + () -> { + if (mPreference != null) { + mPreference.setButton1Enabled( + false); + } + }); + } + }); + ThreadUtils.postOnMainThread( + () -> { + if (mPreference != null) { + mPreference.setButton1Enabled(true); + mPreference + .setButton1Text(R.string.bluetooth_device_context_connect) + .setButton1Icon(R.drawable.ic_add_24dp) + .setButton1OnClickListener(clickToRejoin); + } + }); + } + } else { + Log.w(TAG, "updateButton(): preference is null!"); + } + } + @Override public int getAvailabilityStatus() { return AVAILABLE; diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java index 89f24bccdbd..3524543bfc5 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java @@ -16,22 +16,64 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastAssistant; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; import androidx.preference.PreferenceScreen; import com.android.settings.R; +import com.android.settings.bluetooth.Utils; import com.android.settings.core.BasePreferenceController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.widget.EntityHeaderController; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.LayoutPreference; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + import javax.annotation.Nullable; public class AudioStreamHeaderController extends BasePreferenceController implements DefaultLifecycleObserver { + private static final String TAG = "AudioStreamHeaderController"; private static final String KEY = "audio_stream_header"; + private final Executor mExecutor; + private final AudioStreamsHelper mAudioStreamsHelper; + @Nullable private final LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; + private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = + new AudioStreamsBroadcastAssistantCallback() { + @Override + public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { + super.onSourceRemoved(sink, sourceId, reason); + updateSummary(); + } + + @Override + public void onSourceLost(int broadcastId) { + super.onSourceLost(broadcastId); + updateSummary(); + } + + @Override + public void onReceiveStateChanged( + BluetoothDevice sink, + int sourceId, + BluetoothLeBroadcastReceiveState state) { + super.onReceiveStateChanged(sink, sourceId, state); + if (mAudioStreamsHelper.isConnected(state)) { + updateSummary(); + } + } + }; + private @Nullable EntityHeaderController mHeaderController; private @Nullable DashboardFragment mFragment; private String mBroadcastName = ""; @@ -39,6 +81,27 @@ public class AudioStreamHeaderController extends BasePreferenceController public AudioStreamHeaderController(Context context, String preferenceKey) { super(context, preferenceKey); + mExecutor = Executors.newSingleThreadExecutor(); + mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context)); + mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); + } + + @Override + public void onStart(@NonNull LifecycleOwner owner) { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "onStart(): LeBroadcastAssistant is null!"); + return; + } + mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + if (mLeBroadcastAssistant == null) { + Log.w(TAG, "onStop(): LeBroadcastAssistant is null!"); + return; + } + mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); } @Override @@ -55,14 +118,37 @@ public class AudioStreamHeaderController extends BasePreferenceController } mHeaderController.setIcon( screen.getContext().getDrawable(R.drawable.ic_bt_audio_sharing)); - // TODO(chelseahao): update this based on stream connection state - mHeaderController.setSummary("Listening now"); - mHeaderController.done(true); screen.addPreference(headerPreference); + updateSummary(); } super.displayPreference(screen); } + private void updateSummary() { + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + var latestSummary = + mAudioStreamsHelper.getAllConnectedSources().stream() + .map( + BluetoothLeBroadcastReceiveState + ::getBroadcastId) + .anyMatch( + connectedBroadcastId -> + connectedBroadcastId + == mBroadcastId) + ? "Listening now" + : ""; + ThreadUtils.postOnMainThread( + () -> { + if (mHeaderController != null) { + mHeaderController.setSummary(latestSummary); + mHeaderController.done(true); + } + }); + }); + } + @Override public int getAvailabilityStatus() { return AVAILABLE; diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java index 84e753c5526..9fb5b21fed9 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java @@ -24,21 +24,12 @@ import android.util.Log; import com.android.settingslib.bluetooth.BluetoothUtils; -import java.util.Locale; - public class AudioStreamsBroadcastAssistantCallback implements BluetoothLeBroadcastAssistant.Callback { private static final String TAG = "AudioStreamsBroadcastAssistantCallback"; private static final boolean DEBUG = BluetoothUtils.D; - private final AudioStreamsProgressCategoryController mCategoryController; - - public AudioStreamsBroadcastAssistantCallback( - AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) { - mCategoryController = audioStreamsProgressCategoryController; - } - @Override public void onReceiveStateChanged( BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) { @@ -52,45 +43,30 @@ public class AudioStreamsBroadcastAssistantCallback + " state: " + state); } - mCategoryController.handleSourceConnected(state); } @Override public void onSearchStartFailed(int reason) { Log.w(TAG, "onSearchStartFailed() reason : " + reason); - mCategoryController.showToast( - String.format(Locale.US, "Failed to start scanning, reason %d", reason)); } @Override public void onSearchStarted(int reason) { - if (mCategoryController == null) { - Log.w(TAG, "onSearchStarted() : mCategoryController is null!"); - return; - } if (DEBUG) { Log.d(TAG, "onSearchStarted() reason : " + reason); } - mCategoryController.setScanning(true); } @Override public void onSearchStopFailed(int reason) { Log.w(TAG, "onSearchStopFailed() reason : " + reason); - mCategoryController.showToast( - String.format(Locale.US, "Failed to stop scanning, reason %d", reason)); } @Override public void onSearchStopped(int reason) { - if (mCategoryController == null) { - Log.w(TAG, "onSearchStopped() : mCategoryController is null!"); - return; - } if (DEBUG) { Log.d(TAG, "onSearchStopped() reason : " + reason); } - mCategoryController.setScanning(false); } @Override @@ -106,8 +82,6 @@ public class AudioStreamsBroadcastAssistantCallback + " reason: " + reason); } - mCategoryController.showToast( - String.format(Locale.US, "Failed to join broadcast, reason %d", reason)); } @Override @@ -126,14 +100,9 @@ public class AudioStreamsBroadcastAssistantCallback @Override public void onSourceFound(BluetoothLeBroadcastMetadata source) { - if (mCategoryController == null) { - Log.w(TAG, "onSourceFound() : mCategoryController is null!"); - return; - } if (DEBUG) { Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId()); } - mCategoryController.handleSourceFound(source); } @Override @@ -141,7 +110,6 @@ public class AudioStreamsBroadcastAssistantCallback if (DEBUG) { Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId); } - mCategoryController.handleSourceLost(broadcastId); } @Override @@ -153,12 +121,6 @@ public class AudioStreamsBroadcastAssistantCallback @Override public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) { Log.w(TAG, "onSourceRemoveFailed() sourceId : " + sourceId + " reason : " + reason); - mCategoryController.showToast( - String.format( - Locale.US, - "Failed to remove source %d for sink %s", - sourceId, - sink.getAddress())); } @Override @@ -166,8 +128,5 @@ public class AudioStreamsBroadcastAssistantCallback if (DEBUG) { Log.d(TAG, "onSourceRemoved() sourceId : " + sourceId + " reason : " + reason); } - mCategoryController.showToast( - String.format( - Locale.US, "Source %d removed for sink %s", sourceId, sink.getAddress())); } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java new file mode 100644 index 00000000000..15a06030264 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 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.audiostreams; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.util.Log; + +import java.util.Locale; + +public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback { + private static final String TAG = "AudioStreamsProgressCategoryCallback"; + + private final AudioStreamsProgressCategoryController mCategoryController; + + public AudioStreamsProgressCategoryCallback( + AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) { + mCategoryController = audioStreamsProgressCategoryController; + } + + @Override + public void onReceiveStateChanged( + BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) { + super.onReceiveStateChanged(sink, sourceId, state); + mCategoryController.handleSourceConnected(state); + } + + @Override + public void onSearchStartFailed(int reason) { + super.onSearchStartFailed(reason); + mCategoryController.showToast( + String.format(Locale.US, "Failed to start scanning, reason %d", reason)); + } + + @Override + public void onSearchStarted(int reason) { + super.onSearchStarted(reason); + if (mCategoryController == null) { + Log.w(TAG, "onSearchStarted() : mCategoryController is null!"); + return; + } + mCategoryController.setScanning(true); + } + + @Override + public void onSearchStopFailed(int reason) { + super.onSearchStopFailed(reason); + mCategoryController.showToast( + String.format(Locale.US, "Failed to stop scanning, reason %d", reason)); + } + + @Override + public void onSearchStopped(int reason) { + super.onSearchStopped(reason); + if (mCategoryController == null) { + Log.w(TAG, "onSearchStopped() : mCategoryController is null!"); + return; + } + mCategoryController.setScanning(false); + } + + @Override + public void onSourceAddFailed( + BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) { + super.onSourceAddFailed(sink, source, reason); + mCategoryController.showToast( + String.format(Locale.US, "Failed to join broadcast, reason %d", reason)); + } + + @Override + public void onSourceFound(BluetoothLeBroadcastMetadata source) { + super.onSourceFound(source); + if (mCategoryController == null) { + Log.w(TAG, "onSourceFound() : mCategoryController is null!"); + return; + } + mCategoryController.handleSourceFound(source); + } + + @Override + public void onSourceLost(int broadcastId) { + super.onSourceLost(broadcastId); + mCategoryController.handleSourceLost(broadcastId); + } + + @Override + public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) { + super.onSourceRemoveFailed(sink, sourceId, reason); + mCategoryController.showToast( + String.format( + Locale.US, + "Failed to remove source %d for sink %s", + sourceId, + sink.getAddress())); + } + + @Override + public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) { + super.onSourceRemoved(sink, sourceId, reason); + mCategoryController.showToast( + String.format( + Locale.US, "Source %d removed for sink %s", sourceId, sink.getAddress())); + } +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java index cb9975da9aa..b3b074335aa 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java @@ -74,6 +74,9 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro } }; + private final AudioStreamsRepository mAudioStreamsRepository = + AudioStreamsRepository.getInstance(); + enum AudioStreamState { // When mTimedSourceFromQrCode is present and this source has not been synced. WAIT_FOR_SYNC, @@ -86,7 +89,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro } private final Executor mExecutor; - private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback; + private final AudioStreamsProgressCategoryCallback mBroadcastAssistantCallback; private final AudioStreamsHelper mAudioStreamsHelper; private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; private final @Nullable LocalBluetoothManager mBluetoothManager; @@ -102,7 +105,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro mBluetoothManager = Utils.getLocalBtManager(mContext); mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager); mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant(); - mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this); + mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this); } @Override @@ -170,6 +173,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro source, (AudioStreamPreference) preference)); } else { mAudioStreamsHelper.addSource(source); + mAudioStreamsRepository.cacheMetadata(source); ((AudioStreamPreference) preference) .setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD); updatePreferenceConnectionState( @@ -202,6 +206,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro return v; } mAudioStreamsHelper.addSource(pendingSource); + mAudioStreamsRepository.cacheMetadata(pendingSource); mTimedSourceFromQrCode.consumed(); v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD); updatePreferenceConnectionState( @@ -236,6 +241,7 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro var fromState = v.getAudioStreamState(); if (fromState == AudioStreamState.SYNCED) { mAudioStreamsHelper.addSource(metadataFromQrCode); + mAudioStreamsRepository.cacheMetadata(metadataFromQrCode); mTimedSourceFromQrCode.consumed(); v.setAudioStreamState(AudioStreamState.WAIT_FOR_SOURCE_ADD); updatePreferenceConnectionState( @@ -302,6 +308,16 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro v, sourceAddedState, p -> launchDetailFragment(broadcastIdConnected)); return v; }); + // Saved connected metadata for user to re-join this broadcast later. + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + var cached = + mAudioStreamsRepository.getCachedMetadata(broadcastIdConnected); + if (cached != null) { + mAudioStreamsRepository.saveMetadata(mContext, cached); + } + }); } private static String getPreferenceSummary(AudioStreamState state) { @@ -457,11 +473,13 @@ public class AudioStreamsProgressCategoryController extends BasePreferenceContro R.id.broadcast_edit_text)) .getText() .toString(); - mAudioStreamsHelper.addSource( + var metadata = new BluetoothLeBroadcastMetadata.Builder(source) .setBroadcastCode( code.getBytes(StandardCharsets.UTF_8)) - .build()); + .build(); + mAudioStreamsHelper.addSource(metadata); + mAudioStreamsRepository.cacheMetadata(metadata); preference.setAudioStreamState( AudioStreamState.WAIT_FOR_SOURCE_ADD); updatePreferenceConnectionState( diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java new file mode 100644 index 00000000000..65245acf177 --- /dev/null +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2024 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.audiostreams; + +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.Nullable; + +/** Manages the caching and storage of Bluetooth audio stream metadata. */ +public class AudioStreamsRepository { + + private static final String TAG = "AudioStreamsRepository"; + private static final boolean DEBUG = BluetoothUtils.D; + + private static final String PREF_KEY = "bluetooth_audio_stream_pref"; + private static final String METADATA_KEY = "bluetooth_audio_stream_metadata"; + + @Nullable + private static AudioStreamsRepository sInstance = null; + + private AudioStreamsRepository() {} + + /** + * Gets the single instance of AudioStreamsRepository. + * + * @return The AudioStreamsRepository instance. + */ + public static synchronized AudioStreamsRepository getInstance() { + if (sInstance == null) { + sInstance = new AudioStreamsRepository(); + } + return sInstance; + } + + private final ConcurrentHashMap + mBroadcastIdToMetadataCacheMap = new ConcurrentHashMap<>(); + + /** + * Caches BluetoothLeBroadcastMetadata in a local cache. + * + * @param metadata The BluetoothLeBroadcastMetadata to be cached. + */ + void cacheMetadata(BluetoothLeBroadcastMetadata metadata) { + if (DEBUG) { + Log.d( + TAG, + "cacheMetadata(): broadcastId " + + metadata.getBroadcastId() + + " saved in local cache."); + } + mBroadcastIdToMetadataCacheMap.put(metadata.getBroadcastId(), metadata); + } + + /** + * Gets cached BluetoothLeBroadcastMetadata by broadcastId. + * + * @param broadcastId The broadcastId to look up in the cache. + * @return The cached BluetoothLeBroadcastMetadata or null if not found. + */ + @Nullable + BluetoothLeBroadcastMetadata getCachedMetadata(int broadcastId) { + var metadata = mBroadcastIdToMetadataCacheMap.get(broadcastId); + if (metadata == null) { + Log.w( + TAG, + "getCachedMetadata(): broadcastId not found in" + + " mBroadcastIdToMetadataCacheMap."); + return null; + } + return metadata; + } + + /** + * Saves metadata to SharedPreferences asynchronously. + * + * @param context The context. + * @param metadata The BluetoothLeBroadcastMetadata to be saved. + */ + void saveMetadata(Context context, BluetoothLeBroadcastMetadata metadata) { + var unused = + ThreadUtils.postOnBackgroundThread( + () -> { + SharedPreferences sharedPref = + context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE); + if (sharedPref != null) { + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString( + METADATA_KEY, + BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString( + metadata)); + editor.apply(); + if (DEBUG) { + Log.d( + TAG, + "saveMetadata(): broadcastId " + + metadata.getBroadcastId() + + " metadata saved in storage."); + } + } + }); + } + + /** + * Gets saved metadata from SharedPreferences. + * + * @param context The context. + * @param broadcastId The broadcastId to retrieve metadata for. + * @return The saved BluetoothLeBroadcastMetadata or null if not found. + */ + @Nullable + BluetoothLeBroadcastMetadata getSavedMetadata(Context context, int broadcastId) { + SharedPreferences sharedPref = context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE); + if (sharedPref != null) { + String savedMetadataStr = sharedPref.getString(METADATA_KEY, null); + if (savedMetadataStr == null) { + Log.w(TAG, "getSavedMetadata(): savedMetadataStr is null"); + return null; + } + var savedMetadata = + BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata( + savedMetadataStr); + if (savedMetadata == null || savedMetadata.getBroadcastId() != broadcastId) { + Log.w(TAG, "getSavedMetadata(): savedMetadata doesn't match broadcast Id."); + return null; + } + if (DEBUG) { + Log.d( + TAG, + "getSavedMetadata(): broadcastId " + + savedMetadata.getBroadcastId() + + " metadata found in storage."); + } + return savedMetadata; + } + return null; + } +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java index 549e7258347..e006cecf67b 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java @@ -16,7 +16,6 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; -import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.Intent; @@ -124,10 +123,6 @@ public class AudioStreamsScanQrCodeController extends BasePreferenceController }); } - void addSource(BluetoothLeBroadcastMetadata source) { - mAudioStreamsHelper.addSource(source); - } - private void updateVisibility() { ThreadUtils.postOnBackgroundThread( () -> { From 8b5da73e4d66d92a04c8728dd4dd9b6a2b3346e1 Mon Sep 17 00:00:00 2001 From: chelseahao Date: Tue, 23 Jan 2024 19:38:59 +0800 Subject: [PATCH 09/15] [Audiosharing] Some UI tweaks (e.g, sort by RSSI) Bug: 308368124 Test: manual Change-Id: Ie066077f6ef47a57b9fb1c85bc7200498dcae093 --- res/layout/qrcode_scanner_fragment.xml | 25 ++-------- res/xml/bluetooth_audio_streams.xml | 28 +++++++---- res/xml/bluetooth_audio_streams_dialog.xml | 45 ++++++++++------- res/xml/bluetooth_audio_streams_qr_code.xml | 9 +++- .../AudioStreamConfirmDialog.java | 2 +- .../audiostreams/AudioStreamPreference.java | 32 ++++++++++-- ...udioStreamsProgressCategoryController.java | 16 ++++-- ...udioStreamsProgressCategoryPreference.java | 37 ++++++++++++++ .../AudioStreamsQrCodeFragment.java | 50 +++++++++++++------ .../qrcode/QrCodeScanModeFragment.java | 1 + 10 files changed, 175 insertions(+), 70 deletions(-) diff --git a/res/layout/qrcode_scanner_fragment.xml b/res/layout/qrcode_scanner_fragment.xml index e6d1c32bfe4..d402dc36ee2 100644 --- a/res/layout/qrcode_scanner_fragment.xml +++ b/res/layout/qrcode_scanner_fragment.xml @@ -17,7 +17,6 @@ @@ -26,36 +25,22 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="3" - android:layout_marginBottom="35dp"> + android:layout_marginBottom="55dp"> - - - - + android:layout_marginTop="20dp"/> diff --git a/res/xml/bluetooth_audio_streams.xml b/res/xml/bluetooth_audio_streams.xml index 95ee710e2dd..e7e708e484a 100644 --- a/res/xml/bluetooth_audio_streams.xml +++ b/res/xml/bluetooth_audio_streams.xml @@ -18,23 +18,31 @@ + android:title="Find an audio stream"> - + + android:title="Audio streams nearby" + settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController"> + + + + \ No newline at end of file diff --git a/res/xml/bluetooth_audio_streams_dialog.xml b/res/xml/bluetooth_audio_streams_dialog.xml index 502e55a1305..024e53763b9 100644 --- a/res/xml/bluetooth_audio_streams_dialog.xml +++ b/res/xml/bluetooth_audio_streams_dialog.xml @@ -16,6 +16,7 @@ --> @@ -23,70 +24,78 @@ android:id="@+id/dialog_bg" android:layout_width="match_parent" android:layout_height="match_parent" + android:paddingStart="25dp" + android:paddingEnd="25dp" android:orientation="vertical"> - + android:layout_marginBottom="@dimen/broadcast_dialog_margin">