From ccdc3c98fa36a37e5594436d44ca7c47a6795f4f Mon Sep 17 00:00:00 2001 From: Jaewan Kim Date: Tue, 18 Feb 2025 08:30:14 -0800 Subject: [PATCH 01/15] Reduce storage requirement for Terminal App This is for cuttlefish tablet which has 16 GB storage and roughly 8 GB free storage. Change-Id: I3658fe966f3cf08b5bfea409cff793ba60994cc2 Test: Manually Flags: Exempt. For testing with cuttlefish --- .../linuxterminal/LinuxTerminalPreferenceController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java b/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java index 0835081efff..93d3a0bf184 100644 --- a/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java +++ b/src/com/android/settings/development/linuxterminal/LinuxTerminalPreferenceController.java @@ -44,7 +44,7 @@ public class LinuxTerminalPreferenceController extends DeveloperOptionsPreferenc static final long MEMORY_MIN_BYTES = DataUnit.GIGABYTES.toBytes(4); // 4_000_000_000 @VisibleForTesting - static final long STORAGE_MIN_BYTES = DataUnit.GIGABYTES.toBytes(64); // 64_000_000_000 + static final long STORAGE_MIN_BYTES = DataUnit.GIGABYTES.toBytes(16); // 16_000_000_000 private static final String LINUX_TERMINAL_KEY = "linux_terminal"; From 1d32a08384f316b03d45d4eadb7a8c7b2e7cdbf8 Mon Sep 17 00:00:00 2001 From: Evan Chen Date: Mon, 3 Mar 2025 18:24:54 +0000 Subject: [PATCH 02/15] Fix the namespace for CDM flag in Settings Test: unit test Bug: 398042032 Flag: com.android.settings.flags.enable_remove_association_bt_unpair Change-Id: I6bb813d6dce5a879e25e99f3cb680e38f36e90b2 --- aconfig/settings_bluetooth_declarations.aconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aconfig/settings_bluetooth_declarations.aconfig b/aconfig/settings_bluetooth_declarations.aconfig index 7f3089500f6..e2edcb53a4c 100644 --- a/aconfig/settings_bluetooth_declarations.aconfig +++ b/aconfig/settings_bluetooth_declarations.aconfig @@ -58,7 +58,7 @@ flag { flag { name: "enable_remove_association_bt_unpair" is_exported: true - namespace: "companion_device_manager" + namespace: "companion" description: "Allow to disassociate when to forget a BT pair device" bug: "365613753" } From 6ed930462b018c0021fabf98a876418729e5f16c Mon Sep 17 00:00:00 2001 From: Graciela Putri Date: Thu, 6 Mar 2025 03:36:22 -0800 Subject: [PATCH 03/15] Remove (experimental) from app aspect ratio settings title Flag: EXEMPT string change Fix: 305215544 Test: m Change-Id: Idbb01ec99014efc40c285212e5e6ef8c91fa8333 --- AndroidManifest.xml | 4 ++-- res/values/strings.xml | 14 -------------- res/xml/apps.xml | 2 +- res/xml/user_aspect_ratio_details.xml | 2 +- .../app/appcompat/UserAspectRatioAppPreference.kt | 2 +- .../appcompat/UserAspectRatioAppsPageProvider.kt | 4 ++-- .../appcompat/UserAspectRatioAppPreferenceTest.kt | 2 +- .../UserAspectRatioAppsPageProviderTest.kt | 6 +++--- 8 files changed, 11 insertions(+), 25 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index bbe796320c2..8f5699c7572 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1899,7 +1899,7 @@ + android:label="@string/aspect_ratio_title"> @@ -1912,7 +1912,7 @@ + android:label="@string/aspect_ratio_title"> diff --git a/res/values/strings.xml b/res/values/strings.xml index 2958559101a..de844f2be9d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -13830,20 +13830,6 @@ Data usage charges may apply. The app will restart when you change aspect ratio. You may lose unsaved changes. Some apps may not be optimized for certain aspect ratios. - - - Aspect ratio (experimental) - - Aspect ratio (experiment) - - Aspect ratio (labs) - - Experimental - - Experiment - - Labs - Fingerprint sensor diff --git a/res/xml/apps.xml b/res/xml/apps.xml index 77b210f1821..b6874973b22 100644 --- a/res/xml/apps.xml +++ b/res/xml/apps.xml @@ -123,7 +123,7 @@ + android:title="@string/aspect_ratio_title"> Date: Thu, 20 Feb 2025 08:29:24 -0800 Subject: [PATCH 04/15] Create new device state auto rotate setting manager As part of auto-rotate setting refactor, create a new manager to be used between sysui and settings. This manager will replace the current DeviceStateRotationLockManager. Next CL: Integrate this manager to be used when auto-rotate refactor flag is ON. For more info:go/auto-rotate-refactor Bug: 394303723 Flag: com.android.window.flags.enable_device_state_auto_rotate_setting_refactor Test: atest DeviceStateAutoRotateSettingManagerImplTest Change-Id: Id1e09174fa3fb094f3aaf635b622b4bb9610f7f2 --- .../display/DeviceStateAutoRotateSettingController.java | 9 +++++---- .../settings/display/DeviceStateAutoRotationHelper.java | 2 +- .../settings/display/SmartAutoRotateController.java | 9 +++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/com/android/settings/display/DeviceStateAutoRotateSettingController.java b/src/com/android/settings/display/DeviceStateAutoRotateSettingController.java index e38f5d41bda..d3950ee1396 100644 --- a/src/com/android/settings/display/DeviceStateAutoRotateSettingController.java +++ b/src/com/android/settings/display/DeviceStateAutoRotateSettingController.java @@ -34,6 +34,7 @@ import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.devicestate.DeviceStateAutoRotateSettingManager; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; import com.android.settingslib.search.SearchIndexableRaw; @@ -47,8 +48,8 @@ public class DeviceStateAutoRotateSettingController extends TogglePreferenceCont private final DeviceStateRotationLockSettingsManager mAutoRotateSettingsManager; private final int mOrder; - private final DeviceStateRotationLockSettingsManager.DeviceStateRotationLockSettingsListener - mDeviceStateRotationLockSettingsListener = () -> updateState(mPreference); + private final DeviceStateAutoRotateSettingManager.DeviceStateAutoRotateSettingListener + mDeviceStateAutoRotateSettingListener = () -> updateState(mPreference); private final int mDeviceState; private final String mDeviceStateDescription; private final MetricsFeatureProvider mMetricsFeatureProvider; @@ -77,12 +78,12 @@ public class DeviceStateAutoRotateSettingController extends TogglePreferenceCont @OnLifecycleEvent(ON_START) void onStart() { - mAutoRotateSettingsManager.registerListener(mDeviceStateRotationLockSettingsListener); + mAutoRotateSettingsManager.registerListener(mDeviceStateAutoRotateSettingListener); } @OnLifecycleEvent(ON_STOP) void onStop() { - mAutoRotateSettingsManager.unregisterListener(mDeviceStateRotationLockSettingsListener); + mAutoRotateSettingsManager.unregisterListener(mDeviceStateAutoRotateSettingListener); } @Override diff --git a/src/com/android/settings/display/DeviceStateAutoRotationHelper.java b/src/com/android/settings/display/DeviceStateAutoRotationHelper.java index 223ef1aa4fa..3bf9def2316 100644 --- a/src/com/android/settings/display/DeviceStateAutoRotationHelper.java +++ b/src/com/android/settings/display/DeviceStateAutoRotationHelper.java @@ -26,7 +26,7 @@ import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; -import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager.SettableDeviceState; +import com.android.settingslib.devicestate.SettableDeviceState; import com.android.settingslib.search.SearchIndexableRaw; import com.google.common.collect.ImmutableList; diff --git a/src/com/android/settings/display/SmartAutoRotateController.java b/src/com/android/settings/display/SmartAutoRotateController.java index b5e3af223fd..c99b2f853ca 100644 --- a/src/com/android/settings/display/SmartAutoRotateController.java +++ b/src/com/android/settings/display/SmartAutoRotateController.java @@ -46,6 +46,7 @@ import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.devicestate.DeviceStateAutoRotateSettingManager; import com.android.settingslib.devicestate.DeviceStateRotationLockSettingsManager; /** @@ -75,8 +76,8 @@ public class SmartAutoRotateController extends TogglePreferenceController implem }; private final DeviceStateRotationLockSettingsManager mDeviceStateAutoRotateSettingsManager; - private final DeviceStateRotationLockSettingsManager.DeviceStateRotationLockSettingsListener - mDeviceStateRotationLockSettingsListener = () -> updateState(mPreference); + private final DeviceStateAutoRotateSettingManager.DeviceStateAutoRotateSettingListener + mDeviceStateAutoRotateSettingListener = () -> updateState(mPreference); private RotationPolicy.RotationPolicyListener mRotationPolicyListener; public SmartAutoRotateController(Context context, String preferenceKey) { @@ -140,7 +141,7 @@ public class SmartAutoRotateController extends TogglePreferenceController implem } RotationPolicy.registerRotationPolicyListener(mContext, mRotationPolicyListener); mDeviceStateAutoRotateSettingsManager.registerListener( - mDeviceStateRotationLockSettingsListener); + mDeviceStateAutoRotateSettingListener); mPrivacyManager.addSensorPrivacyListener(CAMERA, mPrivacyChangedListener); } @@ -152,7 +153,7 @@ public class SmartAutoRotateController extends TogglePreferenceController implem mRotationPolicyListener = null; } mDeviceStateAutoRotateSettingsManager.unregisterListener( - mDeviceStateRotationLockSettingsListener); + mDeviceStateAutoRotateSettingListener); mPrivacyManager.removeSensorPrivacyListener(CAMERA, mPrivacyChangedListener); } From 52845fdf8de410ba05b278fecd7a8afb977786ec Mon Sep 17 00:00:00 2001 From: Milton Wu Date: Mon, 10 Mar 2025 09:07:16 +0000 Subject: [PATCH 05/15] Fix biometric activities launched twice Save launched state Bug: 401461494 Test: After pressing Done in biometric confirmation page, intro page don not show again Flag: EXEMPT bug fix Change-Id: I7dfca8e2a6752a20de0429b61f8d9885fdd12e47 --- .../settings/biometrics/face/FaceEnroll.kt | 41 ++++++++++++++----- .../fingerprint/FingerprintEnroll.kt | 38 +++++++++++++---- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/src/com/android/settings/biometrics/face/FaceEnroll.kt b/src/com/android/settings/biometrics/face/FaceEnroll.kt index 2ed628d30ed..74f1613aafe 100644 --- a/src/com/android/settings/biometrics/face/FaceEnroll.kt +++ b/src/com/android/settings/biometrics/face/FaceEnroll.kt @@ -23,7 +23,6 @@ import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED import com.android.settings.biometrics.combination.CombinedBiometricStatusUtils - import com.android.settings.overlay.FeatureFactory.Companion.featureFactory class FaceEnroll: AppCompatActivity() { @@ -39,18 +38,33 @@ class FaceEnroll: AppCompatActivity() { private val enrollActivityProvider: FaceEnrollActivityClassProvider get() = featureFactory.faceFeatureProvider.enrollActivityClassProvider + private var isLaunched = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - /** - * Logs the next activity to be launched, creates an intent for that activity, - * adds flags to forward the result, includes any existing extras from the current intent, - * starts the new activity and then finishes the current one - */ - Log.d("FaceEnroll", "forward to $nextActivityClass") - val nextIntent = Intent(this, nextActivityClass) - nextIntent.putExtras(intent) - startActivityForResult(nextIntent, 0) + if (savedInstanceState != null) { + isLaunched = savedInstanceState.getBoolean(KEY_IS_LAUNCHED, isLaunched) + } + + if (!isLaunched) { + /** + * Logs the next activity to be launched, creates an intent for that activity, + * adds flags to forward the result, includes any existing extras from the current intent, + * starts the new activity and then finishes the current one + */ + Log.d("FaceEnroll", "forward to $nextActivityClass") + val nextIntent = Intent(this, nextActivityClass) + nextIntent.putExtras(intent) + startActivityForResult(nextIntent, 0) + + isLaunched = true + } + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(KEY_IS_LAUNCHED, isLaunched) + super.onSaveInstanceState(outState) } override fun onActivityResult( @@ -60,6 +74,7 @@ class FaceEnroll: AppCompatActivity() { caller: ComponentCaller ) { super.onActivityResult(requestCode, resultCode, data, caller) + isLaunched = false if (intent.getBooleanExtra( CombinedBiometricStatusUtils.EXTRA_LAUNCH_FROM_SAFETY_SOURCE_ISSUE, false) && resultCode != RESULT_FINISHED) { @@ -68,4 +83,8 @@ class FaceEnroll: AppCompatActivity() { setResult(resultCode, data) finish() } -} \ No newline at end of file + + private companion object { + const val KEY_IS_LAUNCHED = "isLaunched" + } +} diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnroll.kt b/src/com/android/settings/biometrics/fingerprint/FingerprintEnroll.kt index 795be132e64..229f6c462c8 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnroll.kt +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnroll.kt @@ -62,18 +62,33 @@ open class FingerprintEnroll: AppCompatActivity() { protected val enrollActivityProvider: FingerprintEnrollActivityClassProvider get() = featureFactory.fingerprintFeatureProvider.getEnrollActivityClassProvider(this) + private var isLaunched = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - /** - * Logs the next activity to be launched, creates an intent for that activity, - * adds flags to forward the result, includes any existing extras from the current intent, - * starts the new activity and then finishes the current one - */ - Log.d("FingerprintEnroll", "forward to $nextActivityClass") - val nextIntent = Intent(this, nextActivityClass) - nextIntent.putExtras(intent) - startActivityForResult(nextIntent, 0) + if (savedInstanceState != null) { + isLaunched = savedInstanceState.getBoolean(KEY_IS_LAUNCHED, isLaunched) + } + + if (!isLaunched) { + /** + * Logs the next activity to be launched, creates an intent for that activity, + * adds flags to forward the result, includes any existing extras from the current intent, + * starts the new activity and then finishes the current one + */ + Log.d("FingerprintEnroll", "forward to $nextActivityClass") + val nextIntent = Intent(this, nextActivityClass) + nextIntent.putExtras(intent) + startActivityForResult(nextIntent, 0) + + isLaunched = true + } + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(KEY_IS_LAUNCHED, isLaunched) + super.onSaveInstanceState(outState) } override fun onActivityResult( @@ -83,6 +98,7 @@ open class FingerprintEnroll: AppCompatActivity() { caller: ComponentCaller ) { super.onActivityResult(requestCode, resultCode, data, caller) + isLaunched = false if (intent.getBooleanExtra( CombinedBiometricStatusUtils.EXTRA_LAUNCH_FROM_SAFETY_SOURCE_ISSUE, false) && resultCode != BiometricEnrollBase.RESULT_FINISHED @@ -92,4 +108,8 @@ open class FingerprintEnroll: AppCompatActivity() { setResult(resultCode, data) finish() } + + private companion object { + const val KEY_IS_LAUNCHED = "isLaunched" + } } \ No newline at end of file From f3f3db1d91bcfede98d09e4baafb9d8964f1ae27 Mon Sep 17 00:00:00 2001 From: Yiyi Shen Date: Mon, 10 Mar 2025 19:19:23 +0800 Subject: [PATCH 06/15] [Audiosharing] Allow showing dialog when parent atLeast CREATED Test: atest Flag: com.android.settingslib.flags.promote_audio_sharing_for_second_auto_connected_lea_device Bug: 395786392 Change-Id: I6b4b363dab9531de0874a157254a1e9848ff0448 --- .../audiosharing/AudioSharingDisconnectDialogFragment.java | 2 +- .../audiosharing/AudioSharingJoinDialogFragment.java | 2 +- .../audiosharing/AudioSharingStopDialogFragment.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java index 90ff344bbf3..c3459f5a652 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java @@ -103,7 +103,7 @@ public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFrag return false; } Lifecycle.State currentState = host.getLifecycle().getCurrentState(); - if (!currentState.isAtLeast(Lifecycle.State.STARTED)) { + if (!currentState.isAtLeast(Lifecycle.State.CREATED)) { Log.d(TAG, "Fail to show dialog with state: " + currentState); return false; } diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java index 02c4a4c9a84..f244f5fcdee 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java @@ -102,7 +102,7 @@ public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment { return false; } Lifecycle.State currentState = host.getLifecycle().getCurrentState(); - if (!currentState.isAtLeast(Lifecycle.State.STARTED)) { + if (!currentState.isAtLeast(Lifecycle.State.CREATED)) { Log.d(TAG, "Fail to show dialog with state: " + currentState); return false; } diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java index 65a9ac35e0a..8a11ac534e7 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java @@ -100,7 +100,7 @@ public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment { return false; } Lifecycle.State currentState = host.getLifecycle().getCurrentState(); - if (!currentState.isAtLeast(Lifecycle.State.STARTED)) { + if (!currentState.isAtLeast(Lifecycle.State.CREATED)) { Log.d(TAG, "Fail to show dialog with state: " + currentState); return false; } From 7b199e4d5184d6eeec565bb57737dcf3790d5e23 Mon Sep 17 00:00:00 2001 From: Kasia Krejszeff Date: Thu, 9 Jan 2025 13:07:27 +0000 Subject: [PATCH 07/15] Tap to pause/play the lottie animation in PrivateSpace education screen This is to conform to a11y motion stopping requirements. Change-Id: I73637af20688ee7f5fb5226f871dc57ed9b54e0d Test: manually Bug: 379258725 Flag: EXEMPT bugfix --- .../settings/privatespace/PrivateSpaceEducation.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/com/android/settings/privatespace/PrivateSpaceEducation.java b/src/com/android/settings/privatespace/PrivateSpaceEducation.java index dfaf8b9cfa7..093d7a5eed2 100644 --- a/src/com/android/settings/privatespace/PrivateSpaceEducation.java +++ b/src/com/android/settings/privatespace/PrivateSpaceEducation.java @@ -44,6 +44,8 @@ import java.util.regex.Pattern; public class PrivateSpaceEducation extends InstrumentedFragment { private static final String TAG = "PrivateSpaceEducation"; + private boolean mIsAnimationPlaying = true; + @Override public View onCreateView( LayoutInflater inflater, @@ -73,6 +75,7 @@ public class PrivateSpaceEducation extends InstrumentedFragment { .build()); LottieAnimationView lottieAnimationView = rootView.findViewById(R.id.lottie_animation); LottieColorUtils.applyDynamicColors(getContext(), lottieAnimationView); + lottieAnimationView.setOnClickListener(v -> handleAnimationClick(lottieAnimationView)); TextView infoTextView = rootView.findViewById(R.id.learn_more); Pattern pattern = Pattern.compile(infoTextView.getText().toString()); @@ -110,4 +113,13 @@ public class PrivateSpaceEducation extends InstrumentedFragment { } }; } + + private void handleAnimationClick(LottieAnimationView lottieAnimationView) { + if (mIsAnimationPlaying) { + lottieAnimationView.pauseAnimation(); + } else { + lottieAnimationView.playAnimation(); + } + mIsAnimationPlaying = !mIsAnimationPlaying; + } } From 84f84ca8c039b42b05d60597be32700c5634ff56 Mon Sep 17 00:00:00 2001 From: Jacky Wang Date: Tue, 11 Mar 2025 00:47:02 +0800 Subject: [PATCH 08/15] [Catalyst] Use KeyValueStoreDelegate NO_IFTTT=Catalyst only Bug: 388167106 Flag: com.android.settings.flags.catalyst Test: manual Change-Id: I3d04e1bf2620fe7f910fe63f22c17da290e9172d --- .../VibrationMainSwitchPreference.kt | 13 +++----- .../display/AdaptiveSleepPreference.kt | 13 +++----- .../BatteryPercentageSwitchPreference.kt | 17 +++------- .../PeakRefreshRateSwitchPreference.kt | 17 ++++------ .../AdaptiveConnectivityTogglePreference.kt | 21 ++++++------ .../network/AirplaneModePreference.kt | 13 +++----- ...MediaControlsLockscreenSwitchPreference.kt | 33 ++++++++----------- .../settings/sound/MediaControlsScreen.kt | 24 +++++--------- 8 files changed, 58 insertions(+), 93 deletions(-) diff --git a/src/com/android/settings/accessibility/VibrationMainSwitchPreference.kt b/src/com/android/settings/accessibility/VibrationMainSwitchPreference.kt index c488e27b700..4008ceea43b 100644 --- a/src/com/android/settings/accessibility/VibrationMainSwitchPreference.kt +++ b/src/com/android/settings/accessibility/VibrationMainSwitchPreference.kt @@ -25,7 +25,7 @@ import com.android.settings.R import com.android.settings.contract.KEY_VIBRATION_HAPTICS import com.android.settings.metrics.PreferenceActionMetricsProvider import com.android.settingslib.datastore.KeyValueStore -import com.android.settingslib.datastore.KeyedObservableDelegate +import com.android.settingslib.datastore.KeyValueStoreDelegate import com.android.settingslib.datastore.SettingsSystemStore import com.android.settingslib.metadata.BooleanValuePreference import com.android.settingslib.metadata.PreferenceMetadata @@ -96,18 +96,13 @@ class VibrationMainSwitchPreference : class VibrationMainSwitchStore( context: Context, private val settingsStore: KeyValueStore = SettingsSystemStore.get(context), -) : KeyedObservableDelegate(settingsStore), KeyValueStore { +) : KeyValueStoreDelegate { - override fun contains(key: String) = settingsStore.contains(key) + override val keyValueStoreDelegate + get() = settingsStore override fun getDefaultValue(key: String, valueType: Class) = DEFAULT_VALUE as T - override fun getValue(key: String, valueType: Class) = - settingsStore.getValue(key, valueType) ?: getDefaultValue(key, valueType) - - override fun setValue(key: String, valueType: Class, value: T?) = - settingsStore.setValue(key, valueType, value) - companion object { private const val DEFAULT_VALUE = true } diff --git a/src/com/android/settings/display/AdaptiveSleepPreference.kt b/src/com/android/settings/display/AdaptiveSleepPreference.kt index a38925c5458..32f805faacf 100644 --- a/src/com/android/settings/display/AdaptiveSleepPreference.kt +++ b/src/com/android/settings/display/AdaptiveSleepPreference.kt @@ -33,9 +33,8 @@ import com.android.settings.metrics.PreferenceActionMetricsProvider import com.android.settings.restriction.PreferenceRestrictionMixin import com.android.settingslib.RestrictedSwitchPreference import com.android.settingslib.datastore.KeyValueStore -import com.android.settingslib.datastore.KeyedObservableDelegate +import com.android.settingslib.datastore.KeyValueStoreDelegate import com.android.settingslib.datastore.SettingsSecureStore -import com.android.settingslib.datastore.SettingsStore import com.android.settingslib.metadata.BooleanValuePreference import com.android.settingslib.metadata.PreferenceAvailabilityProvider import com.android.settingslib.metadata.PreferenceLifecycleContext @@ -106,16 +105,14 @@ class AdaptiveSleepPreference : @Suppress("UNCHECKED_CAST") private class Storage( private val context: Context, - private val settingsStore: SettingsStore = SettingsSecureStore.get(context), - ) : KeyedObservableDelegate(settingsStore), KeyValueStore { + private val settingsStore: KeyValueStore = SettingsSecureStore.get(context), + ) : KeyValueStoreDelegate { - override fun contains(key: String) = settingsStore.contains(key) + override val keyValueStoreDelegate + get() = settingsStore override fun getValue(key: String, valueType: Class) = (context.canBeEnabled() && settingsStore.getBoolean(key) == true) as T - - override fun setValue(key: String, valueType: Class, value: T?) = - settingsStore.setBoolean(key, value as Boolean?) } override fun onStart(context: PreferenceLifecycleContext) { diff --git a/src/com/android/settings/display/BatteryPercentageSwitchPreference.kt b/src/com/android/settings/display/BatteryPercentageSwitchPreference.kt index 2ce643609dc..26d1c946d02 100644 --- a/src/com/android/settings/display/BatteryPercentageSwitchPreference.kt +++ b/src/com/android/settings/display/BatteryPercentageSwitchPreference.kt @@ -23,8 +23,7 @@ import com.android.settings.Utils import com.android.settings.contract.KEY_BATTERY_PERCENTAGE import com.android.settings.metrics.PreferenceActionMetricsProvider import com.android.settingslib.datastore.KeyValueStore -import com.android.settingslib.datastore.KeyedObservableDelegate -import com.android.settingslib.datastore.SettingsStore +import com.android.settingslib.datastore.KeyValueStoreDelegate import com.android.settingslib.datastore.SettingsSystemStore import com.android.settingslib.metadata.PreferenceAvailabilityProvider import com.android.settingslib.metadata.ReadWritePermit @@ -71,17 +70,11 @@ class BatteryPercentageSwitchPreference : @Suppress("UNCHECKED_CAST") private class BatteryPercentageStorage( private val context: Context, - private val settingsStore: SettingsStore, - ) : KeyedObservableDelegate(settingsStore), KeyValueStore { + private val settingsStore: KeyValueStore, + ) : KeyValueStoreDelegate { - override fun contains(key: String) = settingsStore.contains(KEY) - - override fun getValue(key: String, valueType: Class) = - (settingsStore.getBoolean(key) ?: getDefaultValue(key, valueType)) as T - - override fun setValue(key: String, valueType: Class, value: T?) { - settingsStore.setBoolean(key, value as Boolean) - } + override val keyValueStoreDelegate + get() = settingsStore override fun getDefaultValue(key: String, valueType: Class) = context.resources.getBoolean( diff --git a/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt b/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt index ee538db2d36..8f1f9b5a477 100644 --- a/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt +++ b/src/com/android/settings/display/PeakRefreshRateSwitchPreference.kt @@ -29,8 +29,7 @@ import com.android.settings.contract.KEY_SMOOTH_DISPLAY import com.android.settings.metrics.PreferenceActionMetricsProvider 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.KeyValueStoreDelegate import com.android.settingslib.datastore.SettingsSystemStore import com.android.settingslib.metadata.PreferenceAvailabilityProvider import com.android.settingslib.metadata.PreferenceLifecycleContext @@ -112,18 +111,16 @@ class PeakRefreshRateSwitchPreference : @Suppress("UNCHECKED_CAST") private class PeakRefreshRateStore( private val context: Context, - private val settingsStore: SettingsStore, - ) : KeyedObservableDelegate(settingsStore), KeyValueStore { + private val settingsStore: KeyValueStore, + ) : KeyValueStoreDelegate { - override fun contains(key: String) = settingsStore.contains(key) + override val keyValueStoreDelegate + get() = settingsStore - override fun getDefaultValue(key: String, valueType: Class): T? { - if (key != KEY) return super.getDefaultValue(key, valueType) - return context.defaultPeakRefreshRate.refreshRateAsBoolean(context) as T - } + override fun getDefaultValue(key: String, valueType: Class) = + context.defaultPeakRefreshRate.refreshRateAsBoolean(context) as T override fun getValue(key: String, valueType: Class): T? { - if (key != KEY) return null val refreshRate = settingsStore.getFloat(KEY) ?: context.defaultPeakRefreshRate return refreshRate.refreshRateAsBoolean(context) as T } diff --git a/src/com/android/settings/network/AdaptiveConnectivityTogglePreference.kt b/src/com/android/settings/network/AdaptiveConnectivityTogglePreference.kt index 80f1d00146a..d4959677e87 100644 --- a/src/com/android/settings/network/AdaptiveConnectivityTogglePreference.kt +++ b/src/com/android/settings/network/AdaptiveConnectivityTogglePreference.kt @@ -24,9 +24,8 @@ import com.android.settings.R import com.android.settings.contract.KEY_ADAPTIVE_CONNECTIVITY import com.android.settings.metrics.PreferenceActionMetricsProvider import com.android.settingslib.datastore.KeyValueStore -import com.android.settingslib.datastore.KeyedObservableDelegate +import com.android.settingslib.datastore.KeyValueStoreDelegate import com.android.settingslib.datastore.SettingsSecureStore -import com.android.settingslib.datastore.SettingsStore import com.android.settingslib.metadata.MainSwitchPreference import com.android.settingslib.metadata.ReadWritePermit import com.android.settingslib.metadata.SensitivityLevel @@ -42,7 +41,7 @@ class AdaptiveConnectivityTogglePreference : override fun tags(context: Context) = arrayOf(KEY_ADAPTIVE_CONNECTIVITY) override fun storage(context: Context): KeyValueStore = - AdaptiveConnectivityToggleStorage(context, SettingsSecureStore.get(context)) + AdaptiveConnectivityToggleStorage(context) override fun getReadPermissions(context: Context) = SettingsSecureStore.getReadPermissions() @@ -64,20 +63,20 @@ class AdaptiveConnectivityTogglePreference : @Suppress("UNCHECKED_CAST") private class AdaptiveConnectivityToggleStorage( private val context: Context, - private val settingsStore: SettingsStore, - ) : KeyedObservableDelegate(settingsStore), KeyValueStore { + private val settingsStore: KeyValueStore = SettingsSecureStore.get(context), + ) : KeyValueStoreDelegate { - override fun contains(key: String) = settingsStore.contains(KEY) + override val keyValueStoreDelegate + get() = settingsStore override fun getDefaultValue(key: String, valueType: Class) = DEFAULT_VALUE as T - override fun getValue(key: String, valueType: Class) = - (settingsStore.getBoolean(key) ?: DEFAULT_VALUE) as T - override fun setValue(key: String, valueType: Class, value: T?) { - settingsStore.setBoolean(key, value as Boolean) - context.getSystemService(WifiManager::class.java)?.setWifiScoringEnabled(value) + settingsStore.setValue(key, valueType, value) + context + .getSystemService(WifiManager::class.java) + ?.setWifiScoringEnabled((value as Boolean?) ?: DEFAULT_VALUE) } } diff --git a/src/com/android/settings/network/AirplaneModePreference.kt b/src/com/android/settings/network/AirplaneModePreference.kt index de2eb1a43f1..5a0dd5a613d 100644 --- a/src/com/android/settings/network/AirplaneModePreference.kt +++ b/src/com/android/settings/network/AirplaneModePreference.kt @@ -36,9 +36,8 @@ import com.android.settings.network.SatelliteRepository.Companion.isSatelliteOn import com.android.settings.restriction.PreferenceRestrictionMixin import com.android.settingslib.RestrictedSwitchPreference import com.android.settingslib.datastore.KeyValueStore -import com.android.settingslib.datastore.KeyedObservableDelegate +import com.android.settingslib.datastore.KeyValueStoreDelegate import com.android.settingslib.datastore.SettingsGlobalStore -import com.android.settingslib.datastore.SettingsStore import com.android.settingslib.metadata.PreferenceAvailabilityProvider import com.android.settingslib.metadata.PreferenceLifecycleContext import com.android.settingslib.metadata.PreferenceLifecycleProvider @@ -92,17 +91,15 @@ class AirplaneModePreference : @Suppress("UNCHECKED_CAST") private class AirplaneModeStorage( private val context: Context, - private val settingsStore: SettingsStore = SettingsGlobalStore.get(context), - ) : KeyedObservableDelegate(settingsStore), KeyValueStore { + private val settingsStore: KeyValueStore = SettingsGlobalStore.get(context), + ) : KeyValueStoreDelegate { - override fun contains(key: String) = settingsStore.contains(KEY) + override val keyValueStoreDelegate + get() = settingsStore override fun getDefaultValue(key: String, valueType: Class) = DEFAULT_VALUE as T - override fun getValue(key: String, valueType: Class): T = - (settingsStore.getBoolean(key) ?: DEFAULT_VALUE) as T - override fun setValue(key: String, valueType: Class, value: T?) { settingsStore.setValue(key, valueType, value) diff --git a/src/com/android/settings/sound/MediaControlsLockscreenSwitchPreference.kt b/src/com/android/settings/sound/MediaControlsLockscreenSwitchPreference.kt index 59c1b8f97c1..2d70b83ef31 100644 --- a/src/com/android/settings/sound/MediaControlsLockscreenSwitchPreference.kt +++ b/src/com/android/settings/sound/MediaControlsLockscreenSwitchPreference.kt @@ -18,22 +18,21 @@ package com.android.settings.sound import android.content.Context import android.provider.Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN - +import com.android.settings.R import com.android.settingslib.datastore.KeyValueStore -import com.android.settingslib.datastore.KeyedObservableDelegate +import com.android.settingslib.datastore.KeyValueStoreDelegate import com.android.settingslib.datastore.SettingsSecureStore -import com.android.settingslib.datastore.SettingsStore import com.android.settingslib.metadata.ReadWritePermit import com.android.settingslib.metadata.SensitivityLevel import com.android.settingslib.metadata.SwitchPreference -import com.android.settings.R // LINT.IfChange -class MediaControlsLockscreenSwitchPreference : SwitchPreference( - KEY, - R.string.media_controls_lockscreen_title, - R.string.media_controls_lockscreen_description, -) { +class MediaControlsLockscreenSwitchPreference : + SwitchPreference( + KEY, + R.string.media_controls_lockscreen_title, + R.string.media_controls_lockscreen_description, + ) { override val sensitivityLevel get() = SensitivityLevel.NO_SENSITIVITY @@ -52,21 +51,17 @@ class MediaControlsLockscreenSwitchPreference : SwitchPreference( MediaControlsLockscreenStore(SettingsSecureStore.get(context)) @Suppress("UNCHECKED_CAST") - private class MediaControlsLockscreenStore(private val settingsStore: SettingsStore) : - KeyedObservableDelegate(settingsStore), KeyValueStore { - override fun contains(key: String) = settingsStore.contains(key) + private class MediaControlsLockscreenStore(private val settingsStore: KeyValueStore) : + KeyValueStoreDelegate { + + override val keyValueStoreDelegate + get() = settingsStore override fun getDefaultValue(key: String, valueType: Class) = true as T - - override fun getValue(key: String, valueType: Class) = - settingsStore.getValue(key, valueType) ?: getDefaultValue(key, valueType) - - override fun setValue(key: String, valueType: Class, value: T?) = - settingsStore.setValue(key, valueType, value) } companion object { const val KEY = MEDIA_CONTROLS_LOCK_SCREEN } } -// LINT.ThenChange(MediaControlsLockScreenPreferenceController.java) \ No newline at end of file +// LINT.ThenChange(MediaControlsLockScreenPreferenceController.java) diff --git a/src/com/android/settings/sound/MediaControlsScreen.kt b/src/com/android/settings/sound/MediaControlsScreen.kt index d63259c641c..f9c2f64d40d 100644 --- a/src/com/android/settings/sound/MediaControlsScreen.kt +++ b/src/com/android/settings/sound/MediaControlsScreen.kt @@ -17,21 +17,18 @@ package com.android.settings.sound import android.content.Context - import com.android.settings.R import com.android.settings.flags.Flags import com.android.settingslib.datastore.AbstractKeyedDataObservable import com.android.settingslib.datastore.HandlerExecutor -import com.android.settingslib.datastore.KeyedObserver import com.android.settingslib.datastore.KeyValueStore -import com.android.settingslib.datastore.KeyedObservableDelegate +import com.android.settingslib.datastore.KeyValueStoreDelegate +import com.android.settingslib.datastore.KeyedObserver import com.android.settingslib.datastore.SettingsSecureStore -import com.android.settingslib.datastore.SettingsStore import com.android.settingslib.metadata.PreferenceChangeReason -import com.android.settingslib.metadata.ProvidePreferenceScreen import com.android.settingslib.metadata.PreferenceSummaryProvider +import com.android.settingslib.metadata.ProvidePreferenceScreen import com.android.settingslib.metadata.preferenceHierarchy - import com.android.settingslib.preference.PreferenceScreenCreator // LINT.IfChange @@ -83,21 +80,16 @@ class MediaControlsScreen(context: Context) : } @Suppress("UNCHECKED_CAST") - class MediaControlsStore(private val settingsStore: SettingsStore) : - KeyedObservableDelegate(settingsStore), KeyValueStore { - override fun contains(key: String) = settingsStore.contains(key) + class MediaControlsStore(private val settingsStore: KeyValueStore) : KeyValueStoreDelegate { + + override val keyValueStoreDelegate + get() = settingsStore override fun getDefaultValue(key: String, valueType: Class) = true as T - - override fun getValue(key: String, valueType: Class) = - settingsStore.getValue(key, valueType) ?: getDefaultValue(key, valueType) - - override fun setValue(key: String, valueType: Class, value: T?) = - settingsStore.setValue(key, valueType, value) } companion object { const val KEY = "media_controls" } } -// LINT.ThenChange(MediaControlsSettings.java) \ No newline at end of file +// LINT.ThenChange(MediaControlsSettings.java) From a389ff59abddaae036ddf12a68467a7b839c7d44 Mon Sep 17 00:00:00 2001 From: Menghan Li Date: Fri, 7 Mar 2025 03:54:35 +0000 Subject: [PATCH 09/15] refactor(A11yFeedback): Simply feedback logic into menu controller Bug: 393980229 Test: atest FeedbackMenuControllerTest Flag: com.android.server.accessibility.enable_low_vision_generic_feedback Change-Id: I03af00957c2bcca1d1cc81970eccad6dd69bb4ac --- .../accessibility/AccessibilitySettings.java | 39 +---- .../ToggleFeaturePreferenceFragment.java | 62 ++----- .../actionbar/FeedbackMenuController.java | 94 +++++++++++ .../AccessibilitySettingsTest.java | 62 ------- ...eColorInversionPreferenceFragmentTest.java | 4 + .../ToggleFeaturePreferenceFragmentTest.java | 75 +-------- .../actionbar/FeedbackMenuControllerTest.java | 158 ++++++++++++++++++ 7 files changed, 276 insertions(+), 218 deletions(-) create mode 100644 src/com/android/settings/accessibility/actionbar/FeedbackMenuController.java create mode 100644 tests/robotests/src/com/android/settings/accessibility/actionbar/FeedbackMenuControllerTest.java diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java index 2c8247f959c..b4b1ef69a08 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettings.java +++ b/src/com/android/settings/accessibility/AccessibilitySettings.java @@ -30,9 +30,6 @@ import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.accessibility.AccessibilityManager; import androidx.annotation.NonNull; @@ -45,6 +42,7 @@ import com.android.internal.accessibility.util.AccessibilityUtils; import com.android.internal.content.PackageMonitor; import com.android.settings.R; import com.android.settings.accessibility.AccessibilityUtil.AccessibilityServiceFragmentType; +import com.android.settings.accessibility.actionbar.FeedbackMenuController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; @@ -105,8 +103,6 @@ public class AccessibilitySettings extends DashboardFragment implements // presentation. private static final long DELAY_UPDATE_SERVICES_MILLIS = 1000; - static final int MENU_ID_SEND_FEEDBACK = 0; - private final Handler mHandler = new Handler(); private final Runnable mUpdateRunnable = new Runnable() { @@ -151,8 +147,6 @@ public class AccessibilitySettings extends DashboardFragment implements private AccessibilitySettingsContentObserver mSettingsContentObserver; - private FeedbackManager mFeedbackManager; - private final Map mCategoryToPrefCategoryMap = new ArrayMap<>(); private final List mServicePreferences = new ArrayList<>(); @@ -216,6 +210,7 @@ public class AccessibilitySettings extends DashboardFragment implements mNeedPreferencesUpdate = false; registerContentMonitors(); registerInputDeviceListener(); + FeedbackMenuController.init(this, SettingsEnums.ACCESSIBILITY); } @Override @@ -252,24 +247,6 @@ public class AccessibilitySettings extends DashboardFragment implements super.onDestroy(); } - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - if (getFeedbackManager().isAvailable()) { - menu.add(Menu.NONE, MENU_ID_SEND_FEEDBACK, Menu.NONE, - R.string.accessibility_send_feedback_title); - } - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == MENU_ID_SEND_FEEDBACK) { - getFeedbackManager().sendFeedback(); - return true; - } - return super.onOptionsItemSelected(item); - } - @Override protected int getPreferenceScreenResId() { return R.xml.accessibility_settings; @@ -280,18 +257,6 @@ public class AccessibilitySettings extends DashboardFragment implements return TAG; } - @VisibleForTesting - void setFeedbackManager(FeedbackManager feedbackManager) { - this.mFeedbackManager = feedbackManager; - } - - private FeedbackManager getFeedbackManager() { - if (mFeedbackManager == null) { - mFeedbackManager = new FeedbackManager(getActivity(), SettingsEnums.ACCESSIBILITY); - } - return mFeedbackManager; - } - /** * Returns the summary for the current state of this accessibilityService. * diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java index d8c39856368..367f55710ba 100644 --- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java @@ -40,9 +40,6 @@ import android.service.quicksettings.TileService; import android.text.Html; import android.text.TextUtils; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; @@ -63,6 +60,7 @@ import com.android.internal.accessibility.common.ShortcutConstants; import com.android.internal.accessibility.util.ShortcutUtils; import com.android.settings.R; import com.android.settings.SettingsActivity; +import com.android.settings.accessibility.actionbar.FeedbackMenuController; import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.flags.Flags; @@ -94,7 +92,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment // , a11y settings will get the resources successfully. private static final String IMG_PREFIX = "R.drawable."; private static final String DRAWABLE_FOLDER = "drawable"; - static final int MENU_ID_SEND_FEEDBACK = 0; protected TopIntroPreference mTopIntroPreference; protected SettingsMainSwitchPreference mToggleServiceSwitchPreference; @@ -108,7 +105,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment protected Intent mSettingsIntent; // The mComponentName maybe null, such as Magnify protected ComponentName mComponentName; - @Nullable private FeedbackManager mFeedbackManager; protected CharSequence mFeatureName; protected Uri mImageUri; protected CharSequence mHtmlDescription; @@ -142,6 +138,8 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment mSettingsContentObserver = new AccessibilitySettingsContentObserver(new Handler()); registerKeysToObserverCallback(mSettingsContentObserver); + + FeedbackMenuController.init(this, getFeedbackCategory()); } protected void registerKeysToObserverCallback( @@ -247,24 +245,6 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment removeActionBarToggleSwitch(); } - @Override - public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { - if (getFeedbackManager().isAvailable()) { - menu.add(Menu.NONE, MENU_ID_SEND_FEEDBACK, Menu.NONE, - R.string.accessibility_send_feedback_title); - } - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == MENU_ID_SEND_FEEDBACK) { - getFeedbackManager().sendFeedback(); - return true; - } - return super.onOptionsItemSelected(item); - } - @Override public int getDialogMetricsCategory(int dialogId) { switch (dialogId) { @@ -280,6 +260,18 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment return SettingsEnums.ACCESSIBILITY_SERVICE; } + /** + * Returns the category of the feedback page. + * + *

By default, this method returns {@link SettingsEnums#PAGE_UNKNOWN}. This indicates that + * the feedback category is unknown, and the absence of a feedback menu. + * + * @return The feedback category, which is {@link SettingsEnums#PAGE_UNKNOWN} by default. + */ + protected int getFeedbackCategory() { + return SettingsEnums.PAGE_UNKNOWN; + } + @Override public int getHelpResource() { return 0; @@ -785,28 +777,4 @@ public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment super.onCreateRecyclerView(inflater, parent, savedInstanceState); return AccessibilityFragmentUtils.addCollectionInfoToAccessibilityDelegate(recyclerView); } - - @VisibleForTesting - void setFeedbackManager(FeedbackManager feedbackManager) { - this.mFeedbackManager = feedbackManager; - } - - private FeedbackManager getFeedbackManager() { - if (mFeedbackManager == null) { - mFeedbackManager = new FeedbackManager(getActivity(), getFeedbackCategory()); - } - return mFeedbackManager; - } - - /** - * Returns the category of the feedback page. - * - *

By default, this method returns {@link SettingsEnums#PAGE_UNKNOWN}. This indicates that - * the feedback category is unknown, and the absence of a feedback menu. - * - * @return The feedback category, which is {@link SettingsEnums#PAGE_UNKNOWN} by default. - */ - protected int getFeedbackCategory() { - return SettingsEnums.PAGE_UNKNOWN; - } } diff --git a/src/com/android/settings/accessibility/actionbar/FeedbackMenuController.java b/src/com/android/settings/accessibility/actionbar/FeedbackMenuController.java new file mode 100644 index 00000000000..ee08276b6e5 --- /dev/null +++ b/src/com/android/settings/accessibility/actionbar/FeedbackMenuController.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2025 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.accessibility.actionbar; + +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import androidx.annotation.NonNull; + +import com.android.settings.R; +import com.android.settings.accessibility.FeedbackManager; +import com.android.settings.core.InstrumentedPreferenceFragment; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnCreateOptionsMenu; +import com.android.settingslib.core.lifecycle.events.OnOptionsItemSelected; + +/** + * A controller that adds feedback menu to any Settings page. + */ +public class FeedbackMenuController implements LifecycleObserver, OnCreateOptionsMenu, + OnOptionsItemSelected { + + /** + * The menu item ID for the feedback menu option. + */ + public static final int MENU_FEEDBACK = Menu.FIRST + 10; + + /** + * The menu item ID for the feedback menu option. + */ + private final FeedbackManager mFeedbackManager; + + /** + * Initializes the FeedbackMenuController for an InstrumentedPreferenceFragment with a provided + * pade ID. + * + * @param host The InstrumentedPreferenceFragment to which the menu controller will be added. + * @param pageId The page ID used for feedback tracking. + */ + public static void init(@NonNull InstrumentedPreferenceFragment host, int pageId) { + host.getSettingsLifecycle().addObserver( + new FeedbackMenuController( + new FeedbackManager(host.getActivity(), pageId))); + } + + /** + * Initializes the FeedbackMenuController for an InstrumentedPreferenceFragment with a provided + * FeedbackManager. + * + * @param host The InstrumentedPreferenceFragment to which the menu controller will be added. + * @param feedbackManager The FeedbackManager to use for handling feedback actions. + */ + public static void init(@NonNull InstrumentedPreferenceFragment host, + @NonNull FeedbackManager feedbackManager) { + host.getSettingsLifecycle().addObserver( + new FeedbackMenuController(feedbackManager)); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + if (!mFeedbackManager.isAvailable()) { + return; + } + menu.add(Menu.NONE, MENU_FEEDBACK, Menu.NONE, R.string.accessibility_send_feedback_title); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem menuItem) { + if (menuItem.getItemId() == MENU_FEEDBACK) { + mFeedbackManager.sendFeedback(); + return true; + } + return false; + } + + private FeedbackMenuController(@NonNull FeedbackManager feedbackManager) { + mFeedbackManager = feedbackManager; + } +} diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java index 6710da90788..a077e47a39e 100644 --- a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySettingsTest.java @@ -21,10 +21,7 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; @@ -46,8 +43,6 @@ import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; -import android.view.Menu; -import android.view.MenuItem; import android.view.accessibility.AccessibilityManager; import androidx.fragment.app.Fragment; @@ -112,7 +107,6 @@ public class AccessibilitySettingsTest { private static final String EMPTY_STRING = ""; private static final String DEFAULT_SUMMARY = "default summary"; private static final String DEFAULT_DESCRIPTION = "default description"; - private static final String DEFAULT_CATEGORY = "default category"; private static final String DEFAULT_LABEL = "default label"; private static final Boolean SERVICE_ENABLED = true; private static final Boolean SERVICE_DISABLED = false; @@ -128,10 +122,6 @@ public class AccessibilitySettingsTest { private ShadowAccessibilityManager mShadowAccessibilityManager; @Mock private LocalBluetoothManager mLocalBluetoothManager; - @Mock - private Menu mMenu; - @Mock - private MenuItem mMenuItem; private ActivityController mActivityController; @@ -451,58 +441,6 @@ public class AccessibilitySettingsTest { } - @Test - @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) - public void onCreateOptionsMenu_enableLowVisionGenericFeedback_shouldAddSendFeedbackMenu() { - setupFragment(); - mFragment.setFeedbackManager( - new FeedbackManager(mFragment.getActivity(), PACKAGE_NAME, DEFAULT_CATEGORY)); - - mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null); - - verify(mMenu).add(anyInt(), anyInt(), anyInt(), anyInt()); - } - - @Test - @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) - public void onCreateOptionsMenu_disableLowVisionGenericFeedback_shouldNotAddSendFeedbackMenu() { - setupFragment(); - mFragment.setFeedbackManager( - new FeedbackManager(mFragment.getActivity(), PACKAGE_NAME, DEFAULT_CATEGORY)); - - mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null); - - verify(mMenu, never()).add(anyInt(), anyInt(), anyInt(), anyInt()); - } - - @Test - @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) - public void onOptionsItemSelected_enableLowVisionGenericFeedback_shouldStartSendFeedback() { - setupFragment(); - mFragment.setFeedbackManager( - new FeedbackManager(mFragment.getActivity(), PACKAGE_NAME, DEFAULT_CATEGORY)); - when(mMenuItem.getItemId()).thenReturn(AccessibilitySettings.MENU_ID_SEND_FEEDBACK); - - mFragment.onOptionsItemSelected(mMenuItem); - - Intent startedIntent = shadowOf(mFragment.getActivity()).getNextStartedActivity(); - assertThat(startedIntent).isNotNull(); - } - - @Test - @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) - public void onOptionsItemSelected_disableLowVisionGenericFeedback_shouldNotStartSendFeedback() { - setupFragment(); - mFragment.setFeedbackManager( - new FeedbackManager(mFragment.getActivity(), PACKAGE_NAME, DEFAULT_CATEGORY)); - when(mMenuItem.getItemId()).thenReturn(AccessibilitySettings.MENU_ID_SEND_FEEDBACK); - - mFragment.onOptionsItemSelected(mMenuItem); - - Intent startedIntent = shadowOf(mFragment.getActivity()).getNextStartedActivity(); - assertThat(startedIntent).isNull(); - } - @Test public void testAccessibilityMenuInSystem_IncludedInInteractionControl() { mShadowAccessibilityManager.setInstalledAccessibilityServiceList( diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleColorInversionPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleColorInversionPreferenceFragmentTest.java index ff73e7feece..9d304ba186f 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleColorInversionPreferenceFragmentTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleColorInversionPreferenceFragmentTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.when; import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; +import android.content.res.Resources; import android.os.Bundle; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; @@ -82,6 +83,8 @@ public class ToggleColorInversionPreferenceFragmentTest { private PreferenceManager mPreferenceManager; @Mock private FragmentActivity mActivity; + @Mock + private Resources mResources; @Before public void setUpTestFragment() { @@ -93,6 +96,7 @@ public class ToggleColorInversionPreferenceFragmentTest { when(mFragment.getContext()).thenReturn(mContext); when(mFragment.getActivity()).thenReturn(mActivity); when(mActivity.getContentResolver()).thenReturn(mContext.getContentResolver()); + when(mActivity.getResources()).thenReturn(mResources); mScreen = spy(new PreferenceScreen(mContext, /* attrs= */ null)); when(mScreen.findPreference(mFragment.getUseServicePreferenceKey())) diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java index f72b591353a..cdd309f4c09 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java @@ -23,8 +23,6 @@ import static com.android.internal.accessibility.common.ShortcutConstants.UserSh import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -38,16 +36,14 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.icu.text.CaseMap; import android.net.Uri; import android.os.Bundle; -import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityManager; @@ -107,19 +103,10 @@ public class ToggleFeaturePreferenceFragmentTest { PLACEHOLDER_PACKAGE_NAME + "tile.placeholder"; private static final ComponentName PLACEHOLDER_TILE_COMPONENT_NAME = new ComponentName( PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_TILE_CLASS_NAME); - private static final String PLACEHOLDER_TILE_TOOLTIP_CONTENT = - PLACEHOLDER_PACKAGE_NAME + "tooltip_content"; - private static final String PLACEHOLDER_CATEGORY = "category"; - private static final String PLACEHOLDER_DIALOG_TITLE = "title"; private static final String DEFAULT_SUMMARY = "default summary"; private static final String DEFAULT_DESCRIPTION = "default description"; private static final String DEFAULT_TOP_INTRO = "default top intro"; - private static final String SOFTWARE_SHORTCUT_KEY = - Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS; - private static final String HARDWARE_SHORTCUT_KEY = - Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; - private TestToggleFeaturePreferenceFragment mFragment; @Spy private final Context mContext = ApplicationProvider.getApplicationContext(); @@ -135,9 +122,7 @@ public class ToggleFeaturePreferenceFragmentTest { @Mock private PackageManager mPackageManager; @Mock - private Menu mMenu; - @Mock - private MenuItem mMenuItem; + private Resources mResources; @Before public void setUpTestFragment() { @@ -150,6 +135,7 @@ public class ToggleFeaturePreferenceFragmentTest { when(mFragment.getContext()).thenReturn(mContext); when(mFragment.getActivity()).thenReturn(mActivity); when(mActivity.getContentResolver()).thenReturn(mContentResolver); + when(mActivity.getResources()).thenReturn(mResources); when(mContext.getPackageManager()).thenReturn(mPackageManager); final PreferenceScreen screen = spy(new PreferenceScreen(mContext, null)); when(screen.getPreferenceManager()).thenReturn(mPreferenceManager); @@ -186,61 +172,6 @@ public class ToggleFeaturePreferenceFragmentTest { any(AccessibilitySettingsContentObserver.class)); } - @Test - @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) - public void onCreateOptionsMenu_enableLowVisionGenericFeedback_shouldAddSendFeedbackMenu() { - mFragment.setFeedbackManager( - new FeedbackManager(mActivity, PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CATEGORY)); - - mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null); - - verify(mMenu).add(anyInt(), eq(ToggleFeaturePreferenceFragment.MENU_ID_SEND_FEEDBACK), - anyInt(), eq(R.string.accessibility_send_feedback_title)); - } - - @Test - @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) - public void onCreateOptionsMenu_disableLowVisionGenericFeedback_shouldNotAddSendFeedbackMenu() { - mFragment.setFeedbackManager( - new FeedbackManager(mActivity, PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CATEGORY)); - - mFragment.onCreateOptionsMenu(mMenu, /* inflater= */ null); - - verify(mMenu, never()).add(anyInt(), - eq(ToggleFeaturePreferenceFragment.MENU_ID_SEND_FEEDBACK), anyInt(), - eq(R.string.accessibility_send_feedback_title)); - } - - @Test - @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) - public void onOptionsItemSelected_enableLowVisionGenericFeedback_shouldStartSendFeedback() { - mFragment.setFeedbackManager( - new FeedbackManager(mActivity, PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CATEGORY)); - when(mMenuItem.getItemId()).thenReturn( - ToggleFeaturePreferenceFragment.MENU_ID_SEND_FEEDBACK); - - mFragment.onOptionsItemSelected(mMenuItem); - - verify(mActivity).startActivityForResult( - argThat(intent -> intent != null - && Intent.ACTION_BUG_REPORT.equals(intent.getAction())), anyInt()); - } - - @Test - @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) - public void onOptionsItemSelected_disableLowVisionGenericFeedback_shouldNotStartSendFeedback() { - mFragment.setFeedbackManager( - new FeedbackManager(mActivity, PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_CATEGORY)); - when(mMenuItem.getItemId()).thenReturn( - ToggleFeaturePreferenceFragment.MENU_ID_SEND_FEEDBACK); - - mFragment.onOptionsItemSelected(mMenuItem); - - verify(mActivity, never()).startActivityForResult( - argThat(intent -> intent != null - && Intent.ACTION_BUG_REPORT.equals(intent.getAction())), anyInt()); - } - @Test public void updateShortcutPreferenceData_assignDefaultValueToVariable() { mFragment.mComponentName = PLACEHOLDER_COMPONENT_NAME; diff --git a/tests/robotests/src/com/android/settings/accessibility/actionbar/FeedbackMenuControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/actionbar/FeedbackMenuControllerTest.java new file mode 100644 index 00000000000..7208f496755 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/actionbar/FeedbackMenuControllerTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2025 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.accessibility.actionbar; + +import static com.android.settings.accessibility.actionbar.FeedbackMenuController.MENU_FEEDBACK; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + +import android.app.settings.SettingsEnums; +import android.content.Intent; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.testing.EmptyFragmentActivity; +import androidx.test.ext.junit.rules.ActivityScenarioRule; + +import com.android.settings.accessibility.FeedbackManager; +import com.android.settings.core.InstrumentedPreferenceFragment; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for {@link FeedbackMenuController} */ +@Config(shadows = { + com.android.settings.testutils.shadow.ShadowFragment.class, +}) +@RunWith(RobolectricTestRunner.class) +public class FeedbackMenuControllerTest { + private static final String PACKAGE_NAME = "com.android.test"; + private static final String DEFAULT_CATEGORY = "default category"; + + @Rule + public final MockitoRule mMocks = MockitoJUnit.rule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Rule + public ActivityScenarioRule mActivityScenario = + new ActivityScenarioRule<>(EmptyFragmentActivity.class); + + private FragmentActivity mActivity; + private InstrumentedPreferenceFragment mHost; + private FeedbackManager mFeedbackManager; + @Mock + private Lifecycle mLifecycle; + @Mock + private Menu mMenu; + @Mock + private MenuItem mMenuItem; + + @Before + public void setUp() { + mActivityScenario.getScenario().onActivity(activity -> mActivity = activity); + mHost = spy(new InstrumentedPreferenceFragment() { + @Override + public int getMetricsCategory() { + return 0; + } + }); + when(mHost.getActivity()).thenReturn(mActivity); + when(mMenu.add(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(mMenuItem); + when(mMenuItem.getItemId()).thenReturn(MENU_FEEDBACK); + mFeedbackManager = new FeedbackManager(mActivity, PACKAGE_NAME, DEFAULT_CATEGORY); + } + + @Test + public void init_withPageId_shouldAttachToLifecycle() { + when(mHost.getSettingsLifecycle()).thenReturn(mLifecycle); + + FeedbackMenuController.init(mHost, SettingsEnums.ACCESSIBILITY); + + verify(mLifecycle).addObserver(any(FeedbackMenuController.class)); + } + + @Test + public void init_withFeedbackManager_shouldAttachToLifecycle() { + when(mHost.getSettingsLifecycle()).thenReturn(mLifecycle); + + FeedbackMenuController.init(mHost, mFeedbackManager); + + verify(mLifecycle).addObserver(any(FeedbackMenuController.class)); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) + public void onCreateOptionsMenu_enableLowVisionGenericFeedback_shouldAddSendFeedbackMenu() { + FeedbackMenuController.init(mHost, mFeedbackManager); + + mHost.getSettingsLifecycle().onCreateOptionsMenu(mMenu, /* inflater= */ null); + + verify(mMenu).add(anyInt(), anyInt(), anyInt(), anyInt()); + } + + @Test + @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) + public void onCreateOptionsMenu_disableLowVisionGenericFeedback_shouldNotAddSendFeedbackMenu() { + FeedbackMenuController.init(mHost, mFeedbackManager); + + mHost.getSettingsLifecycle().onCreateOptionsMenu(mMenu, /* inflater= */ null); + + verify(mMenu, never()).add(anyInt(), anyInt(), anyInt(), anyInt()); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) + public void onOptionsItemSelected_enableLowVisionGenericFeedback_shouldStartSendFeedback() { + FeedbackMenuController.init(mHost, mFeedbackManager); + + mHost.getSettingsLifecycle().onOptionsItemSelected(mMenuItem); + + Intent intent = shadowOf(mActivity).getNextStartedActivity(); + assertThat(intent.getAction()).isEqualTo(Intent.ACTION_BUG_REPORT); + } + + @Test + @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_LOW_VISION_GENERIC_FEEDBACK) + public void onOptionsItemSelected_disableLowVisionGenericFeedback_shouldNotStartSendFeedback() { + FeedbackMenuController.init(mHost, mFeedbackManager); + + mHost.getSettingsLifecycle().onOptionsItemSelected(mMenuItem); + + Intent intent = shadowOf(mActivity).getNextStartedActivity(); + assertThat(intent).isNull(); + } +} From 17707fb1a85a393050cfb79552bbeba523fbaebf Mon Sep 17 00:00:00 2001 From: Shawn Lin Date: Tue, 11 Mar 2025 02:54:59 +0000 Subject: [PATCH 10/15] Disable search index of combine biometrics settings page when flag is on Bug: 370940762 Test: m Flag: com.android.settings.flags.biometrics_onboarding_education Change-Id: I64d4db90b9b347bd948bb8f5f8854119f4544d5a --- .../biometrics/combination/CombinedBiometricSettings.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/biometrics/combination/CombinedBiometricSettings.java b/src/com/android/settings/biometrics/combination/CombinedBiometricSettings.java index 9fe47946fbd..6be6a38c045 100644 --- a/src/com/android/settings/biometrics/combination/CombinedBiometricSettings.java +++ b/src/com/android/settings/biometrics/combination/CombinedBiometricSettings.java @@ -204,5 +204,11 @@ public class CombinedBiometricSettings extends BiometricsSettingsBase { } public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new CombinedBiometricSearchIndexProvider(R.xml.security_settings_combined_biometric); + new CombinedBiometricSearchIndexProvider(R.xml.security_settings_combined_biometric) { + @Override + protected boolean isPageSearchEnabled(Context context) { + return super.isPageSearchEnabled(context) + && !Flags.biometricsOnboardingEducation(); + } + }; } From 02bd645834b92995b39e9db6bb6cdbdc67577807 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Mon, 10 Mar 2025 18:32:22 +0800 Subject: [PATCH 11/15] Reimplement device details UI without ComposePreference Replace ComposePreference with androidx.preference.Preference. ANC toggle will still use Compose until SegmentedButtonPreference is ready. BUG: 402036473 Test: local tested Flag: com.android.settings.flags.enable_bluetooth_device_details_polish Change-Id: I5114af8f2d679d695b3c5ef4d7be2874245c435e --- ...etails_spotlight_preference_background.xml | 28 + .../bluetooth_device_spotlight_preference.xml | 73 +++ .../ui/layout/DeviceSettingLayout.kt | 28 - .../ui/view/DeviceDetailsFragmentFormatter.kt | 492 +++++++++--------- .../BluetoothDeviceDetailsViewModel.kt | 43 -- .../DeviceDetailsFragmentFormatterTest.kt | 200 ++++++- .../BluetoothDeviceDetailsViewModelTest.kt | 69 --- 7 files changed, 522 insertions(+), 411 deletions(-) create mode 100644 res/drawable/device_details_spotlight_preference_background.xml create mode 100644 res/layout/bluetooth_device_spotlight_preference.xml delete mode 100644 src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt diff --git a/res/drawable/device_details_spotlight_preference_background.xml b/res/drawable/device_details_spotlight_preference_background.xml new file mode 100644 index 00000000000..58a29a10dcd --- /dev/null +++ b/res/drawable/device_details_spotlight_preference_background.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/res/layout/bluetooth_device_spotlight_preference.xml b/res/layout/bluetooth_device_spotlight_preference.xml new file mode 100644 index 00000000000..ee2778a53ec --- /dev/null +++ b/res/layout/bluetooth_device_spotlight_preference.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt b/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt deleted file mode 100644 index 5987e5a2079..00000000000 --- a/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.bluetooth.ui.layout - -import kotlinx.coroutines.flow.Flow - -/** Represent the layout of device settings. */ -data class DeviceSettingLayout(val rows: List) - -/** Represent a row in the layout. */ -data class DeviceSettingLayoutRow(val columns: Flow>) - -/** Represent a column in a row. */ -data class DeviceSettingLayoutColumn(val settingId: Int, val highlighted: Boolean) diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt index b7ec32eb8e6..0658b1da40d 100644 --- a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt +++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt @@ -21,35 +21,26 @@ import android.app.settings.SettingsEnums import android.bluetooth.BluetoothAdapter import android.content.Context import android.content.Intent +import android.graphics.drawable.Drawable import android.os.Bundle import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toDrawable import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import androidx.preference.SwitchPreferenceCompat +import androidx.preference.TwoStatePreference import com.android.settings.R import com.android.settings.bluetooth.BlockingPrefWithSliceController import com.android.settings.bluetooth.BluetoothDetailsProfilesController -import com.android.settings.bluetooth.ui.composable.Icon import com.android.settings.bluetooth.ui.composable.MultiTogglePreference -import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel import com.android.settings.bluetooth.ui.view.DeviceDetailsMoreSettingsFragment.Companion.KEY_DEVICE_ADDRESS @@ -58,6 +49,7 @@ import com.android.settings.core.SubSettingLauncher import com.android.settings.dashboard.DashboardFragment import com.android.settings.overlay.FeatureFactory import com.android.settings.spa.preference.ComposePreference +import com.android.settingslib.PrimarySwitchPreference import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingActionModel @@ -67,24 +59,15 @@ import com.android.settingslib.core.AbstractPreferenceController import com.android.settingslib.core.lifecycle.LifecycleObserver import com.android.settingslib.core.lifecycle.events.OnPause import com.android.settingslib.core.lifecycle.events.OnStop -import com.android.settingslib.spa.framework.theme.SettingsDimension -import com.android.settingslib.spa.widget.preference.Preference as SpaPreference -import com.android.settingslib.spa.widget.preference.PreferenceModel -import com.android.settingslib.spa.widget.preference.SwitchPreference -import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel -import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference -import com.android.settingslib.spa.widget.ui.Footer +import com.android.settingslib.widget.FooterPreference import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -105,7 +88,7 @@ interface DeviceDetailsFragmentFormatter { @OptIn(ExperimentalCoroutinesApi::class) class DeviceDetailsFragmentFormatterImpl( private val context: Context, - private val fragment: DashboardFragment, + private val dashboardFragment: DashboardFragment, controllers: List, private val bluetoothAdapter: BluetoothAdapter, private val cachedDevice: CachedBluetoothDevice, @@ -120,32 +103,30 @@ class DeviceDetailsFragmentFormatterImpl( private val viewModel: BluetoothDeviceDetailsViewModel = ViewModelProvider( - fragment, - BluetoothDeviceDetailsViewModel.Factory( - fragment.requireActivity().application, - bluetoothAdapter, - cachedDevice, - backgroundCoroutineContext, - ), - ) + dashboardFragment, + BluetoothDeviceDetailsViewModel.Factory( + dashboardFragment.requireActivity().application, + bluetoothAdapter, + cachedDevice, + backgroundCoroutineContext, + ), + ) .get(BluetoothDeviceDetailsViewModel::class.java) /** Updates bluetooth device details fragment layout. */ override fun updateLayout(fragmentType: FragmentTypeModel) { - fragment.setLoading(true, false) + dashboardFragment.setLoading(true, false) isLoading = true - fragment.lifecycleScope.launch { updateLayoutInternal(fragmentType) } + dashboardFragment.lifecycleScope.launch { updateLayoutInternal(fragmentType) } } private suspend fun updateLayoutInternal(fragmentType: FragmentTypeModel) { - val items = viewModel.getItems(fragmentType) ?: run { - fragment.setLoading(false, false) - return - } - val layout = viewModel.getLayout(fragmentType) ?: run { - fragment.setLoading(false, false) - return - } + val items = + viewModel.getItems(fragmentType) + ?: run { + dashboardFragment.setLoading(false, false) + return + } val prefKeyToSettingId = items @@ -153,14 +134,14 @@ class DeviceDetailsFragmentFormatterImpl( .associateBy({ it.preferenceKey }, { it.settingId }) val settingIdToXmlPreferences: MutableMap = HashMap() - for (i in 0 until fragment.preferenceScreen.preferenceCount) { - val pref = fragment.preferenceScreen.getPreference(i) + for (i in 0 until dashboardFragment.preferenceScreen.preferenceCount) { + val pref = dashboardFragment.preferenceScreen.getPreference(i) prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref } if (pref.key !in prefKeyToSettingId) { getController(pref.key)?.let { disableController(it) } } } - fragment.preferenceScreen.removeAll() + dashboardFragment.preferenceScreen.removeAll() for (job in prefVisibilityJobs) { job.cancel() } @@ -170,53 +151,83 @@ class DeviceDetailsFragmentFormatterImpl( val settingId = settingItem.settingId if (settingIdToXmlPreferences.containsKey(settingId)) { val pref = settingIdToXmlPreferences[settingId]!!.apply { order = row } - fragment.preferenceScreen.addPreference(pref) + dashboardFragment.preferenceScreen.addPreference(pref) } else { val prefKey = getPreferenceKey(settingId) + prefVisibilityJobs.add( - getDevicesSettingForRow(layout, row) - .onEach { logItemShown(prefKey, it.isNotEmpty()) } - .launchIn(fragment.lifecycleScope) + viewModel + .getDeviceSetting(cachedDevice, settingId) + .onEach { logItemShown(prefKey, it != null) } + .launchIn(dashboardFragment.lifecycleScope) ) - val pref = - ComposePreference(context) - .apply { - key = prefKey - order = row + if (settingId == DeviceSettingId.DEVICE_SETTING_ID_ANC) { + // TODO(b/399316980): replace it with SegmentedButtonPreference once it's ready. + val pref = + ComposePreference(context) + .apply { + key = prefKey + order = row + } + .also { pref -> + pref.setContent { + buildComposePreference(cachedDevice, settingId, prefKey) + } + } + dashboardFragment.preferenceScreen.addPreference(pref) + } else { + viewModel + .getDeviceSetting(cachedDevice, settingId) + .onEach { + val existedPref = + dashboardFragment.preferenceScreen.findPreference( + prefKey + ) + val item = + it + ?: run { + existedPref?.let { + dashboardFragment.preferenceScreen.removePreference( + existedPref + ) + } + return@onEach + } + buildPreference(existedPref, item, prefKey, settingItem.highlighted) + ?.apply { + key = prefKey + order = row + } + ?.also { dashboardFragment.preferenceScreen.addPreference(it) } } - .also { pref -> pref.setContent { buildPreference(layout, row, prefKey) } } - fragment.preferenceScreen.addPreference(pref) + .launchIn(dashboardFragment.lifecycleScope) + } } } - // TODO(b/343317785): figure out how to remove the foot preference. - fragment.preferenceScreen.addPreference(ComposePreference(context).apply { - order = 10000 - isEnabled = false - isSelectable = false - setContent { Spacer(modifier = Modifier.height(1.dp)) } - }) for (row in items.indices) { val settingItem = items[row] val settingId = settingItem.settingId - if (settingIdToXmlPreferences.containsKey(settingId)) { - val pref = fragment.preferenceScreen.getPreference(row) + settingIdToXmlPreferences[settingId]?.let { pref -> if (settingId == DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES) { (getController(pref.key) as? BluetoothDetailsProfilesController)?.run { - if (settingItem is DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem) { + if ( + settingItem + is DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem + ) { setInvisibleProfiles(settingItem.invisibleProfiles) setHasExtraSpace(false) } } } - getController(pref.key)?.displayPreference(fragment.preferenceScreen) + getController(pref.key)?.displayPreference(dashboardFragment.preferenceScreen) logItemShown(pref.key, pref.isVisible) } } - fragment.lifecycleScope.launch { + dashboardFragment.lifecycleScope.launch { if (isLoading) { - fragment.setLoading(false, false) + dashboardFragment.setLoading(false, false) isLoading = false } } @@ -236,87 +247,170 @@ class DeviceDetailsFragmentFormatterImpl( } ?: emit(null) } - private fun getDevicesSettingForRow( - layout: DeviceSettingLayout, - row: Int, - ): Flow> = - layout.rows[row].columns.flatMapLatest { columns -> - if (columns.isEmpty()) { - flowOf(emptyList()) - } else { - combine( - columns.map { column -> - viewModel.getDeviceSetting(cachedDevice, column.settingId) - } - ) { - it.toList().filterNotNull() + private fun buildPreference( + existedPref: Preference?, + model: DeviceSettingPreferenceModel, + prefKey: String, + highlighted: Boolean, + ): Preference? = + when (model) { + is DeviceSettingPreferenceModel.PlainPreference -> { + val pref = + existedPref + ?: run { + if (highlighted) SpotlightPreference(context) else Preference(context) + } + pref.apply { + title = model.title + summary = model.summary + icon = getDrawable(model.icon) + onPreferenceClickListener = + object : Preference.OnPreferenceClickListener { + override fun onPreferenceClick(p: Preference): Boolean { + logItemClick(prefKey, EVENT_CLICK_PRIMARY) + model.action?.let { triggerAction(it) } + return true + } + } } } + is DeviceSettingPreferenceModel.SwitchPreference -> + if (model.action == null) { + val pref = + existedPref as? SwitchPreferenceCompat ?: SwitchPreferenceCompat(context) + pref.apply { + title = model.title + summary = model.summary + icon = getDrawable(model.icon) + isChecked = model.checked + isEnabled = !model.disabled + onPreferenceChangeListener = + object : Preference.OnPreferenceChangeListener { + override fun onPreferenceChange( + p: Preference, + value: Any?, + ): Boolean { + (p as? TwoStatePreference)?.let { newState -> + val newState = value as? Boolean ?: return false + logItemClick( + prefKey, + if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF, + ) + isEnabled = false + model.onCheckedChange.invoke(newState) + } + return false + } + } + } + } else { + val pref = + existedPref as? PrimarySwitchPreference ?: PrimarySwitchPreference(context) + pref.apply { + title = model.title + summary = model.summary + icon = getDrawable(model.icon) + isChecked = model.checked + isEnabled = !model.disabled + isSwitchEnabled = !model.disabled + onPreferenceClickListener = + object : Preference.OnPreferenceClickListener { + override fun onPreferenceClick(p: Preference): Boolean { + logItemClick(prefKey, EVENT_CLICK_PRIMARY) + triggerAction(model.action) + return true + } + } + onPreferenceChangeListener = + object : Preference.OnPreferenceChangeListener { + override fun onPreferenceChange( + p: Preference, + value: Any?, + ): Boolean { + val newState = value as? Boolean ?: return false + logItemClick( + prefKey, + if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF, + ) + isSwitchEnabled = false + model.onCheckedChange.invoke(newState) + return false + } + } + } + } + + is DeviceSettingPreferenceModel.MultiTogglePreference -> { + // TODO(b/399316980): implemented it by SegmentedButtonPreference + null + } + is DeviceSettingPreferenceModel.FooterPreference -> { + val pref = existedPref as? FooterPreference ?: FooterPreference(context) + pref.apply { title = model.footerText } + } + is DeviceSettingPreferenceModel.MoreSettingsPreference -> { + val pref = existedPref ?: Preference(context) + pref.apply { + title = + context.getString(R.string.bluetooth_device_more_settings_preference_title) + summary = + context.getString( + R.string.bluetooth_device_more_settings_preference_summary + ) + icon = context.getDrawable(R.drawable.ic_chevron_right_24dp) + onPreferenceClickListener = + object : Preference.OnPreferenceClickListener { + override fun onPreferenceClick(p: Preference): Boolean { + logItemClick(prefKey, EVENT_CLICK_PRIMARY) + SubSettingLauncher(context) + .setDestination( + DeviceDetailsMoreSettingsFragment::class.java.name + ) + .setSourceMetricsCategory( + dashboardFragment.getMetricsCategory() + ) + .setArguments( + Bundle().apply { + putString(KEY_DEVICE_ADDRESS, cachedDevice.address) + } + ) + .launch() + return true + } + } + } + } + is DeviceSettingPreferenceModel.HelpPreference -> { + null + } } - @Composable - private fun buildPreference(layout: DeviceSettingLayout, row: Int, prefKey: String) { - val contents by - remember(row) { getDevicesSettingForRow(layout, row) } - .collectAsStateWithLifecycle(initialValue = listOf()) - - val highlighted by - remember(row) { - layout.rows[row].columns.map { columns -> columns.any { it.highlighted } } + private fun getDrawable(deviceSettingIcon: DeviceSettingIcon?): Drawable? = + when (deviceSettingIcon) { + is DeviceSettingIcon.BitmapIcon -> + deviceSettingIcon.bitmap.toDrawable(context.resources) + is DeviceSettingIcon.ResourceIcon -> context.getDrawable(deviceSettingIcon.resId) + null -> null } - .collectAsStateWithLifecycle(initialValue = false) + + @Composable + private fun buildComposePreference( + cachedDevice: CachedBluetoothDevice, + settingId: Int, + prefKey: String, + ) { + val contents by + remember(settingId) { viewModel.getDeviceSetting(cachedDevice, settingId) } + .collectAsStateWithLifecycle(initialValue = null) val settings = contents - AnimatedVisibility(visible = settings.isNotEmpty(), enter = fadeIn(), exit = fadeOut()) { - Box { - Box( - modifier = - Modifier.matchParentSize() - .padding(16.dp, 0.dp, 8.dp, 0.dp) - .background( - color = - if (highlighted) { - MaterialTheme.colorScheme.primaryContainer - } else { - Color.Transparent - }, - shape = RoundedCornerShape(28.dp), - ) - ) {} - buildPreferences(settings, prefKey) + AnimatedVisibility(visible = settings != null, enter = fadeIn(), exit = fadeOut()) { + (settings as? DeviceSettingPreferenceModel.MultiTogglePreference)?.let { + buildMultiTogglePreference(it, prefKey) } } } - @Composable - fun buildPreferences(settings: List, prefKey: String) { - when (settings.size) { - 0 -> {} - 1 -> { - when (val setting = settings[0]) { - is DeviceSettingPreferenceModel.PlainPreference -> { - buildPlainPreference(setting, prefKey) - } - is DeviceSettingPreferenceModel.SwitchPreference -> { - buildSwitchPreference(setting, prefKey) - } - is DeviceSettingPreferenceModel.MultiTogglePreference -> { - buildMultiTogglePreference(setting, prefKey) - } - is DeviceSettingPreferenceModel.FooterPreference -> { - buildFooterPreference(setting) - } - is DeviceSettingPreferenceModel.MoreSettingsPreference -> { - buildMoreSettingsPreference(prefKey) - } - is DeviceSettingPreferenceModel.HelpPreference -> {} - null -> {} - } - } - else -> {} - } - } - @Composable private fun buildMultiTogglePreference( pref: DeviceSettingPreferenceModel.MultiTogglePreference, @@ -332,107 +426,6 @@ class DeviceDetailsFragmentFormatterImpl( ) } - @Composable - private fun buildSwitchPreference( - model: DeviceSettingPreferenceModel.SwitchPreference, - prefKey: String, - ) { - val switchPrefModel = - object : SwitchPreferenceModel { - override val title = model.title - override val summary = { model.summary ?: "" } - override val checked = { model.checked } - override val onCheckedChange = { newState: Boolean -> - logItemClick(prefKey, if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF) - model.onCheckedChange(newState) - } - override val changeable = { !model.disabled } - override val icon: (@Composable () -> Unit)? - get() { - if (model.icon == null) { - return null - } - return { deviceSettingIcon(model.icon) } - } - } - if (model.action != null) { - TwoTargetSwitchPreference( - switchPrefModel, - primaryOnClick = { - logItemClick(prefKey, EVENT_CLICK_PRIMARY) - triggerAction(model.action) - }, - primaryEnabled = { !model.disabled }, - ) - } else { - SwitchPreference(switchPrefModel) - } - } - - @Composable - private fun buildPlainPreference( - model: DeviceSettingPreferenceModel.PlainPreference, - prefKey: String, - ) { - SpaPreference( - object : PreferenceModel { - override val title = model.title - override val summary = { model.summary ?: "" } - override val onClick = { - logItemClick(prefKey, EVENT_CLICK_PRIMARY) - model.action?.let { triggerAction(it) } - Unit - } - override val icon: (@Composable () -> Unit)? - get() { - if (model.icon == null) { - return null - } - return { deviceSettingIcon(model.icon) } - } - } - ) - } - - @Composable - fun buildMoreSettingsPreference(prefKey: String) { - SpaPreference( - object : PreferenceModel { - override val title = - stringResource(R.string.bluetooth_device_more_settings_preference_title) - override val summary = { - context.getString(R.string.bluetooth_device_more_settings_preference_summary) - } - override val onClick = { - logItemClick(prefKey, EVENT_CLICK_PRIMARY) - SubSettingLauncher(context) - .setDestination(DeviceDetailsMoreSettingsFragment::class.java.name) - .setSourceMetricsCategory(fragment.getMetricsCategory()) - .setArguments( - Bundle().apply { putString(KEY_DEVICE_ADDRESS, cachedDevice.address) } - ) - .launch() - } - override val icon = - @Composable { - deviceSettingIcon( - DeviceSettingIcon.ResourceIcon(R.drawable.ic_chevron_right_24dp) - ) - } - } - ) - } - - @Composable - fun buildFooterPreference(model: DeviceSettingPreferenceModel.FooterPreference) { - Footer(footerText = model.footerText) - } - - @Composable - private fun deviceSettingIcon(icon: DeviceSettingIcon?) { - icon?.let { Icon(it, modifier = Modifier.size(SettingsDimension.itemIconSize)) } - } - private fun logItemClick(preferenceKey: String, value: Int = 0) { logAction(preferenceKey, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_CLICKED, value) } @@ -452,7 +445,7 @@ class DeviceDetailsFragmentFormatterImpl( if (it) EVENT_VISIBLE else EVENT_INVISIBLE, ) } - .launchIn(fragment.lifecycleScope) + .launchIn(dashboardFragment.lifecycleScope) } } .value = visible @@ -485,7 +478,7 @@ class DeviceDetailsFragmentFormatterImpl( private fun disableController(controller: AbstractPreferenceController) { if (controller is LifecycleObserver) { - fragment.settingsLifecycle.removeObserver(controller as LifecycleObserver) + dashboardFragment.settingsLifecycle.removeObserver(controller as LifecycleObserver) } if (controller is BlockingPrefWithSliceController) { @@ -504,6 +497,19 @@ class DeviceDetailsFragmentFormatterImpl( private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}" + private class SpotlightPreference(context: Context) : Preference(context) { + + init { + layoutResource = R.layout.bluetooth_device_spotlight_preference + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.isDividerAllowedBelow = false + holder.isDividerAllowedAbove = false + } + } + private companion object { const val TAG = "DeviceDetailsFormatter" const val EVENT_SWITCH_OFF = 0 diff --git a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt index 8d3b8539b98..5434ec98c55 100644 --- a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt +++ b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt @@ -23,9 +23,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.android.settings.R -import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout -import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutColumn -import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel import com.android.settings.overlay.FeatureFactory.Companion.featureFactory @@ -39,11 +36,8 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn class BluetoothDeviceDetailsViewModel( private val application: Application, @@ -141,43 +135,6 @@ class BluetoothDeviceDetailsViewModel( } } - suspend fun getLayout(fragment: FragmentTypeModel): DeviceSettingLayout? { - val configItems = getItems(fragment) ?: return null - val idToDeviceSetting = - configItems - .filterIsInstance() - .associateBy({ it.settingId }, { getDeviceSetting(cachedDevice, it.settingId) }) - - val configDeviceSetting = - configItems.map { idToDeviceSetting[it.settingId] ?: flowOf(null) } - val positionToSettingIds = - combine(configDeviceSetting) { settings -> - val positionMapping = mutableMapOf>() - for (i in settings.indices) { - val configItem = configItems[i] - val setting = settings[i] - val isXmlPreference = configItem is DeviceSettingConfigItemModel.BuiltinItem - if (!isXmlPreference && setting == null) { - continue - } - positionMapping[i] = - listOf( - DeviceSettingLayoutColumn( - configItem.settingId, - configItem.highlighted, - ) - ) - } - positionMapping - } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = mapOf()) - return DeviceSettingLayout( - configItems.indices.map { idx -> - DeviceSettingLayoutRow(positionToSettingIds.map { it[idx] ?: emptyList() }) - } - ) - } - class Factory( private val application: Application, private val bluetoothAdapter: BluetoothAdapter, diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt index d0bd27d7a6e..1eb15e50f62 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt +++ b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt @@ -16,7 +16,7 @@ package com.android.settings.bluetooth.ui.view -import android.app.settings.SettingsEnums; +import android.app.settings.SettingsEnums import android.bluetooth.BluetoothAdapter import android.content.Context import android.content.Intent @@ -25,6 +25,7 @@ import androidx.fragment.app.FragmentActivity import androidx.preference.Preference import androidx.preference.PreferenceManager import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat import androidx.test.core.app.ApplicationProvider import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel @@ -33,6 +34,7 @@ import com.android.settings.testutils.FakeFeatureFactory import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingActionModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon @@ -58,14 +60,15 @@ import org.mockito.Mock import org.mockito.Mockito.any import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +import org.mockito.Spy import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule +import org.mockito.kotlin.doNothing import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.shadows.ShadowLooper import org.robolectric.shadows.ShadowLooper.shadowMainLooper - @ExperimentalCoroutinesApi @RunWith(RobolectricTestRunner::class) class DeviceDetailsFragmentFormatterTest { @@ -78,7 +81,7 @@ class DeviceDetailsFragmentFormatterTest { @Mock private lateinit var headerController: AbstractPreferenceController @Mock private lateinit var buttonController: AbstractPreferenceController - private lateinit var context: Context + @Spy private val context: Context = ApplicationProvider.getApplicationContext() private lateinit var fragment: TestFragment private lateinit var underTest: DeviceDetailsFragmentFormatter private lateinit var featureFactory: FakeFeatureFactory @@ -87,11 +90,15 @@ class DeviceDetailsFragmentFormatterTest { @Before fun setUp() { - context = ApplicationProvider.getApplicationContext() featureFactory = FakeFeatureFactory.setupForTest() + doNothing().`when`(context).startActivity(any(Intent::class.java)) `when`( featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository( - eq(context), eq(bluetoothAdapter), any())) + any(), + eq(bluetoothAdapter), + any(), + ) + ) .thenReturn(repository) fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java) assertThat(fragmentActivity.applicationContext).isNotNull() @@ -115,7 +122,8 @@ class DeviceDetailsFragmentFormatterTest { listOf(profileController, headerController, buttonController), bluetoothAdapter, cachedDevice, - testScope.testScheduler) + testScope.testScheduler, + ) } @Test @@ -124,11 +132,16 @@ class DeviceDetailsFragmentFormatterTest { `when`(repository.getDeviceSettingsConfig(cachedDevice)) .thenReturn( DeviceSettingConfigModel( - listOf(), listOf(), DeviceSettingConfigItemModel.AppProvidedItem(12345, false))) - val intent = Intent().apply { - setAction(Intent.ACTION_VIEW) - setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } + listOf(), + listOf(), + DeviceSettingConfigItemModel.AppProvidedItem(12345, false), + ) + ) + val intent = + Intent().apply { + setAction(Intent.ACTION_VIEW) + setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } `when`(repository.getDeviceSetting(cachedDevice, 12345)) .thenReturn( flowOf( @@ -136,12 +149,15 @@ class DeviceDetailsFragmentFormatterTest { cachedDevice = cachedDevice, id = 12345, intent = intent, - ))) + ) + ) + ) var helpPreference: DeviceSettingPreferenceModel.HelpPreference? = null - underTest.getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment).onEach { - helpPreference = it - }.launchIn(testScope.backgroundScope) + underTest + .getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment) + .onEach { helpPreference = it } + .launchIn(testScope.backgroundScope) delay(100) runCurrent() ShadowLooper.idleMainLooper() @@ -171,13 +187,19 @@ class DeviceDetailsFragmentFormatterTest { listOf( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingId.DEVICE_SETTING_ID_HEADER, - highlighted = false, preferenceKey = "bluetooth_device_header"), + highlighted = false, + preferenceKey = "bluetooth_device_header", + ), DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES, - highlighted = false, preferenceKey = "bluetooth_profiles"), + highlighted = false, + preferenceKey = "bluetooth_profiles", + ), ), listOf(), - null)) + null, + ) + ) underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment) runCurrent() @@ -189,13 +211,17 @@ class DeviceDetailsFragmentFormatterTest { SettingsEnums.PAGE_UNKNOWN, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN, 0, - "bluetooth_device_header", 1) + "bluetooth_device_header", + 1, + ) verify(featureFactory.metricsFeatureProvider) .action( SettingsEnums.PAGE_UNKNOWN, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN, 0, - "bluetooth_profiles", 1) + "bluetooth_profiles", + 1, + ) } } @@ -209,16 +235,22 @@ class DeviceDetailsFragmentFormatterTest { DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingId.DEVICE_SETTING_ID_HEADER, highlighted = false, - preferenceKey = "bluetooth_device_header"), + preferenceKey = "bluetooth_device_header", + ), DeviceSettingConfigItemModel.AppProvidedItem( - DeviceSettingId.DEVICE_SETTING_ID_ANC, highlighted = false), + DeviceSettingId.DEVICE_SETTING_ID_ANC, + highlighted = false, + ), DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES, highlighted = false, - preferenceKey = "bluetooth_profiles"), + preferenceKey = "bluetooth_profiles", + ), ), listOf(), - null)) + null, + ) + ) `when`(repository.getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC)) .thenReturn( flowOf( @@ -231,11 +263,17 @@ class DeviceDetailsFragmentFormatterTest { ToggleModel( "", DeviceSettingIcon.BitmapIcon( - Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)))), + Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + ), + ) + ), isActive = true, state = DeviceSettingStateModel.MultiTogglePreferenceState(0), isAllowedChangingState = true, - updateState = {}))) + updateState = {}, + ) + ) + ) underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment) runCurrent() @@ -244,13 +282,119 @@ class DeviceDetailsFragmentFormatterTest { .containsExactly( "bluetooth_device_header", "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}", - "bluetooth_profiles") + "bluetooth_profiles", + ) verify(featureFactory.metricsFeatureProvider) .action( SettingsEnums.PAGE_UNKNOWN, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN, 0, - "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}", 1 + "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}", + 1, + ) + } + } + + @Test + fun updateLayout_plainPreferenceClicked() { + testScope.runTest { + val settingId = 12345 + val intent = Intent("test_intent") + `when`(repository.getDeviceSettingsConfig(cachedDevice)) + .thenReturn( + DeviceSettingConfigModel( + listOf( + DeviceSettingConfigItemModel.AppProvidedItem( + settingId, + highlighted = false, + ) + ), + listOf(), + null, + ) + ) + + `when`(repository.getDeviceSetting(cachedDevice, settingId)) + .thenReturn( + flowOf( + DeviceSettingModel.ActionSwitchPreference( + cachedDevice = cachedDevice, + id = settingId, + title = "title", + summary = "summary", + icon = null, + action = DeviceSettingActionModel.IntentAction(intent), + ) + ) + ) + + underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment) + runCurrent() + val displayedPrefs = getDisplayedPreferences() + displayedPrefs[0].performClick() + + assertThat(displayedPrefs).hasSize(1) + verify(context).startActivity(intent) + verify(featureFactory.metricsFeatureProvider) + .action( + SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_CLICKED, + 0, + "DEVICE_SETTING_$settingId", + 2, + ) + } + } + + @Test + fun updateLayout_switchPreferenceClicked() { + val settingId = 12345 + testScope.runTest { + `when`(repository.getDeviceSettingsConfig(cachedDevice)) + .thenReturn( + DeviceSettingConfigModel( + listOf( + DeviceSettingConfigItemModel.AppProvidedItem( + settingId, + highlighted = false, + ) + ), + listOf(), + null, + ) + ) + + `when`(repository.getDeviceSetting(cachedDevice, settingId)) + .thenReturn( + flowOf( + DeviceSettingModel.ActionSwitchPreference( + cachedDevice = cachedDevice, + id = settingId, + title = "title", + summary = "summary", + icon = null, + action = null, + switchState = DeviceSettingStateModel.ActionSwitchPreferenceState(true), + isAllowedChangingState = true, + updateState = {}, + ) + ) + ) + + underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment) + runCurrent() + val displayedPrefs = getDisplayedPreferences() + displayedPrefs[0].performClick() + + assertThat(displayedPrefs).hasSize(1) + assertThat(displayedPrefs[0]).isInstanceOf(SwitchPreferenceCompat::class.java) + verify(featureFactory.metricsFeatureProvider) + .action( + SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_CLICKED, + 0, + "DEVICE_SETTING_$settingId", + 0, ) } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt index caeea942f62..bf66a04cede 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt +++ b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt @@ -20,7 +20,6 @@ import android.app.Application import android.bluetooth.BluetoothAdapter import android.graphics.Bitmap import androidx.test.core.app.ApplicationProvider -import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel import com.android.settings.testutils.FakeFeatureFactory @@ -164,74 +163,6 @@ class BluetoothDeviceDetailsViewModelTest { } } - @Test - fun getLayout_builtinDeviceSettings() { - testScope.runTest { - `when`(repository.getDeviceSettingsConfig(cachedDevice)) - .thenReturn( - DeviceSettingConfigModel( - listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), null)) - - val layout = underTest.getLayout(FragmentTypeModel.DeviceDetailsMainFragment)!! - - assertThat(getLatestLayout(layout)) - .isEqualTo( - listOf( - listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER), - listOf(DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS))) - } - } - - @Test - fun getLayout_remoteDeviceSettings() { - val remoteSettingId1 = 10001 - val remoteSettingId2 = 10002 - val remoteSettingId3 = 10003 - testScope.runTest { - `when`(repository.getDeviceSettingsConfig(cachedDevice)) - .thenReturn( - DeviceSettingConfigModel( - listOf( - BUILTIN_SETTING_ITEM_1, - buildRemoteSettingItem(remoteSettingId1), - buildRemoteSettingItem(remoteSettingId2), - buildRemoteSettingItem(remoteSettingId3), - ), - listOf(), - null)) - `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1)) - .thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId1))) - `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId2)) - .thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId2))) - `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId3)) - .thenReturn(flowOf(buildActionSwitchPreference(remoteSettingId3))) - - val layout = underTest.getLayout(FragmentTypeModel.DeviceDetailsMainFragment)!! - - assertThat(getLatestLayout(layout)) - .isEqualTo( - listOf( - listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER), - listOf(remoteSettingId1), - listOf(remoteSettingId2), - listOf(remoteSettingId3), - )) - } - } - - private fun getLatestLayout(layout: DeviceSettingLayout): List> { - val latestLayout = MutableList(layout.rows.size) { emptyList() } - for (i in layout.rows.indices) { - layout.rows[i] - .columns - .onEach { latestLayout[i] = it.map { c -> c.settingId } } - .launchIn(testScope.backgroundScope) - } - - testScope.runCurrent() - return latestLayout.filter { !it.isEmpty() }.toList() - } - private fun buildMultiTogglePreference(settingId: Int) = DeviceSettingModel.MultiTogglePreference( cachedDevice, From 6e0bbb6259b88de5afaf13d15299f7b2208cae0b Mon Sep 17 00:00:00 2001 From: Ze Li Date: Tue, 11 Mar 2025 15:45:04 +0800 Subject: [PATCH 12/15] [Bluetooth Pairing] Change allow phone book permission Switch to Material3 style. Test: manual test Bug: 318785412 Flag: EXEMPT bug fix Change-Id: I5546cb504a6db939a4ab62264d496adcf93aa0e2 --- res/layout/bluetooth_pin_confirm.xml | 3 ++- .../BluetoothPairingDialogFragment.java | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/res/layout/bluetooth_pin_confirm.xml b/res/layout/bluetooth_pin_confirm.xml index 9387d5ddd82..69ea777c9f2 100644 --- a/res/layout/bluetooth_pin_confirm.xml +++ b/res/layout/bluetooth_pin_confirm.xml @@ -101,7 +101,8 @@ android:hyphenationFrequency="normalFast" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Caption" /> - Date: Fri, 7 Mar 2025 20:33:46 +0000 Subject: [PATCH 13/15] Tap to pause/play the lottie animation in remaining PrivateSpace setup screens This is to conform to a11y motion stopping requirements. Test: manually Bug: 379258725 Flag: EXEMPT bugfix Change-Id: Ic06cb03f5490def37894b8f448e9e435ad5baf35 --- .../privatespace/PrivateSpaceSetLockFragment.java | 12 ++++++++++++ .../settings/privatespace/SetupSuccessFragment.java | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java b/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java index 28ac97fb0b0..47cf3baa76c 100644 --- a/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java +++ b/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java @@ -51,6 +51,8 @@ public class PrivateSpaceSetLockFragment extends InstrumentedFragment { private static final String TAG = "PrivateSpaceSetLockFrag"; private static final int HEADER_TEXT_MAX_LINES = 4; + private boolean mIsAnimationPlaying = true; + @Override public View onCreateView( LayoutInflater inflater, @@ -91,6 +93,7 @@ public class PrivateSpaceSetLockFragment extends InstrumentedFragment { requireActivity().getOnBackPressedDispatcher().addCallback(this, callback); LottieAnimationView lottieAnimationView = rootView.findViewById(R.id.lottie_animation); LottieColorUtils.applyDynamicColors(getContext(), lottieAnimationView); + lottieAnimationView.setOnClickListener(v -> handleAnimationClick(lottieAnimationView)); return rootView; } @@ -130,4 +133,13 @@ public class PrivateSpaceSetLockFragment extends InstrumentedFragment { Log.w(TAG, "Private profile user handle is null"); } } + + private void handleAnimationClick(LottieAnimationView lottieAnimationView) { + if (mIsAnimationPlaying) { + lottieAnimationView.pauseAnimation(); + } else { + lottieAnimationView.playAnimation(); + } + mIsAnimationPlaying = !mIsAnimationPlaying; + } } diff --git a/src/com/android/settings/privatespace/SetupSuccessFragment.java b/src/com/android/settings/privatespace/SetupSuccessFragment.java index bfd9e961837..538912e1c65 100644 --- a/src/com/android/settings/privatespace/SetupSuccessFragment.java +++ b/src/com/android/settings/privatespace/SetupSuccessFragment.java @@ -49,6 +49,8 @@ import java.util.List; public class SetupSuccessFragment extends InstrumentedFragment { private static final String TAG = "SetupSuccessFragment"; + private boolean mIsAnimationPlaying = true; + @Override public View onCreateView( LayoutInflater inflater, @@ -80,6 +82,7 @@ public class SetupSuccessFragment extends InstrumentedFragment { requireActivity().getOnBackPressedDispatcher().addCallback(this, callback); LottieAnimationView lottieAnimationView = rootView.findViewById(R.id.lottie_animation); LottieColorUtils.applyDynamicColors(getContext(), lottieAnimationView); + lottieAnimationView.setOnClickListener(v -> handleAnimationClick(lottieAnimationView)); return rootView; } @@ -141,4 +144,13 @@ public class SetupSuccessFragment extends InstrumentedFragment { task.finishAndRemoveTask(); } } + + private void handleAnimationClick(LottieAnimationView lottieAnimationView) { + if (mIsAnimationPlaying) { + lottieAnimationView.pauseAnimation(); + } else { + lottieAnimationView.playAnimation(); + } + mIsAnimationPlaying = !mIsAnimationPlaying; + } } From 177fd578c950f88f5070a90b5a20ca05bfb95a23 Mon Sep 17 00:00:00 2001 From: Pawan Wagh Date: Fri, 7 Mar 2025 19:58:25 +0000 Subject: [PATCH 14/15] Show failure reasons to user Sometimes device might have pending update which can cause 16KB toggle to fail. Show the pending update errors to user. Flag: EXEMPT bug_fix Test: atest Enable16KbTest Bug: 394678137 Bug: 400733054 Change-Id: Id032b2f14afb74af3b7458a426addc7e32e3a6f7 --- res/values/strings.xml | 5 +++++ .../Enable16kPagesPreferenceController.java | 20 +++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index e0e47932339..3da882d36ea 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -13149,6 +13149,11 @@ Data usage charges may apply. Application will be stopped to apply page size compat setting. + + Kernel update failed. Check and install any pending updates. + + Kernel update failed. Error occurred while applying OTA. + DSU Loader diff --git a/src/com/android/settings/development/Enable16kPagesPreferenceController.java b/src/com/android/settings/development/Enable16kPagesPreferenceController.java index d8ad55f4146..1c1b713562a 100644 --- a/src/com/android/settings/development/Enable16kPagesPreferenceController.java +++ b/src/com/android/settings/development/Enable16kPagesPreferenceController.java @@ -186,7 +186,13 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen public void onFailure(@NonNull Throwable t) { hideProgressDialog(); Log.e(TAG, "Failed to call applyPayload of UpdateEngineStable!", t); - displayToast(mContext.getString(R.string.toast_16k_update_failed_text)); + // installUpdate will always throw localized messages. + String message = t.getMessage(); + if (message != null) { + displayToast(message); + } else { + displayToast(mContext.getString(R.string.toast_16k_update_failed_text)); + } } }, ContextCompat.getMainExecutor(mContext)); @@ -208,10 +214,8 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen int status = data.getInt(SystemUpdateManager.KEY_STATUS); if (status != SystemUpdateManager.STATUS_UNKNOWN && status != SystemUpdateManager.STATUS_IDLE) { - throw new RuntimeException( - "System has pending update! Please restart the device to complete applying" - + " pending update. If you are seeing this after using 16KB developer" - + " options, please check configuration and OTA packages!"); + Log.e(TAG, "SystemUpdateManager is not available. Status :" + status); + throw new RuntimeException(mContext.getString(R.string.error_pending_updates)); } // Publish system update info @@ -223,7 +227,11 @@ public class Enable16kPagesPreferenceController extends DeveloperOptionsPreferen Log.i(TAG, "Update file path is " + updateFile.getAbsolutePath()); applyUpdateFile(updateFile); } catch (IOException e) { - throw new RuntimeException(e); + Log.e(TAG, "Error occurred while applying OTA ", e); + throw new RuntimeException(mContext.getString(R.string.error_ota_failed)); + } catch (Exception e) { + Log.e(TAG, "Unknown error occurred while applying OTA ", e); + throw new RuntimeException(mContext.getString(R.string.toast_16k_update_failed_text)); } } From bb8b603f94d1919e7050fcab4ef2586cdcbe2d2d Mon Sep 17 00:00:00 2001 From: Xiaomiao Zhang Date: Fri, 7 Mar 2025 23:59:41 +0000 Subject: [PATCH 15/15] Add SafeSites related content filters preference. Test: atest SupervisionSafeSitesPreferenceTest Test: maunally tested locally Bug: 401568993 Flag: android.app.supervision.flags.enable_web_content_filters_screen Change-Id: I556019bdeba5ed459996102217836cda0e3c7f71 --- res/values/strings.xml | 8 ++ .../SupervisionSafeSitesPreference.kt | 103 ++++++++++++++++++ .../SupervisionWebContentFiltersScreen.kt | 12 +- .../SupervisionSafeSitesPreferenceTest.kt | 53 +++++++++ 4 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 src/com/android/settings/supervision/SupervisionSafeSitesPreference.kt create mode 100644 tests/robotests/src/com/android/settings/supervision/SupervisionSafeSitesPreferenceTest.kt diff --git a/res/values/strings.xml b/res/values/strings.xml index e8d3618f071..1e8ba4e4f1a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -14309,6 +14309,14 @@ Data usage charges may apply. Forgot PIN Web content filters + + Google Chrome and Web + + Try to block explicit sites + + No filter is perfect, but this should help hide sexually explicit sites + + Allow all sites %1$s animation diff --git a/src/com/android/settings/supervision/SupervisionSafeSitesPreference.kt b/src/com/android/settings/supervision/SupervisionSafeSitesPreference.kt new file mode 100644 index 00000000000..aaa5a333a86 --- /dev/null +++ b/src/com/android/settings/supervision/SupervisionSafeSitesPreference.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 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.supervision + +import android.content.Context +import androidx.preference.Preference +import com.android.settings.R +import com.android.settingslib.datastore.KeyValueStore +import com.android.settingslib.datastore.Permissions +import com.android.settingslib.datastore.SettingsSecureStore +import com.android.settingslib.metadata.BooleanValuePreference +import com.android.settingslib.metadata.PreferenceMetadata +import com.android.settingslib.metadata.ReadWritePermit +import com.android.settingslib.metadata.SensitivityLevel +import com.android.settingslib.preference.PreferenceBinding +import com.android.settingslib.preference.forEachRecursively +import com.android.settingslib.widget.SelectorWithWidgetPreference + +/** Base class of web content filters Safe sites preferences. */ +sealed class SupervisionSafeSitesPreference : + BooleanValuePreference, SelectorWithWidgetPreference.OnClickListener, PreferenceBinding { + override fun storage(context: Context): KeyValueStore = SettingsSecureStore.get(context) + + override fun getReadPermissions(context: Context) = Permissions.EMPTY + + override fun getWritePermissions(context: Context) = Permissions.EMPTY + + override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) = + ReadWritePermit.ALLOW + + override fun getWritePermit( + context: Context, + value: Boolean?, + callingPid: Int, + callingUid: Int, + ) = ReadWritePermit.DISALLOW + + override val sensitivityLevel + get() = SensitivityLevel.NO_SENSITIVITY + + override fun createWidget(context: Context) = SelectorWithWidgetPreference(context) + + override fun onRadioButtonClicked(emiter: SelectorWithWidgetPreference) { + emiter.parent?.forEachRecursively { + if (it is SelectorWithWidgetPreference) { + it.isChecked = it == emiter + } + } + } + + override fun bind(preference: Preference, metadata: PreferenceMetadata) { + super.bind(preference, metadata) + (preference as SelectorWithWidgetPreference).also { + // TODO(b/401568468): Set the isChecked value using stored values. + it.isChecked = (it.key == SupervisionAllowAllSitesPreference.KEY) + it.setOnClickListener(this) + } + } +} + +/** The "Try to block explicit sites" preference. */ +class SupervisionBlockExplicitSitesPreference : SupervisionSafeSitesPreference() { + + override val key + get() = KEY + + override val title + get() = R.string.supervision_web_content_filters_browser_block_explicit_sites_title + + override val summary + get() = R.string.supervision_web_content_filters_browser_block_explicit_sites_summary + + companion object { + const val KEY = "web_content_filters_browser_block_explicit_sites" + } +} + +/** The "Allow all sites" preference. */ +class SupervisionAllowAllSitesPreference : SupervisionSafeSitesPreference() { + + override val key + get() = KEY + + override val title + get() = R.string.supervision_web_content_filters_browser_allow_all_sites_title + + companion object { + const val KEY = "web_content_filters_browser_allow_all_sites" + } +} diff --git a/src/com/android/settings/supervision/SupervisionWebContentFiltersScreen.kt b/src/com/android/settings/supervision/SupervisionWebContentFiltersScreen.kt index ef46b2084da..0a2891b5c5e 100644 --- a/src/com/android/settings/supervision/SupervisionWebContentFiltersScreen.kt +++ b/src/com/android/settings/supervision/SupervisionWebContentFiltersScreen.kt @@ -18,6 +18,7 @@ package com.android.settings.supervision import android.app.supervision.flags.Flags import android.content.Context import com.android.settings.R +import com.android.settingslib.metadata.PreferenceCategory import com.android.settingslib.metadata.ProvidePreferenceScreen import com.android.settingslib.metadata.preferenceHierarchy import com.android.settingslib.preference.PreferenceScreenCreator @@ -41,10 +42,19 @@ class SupervisionWebContentFiltersScreen : PreferenceScreenCreator { override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(context, this) { - // TODO(b/395134536) implement the screen. + +PreferenceCategory( + BROWSER_RADIO_BUTTON_GROUP, + R.string.supervision_web_content_filters_browser_title, + ) += + { + +SupervisionBlockExplicitSitesPreference() + +SupervisionAllowAllSitesPreference() + } + // TODO(b/401569571) implement the SafeSearch group. } companion object { const val KEY = "supervision_web_content_filters" + internal const val BROWSER_RADIO_BUTTON_GROUP = "browser_radio_button_group" } } diff --git a/tests/robotests/src/com/android/settings/supervision/SupervisionSafeSitesPreferenceTest.kt b/tests/robotests/src/com/android/settings/supervision/SupervisionSafeSitesPreferenceTest.kt new file mode 100644 index 00000000000..5be7a1167e4 --- /dev/null +++ b/tests/robotests/src/com/android/settings/supervision/SupervisionSafeSitesPreferenceTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 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.supervision + +import android.content.Context +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 + +@RunWith(AndroidJUnit4::class) +class SupervisionSafeSitesPreferenceTest { + private val context: Context = ApplicationProvider.getApplicationContext() + + private val allowAllSitesPreference = SupervisionAllowAllSitesPreference() + + private val blockExplicitSitesPreference = SupervisionBlockExplicitSitesPreference() + + @Test + fun getTitle_allowAllSites() { + assertThat(allowAllSitesPreference.title) + .isEqualTo(R.string.supervision_web_content_filters_browser_allow_all_sites_title) + } + + @Test + fun getTitle_blockExplicitSites() { + assertThat(blockExplicitSitesPreference.title) + .isEqualTo(R.string.supervision_web_content_filters_browser_block_explicit_sites_title) + } + + @Test + fun getSummary_blockExplicitSites() { + assertThat(blockExplicitSitesPreference.summary) + .isEqualTo( + R.string.supervision_web_content_filters_browser_block_explicit_sites_summary + ) + } +}