From baf503050fc615c7f663ad09e5adb5ab7e7d99dc Mon Sep 17 00:00:00 2001 From: Wa Gao Date: Mon, 10 Feb 2025 19:32:11 -0800 Subject: [PATCH 01/14] Disable the ContentProtection setting switch bar when current user is a guest user. Bug: 337774836 Test: m -j256 Settings && atest SettingsRoboTests:ContentProtectionTogglePreferenceControllerTest Flag: EXEMPT bugfix Change-Id: I36173b2e7027765b526dfa2ebca216a7de71e669 --- ...tProtectionTogglePreferenceController.java | 9 ++++ ...tectionTogglePreferenceControllerTest.java | 44 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/security/ContentProtectionTogglePreferenceController.java b/src/com/android/settings/security/ContentProtectionTogglePreferenceController.java index 9203d61f047..69ac6b100be 100644 --- a/src/com/android/settings/security/ContentProtectionTogglePreferenceController.java +++ b/src/com/android/settings/security/ContentProtectionTogglePreferenceController.java @@ -21,6 +21,7 @@ import android.app.admin.DevicePolicyManager; import android.content.ContentResolver; import android.content.Context; import android.os.UserHandle; +import android.os.UserManager; import android.provider.Settings; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; @@ -126,6 +127,14 @@ public class ContentProtectionTogglePreferenceController extends TogglePreferenc && mContentProtectionPolicy != DevicePolicyManager.CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY) { mSwitchBar.setDisabledByAdmin(mEnforcedAdmin); + return; + } + + UserManager userManager = mContext.getSystemService(UserManager.class); + if (userManager != null + && userManager.isGuestUser() + && mSwitchBar != null) { + mSwitchBar.setEnabled(false); } } diff --git a/tests/robotests/src/com/android/settings/security/ContentProtectionTogglePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/security/ContentProtectionTogglePreferenceControllerTest.java index 075ac6c1ba1..6514a4e4043 100644 --- a/tests/robotests/src/com/android/settings/security/ContentProtectionTogglePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/security/ContentProtectionTogglePreferenceControllerTest.java @@ -24,8 +24,11 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; import android.app.admin.DevicePolicyManager; import android.content.Context; @@ -38,6 +41,7 @@ import androidx.preference.PreferenceScreen; import androidx.test.core.app.ApplicationProvider; import com.android.settings.testutils.shadow.ShadowUtils; +import com.android.settings.testutils.shadow.ShadowUserManager; import com.android.settings.widget.SettingsMainSwitchPreference; import com.android.settingslib.RestrictedLockUtils; @@ -53,7 +57,8 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) -@Config(shadows = {ShadowUtils.class}) +@Config(shadows = {ShadowUtils.class, + ShadowUserManager.class}) public class ContentProtectionTogglePreferenceControllerTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @@ -62,7 +67,7 @@ public class ContentProtectionTogglePreferenceControllerTest { private final Context mContext = ApplicationProvider.getApplicationContext(); - @Mock private PreferenceScreen mMockPreferenceScreen; + @Mock private PreferenceScreen mMockPreferenceScreen; @Mock private SettingsMainSwitchPreference mMockSwitchPreference; @@ -74,9 +79,13 @@ public class ContentProtectionTogglePreferenceControllerTest { private TestContentProtectionTogglePreferenceController mController; private int mSettingBackupValue; + private ShadowUserManager mShadowUserManager; + @Before public void setUp() { + mShadowUserManager = ShadowUserManager.getShadow(); + mShadowUserManager.setGuestUser(false); mController = new TestContentProtectionTogglePreferenceController(); SettingsMainSwitchPreference switchPreference = new SettingsMainSwitchPreference(mContext); when(mMockPreferenceScreen.findPreference(mController.getPreferenceKey())) @@ -225,6 +234,7 @@ public class ContentProtectionTogglePreferenceControllerTest { assertThat(mController.mCounterGetEnforcedAdmin).isEqualTo(1); verify(mMockSwitchPreference, never()).setDisabledByAdmin(any()); + verify(mMockSwitchPreference, never()).setEnabled(false); } @Test @@ -237,6 +247,7 @@ public class ContentProtectionTogglePreferenceControllerTest { assertThat(mController.mCounterGetEnforcedAdmin).isEqualTo(1); verify(mMockSwitchPreference).setDisabledByAdmin(mEnforcedAdmin); + verify(mMockSwitchPreference, never()).setEnabled(false); } @Test @@ -249,6 +260,7 @@ public class ContentProtectionTogglePreferenceControllerTest { assertThat(mController.mCounterGetEnforcedAdmin).isEqualTo(1); verify(mMockSwitchPreference, never()).setDisabledByAdmin(any()); + verify(mMockSwitchPreference, never()).setEnabled(false); } @Test @@ -261,6 +273,30 @@ public class ContentProtectionTogglePreferenceControllerTest { assertThat(mController.mCounterGetEnforcedAdmin).isEqualTo(1); verify(mMockSwitchPreference, never()).setDisabledByAdmin(any()); + verify(mMockSwitchPreference, never()).setEnabled(false); + } + + @Test + public void updateState_flagEnabled_noEnforcedAdmin_guestUser_switchBarDisabled() { + mShadowUserManager.setGuestUser(true); + mSetFlagsRule.enableFlags(FLAG_MANAGE_DEVICE_POLICY_ENABLED); + mContentProtectionPolicy = DevicePolicyManager.CONTENT_PROTECTION_ENABLED; + setupForUpdateState(); + + mController.updateState(mMockSwitchPreference); + + verify(mMockSwitchPreference).setEnabled(false); + } + + @Test + public void updateState_flagEnabled_noEnforcedAdmin_nonGuestUser_switchBarEnabled() { + mSetFlagsRule.enableFlags(FLAG_MANAGE_DEVICE_POLICY_ENABLED); + mContentProtectionPolicy = DevicePolicyManager.CONTENT_PROTECTION_ENABLED; + setupForUpdateState(); + + mController.updateState(mMockSwitchPreference); + + verify(mMockSwitchPreference, never()).setEnabled(false); } @Test @@ -273,6 +309,7 @@ public class ContentProtectionTogglePreferenceControllerTest { assertThat(mController.mCounterGetEnforcedAdmin).isEqualTo(1); verify(mMockSwitchPreference, never()).setDisabledByAdmin(any()); + verify(mMockSwitchPreference, never()).setEnabled(false); } @Test @@ -286,6 +323,7 @@ public class ContentProtectionTogglePreferenceControllerTest { assertThat(mController.mCounterGetEnforcedAdmin).isEqualTo(1); verify(mMockSwitchPreference).setDisabledByAdmin(mEnforcedAdmin); + verify(mMockSwitchPreference, never()).setEnabled(false); } @Test @@ -299,6 +337,7 @@ public class ContentProtectionTogglePreferenceControllerTest { assertThat(mController.mCounterGetEnforcedAdmin).isEqualTo(1); verify(mMockSwitchPreference).setDisabledByAdmin(mEnforcedAdmin); + verify(mMockSwitchPreference, never()).setEnabled(false); } @Test @@ -312,6 +351,7 @@ public class ContentProtectionTogglePreferenceControllerTest { assertThat(mController.mCounterGetEnforcedAdmin).isEqualTo(1); verify(mMockSwitchPreference, never()).setDisabledByAdmin(any()); + verify(mMockSwitchPreference, never()).setEnabled(false); } @Test From a38de2654daf30e9bd239e6323e42a6ff113d1fb Mon Sep 17 00:00:00 2001 From: Jack Yu Date: Tue, 18 Feb 2025 13:31:50 -0800 Subject: [PATCH 02/14] Cleaned up the flag enforce_telephony_feature_mapping_for_public_apis Cleaned up the 24Q3 flag enforce_telephony_feature_mapping_for_public_apis Test: Basic telephony functionality tests Test: atest FrameworksTelephonyTests Bug: 297989574 Flag: EXEMPT flag cleanup Change-Id: Iefb9a02eebe06fb42a23d645e6be034c94508487 --- .../android/settings/AirplaneModeEnabler.java | 27 +++++-------------- ...gAidCompatibilityPreferenceController.java | 14 +++------- ...nabledNetworkModePreferenceController.java | 12 +++------ .../Enhanced4gBasePreferenceController.java | 13 +++------ 4 files changed, 19 insertions(+), 47 deletions(-) diff --git a/src/com/android/settings/AirplaneModeEnabler.java b/src/com/android/settings/AirplaneModeEnabler.java index c0d9ffce4d3..7ece8a3861e 100644 --- a/src/com/android/settings/AirplaneModeEnabler.java +++ b/src/com/android/settings/AirplaneModeEnabler.java @@ -29,7 +29,6 @@ import android.util.Log; import androidx.annotation.VisibleForTesting; -import com.android.internal.telephony.flags.Flags; import com.android.settings.network.GlobalSettingsChangeListener; import com.android.settings.network.ProxySubscriptionManager; import com.android.settings.overlay.FeatureFactory; @@ -162,19 +161,13 @@ public class AirplaneModeEnabler extends GlobalSettingsChangeListener { if (context == null || telephonyManager == null) { return false; } - if (Flags.enforceTelephonyFeatureMappingForPublicApis()) { - try { - if (telephonyManager.getEmergencyCallbackMode()) { - return true; - } - } catch (UnsupportedOperationException e) { - // Device doesn't support FEATURE_TELEPHONY_CALLING - // Ignore exception, device is not in ECM mode. - } - } else { + try { if (telephonyManager.getEmergencyCallbackMode()) { return true; } + } catch (UnsupportedOperationException e) { + // Device doesn't support FEATURE_TELEPHONY_CALLING + // Ignore exception, device is not in ECM mode. } final List subInfoList = ProxySubscriptionManager.getInstance(context).getActiveSubscriptionsInfo(); @@ -185,18 +178,12 @@ public class AirplaneModeEnabler extends GlobalSettingsChangeListener { final TelephonyManager telephonyManagerForSubId = telephonyManager.createForSubscriptionId(subInfo.getSubscriptionId()); if (telephonyManagerForSubId != null) { - if (!Flags.enforceTelephonyFeatureMappingForPublicApis()) { + try { if (telephonyManagerForSubId.getEmergencyCallbackMode()) { return true; } - } else { - try { - if (telephonyManagerForSubId.getEmergencyCallbackMode()) { - return true; - } - } catch (UnsupportedOperationException e) { - // Ignore exception, device is not in ECM mode. - } + } catch (UnsupportedOperationException e) { + // Ignore exception, device is not in ECM mode. } } } diff --git a/src/com/android/settings/accessibility/HearingAidCompatibilityPreferenceController.java b/src/com/android/settings/accessibility/HearingAidCompatibilityPreferenceController.java index 821fddd956f..ad962a553aa 100644 --- a/src/com/android/settings/accessibility/HearingAidCompatibilityPreferenceController.java +++ b/src/com/android/settings/accessibility/HearingAidCompatibilityPreferenceController.java @@ -22,7 +22,6 @@ import android.provider.Settings; import android.telephony.TelephonyManager; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.telephony.flags.Flags; import com.android.settings.R; import com.android.settings.core.TogglePreferenceController; import com.android.settings.overlay.FeatureFactory; @@ -51,17 +50,12 @@ public class HearingAidCompatibilityPreferenceController extends TogglePreferenc @Override public int getAvailabilityStatus() { - if (Flags.enforceTelephonyFeatureMappingForPublicApis()) { - try { - return mTelephonyManager.isHearingAidCompatibilitySupported() ? AVAILABLE - : UNSUPPORTED_ON_DEVICE; - } catch (UnsupportedOperationException e) { - // Device doesn't support FEATURE_TELEPHONY_CALLING - return UNSUPPORTED_ON_DEVICE; - } - } else { + try { return mTelephonyManager.isHearingAidCompatibilitySupported() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } catch (UnsupportedOperationException e) { + // Device doesn't support FEATURE_TELEPHONY_CALLING + return UNSUPPORTED_ON_DEVICE; } } diff --git a/src/com/android/settings/network/telephony/EnabledNetworkModePreferenceController.java b/src/com/android/settings/network/telephony/EnabledNetworkModePreferenceController.java index 8051711ddd4..af83c8934fa 100644 --- a/src/com/android/settings/network/telephony/EnabledNetworkModePreferenceController.java +++ b/src/com/android/settings/network/telephony/EnabledNetworkModePreferenceController.java @@ -911,15 +911,11 @@ public class EnabledNetworkModePreferenceController extends // assign current call state so that it helps to show correct preference state even // before first onCallStateChanged() by initial registration. - if (Flags.enforceTelephonyFeatureMappingForPublicApis()) { - try { - mCallState = mTelephonyManager.getCallState(subId); - } catch (UnsupportedOperationException e) { - // Device doesn't support FEATURE_TELEPHONY_CALLING - mCallState = TelephonyManager.CALL_STATE_IDLE; - } - } else { + try { mCallState = mTelephonyManager.getCallState(subId); + } catch (UnsupportedOperationException e) { + // Device doesn't support FEATURE_TELEPHONY_CALLING + mCallState = TelephonyManager.CALL_STATE_IDLE; } mTelephonyManager.registerTelephonyCallback( mContext.getMainExecutor(), mTelephonyCallback); diff --git a/src/com/android/settings/network/telephony/Enhanced4gBasePreferenceController.java b/src/com/android/settings/network/telephony/Enhanced4gBasePreferenceController.java index 615351d26c9..8e63e100d49 100644 --- a/src/com/android/settings/network/telephony/Enhanced4gBasePreferenceController.java +++ b/src/com/android/settings/network/telephony/Enhanced4gBasePreferenceController.java @@ -33,7 +33,6 @@ import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import androidx.preference.TwoStatePreference; -import com.android.internal.telephony.flags.Flags; import com.android.settings.R; import com.android.settings.network.ims.VolteQueryImsState; import com.android.settingslib.core.lifecycle.LifecycleObserver; @@ -233,15 +232,11 @@ public class Enhanced4gBasePreferenceController extends TelephonyTogglePreferenc } // assign current call state so that it helps to show correct preference state even // before first onCallStateChanged() by initial registration. - if (Flags.enforceTelephonyFeatureMappingForPublicApis()) { - try { - mCallState = mTelephonyManager.getCallState(subId); - } catch (UnsupportedOperationException e) { - // Device doesn't support FEATURE_TELEPHONY_CALLING - mCallState = TelephonyManager.CALL_STATE_IDLE; - } - } else { + try { mCallState = mTelephonyManager.getCallState(subId); + } catch (UnsupportedOperationException e) { + // Device doesn't support FEATURE_TELEPHONY_CALLING + mCallState = TelephonyManager.CALL_STATE_IDLE; } mTelephonyManager.registerTelephonyCallback( mContext.getMainExecutor(), mTelephonyCallback); From 602cec999b5ac2efa723c9320dd9c4d3b27a185d Mon Sep 17 00:00:00 2001 From: Pawan Wagh Date: Fri, 21 Feb 2025 01:24:42 +0000 Subject: [PATCH 03/14] Use SwitchPreferenceCompat for UI consistency Test: Install Settings and Check UI Bug: N/A Change-Id: Idf17d4d7616b6d45403e1c19857f5a33b27b04c2 --- res/xml/development_settings.xml | 5 ++--- .../development/Enable16kPagesPreferenceController.java | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index 2beb96d8042..294f9025a64 100644 --- a/res/xml/development_settings.xml +++ b/res/xml/development_settings.xml @@ -114,11 +114,10 @@ android:summary="@string/oem_unlock_enable_summary" settings:useAdditionalSummary="true" /> - + android:summary="@string/enable_16k_pages_summary"/> Date: Fri, 31 Jan 2025 22:48:06 +0000 Subject: [PATCH 04/14] Display: refactor Color Mode settings 1. Refactor Color Mode by moving color summary functionality to the ColorModeUtils class. 2. Migrated `ColorModeUtils` from Java to Kotlin. 3. Changed ColorModePreferenceControllerTest according to changes Bug: 390644464 Flag: EXEMPT refactoring Test: atest com.android.settings.display Test: atest -c packages/apps/Settings/tests/robotests/src/com/android/settings/display/colors/ColorModePreferenceControllerTest.kt Test: atest -c packages/apps/Settings/tests/unit/src/com/android/settings/display/ColorModePreferenceFragmentTest.java Change-Id: I55ac6129b93e4e35bd58f0313215b711ce954c0a --- .../ColorModePreferenceController.java | 23 +++-- .../display/ColorModePreferenceFragment.java | 7 +- .../settings/display/ColorModeUtils.java | 62 ------------ .../settings/display/ColorModeUtils.kt | 65 +++++++++++++ .../ColorModePreferenceControllerTest.kt | 83 ++++++++++++++++ .../ColorModePreferenceControllerTest.java | 97 ------------------- .../ColorModePreferenceFragmentTest.java | 4 + 7 files changed, 172 insertions(+), 169 deletions(-) delete mode 100644 src/com/android/settings/display/ColorModeUtils.java create mode 100644 src/com/android/settings/display/ColorModeUtils.kt create mode 100644 tests/robotests/src/com/android/settings/display/colors/ColorModePreferenceControllerTest.kt delete mode 100644 tests/unit/src/com/android/settings/display/ColorModePreferenceControllerTest.java diff --git a/src/com/android/settings/display/ColorModePreferenceController.java b/src/com/android/settings/display/ColorModePreferenceController.java index 6cd4867b588..04cd2c0f434 100644 --- a/src/com/android/settings/display/ColorModePreferenceController.java +++ b/src/com/android/settings/display/ColorModePreferenceController.java @@ -16,13 +16,15 @@ package com.android.settings.display; import android.content.Context; import android.hardware.display.ColorDisplayManager; -import androidx.annotation.VisibleForTesting; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; import com.android.settings.core.BasePreferenceController; public class ColorModePreferenceController extends BasePreferenceController { - public ColorModePreferenceController(Context context, String key) { + public ColorModePreferenceController(@NonNull Context context, @NonNull String key) { super(context, key); } @@ -36,11 +38,20 @@ public class ColorModePreferenceController extends BasePreferenceController { @Override public CharSequence getSummary() { - return ColorModeUtils.getColorModeMapping(mContext.getResources()).get(getColorMode()); + return getColorModeName(); } - @VisibleForTesting - public int getColorMode() { - return mContext.getSystemService(ColorDisplayManager.class).getColorMode(); + @Override + public void updateState(@Nullable Preference preference) { + if (preference == null) { + return; + } + super.updateState(preference); + preference.setSummary(getSummary()); + } + + @NonNull + private String getColorModeName() { + return ColorModeUtils.getActiveColorModeName(mContext); } } diff --git a/src/com/android/settings/display/ColorModePreferenceFragment.java b/src/com/android/settings/display/ColorModePreferenceFragment.java index ce33e673fde..2318d08d8a8 100644 --- a/src/com/android/settings/display/ColorModePreferenceFragment.java +++ b/src/com/android/settings/display/ColorModePreferenceFragment.java @@ -213,8 +213,7 @@ public class ColorModePreferenceFragment extends RadioButtonPickerFragment { final Map colorModesToSummaries = ColorModeUtils.getColorModeMapping(mResources); final List candidates = new ArrayList<>(); - for (int colorMode : mResources.getIntArray( - com.android.internal.R.array.config_availableColorModes)) { + for (int colorMode : ColorModeUtils.getAvailableColorModes(getContext())) { candidates.add(new ColorModeCandidateInfo( colorModesToSummaries.get(colorMode), getKeyForColorMode(colorMode), @@ -390,8 +389,8 @@ public class ColorModePreferenceFragment extends RadioButtonPickerFragment { @Override protected boolean isPageSearchEnabled(Context context) { - final int[] availableColorModes = context.getResources().getIntArray( - com.android.internal.R.array.config_availableColorModes); + final int[] availableColorModes = + ColorModeUtils.getAvailableColorModes(context); return availableColorModes != null && availableColorModes.length > 0 && !ColorDisplayManager.areAccessibilityTransformsEnabled(context); } diff --git a/src/com/android/settings/display/ColorModeUtils.java b/src/com/android/settings/display/ColorModeUtils.java deleted file mode 100644 index cdd978e4bbc..00000000000 --- a/src/com/android/settings/display/ColorModeUtils.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.display; - -import static android.hardware.display.ColorDisplayManager.COLOR_MODE_AUTOMATIC; -import static android.hardware.display.ColorDisplayManager.COLOR_MODE_BOOSTED; -import static android.hardware.display.ColorDisplayManager.COLOR_MODE_NATURAL; -import static android.hardware.display.ColorDisplayManager.COLOR_MODE_SATURATED; -import static android.hardware.display.ColorDisplayManager.VENDOR_COLOR_MODE_RANGE_MAX; -import static android.hardware.display.ColorDisplayManager.VENDOR_COLOR_MODE_RANGE_MIN; - -import android.content.res.Resources; -import android.util.ArrayMap; - -import com.android.settings.R; - -import java.util.Map; - -final class ColorModeUtils { - - private ColorModeUtils() { - // Do not instantiate. - } - - static Map getColorModeMapping(Resources resources) { - final String[] colorModeOptionsStrings = resources.getStringArray( - R.array.config_color_mode_options_strings); - final int[] colorModeOptionsValues = resources.getIntArray( - R.array.config_color_mode_options_values); - if (colorModeOptionsStrings.length != colorModeOptionsValues.length) { - throw new RuntimeException("Color mode options of unequal length"); - } - - final Map colorModesToSummaries = new ArrayMap<>(); - for (int i = 0; i < colorModeOptionsValues.length; i++) { - final int colorMode = colorModeOptionsValues[i]; - if (colorMode == COLOR_MODE_NATURAL - || colorMode == COLOR_MODE_BOOSTED - || colorMode == COLOR_MODE_SATURATED - || colorMode == COLOR_MODE_AUTOMATIC - || (colorMode >= VENDOR_COLOR_MODE_RANGE_MIN - && colorMode <= VENDOR_COLOR_MODE_RANGE_MAX)) { - colorModesToSummaries.put(colorMode, colorModeOptionsStrings[i]); - } - } - return colorModesToSummaries; - } -} diff --git a/src/com/android/settings/display/ColorModeUtils.kt b/src/com/android/settings/display/ColorModeUtils.kt new file mode 100644 index 00000000000..01a6a50c0fe --- /dev/null +++ b/src/com/android/settings/display/ColorModeUtils.kt @@ -0,0 +1,65 @@ +/* + * 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.display + +import android.content.Context +import android.content.res.Resources +import android.hardware.display.ColorDisplayManager +import android.hardware.display.ColorDisplayManager.* +import android.util.Log + +import com.android.settings.R + +object ColorModeUtils { + + private val TAG = "ColorModeUtils" + + @JvmStatic + fun getColorModeMapping(resources: Resources): Map { + val colorModeOptionsStrings = resources.getStringArray( + R.array.config_color_mode_options_strings + ) + val colorModeOptionsValues = resources.getIntArray( + R.array.config_color_mode_options_values + ) + if (colorModeOptionsStrings.size!= colorModeOptionsValues.size) { + throw RuntimeException("Color mode options of unequal length") + } + + val colorModesToSummaries = colorModeOptionsValues.zip(colorModeOptionsStrings).toMap().filterKeys { colorMode -> + colorMode == COLOR_MODE_NATURAL || + colorMode == COLOR_MODE_BOOSTED || + colorMode == COLOR_MODE_SATURATED || + colorMode == COLOR_MODE_AUTOMATIC || + (colorMode >= VENDOR_COLOR_MODE_RANGE_MIN && + colorMode <= VENDOR_COLOR_MODE_RANGE_MAX) + } + + return colorModesToSummaries + } + + @JvmStatic + fun getColorMode(context: Context): Int = + context.getSystemService(ColorDisplayManager::class.java).colorMode + + @JvmStatic + fun getActiveColorModeName(context: Context): String = + getColorModeMapping(context.resources)[getColorMode(context)] ?: "" + + @JvmStatic + fun getAvailableColorModes(context: Context): IntArray = + context.getResources().getIntArray(com.android.internal.R.array.config_availableColorModes) +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/display/colors/ColorModePreferenceControllerTest.kt b/tests/robotests/src/com/android/settings/display/colors/ColorModePreferenceControllerTest.kt new file mode 100644 index 00000000000..eee87ac00e9 --- /dev/null +++ b/tests/robotests/src/com/android/settings/display/colors/ColorModePreferenceControllerTest.kt @@ -0,0 +1,83 @@ +/* + * 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.display + +import android.content.Context +import android.hardware.display.ColorDisplayManager + +import androidx.preference.Preference +import androidx.test.core.app.ApplicationProvider + +import com.android.settingslib.testutils.shadow.ShadowColorDisplayManager +import com.android.settings.R +import com.google.common.truth.Truth.assertThat + +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.shadow.api.Shadow + +@RunWith(RobolectricTestRunner::class) +@Config(shadows = [ShadowColorDisplayManager::class]) +class ColorModePreferenceControllerTest { + private lateinit var context: Context + private lateinit var preference: Preference + private lateinit var controller: ColorModePreferenceController + private lateinit var shadowColorDisplayManager: ShadowColorDisplayManager + + @Before + fun setup() { + context = ApplicationProvider.getApplicationContext() + controller = ColorModePreferenceController(context, "test") + preference = Preference(context) + shadowColorDisplayManager = Shadow.extract( + context.getSystemService(ColorDisplayManager::class.java)); + } + + @Test + fun updateState_colorModeAutomatic_shouldSetSummaryToAutomatic() { + shadowColorDisplayManager.setColorMode(ColorDisplayManager.COLOR_MODE_AUTOMATIC) + controller.updateState(preference) + val automaticColorModeName = context.getString(R.string.color_mode_option_automatic) + assertThat(preference.summary.toString()).isEqualTo(automaticColorModeName) + } + + @Test + fun updateState_colorModeSaturated_shouldSetSummaryToSaturated() { + shadowColorDisplayManager.setColorMode(ColorDisplayManager.COLOR_MODE_SATURATED) + controller.updateState(preference) + val saturatedColorModeName = context.getString(R.string.color_mode_option_saturated) + assertThat(preference.summary.toString()).isEqualTo(saturatedColorModeName) + } + + @Test + fun updateState_colorModeBoosted_shouldSetSummaryToBoosted() { + shadowColorDisplayManager.setColorMode(ColorDisplayManager.COLOR_MODE_BOOSTED) + controller.updateState(preference) + val boostedColorModeName = context.getString(R.string.color_mode_option_boosted) + assertThat(preference.summary.toString()).isEqualTo(boostedColorModeName) + } + + @Test + fun updateState_colorModeNatural_shouldSetSummaryToNatural() { + shadowColorDisplayManager.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL) + controller.updateState(preference) + val naturalColorModeName = context.getString(R.string.color_mode_option_natural) + assertThat(preference.summary.toString()).isEqualTo(naturalColorModeName) + } +} \ No newline at end of file diff --git a/tests/unit/src/com/android/settings/display/ColorModePreferenceControllerTest.java b/tests/unit/src/com/android/settings/display/ColorModePreferenceControllerTest.java deleted file mode 100644 index e2f77fd5389..00000000000 --- a/tests/unit/src/com/android/settings/display/ColorModePreferenceControllerTest.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.display; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.res.Resources; -import android.hardware.display.ColorDisplayManager; - -import androidx.preference.Preference; -import androidx.test.annotation.UiThreadTest; -import androidx.test.core.app.ApplicationProvider; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -public class ColorModePreferenceControllerTest { - - private Preference mPreference; - private ColorModePreferenceController mController; - - @Before - public void setup() { - final Context context = spy(ApplicationProvider.getApplicationContext()); - mController = spy(new ColorModePreferenceController(context, "test")); - mPreference = new Preference(context); - final Resources res = spy(context.getResources()); - when(res.getIntArray(com.android.internal.R.array.config_availableColorModes)).thenReturn( - new int[]{ - ColorDisplayManager.COLOR_MODE_NATURAL, - ColorDisplayManager.COLOR_MODE_BOOSTED, - ColorDisplayManager.COLOR_MODE_SATURATED, - ColorDisplayManager.COLOR_MODE_AUTOMATIC - }); - doReturn(res).when(context).getResources(); - } - - @Test - @UiThreadTest - public void updateState_colorModeAutomatic_shouldSetSummaryToAutomatic() { - doReturn(ColorDisplayManager.COLOR_MODE_AUTOMATIC).when(mController).getColorMode(); - - mController.updateState(mPreference); - - assertThat(mPreference.getSummary()).isEqualTo("Adaptive"); - } - - @Test - @UiThreadTest - public void updateState_colorModeSaturated_shouldSetSummaryToSaturated() { - doReturn(ColorDisplayManager.COLOR_MODE_SATURATED).when(mController).getColorMode(); - - mController.updateState(mPreference); - - assertThat(mPreference.getSummary()).isEqualTo("Saturated"); - } - - @Test - public void updateState_colorModeBoosted_shouldSetSummaryToBoosted() { - doReturn(ColorDisplayManager.COLOR_MODE_BOOSTED).when(mController).getColorMode(); - - mController.updateState(mPreference); - - assertThat(mPreference.getSummary()).isEqualTo("Boosted"); - } - - @Test - public void updateState_colorModeNatural_shouldSetSummaryToNatural() { - doReturn(ColorDisplayManager.COLOR_MODE_NATURAL).when(mController).getColorMode(); - - mController.updateState(mPreference); - - assertThat(mPreference.getSummary()).isEqualTo("Natural"); - } -} diff --git a/tests/unit/src/com/android/settings/display/ColorModePreferenceFragmentTest.java b/tests/unit/src/com/android/settings/display/ColorModePreferenceFragmentTest.java index 450525c9339..b3a2770baf8 100644 --- a/tests/unit/src/com/android/settings/display/ColorModePreferenceFragmentTest.java +++ b/tests/unit/src/com/android/settings/display/ColorModePreferenceFragmentTest.java @@ -76,6 +76,7 @@ public class ColorModePreferenceFragmentTest { }); doReturn(res).when(mContext).getResources(); mFragment.onAttach(mContext); + doReturn(mContext).when(mFragment).getContext(); final List candidates = mFragment.getCandidates(); @@ -99,6 +100,7 @@ public class ColorModePreferenceFragmentTest { }); doReturn(res).when(mContext).getResources(); mFragment.onAttach(mContext); + doReturn(mContext).when(mFragment).getContext(); List candidates = mFragment.getCandidates(); @@ -116,6 +118,7 @@ public class ColorModePreferenceFragmentTest { }); doReturn(res).when(mContext).getResources(); mFragment.onAttach(mContext); + doReturn(mContext).when(mFragment).getContext(); List candidates = mFragment.getCandidates(); @@ -138,6 +141,7 @@ public class ColorModePreferenceFragmentTest { }); doReturn(res).when(mContext).getResources(); mFragment.onAttach(mContext); + doReturn(mContext).when(mFragment).getContext(); List candidates = mFragment.getCandidates(); From cc96950d120b8259aa1ea03b7d552db3d79cac64 Mon Sep 17 00:00:00 2001 From: Vadym Omelnytskyi Date: Thu, 20 Feb 2025 01:49:48 +0000 Subject: [PATCH 05/14] Display: make Colors settings entry preference reactive Added `display_color_mode` listener to Colors preference. As a result, it becomes reactive and updates its color mode value summary. Flag: EXEMPT minor change Bug: 397659800 Test: changed color mode using `adb` commands and verify that Colors summary reacts and print correct color mode Change-Id: I963768e3dbb43b547ec53e6445b2791ec0f57cff --- .../ColorModePreferenceController.java | 45 +++++++++++++- .../ColorModePreferenceControllerTest.kt | 60 ++++++++++++++++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/display/ColorModePreferenceController.java b/src/com/android/settings/display/ColorModePreferenceController.java index 04cd2c0f434..2fa3452528b 100644 --- a/src/com/android/settings/display/ColorModePreferenceController.java +++ b/src/com/android/settings/display/ColorModePreferenceController.java @@ -14,15 +14,37 @@ package com.android.settings.display; import android.content.Context; +import android.database.ContentObserver; import android.hardware.display.ColorDisplayManager; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; import com.android.settings.core.BasePreferenceController; -public class ColorModePreferenceController extends BasePreferenceController { +public class ColorModePreferenceController extends BasePreferenceController + implements LifecycleObserver { + + private Preference mPreference; + + private final ContentObserver mContentObserver = new ContentObserver( + new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange, @Nullable Uri uri) { + if (mPreference != null) { + updateState(mPreference); + } + } + }; public ColorModePreferenceController(@NonNull Context context, @NonNull String key) { super(context, key); @@ -36,11 +58,32 @@ public class ColorModePreferenceController extends BasePreferenceController { AVAILABLE : DISABLED_FOR_USER; } + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void onResume() { + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.DISPLAY_COLOR_MODE), + /* notifyForDescendants= */ false, + mContentObserver); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + public void onPause() { + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + } + @Override public CharSequence getSummary() { return getColorModeName(); } + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + if (mPreference != null) { + updateState(mPreference); + } + } + @Override public void updateState(@Nullable Preference preference) { if (preference == null) { diff --git a/tests/robotests/src/com/android/settings/display/colors/ColorModePreferenceControllerTest.kt b/tests/robotests/src/com/android/settings/display/colors/ColorModePreferenceControllerTest.kt index eee87ac00e9..4371bae4910 100644 --- a/tests/robotests/src/com/android/settings/display/colors/ColorModePreferenceControllerTest.kt +++ b/tests/robotests/src/com/android/settings/display/colors/ColorModePreferenceControllerTest.kt @@ -15,10 +15,15 @@ */ package com.android.settings.display +import android.content.ContentResolver import android.content.Context +import android.database.ContentObserver import android.hardware.display.ColorDisplayManager +import android.provider.Settings import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import androidx.preference.PreferenceManager import androidx.test.core.app.ApplicationProvider import com.android.settingslib.testutils.shadow.ShadowColorDisplayManager @@ -31,22 +36,34 @@ import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import org.robolectric.shadow.api.Shadow +import org.robolectric.shadows.ShadowContentResolver @RunWith(RobolectricTestRunner::class) -@Config(shadows = [ShadowColorDisplayManager::class]) +@Config(shadows = [ShadowColorDisplayManager::class, ShadowContentResolver::class]) class ColorModePreferenceControllerTest { private lateinit var context: Context private lateinit var preference: Preference private lateinit var controller: ColorModePreferenceController private lateinit var shadowColorDisplayManager: ShadowColorDisplayManager + private lateinit var shadowContentResolver: ShadowContentResolver @Before fun setup() { context = ApplicationProvider.getApplicationContext() + controller = ColorModePreferenceController(context, "test") preference = Preference(context) + val preferenceManager = PreferenceManager(context) + val preferenceScreen = preferenceManager.createPreferenceScreen(context) + preference.setKey(controller.getPreferenceKey()); + preferenceScreen.addPreference(preference) + shadowColorDisplayManager = Shadow.extract( - context.getSystemService(ColorDisplayManager::class.java)); + context.getSystemService(ColorDisplayManager::class.java)) + val contentResolver = context.getContentResolver(); + shadowContentResolver = Shadow.extract(contentResolver) + + controller.displayPreference(preferenceScreen) } @Test @@ -80,4 +97,43 @@ class ColorModePreferenceControllerTest { val naturalColorModeName = context.getString(R.string.color_mode_option_natural) assertThat(preference.summary.toString()).isEqualTo(naturalColorModeName) } + + @Test + fun onResume_verifyRegisterColorModeObserver() { + controller.onResume() + assertThat(shadowContentResolver.getContentObservers( + Settings.System.getUriFor(Settings.System.DISPLAY_COLOR_MODE))) + .hasSize(1) + } + + @Test + fun onPause_verifyUnregisterColorModeObserver() { + controller.onResume() + controller.onPause() + assertThat(shadowContentResolver.getContentObservers( + Settings.System.getUriFor(Settings.System.DISPLAY_COLOR_MODE))) + .isEmpty() + } + + @Test + fun contentObserver_onChange_updatesPreferenceSummary() { + controller.onResume() + assertThat(shadowContentResolver.getContentObservers( + Settings.System.getUriFor(Settings.System.DISPLAY_COLOR_MODE))) + .hasSize(1) + + shadowColorDisplayManager.setColorMode(ColorDisplayManager.COLOR_MODE_NATURAL) + triggerOnChangeListener() + assertThat(preference.summary).isEqualTo(context.getString(R.string.color_mode_option_natural)) + + shadowColorDisplayManager.setColorMode(ColorDisplayManager.COLOR_MODE_AUTOMATIC) + triggerOnChangeListener() + assertThat(preference.summary).isEqualTo(context.getString(R.string.color_mode_option_automatic)) + } + + private fun triggerOnChangeListener() { + shadowContentResolver.getContentObservers( + Settings.System.getUriFor(Settings.System.DISPLAY_COLOR_MODE)) + .forEach {it.onChange(false, null)}; + } } \ No newline at end of file From 3a8f193bb516c3255e3bffee3c0011719dd27e67 Mon Sep 17 00:00:00 2001 From: Omer Ozer Date: Fri, 21 Feb 2025 20:02:27 +0000 Subject: [PATCH 06/14] FactoryResetPreferenceController return null when permission isn't grandted. Flag: EXEMPT bugfix Bug: 396580123 Test: manual Change-Id: Id68dcebc600de21fddf8801757dfb56b92a1a773 --- .../FactoryResetPreferenceController.java | 2 +- .../FactoryResetPreferenceControllerTest.java | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/system/FactoryResetPreferenceController.java b/src/com/android/settings/system/FactoryResetPreferenceController.java index 54c97a389b8..d80e926c3c6 100644 --- a/src/com/android/settings/system/FactoryResetPreferenceController.java +++ b/src/com/android/settings/system/FactoryResetPreferenceController.java @@ -111,7 +111,7 @@ public class FactoryResetPreferenceController extends BasePreferenceController { return prepareFactoryResetWizardRequest; } } - return prepareFactoryResetWizardRequest; + return null; } Log.i(TAG, "Unable to resolve a Factory Reset Handler Activity"); return null; diff --git a/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java index 383ed94b7db..8eb02b332ae 100644 --- a/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java @@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.Manifest; @@ -157,4 +158,21 @@ public class FactoryResetPreferenceControllerTest { assertThat(intentArgumentCaptor.getValue().getAction()) .isEqualTo(FactoryResetPreferenceController.ACTION_PREPARE_FACTORY_RESET); } + + @Test + @RequiresFlagsEnabled(com.android.settings.factory_reset.Flags.FLAG_ENABLE_FACTORY_RESET_WIZARD) + public void handlePreference_factoryResetWizardEnabled_noExistingFrwApp() + throws PackageManager.NameNotFoundException { + PackageInfo info = new PackageInfo(); + info.requestedPermissions = + new String[] {Manifest.permission.PREPARE_FACTORY_RESET}; + info.requestedPermissionsFlags = new int[] {0}; + when(mPackageManager.getPackageInfo(anyString(), anyInt())) + .thenReturn(info); + + assertThat(mController.handlePreferenceTreeClick(mPreference)).isTrue(); + verify(mPackageManager).getPackageInfo(eq(FACTORY_RESET_APP_PACKAGE), + eq(PackageManager.GET_PERMISSIONS)); + verifyNoMoreInteractions(mFactoryResetLauncher); + } } From ae280f8721c939e53c80c158f94d39984e7e7ba0 Mon Sep 17 00:00:00 2001 From: Jacky Wang Date: Tue, 25 Feb 2025 09:01:43 +0800 Subject: [PATCH 07/14] [Catalyst] Lock screen summary is not updated In original implementation, DashboardFragment.onResume calls AbstractPreferenceController.updateState to refresh preference. While after Catalyst migration, it requires explicit update when external dependency is changed. Bug: 397798327 Flag: com.android.settings.flags.catalyst_lockscreen_from_display_settings Test: adb shell settings put secure lock_screen_show_notifications/lock_screen_allow_private_notifications Change-Id: I1c16d7df62cdb1cb53d365f2636aa1dc0a424839 --- .../security/LockScreenPreferenceScreen.kt | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/security/LockScreenPreferenceScreen.kt b/src/com/android/settings/security/LockScreenPreferenceScreen.kt index 9a3226abe1a..8aead19cbb7 100644 --- a/src/com/android/settings/security/LockScreenPreferenceScreen.kt +++ b/src/com/android/settings/security/LockScreenPreferenceScreen.kt @@ -16,12 +16,19 @@ package com.android.settings.security import android.content.Context +import android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS +import android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS import com.android.settings.R import com.android.settings.Settings.LockScreenSettingsActivity import com.android.settings.display.AmbientDisplayAlwaysOnPreference import com.android.settings.flags.Flags import com.android.settings.notification.LockScreenNotificationPreferenceController import com.android.settings.utils.makeLaunchIntent +import com.android.settingslib.datastore.AbstractKeyedDataObservable +import com.android.settingslib.datastore.HandlerExecutor +import com.android.settingslib.datastore.KeyedObserver +import com.android.settingslib.datastore.SettingsSecureStore +import com.android.settingslib.metadata.PreferenceChangeReason import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.PreferenceSummaryProvider import com.android.settingslib.metadata.ProvidePreferenceScreen @@ -29,7 +36,12 @@ import com.android.settingslib.metadata.preferenceHierarchy import com.android.settingslib.preference.PreferenceScreenCreator @ProvidePreferenceScreen(LockScreenPreferenceScreen.KEY) -open class LockScreenPreferenceScreen : PreferenceScreenCreator, PreferenceSummaryProvider { +open class LockScreenPreferenceScreen(private val context: Context) : + AbstractKeyedDataObservable(), PreferenceScreenCreator, PreferenceSummaryProvider { + + private val observer = + KeyedObserver { _, _ -> notifyChange(KEY, PreferenceChangeReason.STATE) } + override val key: String get() = KEY @@ -39,6 +51,20 @@ open class LockScreenPreferenceScreen : PreferenceScreenCreator, PreferenceSumma override val keywords: Int get() = R.string.keywords_ambient_display_screen + override fun onFirstObserverAdded() { + val store = SettingsSecureStore.get(context) + val executor = HandlerExecutor.main + // update summary when lock screen notification settings are changed + store.addObserver(LOCK_SCREEN_SHOW_NOTIFICATIONS, observer, executor) + store.addObserver(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, observer, executor) + } + + override fun onLastObserverRemoved() { + val store = SettingsSecureStore.get(context) + store.removeObserver(LOCK_SCREEN_SHOW_NOTIFICATIONS, observer) + store.removeObserver(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, observer) + } + override fun getSummary(context: Context): CharSequence? = context.getString(LockScreenNotificationPreferenceController.getSummaryResource(context)) From 6429735cb360f433c0d0f2704da5025aedfb2f91 Mon Sep 17 00:00:00 2001 From: Zoey Chen Date: Mon, 24 Feb 2025 20:09:09 -0800 Subject: [PATCH 08/14] [Settings] Fix test failure Change-Id: I06b56a378ab37f476801d0da97b68c4e3e07906c Test: atest SettingsUnitTests:com.android.settings.localepicker.LocaleDialogFragmentTest Bug: 396802384 Flag: EXEMPT bugfix --- .../localepicker/LocaleDialogFragmentTest.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/unit/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java b/tests/unit/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java index 9415d00e98b..5ae1d46e049 100644 --- a/tests/unit/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java +++ b/tests/unit/src/com/android/settings/localepicker/LocaleDialogFragmentTest.java @@ -83,9 +83,11 @@ public class LocaleDialogFragmentTest { LocaleDialogFragment.LocaleDialogController.DialogContent dialogContent = controller.getDialogContent(); - assertEquals(R.string.button_label_confirmation_of_system_locale_change, - dialogContent.mPositiveButton); - assertEquals(R.string.cancel, dialogContent.mNegativeButton); + assertEquals(ResourcesUtils.getResourcesString( + mContext, "button_label_confirmation_of_system_locale_change"), + mContext.getString(dialogContent.mPositiveButton)); + assertEquals(ResourcesUtils.getResourcesString(mContext, "cancel"), + mContext.getString(dialogContent.mNegativeButton)); } @Test @@ -98,7 +100,8 @@ public class LocaleDialogFragmentTest { LocaleDialogFragment.LocaleDialogController.DialogContent dialogContent = controller.getDialogContent(); - assertEquals(R.string.okay, dialogContent.mPositiveButton); + assertEquals(ResourcesUtils.getResourcesString(mContext, "okay"), + mContext.getString(dialogContent.mPositiveButton)); assertTrue(dialogContent.mNegativeButton == 0); } From c0a09796b89990340db720eb2605850331aa92a1 Mon Sep 17 00:00:00 2001 From: Zoey Chen Date: Fri, 14 Feb 2025 11:28:13 +0000 Subject: [PATCH 09/14] [Settings] Add test case for new LocalePicker page - suggested language Bug: 396285228 Test: atest Test: atest SettingsRoboTests:com.android.settings.localepicker.SystemLocaleSuggestedListPreferenceControllerTest Test: atest SettingsUnitTests:com.android.settings.localepicker.SystemLocaleSuggestedListPreferenceControllerTest Flag: EXEMPT refactor Change-Id: I70d731b50775bf45ae0d73e4da9ea78dfcd3826c --- ...alePickerBaseListPreferenceController.java | 11 +- ...SuggestedListPreferenceControllerTest.java | 181 ++++++++++++++++++ ...SuggestedListPreferenceControllerTest.java | 111 +++++++++++ 3 files changed, 299 insertions(+), 4 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/localepicker/SystemLocaleSuggestedListPreferenceControllerTest.java create mode 100644 tests/unit/src/com/android/settings/localepicker/SystemLocaleSuggestedListPreferenceControllerTest.java diff --git a/src/com/android/settings/localepicker/LocalePickerBaseListPreferenceController.java b/src/com/android/settings/localepicker/LocalePickerBaseListPreferenceController.java index 8bcbe313a2d..82cd2143d5c 100644 --- a/src/com/android/settings/localepicker/LocalePickerBaseListPreferenceController.java +++ b/src/com/android/settings/localepicker/LocalePickerBaseListPreferenceController.java @@ -28,6 +28,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.fragment.app.FragmentManager; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; @@ -164,7 +165,8 @@ public abstract class LocalePickerBaseListPreferenceController extends return getSortedLocaleList(searchItem); } - private void setupPreference(List localeInfoList, + @VisibleForTesting + void setupPreference(List localeInfoList, Map existingPreferences) { Log.d(TAG, "setupPreference: isNumberingMode = " + isNumberingMode()); if (isNumberingMode() && getPreferenceCategoryKey().contains(KEY_SUPPORTED)) { @@ -255,7 +257,8 @@ public abstract class LocalePickerBaseListPreferenceController extends return localeInfos; } - private void switchFragment(LocaleStore.LocaleInfo localeInfo) { + @VisibleForTesting + void switchFragment(LocaleStore.LocaleInfo localeInfo) { boolean shouldShowLocaleEditor = shouldShowLocaleEditor(localeInfo); if (shouldShowLocaleEditor) { List feedItemList = getUserLocaleList(); @@ -281,7 +284,6 @@ public abstract class LocalePickerBaseListPreferenceController extends .setArguments(extra) .launch(); } - ((Activity) mContext).finish(); } public void setFragmentManager(@NonNull FragmentManager fragmentManager) { @@ -295,7 +297,8 @@ public abstract class LocalePickerBaseListPreferenceController extends } } - private boolean shouldShowLocaleEditor(LocaleStore.LocaleInfo localeInfo) { + @VisibleForTesting + boolean shouldShowLocaleEditor(LocaleStore.LocaleInfo localeInfo) { boolean isSystemLocale = localeInfo.isSystemLocale(); boolean isRegionLocale = localeInfo.getParent() != null; boolean mayHaveDifferentNumberingSystem = localeInfo.hasNumberingSystems(); diff --git a/tests/robotests/src/com/android/settings/localepicker/SystemLocaleSuggestedListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/localepicker/SystemLocaleSuggestedListPreferenceControllerTest.java new file mode 100644 index 00000000000..968021a568f --- /dev/null +++ b/tests/robotests/src/com/android/settings/localepicker/SystemLocaleSuggestedListPreferenceControllerTest.java @@ -0,0 +1,181 @@ +/** + * 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.localepicker; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +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 android.app.IActivityManager; +import android.content.Context; +import android.content.res.Configuration; +import android.os.LocaleList; +import android.os.Looper; +import android.telephony.TelephonyManager; +import android.util.ArrayMap; + +import com.android.internal.app.LocaleStore; +import com.android.settings.testutils.shadow.ShadowActivityManager; +import com.android.settings.testutils.shadow.ShadowFragment; + +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; +import org.robolectric.shadows.ShadowTelephonyManager; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = { + ShadowFragment.class, + ShadowActivityManager.class, +}) +public class SystemLocaleSuggestedListPreferenceControllerTest { + private static final String KEY_CATEGORY_SYSTEM_SUGGESTED_LIST = + "system_language_suggested_category"; + private static final String KEY_SUGGESTED = "system_locale_suggested_list"; + + private Context mContext; + private PreferenceCategory mPreferenceCategory; + private PreferenceScreen mPreferenceScreen; + private SystemLocaleSuggestedListPreferenceController mController; + private List mLocaleList; + private Map mPreferences = new ArrayMap<>(); + @Mock + private PreferenceManager mPreferenceManager; + @Mock + private IActivityManager mActivityService; + @Mock + private LocaleStore.LocaleInfo mSuggestedLocaleInfo_1; + @Mock + private LocaleStore.LocaleInfo mSuggestedLocaleInfo_2; + @Mock + private FragmentManager mFragmentManager; + @Mock + private FragmentTransaction mFragmentTransaction; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + ShadowActivityManager.setService(mActivityService); + final Configuration config = new Configuration(); + setUpLocaleConditions(); + config.setLocales(new LocaleList(mSuggestedLocaleInfo_1.getLocale(), + mSuggestedLocaleInfo_2.getLocale())); + when(mActivityService.getConfiguration()).thenReturn(config); + ShadowTelephonyManager shadowTelephonyManager = + Shadows.shadowOf(mContext.getSystemService(TelephonyManager.class)); + shadowTelephonyManager.setSimCountryIso("us"); + shadowTelephonyManager.setNetworkCountryIso("us"); + when(mFragmentManager.beginTransaction()).thenReturn(mFragmentTransaction); + mPreferenceScreen = spy(new PreferenceScreen(mContext, null)); + mPreferenceCategory = spy(new PreferenceCategory(mContext, null)); + when(mPreferenceScreen.getPreferenceManager()).thenReturn(mPreferenceManager); + when(mPreferenceCategory.getPreferenceManager()).thenReturn(mPreferenceManager); + mPreferenceManager = new PreferenceManager(mContext); + mPreferenceScreen = mPreferenceManager.createPreferenceScreen(mContext); + mPreferenceCategory.setKey(KEY_CATEGORY_SYSTEM_SUGGESTED_LIST); + mPreferenceScreen.addPreference(mPreferenceCategory); + mController = new SystemLocaleSuggestedListPreferenceController(mContext, KEY_SUGGESTED); + } + + private void setUpLocaleConditions() { + mLocaleList = new ArrayList<>(); + when(mSuggestedLocaleInfo_1.getFullNameNative()).thenReturn("English"); + when(mSuggestedLocaleInfo_1.getLocale()).thenReturn( + LocaleList.forLanguageTags("en-US").get(0)); + mLocaleList.add(mSuggestedLocaleInfo_1); + when(mSuggestedLocaleInfo_2.getFullNameNative()).thenReturn("Español (Estados Unidos)"); + when(mSuggestedLocaleInfo_2.getLocale()).thenReturn( + LocaleList.forLanguageTags("es-US").get(0)); + mLocaleList.add(mSuggestedLocaleInfo_2); + } + + @Test + public void displayPreference_hasSuggestedPreference_categoryIsVisible() { + mController.displayPreference(mPreferenceScreen); + mController.setupPreference(mLocaleList, mPreferences); + + assertTrue(mPreferenceCategory.isVisible()); + assertThat(mPreferenceCategory.getPreferenceCount()).isEqualTo(2); + } + + @Test + public void displayPreference_noSuggestedPreference_categoryIsGone() { + mLocaleList.clear(); + mController.displayPreference(mPreferenceScreen); + mController.setupPreference(mLocaleList, mPreferences); + + assertFalse(mPreferenceCategory.isVisible()); + assertThat(mPreferenceCategory.getPreferenceCount()).isEqualTo(0); + } + + @Test + public void switchFragment_shouldShowLocaleEditor() { + when(mSuggestedLocaleInfo_1.isSuggested()).thenReturn(true); + mController.shouldShowLocaleEditor(mSuggestedLocaleInfo_1); + mController.switchFragment(mSuggestedLocaleInfo_1); + + verify(mFragmentTransaction, never()).add(any(LocaleListEditor.class), + anyString()); + } + + @Test + public void switchFragment_shouldShowRegionNumberingPicker() { + Context activityContext = mock(Context.class); + mController = new SystemLocaleSuggestedListPreferenceController(activityContext, + KEY_SUGGESTED); + when(mSuggestedLocaleInfo_1.isSuggested()).thenReturn(false); + when(mSuggestedLocaleInfo_1.isSystemLocale()).thenReturn(false); + when(mSuggestedLocaleInfo_1.getParent()).thenReturn(null); + mController.shouldShowLocaleEditor(mSuggestedLocaleInfo_1); + mController.switchFragment(mSuggestedLocaleInfo_1); + + verify(mFragmentTransaction, never()).add(any(RegionAndNumberingSystemPickerFragment.class), + anyString()); + } +} diff --git a/tests/unit/src/com/android/settings/localepicker/SystemLocaleSuggestedListPreferenceControllerTest.java b/tests/unit/src/com/android/settings/localepicker/SystemLocaleSuggestedListPreferenceControllerTest.java new file mode 100644 index 00000000000..b9b52f187a8 --- /dev/null +++ b/tests/unit/src/com/android/settings/localepicker/SystemLocaleSuggestedListPreferenceControllerTest.java @@ -0,0 +1,111 @@ +/** + * 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.localepicker; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.os.Looper; + +import com.android.internal.app.LocaleStore; +import com.android.internal.app.SystemLocaleCollector; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +import java.util.Locale; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +public class SystemLocaleSuggestedListPreferenceControllerTest { + private static final String KEY_CATEGORY_SYSTEM_SUGGESTED_LIST = + "system_language_suggested_category"; + private static final String KEY_SUGGESTED = "system_locale_suggested_list"; + + private Context mContext; + private PreferenceManager mPreferenceManager; + private PreferenceCategory mPreferenceCategory; + private PreferenceScreen mPreferenceScreen; + private Preference mSuggestedPreference; + private SystemLocaleSuggestedListPreferenceController mController; + private Set mLocaleList; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + SystemLocaleCollector systemLocaleCollector = new SystemLocaleCollector(mContext, null); + mLocaleList = systemLocaleCollector.getSupportedLocaleList(null, false, false); + mPreferenceManager = new PreferenceManager(mContext); + mPreferenceScreen = mPreferenceManager.createPreferenceScreen(mContext); + mPreferenceCategory = new PreferenceCategory(mContext); + mPreferenceCategory.setKey(KEY_CATEGORY_SYSTEM_SUGGESTED_LIST); + mPreferenceScreen.addPreference(mPreferenceCategory); + mController = new SystemLocaleSuggestedListPreferenceController(mContext, KEY_SUGGESTED); + mController.displayPreference(mPreferenceScreen); + } + + @Test + public void displayPreference_suggestedLocaleShouldBeInSuggestedCategory() { + int count = 0; + for (LocaleStore.LocaleInfo localeInfo : mLocaleList) { + if (localeInfo.isSuggested()) { + count++; + } + } + + assertThat(mPreferenceCategory.getPreferenceCount()).isEqualTo(count); + } + + @Test + public void displayPreference_hasSuggestedPreference_categoryIsVisible() { + int count = 0; + for (LocaleStore.LocaleInfo localeInfo : mLocaleList) { + if (localeInfo.isSuggested()) { + count++; + } + } + + if (count > 0) { + assertTrue(mPreferenceCategory.isVisible()); + } else { + assertFalse(mPreferenceCategory.isVisible()); + } + } +} From e4c2cddfaeba71bec29dfad7869ecfebd21ac7ad Mon Sep 17 00:00:00 2001 From: songferngwang Date: Tue, 25 Feb 2025 08:41:55 +0000 Subject: [PATCH 10/14] Add null checker for telephonymanager Bug: 399023211 Test: atest Enable2gPreferenceControllerTest Flag: EXEMPT bugfix Change-Id: I3872ecfff2296cbec9f0ef54ee7cee5a554f111f --- .../telephony/Enable2gPreferenceController.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/network/telephony/Enable2gPreferenceController.java b/src/com/android/settings/network/telephony/Enable2gPreferenceController.java index e941ce547a4..13120a3c907 100644 --- a/src/com/android/settings/network/telephony/Enable2gPreferenceController.java +++ b/src/com/android/settings/network/telephony/Enable2gPreferenceController.java @@ -162,8 +162,8 @@ public class Enable2gPreferenceController extends TelephonyTogglePreferenceContr @Override public int getAvailabilityStatus(int subId) { if (mTelephonyManager == null) { - Log.w(LOG_TAG, "Telephony manager not yet initialized"); - mTelephonyManager = mContext.getSystemService(TelephonyManager.class); + Log.w(LOG_TAG, "getAvailabilityStatus: Telephony manager not yet initialized"); + return CONDITIONALLY_UNAVAILABLE; } boolean visible = SubscriptionManager.isUsableSubscriptionId(subId) @@ -189,6 +189,10 @@ public class Enable2gPreferenceController extends TelephonyTogglePreferenceContr return false; } + if (mTelephonyManager == null) { + Log.w(LOG_TAG, "isChecked: Telephony manager not yet initialized"); + return false; + } long currentlyAllowedNetworkTypes = mTelephonyManager.getAllowedNetworkTypesForReason( mTelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G); return (currentlyAllowedNetworkTypes & BITMASK_2G) != 0; @@ -214,6 +218,12 @@ public class Enable2gPreferenceController extends TelephonyTogglePreferenceContr if (!SubscriptionManager.isUsableSubscriptionId(mSubId)) { return false; } + + if (mTelephonyManager == null) { + Log.w(LOG_TAG, "setChecked: Telephony manager not yet initialized"); + return false; + } + long currentlyAllowedNetworkTypes = mTelephonyManager.getAllowedNetworkTypesForReason( mTelephonyManager.ALLOWED_NETWORK_TYPES_REASON_ENABLE_2G); boolean enabled = (currentlyAllowedNetworkTypes & BITMASK_2G) != 0; From 8054a152b997590e25b56bf3457f06eb888128c7 Mon Sep 17 00:00:00 2001 From: Ebru Kurnaz Date: Tue, 18 Feb 2025 16:08:55 +0000 Subject: [PATCH 11/14] Call DisplayDensityUtils from ExternalDisplay settings to adjust display size Flag: com.android.settings.flags.display_size_connected_display_setting Test: atest ExternalDisplayPreferenceFragmentTest Bug: 392853666 Change-Id: Iaa97f461e80a4e749ec3d2b47985a6be4a81a95b --- .../accessibility/DisplaySizeData.java | 12 ++- .../accessibility/PreviewSizeData.java | 5 +- .../ExternalDisplayPreferenceFragment.java | 81 +++++++++++++++++-- 3 files changed, 87 insertions(+), 11 deletions(-) diff --git a/src/com/android/settings/accessibility/DisplaySizeData.java b/src/com/android/settings/accessibility/DisplaySizeData.java index 15aeb6b0d9d..64dab1f7a3e 100644 --- a/src/com/android/settings/accessibility/DisplaySizeData.java +++ b/src/com/android/settings/accessibility/DisplaySizeData.java @@ -19,6 +19,8 @@ package com.android.settings.accessibility; import android.content.Context; import android.content.res.Resources; +import androidx.annotation.NonNull; + import com.android.settingslib.display.DisplayDensityUtils; import java.util.Arrays; @@ -28,13 +30,17 @@ import java.util.stream.Collectors; /** * Data class for storing the configurations related to the display size. */ -class DisplaySizeData extends PreviewSizeData { +public class DisplaySizeData extends PreviewSizeData { private final DisplayDensityUtils mDensity; DisplaySizeData(Context context) { + this(context, new DisplayDensityUtils(context)); + } + + public DisplaySizeData(@NonNull Context context, @NonNull DisplayDensityUtils util) { super(context); - mDensity = new DisplayDensityUtils(getContext()); + mDensity = util; final int initialIndex = mDensity.getCurrentIndex(); if (initialIndex < 0) { // Failed to obtain default density, which means we failed to @@ -54,7 +60,7 @@ class DisplaySizeData extends PreviewSizeData { } @Override - void commit(int currentProgress) { + public void commit(int currentProgress) { final int densityDpi = getValues().get(currentProgress); if (densityDpi == getDefaultValue()) { mDensity.clearForcedDisplayDensity(); diff --git a/src/com/android/settings/accessibility/PreviewSizeData.java b/src/com/android/settings/accessibility/PreviewSizeData.java index 5d4204e2e56..fca3a5b2978 100644 --- a/src/com/android/settings/accessibility/PreviewSizeData.java +++ b/src/com/android/settings/accessibility/PreviewSizeData.java @@ -40,7 +40,8 @@ abstract class PreviewSizeData { return mContext; } - List getValues() { + @NonNull + public List getValues() { return mValues; } @@ -56,7 +57,7 @@ abstract class PreviewSizeData { mDefaultValue = defaultValue; } - int getInitialIndex() { + public int getInitialIndex() { return mInitialIndex; } diff --git a/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java b/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java index ef85d93c447..84fbc05f1c9 100644 --- a/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java +++ b/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java @@ -32,8 +32,11 @@ import android.app.Activity; import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; +import android.os.SystemClock; +import android.view.Choreographer; import android.view.Display; import android.view.View; +import android.widget.SeekBar; import android.widget.TextView; import android.window.DesktopExperienceFlags; @@ -48,10 +51,12 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragmentBase; import com.android.settings.accessibility.AccessibilitySeekBarPreference; +import com.android.settings.accessibility.DisplaySizeData; import com.android.settings.accessibility.TextReadingPreferenceFragment; import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener; import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector; import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.display.DisplayDensityUtils; import com.android.settingslib.widget.FooterPreference; import com.android.settingslib.widget.IllustrationPreference; import com.android.settingslib.widget.MainSwitchPreference; @@ -368,7 +373,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen @NonNull private AccessibilitySeekBarPreference reuseSizePreference(Context context, - PrefRefresh refresh) { + PrefRefresh refresh, int displayId) { AccessibilitySeekBarPreference pref = refresh.findUnusedPreference(PrefBasics.EXTERNAL_DISPLAY_SIZE.key); if (pref == null) { @@ -378,11 +383,27 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen pref.setIconEnd(R.drawable.ic_add_24dp); pref.setIconEndContentDescription(R.string.screen_zoom_make_larger_desc); PrefBasics.EXTERNAL_DISPLAY_SIZE.apply(pref); + + setStateForDisplaySizePreference(context, displayId, pref); } refresh.addPreference(pref); return pref; } + private void setStateForDisplaySizePreference(Context context, int displayId, + AccessibilitySeekBarPreference preference) { + var displaySizeData = new DisplaySizeData(context, + new DisplayDensityUtils(context, (info) -> info.displayId == displayId)); + ExternalDisplaySizePreferenceStateHandler seekBarChangeHandler = + new ExternalDisplaySizePreferenceStateHandler( + displaySizeData, preference); + + preference.setMax(displaySizeData.getValues().size() - 1); + preference.setProgress(displaySizeData.getInitialIndex()); + preference.setContinuousUpdates(false); + preference.setOnSeekBarChangeListener(seekBarChangeHandler); + } + private void restoreState(@Nullable Bundle savedInstanceState) { if (savedInstanceState == null) { return; @@ -409,7 +430,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen showTextWhenNoDisplaysToShow(screen, context); } else if (displaysToShow.size() == 1 && ((displayId == INVALID_DISPLAY && !mPreviouslyShownListOfDisplays) - || displaysToShow.get(0).getDisplayId() == displayId)) { + || displaysToShow.get(0).getDisplayId() == displayId)) { showDisplaySettings(displaysToShow.get(0), screen, context); if (displayId == INVALID_DISPLAY && isTopologyPaneEnabled(mInjector)) { // Only show the topology pane if the user did not arrive via the displays list. @@ -514,7 +535,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen } } if (isDisplaySizeSettingEnabled(mInjector)) { - addSizePreference(context, refresh); + addSizePreference(context, refresh, display.getDisplayId()); } } @@ -532,7 +553,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen } private void showDisplaysList(@NonNull List displaysToShow, - @NonNull PrefRefresh screen, @NonNull Context context) { + @NonNull PrefRefresh screen, @NonNull Context context) { maybeAddV2Components(context, screen); int order = PrefBasics.BUILTIN_DISPLAY_LIST.order; for (var display : displaysToShow) { @@ -685,8 +706,8 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen pref.setEnabled(isResolutionSettingEnabled(mInjector)); } - private void addSizePreference(final Context context, PrefRefresh refresh) { - var pref = reuseSizePreference(context, refresh); + private void addSizePreference(final Context context, PrefRefresh refresh, int displayId) { + var pref = reuseSizePreference(context, refresh, displayId); pref.setSummary(EXTERNAL_DISPLAY_SIZE_SUMMARY_RESOURCE); pref.setOnPreferenceClickListener( (Preference p) -> { @@ -735,6 +756,54 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen } } + private static class ExternalDisplaySizePreferenceStateHandler + implements SeekBar.OnSeekBarChangeListener { + private static final long MIN_COMMIT_INTERVAL_MS = 800; + private static final long CHANGE_BY_BUTTON_DELAY_MS = 300; + private final DisplaySizeData mDisplaySizeData; + private int mLastDisplayProgress; + private long mLastCommitTime; + private final AccessibilitySeekBarPreference mPreference; + ExternalDisplaySizePreferenceStateHandler(DisplaySizeData displaySizeData, + AccessibilitySeekBarPreference preference) { + mDisplaySizeData = displaySizeData; + mPreference = preference; + } + + final Choreographer.FrameCallback mCommit = this::tryCommitDisplaySizeConfig; + + private void tryCommitDisplaySizeConfig(long unusedFrameTimeNanos) { + final int displayProgress = mPreference.getProgress(); + if (displayProgress != mLastDisplayProgress) { + mDisplaySizeData.commit(displayProgress); + mLastDisplayProgress = displayProgress; + } + mLastCommitTime = SystemClock.elapsedRealtime(); + } + + private void postCommitDelayed() { + var commitDelayMs = CHANGE_BY_BUTTON_DELAY_MS; + if (SystemClock.elapsedRealtime() - mLastCommitTime < MIN_COMMIT_INTERVAL_MS) { + commitDelayMs += MIN_COMMIT_INTERVAL_MS; + } + + final Choreographer choreographer = Choreographer.getInstance(); + choreographer.removeFrameCallback(mCommit); + choreographer.postFrameCallbackDelayed(mCommit, commitDelayMs); + } + + @Override + public void onProgressChanged(@NonNull SeekBar seekBar, int i, boolean b) { + postCommitDelayed(); + } + + @Override + public void onStartTrackingTouch(@NonNull SeekBar seekBar) {} + + @Override + public void onStopTrackingTouch(@NonNull SeekBar seekBar) {} + } + @VisibleForTesting class DisplayPreference extends TwoTargetPreference implements Preference.OnPreferenceClickListener { From b91d21ba762ec0fba6e77c35ab350c3d6c8e2bb7 Mon Sep 17 00:00:00 2001 From: tom hsu Date: Tue, 25 Feb 2025 11:42:46 +0000 Subject: [PATCH 12/14] Add strings for NTN connection manual type Flag: EXEMPT bug fix Fix: b/397958254 Test: Manual test. see b/397958254#comment10 Change-Id: Ibcd3511edee9b3203c6c0cad68e0988b4b621b39 --- res/values/strings.xml | 8 ++++++++ .../settings/network/telephony/SatelliteSetting.java | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/res/values/strings.xml b/res/values/strings.xml index 3679ba403ab..3ebff346453 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12583,12 +12583,20 @@ How it works When you don\u2019t have a mobile network + + Text a phone number Your phone will auto-connect to a satellite. For the best connection, keep a clear view of the sky. + + If you don\u2019t have a mobile network, you\u2019ll see an option to use satellite messaging. After your phone connects to a satellite + + Follow steps to connect to the satellite You can text anyone, including emergency services. Your phone will reconnect to a mobile network when available. + + After your phone is connected, you can text anyone, including emergency services. %1$s may take longer and is available only in some areas. Weather and certain structures may affect your satellite connection. Calling by satellite isn\u2019t available. Emergency calls may still connect.\n\nIt may take some time for account changes to show in Settings. Contact %2$s for details. diff --git a/src/com/android/settings/network/telephony/SatelliteSetting.java b/src/com/android/settings/network/telephony/SatelliteSetting.java index f99011d1ede..ef5f23c004b 100644 --- a/src/com/android/settings/network/telephony/SatelliteSetting.java +++ b/src/com/android/settings/network/telephony/SatelliteSetting.java @@ -70,6 +70,9 @@ public class SatelliteSetting extends RestrictedDashboardFragment { private static final String PREF_KEY_YOUR_SATELLITE_DATA_PLAN = "key_your_satellite_data_plan"; private static final String PREF_KEY_CATEGORY_ABOUT_SATELLITE = "key_category_about_satellite"; private static final String KEY_FOOTER_PREFERENCE = "satellite_setting_extra_info_footer_pref"; + private static final String KEY_SATELLITE_CONNECTION_GUIDE = "key_satellite_connection_guide"; + private static final String KEY_SUPPORTED_SERVICE = "key_supported_service"; + static final String SUB_ID = "sub_id"; static final String EXTRA_IS_SERVICE_DATA_TYPE = "is_service_data_type"; @@ -227,6 +230,15 @@ public class SatelliteSetting extends RestrictedDashboardFragment { category.setEnabled(false); category.setShouldDisableView(true); } + if (!isCarrierRoamingNtnConnectedTypeManual()) { + return; + } + Preference connectionGuide = findPreference(KEY_SATELLITE_CONNECTION_GUIDE); + connectionGuide.setTitle(R.string.title_satellite_connection_guide_for_manual_type); + connectionGuide.setSummary(R.string.summary_satellite_connection_guide_for_manual_type); + Preference supportedService = findPreference(KEY_SUPPORTED_SERVICE); + supportedService.setTitle(R.string.title_supported_service_for_manual_type); + supportedService.setSummary(R.string.summary_supported_service_for_manual_type); } private void updateFooterContent() { From 3c5aa634764981c6e8fc5e476451d600a6d33433 Mon Sep 17 00:00:00 2001 From: Weng Su Date: Tue, 25 Feb 2025 21:24:18 +0800 Subject: [PATCH 13/14] Fixed accessibility issues in Wi-Fi calling preference - Made radio buttons non-focusable to avoid confusion in Talkback. Bug: 380477792 Flag: EXEMPT resource file only update Test: Manual testing atest ListWithEntrySummaryPreferenceTest Change-Id: I030adc76bf762ff9f2b531561b7a8f39555fe020 --- res/xml/single_choice_list_item_2.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/xml/single_choice_list_item_2.xml b/res/xml/single_choice_list_item_2.xml index ca80f444602..733751b08ca 100644 --- a/res/xml/single_choice_list_item_2.xml +++ b/res/xml/single_choice_list_item_2.xml @@ -27,7 +27,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" - android:clickable="false" /> + android:clickable="false" + android:focusable="false"/> Date: Tue, 28 Jan 2025 15:16:09 -0500 Subject: [PATCH 14/14] Add ability to exclude apps from adjustments Specifically bundling and summarization Test: BundleManageAppsPreferenceControllerTest Test: AdjustmentExcludedAppsPreferenceControllerTest Test: SummarizationManageAppsPreferenceControllerTest Flag: android.app.nm_summarization Flag: android.app.notification_classification_ui Bug: 390415383 Bug: 377697346 Change-Id: Ica4b77212f4660624bfe12be7e6c9c584cd2c812 --- res/xml/bundle_notifications_settings.xml | 23 ++ .../summarization_notifications_settings.xml | 25 +++ src/com/android/settings/Settings.java | 2 + .../ManageApplications.java | 2 + .../ManageApplicationsUtil.kt | 11 +- ...tmentExcludedAppsPreferenceController.java | 212 ++++++++++++++++++ .../BundleManageAppsPreferenceController.java | 44 ++++ .../BundlePreferenceFragment.java | 26 +++ ...izationManageAppsPreferenceController.java | 45 ++++ .../SummarizationPreferenceFragment.java | 26 +++ .../settings/spa/SettingsSpaEnvironment.kt | 5 +- .../spa/notification/AppListNotifications.kt | 70 ++++-- .../notification/AppNotificationController.kt | 62 +++++ .../notification/AppNotificationRepository.kt | 14 ++ .../notification/AppNotificationsListModel.kt | 40 +++- .../spa/notification/NotificationMain.kt | 2 +- ...tExcludedAppsPreferenceControllerTest.java | 151 +++++++++++++ ...dleManageAppsPreferenceControllerTest.java | 79 +++++++ ...ionManageAppsPreferenceControllerTest.java | 81 +++++++ 19 files changed, 890 insertions(+), 30 deletions(-) create mode 100644 src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceController.java create mode 100644 src/com/android/settings/notification/BundleManageAppsPreferenceController.java create mode 100644 src/com/android/settings/notification/SummarizationManageAppsPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/BundleManageAppsPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/SummarizationManageAppsPreferenceControllerTest.java diff --git a/res/xml/bundle_notifications_settings.xml b/res/xml/bundle_notifications_settings.xml index 34efd7dd49b..3ff8c053273 100644 --- a/res/xml/bundle_notifications_settings.xml +++ b/res/xml/bundle_notifications_settings.xml @@ -56,4 +56,27 @@ android:key="recs" android:title="@*android:string/recs_notification_channel_label" settings:controller="com.android.settings.notification.BundleTypePreferenceController"/> + + + + + + + + + + diff --git a/res/xml/summarization_notifications_settings.xml b/res/xml/summarization_notifications_settings.xml index c6de6265709..af9598042ac 100644 --- a/res/xml/summarization_notifications_settings.xml +++ b/res/xml/summarization_notifications_settings.xml @@ -36,4 +36,29 @@ android:key="global_pref" android:title="@string/notification_summarization_main_control_title" settings:controller="com.android.settings.notification.SummarizationGlobalPreferenceController" /> + + + + + + + + + + + + diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index ee866babffc..c8b0783ece5 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -358,6 +358,8 @@ public class Settings extends SettingsActivity { public static class AppBubbleNotificationSettingsActivity extends SettingsActivity { /* empty */ } public static class NotificationAssistantSettingsActivity extends SettingsActivity{ /* empty */ } public static class NotificationAppListActivity extends SettingsActivity { /* empty */ } + public static class NotificationExcludeSummarizationActivity extends SettingsActivity { /* empty */ } + public static class NotificationExcludeClassificationActivity extends SettingsActivity { /* empty */ } /** Activity to manage Cloned Apps page */ public static class ClonedAppsListActivity extends SettingsActivity { /* empty */ } /** Activity to manage Aspect Ratio app list page */ diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index b837e1e9c5d..d3d532f2dd4 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -270,6 +270,8 @@ public class ManageApplications extends InstrumentedFragment public static final int LIST_TYPE_NFC_TAG_APPS = 18; public static final int LIST_TYPE_TURN_SCREEN_ON = 19; public static final int LIST_TYPE_USER_ASPECT_RATIO_APPS = 20; + public static final int LIST_TYPE_NOTIFICATION_EXCLUDE_SUMMARIZATION = 21; + public static final int LIST_TYPE_NOTIFICATION_EXCLUDE_CLASSIFICATION = 22; // List types that should show instant apps. public static final Set LIST_TYPES_WITH_INSTANT = new ArraySet<>(Arrays.asList( diff --git a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt index dca115b97c0..41b92dde868 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt +++ b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt @@ -31,6 +31,8 @@ import com.android.settings.Settings.ManageExternalSourcesActivity import com.android.settings.Settings.ManageExternalStorageActivity import com.android.settings.Settings.MediaManagementAppsActivity import com.android.settings.Settings.NotificationAppListActivity +import com.android.settings.Settings.NotificationExcludeClassificationActivity +import com.android.settings.Settings.NotificationExcludeSummarizationActivity import com.android.settings.Settings.NotificationReviewPermissionsActivity import com.android.settings.Settings.OverlaySettingsActivity import com.android.settings.Settings.StorageUseActivity @@ -44,6 +46,8 @@ import com.android.settings.applications.manageapplications.ManageApplications.L import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_USER_ASPECT_RATIO_APPS import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_BATTERY_OPTIMIZATION import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_CLONED_APPS +import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_NOTIFICATION_EXCLUDE_CLASSIFICATION +import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_NOTIFICATION_EXCLUDE_SUMMARIZATION import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_GAMES import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_HIGH_POWER import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_LONG_BACKGROUND_TASKS @@ -99,6 +103,9 @@ object ManageApplicationsUtil { ChangeNfcTagAppsActivity::class to LIST_TYPE_NFC_TAG_APPS, TurnScreenOnSettingsActivity::class to LIST_TYPE_TURN_SCREEN_ON, UserAspectRatioAppListActivity::class to LIST_TYPE_USER_ASPECT_RATIO_APPS, + NotificationExcludeSummarizationActivity::class to LIST_TYPE_NOTIFICATION_EXCLUDE_SUMMARIZATION, + NotificationExcludeClassificationActivity::class to LIST_TYPE_NOTIFICATION_EXCLUDE_CLASSIFICATION, + ) @JvmField @@ -117,7 +124,7 @@ object ManageApplicationsUtil { LIST_TYPE_MEDIA_MANAGEMENT_APPS -> MediaManagementAppsAppListProvider.getAppListRoute() LIST_TYPE_ALARMS_AND_REMINDERS -> AlarmsAndRemindersAppListProvider.getAppListRoute() LIST_TYPE_WIFI_ACCESS -> WifiControlAppListProvider.getAppListRoute() - LIST_TYPE_NOTIFICATION -> AppListNotificationsPageProvider.name + LIST_TYPE_NOTIFICATION -> AppListNotificationsPageProvider.AllApps.name LIST_TYPE_APPS_LOCALE -> AppLanguagesPageProvider.name LIST_TYPE_MAIN -> AllAppListPageProvider.name LIST_TYPE_NFC_TAG_APPS -> NfcTagAppsSettingsProvider.getAppListRoute() @@ -128,6 +135,8 @@ object ManageApplicationsUtil { //LIST_TYPE_STORAGE -> StorageAppListPageProvider.Apps.name //LIST_TYPE_GAMES -> StorageAppListPageProvider.Games.name LIST_TYPE_BATTERY_OPTIMIZATION -> BatteryOptimizationModeAppListPageProvider.name + LIST_TYPE_NOTIFICATION_EXCLUDE_SUMMARIZATION -> AppListNotificationsPageProvider.ExcludeSummarization.name + LIST_TYPE_NOTIFICATION_EXCLUDE_CLASSIFICATION -> AppListNotificationsPageProvider.ExcludeClassification.name else -> null } } diff --git a/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceController.java b/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceController.java new file mode 100644 index 00000000000..b9cf9737be6 --- /dev/null +++ b/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceController.java @@ -0,0 +1,212 @@ +/* + * 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.notification; + +import static android.service.notification.Adjustment.KEY_SUMMARIZATION; +import static android.service.notification.Adjustment.KEY_TYPE; + +import android.app.Flags; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.service.notification.Adjustment; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.core.text.BidiFormatter; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.applications.AppUtils; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.utils.ThreadUtils; + +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.ArrayList; +import java.util.List; + +/** + * Adds a preference to the PreferenceCategory for every app excluded from an adjustment key + */ +public class AdjustmentExcludedAppsPreferenceController extends BasePreferenceController + implements PreferenceControllerMixin { + + @NonNull private NotificationBackend mBackend; + + @Nullable String mAdjustmentKey; + @Nullable @VisibleForTesting ApplicationsState mApplicationsState; + @VisibleForTesting PreferenceCategory mPreferenceCategory; + @VisibleForTesting Context mPrefContext; + + private ApplicationsState.Session mAppSession; + + public AdjustmentExcludedAppsPreferenceController(@NonNull Context context, + @NonNull String preferenceKey) { + super(context, preferenceKey); + mBackend = new NotificationBackend(); + } + + protected void onAttach(@Nullable ApplicationsState appState, @Nullable Fragment host, + @NonNull NotificationBackend helperBackend, @Adjustment.Keys String adjustment) { + mApplicationsState = appState; + mBackend = helperBackend; + mAdjustmentKey = adjustment; + + if (mApplicationsState != null && host != null) { + mAppSession = mApplicationsState.newSession(mAppSessionCallbacks, host.getLifecycle()); + } + } + + @Override + public void displayPreference(@NonNull PreferenceScreen screen) { + mPreferenceCategory = screen.findPreference(getPreferenceKey()); + mPrefContext = screen.getContext(); + updateAppList(); + super.displayPreference(screen); + } + + @Override + public int getAvailabilityStatus() { + if (!(Flags.nmSummarization() || Flags.nmSummarizationUi() + || Flags.notificationClassificationUi())) { + return CONDITIONALLY_UNAVAILABLE; + } + if (KEY_SUMMARIZATION.equals(mAdjustmentKey) + && mBackend.isNotificationSummarizationSupported()) { + return AVAILABLE; + } + if (KEY_TYPE.equals(mAdjustmentKey) && mBackend.isNotificationBundlingSupported()) { + return AVAILABLE; + } + return CONDITIONALLY_UNAVAILABLE; + } + + /** + * Call this method to trigger the app list to refresh. + */ + public void updateAppList() { + if (mAppSession == null) { + return; + } + + ApplicationsState.AppFilter filter = android.multiuser.Flags.enablePrivateSpaceFeatures() + && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace() + ? ApplicationsState.FILTER_ENABLED_NOT_QUIET + : ApplicationsState.FILTER_ALL_ENABLED; + mAppSession.rebuild(filter, ApplicationsState.ALPHA_COMPARATOR); + } + + // Set the icon for the given preference to the entry icon from cache if available, or look + // it up. + private void updateIcon(Preference pref, ApplicationsState.AppEntry entry) { + synchronized (entry) { + final Drawable cachedIcon = AppUtils.getIconFromCache(entry); + if (cachedIcon != null && entry.mounted) { + pref.setIcon(cachedIcon); + } else { + ListenableFuture unused = ThreadUtils.postOnBackgroundThread(() -> { + final Drawable icon = AppUtils.getIcon(mPrefContext, entry); + if (icon != null) { + ThreadUtils.postOnMainThread(() -> pref.setIcon(icon)); + } + }); + } + } + } + + @VisibleForTesting + void updateAppList(List apps) { + if (mPreferenceCategory == null || apps == null) { + return; + } + + List excludedApps = List.of(mBackend.getAdjustmentDeniedPackages(mAdjustmentKey)); + + for (ApplicationsState.AppEntry app : apps) { + String pkg = app.info.packageName; + final String key = getKey(pkg, app.info.uid); + boolean doesAppPassCriteria = false; + + if (excludedApps.contains(pkg)) { + doesAppPassCriteria = true; + } + Preference pref = mPreferenceCategory.findPreference(key); + if (pref == null) { + if (doesAppPassCriteria) { + // does not exist but should + pref = new Preference(mPrefContext); + pref.setKey(key); + pref.setTitle(BidiFormatter.getInstance().unicodeWrap(app.label)); + updateIcon(pref, app); + mPreferenceCategory.addPreference(pref); + } + } else if (!doesAppPassCriteria) { + // exists but shouldn't anymore + mPreferenceCategory.removePreference(pref); + } + } + } + + /** + * Create a unique key to identify an AppPreference + */ + static String getKey(String pkg, int uid) { + return "all|" + pkg + "|" + uid; + } + + private final ApplicationsState.Callbacks mAppSessionCallbacks = + new ApplicationsState.Callbacks() { + + @Override + public void onRunningStateChanged(boolean running) { + } + + @Override + public void onPackageListChanged() { + } + + @Override + public void onRebuildComplete(@NonNull ArrayList apps) { + updateAppList(apps); + } + + @Override + public void onPackageIconChanged() { + } + + @Override + public void onPackageSizeChanged(@NonNull String packageName) { + } + + @Override + public void onAllSizesComputed() { } + + @Override + public void onLauncherInfoChanged() { + } + + @Override + public void onLoadEntriesCompleted() { + updateAppList(); + } + }; +} diff --git a/src/com/android/settings/notification/BundleManageAppsPreferenceController.java b/src/com/android/settings/notification/BundleManageAppsPreferenceController.java new file mode 100644 index 00000000000..6c096598af9 --- /dev/null +++ b/src/com/android/settings/notification/BundleManageAppsPreferenceController.java @@ -0,0 +1,44 @@ +/* + * 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.notification; + +import android.app.Flags; +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.android.settings.core.BasePreferenceController; + +public class BundleManageAppsPreferenceController extends + BasePreferenceController { + + NotificationBackend mBackend; + + public BundleManageAppsPreferenceController(@NonNull Context context, + @NonNull String preferenceKey) { + super(context, preferenceKey); + mBackend = new NotificationBackend(); + } + + @Override + public int getAvailabilityStatus() { + if (Flags.notificationClassificationUi() && mBackend.isNotificationBundlingSupported()) { + return AVAILABLE; + } + return CONDITIONALLY_UNAVAILABLE; + } +} diff --git a/src/com/android/settings/notification/BundlePreferenceFragment.java b/src/com/android/settings/notification/BundlePreferenceFragment.java index 14de2c26d1c..4a61f9e54af 100644 --- a/src/com/android/settings/notification/BundlePreferenceFragment.java +++ b/src/com/android/settings/notification/BundlePreferenceFragment.java @@ -16,6 +16,11 @@ package com.android.settings.notification; +import static android.service.notification.Adjustment.KEY_SUMMARIZATION; +import static android.service.notification.Adjustment.KEY_TYPE; + +import android.app.Activity; +import android.app.Application; import android.app.settings.SettingsEnums; import android.content.Context; import android.app.Flags; @@ -25,6 +30,7 @@ import androidx.lifecycle.Lifecycle; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.search.SearchIndexable; import org.jetbrains.annotations.NotNull; @@ -49,6 +55,26 @@ public class BundlePreferenceFragment extends DashboardFragment { return "BundlePreferenceFragment"; } + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (use(AdjustmentExcludedAppsPreferenceController.class) != null) { + final Activity activity = getActivity(); + Application app = null; + ApplicationsState appState = null; + if (activity != null) { + app = activity.getApplication(); + } else { + app = null; + } + if (app != null) { + appState = ApplicationsState.getInstance(app); + } + use(AdjustmentExcludedAppsPreferenceController.class).onAttach( + appState, this, new NotificationBackend(), KEY_TYPE); + } + } + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider(R.xml.bundle_notifications_settings) { diff --git a/src/com/android/settings/notification/SummarizationManageAppsPreferenceController.java b/src/com/android/settings/notification/SummarizationManageAppsPreferenceController.java new file mode 100644 index 00000000000..0a19c020fb5 --- /dev/null +++ b/src/com/android/settings/notification/SummarizationManageAppsPreferenceController.java @@ -0,0 +1,45 @@ +/* + * 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.notification; + +import android.app.Flags; +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.android.settings.core.BasePreferenceController; + +public class SummarizationManageAppsPreferenceController extends + BasePreferenceController { + + NotificationBackend mBackend; + + public SummarizationManageAppsPreferenceController(@NonNull Context context, + @NonNull String preferenceKey) { + super(context, preferenceKey); + mBackend = new NotificationBackend(); + } + + @Override + public int getAvailabilityStatus() { + if ((Flags.nmSummarization() || Flags.nmSummarizationUi()) + && mBackend.isNotificationSummarizationSupported()) { + return AVAILABLE; + } + return CONDITIONALLY_UNAVAILABLE; + } +} diff --git a/src/com/android/settings/notification/SummarizationPreferenceFragment.java b/src/com/android/settings/notification/SummarizationPreferenceFragment.java index 1c8f5e3059d..893383a3b8c 100644 --- a/src/com/android/settings/notification/SummarizationPreferenceFragment.java +++ b/src/com/android/settings/notification/SummarizationPreferenceFragment.java @@ -16,13 +16,19 @@ package com.android.settings.notification; +import static android.service.notification.Adjustment.KEY_SUMMARIZATION; + +import android.app.Activity; +import android.app.Application; import android.app.Flags; import android.app.settings.SettingsEnums; import android.content.Context; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.notification.app.HeaderPreferenceController; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.search.SearchIndexable; /** @@ -45,6 +51,26 @@ public class SummarizationPreferenceFragment extends DashboardFragment { return "SummarizationPreferenceFragment"; } + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (use(AdjustmentExcludedAppsPreferenceController.class) != null) { + final Activity activity = getActivity(); + Application app = null; + ApplicationsState appState = null; + if (activity != null) { + app = activity.getApplication(); + } else { + app = null; + } + if (app != null) { + appState = ApplicationsState.getInstance(app); + } + use(AdjustmentExcludedAppsPreferenceController.class).onAttach( + appState, this, new NotificationBackend(), KEY_SUMMARIZATION); + } + } + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider(R.xml.summarization_notifications_settings) { diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt index 7702db6bcde..f78ebd2b3fd 100644 --- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt +++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt @@ -45,7 +45,6 @@ import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider import com.android.settings.spa.app.specialaccess.WriteSystemPreferencesAppListProvider import com.android.settings.spa.app.storage.StorageAppListPageProvider import com.android.settings.spa.core.instrumentation.SpaLogMetricsProvider -import com.android.settings.spa.core.instrumentation.SpaLogProvider import com.android.settings.spa.development.UsageStatsPageProvider import com.android.settings.spa.development.compat.PlatformCompatAppListPageProvider import com.android.settings.spa.home.HomePageProvider @@ -106,7 +105,9 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) { AppInfoSettingsProvider, SpecialAppAccessPageProvider, NotificationMainPageProvider, - AppListNotificationsPageProvider, + AppListNotificationsPageProvider.AllApps, + AppListNotificationsPageProvider.ExcludeClassification, + AppListNotificationsPageProvider.ExcludeSummarization, SystemMainPageProvider, LanguageAndInputPageProvider, AppLanguagesPageProvider, diff --git a/src/com/android/settings/spa/notification/AppListNotifications.kt b/src/com/android/settings/spa/notification/AppListNotifications.kt index 00e439495a4..7aa46206f4d 100644 --- a/src/com/android/settings/spa/notification/AppListNotifications.kt +++ b/src/com/android/settings/spa/notification/AppListNotifications.kt @@ -17,34 +17,72 @@ package com.android.settings.spa.notification import android.os.Bundle +import androidx.annotation.StringRes import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.android.settings.R import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.compose.navigator -import com.android.settingslib.spa.framework.compose.rememberContext import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spaprivileged.template.app.AppList +import com.android.settingslib.spaprivileged.template.app.AppListInput import com.android.settingslib.spaprivileged.template.app.AppListPage -object AppListNotificationsPageProvider : SettingsPageProvider { - override val name = "AppListNotifications" - +sealed class AppListNotificationsPageProvider(private val type: ListType) : SettingsPageProvider { @Composable override fun Page(arguments: Bundle?) { - AppListPage( - title = stringResource(R.string.app_notifications_title), - listModel = rememberContext(::AppNotificationsListModel), - ) + NotificationsAppListPage(type) } - @Composable - fun EntryItem() { - val summary = stringResource(R.string.app_notification_field_summary) - Preference(object : PreferenceModel { - override val title = stringResource(R.string.app_notifications_title) - override val summary = { summary } - override val onClick = navigator(name) - }) + object AllApps : AppListNotificationsPageProvider(ListType.Apps) { + override val name = "AppListNotifications" + + @Composable + fun EntryItem() { + val summary = stringResource(R.string.app_notification_field_summary) + Preference(object : PreferenceModel { + override val title = stringResource(ListType.Apps.titleResource) + override val summary = { summary } + override val onClick = navigator(name) + }) + } + } + + object ExcludeSummarization : AppListNotificationsPageProvider(ListType.ExcludeSummarization) { + override val name = "NotificationsExcludeSummarization" + } + + object ExcludeClassification : AppListNotificationsPageProvider(ListType.ExcludeClassification) { + override val name = "NotificationsExcludeClassification" } } + +@Composable +fun NotificationsAppListPage( + type: ListType, + appList: @Composable AppListInput.() -> Unit = { AppList() } +) { + val context = LocalContext.current + AppListPage( + title = stringResource(type.titleResource), + listModel = remember(context) { AppNotificationsListModel(context, type) }, + appList = appList, + ) +} + +sealed class ListType( + @StringRes val titleResource: Int +) { + object Apps : ListType( + titleResource = R.string.app_notifications_title, + ) + object ExcludeSummarization : ListType( + titleResource = R.string.notification_summarization_manage_excluded_apps_title, + ) + object ExcludeClassification : ListType( + titleResource = R.string.notification_bundle_manage_excluded_apps_title, + ) +} \ No newline at end of file diff --git a/src/com/android/settings/spa/notification/AppNotificationController.kt b/src/com/android/settings/spa/notification/AppNotificationController.kt index 1ce72e01f72..fd35427d5c2 100644 --- a/src/com/android/settings/spa/notification/AppNotificationController.kt +++ b/src/com/android/settings/spa/notification/AppNotificationController.kt @@ -17,12 +17,16 @@ package com.android.settings.spa.notification import android.content.pm.ApplicationInfo +import android.service.notification.Adjustment import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.android.settings.spa.app.storage.StorageAppListModel +import com.android.settings.spa.app.storage.StorageType class AppNotificationController( private val repository: AppNotificationRepository, private val app: ApplicationInfo, + private val listType: ListType, ) { val isEnabled: LiveData get() = _isEnabled @@ -47,4 +51,62 @@ class AppNotificationController( postValue(it) } } + + val isAllowed: LiveData + get() = _isAllowed + + fun getAllowed() = _isAllowed.get() + + fun setAllowed(enabled: Boolean) { + when (listType) { + ListType.ExcludeSummarization -> { + if (repository.setAdjustmentSupportedForPackage( + app, Adjustment.KEY_SUMMARIZATION, enabled)) { + _isAllowed.postValue(enabled) + } + } + ListType.ExcludeClassification -> { + if (repository.setAdjustmentSupportedForPackage( + app, Adjustment.KEY_TYPE, enabled)) { + _isAllowed.postValue(enabled) + } + } + else -> {} + } + } + + private val _isAllowed = object : MutableLiveData() { + override fun onActive() { + when (listType) { + ListType.ExcludeSummarization -> { + postValue(repository.isAdjustmentSupportedForPackage( + app, Adjustment.KEY_SUMMARIZATION)) + } + ListType.ExcludeClassification -> { + postValue(repository.isAdjustmentSupportedForPackage( + app, Adjustment.KEY_TYPE)) + } + else -> {} + } + } + + override fun onInactive() { + } + + fun get(): Boolean = when (listType) { + ListType.ExcludeSummarization -> { + value ?: repository.isAdjustmentSupportedForPackage( + app, Adjustment.KEY_SUMMARIZATION).also { + postValue(it) + } + } + ListType.ExcludeClassification -> { + value ?: repository.isAdjustmentSupportedForPackage( + app, Adjustment.KEY_TYPE).also { + postValue(it) + } + } + else -> false + } + } } diff --git a/src/com/android/settings/spa/notification/AppNotificationRepository.kt b/src/com/android/settings/spa/notification/AppNotificationRepository.kt index d0e700a9e34..fddbf1aae04 100644 --- a/src/com/android/settings/spa/notification/AppNotificationRepository.kt +++ b/src/com/android/settings/spa/notification/AppNotificationRepository.kt @@ -126,6 +126,20 @@ class AppNotificationRepository( } } + fun isAdjustmentSupportedForPackage(app: ApplicationInfo, key: String): Boolean = + notificationManager.isAdjustmentSupportedForPackage(key, app.packageName) + + fun setAdjustmentSupportedForPackage(app: ApplicationInfo, key: String, enabled: Boolean): + Boolean { + return try { + notificationManager.setAdjustmentSupportedForPackage(key, app.packageName, enabled) + true + } catch (e: Exception) { + Log.w(TAG, "Error calling INotificationManager", e) + false + } + } + fun isUserUnlocked(user: Int): Boolean { return try { userManager.isUserUnlocked(user) diff --git a/src/com/android/settings/spa/notification/AppNotificationsListModel.kt b/src/com/android/settings/spa/notification/AppNotificationsListModel.kt index 8a534c8b14d..d979918dac4 100644 --- a/src/com/android/settings/spa/notification/AppNotificationsListModel.kt +++ b/src/com/android/settings/spa/notification/AppNotificationsListModel.kt @@ -36,6 +36,7 @@ import com.android.settingslib.spaprivileged.model.app.AppListModel import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.template.app.AppListItemModel +import com.android.settingslib.spaprivileged.template.app.AppListSwitchItem import com.android.settingslib.spaprivileged.template.app.AppListTwoTargetSwitchItem import com.android.settingslib.utils.StringUtil import kotlinx.coroutines.Dispatchers @@ -52,6 +53,7 @@ data class AppNotificationsRecord( class AppNotificationsListModel( private val context: Context, + private val listType: ListType ) : AppListModel { private val repository = AppNotificationRepository(context) private val now = System.currentTimeMillis() @@ -64,7 +66,7 @@ class AppNotificationsListModel( AppNotificationsRecord( app = app, sentState = usageEvents[app.packageName], - controller = AppNotificationController(repository, app), + controller = AppNotificationController(repository, app, listType), ) } } @@ -129,17 +131,35 @@ class AppNotificationsListModel( @Composable override fun AppListItemModel.AppItem() { - val changeable by produceState(initialValue = false) { - withContext(Dispatchers.Default) { - value = repository.isChangeable(record.app) + when (listType) { + ListType.ExcludeSummarization -> { + AppListSwitchItem( + checked = record.controller.isAllowed.observeAsCallback(), + changeable = { true }, + onCheckedChange = record.controller::setAllowed, + ) + } + ListType.ExcludeClassification -> { + AppListSwitchItem( + checked = record.controller.isAllowed.observeAsCallback(), + changeable = { true }, + onCheckedChange = record.controller::setAllowed, + ) + } + else -> { + val changeable by produceState(initialValue = false) { + withContext(Dispatchers.Default) { + value = repository.isChangeable(record.app) + } + } + AppListTwoTargetSwitchItem( + onClick = { navigateToAppNotificationSettings(app = record.app) }, + checked = record.controller.isEnabled.observeAsCallback(), + changeable = { changeable }, + onCheckedChange = record.controller::setEnabled, + ) } } - AppListTwoTargetSwitchItem( - onClick = { navigateToAppNotificationSettings(app = record.app) }, - checked = record.controller.isEnabled.observeAsCallback(), - changeable = { changeable }, - onCheckedChange = record.controller::setEnabled, - ) } private fun navigateToAppNotificationSettings(app: ApplicationInfo) { diff --git a/src/com/android/settings/spa/notification/NotificationMain.kt b/src/com/android/settings/spa/notification/NotificationMain.kt index b3c7a55465c..6859aa916f6 100644 --- a/src/com/android/settings/spa/notification/NotificationMain.kt +++ b/src/com/android/settings/spa/notification/NotificationMain.kt @@ -41,7 +41,7 @@ object NotificationMainPageProvider : SettingsPageProvider { @Composable override fun Page(arguments: Bundle?) { RegularScaffold(title = getTitle(arguments)) { - AppListNotificationsPageProvider.EntryItem() + AppListNotificationsPageProvider.AllApps.EntryItem() } } diff --git a/tests/robotests/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceControllerTest.java new file mode 100644 index 00000000000..ec68a7a14d4 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceControllerTest.java @@ -0,0 +1,151 @@ +/* + * 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.notification; + +import static android.service.notification.Adjustment.KEY_SUMMARIZATION; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.Flags; +import android.app.INotificationManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; + +import com.android.settingslib.applications.ApplicationsState; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags({Flags.FLAG_NM_SUMMARIZATION_UI, Flags.FLAG_NM_SUMMARIZATION, + Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI}) +public class AdjustmentExcludedAppsPreferenceControllerTest { + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Mock + private NotificationBackend mBackend; + @Mock + private ApplicationsState mApplicationState; + private AdjustmentExcludedAppsPreferenceController mController; + private Context mContext; + @Mock + INotificationManager mInm; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + + mController = new AdjustmentExcludedAppsPreferenceController(mContext, "key"); + mController.onAttach(null, mock(Fragment.class), mBackend, KEY_SUMMARIZATION); + PreferenceScreen screen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + mController.mPreferenceCategory = new PreferenceCategory(mContext); + screen.addPreference(mController.mPreferenceCategory); + + mController.mApplicationsState = mApplicationState; + mController.mPrefContext = mContext; + } + + @Test + public void testIsAvailable() { + when(mBackend.isNotificationBundlingSupported()).thenReturn(true); + when(mBackend.isNotificationSummarizationSupported()).thenReturn(true); + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_flagEnabledNasDoesNotSupport_shouldReturnFalse() throws Exception { + when(mInm.getUnsupportedAdjustmentTypes()).thenReturn(List.of(KEY_SUMMARIZATION)); + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void testUpdateAppList() throws Exception { + when(mBackend.getAdjustmentDeniedPackages(KEY_SUMMARIZATION)).thenReturn( + new String[] {"cannot", "cannot2"}); + + // GIVEN there are four apps, and two have KEY_SUMMARIZATION off + ApplicationsState.AppEntry canSummarize = + mock(ApplicationsState.AppEntry.class); + canSummarize.info = new ApplicationInfo(); + canSummarize.info.packageName = "canSummarize"; + canSummarize.info.uid = 0; + + ApplicationsState.AppEntry canSummarize2 = mock(ApplicationsState.AppEntry.class); + canSummarize2.info = new ApplicationInfo(); + canSummarize2.info.packageName = "canSummarizeTwo"; + canSummarize2.info.uid = 0; + + ApplicationsState.AppEntry cannot = + mock(ApplicationsState.AppEntry.class); + cannot.info = new ApplicationInfo(); + cannot.info.packageName = "cannot"; + cannot.info.uid = 0; + + ApplicationsState.AppEntry cannot2 = + mock(ApplicationsState.AppEntry.class); + cannot2.info = new ApplicationInfo(); + cannot2.info.packageName = "cannot2"; + cannot2.info.uid = 0; + + List appEntries = new ArrayList<>(); + appEntries.add(canSummarize); + appEntries.add(canSummarize2); + appEntries.add(cannot); + appEntries.add(cannot2); + + // WHEN the controller updates the app list with the app entries + mController.updateAppList(appEntries); + + // THEN only the 'cannot' entries make it to the app list + assertThat(mController.mPreferenceCategory.getPreferenceCount()).isEqualTo(2); + assertThat((Preference) mController.mPreferenceCategory.findPreference( + AdjustmentExcludedAppsPreferenceController.getKey( + cannot.info.packageName,cannot.info.uid))).isNotNull(); + assertThat((Preference) mController.mPreferenceCategory.findPreference( + AdjustmentExcludedAppsPreferenceController.getKey( + cannot2.info.packageName,cannot2.info.uid))).isNotNull(); + } + + @Test + public void testUpdateAppList_nullApps() { + mController.updateAppList(null); + assertThat(mController.mPreferenceCategory.getPreferenceCount()).isEqualTo(0); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/BundleManageAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/BundleManageAppsPreferenceControllerTest.java new file mode 100644 index 00000000000..7f595b5c7e0 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/BundleManageAppsPreferenceControllerTest.java @@ -0,0 +1,79 @@ +/* + * 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.notification; + +import static android.service.notification.Adjustment.KEY_SUMMARIZATION; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.app.Flags; +import android.app.INotificationManager; +import android.content.Context; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.Adjustment; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class BundleManageAppsPreferenceControllerTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private static final String PREFERENCE_KEY = "preference_key"; + + private Context mContext; + BundleManageAppsPreferenceController mController; + @Mock + INotificationManager mInm; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI); + mController = new BundleManageAppsPreferenceController(mContext, PREFERENCE_KEY); + mController.mBackend.setNm(mInm); + } + + @Test + public void isAvailable_flagEnabledNasSupports_shouldReturnTrue() { + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_flagEnabledNasDoesNotSupport_shouldReturnFalse() throws Exception { + when(mInm.getUnsupportedAdjustmentTypes()).thenReturn(List.of(Adjustment.KEY_TYPE)); + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_flagDisabledNasSupports_shouldReturnFalse() { + mSetFlagsRule.disableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI); + assertThat(mController.isAvailable()).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/SummarizationManageAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/SummarizationManageAppsPreferenceControllerTest.java new file mode 100644 index 00000000000..b6cedb86585 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/SummarizationManageAppsPreferenceControllerTest.java @@ -0,0 +1,81 @@ +/* + * 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.notification; + +import static android.service.notification.Adjustment.KEY_IMPORTANCE; +import static android.service.notification.Adjustment.KEY_SUMMARIZATION; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import android.app.Flags; +import android.app.INotificationManager; +import android.content.Context; +import android.platform.test.flag.junit.SetFlagsRule; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class SummarizationManageAppsPreferenceControllerTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private static final String PREFERENCE_KEY = "preference_key"; + + private Context mContext; + SummarizationManageAppsPreferenceController mController; + @Mock + INotificationManager mInm; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mSetFlagsRule.enableFlags(Flags.FLAG_NM_SUMMARIZATION, Flags.FLAG_NM_SUMMARIZATION_UI); + mController = new SummarizationManageAppsPreferenceController(mContext, PREFERENCE_KEY); + mController.mBackend.setNm(mInm); + } + + @Test + public void isAvailable_flagEnabledNasSupports_shouldReturnTrue() { + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_flagEnabledNasDoesNotSupport_shouldReturnFalse() throws Exception { + when(mInm.getUnsupportedAdjustmentTypes()).thenReturn(List.of(KEY_SUMMARIZATION)); + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + public void isAvailable_flagDisabledNasSupports_shouldReturnFalse() { + mSetFlagsRule.disableFlags(Flags.FLAG_NM_SUMMARIZATION); + mSetFlagsRule.disableFlags(Flags.FLAG_NM_SUMMARIZATION_UI); + assertThat(mController.isAvailable()).isFalse(); + } +}