From b78509129b8028a0cfae95eb20828ef94e74eb49 Mon Sep 17 00:00:00 2001 From: "Torne (Richard Coles)" Date: Mon, 7 Oct 2024 14:09:15 -0400 Subject: [PATCH 01/16] Clean up android.webkit.update_service_v2. This flag shipped in V and can now be cleaned up. Bug: 356086806 Flag: EXEMPT cleaning up launched flag Test: atest ApplicationFeatureProviderImplTest Change-Id: I763683bdd708902305d53c504abc6fae96d4deb4 --- .../ApplicationFeatureProviderImpl.java | 10 +++------- .../ApplicationFeatureProviderImplTest.java | 14 -------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java b/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java index 851d763ea94..62c5910904a 100644 --- a/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java +++ b/src/com/android/settings/applications/ApplicationFeatureProviderImpl.java @@ -16,8 +16,6 @@ package com.android.settings.applications; -import static android.webkit.Flags.updateServiceV2; - import android.Manifest; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; @@ -173,11 +171,9 @@ public class ApplicationFeatureProviderImpl implements ApplicationFeatureProvide } // Keep WebView default package enabled. - if (updateServiceV2()) { - String packageName = mWebViewUpdateServiceWrapper.getDefaultWebViewPackageName(); - if (packageName != null) { - keepEnabledPackages.add(packageName); - } + String packageName = mWebViewUpdateServiceWrapper.getDefaultWebViewPackageName(); + if (packageName != null) { + keepEnabledPackages.add(packageName); } keepEnabledPackages.addAll(getEnabledPackageAllowlist()); diff --git a/tests/robotests/src/com/android/settings/applications/ApplicationFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/applications/ApplicationFeatureProviderImplTest.java index a23ca692970..3afca4da892 100644 --- a/tests/robotests/src/com/android/settings/applications/ApplicationFeatureProviderImplTest.java +++ b/tests/robotests/src/com/android/settings/applications/ApplicationFeatureProviderImplTest.java @@ -38,11 +38,8 @@ import android.os.Build; import android.os.SystemConfigManager; import android.os.UserHandle; import android.os.UserManager; -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.webkit.Flags; import com.android.settings.testutils.ApplicationTestUtils; import com.android.settings.webview.WebViewUpdateServiceWrapper; @@ -373,7 +370,6 @@ public final class ApplicationFeatureProviderImplTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_UPDATE_SERVICE_V2) public void getKeepEnabledPackages_shouldContainWebViewPackage() { final String testWebViewPackageName = "com.android.webview"; when(mWebViewUpdateServiceWrapper.getDefaultWebViewPackageName()) @@ -382,16 +378,6 @@ public final class ApplicationFeatureProviderImplTest { assertThat(allowlist).contains(testWebViewPackageName); } - @Test - @RequiresFlagsDisabled(Flags.FLAG_UPDATE_SERVICE_V2) - public void getKeepEnabledPackages_shouldNotContainWebViewPackageIfFlagDisabled() { - final String testWebViewPackageName = "com.android.webview"; - when(mWebViewUpdateServiceWrapper.getDefaultWebViewPackageName()) - .thenReturn(testWebViewPackageName); - final Set allowlist = mProvider.getKeepEnabledPackages(); - assertThat(allowlist).doesNotContain(testWebViewPackageName); - } - @Test @Config(shadows = {ShadowSmsApplication.class, ShadowDefaultDialerManager.class}) public void getKeepEnabledPackages_shouldContainPackageInstaller() { From 4296cc1977df32e6cf40017dd7393757b8113de8 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 2 Nov 2023 08:06:59 +0000 Subject: [PATCH 02/16] Disable factory reset in DSU mode Bug: 302317901 Bug: 316578327 Test: build Merged-In: I485eb6ac7beec0893d91ca5fe8ad88ecd96a5cbe Change-Id: I485eb6ac7beec0893d91ca5fe8ad88ecd96a5cbe --- src/com/android/settings/MainClear.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/com/android/settings/MainClear.java b/src/com/android/settings/MainClear.java index a7aa8eacf92..ca560cc3429 100644 --- a/src/com/android/settings/MainClear.java +++ b/src/com/android/settings/MainClear.java @@ -23,10 +23,12 @@ import android.accounts.AccountManager; import android.accounts.AuthenticatorDescription; import android.app.ActionBar; import android.app.Activity; +import android.app.AlertDialog; import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -39,6 +41,7 @@ import android.os.Environment; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; +import android.os.image.DynamicSystemManager; import android.provider.Settings; import android.sysprop.VoldProperties; import android.telephony.euicc.EuiccManager; @@ -262,6 +265,19 @@ public class MainClear extends InstrumentedFragment implements OnGlobalLayoutLis return; } + final DynamicSystemManager dsuManager = (DynamicSystemManager) + getActivity().getSystemService(Context.DYNAMIC_SYSTEM_SERVICE); + if (dsuManager.isInUse()) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.dsu_is_running); + builder.setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) {} + }); + AlertDialog dsuAlertdialog = builder.create(); + dsuAlertdialog.show(); + return; + } + if (runKeyguardConfirmation(KEYGUARD_REQUEST)) { return; } From 27619dd45c4d6e39234e230d34dc6e9c151303a7 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 2 Nov 2023 08:06:59 +0000 Subject: [PATCH 03/16] Disable factory reset in DSU mode Bug: 302317901 Bug: 316578327 Test: build Merged-In: I485eb6ac7beec0893d91ca5fe8ad88ecd96a5cbe Change-Id: I485eb6ac7beec0893d91ca5fe8ad88ecd96a5cbe --- src/com/android/settings/MainClear.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/com/android/settings/MainClear.java b/src/com/android/settings/MainClear.java index a7aa8eacf92..ca560cc3429 100644 --- a/src/com/android/settings/MainClear.java +++ b/src/com/android/settings/MainClear.java @@ -23,10 +23,12 @@ import android.accounts.AccountManager; import android.accounts.AuthenticatorDescription; import android.app.ActionBar; import android.app.Activity; +import android.app.AlertDialog; import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -39,6 +41,7 @@ import android.os.Environment; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; +import android.os.image.DynamicSystemManager; import android.provider.Settings; import android.sysprop.VoldProperties; import android.telephony.euicc.EuiccManager; @@ -262,6 +265,19 @@ public class MainClear extends InstrumentedFragment implements OnGlobalLayoutLis return; } + final DynamicSystemManager dsuManager = (DynamicSystemManager) + getActivity().getSystemService(Context.DYNAMIC_SYSTEM_SERVICE); + if (dsuManager.isInUse()) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.dsu_is_running); + builder.setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) {} + }); + AlertDialog dsuAlertdialog = builder.create(); + dsuAlertdialog.show(); + return; + } + if (runKeyguardConfirmation(KEYGUARD_REQUEST)) { return; } From a2741a76cb802603250696e12fc9b9f220355387 Mon Sep 17 00:00:00 2001 From: t Date: Thu, 2 Nov 2023 08:06:59 +0000 Subject: [PATCH 04/16] Disable factory reset in DSU mode Bug: 302317901 Bug: 316578327 Test: build Merged-In: I485eb6ac7beec0893d91ca5fe8ad88ecd96a5cbe Change-Id: I485eb6ac7beec0893d91ca5fe8ad88ecd96a5cbe --- src/com/android/settings/MainClear.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/com/android/settings/MainClear.java b/src/com/android/settings/MainClear.java index a7aa8eacf92..ca560cc3429 100644 --- a/src/com/android/settings/MainClear.java +++ b/src/com/android/settings/MainClear.java @@ -23,10 +23,12 @@ import android.accounts.AccountManager; import android.accounts.AuthenticatorDescription; import android.app.ActionBar; import android.app.Activity; +import android.app.AlertDialog; import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -39,6 +41,7 @@ import android.os.Environment; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; +import android.os.image.DynamicSystemManager; import android.provider.Settings; import android.sysprop.VoldProperties; import android.telephony.euicc.EuiccManager; @@ -262,6 +265,19 @@ public class MainClear extends InstrumentedFragment implements OnGlobalLayoutLis return; } + final DynamicSystemManager dsuManager = (DynamicSystemManager) + getActivity().getSystemService(Context.DYNAMIC_SYSTEM_SERVICE); + if (dsuManager.isInUse()) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.dsu_is_running); + builder.setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) {} + }); + AlertDialog dsuAlertdialog = builder.create(); + dsuAlertdialog.show(); + return; + } + if (runKeyguardConfirmation(KEYGUARD_REQUEST)) { return; } From 7557a48e2ee1e55bffe2e0f3a7a0330110653159 Mon Sep 17 00:00:00 2001 From: Weng Su Date: Thu, 24 Oct 2024 08:07:31 +0800 Subject: [PATCH 05/16] [Catalyst] Create airplane mode preference Bug: 375925972 Flag: com.android.settings.flags.catalyst_network_provider_and_internet_screen Test: Manual testing atest -c AirplaneModePreferenceTest atest -c VpnPreferenceControllerTest Change-Id: Icf0bb9dfc1a8bda7a001f2ad3c6d6b835c489c0d --- res/xml/network_provider_internet.xml | 2 +- .../network/AirplaneModePreference.kt | 42 +++++++++++ .../AirplaneModePreferenceController.java | 2 + .../network/VpnPreferenceController.java | 5 +- .../network/AirplaneModePreferenceTest.kt | 71 +++++++++++++++++++ .../network/VpnPreferenceControllerTest.java | 3 +- 6 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 src/com/android/settings/network/AirplaneModePreference.kt create mode 100644 tests/robotests/src/com/android/settings/network/AirplaneModePreferenceTest.kt diff --git a/res/xml/network_provider_internet.xml b/res/xml/network_provider_internet.xml index 292f1824552..1437db67da5 100644 --- a/res/xml/network_provider_internet.xml +++ b/res/xml/network_provider_internet.xml @@ -56,7 +56,7 @@ settings:controller="com.android.settings.network.MobileNetworkSummaryController" /> () + private val mockResources = mock() + + private val context = + object : ContextWrapper(ApplicationProvider.getApplicationContext()) { + override fun getPackageManager(): PackageManager = mockPackageManager + + override fun getResources(): Resources = mockResources + } + + private val airplaneModePreference = AirplaneModePreference() + + @Test + fun isAvailable_hasConfigAndNoFeatureLeanback_shouldReturnTrue() { + mockResources.stub { on { getBoolean(anyInt()) } doReturn true } + mockPackageManager.stub { on { hasSystemFeature(FEATURE_LEANBACK) } doReturn false } + + assertThat(airplaneModePreference.isAvailable(context)).isTrue() + } + + @Test + fun isAvailable_noConfig_shouldReturnFalse() { + mockResources.stub { on { getBoolean(anyInt()) } doReturn false } + mockPackageManager.stub { on { hasSystemFeature(FEATURE_LEANBACK) } doReturn false } + + assertThat(airplaneModePreference.isAvailable(context)).isFalse() + } + + @Test + fun isAvailable_hasFeatureLeanback_shouldReturnFalse() { + mockResources.stub { on { getBoolean(anyInt()) } doReturn true } + mockPackageManager.stub { on { hasSystemFeature(FEATURE_LEANBACK) } doReturn true } + + assertThat(airplaneModePreference.isAvailable(context)).isFalse() + } +} diff --git a/tests/unit/src/com/android/settings/network/VpnPreferenceControllerTest.java b/tests/unit/src/com/android/settings/network/VpnPreferenceControllerTest.java index ee239ba84a1..7a6a99af8e5 100644 --- a/tests/unit/src/com/android/settings/network/VpnPreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/network/VpnPreferenceControllerTest.java @@ -34,7 +34,6 @@ import android.net.VpnManager; import android.os.Looper; import android.os.UserHandle; import android.provider.Settings; -import android.provider.SettingsSlicesContract; import androidx.lifecycle.LifecycleOwner; import androidx.preference.Preference; @@ -102,7 +101,7 @@ public class VpnPreferenceControllerTest { controller.displayPreference(mScreen); - verify(mPreference).setDependency(SettingsSlicesContract.KEY_AIRPLANE_MODE); + verify(mPreference).setDependency(Settings.Global.AIRPLANE_MODE_ON); } @Test From 88cf7037b0d9e96cfdb7ffa2c7158e23b636e60e Mon Sep 17 00:00:00 2001 From: lbill Date: Tue, 22 Oct 2024 09:17:46 +0000 Subject: [PATCH 06/16] [Screen off unlock UDFPS] Fingerprint Settings integration 2/2 1. Integrate FingerprintSettings with Toggle 2. Sync the Toggle state with SettingProvider key "screen_off_unlock_udfps" Reference: go/udfps-aof #Settings UI design(Deck) Bug: 373792870 Bug: 369939804 Bug: 369938501 Flag: android.hardware.biometrics.screen_off_unlock_udfps Test: atest FingerprintSettingsFragmentTest atest DevelopmentSettingsDashboardFragmentTest atest FingerprintSettingsUnlockCategoryControllerTest atest FingerprintSettingsScreenOffUnlockUdfpsPreferenceControllerTest Test: adb shell settings put secure screen_off_unlock_udfps <1|0> Change-Id: I03794f53684bfb60b4a854e14507e67f60c55a7d --- Android.bp | 1 + res/values/strings.xml | 10 ++ res/xml/security_settings_fingerprint.xml | 11 +- .../fingerprint/FingerprintSettings.java | 102 +++++++++--- ...eenOffUnlockUdfpsPreferenceController.java | 119 +++++++++++++ .../FingerprintUnlockCategoryController.java | 4 +- ...ffUnlockUdfpsPreferenceControllerTest.java | 157 ++++++++++++++++++ ...tSettingsUnlockCategoryControllerTest.java | 58 ++++++- 8 files changed, 436 insertions(+), 26 deletions(-) create mode 100644 src/com/android/settings/biometrics/fingerprint/FingerprintSettingsScreenOffUnlockUdfpsPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsScreenOffUnlockUdfpsPreferenceControllerTest.java diff --git a/Android.bp b/Android.bp index 087030b37cf..e81ef9e2f06 100644 --- a/Android.bp +++ b/Android.bp @@ -79,6 +79,7 @@ android_library { "BiometricsSharedLib", "SystemUIUnfoldLib", "WifiTrackerLib", + "android.hardware.biometrics.flags-aconfig-java", "android.hardware.dumpstate-V1-java", "android.hardware.dumpstate-V1.0-java", "android.hardware.dumpstate-V1.1-java", diff --git a/res/values/strings.xml b/res/values/strings.xml index 75e901e4e1b..99548de2122 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1234,6 +1234,16 @@ Can\u2019t use fingerprint sensor Visit a repair provider. + + + security_settings_screen_off_unlock_udfps + + Screen-off Fingerprint Unlock + + Use Fingerprint Unlock even when the screen is off + + Screen-off, Unlock + More security settings diff --git a/res/xml/security_settings_fingerprint.xml b/res/xml/security_settings_fingerprint.xml index 9c8b0a3f9ea..32f09244f97 100644 --- a/res/xml/security_settings_fingerprint.xml +++ b/res/xml/security_settings_fingerprint.xml @@ -40,7 +40,16 @@ android:title="@string/security_settings_require_screen_on_to_auth_title" android:summary="@string/security_settings_require_screen_on_to_auth_description" settings:keywords="@string/security_settings_require_screen_on_to_auth_keywords" - settings:controller="com.android.settings.biometrics.fingerprint.FingerprintSettingsRequireScreenOnToAuthPreferenceController" /> + settings:controller="com.android.settings.biometrics.fingerprint.FingerprintSettingsRequireScreenOnToAuthPreferenceController" + settings:isPreferenceVisible="false"/> + + { + final boolean isChecked = ((TwoStatePreference) preference).isChecked(); + mScreenOffUnlockUdfpsPreferenceController.setChecked(!isChecked); + return true; + }); + } + private void updatePreferencesAfterFingerprintRemoved() { updateAddPreference(); - if (isSfps()) { + if (isSfps() || screenOffUnlockUdfps()) { updateFingerprintUnlockCategoryVisibility(); } updatePreferences(); @@ -954,6 +997,18 @@ public class FingerprintSettings extends SubSettings { controller; } + } + } else if (screenOffUnlockUdfps()) { + for (AbstractPreferenceController controller : controllers) { + if (controller.getPreferenceKey() == KEY_FINGERPRINT_UNLOCK_CATEGORY) { + mFingerprintUnlockCategoryPreferenceController = + (FingerprintUnlockCategoryController) controller; + } else if (controller.getPreferenceKey() == KEY_SCREEN_OFF_FINGERPRINT_UNLOCK) { + mScreenOffUnlockUdfpsPreferenceController = + (FingerprintSettingsScreenOffUnlockUdfpsPreferenceController) + controller; + } + } } return controllers; @@ -1070,7 +1125,8 @@ public class FingerprintSettings extends SubSettings { } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { mBiometricsAuthenticationRequested = false; if (resultCode != RESULT_OK) { - if (resultCode == ConfirmDeviceCredentialActivity.BIOMETRIC_LOCKOUT_ERROR_RESULT) { + if (resultCode + == ConfirmDeviceCredentialActivity.BIOMETRIC_LOCKOUT_ERROR_RESULT) { IdentityCheckBiometricErrorDialog .showBiometricErrorDialogAndFinishActivityOnDismiss(getActivity(), Utils.BiometricStatus.LOCKOUT); @@ -1408,7 +1464,7 @@ public class FingerprintSettings extends SubSettings { getContext().getSystemService(DevicePolicyManager.class); String messageId = isProfileChallengeUser ? WORK_PROFILE_FINGERPRINT_LAST_DELETE_MESSAGE - : UNDEFINED; + : UNDEFINED; int defaultMessageId = isProfileChallengeUser ? R.string.fingerprint_last_delete_message_profile_challenge : R.string.fingerprint_last_delete_message; @@ -1417,7 +1473,7 @@ public class FingerprintSettings extends SubSettings { .setTitle(title) .setMessage(devicePolicyManager.getResources().getString( messageId, - () -> message + "\n\n" + getContext().getString(defaultMessageId))) + () -> message + "\n\n" + getContext().getString(defaultMessageId))) .setPositiveButton( R.string.security_settings_fingerprint_enroll_dialog_delete, new DialogInterface.OnClickListener() { diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsScreenOffUnlockUdfpsPreferenceController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsScreenOffUnlockUdfpsPreferenceController.java new file mode 100644 index 00000000000..5c32d90ba7e --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsScreenOffUnlockUdfpsPreferenceController.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.biometrics.fingerprint; + +import static android.hardware.biometrics.Flags.screenOffUnlockUdfps; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.hardware.fingerprint.FingerprintManager; +import android.os.UserHandle; +import android.provider.Settings; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.Utils; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; + +/** + * Preference controller that controls whether show screen off UDFPS unlock toggle for users to + * turn this feature ON or OFF + */ +@SearchIndexable +public class FingerprintSettingsScreenOffUnlockUdfpsPreferenceController + extends FingerprintSettingsPreferenceController { + private static final String TAG = + "FingerprintSettingsScreenOffUnlockUdfpsPreferenceController"; + + @VisibleForTesting + protected FingerprintManager mFingerprintManager; + + public FingerprintSettingsScreenOffUnlockUdfpsPreferenceController( + @NonNull Context context, @NonNull String prefKey) { + super(context, prefKey); + mFingerprintManager = Utils.getFingerprintManagerOrNull(context); + } + + @Override + public boolean isChecked() { + if (!FingerprintSettings.isFingerprintHardwareDetected(mContext)) { + return false; + } else if (getRestrictingAdmin() != null) { + return false; + } + final boolean defEnabled = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_screen_off_udfps_enabled); + final int value = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.SCREEN_OFF_UNLOCK_UDFPS_ENABLED, + defEnabled ? 1 : 0 /* config_screen_off_udfps_enabled */, + getUserHandle()); + return value == 1; + } + + @Override + public boolean setChecked(boolean isChecked) { + Settings.Secure.putIntForUser( + mContext.getContentResolver(), + Settings.Secure.SCREEN_OFF_UNLOCK_UDFPS_ENABLED, + isChecked ? 1 : 0, + getUserHandle()); + return true; + } + + @Override + public void updateState(@NonNull Preference preference) { + super.updateState(preference); + if (!FingerprintSettings.isFingerprintHardwareDetected(mContext)) { + preference.setEnabled(false); + } else if (!mFingerprintManager.hasEnrolledTemplates(getUserId())) { + preference.setEnabled(false); + } else if (getRestrictingAdmin() != null) { + preference.setEnabled(false); + } else { + preference.setEnabled(true); + } + } + + @SuppressLint("MissingPermission") + @Override + public int getAvailabilityStatus() { + if (mFingerprintManager != null + && mFingerprintManager.isHardwareDetected() + && screenOffUnlockUdfps() + && !mFingerprintManager.isPowerbuttonFps()) { + return mFingerprintManager.hasEnrolledTemplates(getUserId()) + ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } else { + return UNSUPPORTED_ON_DEVICE; + } + } + + private int getUserHandle() { + return UserHandle.of(getUserId()).getIdentifier(); + } + + /** + * This feature is not directly searchable. + */ + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() {}; + +} diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintUnlockCategoryController.java b/src/com/android/settings/biometrics/fingerprint/FingerprintUnlockCategoryController.java index 674a0dfa758..c949d3da4d8 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintUnlockCategoryController.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintUnlockCategoryController.java @@ -16,6 +16,8 @@ package com.android.settings.biometrics.fingerprint; +import static android.hardware.biometrics.Flags.screenOffUnlockUdfps; + import android.content.Context; import android.hardware.fingerprint.FingerprintManager; @@ -42,7 +44,7 @@ public class FingerprintUnlockCategoryController extends BasePreferenceControlle public int getAvailabilityStatus() { if (mFingerprintManager != null && mFingerprintManager.isHardwareDetected() - && mFingerprintManager.isPowerbuttonFps()) { + && (mFingerprintManager.isPowerbuttonFps() || screenOffUnlockUdfps())) { return mFingerprintManager.hasEnrolledTemplates(getUserId()) ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } else { diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsScreenOffUnlockUdfpsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsScreenOffUnlockUdfpsPreferenceControllerTest.java new file mode 100644 index 00000000000..7660e889db3 --- /dev/null +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsScreenOffUnlockUdfpsPreferenceControllerTest.java @@ -0,0 +1,157 @@ +/* + * 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.biometrics.fingerprint; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.hardware.fingerprint.FingerprintManager; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; + +import com.android.settings.testutils.shadow.ShadowUtils; +import com.android.settingslib.RestrictedSwitchPreference; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowUtils.class}) +public class FingerprintSettingsScreenOffUnlockUdfpsPreferenceControllerTest { + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock + private FingerprintManager mFingerprintManager; + @Mock + private PackageManager mPackageManager; + @Mock + private RestrictedSwitchPreference mPreference; + + private Context mContext; + private FingerprintSettingsScreenOffUnlockUdfpsPreferenceController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + when(mContext.getSystemService(eq(Context.FINGERPRINT_SERVICE))).thenReturn( + mFingerprintManager); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + + mController = spy(new FingerprintSettingsScreenOffUnlockUdfpsPreferenceController(mContext, + "test_key")); + ReflectionHelpers.setField(mController, "mFingerprintManager", mFingerprintManager); + } + + @After + public void tearDown() { + ShadowUtils.reset(); + } + + @Test + public void onPreferenceChange_settingIsUpdated() { + boolean state = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.SCREEN_OFF_UNLOCK_UDFPS_ENABLED, 1) != 0; + + assertThat(mController.isChecked()).isFalse(); + assertThat(mController.onPreferenceChange(mPreference, !state)).isTrue(); + boolean newState = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.SCREEN_OFF_UNLOCK_UDFPS_ENABLED, 1) != 0; + assertThat(newState).isEqualTo(!state); + } + + @Test + @EnableFlags(android.hardware.biometrics.Flags.FLAG_SCREEN_OFF_UNLOCK_UDFPS) + public void isAvailable_isEnabled_whenUdfpsHardwareDetected_AndHasEnrolledFingerprints() { + assertThat(mController.isAvailable()).isEqualTo(false); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + configure_hardwareDetected_isUdfps_hasEnrolledTemplates( + true /* isHardwareDetected */, + false /* isPowerbuttonFps false implies udfps */, + true /* hasEnrolledTemplates */); + assertThat(mController.isAvailable()).isEqualTo(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + @EnableFlags(android.hardware.biometrics.Flags.FLAG_SCREEN_OFF_UNLOCK_UDFPS) + public void isUnavailable_isDisabled_whenUdfpsHardwareDetected_AndNoEnrolledFingerprints() { + assertThat(mController.isAvailable()).isEqualTo(false); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + configure_hardwareDetected_isUdfps_hasEnrolledTemplates( + true /* isHardwareDetected */, + false /* isPowerbuttonFps false implies udfps */, + false /* hasEnrolledTemplates */); + assertThat(mController.isAvailable()).isEqualTo(false); + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + @EnableFlags(android.hardware.biometrics.Flags.FLAG_SCREEN_OFF_UNLOCK_UDFPS) + public void isUnavailable_whenHardwareNotDetected() { + assertThat(mController.isAvailable()).isFalse(); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + configure_hardwareDetected_isUdfps_hasEnrolledTemplates( + false /* isHardwareDetected */, + false /* isPowerbuttonFps false implies udfps */, + true /* hasEnrolledTemplates */); + assertThat(mController.isAvailable()).isEqualTo(false); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + @EnableFlags(android.hardware.biometrics.Flags.FLAG_SCREEN_OFF_UNLOCK_UDFPS) + public void isUnavailable_onNonUdfpsDevice() { + assertThat(mController.isAvailable()).isFalse(); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + configure_hardwareDetected_isUdfps_hasEnrolledTemplates( + true /* isHardwareDetected */, + true /* isPowerbuttonFps false implies udfps */, + true /* hasEnrolledTemplates */); + assertThat(mController.isAvailable()).isFalse(); + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + private void configure_hardwareDetected_isUdfps_hasEnrolledTemplates( + boolean isHardwareDetected, boolean isPowerbuttonFps, boolean hasEnrolledTemplates) { + when(mFingerprintManager.isHardwareDetected()).thenReturn(isHardwareDetected); + when(mFingerprintManager.isPowerbuttonFps()).thenReturn(isPowerbuttonFps); + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(hasEnrolledTemplates); + } + +} diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsUnlockCategoryControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsUnlockCategoryControllerTest.java index 7b6a70ed7ef..59af9346a29 100644 --- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsUnlockCategoryControllerTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsUnlockCategoryControllerTest.java @@ -30,12 +30,15 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.pm.PackageManager; import android.hardware.fingerprint.FingerprintManager; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import com.android.settings.testutils.shadow.ShadowUtils; import com.android.settingslib.RestrictedSwitchPreference; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -49,6 +52,8 @@ import org.robolectric.util.ReflectionHelpers; @Config(shadows = {ShadowUtils.class}) public class FingerprintSettingsUnlockCategoryControllerTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private FingerprintManager mFingerprintManager; @Mock @@ -59,6 +64,8 @@ public class FingerprintSettingsUnlockCategoryControllerTest { private Context mContext; private FingerprintSettingsRequireScreenOnToAuthPreferenceController mController; + private FingerprintSettingsScreenOffUnlockUdfpsPreferenceController mScreenOffUnlockController; + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -69,7 +76,12 @@ public class FingerprintSettingsUnlockCategoryControllerTest { mController = spy(new FingerprintSettingsRequireScreenOnToAuthPreferenceController(mContext, "test_key")); + mScreenOffUnlockController = spy( + new FingerprintSettingsScreenOffUnlockUdfpsPreferenceController(mContext, + "screen_off_unlock_test_key")); ReflectionHelpers.setField(mController, "mFingerprintManager", mFingerprintManager); + ReflectionHelpers.setField(mScreenOffUnlockController, "mFingerprintManager", + mFingerprintManager); } @After @@ -89,6 +101,20 @@ public class FingerprintSettingsUnlockCategoryControllerTest { assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); } + @Test + @EnableFlags(android.hardware.biometrics.Flags.FLAG_SCREEN_OFF_UNLOCK_UDFPS) + public void isAvailable_isEnabled_whenUdfpsHardwareDetected_AndHasEnrolledFingerprints() { + assertThat(mScreenOffUnlockController.isAvailable()).isEqualTo(false); + assertThat(mScreenOffUnlockController.getAvailabilityStatus()).isEqualTo( + UNSUPPORTED_ON_DEVICE); + configure_hardwareDetected_isSfps_hasEnrolledTemplates( + true /* isHardwareDetected */, + false /* isPowerbuttonFps false implies udfps */, + true /* hasEnrolledTemplates */); + assertThat(mScreenOffUnlockController.isAvailable()).isEqualTo(true); + assertThat(mScreenOffUnlockController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + @Test public void isUnavailable_isDisabled_whenSfpsHardwareDetected_AndNoEnrolledFingerprints() { assertThat(mController.isAvailable()).isEqualTo(false); @@ -102,7 +128,22 @@ public class FingerprintSettingsUnlockCategoryControllerTest { } @Test - public void isUnavailable_whenHardwareNotDetected() { + @EnableFlags(android.hardware.biometrics.Flags.FLAG_SCREEN_OFF_UNLOCK_UDFPS) + public void isUnavailable_isDisabled_whenUdfpsHardwareDetected_AndNoEnrolledFingerprints() { + assertThat(mScreenOffUnlockController.isAvailable()).isEqualTo(false); + assertThat(mScreenOffUnlockController.getAvailabilityStatus()).isEqualTo( + UNSUPPORTED_ON_DEVICE); + configure_hardwareDetected_isSfps_hasEnrolledTemplates( + true /* isHardwareDetected */, + false /* isPowerbuttonFps false implies udfps */, + false /* hasEnrolledTemplates */); + assertThat(mScreenOffUnlockController.isAvailable()).isEqualTo(false); + assertThat(mScreenOffUnlockController.getAvailabilityStatus()).isEqualTo( + CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void isUnavailable_whenHardwareNotDetected_onSfpsDevice() { assertThat(mController.isAvailable()).isFalse(); assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); configure_hardwareDetected_isSfps_hasEnrolledTemplates( @@ -113,6 +154,21 @@ public class FingerprintSettingsUnlockCategoryControllerTest { assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); } + @Test + @EnableFlags(android.hardware.biometrics.Flags.FLAG_SCREEN_OFF_UNLOCK_UDFPS) + public void isUnavailable_whenHardwareNotDetected_onUdfpsDevice() { + assertThat(mScreenOffUnlockController.isAvailable()).isFalse(); + assertThat(mScreenOffUnlockController.getAvailabilityStatus()).isEqualTo( + UNSUPPORTED_ON_DEVICE); + configure_hardwareDetected_isSfps_hasEnrolledTemplates( + false /* isHardwareDetected */, + false /* isPowerbuttonFps false implies udfps */, + true /* hasEnrolledTemplates */); + assertThat(mScreenOffUnlockController.isAvailable()).isEqualTo(false); + assertThat(mScreenOffUnlockController.getAvailabilityStatus()).isEqualTo( + UNSUPPORTED_ON_DEVICE); + } + @Test public void isUnavailable_onNonSfpsDevice() { assertThat(mController.isAvailable()).isFalse(); From 82a44bf59361b49c2c8be059e3fd1dc60fafb6a5 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Wed, 30 Oct 2024 19:31:55 +0800 Subject: [PATCH 07/16] Export Battery usage screen Fix: 375298764 Test: manual Flag: EXEMPT only export activity Change-Id: I8c6a7926ccdf0faebf18626501bfff7073dab319 --- AndroidManifest.xml | 15 +++++++++++++++ src/com/android/settings/Settings.java | 1 + .../settings/core/gateway/SettingsGateway.java | 3 +++ 3 files changed, 19 insertions(+) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cdd95355962..6f89a26ce7c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3365,6 +3365,21 @@ android:value="@string/menu_key_battery"/> + + + + + + + + + Date: Wed, 30 Oct 2024 17:12:23 +0800 Subject: [PATCH 08/16] [Catalyst] Provide launch intent Bug: 373895400 Flag: com.android.settings.flags.catalyst Test: manual Change-Id: If7825798c441dc0009fbd91918d4795873b39fa3 --- .../settings/SettingsPreferenceFragment.java | 13 +++++-- .../android/settings/display/DisplayScreen.kt | 6 ++++ src/com/android/settings/utils/IntentUtils.kt | 34 +++++++++++++++++++ .../HighlightablePreferenceGroupAdapter.java | 6 ++-- 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 src/com/android/settings/utils/IntentUtils.kt diff --git a/src/com/android/settings/SettingsPreferenceFragment.java b/src/com/android/settings/SettingsPreferenceFragment.java index 66397c0e66f..609b96a9709 100644 --- a/src/com/android/settings/SettingsPreferenceFragment.java +++ b/src/com/android/settings/SettingsPreferenceFragment.java @@ -16,6 +16,8 @@ package com.android.settings; +import static com.android.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY; + import android.app.Activity; import android.app.Dialog; import android.app.admin.DevicePolicyManager; @@ -45,6 +47,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.flags.Flags; import com.android.settings.support.actionbar.HelpResourceProvider; import com.android.settings.widget.HighlightablePreferenceGroupAdapter; import com.android.settings.widget.LoadingViewController; @@ -367,9 +370,13 @@ public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceF @Override protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { final Bundle arguments = getArguments(); - mAdapter = new HighlightablePreferenceGroupAdapter(preferenceScreen, - arguments == null - ? null : arguments.getString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY), + String key = arguments == null ? null : arguments.getString(EXTRA_FRAGMENT_ARG_KEY); + if (Flags.catalyst() && key == null) { + Activity activity = getActivity(); + Intent intent = activity != null ? activity.getIntent() : null; + key = intent != null ? intent.getStringExtra(EXTRA_FRAGMENT_ARG_KEY) : null; + } + mAdapter = new HighlightablePreferenceGroupAdapter(preferenceScreen, key, mPreferenceHighlighted); return mAdapter; } diff --git a/src/com/android/settings/display/DisplayScreen.kt b/src/com/android/settings/display/DisplayScreen.kt index bd21e8e668d..099e245dbeb 100644 --- a/src/com/android/settings/display/DisplayScreen.kt +++ b/src/com/android/settings/display/DisplayScreen.kt @@ -18,10 +18,13 @@ package com.android.settings.display import android.content.Context import com.android.settings.DisplaySettings import com.android.settings.R +import com.android.settings.Settings.DisplaySettingsActivity import com.android.settings.display.darkmode.DarkModeScreen import com.android.settings.flags.Flags +import com.android.settings.utils.makeLaunchIntent import com.android.settingslib.metadata.PreferenceAvailabilityProvider import com.android.settingslib.metadata.PreferenceIconProvider +import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.ProvidePreferenceScreen import com.android.settingslib.metadata.preferenceHierarchy import com.android.settingslib.preference.PreferenceScreenCreator @@ -52,6 +55,9 @@ class DisplayScreen : +PeakRefreshRateSwitchPreference() } + override fun getLaunchIntent(context: Context, metadata: PreferenceMetadata?) = + makeLaunchIntent(context, DisplaySettingsActivity::class.java, metadata?.key) + override fun isAvailable(context: Context) = context.resources.getBoolean(R.bool.config_show_top_level_display) diff --git a/src/com/android/settings/utils/IntentUtils.kt b/src/com/android/settings/utils/IntentUtils.kt new file mode 100644 index 00000000000..dbdfa23ce06 --- /dev/null +++ b/src/com/android/settings/utils/IntentUtils.kt @@ -0,0 +1,34 @@ +/* + * 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.utils + +import android.app.Activity +import android.content.Context +import android.content.Intent +import com.android.settings.SettingsActivity + +/** + * Returns the [Intent] to start given settings activity and locate the preference. + * + * @param context context + * @param activityClass activity to start + * @param key preference key to locate + */ +fun makeLaunchIntent(context: Context, activityClass: Class, key: String?) = + Intent(context, activityClass).apply { + if (key != null) putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key) + } diff --git a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java index 82ef58bb145..4eb30d29355 100644 --- a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java +++ b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java @@ -30,6 +30,7 @@ import android.util.TypedValue; import android.view.View; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceGroupAdapter; @@ -64,7 +65,7 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter private final Context mContext; private final int mNormalBackgroundRes; - private final String mHighlightKey; + private final @Nullable String mHighlightKey; private boolean mHighlightRequested; private int mHighlightPosition = RecyclerView.NO_POSITION; @@ -101,7 +102,8 @@ public class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter screen.setInitialExpandedChildrenCount(initialCount); } - public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, String key, + public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup, + @Nullable String key, boolean highlightRequested) { super(preferenceGroup); mHighlightKey = key; From 685074260d906f1ccb2e50c4230df4d7daaffbbf Mon Sep 17 00:00:00 2001 From: songferngwang Date: Wed, 30 Oct 2024 10:53:34 +0000 Subject: [PATCH 09/16] Hide the mobile data enable dialog during factory reset Bug: 365582830 Test: manual testing Flag: EXEMPT bugfix Change-Id: I632c3cbaa994f77dc7d0bc1a4a5ef448230165f2 --- src/com/android/settings/MainClearConfirm.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/MainClearConfirm.java b/src/com/android/settings/MainClearConfirm.java index c9887e81f5b..5aee5de3712 100644 --- a/src/com/android/settings/MainClearConfirm.java +++ b/src/com/android/settings/MainClearConfirm.java @@ -17,6 +17,8 @@ package com.android.settings; +import static android.content.Context.MODE_PRIVATE; + import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.app.ProgressDialog; @@ -25,6 +27,7 @@ import android.app.admin.FactoryResetProtectionPolicy; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.os.AsyncTask; import android.os.Bundle; @@ -33,6 +36,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.service.oemlock.OemLockManager; import android.service.persistentdata.PersistentDataBlockManager; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -42,6 +46,7 @@ import androidx.annotation.VisibleForTesting; import com.android.settings.core.InstrumentedFragment; import com.android.settings.enterprise.ActionDisabledByAdminDialogHelper; +import com.android.settings.network.telephony.SubscriptionActionDialogActivity; import com.android.settingslib.RestrictedLockUtilsInternal; import com.google.android.setupcompat.template.FooterBarMixin; @@ -90,7 +95,7 @@ public class MainClearConfirm extends InstrumentedFragment { } else { pdbManager = null; } - + setSimDialogProgressState(); if (shouldWipePersistentDataBlock(pdbManager)) { new AsyncTask() { @@ -128,6 +133,17 @@ public class MainClearConfirm extends InstrumentedFragment { } else { doMainClear(); } + + } + + private void setSimDialogProgressState() { + if (getActivity() != null) { + final SharedPreferences prefs = getActivity().getSharedPreferences( + SubscriptionActionDialogActivity.SIM_ACTION_DIALOG_PREFS, MODE_PRIVATE); + prefs.edit().putInt(SubscriptionActionDialogActivity.KEY_PROGRESS_STATE, + SubscriptionActionDialogActivity.PROGRESS_IS_SHOWING).apply(); + Log.d(TAG, "SIM dialog setProgressState: 1"); + } } private ProgressDialog getProgressDialog() { From 307cb087604258b04e6b42368d39e88be46fc3aa Mon Sep 17 00:00:00 2001 From: David Liu Date: Thu, 24 Oct 2024 22:28:25 +0000 Subject: [PATCH 10/16] Migrate About phone Add the skeleton of About phone Bug: 373001670 Test: atest MyDeviceInfoScreenTest Flag: com.android.settings.flags.catalyst_my_device_info_pref_screen Change-Id: I7799fa5fb82f9dd96959f1419108d15e7e4f7bb9 --- aconfig/catalyst/about_phone.aconfig | 6 ++ .../aboutphone/MyDeviceInfoFragment.java | 8 +++ .../aboutphone/MyDeviceInfoScreen.kt | 62 +++++++++++++++++++ .../aboutphone/MyDeviceInfoScreenTest.kt | 58 +++++++++++++++++ 4 files changed, 134 insertions(+) create mode 100644 src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoScreen.kt create mode 100644 tests/robotests/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoScreenTest.kt diff --git a/aconfig/catalyst/about_phone.aconfig b/aconfig/catalyst/about_phone.aconfig index 5403b652c6c..8ecde46eae5 100644 --- a/aconfig/catalyst/about_phone.aconfig +++ b/aconfig/catalyst/about_phone.aconfig @@ -15,3 +15,9 @@ flag { bug: "323791114" } +flag { + name: "catalyst_my_device_info_pref_screen" + namespace: "android_settings" + description: "Flag for About phone" + bug: "323791114" +} diff --git a/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java b/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java index 50b60973d5b..e209d85f543 100644 --- a/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java +++ b/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java @@ -25,6 +25,9 @@ import android.os.Bundle; import android.os.UserManager; import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.dashboard.DashboardFragment; @@ -211,6 +214,11 @@ public class MyDeviceInfoFragment extends DashboardFragment controller.updateDeviceName(confirm); } + @Override + public @Nullable String getPreferenceScreenBindingKey(@NonNull Context context) { + return MyDeviceInfoScreen.KEY; + } + /** * For Search. */ diff --git a/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoScreen.kt b/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoScreen.kt new file mode 100644 index 00000000000..254a91e5ab3 --- /dev/null +++ b/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoScreen.kt @@ -0,0 +1,62 @@ +/* + * 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.deviceinfo.aboutphone + +import android.content.Context +import android.os.Build +import android.provider.Settings +import com.android.settings.R +import com.android.settings.flags.Flags +import com.android.settingslib.metadata.PreferenceIconProvider +import com.android.settingslib.metadata.PreferenceSummaryProvider +import com.android.settingslib.metadata.ProvidePreferenceScreen +import com.android.settingslib.metadata.preferenceHierarchy +import com.android.settingslib.preference.PreferenceScreenCreator + +@ProvidePreferenceScreen +class MyDeviceInfoScreen : + PreferenceScreenCreator, PreferenceSummaryProvider, PreferenceIconProvider { + override val key: String + get() = KEY + + override val title: Int + get() = R.string.about_settings + + override fun getSummary(context: Context): CharSequence? { + return Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME) + ?: Build.MODEL + } + + override fun getIcon(context: Context): Int { + return when (Flags.homepageRevamp()) { + true -> R.drawable.ic_settings_about_device_filled + false -> R.drawable.ic_settings_about_device + } + } + + override fun isFlagEnabled(context: Context) = Flags.catalystMyDeviceInfoPrefScreen() + + override fun fragmentClass() = MyDeviceInfoFragment::class.java + + override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) {} + + override fun hasCompleteHierarchy() = false + + companion object { + const val KEY = "my_device_info_pref_screen" + } +} diff --git a/tests/robotests/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoScreenTest.kt b/tests/robotests/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoScreenTest.kt new file mode 100644 index 00000000000..14479d7c2f0 --- /dev/null +++ b/tests/robotests/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoScreenTest.kt @@ -0,0 +1,58 @@ +/* + * 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.deviceinfo.aboutphone + +import android.content.Context +import android.os.Build +import android.provider.Settings.Global +import com.android.settings.flags.Flags +import com.android.settingslib.preference.CatalystScreenTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.robolectric.RuntimeEnvironment + +class MyDeviceInfoScreenTest : CatalystScreenTestCase() { + private lateinit var context: Context + + override val preferenceScreenCreator = MyDeviceInfoScreen() + override val flagName: String + get() = Flags.FLAG_CATALYST_MY_DEVICE_INFO_PREF_SCREEN + + override fun migration() {} + + @Before + fun setup() { + context = RuntimeEnvironment.getApplication() + } + + @Test + fun key() { + assertThat(preferenceScreenCreator.key).isEqualTo(MyDeviceInfoScreen.KEY) + } + + @Test + fun getSummary_deviceNameNotSet_shouldReturnDeviceModel() { + assertThat(preferenceScreenCreator.getSummary(context)?.toString()).isEqualTo(Build.MODEL) + } + + @Test + fun getSummary_deviceNameSet_shouldReturnDeviceName() { + Global.putString(context.contentResolver, Global.DEVICE_NAME, "Test") + assertThat(preferenceScreenCreator.getSummary(context)?.toString()).isEqualTo("Test") + } +} From 7c235e3c0c2e39a15d9586fbad5124089c995c3f Mon Sep 17 00:00:00 2001 From: Austin Delgado Date: Thu, 26 Sep 2024 10:31:21 -0700 Subject: [PATCH 11/16] Force Private Space to always use biometric prompt Bug: 365094949 Bug: 365932447 Bug: 366441553 Test: Verified manually Flag: android.hardware.biometrics.private_space_bp Change-Id: I78cb53e6e38c73d7819f7368546f53bc7026f9b5 --- .../settings/password/ConfirmDeviceCredentialActivity.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java index d656934a26b..302cf1fcdf0 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java @@ -377,7 +377,14 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity { setBiometricPromptPropertiesForPrivateProfile(promptInfo); showBiometricPrompt(promptInfo, effectiveUserId); launchedBiometric = true; + } else if (Flags.privateSpaceBp()) { + promptInfo.setAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL); + setBiometricPromptPropertiesForPrivateProfile(promptInfo); + showBiometricPrompt(promptInfo, mUserId); + launchedBiometric = true; } else { + // TODO(b/376328272): Remove custom private space behavior + mDetails = Utils.getConfirmCredentialStringForUser(this, mUserId, credentialType); showConfirmCredentials(); launchedCDC = true; } From 792d8a97dd1e8e8892b1c99b78aabf53227241f7 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Mon, 28 Oct 2024 15:57:19 +0800 Subject: [PATCH 12/16] [Catalyst] Tethering screen migration - Add a flag for the migration - Add the skeleton of the screen Test: atest TetherScreenTest Bug: 368359963 Flag: com.android.settings.flags.catalyst_tether_settings Change-Id: Ib9987def4f1a644f7661e75c314b3a556e8f210c --- aconfig/catalyst/network_and_internet.aconfig | 7 ++ .../settings/network/tether/TetherScreen.kt | 63 +++++++++++ .../network/tether/TetherSettings.java | 6 + .../network/tether/TetherScreenTest.kt | 103 ++++++++++++++++++ 4 files changed, 179 insertions(+) create mode 100644 src/com/android/settings/network/tether/TetherScreen.kt create mode 100644 tests/robotests/src/com/android/settings/network/tether/TetherScreenTest.kt diff --git a/aconfig/catalyst/network_and_internet.aconfig b/aconfig/catalyst/network_and_internet.aconfig index e8943e66d84..aa26ce42be6 100644 --- a/aconfig/catalyst/network_and_internet.aconfig +++ b/aconfig/catalyst/network_and_internet.aconfig @@ -22,6 +22,13 @@ flag { bug: "323791114" } +flag { + name: "catalyst_tether_settings" + namespace: "android_settings" + description: "Flag for Hotspot & tethering" + bug: "323791114" +} + flag { name: "catalyst_adaptive_connectivity" namespace: "android_settings" diff --git a/src/com/android/settings/network/tether/TetherScreen.kt b/src/com/android/settings/network/tether/TetherScreen.kt new file mode 100644 index 00000000000..20dc4b704fc --- /dev/null +++ b/src/com/android/settings/network/tether/TetherScreen.kt @@ -0,0 +1,63 @@ +/* + * 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.network.tether + +import android.content.Context +import android.net.TetheringManager +import com.android.settings.R +import com.android.settings.flags.Flags +import com.android.settings.network.TetherPreferenceController +import com.android.settingslib.TetherUtil +import com.android.settingslib.Utils +import com.android.settingslib.metadata.PreferenceAvailabilityProvider +import com.android.settingslib.metadata.ProvidePreferenceScreen +import com.android.settingslib.metadata.preferenceHierarchy +import com.android.settingslib.preference.PreferenceScreenCreator + +@ProvidePreferenceScreen +class TetherScreen : PreferenceScreenCreator, PreferenceAvailabilityProvider { + + override val key: String + get() = KEY + + override val icon: Int + get() = R.drawable.ic_wifi_tethering + + override val keywords: Int + get() = R.string.keywords_hotspot_tethering + + override fun getPreferenceTitle(context: Context): CharSequence? = + if (TetherPreferenceController.isTetherConfigDisallowed(context)) { + context.getText(R.string.tether_settings_title_all) + } else { + val tetheringManager = context.getSystemService(TetheringManager::class.java)!! + context.getText(Utils.getTetheringLabel(tetheringManager)) + } + + override fun isAvailable(context: Context) = TetherUtil.isTetherAvailable(context) + + override fun isFlagEnabled(context: Context) = Flags.catalystTetherSettings() + + override fun hasCompleteHierarchy() = false + + override fun fragmentClass() = TetherSettings::class.java + + override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) {} + + companion object { + const val KEY = "tether_settings" + } +} diff --git a/src/com/android/settings/network/tether/TetherSettings.java b/src/com/android/settings/network/tether/TetherSettings.java index 74585149fe6..1db1802d2c0 100644 --- a/src/com/android/settings/network/tether/TetherSettings.java +++ b/src/com/android/settings/network/tether/TetherSettings.java @@ -48,6 +48,7 @@ import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.ViewModelProvider; import androidx.preference.Preference; @@ -731,4 +732,9 @@ public class TetherSettings extends RestrictedDashboardFragment } updateBluetoothAndEthernetState(); } + + @Override + public @Nullable String getPreferenceScreenBindingKey(@NonNull Context context) { + return TetherScreen.KEY; + } } diff --git a/tests/robotests/src/com/android/settings/network/tether/TetherScreenTest.kt b/tests/robotests/src/com/android/settings/network/tether/TetherScreenTest.kt new file mode 100644 index 00000000000..0eeac4353f1 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/tether/TetherScreenTest.kt @@ -0,0 +1,103 @@ +/* + * 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.network.tether + +import android.net.TetheringManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settings.flags.Flags +import com.android.settings.testutils.shadow.ShadowConnectivityManager +import com.android.settings.testutils.shadow.ShadowRestrictedLockUtilsInternal +import com.android.settingslib.Utils +import com.android.settingslib.preference.CatalystScreenTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements + +@RunWith(AndroidJUnit4::class) +@Config(shadows = [ShadowConnectivityManager::class, ShadowRestrictedLockUtilsInternal::class, + ShadowTetheringManager::class]) +class TetherScreenTest : CatalystScreenTestCase() { + override val preferenceScreenCreator = TetherScreen() + + override val flagName: String + get() = Flags.FLAG_CATALYST_TETHER_SETTINGS + + @Before + fun setUp() { + ShadowConnectivityManager.getShadow().setTetheringSupported(true) + } + + @Test + fun key() { + assertThat(preferenceScreenCreator.key).isEqualTo(TetherScreen.KEY) + } + + @Test + fun getPreferenceTitle_tetherConfigDisallowed_shouldShowAll() { + ShadowRestrictedLockUtilsInternal.setRestricted(true) + + assertThat(preferenceScreenCreator.getPreferenceTitle(appContext)).isEqualTo( + appContext.getString(R.string.tether_settings_title_all)) + } + + @Test + fun getPreferenceTitle_tetherConfigAllowed_shouldShowTetheringLabel() { + ShadowRestrictedLockUtilsInternal.setRestricted(false) + val tm = appContext.getSystemService(TetheringManager::class.java) + + assertThat(preferenceScreenCreator.getPreferenceTitle(appContext)).isEqualTo( + appContext.getText(Utils.getTetheringLabel(tm))) + } + + @Test + fun isAvailable_tetherIsAvailable_shouldReturnTrue() { + ShadowRestrictedLockUtilsInternal.setRestricted(false) + + assertThat(preferenceScreenCreator.isAvailable(appContext)).isTrue() + } + + @Test + fun isAvailable_tetherIsUnavailable_shouldReturnFalse() { + ShadowRestrictedLockUtilsInternal.setRestricted(true) + + assertThat(preferenceScreenCreator.isAvailable(appContext)).isFalse() + } +} + +@Implements(TetheringManager::class) +class ShadowTetheringManager { + private val emptyArray = arrayOf() + + @Implementation + fun getTetheredIfaces() = emptyArray + + @Implementation + fun getTetherableIfaces() = emptyArray + + @Implementation + fun getTetherableWifiRegexs() = emptyArray + + @Implementation + fun getTetherableUsbRegexs() = emptyArray + + @Implementation + fun getTetherableBluetoothRegexs() = emptyArray +} From f035eefe67413aa835f57e5c3064ec777c25959b Mon Sep 17 00:00:00 2001 From: Jacky Wang Date: Thu, 31 Oct 2024 09:47:04 +0800 Subject: [PATCH 13/16] [Catalyst] Implement datastore for Smooth display NO_IFTTT=Catalyst only Bug: 374936314 Flag: com.android.settings.flags.catalyst_display_settings_screen Test: manual & adb shell settings get/put/delete system peak_refresh_rate Change-Id: Idc9379935934c32ef040be6c4c838e5fbb65319d --- .../PeakRefreshRateSwitchPreference.kt | 110 ++++++++++++------ 1 file changed, 72 insertions(+), 38 deletions(-) diff --git a/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt b/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt index 793b0e226a7..5302fce5451 100644 --- a/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt +++ b/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt @@ -18,53 +18,51 @@ package com.android.settings.display import android.content.Context import android.hardware.display.DisplayManager import android.provider.DeviceConfig -import android.util.Log +import android.provider.Settings.System.PEAK_REFRESH_RATE import com.android.internal.display.RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE import com.android.internal.display.RefreshRateSettingsUtils.findHighestRefreshRateAmongAllDisplays import com.android.internal.display.RefreshRateSettingsUtils.findHighestRefreshRateForDefaultDisplay import com.android.server.display.feature.flags.Flags import com.android.settings.R import com.android.settingslib.datastore.HandlerExecutor +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.datastore.KeyedObservableDelegate +import com.android.settingslib.datastore.SettingsStore import com.android.settingslib.datastore.SettingsSystemStore import com.android.settingslib.metadata.PreferenceAvailabilityProvider import com.android.settingslib.metadata.PreferenceLifecycleContext import com.android.settingslib.metadata.PreferenceLifecycleProvider import com.android.settingslib.metadata.PreferenceSummaryProvider import com.android.settingslib.metadata.SwitchPreference -import com.android.settingslib.preference.SwitchPreferenceBinding import kotlin.math.roundToInt // LINT.IfChange class PeakRefreshRateSwitchPreference : - SwitchPreference("peak_refresh_rate", R.string.peak_refresh_rate_title), - SwitchPreferenceBinding, + SwitchPreference(PEAK_REFRESH_RATE, R.string.peak_refresh_rate_title), PreferenceAvailabilityProvider, PreferenceSummaryProvider, PreferenceLifecycleProvider { private var propertiesChangedListener: DeviceConfig.OnPropertiesChangedListener? = null - override fun storage(context: Context) = SettingsSystemStore.get(context) + override fun storage(context: Context): KeyValueStore = + PeakRefreshRateStore(context, SettingsSystemStore.get(context)) override fun isAvailable(context: Context) = context.resources.getBoolean(R.bool.config_show_smooth_display) && - (getPeakRefreshRate(context) > DEFAULT_REFRESH_RATE) + context.peakRefreshRate > DEFAULT_REFRESH_RATE - override fun getSummary(context: Context) = - context.getString( - R.string.peak_refresh_rate_summary, - getPeakRefreshRate(context).roundToInt(), - ) + override fun getSummary(context: Context): CharSequence? = + context.getString(R.string.peak_refresh_rate_summary, context.peakRefreshRate.roundToInt()) override fun onStart(context: PreferenceLifecycleContext) { val listener = - object : DeviceConfig.OnPropertiesChangedListener { + DeviceConfig.OnPropertiesChangedListener { // Got notified if any property has been changed in NAMESPACE_DISPLAY_MANAGER. The // KEY_PEAK_REFRESH_RATE_DEFAULT value could be added, changed, removed or // unchanged. // Just force a UI update for any case. - override fun onPropertiesChanged(properties: DeviceConfig.Properties) = - context.notifyPreferenceChange(this@PeakRefreshRateSwitchPreference) + context.notifyPreferenceChange(this) } propertiesChangedListener = listener @@ -83,38 +81,74 @@ class PeakRefreshRateSwitchPreference : } } - private fun getPeakRefreshRate(context: Context): Float = - Math.round( - when { - Flags.backUpSmoothDisplayAndForcePeakRefreshRate() -> - findHighestRefreshRateAmongAllDisplays(context) - else -> findHighestRefreshRateForDefaultDisplay(context) - } - ) - .toFloat() + @Suppress("UNCHECKED_CAST") + private class PeakRefreshRateStore( + private val context: Context, + private val settingsStore: SettingsStore, + ) : KeyedObservableDelegate(settingsStore), KeyValueStore { - private fun getDefaultPeakRefreshRate(context: Context): Float { - var defaultPeakRefreshRate = - DeviceConfig.getFloat( - DeviceConfig.NAMESPACE_DISPLAY_MANAGER, - DisplayManager.DeviceConfig.KEY_PEAK_REFRESH_RATE_DEFAULT, - INVALIDATE_REFRESH_RATE, - ) + override fun contains(key: String) = settingsStore.contains(key) - if (defaultPeakRefreshRate == INVALIDATE_REFRESH_RATE) { - defaultPeakRefreshRate = - context.resources - .getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate) - .toFloat() + override fun getDefaultValue(key: String, valueType: Class): T? { + if (key != PEAK_REFRESH_RATE) return super.getDefaultValue(key, valueType) + return context.defaultPeakRefreshRate.refreshRateAsBoolean(context) as T } - Log.d(TAG, "DeviceConfig getDefaultPeakRefreshRate : $defaultPeakRefreshRate") - return defaultPeakRefreshRate + override fun getValue(key: String, valueType: Class): T? { + if (key != PEAK_REFRESH_RATE) return null + val refreshRate = + settingsStore.getFloat(PEAK_REFRESH_RATE) ?: context.defaultPeakRefreshRate + return refreshRate.refreshRateAsBoolean(context) as T + } + + private fun Float.refreshRateAsBoolean(context: Context) = + this.isInfinite() || roundToInt() == context.peakRefreshRate.roundToInt() + + override fun setValue(key: String, valueType: Class, value: T?) = + when { + key != PEAK_REFRESH_RATE -> {} + value == null -> settingsStore.setFloat(PEAK_REFRESH_RATE, null) + else -> { + val peakRefreshRate = + if (value as Boolean) context.refreshRateIfON() else DEFAULT_REFRESH_RATE + settingsStore.setFloat(PEAK_REFRESH_RATE, peakRefreshRate) + } + } + + private fun Context.refreshRateIfON() = + when { + Flags.backUpSmoothDisplayAndForcePeakRefreshRate() -> Float.POSITIVE_INFINITY + else -> peakRefreshRate + } } companion object { - private const val TAG: String = "PeakRefreshRateSwitchPreference" private const val INVALIDATE_REFRESH_RATE: Float = -1f + + private val Context.peakRefreshRate: Float + get() = + Math.round( + when { + Flags.backUpSmoothDisplayAndForcePeakRefreshRate() -> + findHighestRefreshRateAmongAllDisplays(this) + else -> findHighestRefreshRateForDefaultDisplay(this) + } + ) + .toFloat() + + private val Context.defaultPeakRefreshRate: Float + get() { + val defaultPeakRefreshRate = + DeviceConfig.getFloat( + DeviceConfig.NAMESPACE_DISPLAY_MANAGER, + DisplayManager.DeviceConfig.KEY_PEAK_REFRESH_RATE_DEFAULT, + INVALIDATE_REFRESH_RATE, + ) + if (defaultPeakRefreshRate != INVALIDATE_REFRESH_RATE) return defaultPeakRefreshRate + return resources + .getInteger(com.android.internal.R.integer.config_defaultPeakRefreshRate) + .toFloat() + } } } // LINT.ThenChange(PeakRefreshRatePreferenceController.java) From 75b443375fcc3fc377a17790a86840237b286c57 Mon Sep 17 00:00:00 2001 From: Mill Chen Date: Tue, 22 Oct 2024 06:03:16 +0000 Subject: [PATCH 14/16] [Catalyst] Media volume migration Bug: 364898461 Test: atest MediaVolumePreferenceTest Flag: com.android.settings.flags.catalyst_sound_screen Change-Id: I172f336a7d1c843d071dd920d29aa35b4a1a8aaf --- .../notification/MediaVolumePreference.kt | 118 ++++++++++++++++++ .../MediaVolumePreferenceController.java | 2 + .../settings/notification/SoundScreen.kt | 1 + .../settings/notification/VolumeHelper.kt | 62 +++++++++ .../MediaVolumePreferenceControllerTest.java | 2 + .../notification/MediaVolumePreferenceTest.kt | 56 +++++++++ 6 files changed, 241 insertions(+) create mode 100644 src/com/android/settings/notification/MediaVolumePreference.kt create mode 100644 src/com/android/settings/notification/VolumeHelper.kt create mode 100644 tests/robotests/src/com/android/settings/notification/MediaVolumePreferenceTest.kt diff --git a/src/com/android/settings/notification/MediaVolumePreference.kt b/src/com/android/settings/notification/MediaVolumePreference.kt new file mode 100644 index 00000000000..13fd0297540 --- /dev/null +++ b/src/com/android/settings/notification/MediaVolumePreference.kt @@ -0,0 +1,118 @@ +/* + * 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.notification + +import android.content.Context +import android.media.AudioManager.STREAM_MUSIC +import android.os.UserHandle +import android.os.UserManager +import androidx.preference.Preference +import com.android.settings.R +import com.android.settingslib.RestrictedLockUtilsInternal +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.datastore.NoOpKeyedObservable +import com.android.settingslib.metadata.* +import com.android.settingslib.preference.PreferenceBinding + +// LINT.IfChange +open class MediaVolumePreference : + PreferenceMetadata, + PreferenceBinding, + PersistentPreference, + RangeValue, + PreferenceAvailabilityProvider, + PreferenceIconProvider, + PreferenceRestrictionProvider { + override val key: String + get() = KEY + + override val title: Int + get() = R.string.media_volume_option_title + + override fun getIcon(context: Context) = + when { + VolumeHelper.isMuted(context, STREAM_MUSIC) -> R.drawable.ic_media_stream_off + else -> R.drawable.ic_media_stream + } + + override fun isAvailable(context: Context) = + context.resources.getBoolean(R.bool.config_show_media_volume) + + override fun isRestricted(context: Context) = + RestrictedLockUtilsInternal.hasBaseUserRestriction( + context, + UserManager.DISALLOW_ADJUST_VOLUME, + UserHandle.myUserId(), + ) || + RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + context, + UserManager.DISALLOW_ADJUST_VOLUME, + UserHandle.myUserId(), + ) != null + + override fun storage(context: Context): KeyValueStore { + val helper = createAudioHelper(context) + return object : NoOpKeyedObservable(), KeyValueStore { + override fun contains(key: String) = key == KEY + + @Suppress("UNCHECKED_CAST") + override fun getValue(key: String, valueType: Class) = + helper.getStreamVolume(STREAM_MUSIC) as T + + override fun setValue(key: String, valueType: Class, value: T?) { + helper.setStreamVolume(STREAM_MUSIC, value as Int) + } + } + } + + override fun getMinValue(context: Context) = + createAudioHelper(context).getMinVolume(STREAM_MUSIC) + + override fun getMaxValue(context: Context) = + createAudioHelper(context).getMaxVolume(STREAM_MUSIC) + + override fun createWidget(context: Context) = VolumeSeekBarPreference(context) + + override fun bind(preference: Preference, metadata: PreferenceMetadata) { + super.bind(preference, metadata) + (preference as VolumeSeekBarPreference).apply { + setStream(STREAM_MUSIC) + setMuteIcon(R.drawable.ic_media_stream_off) + setListener { updateContentDescription(this) } + } + } + + open fun createAudioHelper(context: Context) = AudioHelper(context) + + fun updateContentDescription(preference: VolumeSeekBarPreference) { + when { + preference.isMuted() -> + preference.updateContentDescription( + preference.context.getString( + R.string.volume_content_description_silent_mode, + preference.title, + ) + ) + else -> preference.updateContentDescription(preference.title) + } + } + + companion object { + const val KEY = "media_volume" + } +} +// LINT.ThenChange(MediaVolumePreferenceController.java) diff --git a/src/com/android/settings/notification/MediaVolumePreferenceController.java b/src/com/android/settings/notification/MediaVolumePreferenceController.java index e70cf95e232..431806a494d 100644 --- a/src/com/android/settings/notification/MediaVolumePreferenceController.java +++ b/src/com/android/settings/notification/MediaVolumePreferenceController.java @@ -42,6 +42,7 @@ import com.android.settingslib.media.BluetoothMediaDevice; import com.android.settingslib.media.MediaDevice; import com.android.settingslib.media.MediaOutputConstants; +// LINT.IfChange public class MediaVolumePreferenceController extends VolumeSeekBarPreferenceController { private static final String TAG = "MediaVolumePreCtrl"; private static final String KEY_MEDIA_VOLUME = "media_volume"; @@ -204,3 +205,4 @@ public class MediaVolumePreferenceController extends VolumeSeekBarPreferenceCont return MediaOutputIndicatorWorker.class; } } +// LINT.ThenChange(MediaVolumePreference.kt) diff --git a/src/com/android/settings/notification/SoundScreen.kt b/src/com/android/settings/notification/SoundScreen.kt index e36576db4f8..7fc6243c6fa 100644 --- a/src/com/android/settings/notification/SoundScreen.kt +++ b/src/com/android/settings/notification/SoundScreen.kt @@ -49,6 +49,7 @@ class SoundScreen : PreferenceScreenCreator, PreferenceIconProvider { override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) { + +MediaVolumePreference() order -180 +CallVolumePreference() order -170 +DialPadTonePreference() order -50 } diff --git a/src/com/android/settings/notification/VolumeHelper.kt b/src/com/android/settings/notification/VolumeHelper.kt new file mode 100644 index 00000000000..73e490edbc5 --- /dev/null +++ b/src/com/android/settings/notification/VolumeHelper.kt @@ -0,0 +1,62 @@ +/* + * 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.notification + +import android.app.NotificationManager +import android.app.NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS +import android.app.NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA +import android.content.Context +import android.media.AudioManager +import android.media.AudioManager.* +import android.provider.Settings.Global.ZEN_MODE_ALARMS +import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS +import android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS +import android.service.notification.ZenModeConfig + +class VolumeHelper { + companion object { + fun isMuted(context: Context, streamType: Int): Boolean { + val audioManager = context.getSystemService(AudioManager::class.java) + return audioManager.isStreamMute(streamType) && !isZenMuted(context, streamType) + } + + fun isZenMuted(context: Context, streamType: Int): Boolean { + val notificationManager = context.getSystemService(NotificationManager::class.java) + val zenMode = notificationManager.getZenMode() + val notificationPolicy = notificationManager.getConsolidatedNotificationPolicy() + val isAllowAlarms = + (notificationPolicy.priorityCategories and PRIORITY_CATEGORY_ALARMS) != 0 + val isAllowMedia = + (notificationPolicy.priorityCategories and PRIORITY_CATEGORY_MEDIA) != 0 + val isAllowRinger = + !ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(notificationPolicy) + return isNotificationOrRingStream(streamType) + && zenMode == ZEN_MODE_ALARMS || zenMode == ZEN_MODE_NO_INTERRUPTIONS + || (zenMode == ZEN_MODE_IMPORTANT_INTERRUPTIONS + && (!isAllowRinger && isNotificationOrRingStream(streamType) + || !isAllowMedia && isMediaStream(streamType) + || !isAllowAlarms && isAlarmStream(streamType))) + } + + private fun isNotificationOrRingStream(streamType: Int) = + streamType == STREAM_RING || streamType == STREAM_NOTIFICATION + + private fun isAlarmStream(streamType: Int) = streamType == STREAM_ALARM + + private fun isMediaStream(streamType: Int) = streamType == STREAM_MUSIC + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/MediaVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/MediaVolumePreferenceControllerTest.java index a25f47200a3..2965acfa3fb 100644 --- a/tests/robotests/src/com/android/settings/notification/MediaVolumePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/MediaVolumePreferenceControllerTest.java @@ -58,6 +58,7 @@ import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +// LINT.IfChange @RunWith(RobolectricTestRunner.class) @Config(shadows = MediaVolumePreferenceControllerTest.ShadowSliceBackgroundWorker.class) public class MediaVolumePreferenceControllerTest { @@ -269,3 +270,4 @@ public class MediaVolumePreferenceControllerTest { PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } } +// LINT.ThenChange(MediaVolumePreference.kt) diff --git a/tests/robotests/src/com/android/settings/notification/MediaVolumePreferenceTest.kt b/tests/robotests/src/com/android/settings/notification/MediaVolumePreferenceTest.kt new file mode 100644 index 00000000000..383acfbc277 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/MediaVolumePreferenceTest.kt @@ -0,0 +1,56 @@ +/* + * 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.notification + +import android.content.ContextWrapper +import android.content.res.Resources +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +// LINT.IfChange +@RunWith(AndroidJUnit4::class) +class MediaVolumePreferenceTest { + private val mockResources = mock() + + private val mediaVolumePreference = MediaVolumePreference() + private val context = + object : ContextWrapper(ApplicationProvider.getApplicationContext()) { + override fun getResources(): Resources = mockResources + } + + @Test + fun isAvailable_configTrue_shouldReturnTrue() { + mockResources.stub { on { getBoolean(anyInt()) } doReturn true } + + assertThat(mediaVolumePreference.isAvailable(context)).isTrue() + } + + @Test + fun isAvailable_configFalse_shouldReturnFalse() { + mockResources.stub { on { getBoolean(anyInt()) } doReturn false } + + assertThat(mediaVolumePreference.isAvailable(context)).isFalse() + } +} +// LINT.ThenChange(MediaVolumePreferenceControllerTest.java) From 7c9379e5d2edb75e7d2ef06ea8f5e5b1e777f285 Mon Sep 17 00:00:00 2001 From: Mill Chen Date: Wed, 23 Oct 2024 09:42:58 +0000 Subject: [PATCH 15/16] [Catalyst] Ring volume migration (1/n) Bug: 373978964 Test: atest SeparateRingVolumePreferenceTest Flag: com.android.settings.flags.catalyst_sound_screen Change-Id: Ibf8f546c84ffb5467f62a250de37d96b5c974e15 --- .../SeparateRingVolumePreference.kt | 172 ++++++++++++++++++ ...eparateRingVolumePreferenceController.java | 2 + .../settings/notification/SoundScreen.kt | 1 + ...ateRingVolumePreferenceControllerTest.java | 2 + .../SeparateRingVolumePreferenceTest.kt | 150 +++++++++++++++ 5 files changed, 327 insertions(+) create mode 100644 src/com/android/settings/notification/SeparateRingVolumePreference.kt create mode 100644 tests/robotests/src/com/android/settings/notification/SeparateRingVolumePreferenceTest.kt diff --git a/src/com/android/settings/notification/SeparateRingVolumePreference.kt b/src/com/android/settings/notification/SeparateRingVolumePreference.kt new file mode 100644 index 00000000000..2f696b1c0f9 --- /dev/null +++ b/src/com/android/settings/notification/SeparateRingVolumePreference.kt @@ -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.notification + +import android.app.INotificationManager +import android.app.NotificationManager +import android.content.Context +import android.media.AudioManager.RINGER_MODE_NORMAL +import android.media.AudioManager.RINGER_MODE_SILENT +import android.media.AudioManager.RINGER_MODE_VIBRATE +import android.media.AudioManager.STREAM_RING +import android.os.ServiceManager +import android.os.UserHandle +import android.os.UserManager.DISALLOW_ADJUST_VOLUME +import android.os.Vibrator +import android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS +import android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS +import androidx.preference.Preference +import com.android.settings.R +import com.android.settingslib.RestrictedLockUtilsInternal +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.datastore.NoOpKeyedObservable +import com.android.settingslib.metadata.PersistentPreference +import com.android.settingslib.metadata.PreferenceAvailabilityProvider +import com.android.settingslib.metadata.PreferenceIconProvider +import com.android.settingslib.metadata.PreferenceMetadata +import com.android.settingslib.metadata.PreferenceRestrictionProvider +import com.android.settingslib.metadata.RangeValue +import com.android.settingslib.preference.PreferenceBinding + +// LINT.IfChange +open class SeparateRingVolumePreference : + PreferenceMetadata, + PreferenceBinding, + PersistentPreference, + RangeValue, + PreferenceAvailabilityProvider, + PreferenceIconProvider, + PreferenceRestrictionProvider { + override val key: String + get() = KEY + + override val title: Int + get() = R.string.separate_ring_volume_option_title + + override fun getIcon(context: Context) = + when { + VolumeHelper.isMuted(context, STREAM_RING) -> getMuteIcon(context) + else -> R.drawable.ic_ring_volume + } + + override fun isAvailable(context: Context) = !createAudioHelper(context).isSingleVolume() + + override fun isEnabled(context: Context) = + !RestrictedLockUtilsInternal.hasBaseUserRestriction( + context, + DISALLOW_ADJUST_VOLUME, + UserHandle.myUserId(), + ) + + override fun isRestricted(context: Context) = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + context, + DISALLOW_ADJUST_VOLUME, + UserHandle.myUserId(), + ) != null + + override fun storage(context: Context): KeyValueStore { + val helper = createAudioHelper(context) + return object : NoOpKeyedObservable(), KeyValueStore { + override fun contains(key: String) = key == KEY + + @Suppress("UNCHECKED_CAST") + override fun getValue(key: String, valueType: Class) = + helper.getStreamVolume(STREAM_RING) as T + + override fun setValue(key: String, valueType: Class, value: T?) { + helper.setStreamVolume(STREAM_RING, value as Int) + } + } + } + + override fun getMinValue(context: Context) = + createAudioHelper(context).getMinVolume(STREAM_RING) + + override fun getMaxValue(context: Context) = + createAudioHelper(context).getMaxVolume(STREAM_RING) + + override fun createWidget(context: Context) = VolumeSeekBarPreference(context) + + override fun bind(preference: Preference, metadata: PreferenceMetadata) { + super.bind(preference, metadata) + (preference as VolumeSeekBarPreference).apply { + setStream(STREAM_RING) + setMuteIcon(getMuteIcon(preference.context)) + setListener { updateContentDescription(this) } + setSuppressionText(getSuppressionText(preference.context)) + } + } + + open fun createAudioHelper(context: Context) = AudioHelper(context) + + fun updateContentDescription(preference: VolumeSeekBarPreference) { + val context = preference.context + val ringerMode = getEffectiveRingerMode(context) + when (ringerMode) { + RINGER_MODE_VIBRATE -> + preference.updateContentDescription( + context.getString(R.string.ringer_content_description_vibrate_mode) + ) + RINGER_MODE_SILENT -> + preference.updateContentDescription( + context.getString(R.string.ringer_content_description_silent_mode) + ) + else -> preference.updateContentDescription(preference.title) + } + } + + fun getMuteIcon(context: Context): Int { + val ringerMode = getEffectiveRingerMode(context) + return when (ringerMode) { + RINGER_MODE_NORMAL -> R.drawable.ic_ring_volume + RINGER_MODE_VIBRATE -> R.drawable.ic_volume_ringer_vibrate + else -> R.drawable.ic_ring_volume_off + } + } + + fun getEffectiveRingerMode(context: Context): Int { + val hasVibrator = context.getSystemService(Vibrator::class.java)?.hasVibrator() ?: false + val ringerMode = createAudioHelper(context).ringerModeInternal + return when { + !hasVibrator && ringerMode == RINGER_MODE_VIBRATE -> RINGER_MODE_SILENT + else -> ringerMode + } + } + + fun getSuppressionText(context: Context): String? { + val suppressor = NotificationManager.from(context).getEffectsSuppressor() + val notificationManager = + INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE) + ) + val hints = notificationManager.getHintsFromListenerNoToken() + return when { + hintsMatch(hints) -> SuppressorHelper.getSuppressionText(context, suppressor) + else -> null + } + } + + private fun hintsMatch(hints: Int) = + (hints and HINT_HOST_DISABLE_CALL_EFFECTS) != 0 || + (hints and HINT_HOST_DISABLE_EFFECTS) != 0 + + companion object { + const val KEY = "separate_ring_volume" + } +} +// LINT.ThenChange(SeparateRingVolumePreferenceController.java) diff --git a/src/com/android/settings/notification/SeparateRingVolumePreferenceController.java b/src/com/android/settings/notification/SeparateRingVolumePreferenceController.java index 91926e3c977..feb976fe8cd 100644 --- a/src/com/android/settings/notification/SeparateRingVolumePreferenceController.java +++ b/src/com/android/settings/notification/SeparateRingVolumePreferenceController.java @@ -35,6 +35,7 @@ import com.android.settingslib.core.lifecycle.Lifecycle; /** * This slider is used to represent ring volume when ring is separated from notification */ +// LINT.IfChange public class SeparateRingVolumePreferenceController extends RingerModeAffectedVolumePreferenceController { @@ -149,3 +150,4 @@ public class SeparateRingVolumePreferenceController extends } } +// LINT.ThenChange(SeparateRingVolumePreference.kt) diff --git a/src/com/android/settings/notification/SoundScreen.kt b/src/com/android/settings/notification/SoundScreen.kt index 7fc6243c6fa..e074301ca6c 100644 --- a/src/com/android/settings/notification/SoundScreen.kt +++ b/src/com/android/settings/notification/SoundScreen.kt @@ -51,6 +51,7 @@ class SoundScreen : PreferenceScreenCreator, PreferenceIconProvider { preferenceHierarchy(this) { +MediaVolumePreference() order -180 +CallVolumePreference() order -170 + +SeparateRingVolumePreference() order -155 +DialPadTonePreference() order -50 } diff --git a/tests/robotests/src/com/android/settings/notification/SeparateRingVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/SeparateRingVolumePreferenceControllerTest.java index 2974af6f185..a243643d38a 100644 --- a/tests/robotests/src/com/android/settings/notification/SeparateRingVolumePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/SeparateRingVolumePreferenceControllerTest.java @@ -41,6 +41,7 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplication; +// LINT.IfChange @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowDeviceConfig.class}) public class SeparateRingVolumePreferenceControllerTest { @@ -108,3 +109,4 @@ public class SeparateRingVolumePreferenceControllerTest { } } +// LINT.ThenChange(SeparateRingVolumePreferenceTest.kt) diff --git a/tests/robotests/src/com/android/settings/notification/SeparateRingVolumePreferenceTest.kt b/tests/robotests/src/com/android/settings/notification/SeparateRingVolumePreferenceTest.kt new file mode 100644 index 00000000000..21ddd75304f --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/SeparateRingVolumePreferenceTest.kt @@ -0,0 +1,150 @@ +/* + * 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.notification + +import android.content.ContextWrapper +import android.media.AudioManager.RINGER_MODE_NORMAL +import android.media.AudioManager.RINGER_MODE_SILENT +import android.media.AudioManager.RINGER_MODE_VIBRATE +import android.os.Vibrator +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub + +// LINT.IfChange +@RunWith(AndroidJUnit4::class) +class SeparateRingVolumePreferenceTest { + private var audioHelper = mock() + private var vibrator: Vibrator? = null + private var ringVolumePreference = SeparateRingVolumePreference() + + private val context = + object : ContextWrapper(ApplicationProvider.getApplicationContext()) { + override fun getSystemService(name: String): Any? = + when { + name == getSystemServiceName(Vibrator::class.java) -> vibrator + else -> super.getSystemService(name) + } + } + + @Test + fun isAvailable_singleVolume_shouldReturnFalse() { + audioHelper = mock { on { isSingleVolume } doReturn true } + ringVolumePreference = + spy(ringVolumePreference).stub { + onGeneric { createAudioHelper(context) } doReturn audioHelper + } + + assertThat(ringVolumePreference.isAvailable(context)).isFalse() + } + + @Test + fun isAvailable_noSingleVolume_shouldReturnTrue() { + audioHelper = mock { on { isSingleVolume } doReturn false } + ringVolumePreference = + spy(ringVolumePreference).stub { + onGeneric { createAudioHelper(context) } doReturn audioHelper + } + + assertThat(ringVolumePreference.isAvailable(context)).isTrue() + } + + @Test + fun getEffectiveRingerMode_noVibratorAndVibrateMode_shouldReturnSilentMode() { + vibrator = mock { on { hasVibrator() } doReturn false } + audioHelper = mock { on { ringerModeInternal } doReturn RINGER_MODE_VIBRATE } + ringVolumePreference = + spy(ringVolumePreference).stub { + onGeneric { createAudioHelper(context) } doReturn audioHelper + } + + assertThat(ringVolumePreference.getEffectiveRingerMode(context)) + .isEqualTo(RINGER_MODE_SILENT) + } + + @Test + fun getEffectiveRingerMode_hasVibratorAndVibrateMode_shouldReturnVibrateMode() { + vibrator = mock { on { hasVibrator() } doReturn true } + audioHelper = mock { on { ringerModeInternal } doReturn RINGER_MODE_VIBRATE } + ringVolumePreference = + spy(ringVolumePreference).stub { + onGeneric { createAudioHelper(context) } doReturn audioHelper + } + + assertThat(ringVolumePreference.getEffectiveRingerMode(context)) + .isEqualTo(RINGER_MODE_VIBRATE) + } + + @Test + fun getEffectiveRingerMode_hasVibratorAndNormalMode_shouldReturnNormalMode() { + vibrator = mock { on { hasVibrator() } doReturn true } + audioHelper = mock { on { ringerModeInternal } doReturn RINGER_MODE_NORMAL } + ringVolumePreference = + spy(ringVolumePreference).stub { + onGeneric { createAudioHelper(context) } doReturn audioHelper + } + + assertThat(ringVolumePreference.getEffectiveRingerMode(context)) + .isEqualTo(RINGER_MODE_NORMAL) + } + + @Test + fun getMuteIcon_normalMode_shouldReturnRingVolumeIcon() { + vibrator = mock { on { hasVibrator() } doReturn true } + audioHelper = mock { on { ringerModeInternal } doReturn RINGER_MODE_NORMAL } + ringVolumePreference = + spy(ringVolumePreference).stub { + onGeneric { createAudioHelper(context) } doReturn audioHelper + } + + assertThat(ringVolumePreference.getMuteIcon(context)).isEqualTo(R.drawable.ic_ring_volume) + } + + @Test + fun getMuteIcon_vibrateMode_shouldReturnVibrateIcon() { + vibrator = mock { on { hasVibrator() } doReturn true } + audioHelper = mock { on { ringerModeInternal } doReturn RINGER_MODE_VIBRATE } + ringVolumePreference = + spy(ringVolumePreference).stub { + onGeneric { createAudioHelper(context) } doReturn audioHelper + } + + assertThat(ringVolumePreference.getMuteIcon(context)) + .isEqualTo(R.drawable.ic_volume_ringer_vibrate) + } + + @Test + fun getMuteIcon_silentMode_shouldReturnSilentIcon() { + vibrator = mock { on { hasVibrator() } doReturn false } + audioHelper = mock { on { ringerModeInternal } doReturn RINGER_MODE_VIBRATE } + ringVolumePreference = + spy(ringVolumePreference).stub { + onGeneric { createAudioHelper(context) } doReturn audioHelper + } + + assertThat(ringVolumePreference.getMuteIcon(context)) + .isEqualTo(R.drawable.ic_ring_volume_off) + } +} +// LINT.ThenChange(SeparateRingVolumePreferenceControllerTest.java) From 77aa48fe0b7e76a18fd1f7ce83f8a9977614d835 Mon Sep 17 00:00:00 2001 From: Sunny Shao Date: Wed, 30 Oct 2024 11:38:37 +0800 Subject: [PATCH 16/16] [Catalyst] Migrate Brightness level Test: atest DisplayScreenTest Bug: 375895862 Flag: com.android.settings.flags.catalyst_display_settings_screen Change-Id: Ia098e107e0e9f011db6742cc8d0ebecfc179cca6 --- res/xml/display_settings.xml | 1 + .../BrightnessLevelPreferenceController.java | 2 + .../BrightnessLevelRestrictedPreference.kt | 173 ++++++++++++++++++ .../android/settings/display/DisplayScreen.kt | 1 + 4 files changed, 177 insertions(+) create mode 100644 src/com/android/settings/display/BrightnessLevelRestrictedPreference.kt diff --git a/res/xml/display_settings.xml b/res/xml/display_settings.xml index 4e52cf49092..c7e29675757 100644 --- a/res/xml/display_settings.xml +++ b/res/xml/display_settings.xml @@ -28,6 +28,7 @@ diff --git a/src/com/android/settings/display/BrightnessLevelPreferenceController.java b/src/com/android/settings/display/BrightnessLevelPreferenceController.java index 96043db4c41..9c4af66e783 100644 --- a/src/com/android/settings/display/BrightnessLevelPreferenceController.java +++ b/src/com/android/settings/display/BrightnessLevelPreferenceController.java @@ -57,6 +57,7 @@ import java.text.NumberFormat; /** * The top-level preference controller that updates the adaptive brightness level. */ +// LINT.IfChange public class BrightnessLevelPreferenceController extends BasePreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnStart, OnStop { private static final Uri BRIGHTNESS_ADJ_URI; @@ -187,3 +188,4 @@ public class BrightnessLevelPreferenceController extends BasePreferenceControlle return (value - min) / (max - min); } } +// LINT.ThenChange(BrightnessLevelRestrictedPreference.kt) diff --git a/src/com/android/settings/display/BrightnessLevelRestrictedPreference.kt b/src/com/android/settings/display/BrightnessLevelRestrictedPreference.kt new file mode 100644 index 00000000000..a412b8cb310 --- /dev/null +++ b/src/com/android/settings/display/BrightnessLevelRestrictedPreference.kt @@ -0,0 +1,173 @@ +/* + * 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 + +import android.app.ActivityOptions +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_SHOW_BRIGHTNESS_DIALOG +import android.content.Intent.EXTRA_BRIGHTNESS_DIALOG_IS_FULL_WIDTH +import android.hardware.display.BrightnessInfo +import android.hardware.display.DisplayManager +import android.hardware.display.DisplayManager.DisplayListener +import android.os.Process +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings.System +import androidx.preference.Preference +import com.android.settings.R +import com.android.settings.Utils +import com.android.settings.core.SettingsBaseActivity +import com.android.settingslib.RestrictedLockUtilsInternal +import com.android.settingslib.RestrictedPreference +import com.android.settingslib.datastore.HandlerExecutor +import com.android.settingslib.datastore.KeyedObserver +import com.android.settingslib.datastore.SettingsSystemStore +import com.android.settingslib.display.BrightnessUtils.GAMMA_SPACE_MAX +import com.android.settingslib.display.BrightnessUtils.GAMMA_SPACE_MIN +import com.android.settingslib.display.BrightnessUtils.convertLinearToGammaFloat +import com.android.settingslib.metadata.PreferenceLifecycleContext +import com.android.settingslib.metadata.PreferenceLifecycleProvider +import com.android.settingslib.metadata.PreferenceMetadata +import com.android.settingslib.metadata.PreferenceRestrictionProvider +import com.android.settingslib.metadata.PreferenceSummaryProvider +import com.android.settingslib.preference.PreferenceBinding +import com.android.settingslib.transition.SettingsTransitionHelper +import java.text.NumberFormat + +// LINT.IfChange +class BrightnessLevelRestrictedPreference : + PreferenceMetadata, + PreferenceBinding, + PreferenceRestrictionProvider, + PreferenceSummaryProvider, + PreferenceLifecycleProvider, + Preference.OnPreferenceClickListener { + + private var brightnessObserver: KeyedObserver? = null + private var displayListener: DisplayListener? = null + + override val key: String + get() = "brightness" + + override val title: Int + get() = R.string.brightness + + override val keywords: Int + get() = R.string.keywords_display_brightness_level + + override fun getSummary(context: Context) = + NumberFormat.getPercentInstance().format(getCurrentBrightness(context)) + + override fun isEnabled(context: Context) = + !UserManager.get(context) + .hasBaseUserRestriction(UserManager.DISALLOW_CONFIG_BRIGHTNESS, Process.myUserHandle()) + + override fun isRestricted(context: Context) = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + context, + UserManager.DISALLOW_CONFIG_BRIGHTNESS, + UserHandle.myUserId(), + ) != null + + override fun createWidget(context: Context) = RestrictedPreference(context) + + override fun bind(preference: Preference, metadata: PreferenceMetadata) { + super.bind(preference, metadata) + if (preference is RestrictedPreference) preference.useAdminDisabledSummary(true) + preference.onPreferenceClickListener = this + } + + override fun onStart(context: PreferenceLifecycleContext) { + val observer = + object : KeyedObserver { + override fun onKeyChanged(key: String, reason: Int) { + context.notifyPreferenceChange(this@BrightnessLevelRestrictedPreference) + } + } + brightnessObserver = observer + SettingsSystemStore.get(context) + .addObserver(System.SCREEN_AUTO_BRIGHTNESS_ADJ, observer, HandlerExecutor.main) + + val listener = + object : DisplayListener { + override fun onDisplayAdded(displayId: Int) {} + + override fun onDisplayRemoved(displayId: Int) {} + + override fun onDisplayChanged(displayId: Int) { + context.notifyPreferenceChange(this@BrightnessLevelRestrictedPreference) + } + } + displayListener = listener + context + .getSystemService(DisplayManager::class.java) + .registerDisplayListener( + listener, + HandlerExecutor.main, + DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS, + ) + } + + override fun onStop(context: PreferenceLifecycleContext) { + brightnessObserver?.let { + SettingsSystemStore.get(context).removeObserver(System.SCREEN_AUTO_BRIGHTNESS_ADJ, it) + brightnessObserver = null + } + + displayListener?.let { + context.getSystemService(DisplayManager::class.java).unregisterDisplayListener(it) + displayListener = null + } + } + + override fun onPreferenceClick(preference: Preference): Boolean { + val context = preference.context + val intent = + Intent(ACTION_SHOW_BRIGHTNESS_DIALOG) + .setPackage(Utils.SYSTEMUI_PACKAGE_NAME) + .putExtra( + SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE, + SettingsTransitionHelper.TransitionType.TRANSITION_NONE, + ) + .putExtra(EXTRA_BRIGHTNESS_DIALOG_IS_FULL_WIDTH, true) + val options = + ActivityOptions.makeCustomAnimation( + context, + android.R.anim.fade_in, + android.R.anim.fade_out, + ) + context.startActivityForResult(preference.key, intent, 0, options.toBundle()) + return true + } + + private fun getCurrentBrightness(context: Context): Double { + val info: BrightnessInfo? = context.display.brightnessInfo + val value = + info?.run { + convertLinearToGammaFloat(brightness, brightnessMinimum, brightnessMaximum) + } + return getPercentage(value?.toDouble() ?: 0.0) + } + + private fun getPercentage(value: Double): Double = + when { + value > GAMMA_SPACE_MAX -> 1.0 + value < GAMMA_SPACE_MIN -> 0.0 + else -> (value - GAMMA_SPACE_MIN) / (GAMMA_SPACE_MAX - GAMMA_SPACE_MIN) + } +} +// LINT.ThenChange(BrightnessLevelPreferenceController.java) diff --git a/src/com/android/settings/display/DisplayScreen.kt b/src/com/android/settings/display/DisplayScreen.kt index bd21e8e668d..e9d874bd552 100644 --- a/src/com/android/settings/display/DisplayScreen.kt +++ b/src/com/android/settings/display/DisplayScreen.kt @@ -48,6 +48,7 @@ class DisplayScreen : override fun fragmentClass() = DisplaySettings::class.java override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) { + +BrightnessLevelRestrictedPreference() +DarkModeScreen.KEY +PeakRefreshRateSwitchPreference() }