diff --git a/res-product/values/strings.xml b/res-product/values/strings.xml index a42153eca7f..a6db4d9f1dd 100644 --- a/res-product/values/strings.xml +++ b/res-product/values/strings.xml @@ -758,4 +758,8 @@ To listen to an audio stream, first connect headphones that support LE Audio to this phone. To listen to an audio stream, first connect headphones that support LE Audio to this tablet. To listen to an audio stream, first connect headphones that support LE Audio to this device. + + This phone doesn\'t support LE Audio, which is needed to listen to audio streams. + This tablet doesn\'t support LE Audio, which is needed to listen to audio streams. + This device doesn\'t support LE Audio, which is needed to listen to audio streams. diff --git a/res/layout/user_select.xml b/res/layout/user_select.xml index 8c8c37a64fe..cbe097747fa 100644 --- a/res/layout/user_select.xml +++ b/res/layout/user_select.xml @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + diff --git a/res/xml/modes_calls_settings.xml b/res/xml/modes_calls_settings.xml new file mode 100644 index 00000000000..f2ba7f13867 --- /dev/null +++ b/res/xml/modes_calls_settings.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + diff --git a/res/xml/modes_messages_settings.xml b/res/xml/modes_messages_settings.xml new file mode 100644 index 00000000000..d4aee3d1947 --- /dev/null +++ b/res/xml/modes_messages_settings.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/res/xml/modes_other_settings.xml b/res/xml/modes_other_settings.xml new file mode 100644 index 00000000000..2dc2c7e0f61 --- /dev/null +++ b/res/xml/modes_other_settings.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/res/xml/modes_people_settings.xml b/res/xml/modes_people_settings.xml new file mode 100644 index 00000000000..136a357dddc --- /dev/null +++ b/res/xml/modes_people_settings.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/res/xml/modes_rule_settings.xml b/res/xml/modes_rule_settings.xml index 1b79153cf7b..7a08a68d8c0 100644 --- a/res/xml/modes_rule_settings.xml +++ b/res/xml/modes_rule_settings.xml @@ -22,4 +22,12 @@ android:key="header" android:layout="@layout/settings_entity_header" /> + + + + \ No newline at end of file diff --git a/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceController.java b/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceController.java index 4a37a416414..43d11b0148d 100644 --- a/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceController.java +++ b/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceController.java @@ -34,7 +34,6 @@ import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode; -import com.android.settings.core.TogglePreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; @@ -44,8 +43,8 @@ import com.android.settingslib.core.lifecycle.events.OnResume; * feature, where the magnifier will not deactivate on Activity transitions; it will only zoom out * to 100%. */ -public class MagnificationAlwaysOnPreferenceController extends TogglePreferenceController - implements LifecycleObserver, OnResume, OnPause { +public class MagnificationAlwaysOnPreferenceController extends + MagnificationFeaturePreferenceController implements LifecycleObserver, OnResume, OnPause { private static final String TAG = MagnificationAlwaysOnPreferenceController.class.getSimpleName(); @@ -89,7 +88,7 @@ public class MagnificationAlwaysOnPreferenceController extends TogglePreferenceC @Override public int getAvailabilityStatus() { - return AVAILABLE; + return isInSetupWizard() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE; } @Override diff --git a/src/com/android/settings/accessibility/MagnificationFeaturePreferenceController.java b/src/com/android/settings/accessibility/MagnificationFeaturePreferenceController.java new file mode 100644 index 00000000000..9664756ac45 --- /dev/null +++ b/src/com/android/settings/accessibility/MagnificationFeaturePreferenceController.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accessibility; + +import android.content.Context; + +import com.android.settings.core.TogglePreferenceController; + +/** + * A base preference controller of magnification feature with common methods. + */ +public abstract class MagnificationFeaturePreferenceController extends TogglePreferenceController { + + private boolean mInSetupWizard; + + protected MagnificationFeaturePreferenceController(Context context, + String preferenceKey) { + super(context, preferenceKey); + } + + protected final boolean isInSetupWizard() { + return mInSetupWizard; + } + + protected final void setInSetupWizard(boolean inSetupWizard) { + mInSetupWizard = inSetupWizard; + } +} diff --git a/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceController.java b/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceController.java index b269a9975e5..6092b4b809c 100644 --- a/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceController.java +++ b/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceController.java @@ -23,10 +23,10 @@ import android.content.Context; import android.provider.Settings; import com.android.settings.R; -import com.android.settings.core.TogglePreferenceController; /** Controller that accesses and switches the preference status of following typing feature */ -public class MagnificationFollowTypingPreferenceController extends TogglePreferenceController { +public class MagnificationFollowTypingPreferenceController extends + MagnificationFeaturePreferenceController { private static final String TAG = MagnificationFollowTypingPreferenceController.class.getSimpleName(); @@ -38,7 +38,7 @@ public class MagnificationFollowTypingPreferenceController extends TogglePrefere @Override public int getAvailabilityStatus() { - return AVAILABLE; + return isInSetupWizard() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE; } @Override diff --git a/src/com/android/settings/accessibility/MagnificationJoystickPreferenceController.java b/src/com/android/settings/accessibility/MagnificationJoystickPreferenceController.java index b480a0a3f2f..690a9453c29 100644 --- a/src/com/android/settings/accessibility/MagnificationJoystickPreferenceController.java +++ b/src/com/android/settings/accessibility/MagnificationJoystickPreferenceController.java @@ -23,12 +23,12 @@ import android.content.Context; import android.provider.Settings; import com.android.settings.R; -import com.android.settings.core.TogglePreferenceController; /** * Controller that accesses and switches the preference status of the magnification joystick feature */ -public class MagnificationJoystickPreferenceController extends TogglePreferenceController { +public class MagnificationJoystickPreferenceController extends + MagnificationFeaturePreferenceController { private static final String TAG = MagnificationJoystickPreferenceController.class.getSimpleName(); @@ -40,7 +40,7 @@ public class MagnificationJoystickPreferenceController extends TogglePreferenceC @Override public int getAvailabilityStatus() { - return AVAILABLE; + return isInSetupWizard() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE; } @Override diff --git a/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceController.java b/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceController.java index 4eb5090dce6..3e3b8d907df 100644 --- a/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceController.java +++ b/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceController.java @@ -35,13 +35,12 @@ import androidx.preference.TwoStatePreference; import com.android.settings.R; import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode; -import com.android.settings.core.TogglePreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; -public class MagnificationOneFingerPanningPreferenceController - extends TogglePreferenceController implements LifecycleObserver, OnResume, OnPause { +public class MagnificationOneFingerPanningPreferenceController extends + MagnificationFeaturePreferenceController implements LifecycleObserver, OnResume, OnPause { static final String PREF_KEY = Settings.Secure.ACCESSIBILITY_SINGLE_FINGER_PANNING_ENABLED; private TwoStatePreference mSwitchPreference; @@ -82,7 +81,7 @@ public class MagnificationOneFingerPanningPreferenceController @Override public int getAvailabilityStatus() { - return AVAILABLE; + return isInSetupWizard() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE; } @Override diff --git a/src/com/android/settings/accessibility/OWNERS b/src/com/android/settings/accessibility/OWNERS index 268e46777bb..24ff9fd44ba 100644 --- a/src/com/android/settings/accessibility/OWNERS +++ b/src/com/android/settings/accessibility/OWNERS @@ -15,5 +15,5 @@ menghanli@google.com #{LAST_RESORT_SUGGESTION} cipson@google.com #{LAST_RESORT_SUGGESTION} # Partner-team files -per-file HapticFeedbackIntensityPreferenceController.java = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS -per-file *Vibration* = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS +per-file *Haptic* = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS +per-file *Vibrat* = file:platform/frameworks/base:/services/core/java/com/android/server/vibrator/OWNERS diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java index 25c8768375f..0821b87c2f0 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragment.java @@ -82,8 +82,6 @@ public class ToggleScreenMagnificationPreferenceFragment extends private static final TextUtils.SimpleStringSplitter sStringColonSplitter = new TextUtils.SimpleStringSplitter(COMPONENT_NAME_SEPARATOR); - protected TwoStatePreference mFollowingTypingSwitchPreference; - // TODO(b/147021230): Move duplicated functions with android/internal/accessibility into util. private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener; private CheckBox mSoftwareTypeCheckBox; @@ -92,10 +90,13 @@ public class ToggleScreenMagnificationPreferenceFragment extends @Nullable private CheckBox mTwoFingerTripleTapTypeCheckBox; private DialogCreatable mDialogDelegate; + private boolean mInSetupWizard; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getActivity().setTitle(R.string.accessibility_screen_magnification_title); + mInSetupWizard = WizardManagerHelper.isAnySetupWizard(getIntent()); } @Override @@ -169,7 +170,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends .showAccessibilityGestureTutorialDialog(getPrefContext()); case DialogEnums.MAGNIFICATION_EDIT_SHORTCUT: final CharSequence dialogTitle = getShortcutTitle(); - final int dialogType = WizardManagerHelper.isAnySetupWizard(getIntent()) + final int dialogType = mInSetupWizard ? DialogType.EDIT_SHORTCUT_MAGNIFICATION_SUW : DialogType.EDIT_SHORTCUT_MAGNIFICATION; mDialog = AccessibilityDialogUtils.showEditShortcutDialog(getPrefContext(), @@ -240,17 +241,18 @@ public class ToggleScreenMagnificationPreferenceFragment extends } private void addFollowTypingSetting(PreferenceCategory generalCategory) { - mFollowingTypingSwitchPreference = new SwitchPreferenceCompat(getPrefContext()); - mFollowingTypingSwitchPreference.setTitle( + var followingTypingSwitchPreference = new SwitchPreferenceCompat(getPrefContext()); + followingTypingSwitchPreference.setTitle( R.string.accessibility_screen_magnification_follow_typing_title); - mFollowingTypingSwitchPreference.setSummary( + followingTypingSwitchPreference.setSummary( R.string.accessibility_screen_magnification_follow_typing_summary); - mFollowingTypingSwitchPreference.setKey( + followingTypingSwitchPreference.setKey( MagnificationFollowTypingPreferenceController.PREF_KEY); - generalCategory.addPreference(mFollowingTypingSwitchPreference); + generalCategory.addPreference(followingTypingSwitchPreference); var followTypingPreferenceController = new MagnificationFollowTypingPreferenceController( getContext(), MagnificationFollowTypingPreferenceController.PREF_KEY); + followTypingPreferenceController.setInSetupWizard(mInSetupWizard); followTypingPreferenceController.displayPreference(getPreferenceScreen()); addPreferenceController(followTypingPreferenceController); } @@ -282,6 +284,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends var alwaysOnPreferenceController = new MagnificationAlwaysOnPreferenceController( getContext(), MagnificationAlwaysOnPreferenceController.PREF_KEY); + alwaysOnPreferenceController.setInSetupWizard(mInSetupWizard); getSettingsLifecycle().addObserver(alwaysOnPreferenceController); alwaysOnPreferenceController.displayPreference(getPreferenceScreen()); addPreferenceController(alwaysOnPreferenceController); @@ -301,6 +304,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends var oneFingerPanningPreferenceController = new MagnificationOneFingerPanningPreferenceController(getContext()); + oneFingerPanningPreferenceController.setInSetupWizard(mInSetupWizard); getSettingsLifecycle().addObserver(oneFingerPanningPreferenceController); oneFingerPanningPreferenceController.displayPreference(getPreferenceScreen()); addPreferenceController(oneFingerPanningPreferenceController); @@ -329,6 +333,7 @@ public class ToggleScreenMagnificationPreferenceFragment extends getContext(), MagnificationJoystickPreferenceController.PREF_KEY ); + joystickPreferenceController.setInSetupWizard(mInSetupWizard); joystickPreferenceController.displayPreference(getPreferenceScreen()); addPreferenceController(joystickPreferenceController); } diff --git a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java index 9182fbb565a..97405d24e9f 100644 --- a/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java +++ b/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizard.java @@ -72,10 +72,6 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizard if (mSettingsPreference != null) { mSettingsPreference.setVisible(false); } - // Setting of following typing - if (mFollowingTypingSwitchPreference != null) { - mFollowingTypingSwitchPreference.setVisible(false); - } } @Override diff --git a/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java b/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java index 81e84393d41..d40a075b058 100644 --- a/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java +++ b/src/com/android/settings/applications/specialaccess/interactacrossprofiles/InteractAcrossProfilesDetails.java @@ -28,6 +28,8 @@ import static android.app.admin.DevicePolicyResources.Strings.Settings.ONLY_CONN import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.provider.Settings.ACTION_MANAGE_CROSS_PROFILE_ACCESS; +import static android.provider.Settings.Global.CONNECTED_APPS_ALLOWED_PACKAGES; +import static android.provider.Settings.Global.CONNECTED_APPS_DISALLOWED_PACKAGES; import android.Manifest; import android.annotation.UserIdInt; @@ -35,6 +37,7 @@ import android.app.ActionBar; import android.app.AppOpsManager; import android.app.admin.DevicePolicyEventLogger; import android.app.admin.DevicePolicyManager; +import android.app.admin.flags.Flags; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; @@ -49,6 +52,7 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.stats.devicepolicy.DevicePolicyEnums; import android.util.IconDrawableFactory; import android.view.LayoutInflater; @@ -68,6 +72,10 @@ import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedSwitchPreference; import com.android.settingslib.widget.LayoutPreference; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + public class InteractAcrossProfilesDetails extends AppInfoBase implements Preference.OnPreferenceClickListener { @@ -381,6 +389,7 @@ public class InteractAcrossProfilesDetails extends AppInfoBase private void enableInteractAcrossProfiles(boolean newState) { mCrossProfileApps.setInteractAcrossProfilesAppOp( mPackageName, newState ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED); + setUserPreferenceForPackage(newState, mPackageName); } private void handleInstallBannerClick() { @@ -552,4 +561,40 @@ public class InteractAcrossProfilesDetails extends AppInfoBase } return ACTION_MANAGE_CROSS_PROFILE_ACCESS.equals(intent.getAction()); } + + private void setUserPreferenceForPackage(boolean enabled, String crossProfilePackage) { + if (!Flags.backupConnectedAppsSettings()) { + return; + } + String allowedPackagesString = Settings.Global.getString(getContentResolver(), + CONNECTED_APPS_ALLOWED_PACKAGES); + String disallowedPackagesString = Settings.Global.getString(getContentResolver(), + CONNECTED_APPS_DISALLOWED_PACKAGES); + + Set allowedPackagesSet = getSetFromString(allowedPackagesString); + Set disallowedPackagesSet = getSetFromString(disallowedPackagesString); + + if (enabled) { + allowedPackagesSet.add(crossProfilePackage); + disallowedPackagesSet.remove(crossProfilePackage); + + } else { + allowedPackagesSet.remove(crossProfilePackage); + disallowedPackagesSet.add(crossProfilePackage); + } + + Settings.Global.putString(getContentResolver(), + CONNECTED_APPS_ALLOWED_PACKAGES, + String.join(",", allowedPackagesSet)); + + Settings.Global.putString(getContentResolver(), + CONNECTED_APPS_DISALLOWED_PACKAGES, + String.join(",", disallowedPackagesSet)); + } + + private Set getSetFromString(String packages) { + return Optional.ofNullable(packages) + .map(pkg -> Set.of(pkg.split(","))) + .orElse(Collections.emptySet()); + } } diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/model/StageViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/data/model/EnrollStageModel.kt similarity index 80% rename from src/com/android/settings/biometrics/fingerprint2/lib/model/StageViewModel.kt rename to src/com/android/settings/biometrics/fingerprint2/data/model/EnrollStageModel.kt index 81bba157291..ab6ef217b0d 100644 --- a/src/com/android/settings/biometrics/fingerprint2/lib/model/StageViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/data/model/EnrollStageModel.kt @@ -14,38 +14,38 @@ * limitations under the License. */ -package com.android.settings.biometrics.fingerprint2.lib.model +package com.android.settings.biometrics.fingerprint2.data.model /** * A view model that describes the various stages of UDFPS Enrollment. This stages typically update * the enrollment UI in a major way, such as changing the lottie animation or changing the location * of the where the user should press their fingerprint */ -sealed class StageViewModel { +sealed class EnrollStageModel { /** Unknown stage */ - data object Unknown : StageViewModel() + data object Unknown : EnrollStageModel() /** This is the stage that moves the fingerprint icon around during enrollment. */ - data object Guided : StageViewModel() + data object Guided : EnrollStageModel() /** The center stage is the initial stage of enrollment. */ - data object Center : StageViewModel() + data object Center : EnrollStageModel() /** * Fingerprint stage of enrollment. Typically there is some sort of indication that a user should * be using their finger tip to enroll. */ - data object Fingertip : StageViewModel() + data object Fingertip : EnrollStageModel() /** * Left edge stage of enrollment. Typically there is an indication that a user should be using the * left edge of their fingerprint. */ - data object LeftEdge : StageViewModel() + data object LeftEdge : EnrollStageModel() /** * Right edge stage of enrollment. Typically there is an indication that a user should be using * the right edge of their fingerprint. */ - data object RightEdge : StageViewModel() + data object RightEdge : EnrollStageModel() } diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt index 2d4cb409868..e683cb84cf9 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt @@ -16,11 +16,11 @@ package com.android.settings.biometrics.fingerprint2.domain.interactor -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel +import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -typealias EnrollStageThresholds = Map +typealias EnrollStageThresholds = Map /** Interactor that provides enroll stages for enrollment. */ interface EnrollStageInteractor { @@ -33,11 +33,11 @@ class EnrollStageInteractorImpl() : EnrollStageInteractor { override val enrollStageThresholds: Flow = flowOf( mapOf( - 0.0f to StageViewModel.Center, - 0.25f to StageViewModel.Guided, - 0.5f to StageViewModel.Fingertip, - 0.75f to StageViewModel.LeftEdge, - 0.875f to StageViewModel.RightEdge, + 0.0f to EnrollStageModel.Center, + 0.25f to EnrollStageModel.Guided, + 0.5f to EnrollStageModel.Fingertip, + 0.75f to EnrollStageModel.LeftEdge, + 0.875f to EnrollStageModel.RightEdge, ) ) } diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt index 5d1d8c86e37..f9276e63ddf 100644 --- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt @@ -42,6 +42,7 @@ interface OrientationInteractor { * A flow that contains the rotation info matched against the def [config_reverseDefaultRotation] */ val rotationFromDefault: Flow + /** * A Helper function that computes rotation if device is in * [R.bool.config_reverseDefaultConfigRotation] diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UdfpsEnrollInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UdfpsEnrollInteractor.kt new file mode 100644 index 00000000000..ec09ffd0011 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UdfpsEnrollInteractor.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.biometrics.fingerprint2.domain.interactor + +import android.graphics.PointF +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.update + +/** + * This interactor provides information about the current offset of the sensor for guided enrollment + * on UDFPS devices. + */ +interface UdfpsEnrollInteractor { + /** Indicates at which step a UDFPS enrollment is in. */ + fun onEnrollmentStep(stepsRemaining: Int, totalStep: Int) + + /** Indicates if guided enrollment should be enabled or not. */ + fun updateGuidedEnrollment(enabled: Boolean) + + /** + * A flow indicating how much the sensor image drawable should be offset for guided enrollment. A + * null point indicates that the icon should be in its default position. + */ + val guidedEnrollmentOffset: Flow +} + +/** Keeps track of which guided enrollment point we should be using */ +class UdfpsEnrollInteractorImpl( + pixelsPerMillimeter: Float, + accessibilityInteractor: AccessibilityInteractor, +) : UdfpsEnrollInteractor { + + private var isGuidedEnrollment = MutableStateFlow(false) + // Number of pixels per mm + val px = pixelsPerMillimeter + private val guidedEnrollmentPoints: MutableList = + mutableListOf( + PointF(2.00f * px, 0.00f * px), + PointF(0.87f * px, -2.70f * px), + PointF(-1.80f * px, -1.31f * px), + PointF(-1.80f * px, 1.31f * px), + PointF(0.88f * px, 2.70f * px), + PointF(3.94f * px, -1.06f * px), + PointF(2.90f * px, -4.14f * px), + PointF(-0.52f * px, -5.95f * px), + PointF(-3.33f * px, -3.33f * px), + PointF(-3.99f * px, -0.35f * px), + PointF(-3.62f * px, 2.54f * px), + PointF(-1.49f * px, 5.57f * px), + PointF(2.29f * px, 4.92f * px), + PointF(3.82f * px, 1.78f * px), + ) + + override fun onEnrollmentStep(stepsRemaining: Int, totalStep: Int) { + val index = (totalStep - stepsRemaining) % guidedEnrollmentPoints.size + _guidedEnrollment.update { guidedEnrollmentPoints[index] } + } + + override fun updateGuidedEnrollment(enabled: Boolean) { + isGuidedEnrollment.update { enabled } + } + + private val _guidedEnrollment = MutableStateFlow(PointF(0f, 0f)) + override val guidedEnrollmentOffset: Flow = + combine( + _guidedEnrollment, + accessibilityInteractor.isAccessibilityEnabled, + isGuidedEnrollment, + ) { point, accessibilityEnabled, guidedEnrollmentEnabled -> + if (accessibilityEnabled || !guidedEnrollmentEnabled) { + return@combine PointF(0f, 0f) + } else { + return@combine PointF(point.x * SCALE, point.y * SCALE) + } + } + + companion object { + private const val SCALE = 0.5f + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt index c8e9ca38d85..6d353a42a62 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt @@ -24,6 +24,7 @@ import android.hardware.fingerprint.FingerprintManager import android.os.Bundle import android.os.Vibrator import android.util.Log +import android.util.TypedValue import android.view.accessibility.AccessibilityManager import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment @@ -54,6 +55,8 @@ import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateI import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractorImpl +import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractor +import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractorImpl import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractorImpl import com.android.settings.biometrics.fingerprint2.lib.model.Default @@ -89,6 +92,7 @@ import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Fing import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.GatekeeperInfo +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Transition import com.android.settings.flags.Flags import com.android.settings.password.ChooseLockGeneric import com.android.settings.password.ChooseLockSettingsHelper @@ -116,6 +120,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { private lateinit var foldStateInteractor: FoldStateInteractor private lateinit var orientationInteractor: OrientationInteractor private lateinit var displayDensityInteractor: DisplayDensityInteractor + private lateinit var udfpsEnrollInteractor: UdfpsEnrollInteractor private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel private lateinit var backgroundViewModel: BackgroundViewModel private lateinit var fingerprintFlowViewModel: FingerprintFlowViewModel @@ -256,6 +261,15 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { fingerprintManager, Settings, ) + val accessibilityInteractor = + AccessibilityInteractorImpl( + getSystemService(AccessibilityManager::class.java)!!, + lifecycleScope, + ) + + val pixelsPerMillimeter = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1f, context.resources.displayMetrics) + udfpsEnrollInteractor = UdfpsEnrollInteractorImpl(pixelsPerMillimeter, accessibilityInteractor) val fingerprintManagerInteractor = FingerprintManagerInteractorImpl( @@ -273,12 +287,6 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { val hasConfirmedDeviceCredential = gatekeeperInfo is GatekeeperInfo.GatekeeperPasswordInfo - val accessibilityInteractor = - AccessibilityInteractorImpl( - getSystemService(AccessibilityManager::class.java)!!, - lifecycleScope, - ) - navigationViewModel = ViewModelProvider( this, @@ -384,6 +392,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { orientationInteractor, backgroundViewModel, fingerprintSensorRepo, + udfpsEnrollInteractor, ), )[UdfpsViewModel::class.java] @@ -435,17 +444,17 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { else -> FingerprintEnrollEnrollingV2Fragment() } } - Introduction -> FingerprintEnrollIntroV2Fragment() + is Introduction -> FingerprintEnrollIntroV2Fragment() else -> null } if (theClass != null) { - supportFragmentManager.fragments.onEach { fragment -> - supportFragmentManager.beginTransaction().remove(fragment).commit() - } - supportFragmentManager .beginTransaction() + .setCustomAnimations( + step.enterTransition.toAnimation(), + step.exitTransition.toAnimation(), + ) .setReorderingAllowed(true) .add(R.id.fragment_container_view, theClass::class.java, null) .commit() @@ -512,3 +521,12 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { } } } + +private fun Transition.toAnimation(): Int { + return when (this) { + Transition.EnterFromLeft -> com.google.android.setupdesign.R.anim.sud_slide_back_in + Transition.EnterFromRight -> com.google.android.setupdesign.R.anim.sud_slide_next_in + Transition.ExitToLeft -> com.google.android.setupdesign.R.anim.sud_slide_next_out + Transition.ExitToRight -> com.google.android.setupdesign.R.anim.sud_slide_back_out + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt index c96a1b45302..a2e52329dd8 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt @@ -32,12 +32,12 @@ import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieCompositionFactory import com.android.settings.R +import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.widget.FingerprintErrorDialog -import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.DescriptionText +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.DescriptionText +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.HeaderText import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.EducationAnimationModel -import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.HeaderText import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget.UdfpsEnrollViewV2 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep @@ -83,6 +83,8 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro window.statusBarColor = color view.setBackgroundColor(color) + udfpsEnrollView.setFinishAnimationCompleted { viewModel.finishedSuccessfully() } + viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { launch { @@ -159,7 +161,14 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro } viewLifecycleOwner.lifecycleScope.launch { - viewModel.enrollStage.collect { udfpsEnrollView.updateStage(it) } + viewModel.guidedEnrollment.collect { + glifLayout.post { udfpsEnrollView.updateGuidedEnrollment(it) } + } + } + viewLifecycleOwner.lifecycleScope.launch { + viewModel.guidedEnrollmentSaved.collect { + glifLayout.post { udfpsEnrollView.onGuidedPointSaved(it) } + } } } } @@ -175,35 +184,35 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro } private fun HeaderText.toResource(): Int { - return when (this.stageViewModel) { - StageViewModel.Center, - StageViewModel.Guided, - StageViewModel.Fingertip, - StageViewModel.Unknown -> R.string.security_settings_udfps_enroll_fingertip_title - StageViewModel.LeftEdge -> R.string.security_settings_udfps_enroll_left_edge_title - StageViewModel.RightEdge -> R.string.security_settings_udfps_enroll_right_edge_title + return when (this.enrollStageModel) { + EnrollStageModel.Center, + EnrollStageModel.Guided, + EnrollStageModel.Fingertip, + EnrollStageModel.Unknown -> R.string.security_settings_udfps_enroll_fingertip_title + EnrollStageModel.LeftEdge -> R.string.security_settings_udfps_enroll_left_edge_title + EnrollStageModel.RightEdge -> R.string.security_settings_udfps_enroll_right_edge_title } } private fun DescriptionText.toResource(): Int? { - return when (this.stageViewModel) { - StageViewModel.Center, - StageViewModel.Guided, - StageViewModel.Fingertip, - StageViewModel.LeftEdge, - StageViewModel.RightEdge -> null - StageViewModel.Unknown -> R.string.security_settings_udfps_enroll_start_message + return when (this.enrollStageModel) { + EnrollStageModel.Center, + EnrollStageModel.Guided, + EnrollStageModel.Fingertip, + EnrollStageModel.LeftEdge, + EnrollStageModel.RightEdge -> null + EnrollStageModel.Unknown -> R.string.security_settings_udfps_enroll_start_message } } private fun EducationAnimationModel.toResource(): Int? { - return when (this.stageViewModel) { - StageViewModel.Center, - StageViewModel.Guided -> R.raw.udfps_center_hint_lottie - StageViewModel.Fingertip -> R.raw.udfps_tip_hint_lottie - StageViewModel.LeftEdge -> R.raw.udfps_left_edge_hint_lottie - StageViewModel.RightEdge -> R.raw.udfps_right_edge_hint_lottie - StageViewModel.Unknown -> null + return when (this.enrollStageModel) { + EnrollStageModel.Center, + EnrollStageModel.Guided -> R.raw.udfps_center_hint_lottie + EnrollStageModel.Fingertip -> R.raw.udfps_tip_hint_lottie + EnrollStageModel.LeftEdge -> R.raw.udfps_left_edge_hint_lottie + EnrollStageModel.RightEdge -> R.raw.udfps_right_edge_hint_lottie + EnrollStageModel.Unknown -> null } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/DescriptionText.kt similarity index 84% rename from src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt rename to src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/DescriptionText.kt index 175fea0ac13..a949545ec19 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/DescriptionText.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel +package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel +import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel /** Represents the description text for UDFPS enrollment */ data class DescriptionText( val isSuw: Boolean, val isAccessibility: Boolean, - val stageViewModel: StageViewModel, + val enrollStageModel: EnrollStageModel, ) diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/HeaderText.kt similarity index 83% rename from src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt rename to src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/HeaderText.kt index c565f35487c..9e45107537f 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/HeaderText.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel +package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel +import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel /** Represents the header text for UDFPS enrollment */ data class HeaderText( val isSuw: Boolean, val isAccessibility: Boolean, - val stageViewModel: StageViewModel, + val enrollStageModel: EnrollStageModel, ) diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt index a274179e303..64f345f13a5 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt @@ -16,11 +16,11 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel +import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel /** Represents the lottie for UDFPS enrollment */ data class EducationAnimationModel( val isSuw: Boolean, val isAccessibility: Boolean, - val stageViewModel: StageViewModel, + val enrollStageModel: EnrollStageModel, ) diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt index 37822378f21..a22f680b641 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt @@ -17,20 +17,24 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel import android.graphics.Point +import android.graphics.PointF import android.view.Surface import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository +import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel import com.android.settings.biometrics.fingerprint2.data.repository.SimulatedTouchEventsRepository import com.android.settings.biometrics.fingerprint2.domain.interactor.DebuggingInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintVibrationEffects import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor +import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractor import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.DescriptionText +import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.HeaderText import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintAction import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel @@ -61,9 +65,11 @@ class UdfpsViewModel( orientationInteractor: OrientationInteractor, backgroundViewModel: BackgroundViewModel, sensorRepository: FingerprintSensorRepository, + udfpsEnrollInteractor: UdfpsEnrollInteractor, ) : ViewModel() { private val isSetupWizard = flowOf(false) + private var shouldResetErollment = false private var _enrollState: Flow = fingerprintEnrollEnrollingViewModel.enrollFlow @@ -112,6 +118,17 @@ class UdfpsViewModel( } } + /** + * This indicates at which point the UI should offset the fingerprint sensor icon for guided + * enrollment. + */ + val guidedEnrollment: Flow = + udfpsEnrollInteractor.guidedEnrollmentOffset.distinctUntilChanged() + + /** The saved version of [guidedEnrollment] */ + val guidedEnrollmentSaved: Flow = + guidedEnrollment.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1) + /** * This is the saved progress, this is for when views are recreated and need saved state for the * first time. @@ -132,13 +149,13 @@ class UdfpsViewModel( } } - /** Determines the current [StageViewModel] enrollment is in */ - val enrollStage: Flow = + /** Determines the current [EnrollStageModel] enrollment is in */ + private val enrollStage: Flow = combine(enrollStageInteractor.enrollStageThresholds, enrollState) { thresholds, event -> if (event is FingerEnrollState.EnrollProgress) { val progress = (event.totalStepsRequired - event.remainingSteps).toFloat() / event.totalStepsRequired - var stageToReturn: StageViewModel = StageViewModel.Center + var stageToReturn: EnrollStageModel = EnrollStageModel.Center thresholds.forEach { (threshold, stage) -> if (progress < threshold) { return@forEach @@ -153,6 +170,40 @@ class UdfpsViewModel( .filterNotNull() .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1) + init { + viewModelScope.launch { + enrollState + .combine(accessibilityEnabled) { event, isEnabled -> Pair(event, isEnabled) } + .collect { + if ( + when (it.first) { + is FingerEnrollState.EnrollError -> true + is FingerEnrollState.EnrollHelp -> it.second + is FingerEnrollState.EnrollProgress -> true + else -> false + } + ) { + vibrate(it.first) + } + } + } + viewModelScope.launch { + enrollStage.collect { + udfpsEnrollInteractor.updateGuidedEnrollment(it is EnrollStageModel.Guided) + } + } + + viewModelScope.launch { + enrollState.filterIsInstance().collect { + udfpsEnrollInteractor.onEnrollmentStep(it.remainingSteps, it.totalStepsRequired) + } + } + + viewModelScope.launch { + backgroundViewModel.background.filter { true }.collect { didGoToBackground() } + } + } + /** Indicates if we should show the lottie. */ val shouldShowLottie: Flow = combine( @@ -183,7 +234,7 @@ class UdfpsViewModel( } .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1) - private val shouldClearDescriptionText = enrollStage.map { it is StageViewModel.Unknown } + private val shouldClearDescriptionText = enrollStage.map { it is EnrollStageModel.Unknown } /** The description text for UDFPS enrollment */ val descriptionText: Flow = @@ -202,6 +253,10 @@ class UdfpsViewModel( /** Indicates if the consumer is ready for enrollment */ fun readyForEnrollment() { + if (shouldResetErollment) { + shouldResetErollment = false + _enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow + } fingerprintEnrollEnrollingViewModel.canEnroll() } @@ -237,8 +292,12 @@ class UdfpsViewModel( } private fun doReset() { - /** Indicates if the icon should be animating or not */ _enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow + progressSaved = + enrollState + .filterIsInstance() + .filterNotNull() + .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1) } /** The lottie that should be shown for UDFPS Enrollment */ @@ -272,6 +331,7 @@ class UdfpsViewModel( private val orientationInteractor: OrientationInteractor, private val backgroundViewModel: BackgroundViewModel, private val sensorRepository: FingerprintSensorRepository, + private val udfpsEnrollInteractor: UdfpsEnrollInteractor, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") @@ -287,6 +347,7 @@ class UdfpsViewModel( orientationInteractor, backgroundViewModel, sensorRepository, + udfpsEnrollInteractor, ) as T } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt deleted file mode 100644 index 141924161d1..00000000000 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget - -import android.content.Context -import android.graphics.PointF -import android.util.TypedValue -import android.view.accessibility.AccessibilityManager -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel - -/** Keeps track of which guided enrollment point we should be using */ -class UdfpsEnrollHelperV2(private val mContext: Context) { - - private var isGuidedEnrollment: Boolean = false - private val accessibilityEnabled: Boolean - private val guidedEnrollmentPoints: MutableList - /** The current index of [guidedEnrollmentPoints] for the guided enrollment. */ - private var index = 0 - - init { - val am = mContext.getSystemService(AccessibilityManager::class.java) - accessibilityEnabled = am!!.isEnabled - guidedEnrollmentPoints = ArrayList() - - // Number of pixels per mm - val px = - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1f, mContext.resources.displayMetrics) - guidedEnrollmentPoints.add(PointF(2.00f * px, 0.00f * px)) - guidedEnrollmentPoints.add(PointF(0.87f * px, -2.70f * px)) - guidedEnrollmentPoints.add(PointF(-1.80f * px, -1.31f * px)) - guidedEnrollmentPoints.add(PointF(-1.80f * px, 1.31f * px)) - guidedEnrollmentPoints.add(PointF(0.88f * px, 2.70f * px)) - guidedEnrollmentPoints.add(PointF(3.94f * px, -1.06f * px)) - guidedEnrollmentPoints.add(PointF(2.90f * px, -4.14f * px)) - guidedEnrollmentPoints.add(PointF(-0.52f * px, -5.95f * px)) - guidedEnrollmentPoints.add(PointF(-3.33f * px, -3.33f * px)) - guidedEnrollmentPoints.add(PointF(-3.99f * px, -0.35f * px)) - guidedEnrollmentPoints.add(PointF(-3.62f * px, 2.54f * px)) - guidedEnrollmentPoints.add(PointF(-1.49f * px, 5.57f * px)) - guidedEnrollmentPoints.add(PointF(2.29f * px, 4.92f * px)) - guidedEnrollmentPoints.add(PointF(3.82f * px, 1.78f * px)) - } - - /** - * This indicates whether we should be offsetting the enrollment icon based on - * [guidedEnrollmentPoints] - */ - fun onUpdateStage(stage: StageViewModel) { - this.isGuidedEnrollment = stage is StageViewModel.Guided - } - - /** Updates [index] to be used by [guidedEnrollmentPoints] */ - fun onEnrollmentProgress(remaining: Int, totalSteps: Int) { - index = totalSteps - remaining - } - - /** - * Returns the current guided enrollment point, or (0,0) if we are not in guided enrollment or are - * in accessibility. - */ - val guidedEnrollmentLocation: PointF? - get() { - if (accessibilityEnabled || !isGuidedEnrollment) { - return null - } - val scale = SCALE - val originalPoint = guidedEnrollmentPoints[index % guidedEnrollmentPoints.size] - return PointF(originalPoint.x * scale, originalPoint.y * scale) - } - - companion object { - private const val TAG = "UdfpsEnrollHelperV2" - private const val SCALE = 0.5f - } -} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt index 0d489954b80..c209c55cb76 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt @@ -24,6 +24,7 @@ import android.graphics.Canvas import android.graphics.ColorFilter import android.graphics.Paint import android.graphics.PixelFormat +import android.graphics.PointF import android.graphics.Rect import android.graphics.RectF import android.graphics.drawable.Drawable @@ -37,7 +38,6 @@ import androidx.core.animation.addListener import androidx.core.graphics.toRect import androidx.core.graphics.toRectF import com.android.settings.R -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel import kotlin.math.sin /** @@ -51,11 +51,11 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS private val fingerprintDrawable: ShapeDrawable private val sensorOutlinePaint: Paint private val blueFill: Paint - private val helper = UdfpsEnrollHelperV2(context) @ColorInt private var enrollIconColor = 0 @ColorInt private var movingTargetFill = 0 private var currentScale = 1.0f private var alpha = 0 + private var guidedEnrollmentOffset: PointF? = null /** * This is the physical location of the sensor. This rect will be updated by [drawSensorRectAt] @@ -143,45 +143,6 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS invalidateSelf() } - /** Update the progress of the icon */ - fun onEnrollmentProgress(remaining: Int, totalSteps: Int, isRecreating: Boolean = false) { - restoreAnimationTime() - // If we are restoring this view from a saved state, set animation duration to 0 to avoid - // animating progress that has already occurred. - if (isRecreating) { - setAnimationTimeToZero() - } else { - restoreAnimationTime() - } - - helper.onEnrollmentProgress(remaining, totalSteps) - val offset = helper.guidedEnrollmentLocation - val currentBounds = getCurrLocation().toRect() - if (offset != null) { - // This is the desired location of the sensor rect, the [EnrollHelper] - // offsets the initial sensor rect by a bit to get the user to move their finger a bit more. - val targetRect = Rect(sensorRectBounds).toRectF() - targetRect.offset(offset.x, offset.y) - val shouldAnimateMovement = - !currentBounds.equals(targetRect) && offset.x != 0f && offset.y != 0f - if (shouldAnimateMovement) { - targetAnimatorSet?.cancel() - animateMovement(currentBounds, targetRect, true) - } - } else { - // If we are not offsetting the sensor, move it back to its original place - animateMovement(currentBounds, sensorRectBounds.toRectF(), false) - } - - invalidateSelf() - } - - /** Update the stage of the icon */ - fun updateStage(it: StageViewModel) { - helper.onUpdateStage(it) - invalidateSelf() - } - /** Stop drawing the fingerprint icon. */ fun stopDrawing() { alpha = 0 @@ -211,6 +172,7 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS if (currentBounds.equals(offsetRect)) { return } + val xAnimator = ValueAnimator.ofFloat(currentBounds.left.toFloat(), offsetRect.left) xAnimator.addUpdateListener { currX = it.animatedValue as Float @@ -260,6 +222,40 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS targetAnimationDuration = TARGET_ANIM_DURATION_LONG } + /** + * Indicates a change to guided enrollment has occurred. Also indicates if we are recreating the + * view, in which case their is no need to animate the icon to whatever position it was in. + */ + fun updateGuidedEnrollment(point: PointF, isRecreating: Boolean) { + guidedEnrollmentOffset = point + if (isRecreating) { + setAnimationTimeToZero() + } else { + restoreAnimationTime() + } + + val currentBounds = getCurrLocation().toRect() + val offset = guidedEnrollmentOffset + if (offset?.x != 0f && offset?.y != 0f) { + val targetRect = Rect(sensorRectBounds).toRectF() + // This is the desired location of the sensor rect, the [EnrollHelper] + // offsets the initial sensor rect by a bit to get the user to move their finger a bit more. + targetRect.offset(offset!!.x, offset!!.y) + val shouldAnimateMovement = !currentBounds.equals(targetRect) + if (shouldAnimateMovement) { + targetAnimatorSet?.cancel() + animateMovement(currentBounds, targetRect, true) + } else { + // If we are not offsetting the sensor, move it back to its original place + animateMovement(currentBounds, sensorRectBounds.toRectF(), false) + } + } else { + // If we are not offsetting the sensor, move it back to its original place + animateMovement(currentBounds, sensorRectBounds.toRectF(), false) + } + invalidateSelf() + } + companion object { private const val TAG = "UdfpsEnrollDrawableV2" private const val DEFAULT_STROKE_WIDTH = 3f diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt index 8f0e84564a6..bf2f0261112 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt @@ -27,10 +27,12 @@ import android.graphics.Rect import android.graphics.drawable.Drawable import android.util.AttributeSet import android.util.DisplayMetrics +import android.util.Log import android.view.animation.DecelerateInterpolator import android.view.animation.Interpolator import android.view.animation.OvershootInterpolator import androidx.annotation.ColorInt +import androidx.core.animation.addListener import androidx.core.animation.doOnEnd import androidx.core.graphics.toRectF import com.android.internal.annotations.VisibleForTesting @@ -46,6 +48,7 @@ import kotlin.math.sin class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: AttributeSet?) : Drawable() { private val sensorRect: Rect = Rect() + private var onFinishedCompletionAnimation: (() -> Unit)? = null private var rotation: Int = 0 private val strokeWidthPx: Float @@ -287,6 +290,12 @@ class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: Attr checkMarkDrawable.bounds = newBounds checkMarkDrawable.setVisible(true, false) } + doOnEnd { + onFinishedCompletionAnimation?.let{ + it() + } + + } start() } } @@ -380,6 +389,13 @@ class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: Attr checkmarkAnimationDuration = CHECKMARK_ANIMATION_DURATION_MS } + /** + * Indicates that the finish animation has completed, and enrollment can proceed to the next stage + */ + fun setFinishAnimationCompleted(onFinishedAnimation: () -> Unit) { + this.onFinishedCompletionAnimation = onFinishedAnimation + } + companion object { private const val TAG = "UdfpsProgressBar" private const val FILL_COLOR_ANIMATION_DURATION_MS = 350L diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt index b355f7735d7..586408f0c28 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt @@ -18,6 +18,7 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrol import android.content.Context import android.graphics.Point +import android.graphics.PointF import android.graphics.Rect import android.util.AttributeSet import android.util.Log @@ -31,7 +32,6 @@ import android.widget.FrameLayout import android.widget.ImageView import com.android.settings.R import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState -import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel import com.android.systemui.biometrics.UdfpsUtils import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams @@ -53,6 +53,13 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co private val udfpsUtils: UdfpsUtils = UdfpsUtils() private lateinit var touchExplorationAnnouncer: TouchExplorationAnnouncer private var isRecreating = false + private var onFinishedCompletionAnimation: (() -> Unit)? = null + + init { + fingerprintProgressDrawable.setFinishAnimationCompleted { + onFinishedCompletionAnimation?.let { it() } + } + } /** * This function computes the center (x,y) location with respect to the parent [FrameLayout] for @@ -112,11 +119,6 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co touchExplorationAnnouncer = TouchExplorationAnnouncer(context, this, overlayParams, udfpsUtils) } - /** Updates the current enrollment stage. */ - fun updateStage(it: StageViewModel) { - fingerprintIcon.updateStage(it) - } - /** Receive enroll progress event */ fun onUdfpsEvent(event: FingerEnrollState) { when (event) { @@ -174,7 +176,6 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co /** Receive enroll progress event */ private fun onEnrollmentProgress(remaining: Int, totalSteps: Int) { - fingerprintIcon.onEnrollmentProgress(remaining, totalSteps) fingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps) } @@ -241,10 +242,25 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co /** Indicates we should should restore the views saved state. */ fun onEnrollProgressSaved(it: FingerEnrollState.EnrollProgress) { - fingerprintIcon.onEnrollmentProgress(it.remainingSteps, it.totalStepsRequired, true) fingerprintProgressDrawable.onEnrollmentProgress(it.remainingSteps, it.totalStepsRequired, true) } + /** Indicates we are recreating the UI from a saved state. */ + fun onGuidedPointSaved(it: PointF) { + fingerprintIcon.updateGuidedEnrollment(it, true) + } + + /** + * Indicates that the finish animation has completed, and enrollment can proceed to the next stage + */ + fun setFinishAnimationCompleted(onFinishedAnimation: () -> Unit) { + this.onFinishedCompletionAnimation = onFinishedAnimation + } + + fun updateGuidedEnrollment(point: PointF) { + fingerprintIcon.updateGuidedEnrollment(point, false) + } + companion object { private const val TAG = "UdfpsEnrollView" } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintNavigationStep.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintNavigationStep.kt index 76b4895f2bb..ecb330ea62b 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintNavigationStep.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintNavigationStep.kt @@ -88,7 +88,10 @@ sealed interface FingerprintNavigationStep { } /** UiSteps should have a 1 to 1 mapping between each screen of FingerprintEnrollment */ - sealed class UiStep : FingerprintNavigationStep + sealed class UiStep( + val enterTransition: Transition = Transition.EnterFromRight, + val exitTransition: Transition = Transition.ExitToLeft, + ) : FingerprintNavigationStep /** This is the landing page for enrollment, where no content is shown. */ data object Init : UiStep() { @@ -103,7 +106,7 @@ sealed interface FingerprintNavigationStep { } else if (state.flowType is FastEnroll) { TransitionStep(Enrollment(state.fingerprintSensor!!)) } else { - TransitionStep(Introduction) + TransitionStep(Introduction()) } } else -> null @@ -118,7 +121,7 @@ sealed interface FingerprintNavigationStep { action: FingerprintAction, ): FingerprintNavigationStep? { return when (action) { - FingerprintAction.CONFIRM_DEVICE_SUCCESS -> TransitionStep(Introduction) + FingerprintAction.CONFIRM_DEVICE_SUCCESS -> TransitionStep(Introduction()) FingerprintAction.CONFIRM_DEVICE_FAIL -> Finish(null) else -> null } @@ -126,7 +129,10 @@ sealed interface FingerprintNavigationStep { } /** Indicates the FingerprintIntroduction screen is being presented to the user */ - data object Introduction : UiStep() { + class Introduction( + enterTransition: Transition = Transition.EnterFromRight, + exitTransition: Transition = Transition.ExitToLeft, + ) : UiStep(enterTransition, exitTransition) { override fun update( state: NavigationState, action: FingerprintAction, @@ -141,7 +147,11 @@ sealed interface FingerprintNavigationStep { } /** Indicates the FingerprintEducation screen is being presented to the user */ - data class Education(val sensor: FingerprintSensor) : UiStep() { + class Education( + val sensor: FingerprintSensor, + enterTransition: Transition = Transition.EnterFromRight, + exitTransition: Transition = Transition.ExitToLeft, + ) : UiStep(enterTransition, exitTransition) { override fun update( state: NavigationState, action: FingerprintAction, @@ -149,7 +159,8 @@ sealed interface FingerprintNavigationStep { return when (action) { FingerprintAction.NEXT -> TransitionStep(Enrollment(state.fingerprintSensor!!)) FingerprintAction.NEGATIVE_BUTTON_PRESSED, - FingerprintAction.PREV -> TransitionStep(Introduction) + FingerprintAction.PREV -> + TransitionStep(Introduction(Transition.EnterFromLeft, Transition.ExitToRight)) else -> null } } @@ -179,7 +190,10 @@ sealed interface FingerprintNavigationStep { ): FingerprintNavigationStep? { return when (action) { FingerprintAction.NEXT -> Finish(null) - FingerprintAction.PREV -> TransitionStep(Education(state.fingerprintSensor!!)) + FingerprintAction.PREV -> + TransitionStep( + Education(state.fingerprintSensor!!, Transition.EnterFromLeft, Transition.ExitToRight) + ) FingerprintAction.ADD_ANOTHER -> TransitionStep(Enrollment(state.fingerprintSensor!!)) else -> null } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/TransitionViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/TransitionViewModel.kt new file mode 100644 index 00000000000..8fb72916cd7 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/TransitionViewModel.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel + +/** Indicates the type of transitions that can occur between fragments */ +sealed class Transition { + /** + * Indicates the new fragment should slide in from the left side + */ + data object EnterFromLeft : Transition() + + /** + * Indicates the new fragment should slide in from the right side + */ + data object EnterFromRight : Transition() + + /** + * Indicates the old fragment should slide out to the left side + */ + data object ExitToLeft : Transition() + + /** + * Indicates the old fragment should slide out to the right side + */ + data object ExitToRight : Transition() +} + diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java index 8e2795811ce..f29405dd698 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java @@ -54,9 +54,6 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (!AudioSharingUtils.isFeatureEnabled()) { - return; - } setShowsDialog(true); mActivity = getActivity(); if (mActivity == null) { @@ -84,6 +81,9 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { + if (!AudioSharingUtils.isFeatureEnabled()) { + return getUnsupporteDialog(); + } if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) { CachedBluetoothDevice connectedLeDevice = AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected( @@ -137,6 +137,21 @@ public class AudioStreamConfirmDialog extends InstrumentedDialogFragment { .build(); } + private Dialog getUnsupporteDialog() { + return new AudioStreamsDialogFragment.DialogBuilder(getActivity()) + .setTitle(getString(R.string.audio_streams_dialog_cannot_listen)) + .setSubTitle2(getString(R.string.audio_streams_dialog_unsupported_device_subtitle)) + .setRightButtonText(getString(R.string.audio_streams_dialog_close)) + .setRightButtonOnClickListener( + unused -> { + dismiss(); + if (mActivity != null) { + mActivity.finish(); + } + }) + .build(); + } + private Dialog getErrorDialog(String name) { return new AudioStreamsDialogFragment.DialogBuilder(getActivity()) .setTitle(getString(R.string.audio_streams_dialog_cannot_listen)) diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java index 695ad939d8c..ddb0b425d71 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java @@ -19,16 +19,12 @@ package com.android.settings.connecteddevice.audiosharing.audiostreams; import android.os.Bundle; import com.android.settings.SettingsActivity; -import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils; public class AudioStreamConfirmDialogActivity extends SettingsActivity { @Override protected void onCreate(Bundle savedState) { super.onCreate(savedState); - if (!AudioSharingUtils.isFeatureEnabled()) { - finish(); - } } @Override diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerGaugePreference.java b/src/com/android/settings/fuelgauge/batteryusage/PowerGaugePreference.java index ba2f89e3b48..1fc9abd4c5e 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/PowerGaugePreference.java +++ b/src/com/android/settings/fuelgauge/batteryusage/PowerGaugePreference.java @@ -44,6 +44,8 @@ public class PowerGaugePreference extends AppPreference { private static final float UNSELECTABLE_ALPHA_LIGHT_MODE = 0.65f; private static final float UNSELECTABLE_ALPHA_DARK_MODE = 0.65f; + private final int mTitleColorNormal; + private BatteryEntry mInfo; private BatteryDiffEntry mBatteryDiffEntry; private CharSequence mContentDescription; @@ -78,6 +80,8 @@ public class PowerGaugePreference extends AppPreference { mInfo = info; mContentDescription = contentDescription; mShowAnomalyIcon = false; + mTitleColorNormal = + Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary); } /** Sets the content description. */ @@ -155,6 +159,13 @@ public class PowerGaugePreference extends AppPreference { final TextView titleView = (TextView) view.findViewById(android.R.id.title); titleView.setContentDescription(mContentDescription); } + + if (!isSelectable()) { + // Set colors consistently to meet contrast requirements for non-selectable items + ((TextView) view.findViewById(android.R.id.title)).setTextColor(mTitleColorNormal); + ((TextView) view.findViewById(android.R.id.summary)).setTextColor(mTitleColorNormal); + subtitle.setTextColor(mTitleColorNormal); + } } private static void setViewAlpha(View view, float alpha) { diff --git a/src/com/android/settings/notification/modes/ZenModeCallsFragment.java b/src/com/android/settings/notification/modes/ZenModeCallsFragment.java new file mode 100644 index 00000000000..4c85bf56727 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeCallsFragment.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +/** + * DND Calls Settings page to determine which priority senders can bypass DND when this mode is + * activated. + */ +public class ZenModeCallsFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List controllers = new ArrayList<>(); + controllers.add(new ZenModePrioritySendersPreferenceController(context, + "zen_mode_settings_category_calls", false, mBackend)); + controllers.add(new ZenModeRepeatCallersPreferenceController(context, + "zen_mode_repeat_callers", mBackend, + context.getResources().getInteger(com.android.internal.R.integer + .config_zen_repeat_callers_threshold))); + return controllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_calls_settings; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.DND_CALLS; + } + + @Override + public void onResume() { + super.onResume(); + use(ZenModePrioritySendersPreferenceController.class).onResume(); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java new file mode 100644 index 00000000000..1d1d7505944 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; +import androidx.preference.Preference; +import com.android.settings.core.SubSettingLauncher; + +public class ZenModeCallsLinkPreferenceController extends AbstractZenModePreferenceController { + + private ZenModeSummaryHelper mSummaryHelper; + + public ZenModeCallsLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + mSummaryHelper = new ZenModeSummaryHelper(context, backend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, getMode().getId()); + // TODO(b/332937635): Update metrics category + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModeCallsFragment.class.getName()) + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + preference.setSummary(mSummaryHelper.getCallsSettingSummary(getMode())); + } +} \ No newline at end of file diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index d2126810358..51772f00a45 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -20,9 +20,6 @@ import android.app.AutomaticZenRule; import android.app.settings.SettingsEnums; import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; - import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; @@ -42,6 +39,10 @@ public class ZenModeFragment extends ZenModeFragmentBase { // {@link AbstractZenModePreferenceController}. List prefControllers = new ArrayList<>(); prefControllers.add(new ZenModeHeaderController(context, "header", this, mBackend)); + prefControllers.add(new ZenModePeopleLinkPreferenceController( + context, "zen_mode_people", mBackend)); + prefControllers.add(new ZenModeOtherLinkPreferenceController( + context, "zen_other_settings", mBackend)); return prefControllers; } diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java b/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java new file mode 100644 index 00000000000..c86f8dd4750 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +/** + * DND Messages Settings page to determine which priority senders can bypass DND. + * "Messages" include SMS, MMS, and messaging apps. + */ +public class ZenModeMessagesFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List controllers = new ArrayList<>(); + controllers.add(new ZenModePrioritySendersPreferenceController(context, + "zen_mode_settings_category_messages", true, mBackend)); + return controllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_messages_settings; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.DND_MESSAGES; + } + + @Override + public void onResume() { + super.onResume(); + use(ZenModePrioritySendersPreferenceController.class).onResume(); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java new file mode 100644 index 00000000000..8261008aaf4 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; +import androidx.preference.Preference; +import com.android.settings.core.SubSettingLauncher; + +public class ZenModeMessagesLinkPreferenceController extends AbstractZenModePreferenceController { + private final ZenModeSummaryHelper mSummaryHelper; + + public ZenModeMessagesLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + mSummaryHelper = new ZenModeSummaryHelper(context, backend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, getMode().getId()); + // TODO(b/332937635): Update metrics category + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModeMessagesFragment.class.getName()) + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + + preference.setEnabled(true); + preference.setSummary(mSummaryHelper.getMessagesSettingSummary(getMode().getPolicy())); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeOtherFragment.java b/src/com/android/settings/notification/modes/ZenModeOtherFragment.java new file mode 100644 index 00000000000..1149cd1312f --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeOtherFragment.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +/** + * Mode > Alarms & Other Interruptions + */ +public class ZenModeOtherFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List controllers = new ArrayList<>(); + controllers.add(new ZenModeOtherPreferenceController( + context, "modes_category_alarm", mBackend)); + controllers.add(new ZenModeOtherPreferenceController( + context, "modes_category_media", mBackend)); + controllers.add(new ZenModeOtherPreferenceController( + context, "modes_category_system", mBackend)); + controllers.add(new ZenModeOtherPreferenceController( + context, "modes_category_reminders", mBackend)); + controllers.add(new ZenModeOtherPreferenceController( + context, "modes_category_events", mBackend)); + return controllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_other_settings; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.NOTIFICATION_ZEN_MODE_PRIORITY; + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java new file mode 100644 index 00000000000..a43f8b056e1 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + + +import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; +import androidx.preference.Preference; +import com.android.settings.core.SubSettingLauncher; + +/** + * Preference with a link and summary about what other sounds can break through the mode + */ +public class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceController { + + ZenModeSummaryHelper mSummaryHelper; + + public ZenModeOtherLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, getMode().getId()); + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModeOtherFragment.class.getName()) + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(getMode())); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java new file mode 100644 index 00000000000..e31fa0fa08b --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM; + +import android.content.Context; +import android.service.notification.ZenPolicy; +import androidx.preference.Preference; +import androidx.preference.TwoStatePreference; + +public class ZenModeOtherPreferenceController extends AbstractZenModePreferenceController + implements Preference.OnPreferenceChangeListener { + + public ZenModeOtherPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + TwoStatePreference pref = (TwoStatePreference) preference; + pref.setChecked(getMode().getPolicy().isCategoryAllowed(getCategory(), true)); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean allow = (Boolean) newValue; + + ZenPolicy diffPolicy = new ZenPolicy.Builder() + .allowCategory(getCategory(), allow) + .build(); + getMode().setPolicy(diffPolicy); + mBackend.updateMode(getMode()); + + return true; + } + + private int getCategory() { + switch (getPreferenceKey()) { + case "modes_category_alarm": + return PRIORITY_CATEGORY_ALARMS; + case "modes_category_media": + return PRIORITY_CATEGORY_MEDIA; + case "modes_category_system": + return PRIORITY_CATEGORY_SYSTEM; + case "modes_category_reminders": + return PRIORITY_CATEGORY_REMINDERS; + case "modes_category_events": + return PRIORITY_CATEGORY_EVENTS; + } + return -1; + } +} diff --git a/src/com/android/settings/notification/modes/ZenModePeopleFragment.java b/src/com/android/settings/notification/modes/ZenModePeopleFragment.java new file mode 100644 index 00000000000..e1f753cef05 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModePeopleFragment.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +/** + * Settings page that shows what calls and messages will break through the mode and links to the + * configuration pages for both. + */ +public class ZenModePeopleFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List prefControllers = new ArrayList<>(); + prefControllers.add(new ZenModeCallsLinkPreferenceController( + context, "zen_mode_people_calls", mBackend)); + prefControllers.add(new ZenModeMessagesLinkPreferenceController( + context, "zen_mode_people_messages", mBackend)); + return prefControllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_people_settings; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.DND_PEOPLE; + } +} diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java new file mode 100644 index 00000000000..f12200627ef --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + + +import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; +import androidx.preference.Preference; +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; + +/** + * Preference with a link and summary about what calls and messages can break through the mode + */ +public class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceController { + + ZenModeSummaryHelper mSummaryHelper; + + public ZenModePeopleLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, getMode().getId()); + // TODO(b/332937635): Update metrics category + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModePeopleFragment.class.getName()) + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + preference.setSummary(mSummaryHelper.getPeopleSummary(getMode())); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java new file mode 100644 index 00000000000..a71bbe844b4 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_UNSET; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_UNSET; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; +import android.icu.text.MessageFormat; +import android.provider.Contacts; +import android.service.notification.ConversationChannelWrapper; +import android.service.notification.ZenPolicy; +import android.view.View; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.notification.app.ConversationListSettings; +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Common preference controller functionality for zen mode priority senders preferences for both + * messages and calls. + * + * These controllers handle the settings regarding which priority senders that are allowed to + * bypass DND for calls or messages, which may be one of the following values: starred contacts, all + * contacts, priority conversations (for messages only), anyone, or no one. + */ +public class ZenModePrioritySendersPreferenceController + extends AbstractZenModePreferenceController { + private final boolean mIsMessages; // if this is false, then this preference is for calls + + static final String KEY_ANY = "senders_anyone"; + static final String KEY_CONTACTS = "senders_contacts"; + static final String KEY_STARRED = "senders_starred_contacts"; + static final String KEY_IMPORTANT = "conversations_important"; + static final String KEY_NONE = "senders_none"; + + private int mNumImportantConversations = CONVERSATION_SENDERS_UNSET; + + private static final Intent ALL_CONTACTS_INTENT = + new Intent(Contacts.Intents.UI.LIST_DEFAULT) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + private static final Intent STARRED_CONTACTS_INTENT = + new Intent(Contacts.Intents.UI.LIST_STARRED_ACTION) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + private static final Intent FALLBACK_INTENT = new Intent(Intent.ACTION_MAIN) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + private final PackageManager mPackageManager; + private PreferenceCategory mPreferenceCategory; + private List mSelectorPreferences = new ArrayList<>(); + + private final ZenModeSummaryHelper mZenModeSummaryHelper; + + public ZenModePrioritySendersPreferenceController(Context context, String key, + boolean isMessages, ZenModesBackend backend) { + super(context, key, backend); + mIsMessages = isMessages; + + String contactsPackage = context.getString(R.string.config_contacts_package_name); + ALL_CONTACTS_INTENT.setPackage(contactsPackage); + STARRED_CONTACTS_INTENT.setPackage(contactsPackage); + FALLBACK_INTENT.setPackage(contactsPackage); + + mPackageManager = mContext.getPackageManager(); + if (!FALLBACK_INTENT.hasCategory(Intent.CATEGORY_APP_CONTACTS)) { + FALLBACK_INTENT.addCategory(Intent.CATEGORY_APP_CONTACTS); + } + mZenModeSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + mPreferenceCategory = screen.findPreference(getPreferenceKey()); + if (mPreferenceCategory.getPreferenceCount() == 0) { + makeSelectorPreference(KEY_STARRED, + com.android.settings.R.string.zen_mode_from_starred, mIsMessages); + makeSelectorPreference(KEY_CONTACTS, + com.android.settings.R.string.zen_mode_from_contacts, mIsMessages); + if (mIsMessages) { + makeSelectorPreference(KEY_IMPORTANT, + com.android.settings.R.string.zen_mode_from_important_conversations, true); + } + makeSelectorPreference(KEY_ANY, + com.android.settings.R.string.zen_mode_from_anyone, mIsMessages); + makeSelectorPreference(KEY_NONE, + com.android.settings.R.string.zen_mode_none_messages, mIsMessages); + } + super.displayPreference(screen); + } + + @Override + public void updateState(Preference preference) { + if (mIsMessages) { + updateChannelCounts(); + } + final int currContactsSetting = getPrioritySenders(); + final int currConversationsSetting = getPriorityConversationSenders(); + for (SelectorWithWidgetPreference pref : mSelectorPreferences) { + // for each preference, check whether the current state matches what this state + // would look like if the button were checked. + final int[] checkedState = keyToSettingEndState(pref.getKey(), true); + final int checkedContactsSetting = checkedState[0]; + final int checkedConversationsSetting = checkedState[1]; + + boolean match = checkedContactsSetting == currContactsSetting; + if (mIsMessages && checkedConversationsSetting != CONVERSATION_SENDERS_UNSET) { + // "CONVERSATION_SENDERS_UNSET" in checkedContactsSetting means this preference + // doesn't govern the priority senders setting, so the full match happens when + // either the priority senders setting matches or if it's CONVERSATION_SENDERS_UNSET + // so only the conversation setting needs to match. + match = (match || checkedContactsSetting == PEOPLE_TYPE_UNSET) + && (checkedConversationsSetting == currConversationsSetting); + } + + pref.setChecked(match); + } + updateSummaries(); + } + + public void onResume() { + if (mIsMessages) { + updateChannelCounts(); + } + updateSummaries(); + } + + private void updateChannelCounts() { + ParceledListSlice impConversations = + mBackend.getConversations(true); + int numImportantConversations = 0; + if (impConversations != null) { + for (ConversationChannelWrapper conversation : impConversations.getList()) { + if (!conversation.getNotificationChannel().isDemoted()) { + numImportantConversations++; + } + } + } + mNumImportantConversations = numImportantConversations; + } + + private int getPrioritySenders() { + if (mIsMessages) { + return getMode().getPolicy().getPriorityMessageSenders(); + } else { + return getMode().getPolicy().getPriorityCallSenders(); + } + } + + private int getPriorityConversationSenders() { + if (mIsMessages) { + return getMode().getPolicy().getPriorityConversationSenders(); + } + return CONVERSATION_SENDERS_UNSET; + } + + private SelectorWithWidgetPreference makeSelectorPreference(String key, int titleId, + boolean isCheckbox) { + final SelectorWithWidgetPreference pref = + new SelectorWithWidgetPreference(mPreferenceCategory.getContext(), isCheckbox); + pref.setKey(key); + pref.setTitle(titleId); + pref.setOnClickListener(mSelectorClickListener); + + View.OnClickListener widgetClickListener = getWidgetClickListener(key); + if (widgetClickListener != null) { + pref.setExtraWidgetOnClickListener(widgetClickListener); + } + + mPreferenceCategory.addPreference(pref); + mSelectorPreferences.add(pref); + return pref; + } + + private View.OnClickListener getWidgetClickListener(String key) { + if (!KEY_CONTACTS.equals(key) && !KEY_STARRED.equals(key) && !KEY_IMPORTANT.equals(key)) { + return null; + } + + if (KEY_STARRED.equals(key) && !isStarredIntentValid()) { + return null; + } + + if (KEY_CONTACTS.equals(key) && !isContactsIntentValid()) { + return null; + } + + return v -> { + if (KEY_STARRED.equals(key) + && STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) { + mContext.startActivity(STARRED_CONTACTS_INTENT); + } else if (KEY_CONTACTS.equals(key) + && ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) { + mContext.startActivity(ALL_CONTACTS_INTENT); + } else if (KEY_IMPORTANT.equals(key)) { + new SubSettingLauncher(mContext) + .setDestination(ConversationListSettings.class.getName()) + .setSourceMetricsCategory(SettingsEnums.DND_CONVERSATIONS) + .launch(); + } else { + mContext.startActivity(FALLBACK_INTENT); + } + }; + } + + private boolean isStarredIntentValid() { + return STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null + || FALLBACK_INTENT.resolveActivity(mPackageManager) != null; + } + + private boolean isContactsIntentValid() { + return ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null + || FALLBACK_INTENT.resolveActivity(mPackageManager) != null; + } + + void updateSummaries() { + for (SelectorWithWidgetPreference pref : mSelectorPreferences) { + pref.setSummary(getSummary(pref.getKey())); + } + } + + // Gets the desired end state of the priority senders and conversations for the given key + // and whether it is being checked or unchecked. [type]_UNSET indicates no change in state. + // + // Returns an integer array with 2 entries. The first entry is the setting for priority senders + // and the second entry is for priority conversation senders; if isMessages is false, then + // no changes will ever be prescribed for conversation senders. + int[] keyToSettingEndState(String key, boolean checked) { + int[] endState = new int[]{ PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET }; + if (!checked) { + // Unchecking any priority-senders-based state should reset the state to NONE. + // "Unchecking" the NONE state doesn't do anything, in practice. + switch (key) { + case KEY_STARRED: + case KEY_CONTACTS: + case KEY_ANY: + case KEY_NONE: + endState[0] = PEOPLE_TYPE_NONE; + } + + // For messages, unchecking "priority conversations" and "any" should reset conversation + // state to "NONE" as well. + if (mIsMessages) { + switch (key) { + case KEY_IMPORTANT: + case KEY_ANY: + case KEY_NONE: + endState[1] = CONVERSATION_SENDERS_NONE; + } + } + } else { + // All below is for the enabling (checked) state. + switch (key) { + case KEY_STARRED: + endState[0] = PEOPLE_TYPE_STARRED; + break; + case KEY_CONTACTS: + endState[0] = PEOPLE_TYPE_CONTACTS; + break; + case KEY_ANY: + endState[0] = PEOPLE_TYPE_ANYONE; + break; + case KEY_NONE: + endState[0] = PEOPLE_TYPE_NONE; + } + + // In the messages case *only*, also handle changing of conversation settings. + if (mIsMessages) { + switch (key) { + case KEY_IMPORTANT: + endState[1] = CONVERSATION_SENDERS_IMPORTANT; + break; + case KEY_ANY: + endState[1] = CONVERSATION_SENDERS_ANYONE; + break; + case KEY_NONE: + endState[1] = CONVERSATION_SENDERS_NONE; + } + } + } + + // Error case check: if somehow, after all of that, endState is still + // {PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET}, something has gone wrong. + if (endState[0] == PEOPLE_TYPE_UNSET && endState[1] == CONVERSATION_SENDERS_UNSET) { + throw new IllegalArgumentException("invalid key " + key); + } + + return endState; + } + + // Returns the preferences, if any, that should be newly saved for the specified setting and + // checked state in an array where index 0 is the new senders setting and 1 the new + // conversations setting. A return value of [type]_UNSET indicates that nothing should + // change. + // + // The returned conversations setting will always be CONVERSATION_SENDERS_UNSET (not to change) + // in the calls case. + // + // Checking and unchecking is mostly an operation of setting or unsetting the relevant + // preference, except for some special handling where the conversation setting overlaps: + // - setting or unsetting "priority contacts" or "contacts" has no effect on the + // priority conversation setting, and vice versa + // - if "priority conversations" is selected, and the user checks "anyone", the conversation + // setting is also set to any conversations + // - if "anyone" is previously selected, and the user clicks "priority conversations", then + // the contacts setting is additionally reset to "none". + // - if "anyone" is previously selected, and the user clicks one of the contacts values, + // then the conversations setting is additionally reset to "none". + int[] settingsToSaveOnClick(SelectorWithWidgetPreference preference, + int currSendersSetting, int currConvosSetting) { + int[] savedSettings = new int[]{ PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET }; + + // If the preference isn't a checkbox, always consider this to be "checking" the setting. + // Otherwise, toggle. + final int[] endState = keyToSettingEndState(preference.getKey(), + preference.isCheckBox() ? preference.isChecked() : true); + final int prioritySendersSetting = endState[0]; + final int priorityConvosSetting = endState[1]; + + if (prioritySendersSetting != PEOPLE_TYPE_UNSET + && prioritySendersSetting != currSendersSetting) { + savedSettings[0] = prioritySendersSetting; + } + + // Only handle conversation settings for the messages case. If not messages, there should + // never be any change to the conversation senders setting. + if (mIsMessages) { + if (priorityConvosSetting != CONVERSATION_SENDERS_UNSET + && priorityConvosSetting != currConvosSetting) { + savedSettings[1] = priorityConvosSetting; + } + + // Special-case handling for the "priority conversations" checkbox: + // If a specific selection exists for priority senders (starred, contacts), we leave + // it untouched. Otherwise (when the senders is set to "any"), set it to NONE. + if (preference.getKey() == KEY_IMPORTANT + && currSendersSetting == PEOPLE_TYPE_ANYONE) { + savedSettings[0] = PEOPLE_TYPE_NONE; + } + + // Flip-side special case for clicking either "contacts" option: if a specific selection + // exists for priority conversations, leave it untouched; otherwise, set to none. + if ((preference.getKey() == KEY_STARRED || preference.getKey() == KEY_CONTACTS) + && currConvosSetting == CONVERSATION_SENDERS_ANYONE) { + savedSettings[1] = CONVERSATION_SENDERS_NONE; + } + } + + return savedSettings; + } + + private String getSummary(String key) { + switch (key) { + case KEY_STARRED: + return mZenModeSummaryHelper.getStarredContactsSummary(); + case KEY_CONTACTS: + return mZenModeSummaryHelper.getContactsNumberSummary(); + case KEY_IMPORTANT: + return getConversationSummary(); + case KEY_ANY: + return mContext.getResources().getString(mIsMessages + ? R.string.zen_mode_all_messages_summary + : R.string.zen_mode_all_calls_summary); + case KEY_NONE: + default: + return null; + } + } + + private String getConversationSummary() { + final int numConversations = mNumImportantConversations; + + if (numConversations == CONVERSATION_SENDERS_UNSET) { + return null; + } else { + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_conversations_count), + Locale.getDefault()); + Map args = new HashMap<>(); + args.put("count", numConversations); + return msgFormat.format(args); + } + } + + @VisibleForTesting + SelectorWithWidgetPreference.OnClickListener mSelectorClickListener = + new SelectorWithWidgetPreference.OnClickListener() { + @Override + public void onRadioButtonClicked(SelectorWithWidgetPreference preference) { + // The settingsToSaveOnClick function takes whether the preference is a + // checkbox into account to determine whether this selection is checked or unchecked. + final int[] settingsToSave = settingsToSaveOnClick(preference, + getPrioritySenders(), getPriorityConversationSenders()); + final int prioritySendersSetting = settingsToSave[0]; + final int priorityConvosSetting = settingsToSave[1]; + + ZenPolicy.Builder diffPolicy = new ZenPolicy.Builder(); + if (prioritySendersSetting != PEOPLE_TYPE_UNSET) { + if (mIsMessages) { + diffPolicy.allowMessages(prioritySendersSetting); + + } else { + diffPolicy.allowCalls(prioritySendersSetting); + } + } + if (mIsMessages && priorityConvosSetting != CONVERSATION_SENDERS_UNSET) { + diffPolicy.allowConversations(priorityConvosSetting); + } + getMode().setPolicy(diffPolicy.build()); + mBackend.updateMode(getMode()); + } + }; +} diff --git a/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java new file mode 100644 index 00000000000..d6de9c22d3c --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; +import static android.service.notification.ZenPolicy.STATE_ALLOW; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.provider.Settings; +import android.service.notification.ZenPolicy; +import android.util.Log; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; +import com.android.settings.R; + +public class ZenModeRepeatCallersPreferenceController extends AbstractZenModePreferenceController + implements Preference.OnPreferenceChangeListener { + + private final int mRepeatCallersThreshold; + + public ZenModeRepeatCallersPreferenceController(Context context, + String key, ZenModesBackend backend, int repeatCallersThreshold) { + super(context, key, backend); + + mRepeatCallersThreshold = repeatCallersThreshold; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + TwoStatePreference pref = (TwoStatePreference) preference; + + boolean anyCallersCanBypassDnd = + getMode().getPolicy().getPriorityCategoryCalls() == STATE_ALLOW + && getMode().getPolicy().getPriorityCallSenders() == PEOPLE_TYPE_ANYONE; + // if any caller can bypass dnd then repeat callers preference is disabled + if (anyCallersCanBypassDnd) { + pref.setEnabled(false); + pref.setChecked(true); + } else { + pref.setEnabled(true); + pref.setChecked( + getMode().getPolicy().getPriorityCategoryRepeatCallers() == STATE_ALLOW); + } + + setRepeatCallerSummary(preference); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean allowRepeatCallers = (Boolean) newValue; + ZenPolicy diffPolicy = new ZenPolicy.Builder() + .allowRepeatCallers(allowRepeatCallers) + .build(); + getMode().setPolicy(diffPolicy); + mBackend.updateMode(getMode()); + return true; + } + + private void setRepeatCallerSummary(Preference preference) { + preference.setSummary(mContext.getString(R.string.zen_mode_repeat_callers_summary, + mRepeatCallersThreshold)); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java new file mode 100644 index 00000000000..cf0c3db7499 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.notification.modes; + +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_CALLS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_CONVERSATIONS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MESSAGES; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REPEAT_CALLERS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM; + +import android.content.Context; +import android.icu.text.MessageFormat; +import android.service.notification.ZenPolicy; + +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Predicate; + +public class ZenModeSummaryHelper { + + private Context mContext; + private ZenModesBackend mBackend; + + public ZenModeSummaryHelper(Context context, ZenModesBackend backend) { + mContext = context; + mBackend = backend; + } + + private static final int[] ALL_PRIORITY_CATEGORIES = { + PRIORITY_CATEGORY_ALARMS, + PRIORITY_CATEGORY_MEDIA, + PRIORITY_CATEGORY_SYSTEM, + PRIORITY_CATEGORY_MESSAGES, + PRIORITY_CATEGORY_CONVERSATIONS, + PRIORITY_CATEGORY_EVENTS, + PRIORITY_CATEGORY_REMINDERS, + PRIORITY_CATEGORY_CALLS, + PRIORITY_CATEGORY_REPEAT_CALLERS, + }; + + String getOtherSoundCategoriesSummary(ZenMode zenMode) { + List enabledCategories = getEnabledCategories( + zenMode.getPolicy(), + category -> PRIORITY_CATEGORY_ALARMS == category + || PRIORITY_CATEGORY_MEDIA == category + || PRIORITY_CATEGORY_SYSTEM == category + || PRIORITY_CATEGORY_REMINDERS == category + || PRIORITY_CATEGORY_EVENTS == category, + true); + int numCategories = enabledCategories.size(); + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_other_sounds_summary), + Locale.getDefault()); + Map args = new HashMap<>(); + args.put("count", numCategories); + if (numCategories >= 1) { + args.put("sound_category_1", enabledCategories.get(0)); + if (numCategories >= 2) { + args.put("sound_category_2", enabledCategories.get(1)); + if (numCategories == 3) { + args.put("sound_category_3", enabledCategories.get(2)); + } + } + } + return msgFormat.format(args); + } + + String getCallsSettingSummary(ZenMode zenMode) { + List enabledCategories = getEnabledCategories(zenMode.getPolicy(), + category -> PRIORITY_CATEGORY_CALLS == category + || PRIORITY_CATEGORY_REPEAT_CALLERS == category, true); + int numCategories = enabledCategories.size(); + if (numCategories == 0) { + return mContext.getString(R.string.zen_mode_none_calls); + } else if (numCategories == 1) { + return mContext.getString(R.string.zen_mode_calls_summary_one, + enabledCategories.get(0)); + } else { + return mContext.getString(R.string.zen_mode_calls_summary_two, + enabledCategories.get(0), + enabledCategories.get(1)); + } + } + + String getMessagesSettingSummary(ZenPolicy policy) { + List enabledCategories = getEnabledCategories(policy, + category -> PRIORITY_CATEGORY_MESSAGES == category + || PRIORITY_CATEGORY_CONVERSATIONS == category, true); + int numCategories = enabledCategories.size(); + if (numCategories == 0) { + return mContext.getString(R.string.zen_mode_none_messages); + } else if (numCategories == 1) { + return enabledCategories.get(0); + } else { + // While this string name seems like a slight misnomer: it's borrowing the analogous + // calls-summary functionality to combine two permissions. + return mContext.getString(R.string.zen_mode_calls_summary_two, + enabledCategories.get(0), + enabledCategories.get(1)); + } + } + + String getBlockedEffectsSummary(ZenMode zenMode) { + if (zenMode.getPolicy().shouldShowAllVisualEffects()) { + return mContext.getResources().getString( + R.string.zen_mode_restrict_notifications_summary_muted); + } else if (zenMode.getPolicy().shouldHideAllVisualEffects()) { + return mContext.getResources().getString( + R.string.zen_mode_restrict_notifications_summary_hidden); + } else { + return mContext.getResources().getString( + R.string.zen_mode_restrict_notifications_summary_custom); + } + } + + private List getEnabledCategories(ZenPolicy policy, + Predicate filteredCategories, boolean capitalizeFirstInList) { + List enabledCategories = new ArrayList<>(); + for (int category : ALL_PRIORITY_CATEGORIES) { + boolean isFirst = capitalizeFirstInList && enabledCategories.isEmpty(); + if (filteredCategories.test(category) && policy.isCategoryAllowed(category, false)) { + if (category == PRIORITY_CATEGORY_REPEAT_CALLERS + && policy.isCategoryAllowed(PRIORITY_CATEGORY_CALLS, false) + && policy.getPriorityCallSenders() == PEOPLE_TYPE_ANYONE) { + continue; + } + + // For conversations, only the "priority conversations" setting is relevant; any + // other setting is subsumed by the messages-specific messaging. + if (category == PRIORITY_CATEGORY_CONVERSATIONS + && policy.isCategoryAllowed(PRIORITY_CATEGORY_CONVERSATIONS, false) + && policy.getPriorityConversationSenders() + != CONVERSATION_SENDERS_IMPORTANT) { + continue; + } + + enabledCategories.add(getCategory(category, policy, isFirst)); + } + } + return enabledCategories; + } + + private String getCategory(int category, ZenPolicy policy, boolean isFirst) { + if (category == PRIORITY_CATEGORY_ALARMS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_alarms_list_first); + } else { + return mContext.getString(R.string.zen_mode_alarms_list); + } + } else if (category == PRIORITY_CATEGORY_MEDIA) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_media_list_first); + } else { + return mContext.getString(R.string.zen_mode_media_list); + } + } else if (category == PRIORITY_CATEGORY_SYSTEM) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_system_list_first); + } else { + return mContext.getString(R.string.zen_mode_system_list); + } + } else if (category == PRIORITY_CATEGORY_MESSAGES) { + if (policy.getPriorityMessageSenders() == PEOPLE_TYPE_ANYONE) { + return mContext.getString(R.string.zen_mode_from_anyone); + } else if (policy.getPriorityMessageSenders() == PEOPLE_TYPE_CONTACTS) { + return mContext.getString(R.string.zen_mode_from_contacts); + } else { + return mContext.getString(R.string.zen_mode_from_starred); + } + } else if (category == PRIORITY_CATEGORY_CONVERSATIONS + && policy.getPriorityConversationSenders() == CONVERSATION_SENDERS_IMPORTANT) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_from_important_conversations); + } else { + return mContext.getString( + R.string.zen_mode_from_important_conversations_second); + } + } else if (category == PRIORITY_CATEGORY_EVENTS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_events_list_first); + } else { + return mContext.getString(R.string.zen_mode_events_list); + } + } else if (category == PRIORITY_CATEGORY_REMINDERS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_reminders_list_first); + } else { + return mContext.getString(R.string.zen_mode_reminders_list); + } + } else if (category == PRIORITY_CATEGORY_CALLS) { + if (policy.getPriorityCallSenders() == PEOPLE_TYPE_ANYONE) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_from_anyone); + } + return mContext.getString(R.string.zen_mode_all_callers); + } else if (policy.getPriorityCallSenders() == PEOPLE_TYPE_CONTACTS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_from_contacts); + } + return mContext.getString(R.string.zen_mode_contacts_callers); + } else { + if (isFirst) { + return mContext.getString(R.string.zen_mode_from_starred); + } + return mContext.getString(R.string.zen_mode_starred_callers); + } + } else if (category == PRIORITY_CATEGORY_REPEAT_CALLERS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_repeat_callers); + } else { + return mContext.getString(R.string.zen_mode_repeat_callers_list); + } + } + + return ""; + } + + public String getStarredContactsSummary() { + List starredContacts = mBackend.getStarredContacts(); + int numStarredContacts = starredContacts.size(); + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_starred_contacts_summary_contacts), + Locale.getDefault()); + Map args = new HashMap<>(); + args.put("count", numStarredContacts); + if (numStarredContacts >= 1) { + args.put("contact_1", starredContacts.get(0)); + if (numStarredContacts >= 2) { + args.put("contact_2", starredContacts.get(1)); + if (numStarredContacts == 3) { + args.put("contact_3", starredContacts.get(2)); + } + } + } + return msgFormat.format(args); + } + + public String getContactsNumberSummary() { + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_contacts_count), + Locale.getDefault()); + Map args = new HashMap<>(); + args.put("count", mBackend.queryAllContactsData().getCount()); + return msgFormat.format(args); + } + + public String getPeopleSummary(ZenMode zenMode) { + final int callersAllowed = zenMode.getPolicy().getPriorityCallSenders(); + final int messagesAllowed = zenMode.getPolicy().getPriorityMessageSenders(); + final int conversationsAllowed = zenMode.getPolicy().getPriorityConversationSenders(); + final boolean areRepeatCallersAllowed = + zenMode.getPolicy().isCategoryAllowed(PRIORITY_CATEGORY_REPEAT_CALLERS, false); + + if (callersAllowed == PEOPLE_TYPE_ANYONE + && messagesAllowed == PEOPLE_TYPE_ANYONE + && conversationsAllowed == CONVERSATION_SENDERS_ANYONE) { + return mContext.getResources().getString(R.string.zen_mode_people_all); + } else if (callersAllowed == PEOPLE_TYPE_NONE + && messagesAllowed == PEOPLE_TYPE_NONE + && conversationsAllowed == CONVERSATION_SENDERS_NONE + && !areRepeatCallersAllowed) { + return mContext.getResources().getString(R.string.zen_mode_people_none); + } else { + return mContext.getResources().getString(R.string.zen_mode_people_some); + } + } +} diff --git a/src/com/android/settings/notification/modes/ZenModesBackend.java b/src/com/android/settings/notification/modes/ZenModesBackend.java index 355adb46198..ac170c6d3d9 100644 --- a/src/com/android/settings/notification/modes/ZenModesBackend.java +++ b/src/com/android/settings/notification/modes/ZenModesBackend.java @@ -21,14 +21,22 @@ import static java.util.Objects.requireNonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AutomaticZenRule; +import android.app.INotificationManager; import android.app.NotificationManager; import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.database.Cursor; import android.net.Uri; +import android.os.ServiceManager; +import android.provider.ContactsContract; import android.provider.Settings; import android.service.notification.Condition; +import android.service.notification.ConversationChannelWrapper; import android.service.notification.ZenAdapters; import android.service.notification.ZenModeConfig; +import android.util.Log; +import androidx.annotation.VisibleForTesting; import com.android.settings.R; import java.time.Duration; @@ -51,6 +59,8 @@ class ZenModesBackend { private static ZenModesBackend sInstance; private final NotificationManager mNotificationManager; + static INotificationManager sINM = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); private final Context mContext; @@ -105,6 +115,54 @@ class ZenModesBackend { } } + public ParceledListSlice getConversations(boolean onlyImportant) { + try { + return sINM.getConversations(onlyImportant); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return ParceledListSlice.emptyList(); + } + } + + public List getStarredContacts() { + Cursor cursor = null; + try { + cursor = queryStarredContactsData(); + return getStarredContacts(cursor); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + @VisibleForTesting + List getStarredContacts(Cursor cursor) { + List starredContacts = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + String contact = cursor.getString(0); + starredContacts.add(contact != null ? contact : + mContext.getString(R.string.zen_mode_starred_contacts_empty_name)); + + } while (cursor.moveToNext()); + } + return starredContacts; + } + + private Cursor queryStarredContactsData() { + return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, + new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}, + ContactsContract.Data.STARRED + "=1", null, + ContactsContract.Data.TIMES_CONTACTED); + } + + Cursor queryAllContactsData() { + return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, + new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}, + null, null, null); + } + private ZenMode getManualDndMode(ZenModeConfig config) { // TODO: b/333530553 - Read ZenDeviceEffects of manual DND. // TODO: b/333682392 - Replace with final strings for name & trigger description diff --git a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java index 3b6d17f4225..595f1d08f29 100644 --- a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java @@ -57,9 +57,9 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment { @Override public void onAttach(@NonNull Context context) { - super.onAttach(context); mContext = context; mBackend = ZenModesBackend.getInstance(context); + super.onAttach(context); } @Override @@ -77,6 +77,12 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment { } } + @Override + public void onResume() { + super.onResume(); + updateZenModeState(); + } + @Override public void onStop() { super.onStop(); diff --git a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt index b15675ed14f..a411c90ea02 100644 --- a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt +++ b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt @@ -29,7 +29,7 @@ import com.android.settings.overlay.FeatureFactory.Companion.featureFactory import com.android.settingslib.R import com.android.settingslib.spa.lifecycle.collectAsCallbackWithLifecycle import com.android.settingslib.spaprivileged.model.app.AppOps -import com.android.settingslib.spaprivileged.model.app.AppOpsController +import com.android.settingslib.spaprivileged.model.app.AppOpsPermissionController import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.app.PackageManagers @@ -49,7 +49,7 @@ data class AlarmsAndRemindersAppRecord( override val app: ApplicationInfo, val isTrumped: Boolean, val isChangeable: Boolean, - var controller: AppOpsController, + val controller: AppOpsPermissionController, ) : AppRecord class AlarmsAndRemindersAppListModel( @@ -84,7 +84,7 @@ class AlarmsAndRemindersAppListModel( @Composable override fun isAllowed(record: AlarmsAndRemindersAppRecord): () -> Boolean? = when { record.isTrumped -> ({ true }) - else -> record.controller.isAllowed.collectAsCallbackWithLifecycle() + else -> record.controller.isAllowedFlow.collectAsCallbackWithLifecycle() } override fun isChangeable(record: AlarmsAndRemindersAppRecord) = record.isChangeable @@ -114,10 +114,11 @@ class AlarmsAndRemindersAppListModel( app = app, isTrumped = isTrumped, isChangeable = hasRequestPermission && !isTrumped, - controller = AppOpsController( + controller = AppOpsPermissionController( context = context, app = app, appOps = APP_OPS, + permission = PERMISSION, ), ) } diff --git a/src/com/android/settings/system/FactoryResetPreferenceController.java b/src/com/android/settings/system/FactoryResetPreferenceController.java index 6d811797188..df7cc3df3a2 100644 --- a/src/com/android/settings/system/FactoryResetPreferenceController.java +++ b/src/com/android/settings/system/FactoryResetPreferenceController.java @@ -90,7 +90,8 @@ public class FactoryResetPreferenceController extends BasePreferenceController { String packageName = resolution.activityInfo.packageName; PackageInfo factoryResetWizardPackageInfo; try { - factoryResetWizardPackageInfo = pm.getPackageInfo(packageName, 0); + factoryResetWizardPackageInfo = pm.getPackageInfo(packageName, + PackageManager.GET_PERMISSIONS); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Unable to resolve a Factory Reset Handler Application"); return null; diff --git a/tests/robotests/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceControllerTest.java index 3e97cec0f4a..f0decabccee 100644 --- a/tests/robotests/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/MagnificationAlwaysOnPreferenceControllerTest.java @@ -18,6 +18,8 @@ package com.android.settings.accessibility; import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; import static com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode; +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; import static com.google.common.truth.Truth.assertThat; @@ -148,4 +150,15 @@ public class MagnificationAlwaysOnPreferenceControllerTest { mController.updateState(mSwitchPreference); assertThat(mSwitchPreference.isEnabled()).isTrue(); } + + @Test + public void getAvailableStatus_notInSetupWizard_returnAvailable() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailableStatus_inSetupWizard_returnConditionallyUnavailable() { + mController.setInSetupWizard(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } } diff --git a/tests/robotests/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceControllerTest.java index 3aeeca8f59c..bc0563a9631 100644 --- a/tests/robotests/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/MagnificationFollowTypingPreferenceControllerTest.java @@ -17,6 +17,8 @@ package com.android.settings.accessibility; import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; import static com.google.common.truth.Truth.assertThat; @@ -61,6 +63,17 @@ public class MagnificationFollowTypingPreferenceControllerTest { reset(mSwitchPreference); } + @Test + public void getAvailableStatus_notInSetupWizard_returnAvailable() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailableStatus_inSetupWizard_returnConditionallyUnavailable() { + mController.setInSetupWizard(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + @Test public void performClick_switchDefaultStateForFollowTyping_shouldReturnFalse() { mSwitchPreference.performClick(); diff --git a/tests/robotests/src/com/android/settings/accessibility/MagnificationJoystickPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/MagnificationJoystickPreferenceControllerTest.java index 11258bb6041..3fe3ed6d6d1 100644 --- a/tests/robotests/src/com/android/settings/accessibility/MagnificationJoystickPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/MagnificationJoystickPreferenceControllerTest.java @@ -17,6 +17,8 @@ package com.android.settings.accessibility; import static com.android.settings.accessibility.AccessibilityUtil.State.OFF; +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; import static com.google.common.truth.Truth.assertThat; @@ -80,4 +82,15 @@ public class MagnificationJoystickPreferenceControllerTest { assertThat(mController.isChecked()).isFalse(); assertThat(mSwitchPreference.isChecked()).isFalse(); } + + @Test + public void getAvailableStatus_notInSetupWizard_returnAvailable() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailableStatus_inSetupWizard_returnConditionallyUnavailable() { + mController.setInSetupWizard(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } } diff --git a/tests/robotests/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceControllerTest.java index bfc8313d142..8b4bcd8fe47 100644 --- a/tests/robotests/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/MagnificationOneFingerPanningPreferenceControllerTest.java @@ -19,6 +19,8 @@ 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.android.settings.accessibility.MagnificationCapabilities.MagnificationMode; +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; import static com.google.common.truth.Truth.assertThat; @@ -186,6 +188,17 @@ public class MagnificationOneFingerPanningPreferenceControllerTest { assertThat(mSwitchPreference.isChecked()).isFalse(); } + @Test + public void getAvailableStatus_notInSetupWizard_returnAvailable() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailableStatus_inSetupWizard_returnConditionallyUnavailable() { + mController.setInSetupWizard(true); + assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + private String enabledSummary() { return mContext.getString( R.string.accessibility_magnification_one_finger_panning_summary_on); diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java index f26e838375d..59bae921b0d 100644 --- a/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest.java @@ -32,7 +32,6 @@ import androidx.lifecycle.LifecycleOwner; import androidx.preference.Preference; import androidx.preference.PreferenceManager; import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreferenceCompat; import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; @@ -97,7 +96,6 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest { verify(mFooterBarMixin).setPrimaryButton(any()); assertThat(mFragment.mTopIntroPreference.isVisible()).isFalse(); assertThat(mFragment.mSettingsPreference.isVisible()).isFalse(); - assertThat(mFragment.mFollowingTypingSwitchPreference.isVisible()).isFalse(); } @Test @@ -124,7 +122,6 @@ public class ToggleScreenMagnificationPreferenceFragmentForSetupWizardTest { mPreferenceManager.setPreferences(mPreferenceManager.createPreferenceScreen(context)); mTopIntroPreference = new TopIntroPreference(context); mSettingsPreference = new Preference(context); - mFollowingTypingSwitchPreference = new SwitchPreferenceCompat(context); } @Override diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt b/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt index e876289866e..e30819bcb2f 100644 --- a/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt +++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint2/fragment/FingerprintEnrollIntroFragmentTest.kt @@ -90,7 +90,7 @@ class FingerprintEnrollIntroFragmentTest { private val navigationViewModel = FingerprintNavigationViewModel( - Introduction, + Introduction(), false, flowViewModel, interactor diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..04d625a2848 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; +import androidx.preference.Preference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowApplication; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(RobolectricTestRunner.class) +public final class ZenModeCallsLinkPreferenceControllerTest { + + private ZenModeCallsLinkPreferenceController mController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mController = new ZenModeCallsLinkPreferenceController( + mContext, "something", mBackend); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testHasSummary() { + Preference pref = mock(Preference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(), true); + mController.updateZenMode(pref, zenMode); + verify(pref).setSummary(any()); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..cfeefb40a43 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; +import androidx.preference.Preference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public final class ZenModeMessagesLinkPreferenceControllerTest { + + private ZenModeMessagesLinkPreferenceController mController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mController = new ZenModeMessagesLinkPreferenceController( + mContext, "something", mBackend); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testHasSummary() { + Preference pref = mock(Preference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(), true); + mController.updateZenMode(pref, zenMode); + verify(pref).setSummary(any()); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..26da6ab8dc6 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; +import androidx.preference.Preference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public final class ZenModeOtherLinkPreferenceControllerTest { + + private ZenModeOtherLinkPreferenceController mController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mController = new ZenModeOtherLinkPreferenceController( + mContext, "something", mBackend); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testHasSummary() { + Preference pref = mock(Preference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(), true); + mController.updateZenMode(pref, zenMode); + verify(pref).setSummary(any()); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherPreferenceControllerTest.java new file mode 100644 index 00000000000..6dd918a145f --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherPreferenceControllerTest.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.service.notification.ZenPolicy.STATE_ALLOW; +import static android.service.notification.ZenPolicy.STATE_UNSET; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; +import androidx.preference.TwoStatePreference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public final class ZenModeOtherPreferenceControllerTest { + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + } + + @Test + public void testUpdateState_alarms() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_alarm", mBackend); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testOnPreferenceChange_alarms() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAlarms(false).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_alarm", mBackend); + + controller.updateZenMode(preference, zenMode); + + controller.onPreferenceChange(preference, true); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getPolicy().getPriorityCategoryAlarms()) + .isEqualTo(STATE_ALLOW); + assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents()) + .isEqualTo(STATE_UNSET); + } + + @Test + public void testUpdateState_media() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowMedia(true).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_media", mBackend); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testOnPreferenceChange_media() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowMedia(false).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_media", mBackend); + + controller.updateZenMode(preference, zenMode); + + controller.onPreferenceChange(preference, true); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getPolicy().getPriorityCategoryMedia()) + .isEqualTo(STATE_ALLOW); + assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents()) + .isEqualTo(STATE_UNSET); + } + + @Test + public void testUpdateState_system() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowSystem(true).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_system", mBackend); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testOnPreferenceChange_system() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowSystem(false).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_system", mBackend); + + controller.updateZenMode(preference, zenMode); + + controller.onPreferenceChange(preference, true); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getPolicy().getPriorityCategorySystem()) + .isEqualTo(STATE_ALLOW); + assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents()) + .isEqualTo(STATE_UNSET); + } + + @Test + public void testUpdateState_reminders() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowReminders(true).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_reminders", + mBackend); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testOnPreferenceChange_reminders() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowReminders(false).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_reminders", + mBackend); + + controller.updateZenMode(preference, zenMode); + + controller.onPreferenceChange(preference, true); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getPolicy().getPriorityCategoryReminders()) + .isEqualTo(STATE_ALLOW); + assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents()) + .isEqualTo(STATE_UNSET); + } + + @Test + public void testUpdateState_events() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowEvents(true).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_events", mBackend); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testOnPreferenceChange_events() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowEvents(false).build()) + .build(), true); + + ZenModeOtherPreferenceController controller = + new ZenModeOtherPreferenceController(mContext, "modes_category_events", mBackend); + + controller.updateZenMode(preference, zenMode); + + controller.onPreferenceChange(preference, true); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents()) + .isEqualTo(STATE_ALLOW); + assertThat(captor.getValue().getPolicy().getPriorityCategoryAlarms()) + .isEqualTo(STATE_UNSET); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..81e64648a33 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; +import androidx.preference.Preference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public final class ZenModePeopleLinkPreferenceControllerTest { + + private ZenModePeopleLinkPreferenceController mController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mController = new ZenModePeopleLinkPreferenceController( + mContext, "something", mBackend); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testHasSummary() { + Preference pref = mock(Preference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(), true); + mController.updateZenMode(pref, zenMode); + verify(pref).setSummary(any()); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java new file mode 100644 index 00000000000..91eb59a77dc --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java @@ -0,0 +1,509 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_UNSET; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_UNSET; +import static android.service.notification.ZenPolicy.STATE_ALLOW; +import static android.service.notification.ZenPolicy.STATE_DISALLOW; +import static android.service.notification.ZenPolicy.STATE_UNSET; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_ANY; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_CONTACTS; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_IMPORTANT; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_NONE; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_STARRED; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; +import com.android.settingslib.widget.SelectorWithWidgetPreference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public final class ZenModePrioritySendersPreferenceControllerTest { + + private ZenModePrioritySendersPreferenceController mCallsController; + private ZenModePrioritySendersPreferenceController mMessagesController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Mock + private PreferenceCategory mMockMessagesPrefCategory, mMockCallsPrefCategory; + @Mock + private PreferenceScreen mPreferenceScreen; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mMessagesController = new ZenModePrioritySendersPreferenceController( + mContext, "messages", true, mBackend); + mCallsController = new ZenModePrioritySendersPreferenceController( + mContext, "calls", false, mBackend); + when(mMockMessagesPrefCategory.getContext()).thenReturn(mContext); + when(mMockCallsPrefCategory.getContext()).thenReturn(mContext); + when(mPreferenceScreen.findPreference(mMessagesController.getPreferenceKey())) + .thenReturn(mMockMessagesPrefCategory); + when(mPreferenceScreen.findPreference(mCallsController.getPreferenceKey())) + .thenReturn(mMockCallsPrefCategory); + } + + // Makes a preference with the provided key and whether it's a checkbox with + // mSelectorClickListener as the onClickListener set. + private SelectorWithWidgetPreference makePreference( + String key, boolean isCheckbox, boolean isMessages) { + final SelectorWithWidgetPreference pref = + new SelectorWithWidgetPreference(mContext, isCheckbox); + pref.setKey(key); + pref.setOnClickListener( + isMessages ? mMessagesController.mSelectorClickListener + : mCallsController.mSelectorClickListener); + return pref; + } + + // Extension of ArgumentMatcher to check that a preference argument has the correct preference + // key, but doesn't check any other properties. + private class PrefKeyMatcher implements ArgumentMatcher { + private String mKey; + PrefKeyMatcher(String key) { + mKey = key; + } + + public boolean matches(SelectorWithWidgetPreference pref) { + return pref.getKey() != null && pref.getKey().equals(mKey); + } + + public String toString() { + return "SelectorWithWidgetPreference matcher for key " + mKey; + } + } + + @Test + public void testDisplayPreferences_makeMessagesPrefs() { + ArgumentCaptor prefCaptor = + ArgumentCaptor.forClass(SelectorWithWidgetPreference.class); + when(mMockMessagesPrefCategory.getPreferenceCount()).thenReturn(0); // not yet created + mMessagesController.displayPreference(mPreferenceScreen); + + // Starred contacts, Contacts, Priority Conversations, Any, None + verify(mMockMessagesPrefCategory, times(5)).addPreference(prefCaptor.capture()); + } + + @Test + public void testDisplayPreferences_makeCallsPrefs() { + ArgumentCaptor prefCaptor = + ArgumentCaptor.forClass(SelectorWithWidgetPreference.class); + when(mMockCallsPrefCategory.getPreferenceCount()).thenReturn(0); // not yet created + mCallsController.displayPreference(mPreferenceScreen); + + // Starred contacts, Contacts, Any, None + verify(mMockCallsPrefCategory, times(4)).addPreference(prefCaptor.capture()); + + // Make sure we never have the conversation one + verify(mMockCallsPrefCategory, never()) + .addPreference(argThat(new PrefKeyMatcher(KEY_IMPORTANT))); + } + + @Test + public void testDisplayPreferences_createdOnlyOnce() { + // Return a nonzero number of child preference when asked. + // Then when displayPreference is called, it should never make any new preferences. + when(mMockCallsPrefCategory.getPreferenceCount()).thenReturn(4); // already created + mCallsController.displayPreference(mPreferenceScreen); + mCallsController.displayPreference(mPreferenceScreen); + mCallsController.displayPreference(mPreferenceScreen); + + // Even though we called display 3 times we shouldn't add more preferences here. + verify(mMockCallsPrefCategory, never()) + .addPreference(any(SelectorWithWidgetPreference.class)); + } + + @Test + public void testKeyToSettingEndState_messagesCheck() { + int[] endState; + + // For KEY_NONE everything should be none. + endState = mMessagesController.keyToSettingEndState(KEY_NONE, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For KEY_ANY everything should be allowed. + endState = mMessagesController.keyToSettingEndState(KEY_ANY, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_ANYONE); + + // For [starred] contacts, we should set the priority senders, but not the conversations + endState = mMessagesController.keyToSettingEndState(KEY_STARRED, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + endState = mMessagesController.keyToSettingEndState(KEY_CONTACTS, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For priority conversations, we should set the conversations but not priority senders + endState = mMessagesController.keyToSettingEndState(KEY_IMPORTANT, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_IMPORTANT); + } + + @Test + public void testKeyToSettingEndState_messagesUncheck() { + int[] endState; + + // For KEY_NONE, "unchecking" still means "none". + endState = mMessagesController.keyToSettingEndState(KEY_NONE, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For KEY_ANY unchecking resets the state to "none". + endState = mMessagesController.keyToSettingEndState(KEY_ANY, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + endState = mMessagesController.keyToSettingEndState(KEY_STARRED, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + endState = mMessagesController.keyToSettingEndState(KEY_CONTACTS, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For priority conversations, we should set the conversations but not priority senders + endState = mMessagesController.keyToSettingEndState(KEY_IMPORTANT, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + } + + @Test + public void testKeyToSettingEndState_callsCheck() { + int[] endState; + + // For calls: we should never set conversations, as this is unrelated to calls. + // For KEY_NONE senders should be none. + endState = mCallsController.keyToSettingEndState(KEY_NONE, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY senders should be ANY. + endState = mCallsController.keyToSettingEndState(KEY_ANY, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For [starred] contacts, we should set the priority senders accordingly + endState = mCallsController.keyToSettingEndState(KEY_STARRED, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + endState = mCallsController.keyToSettingEndState(KEY_CONTACTS, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + } + + @Test + public void testKeyToSettingEndState_callsUncheck() { + int[] endState; + + // A calls setup should never set conversations settings. + // For KEY_NONE, "unchecking" still means "none". + endState = mCallsController.keyToSettingEndState(KEY_NONE, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY unchecking resets the state to "none". + endState = mCallsController.keyToSettingEndState(KEY_ANY, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + endState = mCallsController.keyToSettingEndState(KEY_STARRED, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + endState = mCallsController.keyToSettingEndState(KEY_CONTACTS, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + } + + @Test + public void testSettingsToSaveOnClick_messagesCheck() { + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + SelectorWithWidgetPreference impPref = makePreference(KEY_IMPORTANT, true, true); + int[] endState; + + // For KEY_NONE everything should be none. + nonePref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For KEY_ANY everything should be allowed. + anyPref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_ANYONE); + + // For [starred] contacts, we should set the priority senders, but not the conversations + starredPref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + contactsPref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For priority conversations, we should set the conversations but not priority senders + impPref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + impPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_IMPORTANT); + } + + @Test + public void testSettingsToSaveOnClick_messagesUncheck() { + int[] endState; + + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + SelectorWithWidgetPreference impPref = makePreference(KEY_IMPORTANT, true, true); + + // For KEY_NONE, "unchecking" still means "none". + nonePref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY unchecking resets the state to "none". + anyPref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + starredPref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + contactsPref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For priority conversations, we should set the conversations but not priority senders + impPref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + impPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + } + + @Test + public void testSettingsToSaveOnClick_callsCheck() { + int[] endState; + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + + // For calls: we should never set conversations, as this is unrelated to calls. + // For KEY_NONE senders should be none. + nonePref.setChecked(true); + endState = mCallsController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY senders should be ANY. + anyPref.setChecked(true); + endState = mCallsController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For [starred] contacts, we should set the priority senders accordingly + starredPref.setChecked(true); + endState = mCallsController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + contactsPref.setChecked(true); + endState = mCallsController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + } + + @Test + public void testSettingsToSaveOnClick_callsUncheck() { + int[] endState; + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + + // A calls setup should never set conversations settings. + // For KEY_NONE, "unchecking" still means "none". + nonePref.setChecked(false); + endState = mCallsController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(STATE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY unchecking resets the state to "none". + anyPref.setChecked(false); + endState = mCallsController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + starredPref.setChecked(false); + endState = mCallsController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + contactsPref.setChecked(false); + endState = mCallsController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + } + + @Test + public void testSettingsToSave_messages_noChange() { + int[] savedSettings; + + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + nonePref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + anyPref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + starredPref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + + contactsPref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference impPref = makePreference(KEY_IMPORTANT, true, true); + impPref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + impPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + } + + @Test + public void testSettingsToSave_calls_noChange() { + int[] savedSettings; + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, false, false); + + savedSettings = mMessagesController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, false, false); + + savedSettings = mMessagesController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, false, false); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, false, false); + savedSettings = mMessagesController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + + savedSettings = mMessagesController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceControllerTest.java new file mode 100644 index 00000000000..7bbb042c471 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceControllerTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; +import static android.service.notification.ZenPolicy.STATE_ALLOW; +import static android.service.notification.ZenPolicy.STATE_DISALLOW; +import static android.service.notification.ZenPolicy.STATE_UNSET; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; +import androidx.preference.TwoStatePreference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public final class ZenModeRepeatCallersPreferenceControllerTest { + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + } + + @Test + public void testUpdateState_allCalls() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowCalls(PEOPLE_TYPE_ANYONE) + .build()) + .build(), true); + + ZenModeRepeatCallersPreferenceController controller = + new ZenModeRepeatCallersPreferenceController(mContext, "repeat", mBackend, 1); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + verify(preference).setEnabled(false); + } + + @Test + public void testUpdateState_someCalls() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowCalls(PEOPLE_TYPE_STARRED) + .allowRepeatCallers(true) + .build()) + .build(), true); + + ZenModeRepeatCallersPreferenceController controller = + new ZenModeRepeatCallersPreferenceController(mContext, "repeat", mBackend, 1); + + controller.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + verify(preference).setEnabled(true); + } + + @Test + public void testOnPreferenceChange() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build()) + .build(), true); + + ZenModeRepeatCallersPreferenceController controller = + new ZenModeRepeatCallersPreferenceController(mContext, "repeat", mBackend, 1); + + controller.updateZenMode(preference, zenMode); + + controller.onPreferenceChange(preference, false); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + assertThat(captor.getValue().getPolicy().getPriorityCategoryRepeatCallers()) + .isEqualTo(STATE_DISALLOW); + assertThat(captor.getValue().getPolicy().getPriorityCategoryEvents()) + .isEqualTo(STATE_UNSET); + assertThat(captor.getValue().getPolicy().getPriorityCallSenders()) + .isEqualTo(STATE_UNSET); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java new file mode 100644 index 00000000000..67be82f9032 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.AutomaticZenRule; +import android.content.Context; +import android.net.Uri; +import android.service.notification.ZenPolicy; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class ZenModesSummaryHelperTest { + private Context mContext; + private ZenModesBackend mBackend; + + private ZenModeSummaryHelper mSummaryHelper; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mBackend = new ZenModesBackend(mContext); + mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); + } + + @Test + public void getPeopleSummary_noOne() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("No one can interrupt"); + } + + @Test + public void getPeopleSummary_some() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_CONTACTS).build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("Some people can interrupt"); + } + + @Test + public void getPeopleSummary_all() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_ANYONE). + allowConversations(CONVERSATION_SENDERS_ANYONE) + .allowMessages(PEOPLE_TYPE_ANYONE).build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("All people can interrupt"); + } + + @Test + public void getOtherSoundCategoriesSummary_single() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo( + "Alarms can interrupt"); + } + + @Test + public void getOtherSoundCategoriesSummary_duo() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).allowMedia(true).build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo( + "Alarms and media can interrupt"); + } + + @Test + public void getOtherSoundCategoriesSummary_trio() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowAlarms(true) + .allowMedia(true) + .allowSystem(true) + .build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo( + "Alarms, media, and touch sounds can interrupt"); + } + + @Test + public void getOtherSoundCategoriesSummary_quad() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowAlarms(true) + .allowMedia(true) + .allowSystem(true) + .allowReminders(true) + .build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo( + "Alarms, media, and 2 more can interrupt"); + } + + @Test + public void getOtherSoundCategoriesSummary_all() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowAlarms(true) + .allowMedia(true) + .allowSystem(true) + .allowReminders(true) + .allowEvents(true) + .build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo( + "Alarms, media, and 3 more can interrupt"); + } +} diff --git a/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java index 0dede096ce8..383ed94b7db 100644 --- a/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/system/FactoryResetPreferenceControllerTest.java @@ -145,10 +145,13 @@ public class FactoryResetPreferenceControllerTest { @Test @RequiresFlagsEnabled(com.android.settings.factory_reset.Flags.FLAG_ENABLE_FACTORY_RESET_WIZARD) - public void handlePreference_factoryResetWizardEnabled() { + public void handlePreference_factoryResetWizardEnabled() + throws PackageManager.NameNotFoundException { ArgumentCaptor intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class); assertThat(mController.handlePreferenceTreeClick(mPreference)).isTrue(); + verify(mPackageManager).getPackageInfo(eq(FACTORY_RESET_APP_PACKAGE), + eq(PackageManager.GET_PERMISSIONS)); verify(mFactoryResetLauncher).launch(intentArgumentCaptor.capture()); assertThat(intentArgumentCaptor.getValue()).isNotNull(); assertThat(intentArgumentCaptor.getValue().getAction()) diff --git a/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/FingerprintEnrollIntroScreenshotTest.kt b/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/FingerprintEnrollIntroScreenshotTest.kt index 40a23d7fd56..68d600b1298 100644 --- a/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/FingerprintEnrollIntroScreenshotTest.kt +++ b/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/fragment/FingerprintEnrollIntroScreenshotTest.kt @@ -28,7 +28,7 @@ import platform.test.screenshot.ViewScreenshotTestRule.Mode @RunWith(AndroidJUnit4::class) class FingerprintEnrollIntroScreenshotTest { - private val injector: Injector = Injector(FingerprintNavigationStep.Introduction) + private val injector: Injector = Injector(FingerprintNavigationStep.Introduction()) @Rule @JvmField diff --git a/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt index 863a6e2e97b..7e5f830d644 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/WifiControlAppListModelTest.kt @@ -17,14 +17,13 @@ package com.android.settings.spa.app.specialaccess import android.Manifest -import android.app.AppOpsManager import android.content.Context import android.content.pm.ApplicationInfo import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull -import com.android.settingslib.spaprivileged.model.app.IAppOpsController +import com.android.settingslib.spaprivileged.model.app.IAppOpsPermissionController import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.google.common.truth.Truth.assertThat @@ -117,14 +116,14 @@ class WifiControlAppListModelTest { app = APP_NOT_REQUEST_PERMISSION, hasRequestPermission = false, hasRequestBroaderPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val appRequestedNetworkSettingsRecord = AppOpPermissionRecord( app = APP_REQUESTED_NETWORK_SETTINGS, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val recordListFlow = @@ -144,7 +143,7 @@ class WifiControlAppListModelTest { app = APP, hasRequestPermission = false, hasRequestBroaderPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isAllowed = getIsAllowed(record) @@ -159,7 +158,7 @@ class WifiControlAppListModelTest { app = APP, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ALLOWED), + appOpsPermissionController = FakeAppOpsPermissionController(true), ) val isAllowed = getIsAllowed(record) @@ -174,7 +173,7 @@ class WifiControlAppListModelTest { app = APP, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_IGNORED), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isAllowed = getIsAllowed(record) @@ -189,7 +188,7 @@ class WifiControlAppListModelTest { app = APP, hasRequestPermission = false, hasRequestBroaderPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isChangeable = listModel.isChangeable(record) @@ -198,13 +197,13 @@ class WifiControlAppListModelTest { } @Test - fun isChangeable_notChangableWhenRequestedNetworkSettingPermissions() { + fun isChangeable_notChangeableWhenRequestedNetworkSettingPermissions() { val record = AppOpPermissionRecord( app = APP, hasRequestPermission = false, hasRequestBroaderPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isChangeable = listModel.isChangeable(record) @@ -213,13 +212,13 @@ class WifiControlAppListModelTest { } @Test - fun isChangeable_changableWhenRequestedChangeWifiStatePermission() { + fun isChangeable_changeableWhenRequestedChangeWifiStatePermission() { val record = AppOpPermissionRecord( app = APP, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) val isChangeable = listModel.isChangeable(record) @@ -229,18 +228,18 @@ class WifiControlAppListModelTest { @Test fun setAllowed_shouldCallController() { - val appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val appOpsPermissionController = FakeAppOpsPermissionController(false) val record = AppOpPermissionRecord( app = APP, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = appOpsController, + appOpsPermissionController = appOpsPermissionController, ) listModel.setAllowed(record = record, newAllowed = true) - assertThat(appOpsController.setAllowedCalledWith).isTrue() + assertThat(appOpsPermissionController.setAllowedCalledWith).isTrue() } private fun getIsAllowed(record: AppOpPermissionRecord): Boolean? { @@ -266,14 +265,12 @@ class WifiControlAppListModelTest { } } -private class FakeAppOpsController(private val fakeMode: Int) : IAppOpsController { +private class FakeAppOpsPermissionController(allowed: Boolean) : IAppOpsPermissionController { var setAllowedCalledWith: Boolean? = null - override val modeFlow = flowOf(fakeMode) + override val isAllowedFlow = flowOf(allowed) override fun setAllowed(allowed: Boolean) { setAllowedCalledWith = allowed } - - override fun getMode() = fakeMode } diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt index ec070fdde05..a5603931568 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt @@ -30,10 +30,10 @@ import com.android.media.flags.Flags import com.android.settings.R import com.android.settings.testutils.FakeFeatureFactory import com.android.settingslib.spaprivileged.model.app.AppOps -import com.android.settingslib.spaprivileged.model.app.IAppOpsController +import com.android.settingslib.spaprivileged.model.app.IAppOpsPermissionController import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import org.junit.Before import org.junit.Rule import org.junit.Test @@ -50,7 +50,7 @@ class MediaRoutingControlTest { @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() - @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule(); + @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule() @Spy private val context: Context = ApplicationProvider.getApplicationContext() @@ -86,29 +86,29 @@ class MediaRoutingControlTest { @Test fun setAllowed_callWithNewStatusAsTrue_shouldChangeAppControllerModeToAllowed() { - val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val fakeAppOpsPermissionController = FakeAppOpsPermissionController(false) val permissionRequestedRecord = AppOpPermissionRecord( app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = fakeAppOpController, + appOpsPermissionController = fakeAppOpsPermissionController, ) listModel.setAllowed(permissionRequestedRecord, true) - assertThat(fakeAppOpController.getMode()).isEqualTo(AppOpsManager.MODE_ALLOWED) + assertThat(fakeAppOpsPermissionController.setAllowedCalledWith).isTrue() } @Test fun setAllowed_callWithNewStatusAsTrue_shouldLogPermissionToggleActionAsAllowed() { - val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val fakeAppOpsPermissionController = FakeAppOpsPermissionController(false) val permissionRequestedRecord = AppOpPermissionRecord( app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = fakeAppOpController, + appOpsPermissionController = fakeAppOpsPermissionController, ) listModel.setAllowed(permissionRequestedRecord, true) @@ -119,29 +119,29 @@ class MediaRoutingControlTest { @Test fun setAllowed_callWithNewStatusAsFalse_shouldChangeAppControllerModeToErrored() { - val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val fakeAppOpsPermissionController = FakeAppOpsPermissionController(false) val permissionRequestedRecord = AppOpPermissionRecord( app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = fakeAppOpController, + appOpsPermissionController = fakeAppOpsPermissionController, ) listModel.setAllowed(permissionRequestedRecord, false) - assertThat(fakeAppOpController.getMode()).isEqualTo(AppOpsManager.MODE_ERRORED) + assertThat(fakeAppOpsPermissionController.setAllowedCalledWith).isFalse() } @Test fun setAllowed_callWithNewStatusAsFalse_shouldLogPermissionToggleActionAsDenied() { - val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) + val fakeAppOpsPermissionController = FakeAppOpsPermissionController(false) val permissionRequestedRecord = AppOpPermissionRecord( app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = fakeAppOpController, + appOpsPermissionController = fakeAppOpsPermissionController, ) listModel.setAllowed(permissionRequestedRecord, false) @@ -158,8 +158,7 @@ class MediaRoutingControlTest { app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = - FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)) .thenReturn(listOf(PACKAGE_NAME)) @@ -177,8 +176,7 @@ class MediaRoutingControlTest { app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = false, hasRequestBroaderPermission = false, - appOpsController = - FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)) .thenReturn(listOf(PACKAGE_NAME)) @@ -196,8 +194,7 @@ class MediaRoutingControlTest { app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = - FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)) .thenReturn(listOf("other.package.name")) @@ -215,8 +212,7 @@ class MediaRoutingControlTest { app = ApplicationInfo().apply { packageName = PACKAGE_NAME }, hasRequestPermission = true, hasRequestBroaderPermission = false, - appOpsController = - FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + appOpsPermissionController = FakeAppOpsPermissionController(false), ) whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)) .thenReturn(listOf(PACKAGE_NAME)) @@ -226,15 +222,14 @@ class MediaRoutingControlTest { assertThat(isSpecialAccessChangeable).isFalse() } - private class FakeAppOpsController(fakeMode: Int) : IAppOpsController { + private class FakeAppOpsPermissionController(allowed: Boolean) : IAppOpsPermissionController { + var setAllowedCalledWith: Boolean? = null - override val modeFlow = MutableStateFlow(fakeMode) + override val isAllowedFlow = flowOf(allowed) override fun setAllowed(allowed: Boolean) { - modeFlow.value = if (allowed) AppOpsManager.MODE_ALLOWED else AppOpsManager.MODE_ERRORED + setAllowedCalledWith = allowed } - - override fun getMode(): Int = modeFlow.value } companion object { diff --git a/tests/unit/src/com/android/settings/network/UiccSlotUtilTest.java b/tests/unit/src/com/android/settings/network/UiccSlotUtilTest.java index 1a895b3d0fd..2aa15733e45 100644 --- a/tests/unit/src/com/android/settings/network/UiccSlotUtilTest.java +++ b/tests/unit/src/com/android/settings/network/UiccSlotUtilTest.java @@ -41,7 +41,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -754,12 +753,11 @@ public class UiccSlotUtilTest { } @Test - @Ignore("b/337417975") public void onReceiveSimCardStateChangeReceiver_receiveAction_timerCountDown() { CountDownLatch latch = spy(new CountDownLatch(1)); UiccSlotUtil.SimCardStateChangeReceiver receive = new UiccSlotUtil.SimCardStateChangeReceiver(latch); - Intent intent = new Intent(TelephonyManager.ACTION_SIM_SLOT_STATUS_CHANGED); + Intent intent = new Intent(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED); intent.putExtra(TelephonyManager.EXTRA_SIM_STATE, TelephonyManager.SIM_STATE_PRESENT); receive.onReceive(mContext, intent);