diff --git a/Android.bp b/Android.bp index 150bdaf84a1..c0a4c7c24f2 100644 --- a/Android.bp +++ b/Android.bp @@ -139,6 +139,7 @@ android_library { "aconfig_settings_flags", "aconfig_settingslib_flags", "android.app.flags-aconfig", + "android.app.supervision.flags-aconfig", "android.provider.flags-aconfig", "android.security.flags-aconfig", "android.view.contentprotection.flags-aconfig", diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8f5699c7572..90119a9a57b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2817,6 +2817,15 @@ + + + + + + + - + android:layout_gravity="center_horizontal" + android:gravity="center_vertical" + android:paddingEnd="36dp"> + + + + diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml index 2c83a27e557..4d0f7abee5d 100644 --- a/res/values-night/colors.xml +++ b/res/values-night/colors.xml @@ -94,8 +94,5 @@ @android:color/system_accent1_100 @android:color/system_accent1_100 - - - @android:color/system_on_primary_container_dark diff --git a/res/values/arrays.xml b/res/values/arrays.xml index e653c63a507..80c3e8d2c4b 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -1059,6 +1059,20 @@ either_charging_or_docked + + @string/when_to_show_hubmode_never + @string/when_to_show_hubmode_charging + @string/when_to_show_hubmode_charging_and_upright + @string/when_to_show_hubmode_docked + + + + never + while_charging + while_charging_and_upright + while_docked + + @string/zen_mode_from_anyone @string/zen_mode_from_contacts diff --git a/res/values/colors.xml b/res/values/colors.xml index 3201dca98ea..9c9a29c79f9 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -64,8 +64,8 @@ @color/homepage_pink_bg @color/homepage_pink_fg @color/homepage_pink_bg - @color/homepage_pink_fg - @color/homepage_pink_bg + @color/homepage_orange_fg + @color/homepage_orange_bg @color/homepage_orange_fg @color/homepage_orange_bg @color/homepage_orange_fg @@ -275,8 +275,4 @@ @android:color/system_accent1_800 @android:color/system_accent1_800 @android:color/system_neutral2_50 - - - @android:color/system_on_primary_dark - @android:color/system_outline_light diff --git a/res/values/config.xml b/res/values/config.xml index c20f08035c2..9d2e43df985 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -810,13 +810,23 @@ - + @layout/accessibility_text_reading_preview_app_grid @layout/screen_zoom_preview_1 @layout/accessibility_text_reading_preview_mail_content + + + @string/preview_pager_home_content_description + @string/preview_pager_message_content_description + @string/preview_pager_email_content_description + + com.android.vending diff --git a/res/values/strings.xml b/res/values/strings.xml index 79020f903d0..0f92d31fa20 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -78,6 +78,12 @@ Preview + + Home screen preview + + Message preview + + Email preview QR code @@ -497,7 +503,7 @@ https://support.google.com/android?p=per_language_app_settings - Change region to %s ? + Change region to %s? Your device will keep %s as a system language @@ -506,7 +512,7 @@ %1$s will replace %2$s in your preferred languages - Change system language to %s ? + Change system language to %s? Add %s to preferred languages? @@ -547,7 +553,7 @@ More language settings - Change region to %1$s ? + Change region to %1$s? Your device will keep %1$s as a system language @@ -833,6 +839,8 @@ Face added Setup needed + + Add face Face @@ -1006,6 +1014,8 @@ } Setup needed + + Add fingerprint Set up your fingerprint @@ -1771,6 +1781,8 @@ None + + Add PIN, pattern, password, or swipe Swipe @@ -3655,6 +3667,21 @@ Communal Communal settings + + Hub mode + + Widgets on lock screen + + + When to automatically show + + Never + + While charging + + While charging and upright + + While docked @@ -3897,6 +3924,18 @@ Bluetooth tethering + + + IP address + + MAC address + + Transit link speed + + Receive link speed + + Network usage + Ethernet tethering @@ -5691,6 +5730,9 @@ Longer Auto click time + + + Use autoclick Autoclick shortcut @@ -14309,5 +14351,7 @@ Data usage charges may apply. Allow all sites + + Enter supervision PIN %1$s animation diff --git a/res/xml/accessibility_autoclick_settings.xml b/res/xml/accessibility_autoclick_settings.xml index 3878e6c5c9b..02f53c0e4bb 100644 --- a/res/xml/accessibility_autoclick_settings.xml +++ b/res/xml/accessibility_autoclick_settings.xml @@ -30,6 +30,11 @@ settings:searchable="false" settings:lottie_rawRes="@drawable/accessibility_dwell"/> + + + + + + + + + + + + + + diff --git a/res/xml/mouse_settings.xml b/res/xml/mouse_settings.xml index ec1c39d31d2..98026bdcad2 100644 --- a/res/xml/mouse_settings.xml +++ b/res/xml/mouse_settings.xml @@ -27,7 +27,7 @@ android:order="10" settings:controller="com.android.settings.inputmethod.MousePointerAccelerationPreferenceController" /> - - + - + - - - - - + diff --git a/res/xml/touchpad_settings.xml b/res/xml/touchpad_settings.xml index 7369d485cbb..c71790ee93a 100644 --- a/res/xml/touchpad_settings.xml +++ b/res/xml/touchpad_settings.xml @@ -61,7 +61,7 @@ settings:controller="com.android.settings.inputmethod.TouchpadAccelerationPreferenceController" android:order="38"/> - + + diff --git a/res/xml/widgets_on_lockscreen_settings.xml b/res/xml/widgets_on_lockscreen_settings.xml new file mode 100644 index 00000000000..056df63ebd4 --- /dev/null +++ b/res/xml/widgets_on_lockscreen_settings.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/src/com/android/settings/accessibility/TextReadingPreviewController.java b/src/com/android/settings/accessibility/TextReadingPreviewController.java index 99f1f3fa0c3..e268aaa981f 100644 --- a/src/com/android/settings/accessibility/TextReadingPreviewController.java +++ b/src/com/android/settings/accessibility/TextReadingPreviewController.java @@ -104,11 +104,13 @@ class TextReadingPreviewController extends BasePreferenceController implements final boolean isLayoutRtl = origConfig.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; final int[] previewSamples = getPreviewSampleLayouts(mContext); + final int[] previewContentDescriptions = getPreviewSampleContentDescriptions(mContext); final PreviewPagerAdapter pagerAdapter = new PreviewPagerAdapter(mContext, isLayoutRtl, previewSamples, createConfig(origConfig)); mPreviewPreference.setPreviewAdapter(pagerAdapter); mPreviewPreference.setCurrentItem( isLayoutRtl ? previewSamples.length - 1 : FRAME_INITIAL_INDEX); + mPreviewPreference.setContentDescription(previewContentDescriptions); final int initialPagerIndex = mLastFontProgress * mDisplaySizeData.getValues().size() + mLastDisplayProgress; @@ -188,6 +190,20 @@ class TextReadingPreviewController extends BasePreferenceController implements return previewSamples; } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static int[] getPreviewSampleContentDescriptions(Context context) { + TypedArray typedArray = context.getResources().obtainTypedArray( + R.array.config_text_reading_preview_content_descriptions); + int previewCount = typedArray.length(); + int[] previewContentDescriptions = new int[previewCount]; + for (int i = 0; i < previewCount; i++) { + previewContentDescriptions[i] = + typedArray.getResourceId(i, R.string.preview_pager_content_description); + } + typedArray.recycle(); + return previewContentDescriptions; + } + private int getPagerIndex() { final int displayDataSize = mDisplaySizeData.getValues().size(); final int fontSizeProgress = mFontSizePreference.getProgress(); diff --git a/src/com/android/settings/accessibility/TextReadingPreviewPreference.java b/src/com/android/settings/accessibility/TextReadingPreviewPreference.java index 5c866d9d42c..a4676baf58a 100644 --- a/src/com/android/settings/accessibility/TextReadingPreviewPreference.java +++ b/src/com/android/settings/accessibility/TextReadingPreviewPreference.java @@ -43,28 +43,11 @@ public class TextReadingPreviewPreference extends Preference { private int mCurrentItem; private int mLastLayerIndex; private PreviewPagerAdapter mPreviewAdapter; + private int[] mContentDescriptions; private int mLayoutMinHorizontalPadding = 0; private int mBackgroundMinHorizontalPadding = 0; - private final ViewPager.OnPageChangeListener mPageChangeListener = - new ViewPager.OnPageChangeListener() { - @Override - public void onPageScrolled(int i, float v, int i1) { - // Do nothing - } - - @Override - public void onPageSelected(int i) { - mCurrentItem = i; - } - - @Override - public void onPageScrollStateChanged(int i) { - // Do nothing - } - }; - TextReadingPreviewPreference(Context context) { super(context); init(); @@ -95,7 +78,23 @@ public class TextReadingPreviewPreference extends Preference { adjustPaddings(previewLayout, backgroundView); final ViewPager viewPager = (ViewPager) holder.findViewById(R.id.preview_pager); - viewPager.addOnPageChangeListener(mPageChangeListener); + viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int i, float v, int i1) { + // Do nothing + } + + @Override + public void onPageSelected(int i) { + mCurrentItem = i; + viewPager.setContentDescription(getContext().getString(mContentDescriptions[i])); + } + + @Override + public void onPageScrollStateChanged(int i) { + // Do nothing + } + }); final DotsPageIndicator pageIndicator = (DotsPageIndicator) holder.findViewById(R.id.page_indicator); updateAdapterIfNeeded(viewPager, pageIndicator, mPreviewAdapter); @@ -122,6 +121,10 @@ public class TextReadingPreviewPreference extends Preference { viewPager.setCurrentItem(getCurrentItem() + 1)); nextButton.setContentDescription(getContext().getString( R.string.preview_pager_next_button)); + + // Initialize the content description since the OnPageChangeListener#onPageSelected won't + // be called during setup. + viewPager.setContentDescription(getContext().getString(mContentDescriptions[0])); } @Override @@ -170,6 +173,10 @@ public class TextReadingPreviewPreference extends Preference { ); } + void setContentDescription(int[] stringIds) { + mContentDescriptions = stringIds; + } + void setPreviewAdapter(PreviewPagerAdapter previewAdapter) { if (previewAdapter != mPreviewAdapter) { mPreviewAdapter = previewAdapter; diff --git a/src/com/android/settings/accessibility/ToggleAutoclickMainSwitchPreferenceController.java b/src/com/android/settings/accessibility/ToggleAutoclickMainSwitchPreferenceController.java new file mode 100644 index 00000000000..1ecdac9eef8 --- /dev/null +++ b/src/com/android/settings/accessibility/ToggleAutoclickMainSwitchPreferenceController.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accessibility; + +import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; +import static com.android.settings.accessibility.AccessibilityUtil.State.ON; + +import android.content.ContentResolver; +import android.content.Context; +import android.provider.Settings; + +import androidx.annotation.NonNull; + +import com.android.server.accessibility.Flags; +import com.android.settings.R; +import com.android.settings.core.TogglePreferenceController; + +/** The controller to handle main switch to turn on or turn off accessibility autoclick. */ +public class ToggleAutoclickMainSwitchPreferenceController + extends TogglePreferenceController { + + private final ContentResolver mContentResolver; + + public ToggleAutoclickMainSwitchPreferenceController( + @NonNull Context context, @NonNull String preferenceKey) { + super(context, preferenceKey); + mContentResolver = context.getContentResolver(); + } + + @Override + public int getAvailabilityStatus() { + return Flags.enableAutoclickIndicator() ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public boolean isChecked() { + return Settings.Secure.getInt(mContentResolver, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, OFF) == ON; + } + + @Override + public boolean setChecked(boolean isChecked) { + Settings.Secure.putInt(mContentResolver, + Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, + isChecked ? ON : OFF); + + return true; + } + + @Override + public int getSliceHighlightMenuRes() { + return R.string.menu_key_system; + } +} diff --git a/src/com/android/settings/applications/appcompat/OWNERS b/src/com/android/settings/applications/appcompat/OWNERS new file mode 100644 index 00000000000..5ef6ded95f1 --- /dev/null +++ b/src/com/android/settings/applications/appcompat/OWNERS @@ -0,0 +1,5 @@ +# Bug component: 970984 +# Large Screen Experiences App Compat +gracielawputri@google.com +mcarli@google.com +mariiasand@google.com \ No newline at end of file diff --git a/src/com/android/settings/biometrics/BiometricSettingsProvider.kt b/src/com/android/settings/biometrics/BiometricSettingsProvider.kt index 308f24508e3..ee4cf4879c1 100644 --- a/src/com/android/settings/biometrics/BiometricSettingsProvider.kt +++ b/src/com/android/settings/biometrics/BiometricSettingsProvider.kt @@ -30,7 +30,7 @@ class BiometricSettingsProvider : ContentProvider() { } override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { - throw UnsupportedOperationException("query operation not supported currently.") + throw UnsupportedOperationException("delete operation not supported currently.") } override fun getType(uri: Uri): String? { diff --git a/src/com/android/settings/biometrics/ParentalControlsUtils.java b/src/com/android/settings/biometrics/ParentalControlsUtils.java index 04ab1b17596..e9c76e55263 100644 --- a/src/com/android/settings/biometrics/ParentalControlsUtils.java +++ b/src/com/android/settings/biometrics/ParentalControlsUtils.java @@ -31,6 +31,7 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.supervision.SupervisionRestrictionsHelper; /** * Utilities for things at the cross-section of biometrics and parental controls. For example, @@ -59,12 +60,7 @@ public class ParentalControlsUtils { UserManager.DISALLOW_BIOMETRIC, userHandle); } - final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); - final SupervisionManager sm = - android.app.supervision.flags.Flags.deprecateDpmSupervisionApis() - ? context.getSystemService(SupervisionManager.class) - : null; - return parentConsentRequiredInternal(dpm, sm, modality, userHandle); + return parentConsentRequiredInternal(context, modality, userHandle); } /** @@ -74,18 +70,22 @@ public class ParentalControlsUtils { @Nullable @VisibleForTesting static RestrictedLockUtils.EnforcedAdmin parentConsentRequiredInternal( - @NonNull DevicePolicyManager dpm, - @Nullable SupervisionManager sm, + @NonNull Context context, @BiometricAuthenticator.Modality int modality, @NonNull UserHandle userHandle) { + final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); + final SupervisionManager sm = + android.app.supervision.flags.Flags.deprecateDpmSupervisionApis() + ? context.getSystemService(SupervisionManager.class) + : null; + if (!ParentalControlsUtilsInternal.parentConsentRequired( dpm, sm, modality, userHandle)) { return null; } if (android.app.supervision.flags.Flags.deprecateDpmSupervisionApis()) { - // Supervision doesn't necessarily have have an admin component. - return new RestrictedLockUtils.EnforcedAdmin( - /* component= */ null, UserManager.DISALLOW_BIOMETRIC, userHandle); + return SupervisionRestrictionsHelper.createEnforcedAdmin( + context, UserManager.DISALLOW_BIOMETRIC, userHandle); } else { final ComponentName cn = ParentalControlsUtilsInternal.getSupervisionComponentName(dpm, userHandle); diff --git a/src/com/android/settings/biometrics/face/FaceSettings.java b/src/com/android/settings/biometrics/face/FaceSettings.java index d2e2a80a406..4e129f0744a 100644 --- a/src/com/android/settings/biometrics/face/FaceSettings.java +++ b/src/com/android/settings/biometrics/face/FaceSettings.java @@ -70,6 +70,7 @@ public class FaceSettings extends DashboardFragment { private static final String TAG = "FaceSettings"; private static final String KEY_TOKEN = "hw_auth_token"; + private static final String KEY_CONFIRMING_PASSWORD = "confirming_password"; private static final String KEY_RE_ENROLL_FACE = "re_enroll_face_unlock"; private static final String KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED = "biometrics_successfully_authenticated"; @@ -163,6 +164,7 @@ public class FaceSettings extends DashboardFragment { public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putByteArray(KEY_TOKEN, mToken); + outState.putBoolean(KEY_CONFIRMING_PASSWORD, mConfirmingPassword); } @Override @@ -273,6 +275,7 @@ public class FaceSettings extends DashboardFragment { if (savedInstanceState != null) { mToken = savedInstanceState.getByteArray(KEY_TOKEN); + mConfirmingPassword = savedInstanceState.getBoolean(KEY_CONFIRMING_PASSWORD); } if (Flags.biometricsOnboardingEducation()) { diff --git a/src/com/android/settings/biometrics/face/FaceStatusUtils.java b/src/com/android/settings/biometrics/face/FaceStatusUtils.java index 302d6773213..ee5277edf7a 100644 --- a/src/com/android/settings/biometrics/face/FaceStatusUtils.java +++ b/src/com/android/settings/biometrics/face/FaceStatusUtils.java @@ -98,11 +98,14 @@ public class FaceStatusUtils { return mContext.getString( com.android.settingslib.widget.restricted.R.string.disabled_by_admin); } else { + int summaryNoneResId = Flags.biometricsOnboardingEducation() + ? R.string.security_settings_face_preference_summary_none_new + : R.string.security_settings_face_preference_summary_none; return mContext.getResources() .getString( hasEnrolled() ? R.string.security_settings_face_preference_summary - : R.string.security_settings_face_preference_summary_none); + : summaryNoneResId); } } diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintStatusUtils.java b/src/com/android/settings/biometrics/fingerprint/FingerprintStatusUtils.java index 1ca564c8e35..b7ca3aa0002 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintStatusUtils.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintStatusUtils.java @@ -108,7 +108,9 @@ public class FingerprintStatusUtils { R.string.security_settings_fingerprint_preference_summary); } else { return mContext.getString( - R.string.security_settings_fingerprint_preference_summary_none); + Flags.biometricsOnboardingEducation() + ? R.string.security_settings_fingerprint_preference_summary_none_new + : R.string.security_settings_fingerprint_preference_summary_none); } } diff --git a/src/com/android/settings/communal/CommunalPreferenceController.java b/src/com/android/settings/communal/CommunalPreferenceController.java index e44afab90a6..16eb0e9dfc4 100644 --- a/src/com/android/settings/communal/CommunalPreferenceController.java +++ b/src/com/android/settings/communal/CommunalPreferenceController.java @@ -39,15 +39,14 @@ public class CommunalPreferenceController extends BasePreferenceController { * Returns whether communal preferences are available. */ public static boolean isAvailable(Context context) { + if (com.android.systemui.Flags.glanceableHubV2()) { + return false; + } + if (!Utils.canCurrentUserDream(context)) { return false; } - if (context.getResources().getBoolean(R.bool.config_show_communal_settings)) { - return true; - } - - return com.android.systemui.Flags.glanceableHubV2() - && context.getResources().getBoolean(R.bool.config_show_communal_settings_mobile); + return context.getResources().getBoolean(R.bool.config_show_communal_settings); } } diff --git a/src/com/android/settings/communal/WhenToStartHubPicker.java b/src/com/android/settings/communal/WhenToStartHubPicker.java new file mode 100644 index 00000000000..59233257a30 --- /dev/null +++ b/src/com/android/settings/communal/WhenToStartHubPicker.java @@ -0,0 +1,179 @@ +/* + * 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.communal; + +import static android.provider.Settings.Secure.GLANCEABLE_HUB_START_CHARGING; +import static android.provider.Settings.Secure.GLANCEABLE_HUB_START_CHARGING_UPRIGHT; +import static android.provider.Settings.Secure.GLANCEABLE_HUB_START_DOCKED; +import static android.provider.Settings.Secure.GLANCEABLE_HUB_START_NEVER; +import static android.provider.Settings.Secure.WHEN_TO_START_GLANCEABLE_HUB; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.provider.Settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.R; +import com.android.settings.widget.RadioButtonPickerFragment; +import com.android.settingslib.widget.CandidateInfo; + +import java.util.ArrayList; +import java.util.List; + +/** + * Fragment that provides radio buttons to allow the user to choose when the hub should auto-start. + */ +public class WhenToStartHubPicker extends RadioButtonPickerFragment { + private static final String TAG = "WhenToStartHubPicker"; + private static final String SHOW_WHILE_CHARGING = "while_charging"; + private static final String SHOW_WHILE_DOCKED = "while_docked"; + private static final String SHOW_WHILE_CHARGING_AND_UPRIGHT = "while_charging_and_upright"; + private static final String SHOW_NEVER = "never"; + + private Context mContext; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + mContext = context; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.when_to_start_hubmode_settings; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.WHEN_TO_SHOW_WIDGETS_ON_LOCKSCREEN; + } + + @Override + protected List getCandidates() { + final List candidates = new ArrayList<>(); + + final String[] entries = entries(); + final String[] values = keys(); + + if (entries == null || entries.length <= 0) return candidates; + if (values == null || values.length != entries.length) { + throw new IllegalArgumentException("Entries and values must be of the same length."); + } + + for (int i = 0; i < entries.length; i++) { + candidates.add(new WhenToStartHubCandidateInfo(entries[i], values[i])); + } + + return candidates; + } + + private String[] entries() { + return getResources().getStringArray(R.array.when_to_start_hubmode_entries); + } + + private String[] keys() { + return getResources().getStringArray(R.array.when_to_start_hubmode_values); + } + + @Override + protected String getDefaultKey() { + final int defaultValue = mContext.getResources().getInteger( + com.android.internal.R.integer.config_whenToStartHubModeDefault); + final int setting = Settings.Secure.getInt( + mContext.getContentResolver(), WHEN_TO_START_GLANCEABLE_HUB, defaultValue); + return getKeyFromSetting(setting); + } + + @Override + protected boolean setDefaultKey(String key) { + Settings.Secure.putInt( + mContext.getContentResolver(), + WHEN_TO_START_GLANCEABLE_HUB, + getSettingFromPrefKey(key)); + + return true; + } + + @Override + protected void onSelectionPerformed(boolean success) { + super.onSelectionPerformed(success); + + getActivity().finish(); + } + + + @Settings.Secure.WhenToStartGlanceableHub + private static int getSettingFromPrefKey(String key) { + switch (key) { + case SHOW_WHILE_CHARGING: + return GLANCEABLE_HUB_START_CHARGING; + case SHOW_WHILE_DOCKED: + return GLANCEABLE_HUB_START_DOCKED; + case SHOW_WHILE_CHARGING_AND_UPRIGHT: + return GLANCEABLE_HUB_START_CHARGING_UPRIGHT; + case SHOW_NEVER: + default: + return GLANCEABLE_HUB_START_NEVER; + } + } + + private static String getKeyFromSetting(@Settings.Secure.WhenToStartGlanceableHub int setting) { + switch (setting) { + case GLANCEABLE_HUB_START_CHARGING: + return SHOW_WHILE_CHARGING; + case GLANCEABLE_HUB_START_DOCKED: + return SHOW_WHILE_DOCKED; + case GLANCEABLE_HUB_START_CHARGING_UPRIGHT: + return SHOW_WHILE_CHARGING_AND_UPRIGHT; + case GLANCEABLE_HUB_START_NEVER: + default: + return SHOW_NEVER; + } + } + + private static final class WhenToStartHubCandidateInfo extends CandidateInfo { + private final String mName; + private final String mKey; + + WhenToStartHubCandidateInfo(String title, String value) { + super(true); + + mName = title; + mKey = value; + } + + @Override + public CharSequence loadLabel() { + return mName; + } + + @Override + @Nullable + public Drawable loadIcon() { + return null; + } + + @Override + public String getKey() { + return mKey; + } + } +} diff --git a/src/com/android/settings/communal/WhenToStartHubPreferenceController.java b/src/com/android/settings/communal/WhenToStartHubPreferenceController.java new file mode 100644 index 00000000000..67931ee7f65 --- /dev/null +++ b/src/com/android/settings/communal/WhenToStartHubPreferenceController.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.communal; + +import static android.provider.Settings.Secure.GLANCEABLE_HUB_START_CHARGING; +import static android.provider.Settings.Secure.GLANCEABLE_HUB_START_CHARGING_UPRIGHT; +import static android.provider.Settings.Secure.GLANCEABLE_HUB_START_DOCKED; +import static android.provider.Settings.Secure.GLANCEABLE_HUB_START_NEVER; +import static android.provider.Settings.Secure.WHEN_TO_START_GLANCEABLE_HUB; + +import android.annotation.StringRes; +import android.content.Context; +import android.provider.Settings; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.PreferenceControllerMixin; + +/** + * A preference controller that is responsible for showing the "when to auto start hub" setting in + * hub settings. + */ +public class WhenToStartHubPreferenceController extends BasePreferenceController implements + PreferenceControllerMixin { + public WhenToStartHubPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + preference.setSummary(getSummaryResId()); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public CharSequence getSummary() { + return mContext.getString(getSummaryResId()); + } + + @StringRes + private int getSummaryResId() { + final int setting = Settings.Secure.getInt( + mContext.getContentResolver(), + WHEN_TO_START_GLANCEABLE_HUB, + GLANCEABLE_HUB_START_NEVER); + + switch (setting) { + case GLANCEABLE_HUB_START_CHARGING: + return R.string.when_to_show_hubmode_charging; + case GLANCEABLE_HUB_START_DOCKED: + return R.string.when_to_show_hubmode_docked; + case GLANCEABLE_HUB_START_CHARGING_UPRIGHT: + return R.string.when_to_show_hubmode_charging_and_upright; + case GLANCEABLE_HUB_START_NEVER: + default: + return R.string.when_to_show_hubmode_never; + } + } +} diff --git a/src/com/android/settings/communal/WidgetsOnLockscreenFragment.java b/src/com/android/settings/communal/WidgetsOnLockscreenFragment.java new file mode 100644 index 00000000000..09a1e4d1c3e --- /dev/null +++ b/src/com/android/settings/communal/WidgetsOnLockscreenFragment.java @@ -0,0 +1,50 @@ +/* + * 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.communal; + +import android.app.settings.SettingsEnums; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; + +/** + * Fragment that contains settings related to communal hub. + */ +@SearchIndexable +public class WidgetsOnLockscreenFragment extends DashboardFragment { + private static final String TAG = "WidgetsOnLockscreenFragment"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.WIDGETS_ON_LOCK_SCREEN; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.widgets_on_lockscreen_settings; + } + + @Override + protected String getLogTag() { + return TAG; + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.widgets_on_lockscreen_settings); +} diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java index 68cb8226b7b..8050b4056f6 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java @@ -26,9 +26,14 @@ import android.bluetooth.BluetoothLeBroadcastAssistant; import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.database.ContentObserver; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; import android.util.Log; import androidx.annotation.NonNull; @@ -87,6 +92,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro @Nullable private final LocalBluetoothProfileManager mProfileManager; @Nullable private final LocalBluetoothLeBroadcast mBroadcast; @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant; + @Nullable private final ContentResolver mContentResolver; private final Executor mExecutor; private final MetricsFeatureProvider mMetricsFeatureProvider; @Nullable private PreferenceGroup mPreferenceGroup; @@ -187,6 +193,17 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro @NonNull BluetoothLeBroadcastReceiveState state) {} }; + @VisibleForTesting + ContentObserver mSettingsObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange) { + Log.d(TAG, "onChange, primary group id has been changed, refresh list"); + if (mBluetoothDeviceUpdater != null) { + mBluetoothDeviceUpdater.refreshPreference(); + } + } + }; + public AudioSharingDevicePreferenceController(Context context) { super(context, KEY); mBtManager = Utils.getLocalBtManager(mContext); @@ -198,6 +215,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro mProfileManager == null ? null : mProfileManager.getLeAudioBroadcastAssistantProfile(); + mContentResolver = context.getContentResolver(); mExecutor = Executors.newSingleThreadExecutor(); mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); } @@ -217,6 +235,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro if (mEventManager == null || mAssistant == null || mDialogHandler == null + || mContentResolver == null || mBluetoothDeviceUpdater == null) { Log.d(TAG, "Skip onStart(), profile is not ready."); return; @@ -225,6 +244,10 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro mEventManager.registerCallback(this); mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); mDialogHandler.registerCallbacks(mExecutor); + mContentResolver.registerContentObserver( + Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast()), + false, + mSettingsObserver); mBluetoothDeviceUpdater.registerCallback(); mBluetoothDeviceUpdater.refreshPreference(); mIsAudioModeOngoingCall.set(isAudioModeOngoingCall(mContext)); @@ -245,6 +268,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro if (mEventManager == null || mAssistant == null || mDialogHandler == null + || mContentResolver == null || mBluetoothDeviceUpdater == null) { Log.d(TAG, "Skip onStop(), profile is not ready."); return; @@ -253,6 +277,7 @@ public class AudioSharingDevicePreferenceController extends BasePreferenceContro mEventManager.unregisterCallback(this); mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); mDialogHandler.unregisterCallbacks(); + mContentResolver.unregisterContentObserver(mSettingsObserver); mBluetoothDeviceUpdater.unregisterCallback(); }); } diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java index 6037577162f..a9cca9ddc99 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java @@ -40,6 +40,7 @@ import com.android.settings.dashboard.DashboardFragment; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.flags.Flags; import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.LayoutPreference; @@ -88,8 +89,10 @@ public class AudioStreamHeaderController extends BasePreferenceController var localSourceState = getLocalSourceState(state); if (localSourceState == STREAMING) { updateSummary(); - mAudioStreamsHelper.startMediaService( - mContext, mBroadcastId, mBroadcastName); + if (!Flags.audioStreamMediaServiceByReceiveState()) { + mAudioStreamsHelper.startMediaService( + mContext, mBroadcastId, mBroadcastName); + } } else if (mHysteresisModeFixAvailable && localSourceState == PAUSED) { // if source paused, only update the summary updateSummary(); diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java index ec8d7bcc206..549046f557b 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java @@ -16,9 +16,13 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PRIVATE_BROADCAST_RECEIVE_DATA; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.DECRYPTION_FAILED; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED; import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING; +import static java.util.Collections.emptyList; + import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -56,11 +60,14 @@ import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.PrivateBroadcastReceiveData; import com.android.settingslib.bluetooth.VolumeControlProfile; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.flags.Flags; import com.android.settingslib.utils.ThreadUtils; import java.util.HashMap; +import java.util.List; import java.util.Map; public class AudioStreamMediaService extends Service { @@ -103,7 +110,7 @@ public class AudioStreamMediaService extends Service { private final PlaybackState.Builder mPlayStateHysteresisBuilder = new PlaybackState.Builder() .setState( - PlaybackState.STATE_STOPPED, + PlaybackState.STATE_PAUSED, STATIC_PLAYBACK_POSITION, ZERO_PLAYBACK_SPEED) .addCustomAction( @@ -122,7 +129,9 @@ public class AudioStreamMediaService extends Service { private int mLatestPositiveVolume = 25; private boolean mHysteresisModeFixAvailable; private int mBroadcastId; - @Nullable private Map mStateByDevice; + @VisibleForTesting + @Nullable + Map mStateByDevice; @Nullable private LocalBluetoothManager mLocalBtManager; @Nullable private AudioStreamsHelper mAudioStreamsHelper; @Nullable private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant; @@ -236,6 +245,19 @@ public class AudioStreamMediaService extends Service { stopSelf(); return START_NOT_STICKY; } + // TODO(b/398700619): Remove hasExtra check when feasible. + if (Flags.audioStreamMediaServiceByReceiveState() && intent.hasExtra( + EXTRA_PRIVATE_BROADCAST_RECEIVE_DATA)) { + PrivateBroadcastReceiveData data = intent.getParcelableExtra( + EXTRA_PRIVATE_BROADCAST_RECEIVE_DATA, PrivateBroadcastReceiveData.class); + if (data == null || !PrivateBroadcastReceiveData.Companion.isValid(data)) { + Log.w(TAG, "Data is null or invalid. Service will not start."); + stopSelf(); + return START_NOT_STICKY; + } + getHandler().post(() -> handleIntentData(data)); + return START_NOT_STICKY; + } getHandler().post(() -> { mBroadcastId = intent.getIntExtra(BROADCAST_ID, -1); if (mBroadcastId == -1) { @@ -258,6 +280,78 @@ public class AudioStreamMediaService extends Service { return START_NOT_STICKY; } + private void handleIntentData(PrivateBroadcastReceiveData data) { + int broadcastId = data.getBroadcastId(); + BluetoothDevice device = data.getSink(); + int sourceId = data.getSourceId(); + var state = data.getState(); + String programInfo = data.getProgramInfo(); + + // Service not running yet. + if (mBroadcastId == 0) { + Log.d(TAG, "handleIntentData(): sending " + data + " to handleInitialSetup()"); + handleInitialSetup(broadcastId, device, state, sourceId, programInfo); + return; + } + + // Service running with a different broadcast id, most likely staled. We have a new + // broadcast Id to handle. + if (mBroadcastId != broadcastId) { + Log.d(TAG, "handleIntentData(): sending " + data + " to handleNewBroadcastId()"); + handleNewBroadcastId(broadcastId, device, state, sourceId, programInfo); + return; + } + + // Service running with the same broadcast Id, we have new device joining or a state update. + if (mStateByDevice != null && (!mStateByDevice.containsKey(device) || mStateByDevice.get( + device) != state)) { + Log.d(TAG, "handleIntentData(): sending " + data + " to handleNewDeviceOrState()"); + handleNewDeviceOrState(device, state, sourceId, programInfo); + } + + Log.d(TAG, "handleIntentData(): nothing to update."); + } + + private void handleInitialSetup(int broadcastId, BluetoothDevice device, + LocalBluetoothLeBroadcastSourceState state, int sourceId, String programInfo) { + if (state == DECRYPTION_FAILED) { + Log.d(TAG, "handleInitialSetup() : decryption failed. Service will not start."); + stopSelf(); + return; + } + mBroadcastId = broadcastId; + mStateByDevice = new HashMap<>(); + mStateByDevice.put(device, state); + MediaSession.Token token = getOrCreateLocalMediaSession( + getBroadcastName(device, sourceId, programInfo)); + startForeground(NOTIFICATION_ID, buildNotification(token)); + } + + private void handleNewBroadcastId(int broadcastId, BluetoothDevice device, + LocalBluetoothLeBroadcastSourceState state, int sourceId, String programInfo) { + if (state == DECRYPTION_FAILED) { + Log.d(TAG, "handleNewBroadcastId() : decryption failed. Ignore."); + return; + } + mBroadcastId = broadcastId; + mStateByDevice = new HashMap<>(); + mStateByDevice.put(device, state); + updateMediaSessionAndNotify(device, sourceId, programInfo); + } + + private void handleNewDeviceOrState(BluetoothDevice device, + LocalBluetoothLeBroadcastSourceState state, int sourceId, String programInfo) { + if (mStateByDevice != null) { + mStateByDevice.put(device, state); + } + if (getDeviceInValidState().isEmpty()) { + Log.d(TAG, "handleNewDeviceOrState() : no device is in valid state. Stop service."); + stopSelf(); + return; + } + updateMediaSessionAndNotify(device, sourceId, programInfo); + } + private MediaSession.Token getOrCreateLocalMediaSession(String title) { if (mLocalSession != null) { return mLocalSession.getSessionToken(); @@ -288,7 +382,8 @@ public class AudioStreamMediaService extends Service { } private String getDeviceName() { - if (mStateByDevice == null || mStateByDevice.isEmpty() || mLocalBtManager == null) { + List validDevices = getDeviceInValidState(); + if (validDevices.isEmpty() || mLocalBtManager == null) { return DEFAULT_DEVICE_NAME; } @@ -297,8 +392,7 @@ public class AudioStreamMediaService extends Service { return DEFAULT_DEVICE_NAME; } - CachedBluetoothDevice device = manager.findDevice( - mStateByDevice.keySet().iterator().next()); + CachedBluetoothDevice device = manager.findDevice(validDevices.getFirst()); return device != null ? device.getName() : DEFAULT_DEVICE_NAME; } @@ -320,6 +414,47 @@ public class AudioStreamMediaService extends Service { return notificationBuilder.build(); } + private void updateMediaSessionAndNotify(BluetoothDevice device, int sourceId, + String programInfo) { + if (mNotificationManager == null || mLocalSession == null) { + Log.w(TAG, "mNotificationManager or mLocalSession is null, ignore update."); + return; + } + mLocalSession.setMetadata(new MediaMetadata.Builder().putString( + MediaMetadata.METADATA_KEY_TITLE, + getBroadcastName(device, sourceId, programInfo)).putLong( + MediaMetadata.METADATA_KEY_DURATION, STATIC_PLAYBACK_DURATION).build()); + mLocalSession.setPlaybackState(getPlaybackState()); + mNotificationManager.notify(NOTIFICATION_ID, + buildNotification(mLocalSession.getSessionToken())); + } + + private String getBroadcastName(BluetoothDevice sink, int sourceId, String programInfo) { + if (mLeBroadcastAssistant == null || sink == null) { + return programInfo; + } + var metadata = mLeBroadcastAssistant.getSourceMetadata(sink, sourceId); + if (metadata == null || metadata.getBroadcastId() != mBroadcastId + || metadata.getBroadcastName() == null || metadata.getBroadcastName().isEmpty()) { + Log.d(TAG, "getBroadcastName(): source metadata not found, using programInfo: " + + programInfo); + return programInfo; + } + return metadata.getBroadcastName(); + } + + private List getDeviceInValidState() { + if (mStateByDevice == null || mStateByDevice.isEmpty()) { + Log.w(TAG, "getDeviceInValidState() : mStateByDevice is null or empty!"); + return emptyList(); + } + if (Flags.audioStreamMediaServiceByReceiveState()) { + return mStateByDevice.entrySet().stream().filter( + entry -> entry.getValue() != DECRYPTION_FAILED).map(Map.Entry::getKey).toList(); + } + return mStateByDevice.keySet().stream().toList(); + } + @Nullable @Override public IBinder onBind(Intent intent) { @@ -342,6 +477,9 @@ public class AudioStreamMediaService extends Service { @Override public void onReceiveStateChanged( BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) { + if (Flags.audioStreamMediaServiceByReceiveState()) { + return; + } super.onReceiveStateChanged(sink, sourceId, state); if (!mHysteresisModeFixAvailable || mStateByDevice == null || !mStateByDevice.containsKey(sink)) { @@ -383,23 +521,21 @@ public class AudioStreamMediaService extends Service { @Override public void onDeviceVolumeChanged( @NonNull BluetoothDevice device, @IntRange(from = -255, to = 255) int volume) { - if (mStateByDevice == null || mStateByDevice.isEmpty()) { - Log.w(TAG, "active device or device has source is null!"); + if (!getDeviceInValidState().contains(device)) { + Log.w(TAG, "onDeviceVolumeChanged() : device not in valid state list"); return; } Log.d( TAG, "onDeviceVolumeChanged() bluetoothDevice : " + device + " volume: " + volume); - if (mStateByDevice.containsKey(device)) { - if (volume == 0) { - mIsMuted = true; - } else { - mIsMuted = false; - mLatestPositiveVolume = volume; - } - if (mLocalSession != null) { - mLocalSession.setPlaybackState(getPlaybackState()); - } + if (volume == 0) { + mIsMuted = true; + } else { + mIsMuted = false; + mLatestPositiveVolume = volume; + } + if (mLocalSession != null) { + mLocalSession.setPlaybackState(getPlaybackState()); } } } @@ -426,7 +562,7 @@ public class AudioStreamMediaService extends Service { && mStateByDevice != null) { mStateByDevice.remove(cachedDevice.getDevice()); } - if (mStateByDevice == null || mStateByDevice.isEmpty()) { + if (getDeviceInValidState().isEmpty()) { Log.d(TAG, "onProfileConnectionStateChanged() : stopSelf"); stopSelf(); } @@ -484,11 +620,7 @@ public class AudioStreamMediaService extends Service { } private void handleOnPlay() { - if (mStateByDevice == null || mStateByDevice.isEmpty()) { - Log.w(TAG, "active device or device has source is null!"); - return; - } - mStateByDevice.keySet().forEach(device -> { + getDeviceInValidState().forEach(device -> { Log.d(TAG, "onPlay() setting volume for device : " + device + " volume: " + mLatestPositiveVolume); setDeviceVolume(device, mLatestPositiveVolume); @@ -496,11 +628,7 @@ public class AudioStreamMediaService extends Service { } private void handleOnPause() { - if (mStateByDevice == null || mStateByDevice.isEmpty()) { - Log.w(TAG, "active device or device has source is null!"); - return; - } - mStateByDevice.keySet().forEach(device -> { + getDeviceInValidState().forEach(device -> { Log.d(TAG, "onPause() setting volume for device : " + device + " volume: " + 0); setDeviceVolume(device, /* volume= */ 0); }); diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedState.java index 88393ab1b67..47a6da9510e 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedState.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedState.java @@ -26,6 +26,7 @@ import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; import com.android.settings.dashboard.DashboardFragment; +import com.android.settingslib.flags.Flags; class SourceAddedState extends AudioStreamStateHandler { @VisibleForTesting @@ -55,10 +56,12 @@ class SourceAddedState extends AudioStreamStateHandler { if (cached != null) { mAudioStreamsRepository.saveMetadata(context, cached); } - helper.startMediaService( - context, - preference.getAudioStreamBroadcastId(), - String.valueOf(preference.getTitle())); + if (!Flags.audioStreamMediaServiceByReceiveState()) { + helper.startMediaService( + context, + preference.getAudioStreamBroadcastId(), + String.valueOf(preference.getTitle())); + } mMetricsFeatureProvider.action( preference.getContext(), SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED, diff --git a/src/com/android/settings/connecteddevice/display/ExternalDisplaySizePreference.java b/src/com/android/settings/connecteddevice/display/ExternalDisplaySizePreference.java index df405ba4a94..bb3e1b37a18 100644 --- a/src/com/android/settings/connecteddevice/display/ExternalDisplaySizePreference.java +++ b/src/com/android/settings/connecteddevice/display/ExternalDisplaySizePreference.java @@ -81,9 +81,11 @@ public class ExternalDisplaySizePreference extends AccessibilitySeekBarPreferenc implements SeekBar.OnSeekBarChangeListener { private static final long MIN_COMMIT_INTERVAL_MS = 800; private static final long CHANGE_BY_BUTTON_DELAY_MS = 300; + private static final long CHANGE_BY_SEEKBAR_DELAY_MS = 100; private final DisplaySizeData mDisplaySizeData; private int mLastDisplayProgress; private long mLastCommitTime; + private boolean mSeekByTouch; ExternalDisplaySizePreferenceStateHandler(DisplaySizeData displaySizeData) { mDisplaySizeData = displaySizeData; } @@ -99,8 +101,7 @@ public class ExternalDisplaySizePreference extends AccessibilitySeekBarPreferenc mLastCommitTime = SystemClock.elapsedRealtime(); } - private void postCommitDelayed() { - var commitDelayMs = CHANGE_BY_BUTTON_DELAY_MS; + private void postCommitDelayed(long commitDelayMs) { if (SystemClock.elapsedRealtime() - mLastCommitTime < MIN_COMMIT_INTERVAL_MS) { commitDelayMs += MIN_COMMIT_INTERVAL_MS; } @@ -112,13 +113,18 @@ public class ExternalDisplaySizePreference extends AccessibilitySeekBarPreferenc @Override public void onProgressChanged(@NonNull SeekBar seekBar, int i, boolean b) { - postCommitDelayed(); + if (!mSeekByTouch) postCommitDelayed(CHANGE_BY_BUTTON_DELAY_MS); } @Override - public void onStartTrackingTouch(@NonNull SeekBar seekBar) {} + public void onStartTrackingTouch(@NonNull SeekBar seekBar) { + mSeekByTouch = true; + } @Override - public void onStopTrackingTouch(@NonNull SeekBar seekBar) {} + public void onStopTrackingTouch(@NonNull SeekBar seekBar) { + mSeekByTouch = false; + postCommitDelayed(CHANGE_BY_SEEKBAR_DELAY_MS); + } } } diff --git a/src/com/android/settings/contract/SettingsContract.kt b/src/com/android/settings/contract/SettingsContract.kt index da3867f35a4..3a354194eb6 100644 --- a/src/com/android/settings/contract/SettingsContract.kt +++ b/src/com/android/settings/contract/SettingsContract.kt @@ -104,3 +104,9 @@ const val KEY_RING_VOLUME = "separate_ring_volume" /** Contract key for the "Remove animation" setting. */ const val KEY_REMOVE_ANIMATION = "remove_animation" + +/** Contract key for the "Pin media player. */ +const val KEY_PIN_MEDIA_PLAYER = "pin_media_player" + +/** Contract key for the "Show media on lock screen. */ +const val KEY_SHOW_MEDIA_ON_LOCK_SCREEN = "show_media_on_lock_screen" diff --git a/src/com/android/settings/display/WidgetsOnLockscreenPreferenceController.java b/src/com/android/settings/display/WidgetsOnLockscreenPreferenceController.java new file mode 100644 index 00000000000..32ba6ec5c23 --- /dev/null +++ b/src/com/android/settings/display/WidgetsOnLockscreenPreferenceController.java @@ -0,0 +1,55 @@ +/* + * 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.os.UserHandle; +import android.os.UserManager; + +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +/** Controls the "widgets on lock screen" preferences (under "Display & touch"). */ +public class WidgetsOnLockscreenPreferenceController extends BasePreferenceController { + public WidgetsOnLockscreenPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return isAvailable(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + } + + /** + * Returns whether "widgets on lock screen" preferences are available. + */ + public static boolean isAvailable(Context context) { + if (!isMainUser(context)) { + return false; + } + + return com.android.systemui.Flags.glanceableHubV2() + && (context.getResources().getBoolean(R.bool.config_show_communal_settings) + || context.getResources().getBoolean( + R.bool.config_show_communal_settings_mobile)); + } + + private static boolean isMainUser(Context context) { + final UserManager userManager = context.getSystemService(UserManager.class); + return userManager.getUserInfo(UserHandle.myUserId()).isMain(); + } +} diff --git a/src/com/android/settings/inputmethod/KeyboardAccessibilityKeysDialogFragment.java b/src/com/android/settings/inputmethod/KeyboardAccessibilityKeysDialogFragment.java index 97d5b24b5ef..16a24204349 100644 --- a/src/com/android/settings/inputmethod/KeyboardAccessibilityKeysDialogFragment.java +++ b/src/com/android/settings/inputmethod/KeyboardAccessibilityKeysDialogFragment.java @@ -122,6 +122,8 @@ public abstract class KeyboardAccessibilityKeysDialogFragment extends DialogFrag R.id.input_setting_keys_value_custom); TextView customValueTextView = accessibilityKeyDialog.findViewById( R.id.input_setting_keys_value_custom_value); + View seekbarView = accessibilityKeyDialog.findViewById( + R.id.input_setting_keys_custom_seekbar_layout); SeekBar customProgressBar = accessibilityKeyDialog.findViewById( R.id.input_setting_keys_value_custom_slider); TextView titleTextView = accessibilityKeyDialog.findViewById( @@ -147,7 +149,7 @@ public abstract class KeyboardAccessibilityKeysDialogFragment extends DialogFrag customValueTextView.setVisibility(isChecked ? View.VISIBLE : View.GONE); customValueTextView.setText( progressToThresholdInSecond(customProgressBar.getProgress())); - customProgressBar.setVisibility(isChecked ? View.VISIBLE : View.GONE); + seekbarView.setVisibility(isChecked ? View.VISIBLE : View.GONE); buttonView.setChecked(isChecked); }); cannedValueRadioGroup.setOnCheckedChangeListener( @@ -174,14 +176,14 @@ public abstract class KeyboardAccessibilityKeysDialogFragment extends DialogFrag // setting initStateBasedOnThreshold(cannedValueRadioGroup, customRadioButton, customValueTextView, - customProgressBar); + customProgressBar, seekbarView); } else if (customRadioButton.isChecked()) { cannedValueRadioGroup.clearCheck(); customRadioButton.setChecked(true); customValueTextView.setVisibility(View.VISIBLE); customValueTextView.setText( progressToThresholdInSecond(customProgressBar.getProgress())); - customProgressBar.setVisibility(View.VISIBLE); + seekbarView.setVisibility(View.VISIBLE); } }); @@ -199,7 +201,7 @@ public abstract class KeyboardAccessibilityKeysDialogFragment extends DialogFrag private void initStateBasedOnThreshold(RadioGroup cannedValueRadioGroup, RadioButton customRadioButton, TextView customValueTextView, - SeekBar customProgressBar) { + SeekBar customProgressBar, View seekbarView) { int inputSettingKeysThreshold = getInputSettingKeysValue(); switch (inputSettingKeysThreshold) { case 600 -> cannedValueRadioGroup.check(R.id.input_setting_keys_value_600); @@ -213,5 +215,6 @@ public abstract class KeyboardAccessibilityKeysDialogFragment extends DialogFrag customRadioButton.setChecked(true); } } + seekbarView.setVisibility(customRadioButton.isChecked() ? View.VISIBLE : View.GONE); } } diff --git a/src/com/android/settings/network/ethernet/EthernetInterface.kt b/src/com/android/settings/network/ethernet/EthernetInterface.kt index 1e2dc74752e..ddb138938a7 100644 --- a/src/com/android/settings/network/ethernet/EthernetInterface.kt +++ b/src/com/android/settings/network/ethernet/EthernetInterface.kt @@ -18,34 +18,68 @@ package com.android.settings.network.ethernet import android.content.Context import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback import android.net.EthernetManager import android.net.EthernetManager.STATE_ABSENT import android.net.EthernetNetworkManagementException import android.net.EthernetNetworkUpdateRequest import android.net.IpConfiguration +import android.net.LinkProperties +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest import android.os.OutcomeReceiver import android.util.Log import androidx.core.content.ContextCompat +import com.google.common.annotations.VisibleForTesting class EthernetInterface(private val context: Context, private val id: String) : EthernetManager.InterfaceStateListener { + interface EthernetInterfaceStateListener { + fun interfaceUpdated() + } + private val ethernetManager: EthernetManager? = context.getSystemService(EthernetManager::class.java) private val connectivityManager: ConnectivityManager? = context.getSystemService(ConnectivityManager::class.java) private val executor = ContextCompat.getMainExecutor(context) + private val interfaceListeners = mutableListOf() private val TAG = "EthernetInterface" + private val networkRequest: NetworkRequest = + NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .build() + private var interfaceState = STATE_ABSENT private var ipConfiguration = IpConfiguration() + private var linkProperties = LinkProperties() fun getInterfaceState() = interfaceState fun getId() = id - fun getConfiguration(): IpConfiguration { - return ipConfiguration + fun getConfiguration() = ipConfiguration + + fun getLinkProperties() = linkProperties + + fun registerListener(listener: EthernetInterfaceStateListener) { + if (interfaceListeners.isEmpty()) { + ethernetManager?.addInterfaceStateListener(ContextCompat.getMainExecutor(context), this) + connectivityManager?.registerNetworkCallback(networkRequest, networkCallback) + } + interfaceListeners.add(listener) + } + + fun unregisterListener(listener: EthernetInterfaceStateListener) { + interfaceListeners.remove(listener) + if (interfaceListeners.isEmpty()) { + connectivityManager?.unregisterNetworkCallback(networkCallback) + ethernetManager?.removeInterfaceStateListener(this) + } } fun setConfiguration(ipConfiguration: IpConfiguration) { @@ -67,10 +101,28 @@ class EthernetInterface(private val context: Context, private val id: String) : ) } + private fun notifyListeners() { + for (listener in interfaceListeners) { + listener.interfaceUpdated() + } + } + override fun onInterfaceStateChanged(id: String, state: Int, role: Int, cfg: IpConfiguration?) { if (id == this.id) { ipConfiguration = cfg ?: IpConfiguration() interfaceState = state + notifyListeners() } } + + @VisibleForTesting + val networkCallback = + object : NetworkCallback() { + override fun onLinkPropertiesChanged(network: Network, lp: LinkProperties) { + if (lp.getInterfaceName().equals(id)) { + linkProperties = lp + notifyListeners() + } + } + } } diff --git a/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsController.kt b/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsController.kt index cb3ad1db8ae..174fdbdf25d 100644 --- a/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsController.kt +++ b/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsController.kt @@ -18,7 +18,14 @@ package com.android.settings.network.ethernet import android.content.Context import android.net.EthernetManager +import android.net.IpConfiguration +import android.net.LinkProperties +import android.net.StaticIpConfiguration import android.widget.ImageView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceScreen import com.android.settings.R @@ -30,13 +37,25 @@ class EthernetInterfaceDetailsController( context: Context, private val fragment: PreferenceFragmentCompat, private val preferenceId: String, -) : AbstractPreferenceController(context) { + private val lifecycle: Lifecycle, +) : + AbstractPreferenceController(context), + EthernetInterface.EthernetInterfaceStateListener, + LifecycleEventObserver { private val KEY_HEADER = "ethernet_details" private val ethernetManager = context.getSystemService(EthernetManager::class.java) private val ethernetInterface = EthernetTrackerImpl.getInstance(context).getInterface(preferenceId) + private lateinit var entityHeaderController: EntityHeaderController + + private var ipAddressPref: Preference? = null + + init { + lifecycle.addObserver(this) + } + override fun isAvailable(): Boolean { return true } @@ -45,10 +64,24 @@ class EthernetInterfaceDetailsController( return KEY_HEADER } + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + when (event) { + Lifecycle.Event.ON_START -> { + ethernetInterface?.registerListener(this) + } + + Lifecycle.Event.ON_STOP -> { + ethernetInterface?.unregisterListener(this) + } + + else -> {} + } + } + override fun displayPreference(screen: PreferenceScreen) { val headerPref: LayoutPreference? = screen.findPreference(KEY_HEADER) - val mEntityHeaderController = + entityHeaderController = EntityHeaderController.newInstance( fragment.getActivity(), fragment, @@ -59,17 +92,49 @@ class EthernetInterfaceDetailsController( iconView?.setScaleType(ImageView.ScaleType.CENTER_INSIDE) - mEntityHeaderController - .setLabel("Ethernet") - .setSummary( - if (ethernetInterface?.getInterfaceState() == EthernetManager.STATE_LINK_UP) { - mContext.getString(R.string.network_connected) - } else { - mContext.getString(R.string.network_disconnected) - } - ) - .setSecondSummary("") - .setIcon(mContext.getDrawable(R.drawable.ic_settings_ethernet)) - .done(true /* rebind */) + if (entityHeaderController != null) { + entityHeaderController + .setLabel("Ethernet") + .setSummary( + if (ethernetInterface?.getInterfaceState() == EthernetManager.STATE_LINK_UP) { + mContext.getString(R.string.network_connected) + } else { + mContext.getString(R.string.network_disconnected) + } + ) + .setSecondSummary("") + .setIcon(mContext.getDrawable(R.drawable.ic_settings_ethernet)) + .done(true /* rebind */) + } + + ipAddressPref = screen.findPreference("ethernet_ip_address") + + if (ethernetInterface?.getInterfaceState() == EthernetManager.STATE_LINK_UP) { + initializeIpDetails() + } + } + + override fun interfaceUpdated() { + entityHeaderController?.setSummary( + if (ethernetInterface?.getInterfaceState() == EthernetManager.STATE_LINK_UP) { + mContext.getString(R.string.network_connected) + } else { + mContext.getString(R.string.network_disconnected) + } + ) + initializeIpDetails() + } + + private fun initializeIpDetails() { + val ipConfiguration: IpConfiguration? = ethernetInterface?.getConfiguration() + val linkProperties: LinkProperties? = ethernetInterface?.getLinkProperties() + + if (ipConfiguration?.getIpAssignment() == IpConfiguration.IpAssignment.STATIC) { + val staticIp: StaticIpConfiguration? = ipConfiguration?.getStaticIpConfiguration() + ipAddressPref?.setSummary(staticIp?.getIpAddress().toString()) + } else { + val addresses = linkProperties?.getAddresses() + ipAddressPref?.setSummary(addresses?.first().toString()) + } } } diff --git a/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsFragment.kt b/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsFragment.kt index a6cf90fed6b..46ed78f5aa5 100644 --- a/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsFragment.kt +++ b/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsFragment.kt @@ -50,6 +50,13 @@ class EthernetInterfaceDetailsFragment : DashboardFragment() { override public fun createPreferenceControllers( context: Context ): List { - return listOf(EthernetInterfaceDetailsController(context, this, preferenceId ?: "")) + return listOf( + EthernetInterfaceDetailsController( + context, + this, + preferenceId ?: "", + getSettingsLifecycle(), + ) + ) } } diff --git a/src/com/android/settings/safetycenter/OWNERS b/src/com/android/settings/safetycenter/OWNERS new file mode 100644 index 00000000000..b89b09bbdbc --- /dev/null +++ b/src/com/android/settings/safetycenter/OWNERS @@ -0,0 +1,4 @@ +jtomljanovic@google.com +elliotsisteron@google.com +deweytyl@google.com +simonjw@google.com diff --git a/src/com/android/settings/security/ActionDisabledByAdvancedProtectionDialog.kt b/src/com/android/settings/security/ActionDisabledByAdvancedProtectionDialog.kt index f9ec7bcbcbe..cf29c3d707e 100644 --- a/src/com/android/settings/security/ActionDisabledByAdvancedProtectionDialog.kt +++ b/src/com/android/settings/security/ActionDisabledByAdvancedProtectionDialog.kt @@ -22,6 +22,7 @@ import android.security.advancedprotection.AdvancedProtectionManager.EXTRA_SUPPO import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_CELLULAR_2G import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_INSTALL_UNKNOWN_SOURCES import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_WEP +import android.content.pm.PackageManager import android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_ENABLE_MTE import android.security.advancedprotection.AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_BLOCKED_INTERACTION import android.security.advancedprotection.AdvancedProtectionManager.SUPPORT_DIALOG_TYPE_DISABLED_SETTING @@ -37,13 +38,17 @@ import com.android.settingslib.spa.SpaDialogWindowTypeActivity import com.android.settingslib.spa.widget.dialog.AlertDialogButton import com.android.settingslib.spa.widget.dialog.SettingsAlertDialogContent import com.android.settingslib.wifi.WifiUtils.Companion.DIALOG_WINDOW_TYPE +import android.security.advancedprotection.AdvancedProtectionManager class ActionDisabledByAdvancedProtectionDialog : SpaDialogWindowTypeActivity() { @Composable override fun Content() { SettingsAlertDialogContent( - confirmButton = AlertDialogButton(getString(R.string.okay)) { finish() }, + confirmButton = AlertDialogButton(getString(R.string.okay)) { + finish() + logDialogShown(learnMoreClicked = false) + }, dismissButton = getSupportButtonIfExists(), title = getString(R.string.disabled_by_advanced_protection_title), icon = { @@ -56,8 +61,8 @@ class ActionDisabledByAdvancedProtectionDialog : SpaDialogWindowTypeActivity() { } private fun getDialogMessage(): String { - val featureId = intent.getIntExtra(EXTRA_SUPPORT_DIALOG_FEATURE, -1) - val type = intent.getIntExtra(EXTRA_SUPPORT_DIALOG_TYPE, SUPPORT_DIALOG_TYPE_UNKNOWN) + val featureId = getIntentFeatureId() + val type = getIntentDialogueType() val messageId = when (type) { SUPPORT_DIALOG_TYPE_DISABLED_SETTING -> { if (featureIdsWithSettingOn.contains(featureId)) { @@ -93,6 +98,7 @@ class ActionDisabledByAdvancedProtectionDialog : SpaDialogWindowTypeActivity() { ) { startActivity(helpIntent) finish() + logDialogShown(learnMoreClicked = true) } } catch (e: Exception) { Log.w(TAG, "Tried to set up help button, but this exception was thrown: ${e.message}") @@ -100,10 +106,29 @@ class ActionDisabledByAdvancedProtectionDialog : SpaDialogWindowTypeActivity() { return null } + private fun logDialogShown(learnMoreClicked: Boolean) { + // We should always have this permission, but just in case we don't, we should not log. + if (checkSelfPermission(android.Manifest.permission.MANAGE_ADVANCED_PROTECTION_MODE) + != PackageManager.PERMISSION_GRANTED) { + return + } + + this.getSystemService(AdvancedProtectionManager::class.java) + .logDialogShown(getIntentFeatureId(), getIntentDialogueType(), learnMoreClicked) + } + override fun getDialogWindowType(): Int? = if (intent.hasExtra(DIALOG_WINDOW_TYPE)) { intent.getIntExtra(DIALOG_WINDOW_TYPE, WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW) } else null + private fun getIntentFeatureId(): Int { + return intent.getIntExtra(EXTRA_SUPPORT_DIALOG_FEATURE, -1) + } + + private fun getIntentDialogueType(): Int { + return intent.getIntExtra(EXTRA_SUPPORT_DIALOG_TYPE, SUPPORT_DIALOG_TYPE_UNKNOWN) + } + private companion object { const val TAG = "AdvancedProtectionDlg" val defaultMessageId = R.string.disabled_by_advanced_protection_action_message diff --git a/src/com/android/settings/security/ScreenLockPreferenceDetailsUtils.java b/src/com/android/settings/security/ScreenLockPreferenceDetailsUtils.java index b1685685bb4..e74db34dee0 100644 --- a/src/com/android/settings/security/ScreenLockPreferenceDetailsUtils.java +++ b/src/com/android/settings/security/ScreenLockPreferenceDetailsUtils.java @@ -178,7 +178,9 @@ public class ScreenLockPreferenceDetailsUtils { if (!mLockPatternUtils.isSecure(userId)) { if (userId == mProfileChallengeUserId || mLockPatternUtils.isLockScreenDisabled(userId)) { - return R.string.unlock_set_unlock_mode_off; + return com.android.settings.flags.Flags.biometricsOnboardingEducation() + ? R.string.unlock_set_unlock_mode_off_new + : R.string.unlock_set_unlock_mode_off; } else { return R.string.unlock_set_unlock_mode_none; } diff --git a/src/com/android/settings/sound/MediaControlsLockscreenSwitchPreference.kt b/src/com/android/settings/sound/MediaControlsLockscreenSwitchPreference.kt index 2d70b83ef31..7ad3a47f20e 100644 --- a/src/com/android/settings/sound/MediaControlsLockscreenSwitchPreference.kt +++ b/src/com/android/settings/sound/MediaControlsLockscreenSwitchPreference.kt @@ -16,9 +16,12 @@ package com.android.settings.sound +import android.app.settings.SettingsEnums.ACTION_SHOW_MEDIA_ON_LOCK_SCREEN import android.content.Context import android.provider.Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN import com.android.settings.R +import com.android.settings.contract.KEY_SHOW_MEDIA_ON_LOCK_SCREEN +import com.android.settings.metrics.PreferenceActionMetricsProvider import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyValueStoreDelegate import com.android.settingslib.datastore.SettingsSecureStore @@ -32,7 +35,13 @@ class MediaControlsLockscreenSwitchPreference : KEY, R.string.media_controls_lockscreen_title, R.string.media_controls_lockscreen_description, - ) { + ), + PreferenceActionMetricsProvider { + + override val preferenceActionMetrics: Int + get() = ACTION_SHOW_MEDIA_ON_LOCK_SCREEN + + override fun tags(context: Context) = arrayOf(KEY_SHOW_MEDIA_ON_LOCK_SCREEN) override val sensitivityLevel get() = SensitivityLevel.NO_SENSITIVITY diff --git a/src/com/android/settings/sound/MediaControlsSwitchPreference.kt b/src/com/android/settings/sound/MediaControlsSwitchPreference.kt index 60df2658b7f..fd5354d4127 100644 --- a/src/com/android/settings/sound/MediaControlsSwitchPreference.kt +++ b/src/com/android/settings/sound/MediaControlsSwitchPreference.kt @@ -16,30 +16,39 @@ package com.android.settings.sound +import android.app.settings.SettingsEnums.ACTION_PIN_MEDIA_PLAYER import android.content.Context import android.provider.Settings.Secure.MEDIA_CONTROLS_RESUME - +import com.android.settings.R +import com.android.settings.contract.KEY_PIN_MEDIA_PLAYER +import com.android.settings.metrics.PreferenceActionMetricsProvider import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.SettingsSecureStore import com.android.settingslib.metadata.ReadWritePermit import com.android.settingslib.metadata.SensitivityLevel import com.android.settingslib.metadata.SwitchPreference -import com.android.settings.R // LINT.IfChange class MediaControlsSwitchPreference( - private val mediaControlsStore: MediaControlsScreen.MediaControlsStore, -) : SwitchPreference( - KEY, - R.string.media_controls_resume_title, - R.string.media_controls_resume_description, -) { + private val mediaControlsStore: MediaControlsScreen.MediaControlsStore +) : + SwitchPreference( + KEY, + R.string.media_controls_resume_title, + R.string.media_controls_resume_description, + ), + PreferenceActionMetricsProvider { override val sensitivityLevel get() = SensitivityLevel.NO_SENSITIVITY override val keywords: Int get() = R.string.keywords_media_controls + override val preferenceActionMetrics: Int + get() = ACTION_PIN_MEDIA_PLAYER + + override fun tags(context: Context) = arrayOf(KEY_PIN_MEDIA_PLAYER) + override fun getReadPermissions(context: Context) = SettingsSecureStore.getReadPermissions() override fun getWritePermissions(context: Context) = SettingsSecureStore.getWritePermissions() @@ -56,4 +65,4 @@ class MediaControlsSwitchPreference( const val KEY = MEDIA_CONTROLS_RESUME } } -// LINT.ThenChange(MediaControlsPreferenceController.java) \ No newline at end of file +// LINT.ThenChange(MediaControlsPreferenceController.java) diff --git a/src/com/android/settings/spa/app/appcompat/OWNERS b/src/com/android/settings/spa/app/appcompat/OWNERS new file mode 100644 index 00000000000..5ef6ded95f1 --- /dev/null +++ b/src/com/android/settings/spa/app/appcompat/OWNERS @@ -0,0 +1,5 @@ +# Bug component: 970984 +# Large Screen Experiences App Compat +gracielawputri@google.com +mcarli@google.com +mariiasand@google.com \ No newline at end of file diff --git a/src/com/android/settings/supervision/ConfirmSupervisionCredentialsActivity.kt b/src/com/android/settings/supervision/ConfirmSupervisionCredentialsActivity.kt new file mode 100644 index 00000000000..b459f5383b5 --- /dev/null +++ b/src/com/android/settings/supervision/ConfirmSupervisionCredentialsActivity.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.supervision + +import android.Manifest.permission.USE_BIOMETRIC +import android.app.Activity +import android.content.pm.PackageManager +import android.hardware.biometrics.BiometricManager +import android.hardware.biometrics.BiometricPrompt +import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback +import android.os.Bundle +import android.os.CancellationSignal +import android.util.Log +import androidx.annotation.RequiresPermission +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import com.android.settings.R + +/** + * Activity for confirming supervision credentials using device credential authentication. + * + * This activity displays an authentication prompt to the user, requiring them to authenticate using + * their device credentials (PIN, pattern, or password). It is specifically designed for verifying + * credentials for supervision purposes. + * + * It returns `Activity.RESULT_OK` if authentication succeeds, and `Activity.RESULT_CANCELED` if + * authentication fails or is canceled by the user. + * + * Usage: + * 1. Start this activity using `startActivityForResult()`. + * 2. Handle the result in `onActivityResult()`. + * + * Permissions: + * - Requires `android.permission.USE_BIOMETRIC`. + */ +class ConfirmSupervisionCredentialsActivity : FragmentActivity() { + private val mAuthenticationCallback = + object : AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + Log.w(TAG, "onAuthenticationError(errorCode=$errorCode, errString=$errString)") + setResult(Activity.RESULT_CANCELED) + finish() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) { + setResult(Activity.RESULT_OK) + finish() + } + + override fun onAuthenticationFailed() { + setResult(Activity.RESULT_CANCELED) + finish() + } + } + + @RequiresPermission(USE_BIOMETRIC) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // TODO(b/392961554): Check if caller is the SYSTEM_SUPERVISION role holder. Call + // RoleManager#getRoleHolders(SYSTEM_SUPERVISION) and check if getCallingPackage() is in the + // list. + if (checkCallingOrSelfPermission(USE_BIOMETRIC) == PackageManager.PERMISSION_GRANTED) { + showBiometricPrompt() + } + } + + @RequiresPermission(USE_BIOMETRIC) + fun showBiometricPrompt() { + // TODO(b/392961554): adapts to new user profile type to trigger PIN verification dialog. + val biometricPrompt = + BiometricPrompt.Builder(this) + .setTitle(getString(R.string.supervision_full_screen_pin_verification_title)) + .setConfirmationRequired(true) + .setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + .build() + biometricPrompt.authenticate( + CancellationSignal(), + ContextCompat.getMainExecutor(this), + mAuthenticationCallback, + ) + } + + companion object { + // TODO(b/392961554): remove this tag and use shared tag after http://ag/31997167 is + // submitted. + const val TAG = "SupervisionSettings" + } +} diff --git a/src/com/android/settings/supervision/SupervisionDashboardScreen.kt b/src/com/android/settings/supervision/SupervisionDashboardScreen.kt index 86f77f726b6..674c0f3a7fb 100644 --- a/src/com/android/settings/supervision/SupervisionDashboardScreen.kt +++ b/src/com/android/settings/supervision/SupervisionDashboardScreen.kt @@ -56,7 +56,7 @@ class SupervisionDashboardScreen : PreferenceScreenCreator { override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(context, this) { - +SupervisionMainSwitchPreference() + +SupervisionMainSwitchPreference(context) +TitlelessPreferenceGroup(SUPERVISION_DYNAMIC_GROUP_1) += { +SupervisionWebContentFiltersScreen.KEY } diff --git a/src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt b/src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt index 5a84137ecb6..88afc55147c 100644 --- a/src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt +++ b/src/com/android/settings/supervision/SupervisionMainSwitchPreference.kt @@ -15,8 +15,10 @@ */ package com.android.settings.supervision +import android.app.Activity import android.app.supervision.SupervisionManager import android.content.Context +import android.content.Intent import androidx.preference.Preference import com.android.settings.R import com.android.settingslib.datastore.KeyValueStore @@ -32,19 +34,22 @@ import com.android.settingslib.preference.MainSwitchPreferenceBinding import com.android.settingslib.preference.forEachRecursively /** Main toggle to enable or disable device supervision. */ -class SupervisionMainSwitchPreference : +class SupervisionMainSwitchPreference(context: Context) : MainSwitchPreference(KEY, R.string.device_supervision_switch_title), PreferenceSummaryProvider, MainSwitchPreferenceBinding, Preference.OnPreferenceChangeListener, PreferenceLifecycleProvider { + private val supervisionMainSwitchStorage = SupervisionMainSwitchStorage(context) + private lateinit var lifeCycleContext: PreferenceLifecycleContext + // TODO(b/383568136): Make presence of summary conditional on whether PIN // has been set up before or not. override fun getSummary(context: Context): CharSequence? = context.getString(R.string.device_supervision_switch_no_pin_summary) - override fun storage(context: Context): KeyValueStore = SupervisionMainSwitchStorage(context) + override fun storage(context: Context): KeyValueStore = supervisionMainSwitchStorage override fun getReadPermit(context: Context, callingPid: Int, callingUid: Int) = ReadWritePermit.DISALLOW @@ -55,26 +60,49 @@ class SupervisionMainSwitchPreference : override val sensitivityLevel: Int get() = SensitivityLevel.HIGH_SENSITIVITY + override fun onCreate(context: PreferenceLifecycleContext) { + lifeCycleContext = context + } + + override fun onResume(context: PreferenceLifecycleContext) { + updateDependentPreferencesEnabledState( + context.findPreference(KEY), + supervisionMainSwitchStorage.getBoolean(KEY)!!, + ) + } + + override fun onActivityResult( + context: PreferenceLifecycleContext, + requestCode: Int, + resultCode: Int, + data: Intent?, + ): Boolean { + if (resultCode == Activity.RESULT_OK) { + val mainSwitchPreference = + context.requirePreference(KEY) + val newValue = !supervisionMainSwitchStorage.getBoolean(KEY)!! + mainSwitchPreference.setChecked(newValue) + updateDependentPreferencesEnabledState(mainSwitchPreference, newValue) + } + + return true + } + override fun bind(preference: Preference, metadata: PreferenceMetadata) { super.bind(preference, metadata) preference.onPreferenceChangeListener = this } - override fun onResume(context: PreferenceLifecycleContext) { - val currentValue = storage(context.applicationContext)?.getBoolean(key) ?: false - - updateDependentPreferencesEnabledState( - context.findPreference(KEY), - currentValue, - ) - } - override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { if (newValue !is Boolean) return true - updateDependentPreferencesEnabledState(preference, newValue) - - return true + val intent = Intent(lifeCycleContext, ConfirmSupervisionCredentialsActivity::class.java) + lifeCycleContext.startActivityForResult( + intent, + REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS, + null, + ) + return false } private fun updateDependentPreferencesEnabledState( @@ -83,9 +111,8 @@ class SupervisionMainSwitchPreference : ) { preference?.parent?.forEachRecursively { if ( - it.parent?.key?.toString() == - SupervisionDashboardScreen.SUPERVISION_DYNAMIC_GROUP_1 || - it.key?.toString() == SupervisionPinManagementScreen.KEY + it.parent?.key == SupervisionDashboardScreen.SUPERVISION_DYNAMIC_GROUP_1 || + it.key == SupervisionPinManagementScreen.KEY ) { it.isEnabled = isChecked } @@ -103,7 +130,6 @@ class SupervisionMainSwitchPreference : as T override fun setValue(key: String, valueType: Class, value: T?) { - // TODO(b/392694561): add PIN protection to main toggle. if (key == KEY && value is Boolean) { val supervisionManager = context.getSystemService(SupervisionManager::class.java) supervisionManager?.setSupervisionEnabled(value) @@ -113,5 +139,6 @@ class SupervisionMainSwitchPreference : companion object { const val KEY = "device_supervision_switch" + const val REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS = 0 } } diff --git a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewControllerTest.java index 375952f725c..81c869d5063 100644 --- a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewControllerTest.java @@ -16,6 +16,8 @@ package com.android.settings.accessibility; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -76,6 +78,15 @@ public class TextReadingPreviewControllerTest { mDisplaySizePreference = new AccessibilitySeekBarPreference(mContext, /* attr= */ null); } + @Test + public void numberOfPreviewSamples_numberOfPreviewContentDescription_isEqual() { + int[] previewSamples = TextReadingPreviewController.getPreviewSampleLayouts(mContext); + int[] previewContentDescriptions = + TextReadingPreviewController.getPreviewSampleContentDescriptions(mContext); + + assertThat(previewSamples.length).isEqualTo(previewContentDescriptions.length); + } + @Test public void initPreviewerAdapter_verifyAction() { when(mPreferenceScreen.findPreference(PREVIEW_KEY)).thenReturn(mPreviewPreference); diff --git a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewPreferenceTest.java b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewPreferenceTest.java index 4ca1dca3de5..9cd8fa232c5 100644 --- a/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/TextReadingPreviewPreferenceTest.java @@ -49,29 +49,48 @@ import org.robolectric.RobolectricTestRunner; */ @RunWith(RobolectricTestRunner.class) public class TextReadingPreviewPreferenceTest { - + private Context mContext; private TextReadingPreviewPreference mTextReadingPreviewPreference; private PreferenceViewHolder mHolder; private ViewPager mViewPager; private PreviewPagerAdapter mPreviewPagerAdapter; private int mPreviewSampleCount; + private int[] mPreviewContentDescriptions; @Before public void setUp() { - final Context context = ApplicationProvider.getApplicationContext(); - final int[] previewSamples = TextReadingPreviewController.getPreviewSampleLayouts(context); + mContext = ApplicationProvider.getApplicationContext(); + mPreviewContentDescriptions = + TextReadingPreviewController.getPreviewSampleContentDescriptions(mContext); + final int[] previewSamples = TextReadingPreviewController.getPreviewSampleLayouts(mContext); mPreviewSampleCount = previewSamples.length; final Configuration[] configurations = createConfigurations(mPreviewSampleCount); - mTextReadingPreviewPreference = new TextReadingPreviewPreference(context); + mTextReadingPreviewPreference = new TextReadingPreviewPreference(mContext); mPreviewPagerAdapter = - spy(new PreviewPagerAdapter(context, /* isLayoutRtl= */ false, + spy(new PreviewPagerAdapter(mContext, /* isLayoutRtl= */ false, previewSamples, configurations)); - final LayoutInflater inflater = LayoutInflater.from(context); + final LayoutInflater inflater = LayoutInflater.from(mContext); final View view = inflater.inflate(mTextReadingPreviewPreference.getLayoutResource(), - new LinearLayout(context), false); + new LinearLayout(mContext), false); mHolder = PreferenceViewHolder.createInstanceForTests(view); mViewPager = view.findViewById(R.id.preview_pager); + mTextReadingPreviewPreference.setContentDescription(mPreviewContentDescriptions); + } + + @Test + public void changePreviewPage_getExpectedContentDescription() { + mTextReadingPreviewPreference.setPreviewAdapter(mPreviewPagerAdapter); + mTextReadingPreviewPreference.onBindViewHolder(mHolder); + + // Verify the initial content description + assertThat(mViewPager.getContentDescription().toString()) + .isEqualTo(mContext.getString(mPreviewContentDescriptions[0])); + + // Change the preview page + mViewPager.setCurrentItem(1); + assertThat(mViewPager.getContentDescription().toString()) + .isEqualTo(mContext.getString(mPreviewContentDescriptions[1])); } @Test diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleAutoclickMainSwitchPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleAutoclickMainSwitchPreferenceControllerTest.java new file mode 100644 index 00000000000..b2f648acb70 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleAutoclickMainSwitchPreferenceControllerTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accessibility; + +import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; +import static com.android.settings.accessibility.AccessibilityUtil.State.ON; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.core.BasePreferenceController; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ToggleAutoclickMainSwitchPreferenceControllerTest { + + private static final String PREFERENCE_KEY = "accessibility_autoclick_main_switch"; + + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private final Context mContext = ApplicationProvider.getApplicationContext(); + private ToggleAutoclickMainSwitchPreferenceController mController; + + @Before + public void setUp() { + mController = new ToggleAutoclickMainSwitchPreferenceController(mContext, PREFERENCE_KEY); + } + + @Test + @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR) + public void getAvailabilityStatus_availableWhenFlagOn() { + assertThat(mController.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.AVAILABLE); + } + + @Test + public void setChecked_withTrue_shouldUpdateSetting() { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, OFF); + + mController.setChecked(true); + + assertThat(Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, OFF)) + .isEqualTo(ON); + } + + @Test + public void setChecked_withFalse_shouldUpdateSetting() { + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, ON); + + mController.setChecked(false); + + assertThat(Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED, OFF)) + .isEqualTo(OFF); + } +} diff --git a/tests/robotests/src/com/android/settings/biometrics/face/FaceStatusPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/face/FaceStatusPreferenceControllerTest.java index c426f9479ef..8f565147247 100644 --- a/tests/robotests/src/com/android/settings/biometrics/face/FaceStatusPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/face/FaceStatusPreferenceControllerTest.java @@ -36,6 +36,9 @@ import android.content.pm.PackageManager; import android.hardware.face.Face; import android.hardware.face.FaceManager; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.lifecycle.LifecycleOwner; import androidx.preference.Preference; @@ -50,6 +53,7 @@ import com.android.settingslib.core.lifecycle.Lifecycle; import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -67,6 +71,8 @@ public class FaceStatusPreferenceControllerTest { private static final String TEST_PREF_KEY = "baz"; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private LockPatternUtils mLockPatternUtils; @Mock @@ -125,7 +131,8 @@ public class FaceStatusPreferenceControllerTest { } @Test - public void updateState_noFace_shouldShowDefaultSummary() { + @DisableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void updateState_noFace_flagOff_shouldShowDefaultSummary() { when(mFaceManager.isHardwareDetected()).thenReturn(true); mController.updateState(mPreference); @@ -135,6 +142,18 @@ public class FaceStatusPreferenceControllerTest { assertThat(mPreference.isVisible()).isTrue(); } + @Test + @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void updateState_noFace_flagOn_shouldShowDefaultSummary() { + when(mFaceManager.isHardwareDetected()).thenReturn(true); + + mController.updateState(mPreference); + + assertThat(mPreference.getSummary()).isEqualTo( + mContext.getString(R.string.security_settings_face_preference_summary_none_new)); + assertThat(mPreference.isVisible()).isTrue(); + } + @Test public void updateState_hasFace_shouldShowSummary() { when(mFaceManager.isHardwareDetected()).thenReturn(true); diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceControllerTest.java index 000ee933dc4..de8c0709884 100644 --- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintStatusPreferenceControllerTest.java @@ -36,6 +36,9 @@ import android.content.pm.PackageManager; import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintManager; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.lifecycle.LifecycleOwner; import androidx.preference.Preference; @@ -51,6 +54,7 @@ import com.android.settingslib.utils.StringUtil; import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -66,6 +70,8 @@ import java.util.Collections; @Config(shadows = {ShadowRestrictedLockUtilsInternal.class}) public class FingerprintStatusPreferenceControllerTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private LockPatternUtils mLockPatternUtils; @Mock @@ -125,7 +131,8 @@ public class FingerprintStatusPreferenceControllerTest { } @Test - public void updateState_noFingerprint_shouldShowDefaultSummary() { + @DisableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void updateState_noFingerprint_flagOff_shouldShowDefaultSummary() { when(mFingerprintManager.isHardwareDetected()).thenReturn(true); mController.updateState(mPreference); @@ -135,6 +142,19 @@ public class FingerprintStatusPreferenceControllerTest { assertThat(mPreference.isVisible()).isTrue(); } + @Test + @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void updateState_noFingerprint_flagOn_shouldShowDefaultSummary() { + when(mFingerprintManager.isHardwareDetected()).thenReturn(true); + + mController.updateState(mPreference); + + assertThat(mPreference.getSummary()).isEqualTo( + mContext.getString( + R.string.security_settings_fingerprint_preference_summary_none_new)); + assertThat(mPreference.isVisible()).isTrue(); + } + @Test public void updateState_hasFingerprint_shouldShowSummary() { when(mFingerprintManager.isHardwareDetected()).thenReturn(true); diff --git a/tests/robotests/src/com/android/settings/communal/CommunalPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/communal/CommunalPreferenceControllerTest.java index cc970eb6b1d..17a51794ebc 100644 --- a/tests/robotests/src/com/android/settings/communal/CommunalPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/communal/CommunalPreferenceControllerTest.java @@ -16,11 +16,8 @@ package com.android.settings.communal; -import static com.android.settings.core.BasePreferenceController.AVAILABLE; import static com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2; -import static com.google.common.truth.Truth.assertThat; - import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -66,6 +63,7 @@ public class CommunalPreferenceControllerTest { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) public void isAvailable_communalEnabled_shouldBeTrueForPrimaryUser() { setCommunalEnabled(true); mShadowUserManager.setUserForeground(true); @@ -73,6 +71,7 @@ public class CommunalPreferenceControllerTest { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) public void isAvailable_communalEnabled_shouldBeFalseForSecondaryUser() { setCommunalEnabled(true); mShadowUserManager.setUserForeground(false); @@ -80,6 +79,7 @@ public class CommunalPreferenceControllerTest { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) public void isAvailable_communalDisabled_shouldBeFalseForPrimaryUser() { setCommunalEnabled(false); mShadowUserManager.setUserForeground(true); @@ -88,36 +88,8 @@ public class CommunalPreferenceControllerTest { @Test @EnableFlags(FLAG_GLANCEABLE_HUB_V2) - public void isAvailable_communalOnMobileEnabled_shouldBeTrueForPrimaryUser() { - setCommunalEnabled(false); - setCommunalOnMobileEnabled(true); - mShadowUserManager.setUserForeground(true); - assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); - } - - @Test - @EnableFlags(FLAG_GLANCEABLE_HUB_V2) - public void isAvailable_communalOnMobileEnabled_shouldBeFalseForSecondaryUser() { - setCommunalEnabled(false); - setCommunalOnMobileEnabled(true); - mShadowUserManager.setUserForeground(false); - assertFalse(mController.isAvailable()); - } - - @Test - @EnableFlags(FLAG_GLANCEABLE_HUB_V2) - public void isAvailable_communalOnMobileDisabled_shouldBeFalseForPrimaryUser() { - setCommunalEnabled(false); - setCommunalOnMobileEnabled(false); - mShadowUserManager.setUserForeground(true); - assertFalse(mController.isAvailable()); - } - - @Test - @DisableFlags(FLAG_GLANCEABLE_HUB_V2) - public void isAvailable_glanceableHubV2FlagDisabled_shouldBeFalseForPrimaryUser() { - setCommunalEnabled(false); - setCommunalOnMobileEnabled(true); + public void isAvailable_glanceableHubV2Enabled_shouldBeFalseForPrimaryUser() { + setCommunalEnabled(true); mShadowUserManager.setUserForeground(true); assertFalse(mController.isAvailable()); } @@ -125,9 +97,4 @@ public class CommunalPreferenceControllerTest { private void setCommunalEnabled(boolean enabled) { SettingsShadowResources.overrideResource(R.bool.config_show_communal_settings, enabled); } - - private void setCommunalOnMobileEnabled(boolean enabled) { - SettingsShadowResources.overrideResource( - R.bool.config_show_communal_settings_mobile, enabled); - } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceControllerTest.java index 29f427449e7..a2484127b90 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceControllerTest.java @@ -45,6 +45,7 @@ import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothStatusCodes; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; @@ -79,6 +80,7 @@ import com.android.settings.testutils.shadow.ShadowFragment; import com.android.settingslib.bluetooth.A2dpProfile; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.BluetoothEventManager; +import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.HeadsetProfile; @@ -144,6 +146,7 @@ public class AudioSharingDevicePreferenceControllerTest { @Mock private LeAudioProfile mLeAudioProfile; @Mock private A2dpProfile mA2dpProfile; @Mock private HeadsetProfile mHeadsetProfile; + @Mock private ContentResolver mContentResolver; private Context mContext; private AudioSharingDevicePreferenceController mController; @@ -156,7 +159,7 @@ public class AudioSharingDevicePreferenceControllerTest { @Before public void setUp() { - mContext = ApplicationProvider.getApplicationContext(); + mContext = spy(ApplicationProvider.getApplicationContext()); mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); mShadowBluetoothAdapter.setEnabled(true); mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( @@ -185,6 +188,7 @@ public class AudioSharingDevicePreferenceControllerTest { when(mA2dpProfile.getProfileId()).thenReturn(BluetoothProfile.A2DP); when(mLeAudioProfile.getProfileId()).thenReturn(BluetoothProfile.LE_AUDIO); when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true); + when(mContext.getContentResolver()).thenReturn(mContentResolver); when(mScreen.getContext()).thenReturn(mContext); mPreferenceGroup = spy(new PreferenceCategory(mContext)); doReturn(mPreferenceManager).when(mPreferenceGroup).getPreferenceManager(); @@ -211,6 +215,12 @@ public class AudioSharingDevicePreferenceControllerTest { verify(mAssistant, never()) .registerServiceCallBack( any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class)); + verify(mContentResolver, never()) + .registerContentObserver( + Settings.Secure.getUriFor( + BluetoothUtils.getPrimaryGroupIdUriForBroadcast()), + false, + mController.mSettingsObserver); verify(mBluetoothDeviceUpdater, never()).registerCallback(); verify(mBluetoothDeviceUpdater, never()).refreshPreference(); } @@ -224,6 +234,12 @@ public class AudioSharingDevicePreferenceControllerTest { verify(mAssistant) .registerServiceCallBack( any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class)); + verify(mContentResolver) + .registerContentObserver( + Settings.Secure.getUriFor( + BluetoothUtils.getPrimaryGroupIdUriForBroadcast()), + false, + mController.mSettingsObserver); verify(mBluetoothDeviceUpdater).registerCallback(); verify(mBluetoothDeviceUpdater).refreshPreference(); } @@ -236,6 +252,7 @@ public class AudioSharingDevicePreferenceControllerTest { verify(mDialogHandler, never()).unregisterCallbacks(); verify(mAssistant, never()) .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class)); + verify(mContentResolver, never()).unregisterContentObserver(mController.mSettingsObserver); verify(mBluetoothDeviceUpdater, never()).unregisterCallback(); } @@ -247,6 +264,7 @@ public class AudioSharingDevicePreferenceControllerTest { verify(mDialogHandler).unregisterCallbacks(); verify(mAssistant) .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class)); + verify(mContentResolver).unregisterContentObserver(mController.mSettingsObserver); verify(mBluetoothDeviceUpdater).unregisterCallback(); } @@ -485,6 +503,12 @@ public class AudioSharingDevicePreferenceControllerTest { verifyNoInteractions(mDialogHandler); } + @Test + public void onFallbackDeviceChanged_updateSummary() { + mController.mSettingsObserver.onChange(true); + verify(mBluetoothDeviceUpdater).refreshPreference(); + } + @Test @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) public void handleDeviceClickFromIntent_noDevice_doNothing() { diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java index 056514620d2..820f13028f4 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java @@ -26,6 +26,9 @@ import static com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -39,6 +42,7 @@ import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothStatusCodes; import android.content.Context; import android.graphics.drawable.Drawable; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import androidx.lifecycle.LifecycleOwner; @@ -52,6 +56,7 @@ import com.android.settings.testutils.shadow.ShadowThreadUtils; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.flags.Flags; import com.android.settingslib.widget.LayoutPreference; import org.junit.After; @@ -254,6 +259,7 @@ public class AudioStreamHeaderControllerTest { } @Test + @EnableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE) public void testCallback_onReceiveStateChanged_updateButton() { when(mAudioStreamsHelper.getConnectedBroadcastIdAndState(anyBoolean())) .thenReturn(Map.of(BROADCAST_ID, STREAMING)); @@ -271,6 +277,7 @@ public class AudioStreamHeaderControllerTest { verify(mHeaderController, times(2)) .setSummary(mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)); verify(mHeaderController, times(2)).done(true); + verify(mAudioStreamsHelper, never()).startMediaService(any(), anyInt(), anyString()); } @Test diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java index c82c9787ccd..c00e2a5e379 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaServiceTest.java @@ -19,6 +19,10 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_ID; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES; import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.LEAVE_BROADCAST_ACTION; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PRIVATE_BROADCAST_RECEIVE_DATA; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.DECRYPTION_FAILED; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.PAUSED; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState.STREAMING; import static com.google.common.truth.Truth.assertThat; @@ -57,6 +61,7 @@ import android.os.Looper; import android.os.RemoteException; import android.platform.test.flag.junit.SetFlagsRule; import android.util.DisplayMetrics; +import android.view.KeyEvent; import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper; import com.android.settings.testutils.FakeFeatureFactory; @@ -69,6 +74,7 @@ import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.bluetooth.PrivateBroadcastReceiveData; import com.android.settingslib.bluetooth.VolumeControlProfile; import com.android.settingslib.flags.Flags; @@ -116,6 +122,8 @@ public class AudioStreamMediaServiceTest { @Mock private VolumeControlProfile mVolumeControlProfile; @Mock private CachedBluetoothDevice mCachedBluetoothDevice; @Mock private BluetoothDevice mDevice; + @Mock + private BluetoothDevice mDevice2; @Mock private ISession mISession; @Mock private ISessionController mISessionController; @Mock private PackageManager mPackageManager; @@ -240,6 +248,7 @@ public class AudioStreamMediaServiceTest { @Test public void onDestroy_flagOn_cleanup() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); var devices = new ArrayList(); devices.add(mDevice); @@ -256,8 +265,33 @@ public class AudioStreamMediaServiceTest { verify(mVolumeControlProfile).unregisterCallback(any()); } + @Test + public void byReceiveStateFlagOn_onDestroy_flagOn_cleanup() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + + Intent intent = setupReceiveDataIntent(1, mDevice, STREAMING); + mAudioStreamMediaService.onCreate(); + mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); + mAudioStreamMediaService.onDestroy(); + + verify(mBluetoothEventManager).unregisterCallback(any()); + verify(mLeBroadcastAssistant).unregisterServiceCallBack(any()); + verify(mVolumeControlProfile).unregisterCallback(any()); + } + + @Test + public void byReceiveStateFlagOn_onStartCommand_invalidData_stopSelf() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + Intent intent = setupReceiveDataIntent(-1, mDevice, STREAMING); + mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); + + verify(mAudioStreamMediaService).stopSelf(); + } + @Test public void onStartCommand_noBroadcastId_stopSelf() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); mAudioStreamMediaService.onStartCommand(new Intent(), /* flags= */ 0, /* startId= */ 0); verify(mAudioStreamMediaService).stopSelf(); @@ -265,6 +299,7 @@ public class AudioStreamMediaServiceTest { @Test public void onStartCommand_noDevice_stopSelf() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); Intent intent = new Intent(); intent.putExtra(BROADCAST_ID, 1); @@ -273,8 +308,110 @@ public class AudioStreamMediaServiceTest { verify(mAudioStreamMediaService).stopSelf(); } + @Test + public void byReceiveStateFlagOn_onStartCommand_createSessionAndStartForeground() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + Intent intent = setupReceiveDataIntent(1, mDevice, STREAMING); + mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); + + ArgumentCaptor notificationCapture = ArgumentCaptor.forClass( + Notification.class); + verify(mAudioStreamMediaService).startForeground(anyInt(), notificationCapture.capture()); + var notification = notificationCapture.getValue(); + assertThat(notification.getSmallIcon()).isNotNull(); + assertThat(notification.isStyle(Notification.MediaStyle.class)).isTrue(); + + verify(mAudioStreamMediaService, never()).stopSelf(); + } + + @Test + public void byReceiveStateFlagOn_onStartCommand_decryptionFailed_stopSelf() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + Intent intent = setupReceiveDataIntent(1, mDevice, DECRYPTION_FAILED); + mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); + + verify(mAudioStreamMediaService).stopSelf(); + } + + @Test + public void byReceiveStateFlagOn_onStartCommand_addDevice() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + mAudioStreamMediaService.onCreate(); + Intent intent1 = setupReceiveDataIntent(1, mDevice, STREAMING); + mAudioStreamMediaService.onStartCommand(intent1, /* flags= */ 0, /* startId= */ 0); + Intent intent2 = setupReceiveDataIntent(1, mDevice2, PAUSED); + mAudioStreamMediaService.onStartCommand(intent2, /* flags= */ 0, /* startId= */ 0); + + ArgumentCaptor notificationCapture = ArgumentCaptor.forClass( + Notification.class); + verify(mAudioStreamMediaService).startForeground(anyInt(), notificationCapture.capture()); + var notification = notificationCapture.getValue(); + assertThat(notification.getSmallIcon()).isNotNull(); + assertThat(notification.isStyle(Notification.MediaStyle.class)).isTrue(); + + assertThat(mAudioStreamMediaService.mStateByDevice).isNotNull(); + var deviceState = mAudioStreamMediaService.mStateByDevice.get(mDevice); + assertThat(deviceState).isNotNull(); + assertThat(deviceState).isEqualTo(STREAMING); + var device2State = mAudioStreamMediaService.mStateByDevice.get(mDevice2); + assertThat(device2State).isNotNull(); + assertThat(device2State).isEqualTo(PAUSED); + verify(mAudioStreamMediaService, never()).stopSelf(); + verify(mNotificationManager).notify(anyInt(), any()); + } + + @Test + public void byReceiveStateFlagOn_onStartCommand_updateState() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + mAudioStreamMediaService.onCreate(); + Intent intent1 = setupReceiveDataIntent(1, mDevice, STREAMING); + mAudioStreamMediaService.onStartCommand(intent1, /* flags= */ 0, /* startId= */ 0); + Intent intent2 = setupReceiveDataIntent(1, mDevice, PAUSED); + mAudioStreamMediaService.onStartCommand(intent2, /* flags= */ 0, /* startId= */ 0); + + ArgumentCaptor notificationCapture = ArgumentCaptor.forClass( + Notification.class); + verify(mAudioStreamMediaService).startForeground(anyInt(), notificationCapture.capture()); + var notification = notificationCapture.getValue(); + assertThat(notification.getSmallIcon()).isNotNull(); + assertThat(notification.isStyle(Notification.MediaStyle.class)).isTrue(); + + assertThat(mAudioStreamMediaService.mStateByDevice).isNotNull(); + var deviceState = mAudioStreamMediaService.mStateByDevice.get(mDevice); + assertThat(deviceState).isNotNull(); + assertThat(deviceState).isEqualTo(PAUSED); + verify(mAudioStreamMediaService, never()).stopSelf(); + verify(mNotificationManager).notify(anyInt(), any()); + } + + @Test + public void byReceiveStateFlagOn_onStartCommand_newBroadcastId() { + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + mAudioStreamMediaService.onCreate(); + Intent intent1 = setupReceiveDataIntent(1, mDevice, STREAMING); + mAudioStreamMediaService.onStartCommand(intent1, /* flags= */ 0, /* startId= */ 0); + Intent intent2 = setupReceiveDataIntent(2, mDevice2, PAUSED); + mAudioStreamMediaService.onStartCommand(intent2, /* flags= */ 0, /* startId= */ 0); + + ArgumentCaptor notificationCapture = ArgumentCaptor.forClass( + Notification.class); + verify(mAudioStreamMediaService).startForeground(anyInt(), notificationCapture.capture()); + var notification = notificationCapture.getValue(); + assertThat(notification.getSmallIcon()).isNotNull(); + assertThat(notification.isStyle(Notification.MediaStyle.class)).isTrue(); + + assertThat(mAudioStreamMediaService.mStateByDevice).isNotNull(); + var oldDeviceState = mAudioStreamMediaService.mStateByDevice.get(mDevice); + assertThat(oldDeviceState).isNull(); + var newDeviceState = mAudioStreamMediaService.mStateByDevice.get(mDevice2); + assertThat(newDeviceState).isEqualTo(PAUSED); + verify(mAudioStreamMediaService, never()).stopSelf(); + verify(mNotificationManager).notify(anyInt(), any()); + } + @Test public void onStartCommand_createSessionAndStartForeground() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); var devices = new ArrayList(); devices.add(mDevice); @@ -317,6 +454,7 @@ public class AudioStreamMediaServiceTest { @Test public void assistantCallback_onReceiveStateChanged_connected_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); @@ -338,6 +476,7 @@ public class AudioStreamMediaServiceTest { @Test public void assistantCallback_onReceiveStateChanged_hysteresis_updateNotification() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); @@ -357,6 +496,7 @@ public class AudioStreamMediaServiceTest { @Test public void assistantCallback_onReceiveStateChanged_hysteresis_flagOff_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); @@ -386,6 +526,7 @@ public class AudioStreamMediaServiceTest { @Test public void bluetoothCallback_onDeviceDisconnect_stopSelf() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); mAudioStreamMediaService.onCreate(); assertThat(mAudioStreamMediaService.mBluetoothCallback).isNotNull(); mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); @@ -398,9 +539,26 @@ public class AudioStreamMediaServiceTest { verify(mAudioStreamMediaService).stopSelf(); } + @Test + public void byReceiveStateFlagOn_bluetoothCallback_onDeviceDisconnect_stopSelf() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + mAudioStreamMediaService.onCreate(); + assertThat(mAudioStreamMediaService.mBluetoothCallback).isNotNull(); + Intent intent = setupReceiveDataIntent(1, mDevice, STREAMING); + mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); + + mAudioStreamMediaService.mBluetoothCallback.onProfileConnectionStateChanged( + mCachedBluetoothDevice, BluetoothAdapter.STATE_DISCONNECTED, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + + verify(mAudioStreamMediaService).stopSelf(); + } + @Test public void mediaSessionCallback_onPause_setVolume() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); mAudioStreamMediaService.onCreate(); mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); @@ -415,9 +573,26 @@ public class AudioStreamMediaServiceTest { eq(1)); } + @Test + public void byReceiveStateFlagOn_mediaSessionCallback_onPause_setVolume() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + + mAudioStreamMediaService.onCreate(); + Intent intent = setupReceiveDataIntent(1, mDevice, STREAMING); + mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); + assertThat(mAudioStreamMediaService.mMediaSessionCallback).isNotNull(); + mAudioStreamMediaService.mMediaSessionCallback.onPause(); + + verify(mVolumeControlProfile).setDeviceVolume(any(), anyInt(), anyBoolean()); + verify(mFeatureFactory.metricsFeatureProvider).action(any(), + eq(SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_MUTE_BUTTON_CLICK), eq(1)); + } + @Test public void mediaSessionCallback_onPlay_setVolume() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); mAudioStreamMediaService.onCreate(); mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); @@ -432,9 +607,66 @@ public class AudioStreamMediaServiceTest { eq(0)); } + @Test + public void byReceiveStateFlagOn_mediaSessionCallback_onPlay_setVolume() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + + mAudioStreamMediaService.onCreate(); + Intent intent = setupReceiveDataIntent(1, mDevice, STREAMING); + mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); + assertThat(mAudioStreamMediaService.mMediaSessionCallback).isNotNull(); + mAudioStreamMediaService.mMediaSessionCallback.onPlay(); + + verify(mVolumeControlProfile).setDeviceVolume(any(), anyInt(), anyBoolean()); + verify(mFeatureFactory.metricsFeatureProvider).action(any(), + eq(SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_MUTE_BUTTON_CLICK), eq(0)); + } + + @Test + public void byReceiveStateFlagOn_mediaSessionCallback_onButtonEventPlay_setVolume() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + + mAudioStreamMediaService.onCreate(); + Intent intent = setupReceiveDataIntent(1, mDevice, STREAMING); + mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); + assertThat(mAudioStreamMediaService.mMediaSessionCallback).isNotNull(); + + Intent buttonEvent = new Intent(); + buttonEvent.putExtra(Intent.EXTRA_KEY_EVENT, + new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY)); + mAudioStreamMediaService.mMediaSessionCallback.onMediaButtonEvent(buttonEvent); + + verify(mVolumeControlProfile).setDeviceVolume(any(), anyInt(), anyBoolean()); + verify(mFeatureFactory.metricsFeatureProvider).action(any(), + eq(SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_MUTE_BUTTON_CLICK), eq(0)); + } + + @Test + public void byReceiveStateFlagOn_mediaSessionCallback_onButtonEventPause_setVolume() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + + mAudioStreamMediaService.onCreate(); + Intent intent = setupReceiveDataIntent(1, mDevice, STREAMING); + mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); + assertThat(mAudioStreamMediaService.mMediaSessionCallback).isNotNull(); + + Intent buttonEvent = new Intent(); + buttonEvent.putExtra(Intent.EXTRA_KEY_EVENT, + new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE)); + mAudioStreamMediaService.mMediaSessionCallback.onMediaButtonEvent(buttonEvent); + + verify(mVolumeControlProfile).setDeviceVolume(any(), anyInt(), anyBoolean()); + verify(mFeatureFactory.metricsFeatureProvider).action(any(), + eq(SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_MUTE_BUTTON_CLICK), eq(1)); + } + @Test public void mediaSessionCallback_onCustomAction_leaveBroadcast() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); mAudioStreamMediaService.onCreate(); mAudioStreamMediaService.onStartCommand(setupIntent(), /* flags= */ 0, /* startId= */ 0); @@ -449,6 +681,23 @@ public class AudioStreamMediaServiceTest { eq(SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_LEAVE_BUTTON_CLICK)); } + @Test + public void byReceiveStateFlagOn_mediaSessionCallback_onCustomAction_leaveBroadcast() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE); + + mAudioStreamMediaService.onCreate(); + Intent intent = setupReceiveDataIntent(1, mDevice, STREAMING); + mAudioStreamMediaService.onStartCommand(intent, /* flags= */ 0, /* startId= */ 0); + assertThat(mAudioStreamMediaService.mMediaSessionCallback).isNotNull(); + mAudioStreamMediaService.mMediaSessionCallback.onCustomAction(LEAVE_BROADCAST_ACTION, + Bundle.EMPTY); + + verify(mAudioStreamsHelper).removeSource(anyInt()); + verify(mFeatureFactory.metricsFeatureProvider).action(any(), + eq(SettingsEnums.ACTION_AUDIO_STREAM_NOTIFICATION_LEAVE_BUTTON_CLICK)); + } + @Test public void onBind_returnNull() { IBinder binder = mAudioStreamMediaService.onBind(new Intent()); @@ -466,4 +715,13 @@ public class AudioStreamMediaServiceTest { intent.putParcelableArrayListExtra(DEVICES, devices); return intent; } + + private Intent setupReceiveDataIntent(int broadcastId, BluetoothDevice device, + LocalBluetoothLeBroadcastAssistant.LocalBluetoothLeBroadcastSourceState state) { + when(mCachedBluetoothDevice.getDevice()).thenReturn(mDevice); + Intent intent = new Intent(); + intent.putExtra(EXTRA_PRIVATE_BROADCAST_RECEIVE_DATA, + new PrivateBroadcastReceiveData(device, 1, broadcastId, "programInfo", state)); + return intent; + } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java index 59a42a1023d..bda0e6ea21a 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java @@ -22,9 +22,12 @@ import static com.android.settings.connecteddevice.audiosharing.audiostreams.Sou import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -33,6 +36,9 @@ import android.bluetooth.BluetoothLeBroadcastMetadata; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.fragment.app.FragmentActivity; import androidx.preference.Preference; @@ -43,6 +49,7 @@ import com.android.settings.SettingsActivity; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.shadow.ShadowFragment; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; +import com.android.settingslib.flags.Flags; import org.junit.Before; import org.junit.Rule; @@ -62,6 +69,7 @@ import org.robolectric.annotation.Config; }) public class SourceAddedStateTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private static final int BROADCAST_ID = 1; private static final String BROADCAST_TITLE = "title"; private final Context mContext = ApplicationProvider.getApplicationContext(); @@ -105,7 +113,8 @@ public class SourceAddedStateTest { } @Test - public void testPerformAction() { + @DisableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE) + public void testPerformAction_startService() { mInstance.setAudioStreamsRepositoryForTesting(mRepository); BluetoothLeBroadcastMetadata mockMetadata = mock(BluetoothLeBroadcastMetadata.class); when(mRepository.getCachedMetadata(anyInt())).thenReturn(mockMetadata); @@ -124,6 +133,27 @@ public class SourceAddedStateTest { verify(mHelper).startMediaService(eq(mContext), eq(BROADCAST_ID), eq(BROADCAST_TITLE)); } + @Test + @EnableFlags(Flags.FLAG_AUDIO_STREAM_MEDIA_SERVICE_BY_RECEIVE_STATE) + public void testPerformAction_skipStartService() { + mInstance.setAudioStreamsRepositoryForTesting(mRepository); + BluetoothLeBroadcastMetadata mockMetadata = mock(BluetoothLeBroadcastMetadata.class); + when(mRepository.getCachedMetadata(anyInt())).thenReturn(mockMetadata); + when(mPreference.getContext()).thenReturn(mContext); + when(mPreference.getSourceOriginForLogging()) + .thenReturn(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS); + + mInstance.performAction(mPreference, mController, mHelper); + + verify(mRepository).saveMetadata(eq(mContext), eq(mockMetadata)); + verify(mFeatureFactory.metricsFeatureProvider) + .action( + eq(mContext), + eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED), + eq(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS.ordinal())); + verify(mHelper, never()).startMediaService(any(), anyInt(), anyString()); + } + @Test public void testGetOnClickListener_startSubSettings() { when(mController.getFragment()).thenReturn(mFragment); diff --git a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsControllerTest.kt b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsControllerTest.kt index fb8e4762bbc..5060bd65b33 100644 --- a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsControllerTest.kt +++ b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceDetailsControllerTest.kt @@ -18,22 +18,30 @@ package com.android.settings.network.ethernet import android.content.Context import android.content.ContextWrapper +import androidx.lifecycle.Lifecycle import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock @RunWith(AndroidJUnit4::class) class EthernetInterfaceDetailsControllerTest { private val ethernetInterfaceDetailsFragment = EthernetInterfaceDetailsFragment() + private val lifecycle = mock() private val context: Context = object : ContextWrapper(ApplicationProvider.getApplicationContext()) {} private val ethernetInterfaceDetailsController = - EthernetInterfaceDetailsController(context, ethernetInterfaceDetailsFragment, "eth0") + EthernetInterfaceDetailsController( + context, + ethernetInterfaceDetailsFragment, + "eth0", + lifecycle, + ) @Test fun isAvailable_ShouldReturnTrue() { diff --git a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTest.kt b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTest.kt index 94487be2372..473a4d94635 100644 --- a/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTest.kt +++ b/tests/robotests/src/com/android/settings/network/ethernet/EthernetInterfaceTest.kt @@ -18,14 +18,19 @@ package com.android.settings.network.ethernet import android.content.Context import android.content.ContextWrapper +import android.net.ConnectivityManager import android.net.EthernetManager import android.net.EthernetManager.STATE_ABSENT import android.net.EthernetManager.STATE_LINK_DOWN import android.net.EthernetManager.STATE_LINK_UP import android.net.IpConfiguration +import android.net.LinkProperties +import android.net.Network import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -34,12 +39,15 @@ import org.mockito.kotlin.mock class EthernetInterfaceTest { private val mockEthernetManager = mock() + private val mockConnectivityManager = mock() + private val mockNetwork = mock() private val context: Context = object : ContextWrapper(ApplicationProvider.getApplicationContext()) { override fun getSystemService(name: String): Any? = when (name) { Context.ETHERNET_SERVICE -> mockEthernetManager + Context.CONNECTIVITY_SERVICE -> mockConnectivityManager else -> super.getSystemService(name) } } @@ -85,4 +93,27 @@ class EthernetInterfaceTest { IpConfiguration.IpAssignment.UNASSIGNED, ) } + + @Test + fun linkPropertiesChanged_shouldUpdate() { + val linkProperties = LinkProperties() + linkProperties.setInterfaceName("eth0") + linkProperties.setUsePrivateDns(true) + + ethernetInterface.networkCallback.onLinkPropertiesChanged(mockNetwork, linkProperties) + + assertEquals(ethernetInterface.getLinkProperties().getInterfaceName(), "eth0") + assertTrue(ethernetInterface.getLinkProperties().isPrivateDnsActive()) + } + + @Test + fun linkPropertiesChanged_iddoesnotmatch_shouldNotUpdate() { + val linkProperties = LinkProperties() + linkProperties.setInterfaceName("eth1") + linkProperties.setUsePrivateDns(true) + + ethernetInterface.networkCallback.onLinkPropertiesChanged(mockNetwork, linkProperties) + + assertFalse(ethernetInterface.getLinkProperties().isPrivateDnsActive()) + } } diff --git a/tests/robotests/src/com/android/settings/supervision/SupervisionDashboardScreenTest.kt b/tests/robotests/src/com/android/settings/supervision/SupervisionDashboardScreenTest.kt index d5fa2972513..bf579d9fd89 100644 --- a/tests/robotests/src/com/android/settings/supervision/SupervisionDashboardScreenTest.kt +++ b/tests/robotests/src/com/android/settings/supervision/SupervisionDashboardScreenTest.kt @@ -15,6 +15,7 @@ */ package com.android.settings.supervision +import android.app.Activity import android.app.supervision.flags.Flags import android.content.Context import android.platform.test.annotations.DisableFlags @@ -24,6 +25,7 @@ import androidx.fragment.app.testing.FragmentScenario import androidx.preference.Preference import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.supervision.SupervisionMainSwitchPreference.Companion.REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS import com.android.settingslib.widget.MainSwitchPreference import com.google.common.truth.Truth.assertThat import org.junit.Rule @@ -57,7 +59,7 @@ class SupervisionDashboardScreenTest { @Test @EnableFlags(Flags.FLAG_ENABLE_SUPERVISION_SETTINGS_SCREEN) - fun toggleMainSwitch_disablesChildPreferences() { + fun toggleMainSwitch_pinVerificationSucceeded_enablesChildPreferences() { FragmentScenario.launchInContainer(preferenceScreenCreator.fragmentClass()).onFragment { fragment -> val mainSwitchPreference = @@ -68,8 +70,38 @@ class SupervisionDashboardScreenTest { assertThat(childPreference.isEnabled).isFalse() mainSwitchPreference.performClick() + // Pretend the PIN verification succeeded. + fragment.onActivityResult( + requestCode = REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS, + resultCode = Activity.RESULT_OK, + data = null, + ) assertThat(childPreference.isEnabled).isTrue() } } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_SUPERVISION_SETTINGS_SCREEN) + fun toggleMainSwitch_pinVerificationFailed_childPreferencesRemainDisabled() { + FragmentScenario.launchInContainer(preferenceScreenCreator.fragmentClass()).onFragment { + fragment -> + val mainSwitchPreference = + fragment.findPreference(SupervisionMainSwitchPreference.KEY)!! + val childPreference = + fragment.findPreference(SupervisionPinManagementScreen.KEY)!! + + assertThat(childPreference.isEnabled).isFalse() + + mainSwitchPreference.performClick() + // Pretend the PIN verification failed. + fragment.onActivityResult( + requestCode = REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS, + resultCode = Activity.RESULT_CANCELED, + data = null, + ) + + assertThat(childPreference.isEnabled).isFalse() + } + } } diff --git a/tests/robotests/src/com/android/settings/supervision/SupervisionMainSwitchPreferenceTest.kt b/tests/robotests/src/com/android/settings/supervision/SupervisionMainSwitchPreferenceTest.kt index 8b15c29b869..c7c393d9f9e 100644 --- a/tests/robotests/src/com/android/settings/supervision/SupervisionMainSwitchPreferenceTest.kt +++ b/tests/robotests/src/com/android/settings/supervision/SupervisionMainSwitchPreferenceTest.kt @@ -15,25 +15,33 @@ */ package com.android.settings.supervision +import android.app.Activity import android.app.supervision.SupervisionManager import android.content.Context import android.content.ContextWrapper +import android.content.Intent +import androidx.preference.Preference import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.supervision.SupervisionMainSwitchPreference.Companion.REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS +import com.android.settingslib.metadata.PreferenceLifecycleContext import com.android.settingslib.preference.createAndBindWidget import com.android.settingslib.widget.MainSwitchPreference import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.stub import org.mockito.kotlin.verify @RunWith(AndroidJUnit4::class) class SupervisionMainSwitchPreferenceTest { - private val preference = SupervisionMainSwitchPreference() - + private val mockLifeCycleContext = mock() private val mockSupervisionManager = mock() private val appContext: Context = ApplicationProvider.getApplicationContext() @@ -46,6 +54,13 @@ class SupervisionMainSwitchPreferenceTest { } } + private val preference = SupervisionMainSwitchPreference(context) + + @Before + fun setUp() { + preference.onCreate(mockLifeCycleContext) + } + @Test fun checked_supervisionEnabled_returnTrue() { setSupervisionEnabled(true) @@ -61,7 +76,7 @@ class SupervisionMainSwitchPreferenceTest { } @Test - fun toggleOn() { + fun toggleOn_triggersPinVerification() { setSupervisionEnabled(false) val widget = getMainSwitchPreference() @@ -69,26 +84,90 @@ class SupervisionMainSwitchPreferenceTest { widget.performClick() + verifyConfirmSupervisionCredentialsActivityStarted() + assertThat(widget.isChecked).isFalse() + verify(mockSupervisionManager, never()).setSupervisionEnabled(false) + } + + @Test + fun toggleOn_pinVerificationSucceeded_supervisionEnabled() { + setSupervisionEnabled(false) + val widget = getMainSwitchPreference() + + assertThat(widget.isChecked).isFalse() + + preference.onActivityResult( + mockLifeCycleContext, + REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS, + Activity.RESULT_OK, + null, + ) + assertThat(widget.isChecked).isTrue() verify(mockSupervisionManager).setSupervisionEnabled(true) } @Test - fun toggleOff() { + fun toggleOff_pinVerificationSucceeded_supervisionDisabled() { setSupervisionEnabled(true) val widget = getMainSwitchPreference() assertThat(widget.isChecked).isTrue() - widget.performClick() + preference.onActivityResult( + mockLifeCycleContext, + REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS, + Activity.RESULT_OK, + null, + ) assertThat(widget.isChecked).isFalse() verify(mockSupervisionManager).setSupervisionEnabled(false) } - private fun getMainSwitchPreference(): MainSwitchPreference = - preference.createAndBindWidget(context) + @Test + fun toggleOff_pinVerificationFailed_supervisionNotEnabled() { + setSupervisionEnabled(true) + val widget = getMainSwitchPreference() + + assertThat(widget.isChecked).isTrue() + + preference.onActivityResult( + mockLifeCycleContext, + REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS, + Activity.RESULT_CANCELED, + null, + ) + + assertThat(widget.isChecked).isTrue() + verify(mockSupervisionManager, never()).setSupervisionEnabled(true) + } private fun setSupervisionEnabled(enabled: Boolean) = mockSupervisionManager.stub { on { isSupervisionEnabled } doReturn enabled } + + private fun getMainSwitchPreference(): MainSwitchPreference { + val widget: MainSwitchPreference = preference.createAndBindWidget(context) + + mockLifeCycleContext.stub { + on { findPreference(SupervisionMainSwitchPreference.KEY) } doReturn widget + on { + requirePreference(SupervisionMainSwitchPreference.KEY) + } doReturn widget + } + return widget + } + + private fun verifyConfirmSupervisionCredentialsActivityStarted() { + val intentCaptor = argumentCaptor() + verify(mockLifeCycleContext) + .startActivityForResult( + intentCaptor.capture(), + eq(REQUEST_CODE_CONFIRM_SUPERVISION_CREDENTIALS), + eq(null), + ) + assertThat(intentCaptor.allValues.size).isEqualTo(1) + assertThat(intentCaptor.firstValue.component?.className) + .isEqualTo(ConfirmSupervisionCredentialsActivity::class.java.name) + } } diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appcompat/OWNERS b/tests/spa_unit/src/com/android/settings/spa/app/appcompat/OWNERS new file mode 100644 index 00000000000..5ef6ded95f1 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/appcompat/OWNERS @@ -0,0 +1,5 @@ +# Bug component: 970984 +# Large Screen Experiences App Compat +gracielawputri@google.com +mcarli@google.com +mariiasand@google.com \ No newline at end of file diff --git a/tests/unit/src/com/android/settings/applications/appcompat/OWNERS b/tests/unit/src/com/android/settings/applications/appcompat/OWNERS new file mode 100644 index 00000000000..5ef6ded95f1 --- /dev/null +++ b/tests/unit/src/com/android/settings/applications/appcompat/OWNERS @@ -0,0 +1,5 @@ +# Bug component: 970984 +# Large Screen Experiences App Compat +gracielawputri@google.com +mcarli@google.com +mariiasand@google.com \ No newline at end of file diff --git a/tests/unit/src/com/android/settings/biometrics/ParentalControlsUtilsTest.java b/tests/unit/src/com/android/settings/biometrics/ParentalControlsUtilsTest.java index 2b6184b9153..2ca90c6c6af 100644 --- a/tests/unit/src/com/android/settings/biometrics/ParentalControlsUtilsTest.java +++ b/tests/unit/src/com/android/settings/biometrics/ParentalControlsUtilsTest.java @@ -27,13 +27,12 @@ import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.app.admin.DevicePolicyManager; import android.app.supervision.SupervisionManager; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; import android.os.UserHandle; @@ -44,6 +43,7 @@ import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.settingslib.RestrictedLockUtils; @@ -53,22 +53,29 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; @RunWith(AndroidJUnit4.class) public class ParentalControlsUtilsTest { - @Rule public final CheckFlagsRule checkFlags = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Rule + public final CheckFlagsRule checkFlags = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Rule + public final MockitoRule mocks = MockitoJUnit.rule(); - @Mock private Context mContext; - @Mock private DevicePolicyManager mDpm; - @Mock private SupervisionManager mSm; + private Context mContext; + @Mock + private DevicePolicyManager mDpm; + @Mock + private SupervisionManager mSm; - private ComponentName mSupervisionComponentName = new ComponentName("pkg", "cls"); + private final ComponentName mSupervisionComponent = new ComponentName("pkg", "cls"); @Before public void setUp() { - MockitoAnnotations.initMocks(this); - when(mContext.getContentResolver()).thenReturn(mock(ContentResolver.class)); + mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getSystemService(DevicePolicyManager.class)).thenReturn(mDpm); + when(mContext.getSystemService(SupervisionManager.class)).thenReturn(mSm); } /** @@ -85,7 +92,7 @@ public class ParentalControlsUtilsTest { .thenReturn(keyguardDisabledFlags); return ParentalControlsUtils.parentConsentRequiredInternal( - mDpm, mSm, modality, new UserHandle(UserHandle.myUserId())); + mContext, modality, new UserHandle(UserHandle.myUserId())); } /** @@ -97,11 +104,13 @@ public class ParentalControlsUtilsTest { boolean supervisionEnabled, @BiometricAuthenticator.Modality int modality, int keyguardDisabledFlags) { - when(mSm.isSupervisionEnabledForUser(anyInt())).thenReturn(supervisionEnabled); when(mDpm.getKeyguardDisabledFeatures(eq(null))).thenReturn(keyguardDisabledFlags); + when(mSm.isSupervisionEnabledForUser(anyInt())).thenReturn(supervisionEnabled); + when(mSm.getActiveSupervisionAppPackage()).thenReturn( + supervisionEnabled ? mSupervisionComponent.getPackageName() : null); return ParentalControlsUtils.parentConsentRequiredInternal( - mDpm, mSm, modality, new UserHandle(UserHandle.myUserId())); + mContext, modality, new UserHandle(UserHandle.myUserId())); } @Test @@ -115,11 +124,11 @@ public class ParentalControlsUtilsTest { for (int i = 0; i < tests.length; i++) { RestrictedLockUtils.EnforcedAdmin admin = getEnforcedAdminForCombination( - mSupervisionComponentName, tests[i][0] /* modality */, + mSupervisionComponent, tests[i][0] /* modality */, tests[i][1] /* keyguardDisableFlags */); assertNotNull(admin); assertEquals(UserManager.DISALLOW_BIOMETRIC, admin.enforcedRestriction); - assertEquals(mSupervisionComponentName, admin.component); + assertEquals(mSupervisionComponent, admin.component); } } diff --git a/tests/unit/src/com/android/settings/biometrics/combination/CombinedBiometricStatusUtilsTest.java b/tests/unit/src/com/android/settings/biometrics/combination/CombinedBiometricStatusUtilsTest.java index f2cf6b9f585..8425f4fa679 100644 --- a/tests/unit/src/com/android/settings/biometrics/combination/CombinedBiometricStatusUtilsTest.java +++ b/tests/unit/src/com/android/settings/biometrics/combination/CombinedBiometricStatusUtilsTest.java @@ -198,6 +198,7 @@ public class CombinedBiometricStatusUtilsTest { @EnableFlags(android.app.supervision.flags.Flags.FLAG_DEPRECATE_DPM_SUPERVISION_APIS) public void getDisabledAdmin_whenFingerprintDisabled_whenFaceDisabled_returnsRestrictions() { when(mSupervisionManager.isSupervisionEnabledForUser(USER_ID)).thenReturn(true); + when(mSupervisionManager.getActiveSupervisionAppPackage()).thenReturn("supervision.pkg"); when(mDevicePolicyManager.getKeyguardDisabledFeatures(null)) .thenReturn(KEYGUARD_DISABLE_FACE | KEYGUARD_DISABLE_FINGERPRINT); diff --git a/tests/unit/src/com/android/settings/biometrics/face/FaceStatusUtilsTest.java b/tests/unit/src/com/android/settings/biometrics/face/FaceStatusUtilsTest.java index f5d090af72b..e6bfdbd372f 100644 --- a/tests/unit/src/com/android/settings/biometrics/face/FaceStatusUtilsTest.java +++ b/tests/unit/src/com/android/settings/biometrics/face/FaceStatusUtilsTest.java @@ -155,6 +155,7 @@ public class FaceStatusUtilsTest { @EnableFlags(android.app.supervision.flags.Flags.FLAG_DEPRECATE_DPM_SUPERVISION_APIS) public void getDisabledAdmin_whenFaceDisabled_returnsRestriction() { when(mSupervisionManager.isSupervisionEnabledForUser(USER_ID)).thenReturn(true); + when(mSupervisionManager.getActiveSupervisionAppPackage()).thenReturn("supervision.pkg"); when(mDevicePolicyManager.getKeyguardDisabledFeatures(null)) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FACE); @@ -172,7 +173,8 @@ public class FaceStatusUtilsTest { } @Test - public void getSummary_whenNotEnrolled_returnsSummaryNone() { + @DisableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void getSummary_whenNotEnrolled_flagOff_returnsSummaryNone() { when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false); assertThat(mFaceStatusUtils.getSummary()) @@ -181,6 +183,17 @@ public class FaceStatusUtilsTest { "security_settings_face_preference_summary_none")); } + @Test + @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void getSummary_whenNotEnrolled_flagOn_returnsSummaryNone() { + when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false); + + assertThat(mFaceStatusUtils.getSummary()) + .isEqualTo(ResourcesUtils.getResourcesString( + mApplicationContext, + "security_settings_face_preference_summary_none_new")); + } + @Test public void getSummary_whenEnrolled_returnsSummary() { when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); diff --git a/tests/unit/src/com/android/settings/biometrics/fingerprint/FingerprintStatusUtilsTest.java b/tests/unit/src/com/android/settings/biometrics/fingerprint/FingerprintStatusUtilsTest.java index 375c1ff0c0f..9c77a6d09d6 100644 --- a/tests/unit/src/com/android/settings/biometrics/fingerprint/FingerprintStatusUtilsTest.java +++ b/tests/unit/src/com/android/settings/biometrics/fingerprint/FingerprintStatusUtilsTest.java @@ -161,6 +161,7 @@ public class FingerprintStatusUtilsTest { @EnableFlags(android.app.supervision.flags.Flags.FLAG_DEPRECATE_DPM_SUPERVISION_APIS) public void getDisabledAdmin_whenFingerprintDisabled_returnsRestriction() { when(mSupervisionManager.isSupervisionEnabledForUser(USER_ID)).thenReturn(true); + when(mSupervisionManager.getActiveSupervisionAppPackage()).thenReturn("supervision.pkg"); when(mDevicePolicyManager.getKeyguardDisabledFeatures(null)) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT); @@ -179,7 +180,8 @@ public class FingerprintStatusUtilsTest { } @Test - public void getSummary_whenNotEnrolled_returnsSummaryNone() { + @DisableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void getSummary_whenNotEnrolled_flagOff_returnsSummaryNone() { when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(false); assertThat(mFingerprintStatusUtils.getSummary()) @@ -188,6 +190,17 @@ public class FingerprintStatusUtilsTest { "security_settings_fingerprint_preference_summary_none")); } + @Test + @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void getSummary_whenNotEnrolled_flagOn_returnsSummaryNone() { + when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(false); + + assertThat(mFingerprintStatusUtils.getSummary()) + .isEqualTo(ResourcesUtils.getResourcesString( + mApplicationContext, + "security_settings_fingerprint_preference_summary_none_new")); + } + @Test public void getSummary_whenEnrolled_returnsSummary() { final int enrolledFingerprintsCount = 2; diff --git a/tests/unit/src/com/android/settings/safetycenter/FaceSafetySourceTest.java b/tests/unit/src/com/android/settings/safetycenter/FaceSafetySourceTest.java index 9a162ae9a1f..1d6f48f728c 100644 --- a/tests/unit/src/com/android/settings/safetycenter/FaceSafetySourceTest.java +++ b/tests/unit/src/com/android/settings/safetycenter/FaceSafetySourceTest.java @@ -187,7 +187,7 @@ public class FaceSafetySourceTest { assertSafetySourceDisabledDataSetWithSingularSummary( "security_settings_face_preference_title_new", - "security_settings_face_preference_summary_none"); + "security_settings_face_preference_summary_none_new"); } @Test @@ -195,6 +195,7 @@ public class FaceSafetySourceTest { @EnableFlags(android.app.supervision.flags.Flags.FLAG_DEPRECATE_DPM_SUPERVISION_APIS) public void setSafetySourceData_withFaceNotEnrolled_whenSupervisionIsOn_setsData() { when(mSupervisionManager.isSupervisionEnabledForUser(USER_ID)).thenReturn(true); + when(mSupervisionManager.getActiveSupervisionAppPackage()).thenReturn("supervision.pkg"); when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); when(mFaceManager.isHardwareDetected()).thenReturn(true); when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false); @@ -205,7 +206,7 @@ public class FaceSafetySourceTest { assertSafetySourceDisabledDataSetWithSingularSummary( "security_settings_face_preference_title_new", - "security_settings_face_preference_summary_none"); + "security_settings_face_preference_summary_none_new"); } @Test @@ -220,7 +221,7 @@ public class FaceSafetySourceTest { assertSafetySourceEnabledDataSetWithSingularSummary( "security_settings_face_preference_title_new", - "security_settings_face_preference_summary_none", + "security_settings_face_preference_summary_none_new", FaceEnrollIntroductionInternal.class.getName()); } @@ -248,6 +249,7 @@ public class FaceSafetySourceTest { @EnableFlags(android.app.supervision.flags.Flags.FLAG_DEPRECATE_DPM_SUPERVISION_APIS) public void setSafetySourceData_withFaceEnrolled_whenSupervisionIsOn_setsData() { when(mSupervisionManager.isSupervisionEnabledForUser(USER_ID)).thenReturn(true); + when(mSupervisionManager.getActiveSupervisionAppPackage()).thenReturn("supervision.pkg"); when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); when(mFaceManager.isHardwareDetected()).thenReturn(true); when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); diff --git a/tests/unit/src/com/android/settings/safetycenter/FingerprintSafetySourceTest.java b/tests/unit/src/com/android/settings/safetycenter/FingerprintSafetySourceTest.java index fd109f61638..dac2699ac71 100644 --- a/tests/unit/src/com/android/settings/safetycenter/FingerprintSafetySourceTest.java +++ b/tests/unit/src/com/android/settings/safetycenter/FingerprintSafetySourceTest.java @@ -203,7 +203,7 @@ public class FingerprintSafetySourceTest { assertSafetySourceDisabledDataSetWithSingularSummary( "security_settings_fingerprint", - "security_settings_fingerprint_preference_summary_none"); + "security_settings_fingerprint_preference_summary_none_new"); } @Test @@ -211,6 +211,7 @@ public class FingerprintSafetySourceTest { @EnableFlags(android.app.supervision.flags.Flags.FLAG_DEPRECATE_DPM_SUPERVISION_APIS) public void setSafetySourceData_withFingerprintNotEnrolled_whenSupervisionIsOn_setsData() { when(mSupervisionManager.isSupervisionEnabledForUser(USER_ID)).thenReturn(true); + when(mSupervisionManager.getActiveSupervisionAppPackage()).thenReturn("supervision.pkg"); when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); when(mFingerprintManager.isHardwareDetected()).thenReturn(true); when(mFingerprintManager.hasEnrolledFingerprints(anyInt())).thenReturn(false); @@ -222,7 +223,7 @@ public class FingerprintSafetySourceTest { assertSafetySourceDisabledDataSetWithSingularSummary( "security_settings_fingerprint", - "security_settings_fingerprint_preference_summary_none"); + "security_settings_fingerprint_preference_summary_none_new"); } @Test @@ -238,7 +239,7 @@ public class FingerprintSafetySourceTest { assertSafetySourceEnabledDataSetWithSingularSummary( "security_settings_fingerprint", - "security_settings_fingerprint_preference_summary_none", + "security_settings_fingerprint_preference_summary_none_new", FingerprintSettings.class.getName()); } @@ -272,6 +273,7 @@ public class FingerprintSafetySourceTest { public void setSafetySourceData_withFingerprintsEnrolled_whenSupervisionIsOn_setsData() { int enrolledFingerprintsCount = 2; when(mSupervisionManager.isSupervisionEnabledForUser(USER_ID)).thenReturn(true); + when(mSupervisionManager.getActiveSupervisionAppPackage()).thenReturn("supervision.pkg"); when(mSafetyCenterManagerWrapper.isEnabled(mApplicationContext)).thenReturn(true); when(mFingerprintManager.isHardwareDetected()).thenReturn(true); when(mFingerprintManager.hasEnrolledFingerprints(anyInt())).thenReturn(true); diff --git a/tests/unit/src/com/android/settings/safetycenter/OWNERS b/tests/unit/src/com/android/settings/safetycenter/OWNERS new file mode 100644 index 00000000000..67321c16c74 --- /dev/null +++ b/tests/unit/src/com/android/settings/safetycenter/OWNERS @@ -0,0 +1 @@ +include /src/com/android/settings/safetycenter/OWNERS diff --git a/tests/unit/src/com/android/settings/security/ScreenLockPreferenceDetailsUtilsTest.java b/tests/unit/src/com/android/settings/security/ScreenLockPreferenceDetailsUtilsTest.java index 25358708043..2e674e0b79e 100644 --- a/tests/unit/src/com/android/settings/security/ScreenLockPreferenceDetailsUtilsTest.java +++ b/tests/unit/src/com/android/settings/security/ScreenLockPreferenceDetailsUtilsTest.java @@ -32,10 +32,13 @@ import android.content.Intent; import android.content.res.Resources; import android.os.UserManager; import android.os.storage.StorageManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -64,6 +67,8 @@ public class ScreenLockPreferenceDetailsUtilsTest { private static final int SOURCE_METRICS_CATEGORY = 10; private static final int USER_ID = 11; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @Mock @@ -118,7 +123,8 @@ public class ScreenLockPreferenceDetailsUtilsTest { } @Test - public void getSummary_unsecureAndDisabledPattern_shouldReturnUnlockModeOff() { + @DisableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void getSummary_unsecureAndDisabledPattern_flagOff_shouldReturnUnlockModeOff() { final String summary = prepareString("unlock_set_unlock_mode_off", "unlockModeOff"); when(mLockPatternUtils.isSecure(USER_ID)).thenReturn(false); @@ -127,6 +133,17 @@ public class ScreenLockPreferenceDetailsUtilsTest { assertThat(mScreenLockPreferenceDetailsUtils.getSummary(USER_ID)).isEqualTo(summary); } + @Test + @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION) + public void getSummary_unsecureAndDisabledPattern_flagOn_shouldReturnUnlockModeOff() { + final String summary = prepareString("unlock_set_unlock_mode_off_new", "unlockModeOff"); + + when(mLockPatternUtils.isSecure(USER_ID)).thenReturn(false); + when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(true); + + assertThat(mScreenLockPreferenceDetailsUtils.getSummary(USER_ID)).isEqualTo(summary); + } + @Test public void getSummary_unsecurePattern_shouldReturnUnlockModeNone() { final String summary =