From 5ccc566161095cee5d5624ed35ebca0973843559 Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Thu, 19 Dec 2024 11:29:33 +0000 Subject: [PATCH 01/14] Migrate SeekBarPreference to SliderPreference Flag: EXEMPT flag by System prop Bug: 349675952 Bug: 384648854 Test: manually check the UI Test: atest AmbientVolumePreferenceTest Change-Id: Ibe8ba3f8e46337b9771fd4921aee4dea21612e06 --- .../bluetooth/AmbientVolumePreference.java | 32 +++++++++---------- .../BluetoothDeviceDetailsFragment.java | 1 + .../AmbientVolumePreferenceTest.java | 10 +++--- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/com/android/settings/bluetooth/AmbientVolumePreference.java b/src/com/android/settings/bluetooth/AmbientVolumePreference.java index 8196edf0bd8..4f89007cf5f 100644 --- a/src/com/android/settings/bluetooth/AmbientVolumePreference.java +++ b/src/com/android/settings/bluetooth/AmbientVolumePreference.java @@ -37,8 +37,8 @@ import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceViewHolder; import com.android.settings.R; -import com.android.settings.widget.SeekBarPreference; import com.android.settingslib.bluetooth.AmbientVolumeUi; +import com.android.settingslib.widget.SliderPreference; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; @@ -68,12 +68,12 @@ public class AmbientVolumePreference extends PreferenceGroup implements AmbientV private boolean mExpanded = false; private boolean mMutable = false; private boolean mMuted = false; - private final BiMap mSideToSliderMap = HashBiMap.create(); + private final BiMap mSideToSliderMap = HashBiMap.create(); private int mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; private final OnPreferenceChangeListener mPreferenceChangeListener = (slider, v) -> { - if (slider instanceof SeekBarPreference && v instanceof final Integer value) { + if (slider instanceof SliderPreference && v instanceof final Integer value) { final Integer side = mSideToSliderMap.inverse().get(slider); if (mListener != null && side != null) { mListener.onSliderValueChange(side, value); @@ -173,8 +173,8 @@ public class AmbientVolumePreference extends PreferenceGroup implements AmbientV } mMuted = muted; if (mMutable && mMuted) { - for (SeekBarPreference slider : mSideToSliderMap.values()) { - slider.setProgress(slider.getMin()); + for (SliderPreference slider : mSideToSliderMap.values()) { + slider.setValue(slider.getMin()); } } updateVolumeIcon(); @@ -198,7 +198,7 @@ public class AmbientVolumePreference extends PreferenceGroup implements AmbientV if (!mSideToSliderMap.isEmpty()) { for (int side : VALID_SIDES) { - final SeekBarPreference slider = mSideToSliderMap.get(side); + final SliderPreference slider = mSideToSliderMap.get(side); if (slider != null && findPreference(slider.getKey()) == null) { addPreference(slider); } @@ -209,7 +209,7 @@ public class AmbientVolumePreference extends PreferenceGroup implements AmbientV @Override public void setSliderEnabled(int side, boolean enabled) { - SeekBarPreference slider = mSideToSliderMap.get(side); + SliderPreference slider = mSideToSliderMap.get(side); if (slider != null && slider.isEnabled() != enabled) { slider.setEnabled(enabled); updateLayout(); @@ -218,16 +218,16 @@ public class AmbientVolumePreference extends PreferenceGroup implements AmbientV @Override public void setSliderValue(int side, int value) { - SeekBarPreference slider = mSideToSliderMap.get(side); - if (slider != null && slider.getProgress() != value) { - slider.setProgress(value); + SliderPreference slider = mSideToSliderMap.get(side); + if (slider != null && slider.getValue() != value) { + slider.setValue(value); updateVolumeLevel(); } } @Override public void setSliderRange(int side, int min, int max) { - SeekBarPreference slider = mSideToSliderMap.get(side); + SliderPreference slider = mSideToSliderMap.get(side); if (slider != null) { slider.setMin(min); slider.setMax(max); @@ -243,7 +243,7 @@ public class AmbientVolumePreference extends PreferenceGroup implements AmbientV slider.setVisible(mExpanded); } if (!slider.isEnabled()) { - slider.setProgress(slider.getMin()); + slider.setValue(slider.getMin()); } }); updateVolumeLevel(); @@ -265,14 +265,14 @@ public class AmbientVolumePreference extends PreferenceGroup implements AmbientV } private int getVolumeLevel(int side) { - SeekBarPreference slider = mSideToSliderMap.get(side); + SliderPreference slider = mSideToSliderMap.get(side); if (slider == null || !slider.isEnabled()) { return 0; } final double min = slider.getMin(); final double max = slider.getMax(); final double levelGap = (max - min) / 4.0; - final int value = slider.getProgress(); + final int value = slider.getValue(); return (int) Math.ceil((value - min) / levelGap); } @@ -311,7 +311,7 @@ public class AmbientVolumePreference extends PreferenceGroup implements AmbientV if (mSideToSliderMap.containsKey(side)) { return; } - SeekBarPreference slider = new SeekBarPreference(getContext()); + SliderPreference slider = new SliderPreference(getContext()); slider.setKey(KEY_AMBIENT_VOLUME_SLIDER + "_" + side); slider.setOrder(order); slider.setOnPreferenceChangeListener(mPreferenceChangeListener); @@ -326,7 +326,7 @@ public class AmbientVolumePreference extends PreferenceGroup implements AmbientV } @VisibleForTesting - Map getSliders() { + Map getSliders() { return mSideToSliderMap; } } diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index 403a82429cc..f7829b1eb43 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -373,6 +373,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment } mFormatter.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE); } + setDivider(null); } @Override diff --git a/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java b/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java index 115f642d19b..d8c09786a5d 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/AmbientVolumePreferenceTest.java @@ -41,8 +41,8 @@ import androidx.preference.PreferenceViewHolder; import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; -import com.android.settings.widget.SeekBarPreference; import com.android.settingslib.bluetooth.AmbientVolumeUi; +import com.android.settingslib.widget.SliderPreference; import org.junit.Before; import org.junit.Rule; @@ -99,13 +99,13 @@ public class AmbientVolumePreferenceTest { slider.setMax(4); if (side == SIDE_LEFT) { slider.setKey(KEY_LEFT_SLIDER); - slider.setProgress(TEST_LEFT_VOLUME_LEVEL); + slider.setValue(TEST_LEFT_VOLUME_LEVEL); } else if (side == SIDE_RIGHT) { slider.setKey(KEY_RIGHT_SLIDER); - slider.setProgress(TEST_RIGHT_VOLUME_LEVEL); + slider.setValue(TEST_RIGHT_VOLUME_LEVEL); } else { slider.setKey(KEY_UNIFIED_SLIDER); - slider.setProgress(TEST_UNIFIED_VOLUME_LEVEL); + slider.setValue(TEST_UNIFIED_VOLUME_LEVEL); } }); @@ -223,7 +223,7 @@ public class AmbientVolumePreferenceTest { private void assertControlUiCorrect() { final boolean expanded = mPreference.isExpanded(); - Map sliders = mPreference.getSliders(); + Map sliders = mPreference.getSliders(); assertThat(sliders.get(SIDE_UNIFIED).isVisible()).isEqualTo(!expanded); assertThat(sliders.get(SIDE_LEFT).isVisible()).isEqualTo(expanded); assertThat(sliders.get(SIDE_RIGHT).isVisible()).isEqualTo(expanded); From 5a3cfb9f0cb93911ac352f730e2d0e3396259ae9 Mon Sep 17 00:00:00 2001 From: Solti Date: Thu, 19 Dec 2024 17:30:11 +0000 Subject: [PATCH 02/14] Disable ANGLE as system GLES dev option This change prevents the feature from being inadvertently exposed to the wider user base while it undergoes further refinement and stabilization for a smoother user experience. Test: atest SettingsUnitTests:com.android.settings.development.graphicsdriver.GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest Test: locally build Husky and confirm the dev option is disabled, even with the "debug.graphics.angle.developeroption.enable" prop set. Flag: EXEMPT bugfix Bug: b/384047927 Change-Id: I160840a25d38bf305871d7e146745df0d56f0227 --- ...icsDriverEnableAngleAsSystemDriverController.java | 12 +++++++++++- ...EnableAngleAsSystemDriverControllerJUnitTest.java | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverController.java b/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverController.java index e7ee9e1c37e..1ffbb50e548 100644 --- a/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverController.java +++ b/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverController.java @@ -92,7 +92,17 @@ public class GraphicsDriverEnableAngleAsSystemDriverController // This can be enabled by calling: // `adb shell setprop debug.graphics.angle.developeroption.enable true` private boolean isAngleDeveloperOptionEnabled() { - return mSystemProperties.getBoolean(PROPERTY_DEBUG_ANGLE_DEVELOPER_OPTION, false); + boolean intendedUsingAngleDeveloperOption = + mSystemProperties.getBoolean(PROPERTY_DEBUG_ANGLE_DEVELOPER_OPTION, false); + if (intendedUsingAngleDeveloperOption) { + Log.v(TAG, + "ANGLE developer option is enabled in system properties, " + + "but temporarily overridden."); + } + + // Temporarily disabling for broader rollout. + // The feature requires further maturation before general availability. + return false; } @VisibleForTesting diff --git a/tests/unit/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest.java b/tests/unit/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest.java index 8304e5d6083..e65a2491a7d 100644 --- a/tests/unit/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest.java +++ b/tests/unit/src/com/android/settings/development/graphicsdriver/GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest.java @@ -182,7 +182,7 @@ public class GraphicsDriverEnableAngleAsSystemDriverControllerJUnitTest { @Test public void updateState_PreferenceShouldEnabled() { mController.updateState(mPreference); - assertThat(mPreference.isEnabled()).isTrue(); + assertThat(mPreference.isEnabled()).isFalse(); } @Test From 9aa9199f08b49bf4dfd1675db6cc774c178b7216 Mon Sep 17 00:00:00 2001 From: Menghan Li Date: Fri, 20 Dec 2024 03:13:01 +0000 Subject: [PATCH 03/14] feat(EDT): Migrate entry into Display Settings The EDT toggle will be an subsetting in the DarkTheme settings page - When the Dark Theme main toggle is on, we check the EDT setting to decide applying normal DarkTheme or EDT now. - The EDT preference is disabled when DarkTheme is off Bug: 368721320 Flag: android.view.accessibility.force_invert_color Test: atest ToggleForceInvertPreferenceControllerTest DarkModeSettingsFragmentTest Change-Id: I64e47f92b14ee24a91f469cb55c7bb1285f05c62 --- res/values/strings.xml | 4 +- res/xml/accessibility_color_and_motion.xml | 8 -- res/xml/dark_mode_settings.xml | 44 +++++---- .../accessibility/ColorAndMotionFragment.java | 7 -- .../ColorAndMotionFragmentTest.java | 19 ---- ...leForceInvertPreferenceControllerTest.java | 5 +- .../DarkModeSettingsFragmentTest.java | 99 +++++++++++++++++++ 7 files changed, 129 insertions(+), 57 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/display/darkmode/DarkModeSettingsFragmentTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 730c258601c..11f4bb49cef 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -5398,9 +5398,9 @@ Make the mouse pointer more noticeable - Make all apps dark + Make more apps dark - Applies to apps without their own dark theme. Some apps may have display issues, like inverted colors. + Automatically convert light theme apps to dark theme Remove animations diff --git a/res/xml/accessibility_color_and_motion.xml b/res/xml/accessibility_color_and_motion.xml index ffcdbddf7fe..9cc8f8b9bc7 100644 --- a/res/xml/accessibility_color_and_motion.xml +++ b/res/xml/accessibility_color_and_motion.xml @@ -51,14 +51,6 @@ settings:controller="com.android.settings.display.DarkUIPreferenceController" settings:searchable="false"/> - - - + - + - + + + + + + niks = ColorAndMotionFragment.SEARCH_INDEX_DATA_PROVIDER - .getNonIndexableKeys(mContext); - final List keys = - XmlTestUtils.getKeysFromPreferenceXml(mContext, - R.xml.accessibility_color_and_motion); - - assertThat(niks).doesNotContain(ColorAndMotionFragment.TOGGLE_FORCE_INVERT); - assertThat(keys).containsAtLeastElementsIn(niks); - } - - @Test - @RequiresFlagsDisabled(FLAG_FORCE_INVERT_COLOR) public void getNonIndexableKeys_existInXmlLayout() { final List niks = ColorAndMotionFragment.SEARCH_INDEX_DATA_PROVIDER .getNonIndexableKeys(mContext); @@ -94,7 +76,6 @@ public class ColorAndMotionFragmentTest { XmlTestUtils.getKeysFromPreferenceXml(mContext, R.xml.accessibility_color_and_motion); - assertThat(niks).contains(ColorAndMotionFragment.TOGGLE_FORCE_INVERT); assertThat(keys).containsAtLeastElementsIn(niks); } } diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleForceInvertPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleForceInvertPreferenceControllerTest.java index d8197ef9a52..e4fd2c8b8a3 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleForceInvertPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleForceInvertPreferenceControllerTest.java @@ -52,10 +52,7 @@ public class ToggleForceInvertPreferenceControllerTest { @Before public void setUp() { - mController = new ToggleForceInvertPreferenceController( - mContext, - ColorAndMotionFragment.TOGGLE_FORCE_INVERT - ); + mController = new ToggleForceInvertPreferenceController(mContext, "toggle_force_invert"); } @Test diff --git a/tests/robotests/src/com/android/settings/display/darkmode/DarkModeSettingsFragmentTest.java b/tests/robotests/src/com/android/settings/display/darkmode/DarkModeSettingsFragmentTest.java new file mode 100644 index 00000000000..4bd1daa5bdf --- /dev/null +++ b/tests/robotests/src/com/android/settings/display/darkmode/DarkModeSettingsFragmentTest.java @@ -0,0 +1,99 @@ +/* + * 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.display.darkmode; + +import static android.view.accessibility.Flags.FLAG_FORCE_INVERT_COLOR; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.testutils.XmlTestUtils; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.List; + +/** Tests for {@link DarkModeSettingsFragment}. */ +@RunWith(RobolectricTestRunner.class) +public class DarkModeSettingsFragmentTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + private final Context mContext = ApplicationProvider.getApplicationContext(); + private DarkModeSettingsFragment mFragment; + + @Before + public void setUp() { + mFragment = new DarkModeSettingsFragment(); + } + + @Test + public void getMetricsCategory_returnsCorrectCategory() { + assertThat(mFragment.getMetricsCategory()).isEqualTo( + SettingsEnums.DARK_UI_SETTINGS); + } + + @Test + public void getPreferenceScreenResId_returnsCorrectXml() { + assertThat(mFragment.getPreferenceScreenResId()).isEqualTo( + R.xml.dark_mode_settings); + } + + @Test + public void getLogTag_returnsCorrectTag() { + assertThat(mFragment.getLogTag()).isEqualTo("DarkModeSettingsFrag"); + } + + @Test + @RequiresFlagsEnabled(FLAG_FORCE_INVERT_COLOR) + public void getNonIndexableKeys_forceInvertEnabled_existInXmlLayout() { + final List niks = DarkModeSettingsFragment.SEARCH_INDEX_DATA_PROVIDER + .getNonIndexableKeys(mContext); + final List keys = + XmlTestUtils.getKeysFromPreferenceXml(mContext, + R.xml.dark_mode_settings); + + assertThat(niks).doesNotContain("toggle_force_invert"); + assertThat(keys).containsAtLeastElementsIn(niks); + } + + @Test + @RequiresFlagsDisabled(FLAG_FORCE_INVERT_COLOR) + public void getNonIndexableKeys_existInXmlLayout() { + final List niks = DarkModeSettingsFragment.SEARCH_INDEX_DATA_PROVIDER + .getNonIndexableKeys(mContext); + final List keys = + XmlTestUtils.getKeysFromPreferenceXml(mContext, + R.xml.dark_mode_settings); + + assertThat(niks).contains("toggle_force_invert"); + assertThat(keys).containsAtLeastElementsIn(niks); + } +} From d830acf6252bd1a66117f1650f1402f3625e523d Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Mon, 23 Dec 2024 11:50:51 +0800 Subject: [PATCH 04/14] Send initial status for wifiStateFlow To make sure this flow is never empty, use unknown as default. Fix: 383050153 Flag: EXEMPT bug fix Test: manual - on Network & internet Test: unit test Change-Id: Iaa421749ab917ebc651a95ccdf1a4ab1cb9ba112 --- .../wifi/repository/WifiRepository.kt | 13 +++++---- .../wifi/repository/WifiRepositoryTest.kt | 28 +++++++++++++------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/com/android/settings/wifi/repository/WifiRepository.kt b/src/com/android/settings/wifi/repository/WifiRepository.kt index 77f0b1b47cf..39bf1aaec07 100644 --- a/src/com/android/settings/wifi/repository/WifiRepository.kt +++ b/src/com/android/settings/wifi/repository/WifiRepository.kt @@ -25,6 +25,7 @@ import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverF import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart class WifiRepository( private val context: Context, @@ -32,11 +33,13 @@ class WifiRepository( context.broadcastReceiverFlow(IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION)), ) { - fun wifiStateFlow() = wifiStateChangedActionFlow - .map { intent -> - intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN) - } - .onEach { Log.d(TAG, "wifiStateFlow: $it") } + fun wifiStateFlow(): Flow = + wifiStateChangedActionFlow + .map { intent -> + intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_UNKNOWN) + } + .onStart { emit(WifiManager.WIFI_STATE_UNKNOWN) } + .onEach { Log.d(TAG, "wifiStateFlow: $it") } private companion object { private const val TAG = "WifiRepository" diff --git a/tests/spa_unit/src/com/android/settings/wifi/repository/WifiRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/wifi/repository/WifiRepositoryTest.kt index dae3617c37e..f39e70d9322 100644 --- a/tests/spa_unit/src/com/android/settings/wifi/repository/WifiRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/wifi/repository/WifiRepositoryTest.kt @@ -21,8 +21,9 @@ import android.content.Intent import android.net.wifi.WifiManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.android.settingslib.spa.testutils.lastWithTimeoutOrNull import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import org.junit.Test @@ -33,16 +34,25 @@ class WifiRepositoryTest { private val context: Context = ApplicationProvider.getApplicationContext() - private val mockWifiStateChangedActionFlow = flowOf(Intent().apply { - putExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_ENABLED) - }) - - private val repository = WifiRepository(context, mockWifiStateChangedActionFlow) - @Test - fun wifiStateFlow() = runBlocking { - val wifiState = repository.wifiStateFlow().firstWithTimeoutOrNull() + fun wifiStateFlow_enabled() = runBlocking { + val wifiStateChangedIntent = + Intent().apply { + putExtra(WifiManager.EXTRA_WIFI_STATE, WifiManager.WIFI_STATE_ENABLED) + } + val repository = WifiRepository(context, flowOf(wifiStateChangedIntent)) + + val wifiState = repository.wifiStateFlow().lastWithTimeoutOrNull() assertThat(wifiState).isEqualTo(WifiManager.WIFI_STATE_ENABLED) } + + @Test + fun wifiStateFlow_unknown() = runBlocking { + val repository = WifiRepository(context, emptyFlow()) + + val wifiState = repository.wifiStateFlow().lastWithTimeoutOrNull() + + assertThat(wifiState).isEqualTo(WifiManager.WIFI_STATE_UNKNOWN) + } } From 749103ff8ea13a0a0ed88a3c6e01d3cdd05f43f3 Mon Sep 17 00:00:00 2001 From: Zoey Chen Date: Sun, 22 Dec 2024 22:43:56 -0800 Subject: [PATCH 05/14] [Settings] Add feature flag for new UI Bug: 385659296 Change-Id: Iaab6a60f72a5e79d44de69770622ee2a577a8175 Flag: com.android.settings.flags.settings_expressive_design_enabled Test: manual --- aconfig/settings_globalintl_flag_declarations.aconfig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aconfig/settings_globalintl_flag_declarations.aconfig b/aconfig/settings_globalintl_flag_declarations.aconfig index 909e4642d9a..6cfd140b66f 100644 --- a/aconfig/settings_globalintl_flag_declarations.aconfig +++ b/aconfig/settings_globalintl_flag_declarations.aconfig @@ -27,4 +27,11 @@ flag { namespace: "globalintl" description: "Feature flag for regional preferences APIs" bug: "370379000" +} + +flag { + name: "settings_expressive_design_enabled" + namespace: "globalintl" + description: "Feature flag for expressive design" + bug: "385659296" } \ No newline at end of file From 34ff37e91ca89bb5c372b652c8bad988dc894b8c Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Mon, 23 Dec 2024 13:43:55 +0800 Subject: [PATCH 06/14] [Catalyst] Implement get{Read,Write}Permissions for Dark theme NO_IFTTT=Catalyst migration Test: Devtool Fix: 385271120 Flag: com.android.settings.flags.catalyst_display_settings_screen Change-Id: I09cae1d99d149cc7c481080388c6d44088e857c2 --- .../android/settings/display/darkmode/DarkModeScreen.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/com/android/settings/display/darkmode/DarkModeScreen.kt b/src/com/android/settings/display/darkmode/DarkModeScreen.kt index 807b0aeb633..87f9f23bf70 100644 --- a/src/com/android/settings/display/darkmode/DarkModeScreen.kt +++ b/src/com/android/settings/display/darkmode/DarkModeScreen.kt @@ -16,6 +16,7 @@ package com.android.settings.display.darkmode +import android.Manifest import android.app.UiModeManager import android.content.BroadcastReceiver import android.content.Context @@ -29,6 +30,7 @@ import com.android.settings.flags.Flags import com.android.settingslib.PrimarySwitchPreference import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.NoOpKeyedObservable +import com.android.settingslib.datastore.Permissions import com.android.settingslib.metadata.BooleanValue import com.android.settingslib.metadata.PersistentPreference import com.android.settingslib.metadata.PreferenceLifecycleContext @@ -71,6 +73,11 @@ class DarkModeScreen : override val keywords: Int get() = R.string.keywords_dark_ui_mode + override fun getReadPermissions(context: Context) = Permissions.EMPTY + + override fun getWritePermissions(context: Context) = + Permissions.allOf(Manifest.permission.MODIFY_DAY_NIGHT_MODE) + override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) = ReadWritePermit.ALLOW From b75b3a373f359bd9e5d82d81d42f143926c423fd Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Sun, 22 Dec 2024 23:44:11 -0800 Subject: [PATCH 07/14] Revert "[Biometric Onboarding & Edu] Update Set up Fingerprint/F..." Revert submission 30754151-boe_fp_unlock Reason for revert: prevent from leakage Reverted changes: /q/submissionid:30754151-boe_fp_unlock Change-Id: Ida3d127cc8cd71366c4375e90d6058e510f590c9 --- res-product/values/strings.xml | 38 -- res/drawable/ic_eyeglasses.xml | 26 -- res/drawable/ic_familiar_face_and_zone.xml | 26 -- res/drawable/ic_privacy_tip.xml | 10 - res/drawable/ic_security_privacy_safe.xml | 10 - res/layout/face_enroll_introduction_2.xml | 334 ------------------ res/values/strings.xml | 6 - .../BiometricEnrollIntroduction.java | 6 +- .../face/FaceEnrollIntroduction.java | 45 +-- .../face/FaceEnrollIntroductionTest.java | 5 +- 10 files changed, 8 insertions(+), 498 deletions(-) delete mode 100644 res/drawable/ic_eyeglasses.xml delete mode 100644 res/drawable/ic_familiar_face_and_zone.xml delete mode 100644 res/drawable/ic_privacy_tip.xml delete mode 100644 res/drawable/ic_security_privacy_safe.xml delete mode 100644 res/layout/face_enroll_introduction_2.xml diff --git a/res-product/values/strings.xml b/res-product/values/strings.xml index 32e545bd7c2..d5ef1c888f6 100644 --- a/res-product/values/strings.xml +++ b/res-product/values/strings.xml @@ -89,12 +89,6 @@ Use your face to unlock your tablet or for authentication in apps, like when you sign in to apps or approve a purchase Use your face to unlock your device or for authentication in apps, like when you sign in to apps or approve a purchase - - - - - - Allow your child to use their face to unlock their phone @@ -113,18 +107,6 @@ Using your child\u2019s face to unlock their tablet may be less secure than a strong pattern or PIN. Using your child\u2019s face to unlock their device may be less secure than a strong pattern or PIN. - - - - - - - - - - - - @@ -179,12 +161,6 @@ - - - - - - @@ -217,8 +193,6 @@ Use your face to unlock your device or verify it\u2019s you, like when you sign in to apps or approve a purchase.\n\nKeep in mind:\nYou can only have one face set up at a time. To add another face, delete the current one.\n\nLooking at the device can unlock it when you don\u2019t intend to.\n\nYour device can be unlocked by someone else if it\u2019s held up to your face, even if your eyes are closed.\n\nYour device can be unlocked by someone who looks a lot like you, like an identical sibling. Use your fingerprint to unlock your %s or verify it\u2019s you, like when you sign in to apps or approve a purchase - - Use your fingerprints to unlock your %s or verify it\u2019s you in apps Allow your child to use their fingerprint to unlock their phone or verify it\u2019s them. This happens when they sign in to apps, approve a purchase, and more. @@ -267,18 +241,6 @@ Your child\u2019s tablet can be unlocked when they don\u2019t intend to, like if someone holds it up to their finger. Your child\u2019s device can be unlocked when they don\u2019t intend to, like if someone holds it up to their finger. - - - - - - - - - - - - Use your fingerprint to unlock your tablet or verify it\u2019s you, like when you sign in to apps diff --git a/res/drawable/ic_eyeglasses.xml b/res/drawable/ic_eyeglasses.xml deleted file mode 100644 index c35d7b30529..00000000000 --- a/res/drawable/ic_eyeglasses.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/res/drawable/ic_familiar_face_and_zone.xml b/res/drawable/ic_familiar_face_and_zone.xml deleted file mode 100644 index dbef199e896..00000000000 --- a/res/drawable/ic_familiar_face_and_zone.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - \ No newline at end of file diff --git a/res/drawable/ic_privacy_tip.xml b/res/drawable/ic_privacy_tip.xml deleted file mode 100644 index 8f34eb65f22..00000000000 --- a/res/drawable/ic_privacy_tip.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/res/drawable/ic_security_privacy_safe.xml b/res/drawable/ic_security_privacy_safe.xml deleted file mode 100644 index 99aeaef2496..00000000000 --- a/res/drawable/ic_security_privacy_safe.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - \ No newline at end of file diff --git a/res/layout/face_enroll_introduction_2.xml b/res/layout/face_enroll_introduction_2.xml deleted file mode 100644 index cbc14bce15b..00000000000 --- a/res/layout/face_enroll_introduction_2.xml +++ /dev/null @@ -1,334 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 9f4672b4b75..18b20b9e6c1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -771,8 +771,6 @@ - - Cancel @@ -900,8 +898,6 @@ You and your child are in control Keep in mind - - Secure and helpful Use your fingerprint to unlock your phone or approve purchases.\n\nNote: You can\u2019t use your fingerprint to unlock this device. For more information, contact your organization\u2019s admin. @@ -932,8 +928,6 @@ For best results, use a screen protector that\u2019s Made for Google certified. With other screen protectors, your child\u2019s fingerprint may not work. - - diff --git a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java index 77f11ff6f0f..1f7b3e512b2 100644 --- a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java +++ b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java @@ -37,7 +37,6 @@ import com.android.internal.widget.LockPatternUtils; import com.android.settings.R; import com.android.settings.SetupWizardUtils; import com.android.settings.Utils; -import com.android.settings.flags.Flags; import com.android.settings.password.ChooseLockGeneric; import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settings.password.ConfirmDeviceCredentialActivity; @@ -552,11 +551,8 @@ public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase @NonNull protected PorterDuffColorFilter getIconColorFilter() { if (mIconColorFilter == null) { - final int colorType = Flags.biometricsOnboardingEducation() - ? DynamicColorPalette.ColorType.PRIMARY_TEXT - : DynamicColorPalette.ColorType.ACCENT; mIconColorFilter = new PorterDuffColorFilter( - DynamicColorPalette.getColor(this, colorType), + DynamicColorPalette.getColor(this, DynamicColorPalette.ColorType.ACCENT), PorterDuff.Mode.SRC_IN); } return mIconColorFilter; diff --git a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java index 78e3770810a..d3f75195305 100644 --- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java +++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java @@ -53,7 +53,6 @@ import com.android.settings.biometrics.BiometricEnrollActivity; import com.android.settings.biometrics.BiometricEnrollIntroduction; import com.android.settings.biometrics.BiometricUtils; import com.android.settings.biometrics.MultiBiometricEnrollHelper; -import com.android.settings.flags.Flags; import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settings.password.SetupSkipDialog; import com.android.settings.utils.SensorPrivacyManagerHelper; @@ -145,19 +144,6 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction { final ImageView iconLooking = findViewById(R.id.icon_looking); iconGlasses.getBackground().setColorFilter(getIconColorFilter()); iconLooking.getBackground().setColorFilter(getIconColorFilter()); - if (Flags.biometricsOnboardingEducation()) { - final ImageView iconSecurityPrivacySafe = findViewById(R.id.icon_security_privacy_safe); - final ImageView iconPrivacyTip = findViewById(R.id.icon_privacy_tip); - final ImageView iconFamiliarFaceAndZone = - findViewById(R.id.icon_familiar_face_and_zone); - final ImageView iconTrashCan = findViewById(R.id.icon_trash_can); - final ImageView iconLink = findViewById(R.id.icon_link); - iconSecurityPrivacySafe.getBackground().setColorFilter(getIconColorFilter()); - iconPrivacyTip.getBackground().setColorFilter(getIconColorFilter()); - iconFamiliarFaceAndZone.getBackground().setColorFilter(getIconColorFilter()); - iconTrashCan.getBackground().setColorFilter(getIconColorFilter()); - iconLink.getBackground().setColorFilter(getIconColorFilter()); - } // Set text for views with multiple variations. final TextView infoMessageGlasses = findViewById(R.id.info_message_glasses); @@ -170,19 +156,9 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction { infoMessageLooking.setText(getInfoMessageLooking()); inControlTitle.setText(getInControlTitle()); howMessage.setText(getHowMessage()); - if (Flags.biometricsOnboardingEducation()) { - inControlMessage.setText( - R.string.security_settings_face_enroll_introduction_control_message_2); - final TextView learnMore = findViewById(R.id.message_learn_more); - learnMore.setText(Html.fromHtml(getString( - R.string.security_settings_face_enroll_introduction_learn_more_message), - Html.FROM_HTML_MODE_LEGACY)); - learnMore.setMovementMethod(LinkMovementMethod.getInstance()); - } else { - inControlMessage.setText(Html.fromHtml(getString(getInControlMessage()), - Html.FROM_HTML_MODE_LEGACY)); - inControlMessage.setMovementMethod(LinkMovementMethod.getInstance()); - } + inControlMessage.setText(Html.fromHtml(getString(getInControlMessage()), + Html.FROM_HTML_MODE_LEGACY)); + inControlMessage.setMovementMethod(LinkMovementMethod.getInstance()); lessSecure.setText(getLessSecureMessage()); final ScrollView scrollView = @@ -435,11 +411,7 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction { @Override protected int getLayoutResource() { - if (Flags.biometricsOnboardingEducation()) { - return R.layout.face_enroll_introduction_2; - } else { - return R.layout.face_enroll_introduction; - } + return R.layout.face_enroll_introduction; } @Override @@ -622,13 +594,8 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction { setDescriptionText(getString( R.string.private_space_face_enroll_introduction_message)); } else if (mIsFaceStrong) { - final int messageRes; - if (Flags.biometricsOnboardingEducation()) { - messageRes = R.string.security_settings_face_enroll_introduction_message_class3_2; - } else { - messageRes = R.string.security_settings_face_enroll_introduction_message_class3; - } - setDescriptionText(getString(messageRes)); + setDescriptionText(getString( + R.string.security_settings_face_enroll_introduction_message_class3)); } super.updateDescriptionText(); } diff --git a/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java b/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java index 3bfe8197b1f..81a72694592 100644 --- a/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/face/FaceEnrollIntroductionTest.java @@ -64,7 +64,6 @@ import com.android.settings.R; import com.android.settings.Settings; import com.android.settings.biometrics.BiometricEnrollBase; import com.android.settings.biometrics.BiometricUtils; -import com.android.settings.flags.Flags; import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.SettingsShadowResources; @@ -403,9 +402,7 @@ public class FaceEnrollIntroductionTest { assertThat(getGlifLayout(mActivity).getDescriptionText().toString()).isEqualTo( mContext.getString( - Flags.biometricsOnboardingEducation() - ? R.string.security_settings_face_enroll_introduction_message_class3_2 - : R.string.security_settings_face_enroll_introduction_message_class3)); + R.string.security_settings_face_enroll_introduction_message_class3)); assertThat(mActivity.findViewById(R.id.info_row_less_secure).getVisibility()).isEqualTo( View.GONE); } From 69b90da388db398f58d19e121af0da97621b4540 Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Mon, 23 Dec 2024 15:48:50 +0800 Subject: [PATCH 08/14] [Catalyst] Implement get{Read,Write}Permissions for Smooth display NO_IFTTT=Catalyst migration Test: Devtool Fix: 385267421 Flag: com.android.settings.flags.catalyst_display_settings_screen Change-Id: I475ba6dc0cf44802c293deeda98ea8ca12d4df9e --- .../settings/display/PeakRefreshRateSwitchPreference.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt b/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt index 32d29101847..81592cabd31 100644 --- a/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt +++ b/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt @@ -50,6 +50,10 @@ class PeakRefreshRateSwitchPreference : override fun storage(context: Context): KeyValueStore = PeakRefreshRateStore(context, SettingsSystemStore.get(context)) + override fun getReadPermissions(context: Context) = SettingsSystemStore.getReadPermissions() + + override fun getWritePermissions(context: Context) = SettingsSystemStore.getWritePermissions() + override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) = ReadWritePermit.ALLOW From bedf5c3805c608331fbfcf9d434d6998e01d15a0 Mon Sep 17 00:00:00 2001 From: danielwbhuang Date: Mon, 23 Dec 2024 17:53:23 +0800 Subject: [PATCH 09/14] Migrate ToA to the new entry point 1. Add the new entry point 2. Use flag to control the migration Bug: 379962955 Flag: com.android.settings.flags.regional_preferences_api_enabled Test: check hsv and atest TermsOfAddressCategoryControllerTest Change-Id: Ie78a0f16188f3c4414dcd653189e5772f99ecb4d --- res/xml/language_and_region_settings.xml | 6 ++ .../NewTermsOfAddressController.java | 76 +++++++++++++++++++ .../TermsOfAddressCategoryController.java | 5 +- .../TermsOfAddressCategoryControllerTest.java | 8 ++ 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/com/android/settings/localepicker/NewTermsOfAddressController.java diff --git a/res/xml/language_and_region_settings.xml b/res/xml/language_and_region_settings.xml index d631748fab8..1d59532d3e2 100644 --- a/res/xml/language_and_region_settings.xml +++ b/res/xml/language_and_region_settings.xml @@ -44,6 +44,12 @@ android:name="classname" android:value="com.android.settings.applications.appinfo.AppLocaleDetails" /> + supportedLanguageList = Arrays.asList( + mContext.getResources().getStringArray( + R.array.terms_of_address_supported_languages)); + final List notSupportedLocaleList = Arrays.asList( + mContext.getResources().getStringArray( + R.array.terms_of_address_unsupported_locales)); + + final Locale locale = localeInfo.getLocale().stripExtensions(); + final String language = locale.getLanguage(); + final String localeTag = locale.toLanguageTag(); + + // Supported locales: + // 1. All French is supported except fr-CA. + // 2. QA language en-XA (LTR pseudo locale), ar_XB (RTL pseudo locale). + if ((supportedLanguageList.contains(language) + && !notSupportedLocaleList.contains(localeTag)) + || LocaleList.isPseudoLocale(locale)) { + return AVAILABLE; + } + + return CONDITIONALLY_UNAVAILABLE; + } +} diff --git a/src/com/android/settings/localepicker/TermsOfAddressCategoryController.java b/src/com/android/settings/localepicker/TermsOfAddressCategoryController.java index 01168c7ff7b..1e2fbef7e83 100644 --- a/src/com/android/settings/localepicker/TermsOfAddressCategoryController.java +++ b/src/com/android/settings/localepicker/TermsOfAddressCategoryController.java @@ -20,7 +20,6 @@ import static com.android.settings.flags.Flags.termsOfAddressEnabled; import android.content.Context; import android.os.LocaleList; -import android.text.TextUtils; import android.util.Log; import androidx.preference.PreferenceCategory; @@ -28,6 +27,7 @@ import androidx.preference.PreferenceScreen; import com.android.internal.app.LocaleStore; import com.android.settings.R; +import com.android.settings.flags.Flags; import com.android.settings.widget.PreferenceCategoryController; import java.util.Arrays; @@ -64,6 +64,9 @@ public class TermsOfAddressCategoryController extends PreferenceCategoryControll @Override public int getAvailabilityStatus() { + if (Flags.regionalPreferencesApiEnabled()) { + return CONDITIONALLY_UNAVAILABLE; + } if (!termsOfAddressEnabled()) { return CONDITIONALLY_UNAVAILABLE; diff --git a/tests/unit/src/com/android/settings/localepicker/TermsOfAddressCategoryControllerTest.java b/tests/unit/src/com/android/settings/localepicker/TermsOfAddressCategoryControllerTest.java index b025abdf06a..4728c4541ef 100644 --- a/tests/unit/src/com/android/settings/localepicker/TermsOfAddressCategoryControllerTest.java +++ b/tests/unit/src/com/android/settings/localepicker/TermsOfAddressCategoryControllerTest.java @@ -24,12 +24,17 @@ import static org.mockito.Mockito.spy; import android.content.Context; import android.os.Looper; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.android.settings.flags.Flags; + import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; @@ -45,6 +50,8 @@ public class TermsOfAddressCategoryControllerTest { private TermsOfAddressCategoryController mTermsOfAddressCategoryController; private Locale mCacheLocale; + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -65,6 +72,7 @@ public class TermsOfAddressCategoryControllerTest { } @Test + @DisableFlags(Flags.FLAG_REGIONAL_PREFERENCES_API_ENABLED) public void getAvailabilityStatus_returnAvailable() { Locale.setDefault(Locale.forLanguageTag("fr-FR")); From 1258059febb4d053a76616abd488b4b7f0c674f6 Mon Sep 17 00:00:00 2001 From: jasonwshsu Date: Wed, 13 Nov 2024 16:58:03 +0800 Subject: [PATCH 10/14] [HA Input] Add UI to support hearing device microphone change ability in device details page In this patch: * Add custom dialog preference: HearingDeviceInputRoutingPreference and its controller * Contain radio group to show 'hearing device microphone' and 'this phone's microphone' for user's preference * set/get user's preference via BluetoothDevice#isMicrophonePreferredForCalls, BluetoothDevicwe#setMicrophonePreferredForCalls * check if support HapProfile and if in AudioManager#getDevice(GET_DEVICES_INPUTS) list Bug: 349255906 Test: atest HearingDeviceInputRoutingPreferenceTest BluetoothDetailsHearingDeviceInputRoutingControllerTest BluetoothDetailsHearingDeviceControllerTest Flag: com.android.settingslib.flags.hearing_devices_input_routing_control Change-Id: I2e4dbc7fb98353ed52d0d175df4e8725df6b9a05 --- .../hearing_device_input_routing_dialog.xml | 51 ++++++ res/values/strings.xml | 10 + ...eAudioRoutingBasePreferenceController.java | 2 +- ...uetoothDetailsHearingDeviceController.java | 10 +- ...lsHearingDeviceInputRoutingController.java | 126 +++++++++++++ .../HearingDeviceInputRoutingPreference.java | 172 ++++++++++++++++++ ...ioRoutingBasePreferenceControllerTest.java | 5 +- ...othDetailsHearingDeviceControllerTest.java | 31 +++- ...aringDeviceInputRoutingControllerTest.java | 171 +++++++++++++++++ ...aringDeviceInputRoutingPreferenceTest.java | 114 ++++++++++++ 10 files changed, 687 insertions(+), 5 deletions(-) create mode 100644 res/layout/hearing_device_input_routing_dialog.xml create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingController.java create mode 100644 src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreference.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreferenceTest.java diff --git a/res/layout/hearing_device_input_routing_dialog.xml b/res/layout/hearing_device_input_routing_dialog.xml new file mode 100644 index 00000000000..266126986f5 --- /dev/null +++ b/res/layout/hearing_device_input_routing_dialog.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 8e5e04fb36c..cbff25b5b12 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -180,6 +180,16 @@ Unmute surroundings Couldn\u2019t update surroundings + + Default microphone for calls + + Default microphone + + Choose a microphone for calls. + + Hearing aid microphone + + This phone\'s microphone Audio output diff --git a/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceController.java b/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceController.java index 3599f4874c4..44f02f6ee3b 100644 --- a/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceController.java +++ b/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceController.java @@ -101,7 +101,7 @@ public abstract class HearingDeviceAudioRoutingBasePreferenceController extends final List supportedStrategies = mAudioRoutingHelper.getSupportedStrategies(audioAttributes); final AudioDeviceAttributes hearingDeviceAttributes = - mAudioRoutingHelper.getMatchedHearingDeviceAttributes(hearingDevice); + mAudioRoutingHelper.getMatchedHearingDeviceAttributesForOutput(hearingDevice); if (hearingDeviceAttributes == null) { if (DEBUG) { Log.d(TAG, diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java index 8af08792180..01f8bb4cc8e 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceController.java @@ -42,6 +42,7 @@ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsCon public static final int ORDER_HEARING_DEVICE_SETTINGS = 1; public static final int ORDER_HEARING_AIDS_PRESETS = 2; + public static final int ORDER_HEARING_DEVICE_INPUT_ROUTING = 3; public static final int ORDER_AMBIENT_VOLUME = 4; static final String KEY_HEARING_DEVICE_GROUP = "hearing_device_group"; @@ -62,10 +63,12 @@ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsCon @VisibleForTesting void setSubControllers( BluetoothDetailsHearingDeviceSettingsController hearingDeviceSettingsController, - BluetoothDetailsHearingAidsPresetsController presetsController) { + BluetoothDetailsHearingAidsPresetsController presetsController, + BluetoothDetailsHearingDeviceInputRoutingController inputRoutingController) { mControllers.clear(); mControllers.add(hearingDeviceSettingsController); mControllers.add(presetsController); + mControllers.add(inputRoutingController); } @Override @@ -112,6 +115,11 @@ public class BluetoothDetailsHearingDeviceController extends BluetoothDetailsCon mControllers.add(new BluetoothDetailsAmbientVolumePreferenceController(mContext, mManager, mFragment, mCachedDevice, mLifecycle)); } + if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { + mControllers.add( + new BluetoothDetailsHearingDeviceInputRoutingController(mContext, mFragment, + mCachedDevice, mLifecycle)); + } } @NonNull diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingController.java new file mode 100644 index 00000000000..6c9a075672a --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingController.java @@ -0,0 +1,126 @@ +/* + * 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.bluetooth; + +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP; +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.ORDER_HEARING_DEVICE_INPUT_ROUTING; + +import android.content.Context; +import android.media.AudioManager; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.bluetooth.HearingDeviceInputRoutingPreference.InputRoutingValue; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.HapClientProfile; +import com.android.settingslib.bluetooth.HearingAidAudioRoutingConstants; +import com.android.settingslib.bluetooth.HearingAidAudioRoutingHelper; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import java.util.Arrays; + +/** + * The controller of the hearing device input routing + * + *

It manages the input routing preference and update the routing according to the value. + */ +public class BluetoothDetailsHearingDeviceInputRoutingController extends + BluetoothDetailsController implements + HearingDeviceInputRoutingPreference.InputRoutingCallback { + + private static final String TAG = "BluetoothDetailsHearingDeviceInputRoutingController"; + static final String KEY_HEARING_DEVICE_INPUT_ROUTING = "hearing_device_input_routing"; + + private final HearingAidAudioRoutingHelper mAudioRoutingHelper; + private final AudioManager mAudioManager; + + public BluetoothDetailsHearingDeviceInputRoutingController( + @NonNull Context context, + @NonNull PreferenceFragmentCompat fragment, + @NonNull CachedBluetoothDevice device, + @NonNull Lifecycle lifecycle) { + super(context, fragment, device, lifecycle); + mAudioRoutingHelper = new HearingAidAudioRoutingHelper(context); + mAudioManager = mContext.getSystemService(AudioManager.class); + } + + @Override + public boolean isAvailable() { + boolean isSupportedProfile = mCachedDevice.getProfiles().stream().anyMatch( + profile -> profile instanceof HapClientProfile); + boolean isSupportedInputDevice = Arrays.stream( + mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).anyMatch( + info -> mCachedDevice.getAddress().equals(info.getAddress())); + if (isSupportedProfile && !isSupportedInputDevice) { + Log.d(TAG, "Not supported input type hearing device."); + } + return isSupportedProfile && isSupportedInputDevice; + } + + @Override + protected void init(PreferenceScreen screen) { + PreferenceCategory hearingCategory = screen.findPreference(KEY_HEARING_DEVICE_GROUP); + if (hearingCategory != null) { + hearingCategory.addPreference( + createInputRoutingPreference(hearingCategory.getContext())); + } + } + + @Override + protected void refresh() {} + + @Nullable + @Override + public String getPreferenceKey() { + return KEY_HEARING_DEVICE_INPUT_ROUTING; + } + + private HearingDeviceInputRoutingPreference createInputRoutingPreference(Context context) { + HearingDeviceInputRoutingPreference pref = new HearingDeviceInputRoutingPreference(context); + pref.setKey(KEY_HEARING_DEVICE_INPUT_ROUTING); + pref.setOrder(ORDER_HEARING_DEVICE_INPUT_ROUTING); + pref.setTitle(context.getString(R.string.bluetooth_hearing_device_input_routing_title)); + pref.setChecked(getUserPreferredInputRoutingValue()); + pref.setInputRoutingCallback(this); + return pref; + } + + @InputRoutingValue + private int getUserPreferredInputRoutingValue() { + return mCachedDevice.getDevice().isMicrophonePreferredForCalls() + ? InputRoutingValue.HEARING_DEVICE : InputRoutingValue.BUILTIN_MIC; + } + + @Override + public void onInputRoutingUpdated(int selectedInputRoutingUiValue) { + boolean useBuiltinMic = + (selectedInputRoutingUiValue == InputRoutingValue.BUILTIN_MIC); + boolean status = mAudioRoutingHelper.setPreferredInputDeviceForCalls(mCachedDevice, + useBuiltinMic ? HearingAidAudioRoutingConstants.RoutingValue.BUILTIN_DEVICE + : HearingAidAudioRoutingConstants.RoutingValue.AUTO); + if (!status) { + Log.d(TAG, "Fail to configure setPreferredInputDeviceForCalls"); + } + mCachedDevice.getDevice().setMicrophonePreferredForCalls(!useBuiltinMic); + } +} diff --git a/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreference.java b/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreference.java new file mode 100644 index 00000000000..2d09f6103f1 --- /dev/null +++ b/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreference.java @@ -0,0 +1,172 @@ +/* + * 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.bluetooth; + +import android.content.Context; +import android.content.DialogInterface; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RadioGroup; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.R; +import com.android.settingslib.CustomDialogPreferenceCompat; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * Preference for controlling the input routing for hearing device. + * + *

This preference displays a dialog that allows users to choose which input device that want to + * use when using this hearing device. + */ +public class HearingDeviceInputRoutingPreference extends CustomDialogPreferenceCompat { + + /** + * Annotations for possible input routing UI for this hearing device input routing preference. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + InputRoutingValue.HEARING_DEVICE, + InputRoutingValue.BUILTIN_MIC + }) + public @interface InputRoutingValue { + int HEARING_DEVICE = 0; + int BUILTIN_MIC = 1; + } + + private static final int INVALID_ID = -1; + private final Context mContext; + private final int mFromHearingDeviceButtonId = R.id.input_from_hearing_device; + private final int mFromBuiltinMicButtonId = R.id.input_from_builtin_mic; + + @Nullable + private RadioGroup mInputRoutingGroup; + @Nullable + private InputRoutingCallback mCallback; + // Default value is hearing device as input + @InputRoutingValue + private int mSelectedInputRoutingValue = InputRoutingValue.HEARING_DEVICE; + + + public HearingDeviceInputRoutingPreference(@NonNull Context context) { + this(context, null); + } + + public HearingDeviceInputRoutingPreference(@NonNull Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + + mContext = context; + setDialogTitle(R.string.bluetooth_hearing_device_input_routing_dialog_title); + setDialogLayoutResource(R.layout.hearing_device_input_routing_dialog); + setNegativeButtonText(R.string.cancel); + setPositiveButtonText(R.string.done_button); + } + + /** + * Sets the callback to receive input routing updates. + */ + public void setInputRoutingCallback(@NonNull InputRoutingCallback callback) { + mCallback = callback; + } + + /** + * Sets the {@link InputRoutingValue} value to determine which radio button should be checked, + * and also update summary accordingly. + * + * @param inputRoutingValue The input routing value. + */ + public void setChecked(@InputRoutingValue int inputRoutingValue) { + mSelectedInputRoutingValue = inputRoutingValue; + setSummary(getSummary()); + } + + @Override + protected void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + int prevBtnId = getRadioButtonId(mSelectedInputRoutingValue); + int curBtnId = Objects.requireNonNull(mInputRoutingGroup).getCheckedRadioButtonId(); + if (prevBtnId == curBtnId) { + return; + } + + setChecked(getSelectedInputRoutingValue()); + if (mCallback != null) { + mCallback.onInputRoutingUpdated(mSelectedInputRoutingValue); + } + } + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + + mInputRoutingGroup = view.requireViewById(R.id.input_routing_group); + mInputRoutingGroup.check(getRadioButtonId(mSelectedInputRoutingValue)); + } + + @Nullable + @Override + public CharSequence getSummary() { + return switch (mSelectedInputRoutingValue) { + case InputRoutingValue.HEARING_DEVICE -> mContext.getResources().getString( + R.string.bluetooth_hearing_device_input_routing_hearing_device_option); + case InputRoutingValue.BUILTIN_MIC -> mContext.getResources().getString( + R.string.bluetooth_hearing_device_input_routing_builtin_option); + default -> null; + }; + } + + private int getRadioButtonId(@InputRoutingValue int inputRoutingValue) { + return switch (inputRoutingValue) { + case InputRoutingValue.HEARING_DEVICE -> mFromHearingDeviceButtonId; + case InputRoutingValue.BUILTIN_MIC -> mFromBuiltinMicButtonId; + default -> INVALID_ID; + }; + } + + @InputRoutingValue + private int getSelectedInputRoutingValue() { + int checkedId = Objects.requireNonNull(mInputRoutingGroup).getCheckedRadioButtonId(); + if (checkedId == mFromBuiltinMicButtonId) { + return InputRoutingValue.BUILTIN_MIC; + } else { + // Should always return default value hearing device as input if something error + // happens. + return InputRoutingValue.HEARING_DEVICE; + } + } + + /** + * Callback to be invoked when input routing changes. + */ + public interface InputRoutingCallback { + + /** + * Called when the positive button is clicked and input routing is changed. + * + * @param selectedInputRoutingValue The selected input routing value. + */ + void onInputRoutingUpdated(@InputRoutingValue int selectedInputRoutingValue); + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceControllerTest.java index 4decf68d68c..bf27bf8a721 100644 --- a/tests/robotests/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/HearingDeviceAudioRoutingBasePreferenceControllerTest.java @@ -108,7 +108,7 @@ public class HearingDeviceAudioRoutingBasePreferenceControllerTest { when(mBluetoothDevice.getAnonymizedAddress()).thenReturn(TEST_DEVICE_ADDRESS); when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); doReturn(hearingDeviceAttribute).when( - mAudioRoutingHelper).getMatchedHearingDeviceAttributes(any()); + mAudioRoutingHelper).getMatchedHearingDeviceAttributesForOutput(any()); when(mAudioProductStrategyMedia.getAudioAttributesForLegacyStreamType( AudioManager.STREAM_MUSIC)).thenReturn((new AudioAttributes.Builder()).build()); when(mAudioRoutingHelper.getAudioProductStrategies()).thenReturn( @@ -143,7 +143,8 @@ public class HearingDeviceAudioRoutingBasePreferenceControllerTest { @Test public void onPreferenceChange_noMatchedDeviceAttributes_notCallSetStrategies() { - when(mAudioRoutingHelper.getMatchedHearingDeviceAttributes(any())).thenReturn(null); + when(mAudioRoutingHelper.getMatchedHearingDeviceAttributesForOutput(any())).thenReturn( + null); verify(mAudioRoutingHelper, never()).setPreferredDeviceRoutingStrategies(any(), isNull(), anyInt()); diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java index 4e3c742e284..d0177a8c2a7 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControllerTest.java @@ -56,6 +56,8 @@ public class BluetoothDetailsHearingDeviceControllerTest extends private BluetoothDetailsHearingAidsPresetsController mPresetsController; @Mock private BluetoothDetailsHearingDeviceSettingsController mHearingDeviceSettingsController; + @Mock + private BluetoothDetailsHearingDeviceInputRoutingController mInputRoutingController; private BluetoothDetailsHearingDeviceController mHearingDeviceController; @@ -67,7 +69,7 @@ public class BluetoothDetailsHearingDeviceControllerTest extends mHearingDeviceController = new BluetoothDetailsHearingDeviceController(mContext, mFragment, mLocalManager, mCachedDevice, mLifecycle); mHearingDeviceController.setSubControllers(mHearingDeviceSettingsController, - mPresetsController); + mPresetsController, mInputRoutingController); } @Test @@ -84,6 +86,13 @@ public class BluetoothDetailsHearingDeviceControllerTest extends assertThat(mHearingDeviceController.isAvailable()).isTrue(); } + @Test + public void isAvailable_inputRoutingControllersAvailable_returnFalse() { + when(mInputRoutingController.isAvailable()).thenReturn(true); + + assertThat(mHearingDeviceController.isAvailable()).isTrue(); + } + @Test public void isAvailable_noControllersAvailable_returnFalse() { when(mHearingDeviceSettingsController.isAvailable()).thenReturn(false); @@ -146,4 +155,24 @@ public class BluetoothDetailsHearingDeviceControllerTest extends assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( c -> c instanceof BluetoothDetailsAmbientVolumePreferenceController)).isFalse(); } + + @Test + @RequiresFlagsEnabled( + com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL) + public void initSubControllers_flagEnabled_inputRoutingControllerExist() { + mHearingDeviceController.initSubControllers(false); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsHearingDeviceInputRoutingController)).isTrue(); + } + + @Test + @RequiresFlagsDisabled( + com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL) + public void initSubControllers_flagDisabled_inputRoutingControllerNotExist() { + mHearingDeviceController.initSubControllers(false); + + assertThat(mHearingDeviceController.getSubControllers().stream().anyMatch( + c -> c instanceof BluetoothDetailsHearingDeviceInputRoutingController)).isFalse(); + } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingControllerTest.java new file mode 100644 index 00000000000..dc4924da6d7 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceInputRoutingControllerTest.java @@ -0,0 +1,171 @@ +/* + * 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.bluetooth; + +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceController.KEY_HEARING_DEVICE_GROUP; +import static com.android.settings.bluetooth.BluetoothDetailsHearingDeviceInputRoutingController.KEY_HEARING_DEVICE_INPUT_ROUTING; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothDevice; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.bluetooth.HearingDeviceInputRoutingPreference.InputRoutingValue; +import com.android.settingslib.bluetooth.HapClientProfile; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +import java.util.Collections; +import java.util.List; + +/** Tests for {@link BluetoothDetailsHearingDeviceInputRoutingController}. */ + +@RunWith(RobolectricTestRunner.class) +public class BluetoothDetailsHearingDeviceInputRoutingControllerTest extends + BluetoothDetailsControllerTestBase { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + private static final String TEST_ADDRESS = "55:66:77:88:99:AA"; + + @Mock + private BluetoothDevice mBluetoothDevice; + @Mock + private HapClientProfile mHapClientProfile; + @Spy + private AudioManager mAudioManager; + + private BluetoothDetailsHearingDeviceInputRoutingController mController; + + @Override + public void setUp() { + super.setUp(); + + mContext = spy(ApplicationProvider.getApplicationContext()); + mAudioManager = spy(mContext.getSystemService(AudioManager.class)); + when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager); + setupDevice(makeDefaultDeviceConfig()); + when(mCachedDevice.getDevice()).thenReturn(mBluetoothDevice); + PreferenceCategory deviceControls = new PreferenceCategory(mContext); + deviceControls.setKey(KEY_HEARING_DEVICE_GROUP); + mScreen.addPreference(deviceControls); + mController = new BluetoothDetailsHearingDeviceInputRoutingController(mContext, + mFragment, mCachedDevice, mLifecycle); + } + + @Test + public void init_getExpectedPreference() { + mController.init(mScreen); + + Preference pref = mScreen.findPreference(KEY_HEARING_DEVICE_INPUT_ROUTING); + assertThat(pref.getKey()).isEqualTo(KEY_HEARING_DEVICE_INPUT_ROUTING); + } + + @Test + public void init_setPreferredMicrophoneTrue_expectedSummary() { + when(mBluetoothDevice.isMicrophonePreferredForCalls()).thenReturn(true); + + mController.init(mScreen); + + Preference pref = mScreen.findPreference(KEY_HEARING_DEVICE_INPUT_ROUTING); + assertThat(pref.getSummary().toString()).isEqualTo(mContext.getString( + R.string.bluetooth_hearing_device_input_routing_hearing_device_option)); + } + + @Test + public void init_setPreferredMicrophoneFalse_expectedSummary() { + when(mBluetoothDevice.isMicrophonePreferredForCalls()).thenReturn(false); + mController.init(mScreen); + + Preference pref = mScreen.findPreference(KEY_HEARING_DEVICE_INPUT_ROUTING); + assertThat(pref.getSummary().toString()).isEqualTo(mContext.getString( + R.string.bluetooth_hearing_device_input_routing_builtin_option)); + } + + @Test + public void onInputRoutingUpdated_hearingDevice_setMicrophonePreferredForCallsTrue() { + mController.init(mScreen); + + mController.onInputRoutingUpdated(InputRoutingValue.HEARING_DEVICE); + + verify(mBluetoothDevice).setMicrophonePreferredForCalls(true); + } + + @Test + public void onInputRoutingUpdated_builtin_setMicrophonePreferredForCallsFalse() { + mController.init(mScreen); + + mController.onInputRoutingUpdated(InputRoutingValue.BUILTIN_MIC); + + verify(mBluetoothDevice).setMicrophonePreferredForCalls(false); + } + + @Test + public void isAvailable_validInput_supportHapProfile_returnTrue() { + when(mCachedDevice.getAddress()).thenReturn(TEST_ADDRESS); + AudioDeviceInfo[] mockInfo = new AudioDeviceInfo[] {mockTestAddressInfo(TEST_ADDRESS)}; + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(mockInfo); + when(mCachedDevice.getProfiles()).thenReturn(List.of(mHapClientProfile)); + + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_notSupportHapProfile_returnFalse() { + when(mCachedDevice.getAddress()).thenReturn(TEST_ADDRESS); + AudioDeviceInfo[] mockInfo = new AudioDeviceInfo[] {mockTestAddressInfo(TEST_ADDRESS)}; + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(mockInfo); + when(mCachedDevice.getProfiles()).thenReturn(Collections.emptyList()); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_notValidInputDevice_returnFalse() { + when(mCachedDevice.getAddress()).thenReturn(TEST_ADDRESS); + when(mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn( + new AudioDeviceInfo[] {}); + when(mCachedDevice.getProfiles()).thenReturn(List.of(mHapClientProfile)); + + assertThat(mController.isAvailable()).isFalse(); + } + + private AudioDeviceInfo mockTestAddressInfo(String address) { + final AudioDeviceInfo info = mock(AudioDeviceInfo.class); + when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_BLE_HEADSET); + when(info.getAddress()).thenReturn(address); + return info; + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreferenceTest.java b/tests/robotests/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreferenceTest.java new file mode 100644 index 00000000000..e5778245ef4 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/HearingDeviceInputRoutingPreferenceTest.java @@ -0,0 +1,114 @@ +/* + * 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.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.RadioGroup; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.bluetooth.HearingDeviceInputRoutingPreference.InputRoutingValue; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +/** Tests for {@link HearingDeviceInputRoutingPreference}. */ +@RunWith(RobolectricTestRunner.class) +public class HearingDeviceInputRoutingPreferenceTest { + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + private final Context mContext = ApplicationProvider.getApplicationContext(); + private HearingDeviceInputRoutingPreference mPreference; + private TestInputRoutingCallback mTestInputRoutingCallback; + private View mDialogView; + + @Before + public void setup() { + mDialogView = LayoutInflater.from(mContext).inflate( + R.layout.hearing_device_input_routing_dialog, null); + mTestInputRoutingCallback = spy(new TestInputRoutingCallback()); + mPreference = new HearingDeviceInputRoutingPreference(mContext); + } + + @Test + public void onClick_checkToBuiltinMic_callbackWithBuiltinSpeaker() { + mPreference.setChecked(InputRoutingValue.HEARING_DEVICE); + mPreference.setInputRoutingCallback(mTestInputRoutingCallback); + mPreference.onBindDialogView(mDialogView); + RadioGroup radioGroup = mDialogView.requireViewById(R.id.input_routing_group); + Dialog dialog = mPreference.getDialog(); + + radioGroup.check(R.id.input_from_builtin_mic); + mPreference.onClick(dialog, DialogInterface.BUTTON_POSITIVE); + + verify(mTestInputRoutingCallback).onInputRoutingUpdated(InputRoutingValue.BUILTIN_MIC); + } + + @Test + public void setChecked_checkNoChange_noCallback() { + mPreference.setChecked(InputRoutingValue.HEARING_DEVICE); + mPreference.setInputRoutingCallback(mTestInputRoutingCallback); + mPreference.onBindDialogView(mDialogView); + Dialog dialog = mPreference.getDialog(); + + mPreference.setChecked(InputRoutingValue.HEARING_DEVICE); + mPreference.onClick(dialog, DialogInterface.BUTTON_POSITIVE); + + verify(mTestInputRoutingCallback, never()).onInputRoutingUpdated(anyInt()); + } + + @Test + public void setChecked_builtinMic_expectedSummary() { + mPreference.setChecked(InputRoutingValue.BUILTIN_MIC); + + assertThat(mPreference.getSummary().toString()).isEqualTo( + mContext.getString(R.string.bluetooth_hearing_device_input_routing_builtin_option)); + } + + @Test + public void setChecked_hearingDevice_expectedSummary() { + mPreference.setChecked(InputRoutingValue.HEARING_DEVICE); + + assertThat(mPreference.getSummary().toString()).isEqualTo(mContext.getString( + R.string.bluetooth_hearing_device_input_routing_hearing_device_option)); + } + + private static class TestInputRoutingCallback implements + HearingDeviceInputRoutingPreference.InputRoutingCallback { + + @Override + public void onInputRoutingUpdated(int selectedInputRoutingUiValue) {} + } +} From 0d8356610ea764f9e30ff946669c8a647e1184ca Mon Sep 17 00:00:00 2001 From: chenjean Date: Tue, 24 Dec 2024 09:30:23 +0800 Subject: [PATCH 11/14] feat(HCT): Notification disappear behavoir 1. When a user taps the notification body, it should function like clicking the "Open Settings" action button, taking them directly to the Settings page and the notification should disappear automatically. 2. To ensure the notification remains visible until the user interacts with it, we need to add the FLAG_NO_CLEAR flag. This will prevent the notification from being dismissed when the user uses the "Clear all" button in the notification shade. Bug: 385794406 Flag: com.android.graphics.hwui.flags.high_contrast_text_small_text_rect Test: manual Change-Id: Ie2cd17e744363b9834021ddef48ec5a59f19ec64 --- .../accessibility/HighContrastTextMigrationReceiver.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/accessibility/HighContrastTextMigrationReceiver.java b/src/com/android/settings/accessibility/HighContrastTextMigrationReceiver.java index ee3537bedd3..c808430dc61 100644 --- a/src/com/android/settings/accessibility/HighContrastTextMigrationReceiver.java +++ b/src/com/android/settings/accessibility/HighContrastTextMigrationReceiver.java @@ -123,7 +123,7 @@ public class HighContrastTextMigrationReceiver extends BroadcastReceiver { R.string.accessibility_toggle_high_text_contrast_preference_title)) .setContentText(context.getString( R.string.accessibility_notification_high_contrast_text_content)) - .setAutoCancel(true); + .setFlag(Notification.FLAG_NO_CLEAR, true); Intent settingsIntent = new Intent(Settings.ACTION_TEXT_READING_SETTINGS); settingsIntent.setPackage(context.getPackageName()); @@ -142,9 +142,11 @@ public class HighContrastTextMigrationReceiver extends BroadcastReceiver { settingsPendingIntent ).build(); - notificationBuilder.addAction(settingsAction); + notificationBuilder + .setContentIntent(settingsPendingIntent) + .addAction(settingsAction) + .setAutoCancel(true); } - NotificationManager notificationManager = context.getSystemService(NotificationManager.class); NotificationChannel notificationChannel = new NotificationChannel( From 940fc8f39e5da2686d6cdc957a439ddbf96c6c4f Mon Sep 17 00:00:00 2001 From: tomhsu Date: Tue, 24 Dec 2024 02:21:14 +0000 Subject: [PATCH 12/14] Avoid string mismatch of menu name and dialog title - 2 different strings cause this issue. Flag: EXEMPT bug fix Fix: b/385687217 Test: Manual test Change-Id: Ib1a5b8271d7825d95a09c3d6c11fa66a1a6cb0e5 --- res/values/strings.xml | 5 ----- .../settings/network/telephony/MobileNetworkSettings.java | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 89f6d8fb44d..dfa18c630a4 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12267,11 +12267,6 @@ Inactive / eSIM - - SIM name & color Name diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java index d934c99634b..f201b1ab037 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java +++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java @@ -468,7 +468,7 @@ public class MobileNetworkSettings extends AbstractMobileNetworkSettings impleme public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (mSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { final MenuItem item = menu.add(Menu.NONE, R.id.edit_sim_name, Menu.NONE, - R.string.mobile_network_sim_name); + R.string.mobile_network_sim_label_color_title); item.setIcon(com.android.internal.R.drawable.ic_mode_edit); item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); } From dfb3c5b881d2df089106de188b8855cfd0cd8081 Mon Sep 17 00:00:00 2001 From: Menghan Li Date: Mon, 23 Dec 2024 00:06:30 +0000 Subject: [PATCH 13/14] feat(EDT): Update the preference interaction and visiblity logic The EDT toggle will be an subsetting in the DarkTheme settings page - When the Dark Theme main toggle is on, we check the EDT setting to decide applying normal DarkTheme or EDT now. - The EDT preference is disabled when DarkTheme is off Bug: 368721320 Flag: android.view.accessibility.force_invert_color Test: atest ToggleForceInvertPreferenceControllerTest Change-Id: I97841c5b2f03c8c0fb37e0be309d15f312bfedbd --- ...ToggleForceInvertPreferenceController.java | 28 +++++++++++------- ...leForceInvertPreferenceControllerTest.java | 29 +++++++++++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/com/android/settings/accessibility/ToggleForceInvertPreferenceController.java b/src/com/android/settings/accessibility/ToggleForceInvertPreferenceController.java index 3f2cc130ffe..be738e31efa 100644 --- a/src/com/android/settings/accessibility/ToggleForceInvertPreferenceController.java +++ b/src/com/android/settings/accessibility/ToggleForceInvertPreferenceController.java @@ -16,11 +16,16 @@ package com.android.settings.accessibility; +import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; +import static com.android.settings.accessibility.AccessibilityUtil.State.ON; + import android.content.Context; +import android.content.res.Configuration; import android.provider.Settings; import android.view.accessibility.Flags; -import androidx.annotation.VisibleForTesting; +import androidx.annotation.NonNull; +import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; @@ -28,27 +33,28 @@ import com.android.settings.core.TogglePreferenceController; /** A toggle preference controller for force invert (force dark). */ public class ToggleForceInvertPreferenceController extends TogglePreferenceController { - public static final String SETTINGS_KEY = - Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED; - - @VisibleForTesting - static final int ON = 1; - @VisibleForTesting - static final int OFF = 0; - public ToggleForceInvertPreferenceController(Context context, String preferenceKey) { super(context, preferenceKey); } @Override public boolean isChecked() { - return Settings.Secure.getInt(mContext.getContentResolver(), SETTINGS_KEY, OFF) != OFF; + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, OFF) != OFF; } @Override public boolean setChecked(boolean isChecked) { return Settings.Secure.putInt(mContext.getContentResolver(), - SETTINGS_KEY, isChecked ? ON : OFF); + Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, isChecked ? ON : OFF); + } + + @Override + public void updateState(@NonNull Preference preference) { + super.updateState(preference); + final boolean isDarkModeActivated = (mContext.getResources().getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_YES) != 0; + preference.setEnabled(isDarkModeActivated); } @Override diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleForceInvertPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleForceInvertPreferenceControllerTest.java index e4fd2c8b8a3..a2b259f60e2 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleForceInvertPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleForceInvertPreferenceControllerTest.java @@ -23,13 +23,18 @@ import static com.android.settings.accessibility.AccessibilityUtil.State.ON; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + import android.content.Context; +import android.content.res.Configuration; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; +import androidx.preference.Preference; import androidx.test.core.app.ApplicationProvider; import com.android.settings.core.BasePreferenceController; @@ -69,6 +74,30 @@ public class ToggleForceInvertPreferenceControllerTest { .isEqualTo(BasePreferenceController.AVAILABLE); } + @Test + public void updateState_darkModeOn_preferenceEnabled() { + Configuration config = mContext.getResources().getConfiguration(); + config.uiMode = Configuration.UI_MODE_NIGHT_YES; + mContext.getResources().updateConfiguration(config, null); + + Preference preference = mock(Preference.class); + mController.updateState(preference); + + verify(preference).setEnabled(true); + } + + @Test + public void updateState_darkModeOff_preferenceDisabled() { + Configuration config = mContext.getResources().getConfiguration(); + config.uiMode = Configuration.UI_MODE_NIGHT_NO; + mContext.getResources().updateConfiguration(config, null); + + Preference preference = mock(Preference.class); + mController.updateState(preference); + + verify(preference).setEnabled(false); + } + @Test public void settingOff_reflectsCorrectValue() { setEnabled(false); From 79004d10f09a4b3c843d5f23885b141c738cc5f2 Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Tue, 24 Dec 2024 10:41:53 +0800 Subject: [PATCH 14/14] [Catalyst] Implement get{Read,Write}Permissions for Screen attention NO_IFTTT=Catalyst migration Test: devtool Fix: 385274700 Flag: com.android.settings.flags.catalyst_screen_timeout Change-Id: I77c4584fa92713854520594a1bdb9448937d14fb --- src/com/android/settings/display/AdaptiveSleepPreference.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/com/android/settings/display/AdaptiveSleepPreference.kt b/src/com/android/settings/display/AdaptiveSleepPreference.kt index 7cc320d228a..0fe2a098734 100644 --- a/src/com/android/settings/display/AdaptiveSleepPreference.kt +++ b/src/com/android/settings/display/AdaptiveSleepPreference.kt @@ -77,6 +77,10 @@ class AdaptiveSleepPreference : override fun storage(context: Context): KeyValueStore = Storage(context) + override fun getReadPermissions(context: Context) = SettingsSecureStore.getReadPermissions() + + override fun getWritePermissions(context: Context) = SettingsSecureStore.getWritePermissions() + override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) = ReadWritePermit.ALLOW