From e43c42e211414133c9a129c814921147dc47c021 Mon Sep 17 00:00:00 2001 From: Richard MacGregor Date: Mon, 25 Sep 2023 14:37:39 -0700 Subject: [PATCH 1/7] Cleanup privacy hub related flags Remove privacy_hub_enabled, which has been default true Flag: NONE removing default on legacy flag Bug: 301647969 Test: manual Change-Id: Icaea6253298efaa3444fa4142ce42ffefd159fd5 --- .../settings/privacy/PrivacyHubPreferenceController.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/com/android/settings/privacy/PrivacyHubPreferenceController.java b/src/com/android/settings/privacy/PrivacyHubPreferenceController.java index 20e5290bcf9..4c59f804a2e 100644 --- a/src/com/android/settings/privacy/PrivacyHubPreferenceController.java +++ b/src/com/android/settings/privacy/PrivacyHubPreferenceController.java @@ -18,7 +18,6 @@ package com.android.settings.privacy; import android.content.Context; import android.content.Intent; -import android.provider.DeviceConfig; import androidx.annotation.NonNull; import androidx.preference.Preference; @@ -30,7 +29,6 @@ import com.android.settings.core.BasePreferenceController; * The preference controller for privacy hub top level preference. */ public final class PrivacyHubPreferenceController extends BasePreferenceController { - public static final String PROPERTY_PRIVACY_HUB_ENABLED = "privacy_hub_enabled"; public PrivacyHubPreferenceController(@NonNull Context context, @NonNull String key) { super(context, key); @@ -38,8 +36,7 @@ public final class PrivacyHubPreferenceController extends BasePreferenceControll @Override public int getAvailabilityStatus() { - return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, - PROPERTY_PRIVACY_HUB_ENABLED, true) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + return AVAILABLE; } @Override From dc9701f427ec922b9161d3cf5b5638a06fd72e8e Mon Sep 17 00:00:00 2001 From: yyalan Date: Wed, 12 Jun 2024 14:52:18 +0000 Subject: [PATCH 2/7] Launch oobe from Settings Bug: 344860480 Flag: com.android.systemui.shared.new_touchpad_gestures_tutorial Test: Manual Change-Id: Iabc155d2f99717c6489308ffb1e63ef4e59f0332 --- Android.bp | 1 + ...TouchGesturesButtonPreferenceController.java | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Android.bp b/Android.bp index cb898bef834..bdc9d376c9b 100644 --- a/Android.bp +++ b/Android.bp @@ -85,6 +85,7 @@ android_library { "notification_flags_lib", "securebox", "android.os.flags-aconfig-java", + "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", // Settings dependencies "FingerprintManagerInteractor", diff --git a/src/com/android/settings/inputmethod/TouchGesturesButtonPreferenceController.java b/src/com/android/settings/inputmethod/TouchGesturesButtonPreferenceController.java index 648a3e62f03..245901e16d3 100644 --- a/src/com/android/settings/inputmethod/TouchGesturesButtonPreferenceController.java +++ b/src/com/android/settings/inputmethod/TouchGesturesButtonPreferenceController.java @@ -16,8 +16,11 @@ package com.android.settings.inputmethod; +import static com.android.systemui.shared.Flags.newTouchpadGesturesTutorial; + import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.Intent; import android.util.FeatureFlagUtils; import androidx.fragment.app.Fragment; @@ -34,6 +37,7 @@ public class TouchGesturesButtonPreferenceController extends BasePreferenceContr private static final int ORDER_BOTTOM = 100; private static final String PREFERENCE_KEY = "trackpad_touch_gesture"; private static final String GESTURE_DIALOG_TAG = "GESTURE_DIALOG_TAG"; + private static final String TUTORIAL_ACTION = "com.android.systemui.action.TOUCHPAD_TUTORIAL"; private Fragment mParent; private MetricsFeatureProvider mMetricsFeatureProvider; @@ -73,8 +77,15 @@ public class TouchGesturesButtonPreferenceController extends BasePreferenceContr private void showTouchpadGestureEducation() { mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_LEARN_TOUCHPAD_GESTURE_CLICK); - TrackpadGestureDialogFragment fragment = new TrackpadGestureDialogFragment(); - fragment.setTargetFragment(mParent, 0); - fragment.show(mParent.getActivity().getSupportFragmentManager(), GESTURE_DIALOG_TAG); + if (newTouchpadGesturesTutorial()) { + Intent intent = new Intent(TUTORIAL_ACTION); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + } else { + TrackpadGestureDialogFragment fragment = new TrackpadGestureDialogFragment(); + fragment.setTargetFragment(mParent, 0); + fragment.show(mParent.getActivity().getSupportFragmentManager(), GESTURE_DIALOG_TAG); + } } } From 049de84f2deeb77bf89e3b5182e128422aca4aa8 Mon Sep 17 00:00:00 2001 From: Diya Bera Date: Fri, 28 Jun 2024 16:55:57 +0000 Subject: [PATCH 3/7] Add mandatory biometric prompt to platform surfaces (1/N) 1. Face settings 2. Fingerprint settings 3. Change device credential Flag: android.hardware.biometrics.Flags.MANDATORY_BIOMETRICS Bug: 339910718 Test: atest UtilsTest Change-Id: I69778d1733ea9fb312e7c26ae0fa23b6008dde5d --- res/values/strings.xml | 3 +- src/com/android/settings/Utils.java | 54 +++++++++++++ .../biometrics/BiometricEnrollBase.java | 3 + .../combination/BiometricsSettingsBase.java | 41 ++++++++++ .../biometrics/face/FaceSettings.java | 23 ++++++ .../fingerprint/FingerprintSettings.java | 34 ++++++++ .../settings/password/BiometricFragment.java | 14 +++- .../settings/password/ChooseLockGeneric.java | 13 +++ .../ConfirmDeviceCredentialActivity.java | 13 ++- .../src/com/android/settings/UtilsTest.java | 80 +++++++++++++++++++ 10 files changed, 275 insertions(+), 3 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 075056db64e..d152c7c1f82 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -932,7 +932,8 @@ Face, fingerprint, and %s added Face, fingerprints, and %s added - + + This is needed since Identity Check is on Remote Authenticator Unlock diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index 7ed6ba0969e..27a628c5308 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -24,6 +24,9 @@ import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH; import static android.text.format.DateUtils.FORMAT_SHOW_DATE; +import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_AUTHENTICATORS; +import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT; + import android.app.ActionBar; import android.app.Activity; import android.app.ActivityManager; @@ -54,6 +57,7 @@ import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.VectorDrawable; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.SensorProperties; import android.hardware.face.Face; import android.hardware.face.FaceManager; @@ -122,6 +126,7 @@ import com.android.settings.dashboard.profileselector.ProfileFragmentBridge; import com.android.settings.dashboard.profileselector.ProfileSelectFragment; import com.android.settings.dashboard.profileselector.ProfileSelectFragment.ProfileType; import com.android.settings.password.ChooseLockSettingsHelper; +import com.android.settings.password.ConfirmDeviceCredentialActivity; import com.android.settingslib.widget.ActionBarShadowController; import com.android.settingslib.widget.AdaptiveIcon; @@ -1478,6 +1483,55 @@ public final class Utils extends com.android.settingslib.Utils { disableComponent(pm, new ComponentName(context, Settings.CreateShortcutActivity.class)); } + /** + * Request biometric authentication if all requirements for mandatory biometrics is satisfied. + * @param context of the corresponding activity/fragment + * @param biometricsSuccessfullyAuthenticated if the user has already authenticated using + * biometrics + * @param biometricsAuthenticationRequested if the activity/fragment has already requested for + * biometric prompt + * @return true if all requirements for mandatory biometrics is satisfied + */ + public static boolean requestBiometricAuthenticationForMandatoryBiometrics( + @NonNull Context context, + boolean biometricsSuccessfullyAuthenticated, + boolean biometricsAuthenticationRequested) { + final BiometricManager biometricManager = context.getSystemService(BiometricManager.class); + if (biometricManager == null) { + Log.e(TAG, "Biometric Manager is null."); + return false; + } + final int status = biometricManager.canAuthenticate( + BiometricManager.Authenticators.MANDATORY_BIOMETRICS); + return android.hardware.biometrics.Flags.mandatoryBiometrics() + && status == BiometricManager.BIOMETRIC_SUCCESS + && !biometricsSuccessfullyAuthenticated + && !biometricsAuthenticationRequested; + } + + /** + * Launch biometric prompt for mandatory biometrics. Call + * {@link #requestBiometricAuthenticationForMandatoryBiometrics(Context, boolean, boolean)} + * to check if all requirements for mandatory biometrics is satisfied + * before launching biometric prompt. + * + * @param fragment corresponding fragment of the surface + * @param requestCode for starting the new activity + */ + public static void launchBiometricPromptForMandatoryBiometrics(@NonNull Fragment fragment, + int requestCode) { + final Intent intent = new Intent(); + intent.putExtra(BIOMETRIC_PROMPT_AUTHENTICATORS, + BiometricManager.Authenticators.MANDATORY_BIOMETRICS); + intent.putExtra(BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT, + fragment.getString(R.string.cancel)); + intent.putExtra(KeyguardManager.EXTRA_DESCRIPTION, + fragment.getString(R.string.mandatory_biometrics_prompt_description)); + intent.setClassName(SETTINGS_PACKAGE_NAME, + ConfirmDeviceCredentialActivity.class.getName()); + fragment.startActivityForResult(intent, requestCode); + } + private static void disableComponent(PackageManager pm, ComponentName componentName) { pm.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); diff --git a/src/com/android/settings/biometrics/BiometricEnrollBase.java b/src/com/android/settings/biometrics/BiometricEnrollBase.java index 335d0b9dd99..37ada236343 100644 --- a/src/com/android/settings/biometrics/BiometricEnrollBase.java +++ b/src/com/android/settings/biometrics/BiometricEnrollBase.java @@ -68,6 +68,8 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity { public static final String EXTRA_FINISHED_ENROLL_FACE = "finished_enrolling_face"; public static final String EXTRA_FINISHED_ENROLL_FINGERPRINT = "finished_enrolling_fingerprint"; public static final String EXTRA_LAUNCHED_POSTURE_GUIDANCE = "launched_posture_guidance"; + public static final String EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY = + "biometrics_authenticated_successfully"; /** * Used by the choose fingerprint wizard to indicate the wizard is @@ -115,6 +117,7 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity { public static final int LEARN_MORE_REQUEST = 3; public static final int CONFIRM_REQUEST = 4; public static final int ENROLL_REQUEST = 5; + public static final int BIOMETRIC_AUTH_REQUEST = 6; /** * Request code when starting another biometric enrollment from within a biometric flow. For diff --git a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java index b17478881fe..caa7327394a 100644 --- a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java +++ b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java @@ -65,6 +65,7 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { static final int CONFIRM_REQUEST = 2001; private static final int CHOOSE_LOCK_REQUEST = 2002; protected static final int ACTIVE_UNLOCK_REQUEST = 2003; + private static final int BIOMETRIC_AUTH_REQUEST = 2004; private static final String SAVE_STATE_CONFIRM_CREDETIAL = "confirm_credential"; private static final String DO_NOT_FINISH_ACTIVITY = "do_not_finish_activity"; @@ -72,10 +73,15 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { static final String RETRY_PREFERENCE_KEY = "retry_preference_key"; @VisibleForTesting static final String RETRY_PREFERENCE_BUNDLE = "retry_preference_bundle"; + private static final String BIOMETRICS_AUTH_REQUESTED = "biometrics_auth_requested"; + private static final String BIOMETRICS_AUTHENTICATED_SUCCESSFULLY = + "biometrics_authenticated_successfully"; protected int mUserId; protected long mGkPwHandle; private boolean mConfirmCredential; + private boolean mBiometricsAuthenticationRequested; + private boolean mBiometricsSuccessfullyAuthenticated; @Nullable private FaceManager mFaceManager; @Nullable private FingerprintManager mFingerprintManager; // Do not finish() if choosing/confirming credential, showing fp/face settings, or launching @@ -113,6 +119,9 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { mGkPwHandle = BiometricUtils.getGatekeeperPasswordHandle(getIntent()); } + mBiometricsSuccessfullyAuthenticated = getIntent().getBooleanExtra( + BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, false); + if (savedInstanceState != null) { mConfirmCredential = savedInstanceState.getBoolean(SAVE_STATE_CONFIRM_CREDETIAL); mDoNotFinishActivity = savedInstanceState.getBoolean(DO_NOT_FINISH_ACTIVITY); @@ -123,11 +132,20 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { mGkPwHandle = savedInstanceState.getLong( ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE); } + mBiometricsAuthenticationRequested = savedInstanceState.getBoolean( + BIOMETRICS_AUTH_REQUESTED); + mBiometricsSuccessfullyAuthenticated = savedInstanceState.getBoolean( + BIOMETRICS_AUTHENTICATED_SUCCESSFULLY); } if (mGkPwHandle == 0L && !mConfirmCredential) { mConfirmCredential = true; launchChooseOrConfirmLock(); + } else if (Utils.requestBiometricAuthenticationForMandatoryBiometrics( + getActivity(), mBiometricsSuccessfullyAuthenticated, + mBiometricsAuthenticationRequested)) { + mBiometricsAuthenticationRequested = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST); } updateUnlockPhonePreferenceSummary(); @@ -141,6 +159,12 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { @Override public void onResume() { super.onResume(); + if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), + mBiometricsSuccessfullyAuthenticated, mBiometricsAuthenticationRequested) + && mGkPwHandle != 0L) { + mBiometricsAuthenticationRequested = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST); + } if (!mConfirmCredential) { mDoNotFinishActivity = false; } @@ -177,6 +201,9 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); extras.putInt(BiometricEnrollBase.EXTRA_KEY_SENSOR_ID, sensorId); extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge); + extras.putBoolean( + BiometricEnrollBase.EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, + mBiometricsSuccessfullyAuthenticated); onFaceOrFingerprintPreferenceTreeClick(preference); } catch (IllegalStateException e) { if (retry) { @@ -206,6 +233,9 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { final Bundle extras = preference.getExtras(); extras.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token); extras.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge); + extras.putBoolean( + BiometricEnrollBase.EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, + mBiometricsSuccessfullyAuthenticated); onFaceOrFingerprintPreferenceTreeClick(preference); } catch (IllegalStateException e) { if (retry) { @@ -288,6 +318,10 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { outState.putString(RETRY_PREFERENCE_KEY, mRetryPreferenceKey); outState.putBundle(RETRY_PREFERENCE_BUNDLE, mRetryPreferenceExtra); } + outState.putBoolean(BIOMETRICS_AUTH_REQUESTED, + mBiometricsAuthenticationRequested); + outState.putBoolean(BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, + mBiometricsSuccessfullyAuthenticated); } @Override @@ -315,6 +349,13 @@ public abstract class BiometricsSettingsBase extends DashboardFragment { } mRetryPreferenceKey = null; mRetryPreferenceExtra = null; + } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { + mBiometricsAuthenticationRequested = false; + if (resultCode == RESULT_OK) { + mBiometricsSuccessfullyAuthenticated = true; + } else { + finish(); + } } } diff --git a/src/com/android/settings/biometrics/face/FaceSettings.java b/src/com/android/settings/biometrics/face/FaceSettings.java index 8884ce36253..2a0dd83a491 100644 --- a/src/com/android/settings/biometrics/face/FaceSettings.java +++ b/src/com/android/settings/biometrics/face/FaceSettings.java @@ -20,8 +20,10 @@ import static android.app.Activity.RESULT_OK; import static android.app.admin.DevicePolicyResources.Strings.Settings.FACE_SETTINGS_FOR_WORK_TITLE; import static com.android.settings.Utils.isPrivateProfile; +import static com.android.settings.biometrics.BiometricEnrollBase.BIOMETRIC_AUTH_REQUEST; import static com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST; import static com.android.settings.biometrics.BiometricEnrollBase.ENROLL_REQUEST; +import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY; import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED; import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT; @@ -66,6 +68,8 @@ public class FaceSettings extends DashboardFragment { private static final String TAG = "FaceSettings"; private static final String KEY_TOKEN = "hw_auth_token"; private static final String KEY_RE_ENROLL_FACE = "re_enroll_face_unlock"; + private static final String KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED = + "biometrics_successfully_authenticated"; private static final String PREF_KEY_DELETE_FACE_DATA = "security_settings_face_delete_faces_container"; @@ -93,6 +97,8 @@ public class FaceSettings extends DashboardFragment { private FaceFeatureProvider mFaceFeatureProvider; private boolean mConfirmingPassword; + private boolean mBiometricsAuthenticationRequested; + private boolean mBiometricsSuccessfullyAuthenticated; private final FaceSettingsRemoveButtonPreferenceController.Listener mRemovalListener = () -> { @@ -144,6 +150,8 @@ public class FaceSettings extends DashboardFragment { public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putByteArray(KEY_TOKEN, mToken); + outState.putBoolean(KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED, + mBiometricsSuccessfullyAuthenticated); } @Override @@ -163,6 +171,8 @@ public class FaceSettings extends DashboardFragment { mToken = getIntent().getByteArrayExtra(KEY_TOKEN); mSensorId = getIntent().getIntExtra(BiometricEnrollBase.EXTRA_KEY_SENSOR_ID, -1); mChallenge = getIntent().getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, 0L); + mBiometricsSuccessfullyAuthenticated = getIntent().getBooleanExtra( + EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, false); mUserId = getActivity().getIntent().getIntExtra( Intent.EXTRA_USER_ID, UserHandle.myUserId()); @@ -231,6 +241,8 @@ public class FaceSettings extends DashboardFragment { if (savedInstanceState != null) { mToken = savedInstanceState.getByteArray(KEY_TOKEN); + mBiometricsSuccessfullyAuthenticated = savedInstanceState.getBoolean( + KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED); } } @@ -276,6 +288,10 @@ public class FaceSettings extends DashboardFragment { Log.e(TAG, "Password not set"); finish(); } + } else if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), + mBiometricsSuccessfullyAuthenticated, mBiometricsAuthenticationRequested)) { + mBiometricsAuthenticationRequested = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST); } else { mAttentionController.setToken(mToken); mEnrollController.setToken(mToken); @@ -318,6 +334,13 @@ public class FaceSettings extends DashboardFragment { setResult(resultCode, data); finish(); } + } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { + mBiometricsAuthenticationRequested = false; + if (resultCode == RESULT_OK) { + mBiometricsSuccessfullyAuthenticated = true; + } else { + finish(); + } } } diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java index db87c0facac..e30a3b637f1 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java @@ -23,6 +23,7 @@ import static android.app.admin.DevicePolicyResources.UNDEFINED; import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; import static com.android.settings.Utils.isPrivateProfile; +import static com.android.settings.biometrics.BiometricEnrollBase.BIOMETRIC_AUTH_REQUEST; import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY; import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE; @@ -218,6 +219,10 @@ public class FingerprintSettings extends SubSettings { "security_settings_fingerprint_unlock_category"; private static final String KEY_FINGERPRINT_UNLOCK_FOOTER = "security_settings_fingerprint_footer"; + private static final String KEY_BIOMETRICS_AUTHENTICATION_REQUESTED = + "biometrics_authentication_requested"; + private static final String KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED = + "biometrics_successfully_authenticated"; private static final int MSG_REFRESH_FINGERPRINT_TEMPLATES = 1000; private static final int MSG_FINGER_AUTH_SUCCESS = 1001; @@ -251,6 +256,8 @@ public class FingerprintSettings extends SubSettings { private boolean mInFingerprintLockout; private byte[] mToken; private boolean mLaunchedConfirm; + private boolean mBiometricsAuthenticationRequested; + private boolean mBiometricsSuccessfullyAuthenticated; private boolean mHasFirstEnrolled = true; private Drawable mHighlightDrawable; private int mUserId; @@ -418,6 +425,8 @@ public class FingerprintSettings extends SubSettings { ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN); mChallenge = activity.getIntent() .getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L); + mBiometricsSuccessfullyAuthenticated = getIntent().getBooleanExtra( + BiometricEnrollBase.EXTRA_BIOMETRICS_AUTHENTICATED_SUCCESSFULLY, false); mAuthenticateSidecar = (FingerprintAuthenticateSidecar) getFragmentManager().findFragmentByTag(TAG_AUTHENTICATE_SIDECAR); @@ -459,6 +468,10 @@ public class FingerprintSettings extends SubSettings { mIsEnrolling = savedInstanceState.getBoolean(KEY_IS_ENROLLING, mIsEnrolling); mHasFirstEnrolled = savedInstanceState.getBoolean(KEY_HAS_FIRST_ENROLLED, mHasFirstEnrolled); + mBiometricsSuccessfullyAuthenticated = savedInstanceState.getBoolean( + KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED); + mBiometricsAuthenticationRequested = savedInstanceState.getBoolean( + KEY_BIOMETRICS_AUTHENTICATION_REQUESTED); } // (mLaunchedConfirm or mIsEnrolling) means that we are waiting an activity result. @@ -467,6 +480,10 @@ public class FingerprintSettings extends SubSettings { if (mToken == null) { mLaunchedConfirm = true; launchChooseOrConfirmLock(); + } else if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), + mBiometricsSuccessfullyAuthenticated, mBiometricsAuthenticationRequested)) { + mBiometricsAuthenticationRequested = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST); } else if (!mHasFirstEnrolled) { mIsEnrolling = true; addFirstFingerprint(null); @@ -746,6 +763,12 @@ public class FingerprintSettings extends SubSettings { mCalibrator = FeatureFactory.getFeatureFactory().getFingerprintFeatureProvider() .getUdfpsEnrollCalibrator(getActivity().getApplicationContext(), null, null); + + if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getActivity(), + mBiometricsSuccessfullyAuthenticated, mBiometricsAuthenticationRequested)) { + mBiometricsAuthenticationRequested = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST); + } } private void updatePreferences() { @@ -793,6 +816,10 @@ public class FingerprintSettings extends SubSettings { outState.putSerializable("mFingerprintsRenaming", mFingerprintsRenaming); outState.putBoolean(KEY_IS_ENROLLING, mIsEnrolling); outState.putBoolean(KEY_HAS_FIRST_ENROLLED, mHasFirstEnrolled); + outState.putBoolean(KEY_BIOMETRICS_AUTHENTICATION_REQUESTED, + mBiometricsAuthenticationRequested); + outState.putBoolean(KEY_BIOMETRICS_SUCCESSFULLY_AUTHENTICATED, + mBiometricsSuccessfullyAuthenticated); } @Override @@ -1013,6 +1040,13 @@ public class FingerprintSettings extends SubSettings { mIsEnrolling = false; mHasFirstEnrolled = true; updateAddPreference(); + } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { + mBiometricsAuthenticationRequested = false; + if (resultCode == RESULT_OK) { + mBiometricsSuccessfullyAuthenticated = true; + } else { + finish(); + } } } diff --git a/src/com/android/settings/password/BiometricFragment.java b/src/com/android/settings/password/BiometricFragment.java index 02f5b861ea3..a7a039e8485 100644 --- a/src/com/android/settings/password/BiometricFragment.java +++ b/src/com/android/settings/password/BiometricFragment.java @@ -16,8 +16,11 @@ package com.android.settings.password; +import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED; + import android.app.settings.SettingsEnums; import android.content.ComponentName; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback; import android.hardware.biometrics.BiometricPrompt.AuthenticationResult; @@ -137,7 +140,7 @@ public class BiometricFragment extends InstrumentedFragment { BiometricPrompt.Builder promptBuilder = new BiometricPrompt.Builder(getContext()) .setTitle(promptInfo.getTitle()) .setUseDefaultTitle() // use default title if title is null/empty - .setDeviceCredentialAllowed(true) + .setAllowedAuthenticators(promptInfo.getAuthenticators()) .setSubtitle(promptInfo.getSubtitle()) .setDescription(promptInfo.getDescription()) .setTextForDeviceCredential( @@ -170,6 +173,15 @@ public class BiometricFragment extends InstrumentedFragment { if (promptInfo.isUseDefaultSubtitle()) { promptBuilder.setUseDefaultSubtitle(); } + + if ((promptInfo.getAuthenticators() + & BiometricManager.Authenticators.DEVICE_CREDENTIAL) == 0) { + promptBuilder.setNegativeButton(promptInfo.getNegativeButtonText(), + getContext().getMainExecutor(), + (dialog, which) -> mAuthenticationCallback.onAuthenticationError( + BIOMETRIC_ERROR_USER_CANCELED, + null /* errString */)); + } mBiometricPrompt = promptBuilder.build(); } diff --git a/src/com/android/settings/password/ChooseLockGeneric.java b/src/com/android/settings/password/ChooseLockGeneric.java index ce9a5667dfc..4c18309384c 100644 --- a/src/com/android/settings/password/ChooseLockGeneric.java +++ b/src/com/android/settings/password/ChooseLockGeneric.java @@ -160,11 +160,13 @@ public class ChooseLockGeneric extends SettingsActivity { static final int CHOOSE_LOCK_BEFORE_BIOMETRIC_REQUEST = 103; @VisibleForTesting static final int SKIP_FINGERPRINT_REQUEST = 104; + private static final int BIOMETRIC_AUTH_REQUEST = 105; private LockPatternUtils mLockPatternUtils; private DevicePolicyManager mDpm; private boolean mRequestGatekeeperPasswordHandle = false; private boolean mPasswordConfirmed = false; + private boolean mBiometricsAuthSuccessful = false; private boolean mWaitingForConfirmation = false; private boolean mWaitingForActivityResult = false; private LockscreenCredential mUserPassword; @@ -488,6 +490,17 @@ public class ChooseLockGeneric extends SettingsActivity { ? data.getParcelableExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD) : null; updatePreferencesOrFinish(false /* isRecreatingActivity */); + if (Utils.requestBiometricAuthenticationForMandatoryBiometrics(getContext(), + mBiometricsAuthSuccessful, mWaitingForConfirmation)) { + mWaitingForConfirmation = true; + Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST); + } + } else if (requestCode == BIOMETRIC_AUTH_REQUEST) { + if (resultCode == Activity.RESULT_OK) { + mBiometricsAuthSuccessful = true; + } else { + finish(); + } } else if (requestCode == CHOOSE_LOCK_REQUEST) { if (resultCode != RESULT_CANCELED) { getActivity().setResult(resultCode, data); diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java index 7f362c32904..c0b3093c2f8 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java @@ -17,10 +17,10 @@ package com.android.settings.password; +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED; import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_WORK_PROFILE_PASSWORD_HEADER; import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_WORK_PROFILE_PATTERN_HEADER; import static android.app.admin.DevicePolicyResources.Strings.Settings.CONFIRM_WORK_PROFILE_PIN_HEADER; -import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; import static com.android.systemui.biometrics.Utils.toBitmap; @@ -40,6 +40,7 @@ import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Color; import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.BiometricPrompt; import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback; import android.hardware.biometrics.PromptInfo; @@ -76,6 +77,9 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity { /** Use this extra value to provide a custom logo description for the biometric prompt. **/ public static final String CUSTOM_BIOMETRIC_PROMPT_LOGO_DESCRIPTION_KEY = "custom_logo_description"; + public static final String BIOMETRIC_PROMPT_AUTHENTICATORS = "biometric_prompt_authenticators"; + public static final String BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT = + "biometric_prompt_negative_button_text"; public static class InternalActivity extends ConfirmDeviceCredentialActivity { } @@ -177,6 +181,11 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity { mDetails = intent.getCharSequenceExtra(KeyguardManager.EXTRA_DESCRIPTION); String alternateButton = intent.getStringExtra( KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL); + final int authenticators = intent.getIntExtra(BIOMETRIC_PROMPT_AUTHENTICATORS, + BiometricManager.Authenticators.DEVICE_CREDENTIAL + | BiometricManager.Authenticators.BIOMETRIC_WEAK); + final String negativeButtonText = intent.getStringExtra( + BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT); final boolean frp = KeyguardManager.ACTION_CONFIRM_FRP_CREDENTIAL.equals(intent.getAction()); final boolean repairMode = @@ -213,6 +222,8 @@ public class ConfirmDeviceCredentialActivity extends FragmentActivity { promptInfo.setTitle(mTitle); promptInfo.setDescription(mDetails); promptInfo.setDisallowBiometricsIfPolicyExists(mCheckDevicePolicyManager); + promptInfo.setAuthenticators(authenticators); + promptInfo.setNegativeButtonText(negativeButtonText); if (android.multiuser.Flags.enablePrivateSpaceFeatures() && android.multiuser.Flags.usePrivateSpaceIconInBiometricPrompt() diff --git a/tests/robotests/src/com/android/settings/UtilsTest.java b/tests/robotests/src/com/android/settings/UtilsTest.java index 0c57b014506..77de7496046 100644 --- a/tests/robotests/src/com/android/settings/UtilsTest.java +++ b/tests/robotests/src/com/android/settings/UtilsTest.java @@ -20,6 +20,10 @@ import static android.hardware.biometrics.SensorProperties.STRENGTH_CONVENIENCE; import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG; import static android.hardware.biometrics.SensorProperties.STRENGTH_WEAK; +import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; +import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_AUTHENTICATORS; +import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; @@ -35,10 +39,12 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActionBar; +import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyResourcesManager; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.UserInfo; @@ -47,6 +53,8 @@ import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.VectorDrawable; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.Flags; import android.hardware.face.FaceManager; import android.hardware.face.FaceSensorProperties; import android.hardware.face.FaceSensorPropertiesInternal; @@ -61,21 +69,28 @@ import android.os.UserManager; import android.os.storage.DiskInfo; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.IconDrawableFactory; import android.widget.EditText; import android.widget.ScrollView; import android.widget.TextView; import androidx.core.graphics.drawable.IconCompat; +import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import com.android.internal.widget.LockPatternUtils; +import com.android.settings.password.ConfirmDeviceCredentialActivity; import com.android.settings.testutils.shadow.ShadowLockPatternUtils; import org.junit.After; 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.Robolectric; @@ -92,6 +107,9 @@ import java.util.List; @Config(shadows = ShadowLockPatternUtils.class) public class UtilsTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final String PACKAGE_NAME = "com.android.app"; private static final int USER_ID = 1; @@ -113,6 +131,11 @@ public class UtilsTest { private IconDrawableFactory mIconDrawableFactory; @Mock private ApplicationInfo mApplicationInfo; + @Mock + private BiometricManager mBiometricManager; + @Mock + private Fragment mFragment; + private Context mContext; private UserManager mUserManager; private static final int FLAG_SYSTEM = 0x00000000; @@ -128,6 +151,7 @@ public class UtilsTest { when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)) .thenReturn(connectivityManager); when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mContext.getSystemService(BiometricManager.class)).thenReturn(mBiometricManager); } @After @@ -503,6 +527,62 @@ public class UtilsTest { assertThat(Utils.isFaceNotConvenienceBiometric(mContext)).isFalse(); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testRequestBiometricAuthentication_biometricManagerNull_shouldReturnFalse() { + when(mContext.getSystemService(Context.BIOMETRIC_SERVICE)).thenReturn(null); + assertThat(Utils.requestBiometricAuthenticationForMandatoryBiometrics(mContext, + false /* biometricsSuccessfullyAuthenticated */, + false /* biometricsAuthenticationRequested */)).isFalse(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testRequestBiometricAuthentication_biometricManagerReturnsSuccess_shouldReturnTrue() + throws InterruptedException { + when(mBiometricManager.canAuthenticate( + BiometricManager.Authenticators.MANDATORY_BIOMETRICS)) + .thenReturn(BiometricManager.BIOMETRIC_SUCCESS); + boolean requestBiometricAuthenticationForMandatoryBiometrics = + Utils.requestBiometricAuthenticationForMandatoryBiometrics(mContext, + true /* biometricsSuccessfullyAuthenticated */, + false /* biometricsAuthenticationRequested */); + assertThat(requestBiometricAuthenticationForMandatoryBiometrics).isFalse(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testRequestBiometricAuthentication_biometricManagerReturnsError_shouldReturnFalse() { + when(mBiometricManager.canAuthenticate( + BiometricManager.Authenticators.MANDATORY_BIOMETRICS)) + .thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE); + assertThat(Utils.requestBiometricAuthenticationForMandatoryBiometrics(mContext, + false /* biometricsSuccessfullyAuthenticated */, + false /* biometricsAuthenticationRequested */)).isFalse(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_MANDATORY_BIOMETRICS) + public void testLaunchBiometricPrompt_checkIntentValues() { + when(mFragment.getContext()).thenReturn(mContext); + + final int requestCode = 1; + final ArgumentCaptor intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class); + Utils.launchBiometricPromptForMandatoryBiometrics(mFragment, requestCode); + + verify(mFragment).startActivityForResult(intentArgumentCaptor.capture(), eq(requestCode)); + + final Intent intent = intentArgumentCaptor.getValue(); + + assertThat(intent.getExtra(BIOMETRIC_PROMPT_AUTHENTICATORS)).isEqualTo( + BiometricManager.Authenticators.MANDATORY_BIOMETRICS); + assertThat(intent.getExtra(BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT)).isNotNull(); + assertThat(intent.getExtra(KeyguardManager.EXTRA_DESCRIPTION)).isNotNull(); + assertThat(intent.getComponent().getPackageName()).isEqualTo(SETTINGS_PACKAGE_NAME); + assertThat(intent.getComponent().getClassName()).isEqualTo( + ConfirmDeviceCredentialActivity.class.getName()); + } + private void setUpForConfirmCredentialString(boolean isEffectiveUserManagedProfile) { when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mMockUserManager); when(mMockUserManager.getCredentialOwnerProfile(USER_ID)).thenReturn(USER_ID); From 2639c19474e1bcc4f62855bdf8a994b6a10cf5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Thu, 27 Jun 2024 19:18:06 +0200 Subject: [PATCH 4/7] Add mode: Support for app-provided modes (This completes the add-mode flow except for the choose-a-name-and-icon step for custom modes). Bug: 326442408 Flag: android.app.modes_ui Test: atest com.android.settings.notification.modes Change-Id: I7aceec01ed54d804bcac53d932277c243c1f81bf --- .../ic_zen_mode_new_option_custom.xml | 25 ++ res/values/strings.xml | 6 + .../modes/ConfigurationActivityHelper.java | 143 +++++++++++ .../notification/modes/ZenModeFragment.java | 3 +- ...odeSetTriggerLinkPreferenceController.java | 113 ++------- .../modes/ZenModesFragmentBase.java | 7 + ...nModesListAddModePreferenceController.java | 148 +++++++++++- .../ZenModesListAddModeTypeChooserDialog.java | 116 +++++++++ .../modes/ZenModesListFragment.java | 87 +++++-- .../ZenModesListPreferenceController.java | 8 +- .../notification/modes/ZenServiceListing.java | 170 +++++++++++++ .../ConfigurationActivityHelperTest.java | 183 ++++++++++++++ .../notification/modes/TestModeBuilder.java | 24 +- ...etTriggerLinkPreferenceControllerTest.java | 157 ++++-------- ...esListAddModePreferenceControllerTest.java | 225 ++++++++++++++++++ .../modes/ZenModesListFragmentTest.java | 149 ++++++++++++ .../ZenModesListPreferenceControllerTest.java | 2 +- 17 files changed, 1300 insertions(+), 266 deletions(-) create mode 100644 res/drawable/ic_zen_mode_new_option_custom.xml create mode 100644 src/com/android/settings/notification/modes/ConfigurationActivityHelper.java create mode 100644 src/com/android/settings/notification/modes/ZenModesListAddModeTypeChooserDialog.java create mode 100644 src/com/android/settings/notification/modes/ZenServiceListing.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ConfigurationActivityHelperTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModesListFragmentTest.java diff --git a/res/drawable/ic_zen_mode_new_option_custom.xml b/res/drawable/ic_zen_mode_new_option_custom.xml new file mode 100644 index 00000000000..c3a45ccb0a3 --- /dev/null +++ b/res/drawable/ic_zen_mode_new_option_custom.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index af7473b3c8e..32a81b4a1f7 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8022,6 +8022,12 @@ Disabled + + Create a mode + + + Custom + Limit interruptions diff --git a/src/com/android/settings/notification/modes/ConfigurationActivityHelper.java b/src/com/android/settings/notification/modes/ConfigurationActivityHelper.java new file mode 100644 index 00000000000..d001651d6eb --- /dev/null +++ b/src/com/android/settings/notification/modes/ConfigurationActivityHelper.java @@ -0,0 +1,143 @@ +/* + * 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.EXTRA_AUTOMATIC_RULE_ID; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ComponentInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.service.notification.ConditionProviderService; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.android.settingslib.notification.modes.ZenMode; + +import java.util.List; +import java.util.function.Function; + +class ConfigurationActivityHelper { + + private static final String TAG = "ConfigurationActivityHelper"; + + private final PackageManager mPm; + + ConfigurationActivityHelper(PackageManager pm) { + mPm = pm; + } + + @Nullable + Intent getConfigurationActivityIntentForMode(ZenMode zenMode, + Function approvedServiceFinder) { + + String owner = zenMode.getRule().getPackageName(); + ComponentName configActivity = null; + if (zenMode.getRule().getConfigurationActivity() != null) { + // If a configuration activity is present, use that directly in the intent + configActivity = zenMode.getRule().getConfigurationActivity(); + } else { + // Otherwise, look for a condition provider service for the rule's package + ComponentInfo ci = approvedServiceFinder.apply(zenMode.getRule().getOwner()); + if (ci != null) { + configActivity = extractConfigurationActivityFromComponent(ci); + } + } + + if (configActivity != null + && (owner == null || isSameOwnerPackage(owner, configActivity)) + && isResolvableActivity(configActivity)) { + return new Intent() + .setComponent(configActivity) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .putExtra(ConditionProviderService.EXTRA_RULE_ID, zenMode.getId()) + .putExtra(EXTRA_AUTOMATIC_RULE_ID, zenMode.getId()); + } else { + return null; + } + } + + @Nullable + ComponentName getConfigurationActivityFromApprovedComponent(ComponentInfo ci) { + ComponentName configActivity = extractConfigurationActivityFromComponent(ci); + if (configActivity != null + && isSameOwnerPackage(ci.packageName, configActivity) + && isResolvableActivity(configActivity)) { + return configActivity; + } else { + return null; + } + } + + /** + * Extract the {@link ComponentName} corresponding to the mode configuration activity + * from the component declaring the rule (which may be the Activity itself, or a CPS that points + * to the activity in question in its metadata). + * + *

This method doesn't perform any validation, so the activity may or may not exist. + */ + @Nullable + private ComponentName extractConfigurationActivityFromComponent(ComponentInfo ci) { + if (ci instanceof ActivityInfo) { + // New (activity-backed) rule. + return new ComponentName(ci.packageName, ci.name); + } else if (ci.metaData != null) { + // Old (service-backed) rule. + final String configurationActivity = ci.metaData.getString( + ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY); + if (configurationActivity != null) { + return ComponentName.unflattenFromString(configurationActivity); + } + } + return null; + } + + /** + * Verifies that the activity is the same package as the rule owner. + */ + private boolean isSameOwnerPackage(String ownerPkg, ComponentName activityName) { + try { + int ownerUid = mPm.getPackageUid(ownerPkg, 0); + int configActivityOwnerUid = mPm.getPackageUid(activityName.getPackageName(), 0); + if (ownerUid == configActivityOwnerUid) { + return true; + } else { + Log.w(TAG, String.format("Config activity (%s) not in owner package (%s)", + activityName, ownerPkg)); + return false; + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Failed to find config activity " + activityName); + return false; + } + } + + /** Verifies that the activity exists and hasn't been disabled. */ + private boolean isResolvableActivity(ComponentName activityName) { + Intent intent = new Intent().setComponent(activityName); + List results = mPm.queryIntentActivities(intent, /* flags= */ 0); + + if (intent.resolveActivity(mPm) == null || results.isEmpty()) { + Log.w(TAG, "Cannot resolve: " + activityName); + return false; + } + return true; + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index 3a64fb2f1a0..bb315d9cae0 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -62,8 +62,7 @@ public class ZenModeFragment extends ZenModeFragmentBase { prefControllers.add(new ZenModeDisplayLinkPreferenceController( context, "mode_display_settings", mBackend, mHelperBackend)); prefControllers.add(new ZenModeSetTriggerLinkPreferenceController(context, - "zen_automatic_trigger_category", this, mBackend, - context.getPackageManager())); + "zen_automatic_trigger_category", this, mBackend)); prefControllers.add(new InterruptionFilterPreferenceController( context, "allow_filtering", mBackend)); prefControllers.add(new ManualDurationPreferenceController( diff --git a/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java index 7328d918941..86135a96190 100644 --- a/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java @@ -18,20 +18,12 @@ package com.android.settings.notification.modes; import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR; import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; -import static android.app.NotificationManager.EXTRA_AUTOMATIC_RULE_ID; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ComponentInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.service.notification.ConditionProviderService; import android.util.Log; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; @@ -39,14 +31,10 @@ import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.utils.ManagedServiceSettings; -import com.android.settings.utils.ZenServiceListing; import com.android.settingslib.PrimarySwitchPreference; import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenModesBackend; -import java.util.List; - /** * Preference controller for the link to an individual mode's configuration page. */ @@ -56,23 +44,25 @@ class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenc @VisibleForTesting protected static final String AUTOMATIC_TRIGGER_PREF_KEY = "zen_automatic_trigger_settings"; - private static final ManagedServiceSettings.Config CONFIG = - ZenModesListFragment.getConditionProviderConfig(); - - private ZenServiceListing mServiceListing; - private final PackageManager mPm; + private final ConfigurationActivityHelper mConfigurationActivityHelper; + private final ZenServiceListing mServiceListing; private final DashboardFragment mFragment; ZenModeSetTriggerLinkPreferenceController(Context context, String key, - DashboardFragment fragment, ZenModesBackend backend, - PackageManager packageManager) { - super(context, key, backend); - mFragment = fragment; - mPm = packageManager; + DashboardFragment fragment, ZenModesBackend backend) { + this(context, key, fragment, backend, + new ConfigurationActivityHelper(context.getPackageManager()), + new ZenServiceListing(context)); } @VisibleForTesting - protected void setServiceListing(ZenServiceListing serviceListing) { + ZenModeSetTriggerLinkPreferenceController(Context context, String key, + DashboardFragment fragment, ZenModesBackend backend, + ConfigurationActivityHelper configurationActivityHelper, + ZenServiceListing serviceListing) { + super(context, key, backend); + mFragment = fragment; + mConfigurationActivityHelper = configurationActivityHelper; mServiceListing = serviceListing; } @@ -83,11 +73,9 @@ class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenc @Override public void displayPreference(PreferenceScreen screen, @NonNull ZenMode zenMode) { - if (mServiceListing == null) { - mServiceListing = new ZenServiceListing( - mContext, CONFIG, zenMode.getRule().getPackageName()); - } - mServiceListing.reloadApprovedServices(); + // Preload approved components, but only for the package that owns the rule (since it's the + // only package that can have a valid configurationActivity). + mServiceListing.loadApprovedComponents(zenMode.getRule().getPackageName()); } @Override @@ -130,8 +118,9 @@ class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenc }); } } else { - Intent intent = getAppRuleIntent(zenMode); - if (intent != null && isValidIntent(intent)) { + Intent intent = mConfigurationActivityHelper.getConfigurationActivityIntentForMode( + zenMode, mServiceListing::findService); + if (intent != null) { preference.setVisible(true); switchPref.setTitle(R.string.zen_mode_configuration_link_title); switchPref.setSummary(zenMode.getRule().getTriggerDescription()); @@ -161,68 +150,4 @@ class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenc }); // TODO: b/342156843 - Do we want to jump to the corresponding schedule editing screen? }; - - @VisibleForTesting - protected @Nullable Intent getAppRuleIntent(ZenMode zenMode) { - Intent intent = new Intent().addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - .putExtra(ConditionProviderService.EXTRA_RULE_ID, zenMode.getId()) - .putExtra(EXTRA_AUTOMATIC_RULE_ID, zenMode.getId()); - String owner = zenMode.getRule().getPackageName(); - ComponentName configActivity = null; - if (zenMode.getRule().getConfigurationActivity() != null) { - // If a configuration activity is present, use that directly in the intent - configActivity = zenMode.getRule().getConfigurationActivity(); - } else { - // Otherwise, look for a condition provider service for the rule's package - ComponentInfo ci = mServiceListing.findService(zenMode.getRule().getOwner()); - if (ci == null) { - // do nothing - } else if (ci instanceof ActivityInfo) { - // new activity backed rule - intent.setComponent(new ComponentName(ci.packageName, ci.name)); - return intent; - } else if (ci.metaData != null) { - // old service backed rule - final String configurationActivity = ci.metaData.getString( - ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY); - if (configurationActivity != null) { - configActivity = ComponentName.unflattenFromString(configurationActivity); - } - } - } - - if (configActivity != null) { - // verify that the owner of the rule owns the configuration activity, but only if - // owner exists - intent.setComponent(configActivity); - if (owner == null) { - return intent; - } - try { - int ownerUid = mPm.getPackageUid(owner, 0); - int configActivityOwnerUid = mPm.getPackageUid(configActivity.getPackageName(), 0); - if (ownerUid == configActivityOwnerUid) { - return intent; - } else { - Log.w(TAG, "Config activity not in owner package for " - + zenMode.getRule().getName()); - return null; - } - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Failed to find config activity"); - return null; - } - } - return null; - } - - private boolean isValidIntent(Intent intent) { - List results = mPm.queryIntentActivities( - intent, PackageManager.ResolveInfoFlags.of(0)); - if (intent.resolveActivity(mPm) == null || results.size() == 0) { - Log.w(TAG, "intent for zen rule invalid: " + intent); - return false; - } - return true; - } } diff --git a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java index e1156fef159..0bc06173fab 100644 --- a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java @@ -26,6 +26,8 @@ import android.os.UserManager; import android.provider.Settings.Global; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import com.android.settings.dashboard.RestrictedDashboardFragment; import com.android.settingslib.notification.modes.ZenModesBackend; @@ -57,6 +59,11 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment { return TAG; } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + void setBackend(ZenModesBackend backend) { + mBackend = backend; + } + @Override public void onAttach(@NonNull Context context) { mContext = context; diff --git a/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceController.java b/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceController.java index ba74b93aad9..b4657a37a5b 100644 --- a/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceController.java @@ -16,27 +16,82 @@ package com.android.settings.notification.modes; +import android.app.NotificationManager; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.ComponentInfo; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.graphics.drawable.Drawable; +import android.service.notification.ConditionProviderService; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; import androidx.preference.Preference; -import com.android.settings.utils.ZenServiceListing; +import com.android.settings.R; +import com.android.settingslib.Utils; import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.notification.modes.ZenMode; -import com.android.settingslib.notification.modes.ZenModesBackend; -import java.util.Random; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Function; class ZenModesListAddModePreferenceController extends AbstractPreferenceController { - private final ZenModesBackend mBackend; private final ZenServiceListing mServiceListing; + private final OnAddModeListener mOnAddModeListener; - ZenModesListAddModePreferenceController(Context context, ZenModesBackend backend, - ZenServiceListing serviceListing) { + private final ConfigurationActivityHelper mConfigurationActivityHelper; + private final NotificationManager mNotificationManager; + private final PackageManager mPackageManager; + private final Function mAppIconRetriever; + private final ListeningExecutorService mBackgroundExecutor; + private final Executor mUiThreadExecutor; + + record ModeType(String name, Drawable icon, @Nullable String summary, + @Nullable Intent creationActivityIntent) { } + + interface OnAddModeListener { + void onAvailableModeTypesForAdd(List types); + } + + ZenModesListAddModePreferenceController(Context context, OnAddModeListener onAddModeListener) { + this(context, onAddModeListener, new ZenServiceListing(context), + new ConfigurationActivityHelper(context.getPackageManager()), + context.getSystemService(NotificationManager.class), context.getPackageManager(), + applicationInfo -> Utils.getBadgedIcon(context, applicationInfo), + Executors.newCachedThreadPool(), context.getMainExecutor()); + } + + @VisibleForTesting + ZenModesListAddModePreferenceController(Context context, + OnAddModeListener onAddModeListener, ZenServiceListing serviceListing, + ConfigurationActivityHelper configurationActivityHelper, + NotificationManager notificationManager, PackageManager packageManager, + Function appIconRetriever, + ExecutorService backgroundExecutor, Executor uiThreadExecutor) { super(context); - mBackend = backend; + mOnAddModeListener = onAddModeListener; mServiceListing = serviceListing; + mConfigurationActivityHelper = configurationActivityHelper; + mNotificationManager = notificationManager; + mPackageManager = packageManager; + mAppIconRetriever = appIconRetriever; + mBackgroundExecutor = MoreExecutors.listeningDecorator(backgroundExecutor); + mUiThreadExecutor = uiThreadExecutor; } @Override @@ -52,12 +107,79 @@ class ZenModesListAddModePreferenceController extends AbstractPreferenceControll @Override public void updateState(Preference preference) { preference.setOnPreferenceClickListener(pref -> { - // TODO: b/326442408 - Launch the proper mode creation flow (using mServiceListing). - ZenMode mode = mBackend.addCustomMode("New mode #" + new Random().nextInt(1000)); - if (mode != null) { - ZenSubSettingLauncher.forMode(mContext, mode.getId()).launch(); - } + onClickAddMode(); return true; }); } + + @VisibleForTesting + void onClickAddMode() { + FutureUtil.whenDone( + mBackgroundExecutor.submit(this::getModeProviders), + mOnAddModeListener::onAvailableModeTypesForAdd, + mUiThreadExecutor); + } + + @WorkerThread + private ImmutableList getModeProviders() { + ImmutableSet approvedComponents = mServiceListing.loadApprovedComponents(); + + ArrayList appProvidedModes = new ArrayList<>(); + for (ComponentInfo ci: approvedComponents) { + ModeType modeType = getValidNewModeTypeFromComponent(ci); + if (modeType != null) { + appProvidedModes.add(modeType); + } + } + + return ImmutableList.builder() + .add(new ModeType( + mContext.getString(R.string.zen_mode_new_option_custom), + mContext.getDrawable(R.drawable.ic_zen_mode_new_option_custom), + null, null)) + .addAll(appProvidedModes.stream() + .sorted(Comparator.comparing(ModeType::name)) + .toList()) + .build(); + } + + /** + * Returns a {@link ModeType} object corresponding to the approved {@link ComponentInfo} that + * specifies a creatable rule, if such a mode can actually be created (has an associated and + * enabled configuration activity, has not exceeded the rule instance limit, etc). Otherwise, + * returns {@code null}. + */ + @WorkerThread + @Nullable + private ModeType getValidNewModeTypeFromComponent(ComponentInfo ci) { + if (ci.metaData == null) { + return null; + } + + String ruleType = (ci instanceof ServiceInfo) + ? ci.metaData.getString(ConditionProviderService.META_DATA_RULE_TYPE) + : ci.metaData.getString(NotificationManager.META_DATA_AUTOMATIC_RULE_TYPE); + if (ruleType == null || ruleType.trim().isEmpty()) { + return null; + } + + int ruleInstanceLimit = (ci instanceof ServiceInfo) + ? ci.metaData.getInt(ConditionProviderService.META_DATA_RULE_INSTANCE_LIMIT, -1) + : ci.metaData.getInt(NotificationManager.META_DATA_RULE_INSTANCE_LIMIT, -1); + if (ruleInstanceLimit > 0 && mNotificationManager.getRuleInstanceCount( + ci.getComponentName()) >= ruleInstanceLimit) { + return null; // Would exceed instance limit. + } + + ComponentName configurationActivity = + mConfigurationActivityHelper.getConfigurationActivityFromApprovedComponent(ci); + if (configurationActivity == null) { + return null; + } + + String appName = ci.applicationInfo.loadLabel(mPackageManager).toString(); + Drawable appIcon = mAppIconRetriever.apply(ci.applicationInfo); + Intent configActivityIntent = new Intent().setComponent(configurationActivity); + return new ModeType(ruleType, appIcon, appName, configActivityIntent); + } } diff --git a/src/com/android/settings/notification/modes/ZenModesListAddModeTypeChooserDialog.java b/src/com/android/settings/notification/modes/ZenModesListAddModeTypeChooserDialog.java new file mode 100644 index 00000000000..57d3bf96c2b --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModesListAddModeTypeChooserDialog.java @@ -0,0 +1,116 @@ +/* + * 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.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.notification.modes.ZenModesListAddModePreferenceController.ModeType; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +public class ZenModesListAddModeTypeChooserDialog extends InstrumentedDialogFragment { + + private static final String TAG = "ZenModesListAddModeTypeChooserDialog"; + + private OnChooseModeTypeListener mChooseModeTypeListener; + private ImmutableList mOptions; + + interface OnChooseModeTypeListener { + void onTypeSelected(ModeType type); + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - Update metrics category + return 0; + } + + static void show(DashboardFragment parent, + OnChooseModeTypeListener onChooseModeTypeListener, + List options) { + ZenModesListAddModeTypeChooserDialog dialog = new ZenModesListAddModeTypeChooserDialog(); + dialog.mChooseModeTypeListener = onChooseModeTypeListener; + dialog.mOptions = ImmutableList.copyOf(options); + dialog.setTargetFragment(parent, 0); + dialog.show(parent.getParentFragmentManager(), TAG); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + checkState(getContext() != null); + return new AlertDialog.Builder(getContext()) + .setTitle(R.string.zen_mode_new_title) + .setAdapter(new OptionsAdapter(getContext(), mOptions), + (dialog, which) -> mChooseModeTypeListener.onTypeSelected( + mOptions.get(which))) + .setNegativeButton(R.string.cancel, null) + .create(); + } + + private static class OptionsAdapter extends ArrayAdapter { + + private final LayoutInflater mInflater; + + private OptionsAdapter(Context context, + ImmutableList availableModeProviders) { + super(context, R.layout.zen_mode_type_item, availableModeProviders); + mInflater = LayoutInflater.from(context); + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + if (convertView == null) { + convertView = mInflater.inflate(R.layout.zen_mode_type_item, parent, false); + } + ImageView imageView = checkNotNull(convertView.findViewById(R.id.icon)); + TextView title = checkNotNull(convertView.findViewById(R.id.title)); + TextView subtitle = checkNotNull(convertView.findViewById(R.id.subtitle)); + + ModeType option = checkNotNull(getItem(position)); + imageView.setImageDrawable(option.icon()); + title.setText(option.name()); + subtitle.setText(option.summary()); + subtitle.setVisibility( + Strings.isNullOrEmpty(option.summary()) ? View.GONE : View.VISIBLE); + + return convertView; + } + } +} diff --git a/src/com/android/settings/notification/modes/ZenModesListFragment.java b/src/com/android/settings/notification/modes/ZenModesListFragment.java index 1883945944d..4622996141f 100644 --- a/src/com/android/settings/notification/modes/ZenModesListFragment.java +++ b/src/com/android/settings/notification/modes/ZenModesListFragment.java @@ -16,47 +16,51 @@ package com.android.settings.notification.modes; -import android.app.NotificationManager; import android.app.settings.SettingsEnums; +import android.content.ComponentName; import android.content.Context; -import android.service.notification.ConditionProviderService; +import android.content.Intent; import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; +import androidx.annotation.VisibleForTesting; import com.android.settings.R; +import com.android.settings.notification.modes.ZenModesListAddModePreferenceController.ModeType; +import com.android.settings.notification.modes.ZenModesListAddModePreferenceController.OnAddModeListener; import com.android.settings.search.BaseSearchIndexProvider; -import com.android.settings.utils.ManagedServiceSettings; -import com.android.settings.utils.ZenServiceListing; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.search.SearchIndexable; import com.google.common.collect.ImmutableList; import java.util.List; +import java.util.Optional; +import java.util.Random; @SearchIndexable public class ZenModesListFragment extends ZenModesFragmentBase { - private static final ManagedServiceSettings.Config CONFIG = getConditionProviderConfig(); + static final int REQUEST_NEW_MODE = 101; + + @Nullable private ComponentName mActivityInvokedForAddNew; + @Nullable private ImmutableList mZenModeIdsBeforeAddNew; @Override protected List createPreferenceControllers(Context context) { - ZenServiceListing serviceListing = new ZenServiceListing(getContext(), CONFIG); - serviceListing.reloadApprovedServices(); - return buildPreferenceControllers(context, this, serviceListing); + return buildPreferenceControllers(context, this::onAvailableModeTypesForAdd); } private static List buildPreferenceControllers(Context context, - @Nullable Fragment parent, @Nullable ZenServiceListing serviceListing) { + OnAddModeListener onAddModeListener) { // We need to redefine ZenModesBackend here even though mBackend exists so that this method // can be static; it must be static to be able to be used in SEARCH_INDEX_DATA_PROVIDER. ZenModesBackend backend = ZenModesBackend.getInstance(context); return ImmutableList.of( - new ZenModesListPreferenceController(context, parent, backend), - new ZenModesListAddModePreferenceController(context, backend, serviceListing) + new ZenModesListPreferenceController(context, backend), + new ZenModesListAddModePreferenceController(context, onAddModeListener) ); } @@ -78,14 +82,55 @@ public class ZenModesListFragment extends ZenModesFragmentBase { return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION; } - static ManagedServiceSettings.Config getConditionProviderConfig() { - return new ManagedServiceSettings.Config.Builder() - .setTag(TAG) - .setIntentAction(ConditionProviderService.SERVICE_INTERFACE) - .setConfigurationIntentAction(NotificationManager.ACTION_AUTOMATIC_ZEN_RULE) - .setPermission(android.Manifest.permission.BIND_CONDITION_PROVIDER_SERVICE) - .setNoun("condition provider") - .build(); + private void onAvailableModeTypesForAdd(List types) { + if (types.size() > 1) { + // Show dialog to choose the mode to be created. Continue once the user chooses. + ZenModesListAddModeTypeChooserDialog.show(this, this::onChosenModeTypeForAdd, types); + } else { + // Will be custom_manual. + onChosenModeTypeForAdd(types.get(0)); + } + } + + @VisibleForTesting + void onChosenModeTypeForAdd(ModeType type) { + if (type.creationActivityIntent() != null) { + mActivityInvokedForAddNew = type.creationActivityIntent().getComponent(); + mZenModeIdsBeforeAddNew = ImmutableList.copyOf( + mBackend.getModes().stream().map(ZenMode::getId).toList()); + startActivityForResult(type.creationActivityIntent(), REQUEST_NEW_MODE); + } else { + // Custom-manual mode. + // TODO: b/326442408 - Transition to the choose-name-and-icon fragment. + ZenMode mode = mBackend.addCustomManualMode( + "Mode #" + new Random().nextInt(100), 0); + if (mode != null) { + ZenSubSettingLauncher.forMode(mContext, mode.getId()).launch(); + } + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + // If coming back after starting a 3rd-party configuration activity to create a new mode, + // try to identify the created mode. Ideally this would be part of the resultCode/data, but + // the existing API doesn't work that way... + ComponentName activityInvoked = mActivityInvokedForAddNew; + ImmutableList previousIds = mZenModeIdsBeforeAddNew; + mActivityInvokedForAddNew = null; + mZenModeIdsBeforeAddNew = null; + if (requestCode != REQUEST_NEW_MODE || previousIds == null || activityInvoked == null) { + return; + } + + // If we find a new mode owned by the same package, presumably that's it. Open its page. + Optional createdZenMode = mBackend.getModes().stream() + .filter(m -> !previousIds.contains(m.getId())) + .filter(m -> m.getRule().getPackageName().equals(activityInvoked.getPackageName())) + .findFirst(); + createdZenMode.ifPresent( + mode -> ZenSubSettingLauncher.forMode(mContext, mode.getId()).launch()); } /** @@ -106,7 +151,7 @@ public class ZenModesListFragment extends ZenModesFragmentBase { @Override public List createPreferenceControllers( Context context) { - return buildPreferenceControllers(context, null, null); + return buildPreferenceControllers(context, ignoredType -> {}); } }; } diff --git a/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java b/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java index fb07078cd39..ba12b9ac842 100644 --- a/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java @@ -20,8 +20,6 @@ import android.content.Context; import android.content.res.Resources; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; @@ -43,14 +41,10 @@ import java.util.Map; class ZenModesListPreferenceController extends BasePreferenceController { protected static final String KEY = "zen_modes_list"; - @Nullable - protected Fragment mParent; protected ZenModesBackend mBackend; - public ZenModesListPreferenceController(Context context, @Nullable Fragment parent, - @NonNull ZenModesBackend backend) { + ZenModesListPreferenceController(Context context, @NonNull ZenModesBackend backend) { super(context, KEY); - mParent = parent; mBackend = backend; } diff --git a/src/com/android/settings/notification/modes/ZenServiceListing.java b/src/com/android/settings/notification/modes/ZenServiceListing.java new file mode 100644 index 00000000000..ccecec5f929 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenServiceListing.java @@ -0,0 +1,170 @@ +/* + * 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.ActivityManager; +import android.app.NotificationManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ComponentInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.service.notification.ConditionProviderService; +import android.util.ArraySet; +import android.util.Slog; + +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; + +import com.android.settings.utils.ManagedServiceSettings; + +import com.google.common.collect.ImmutableSet; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +class ZenServiceListing { + + static final ManagedServiceSettings.Config CONFIGURATION = + new ManagedServiceSettings.Config.Builder() + .setTag("ZenServiceListing") + .setIntentAction(ConditionProviderService.SERVICE_INTERFACE) + .setConfigurationIntentAction(NotificationManager.ACTION_AUTOMATIC_ZEN_RULE) + .setPermission(android.Manifest.permission.BIND_CONDITION_PROVIDER_SERVICE) + .setNoun("condition provider") + .build(); + + private final Context mContext; + private final Set mApprovedComponents = new ArraySet<>(); + private final List mZenCallbacks = new ArrayList<>(); + private final NotificationManager mNm; + + ZenServiceListing(Context context) { + mContext = context; + mNm = context.getSystemService(NotificationManager.class); + } + + public ComponentInfo findService(final ComponentName cn) { + if (cn == null) { + return null; + } + for (ComponentInfo component : mApprovedComponents) { + final ComponentName ci = new ComponentName(component.packageName, component.name); + if (ci.equals(cn)) { + return component; + } + } + return null; + } + + public void addZenCallback(Callback callback) { + mZenCallbacks.add(callback); + } + + public void removeZenCallback(Callback callback) { + mZenCallbacks.remove(callback); + } + + @WorkerThread + public ImmutableSet loadApprovedComponents() { + return loadApprovedComponents(null); + } + + @WorkerThread + public ImmutableSet loadApprovedComponents(@Nullable String restrictToPkg) { + mApprovedComponents.clear(); + + List enabledNotificationListenerPkgs = mNm.getEnabledNotificationListenerPackages(); + List components = new ArrayList<>(); + getServices(CONFIGURATION, components, mContext.getPackageManager(), restrictToPkg); + getActivities(CONFIGURATION, components, mContext.getPackageManager(), restrictToPkg); + for (ComponentInfo componentInfo : components) { + final String pkg = componentInfo.getComponentName().getPackageName(); + if (mNm.isNotificationPolicyAccessGrantedForPackage(pkg) + || enabledNotificationListenerPkgs.contains(pkg)) { + mApprovedComponents.add(componentInfo); + } + } + + if (!mApprovedComponents.isEmpty()) { + for (Callback callback : mZenCallbacks) { + callback.onComponentsReloaded(mApprovedComponents); + } + } + + return ImmutableSet.copyOf(mApprovedComponents); + } + + private static void getServices(ManagedServiceSettings.Config c, List list, + PackageManager pm, @Nullable String restrictToPkg) { + final int user = ActivityManager.getCurrentUser(); + + Intent queryIntent = new Intent(c.intentAction); + if (restrictToPkg != null) { + queryIntent.setPackage(restrictToPkg); + } + List installedServices = pm.queryIntentServicesAsUser( + queryIntent, + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, + user); + + for (int i = 0, count = installedServices.size(); i < count; i++) { + ResolveInfo resolveInfo = installedServices.get(i); + ServiceInfo info = resolveInfo.serviceInfo; + + if (!c.permission.equals(info.permission)) { + Slog.w(c.tag, "Skipping " + c.noun + " service " + + info.packageName + "/" + info.name + + ": it does not require the permission " + + c.permission); + continue; + } + if (list != null) { + list.add(info); + } + } + } + + private static void getActivities(ManagedServiceSettings.Config c, List list, + PackageManager pm, @Nullable String restrictToPkg) { + final int user = ActivityManager.getCurrentUser(); + + Intent queryIntent = new Intent(c.configIntentAction); + if (restrictToPkg != null) { + queryIntent.setPackage(restrictToPkg); + } + List resolveInfos = pm.queryIntentActivitiesAsUser( + queryIntent, + PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA, + user); + + for (int i = 0, count = resolveInfos.size(); i < count; i++) { + ResolveInfo resolveInfo = resolveInfos.get(i); + ActivityInfo info = resolveInfo.activityInfo; + if (list != null) { + list.add(info); + } + } + } + + public interface Callback { + void onComponentsReloaded(Set components); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ConfigurationActivityHelperTest.java b/tests/robotests/src/com/android/settings/notification/modes/ConfigurationActivityHelperTest.java new file mode 100644 index 00000000000..1c72e879c45 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ConfigurationActivityHelperTest.java @@ -0,0 +1,183 @@ +/* + * 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.EXTRA_AUTOMATIC_RULE_ID; +import static android.service.notification.ConditionProviderService.EXTRA_RULE_ID; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ComponentInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.service.notification.ConditionProviderService; + +import com.android.settingslib.notification.modes.ZenMode; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +@RunWith(RobolectricTestRunner.class) +public class ConfigurationActivityHelperTest { + + private Context mContext; + private ConfigurationActivityHelper mHelper; + + @Mock private PackageManager mPm; + @Mock private Function mApprovedServiceFinder; + + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.getApplication(); + mHelper = new ConfigurationActivityHelper(mPm); + + when(mPm.queryIntentActivities(any(), anyInt())).thenReturn(List.of(new ResolveInfo())); + } + + @Test + public void getConfigurationActivityIntentForMode_configActivity() throws Exception { + ZenMode mode = new TestModeBuilder() + .setId("id") + .setPackage(mContext.getPackageName()) + .setConfigurationActivity(new ComponentName(mContext.getPackageName(), "test")) + .build(); + when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1); + + Intent res = mHelper.getConfigurationActivityIntentForMode(mode, mApprovedServiceFinder); + + assertThat(res).isNotNull(); + assertThat(res.getStringExtra(EXTRA_RULE_ID)).isEqualTo("id"); + assertThat(res.getStringExtra(EXTRA_AUTOMATIC_RULE_ID)).isEqualTo("id"); + assertThat(res.getComponent()).isEqualTo( + new ComponentName(mContext.getPackageName(), "test")); + } + + @Test + public void getConfigurationActivityIntentForMode_configActivityNotResolvable_returnsNull() + throws Exception { + ZenMode mode = new TestModeBuilder() + .setId("id") + .setPackage(mContext.getPackageName()) + .setConfigurationActivity(new ComponentName(mContext.getPackageName(), "test")) + .build(); + when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1); + when(mPm.queryIntentActivities(any(), anyInt())).thenReturn(new ArrayList<>()); + + Intent res = mHelper.getConfigurationActivityIntentForMode(mode, mApprovedServiceFinder); + + assertThat(res).isNull(); + } + + @Test + public void getConfigurationActivityIntentForMode_configActivityAndWrongPackage_returnsNull() + throws Exception { + ZenMode mode = new TestModeBuilder() + .setPackage(mContext.getPackageName()) + .setConfigurationActivity(new ComponentName("another", "test")) + .build(); + when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1); + + Intent res = mHelper.getConfigurationActivityIntentForMode(mode, mApprovedServiceFinder); + + assertThat(res).isNull(); + } + + @Test + public void getConfigurationActivityIntentForMode_configActivityAndUnspecifiedOwner() + throws Exception { + ZenMode mode = new TestModeBuilder() + .setId("id") + .setPackage(null) + .setConfigurationActivity(new ComponentName("another", "test")) + .build(); + when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1); + + Intent res = mHelper.getConfigurationActivityIntentForMode(mode, mApprovedServiceFinder); + + assertThat(res).isNotNull(); + assertThat(res.getStringExtra(EXTRA_RULE_ID)).isEqualTo("id"); + assertThat(res.getStringExtra(EXTRA_AUTOMATIC_RULE_ID)).isEqualTo("id"); + assertThat(res.getComponent()).isEqualTo(new ComponentName("another", "test")); + } + + @Test + public void getConfigurationActivityIntentForMode_cps() throws Exception { + ZenMode mode = new TestModeBuilder() + .setId("id") + .setPackage(mContext.getPackageName()) + .setOwner(new ComponentName(mContext.getPackageName(), "service")) + .build(); + ComponentInfo ci = new ComponentInfo(); + ci.packageName = mContext.getPackageName(); + ci.metaData = new Bundle(); + ci.metaData.putString(ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY, + ComponentName.flattenToShortString( + new ComponentName(mContext.getPackageName(), "activity"))); + when(mApprovedServiceFinder.apply(new ComponentName(mContext.getPackageName(), "service"))) + .thenReturn(ci); + when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1); + + Intent res = mHelper.getConfigurationActivityIntentForMode(mode, mApprovedServiceFinder); + + assertThat(res).isNotNull(); + assertThat(res.getStringExtra(EXTRA_RULE_ID)).isEqualTo("id"); + assertThat(res.getStringExtra(EXTRA_AUTOMATIC_RULE_ID)).isEqualTo("id"); + assertThat(res.getComponent()).isEqualTo( + new ComponentName(mContext.getPackageName(), "activity")); + } + + @Test + public void getConfigurationActivityIntentForMode_cpsAndWrongPackage_returnsNull() + throws Exception { + ZenMode mode = new TestModeBuilder() + .setPackage("other") + .setOwner(new ComponentName(mContext.getPackageName(), "service")) + .build(); + ComponentInfo ci = new ComponentInfo(); + ci.packageName = mContext.getPackageName(); + ci.metaData = new Bundle(); + ci.metaData.putString(ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY, + ComponentName.flattenToShortString( + new ComponentName(mContext.getPackageName(), "activity"))); + when(mApprovedServiceFinder.apply(new ComponentName(mContext.getPackageName(), "service"))) + .thenReturn(ci); + when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1); + + Intent res = mHelper.getConfigurationActivityIntentForMode(mode, mApprovedServiceFinder); + + assertThat(res).isNull(); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java b/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java index 6a1f47409be..fdb57010e58 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java +++ b/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java @@ -84,6 +84,18 @@ class TestModeBuilder { return this; } + TestModeBuilder setOwner(ComponentName owner) { + mRule.setOwner(owner); + mConfigZenRule.component = owner; + return this; + } + + TestModeBuilder setConfigurationActivity(ComponentName configActivity) { + mRule.setConfigurationActivity(configActivity); + mConfigZenRule.configurationActivity = configActivity; + return this; + } + TestModeBuilder setConditionId(Uri conditionId) { mRule.setConditionId(conditionId); mConfigZenRule.conditionId = conditionId; @@ -150,18 +162,6 @@ class TestModeBuilder { return this; } - TestModeBuilder setConfigurationActivity(ComponentName configActivity) { - mRule.setConfigurationActivity(configActivity); - mConfigZenRule.configurationActivity = configActivity; - return this; - } - - TestModeBuilder setOwner(ComponentName owner) { - mRule.setOwner(owner); - mConfigZenRule.component = owner; - return this; - } - ZenMode build() { return new ZenMode(mId, mRule, mConfigZenRule); } diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java index 4ba21469fad..ffd239b602b 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java @@ -19,30 +19,26 @@ package com.android.settings.notification.modes; import static android.app.AutomaticZenRule.TYPE_OTHER; import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR; import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; -import static android.app.NotificationManager.EXTRA_AUTOMATIC_RULE_ID; import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; -import static android.service.notification.ConditionProviderService.EXTRA_RULE_ID; import static com.android.settings.notification.modes.ZenModeSetTriggerLinkPreferenceController.AUTOMATIC_TRIGGER_PREF_KEY; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.AutomaticZenRule; import android.app.Flags; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.ComponentInfo; import android.content.pm.PackageManager; import android.net.Uri; -import android.os.Bundle; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; -import android.service.notification.ConditionProviderService; import android.service.notification.SystemZenRules; import android.service.notification.ZenModeConfig; @@ -52,7 +48,6 @@ import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.utils.ZenServiceListing; import com.android.settingslib.PrimarySwitchPreference; import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenModesBackend; @@ -80,10 +75,10 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest { private PrimarySwitchPreference mPreference; - @Mock - private ZenServiceListing mServiceListing; @Mock private PackageManager mPm; + @Mock + private ConfigurationActivityHelper mConfigurationActivityHelper; @Mock private PreferenceCategory mPrefCategory; @@ -98,8 +93,9 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest { mContext = ApplicationProvider.getApplicationContext(); mPrefController = new ZenModeSetTriggerLinkPreferenceController(mContext, - "zen_automatic_trigger_category", mFragment, mBackend, mPm); - mPrefController.setServiceListing(mServiceListing); + "zen_automatic_trigger_category", mFragment, mBackend, + mConfigurationActivityHelper, + mock(ZenServiceListing.class)); mPreference = new PrimarySwitchPreference(mContext); when(mPrefCategory.findPreference(AUTOMATIC_TRIGGER_PREF_KEY)).thenReturn(mPreference); @@ -225,6 +221,40 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest { assertThat(mPreference.getOnPreferenceClickListener()).isNotNull(); } + @Test + public void testRuleLink_appWithConfigActivity_linksToConfigActivity() { + ZenMode mode = new TestModeBuilder() + .setPackage("some.package") + .setTriggerDescription("When The Music's Over") + .build(); + Intent configurationIntent = new Intent("configure the mode"); + when(mConfigurationActivityHelper.getConfigurationActivityIntentForMode(any(), any())) + .thenReturn(configurationIntent); + + mPrefController.updateZenMode(mPrefCategory, mode); + + assertThat(mPreference.getTitle()).isNotNull(); + assertThat(mPreference.getTitle().toString()).isEqualTo( + mContext.getString(R.string.zen_mode_configuration_link_title)); + assertThat(mPreference.getSummary()).isNotNull(); + assertThat(mPreference.getSummary().toString()).isEqualTo("When The Music's Over"); + assertThat(mPreference.getIntent()).isEqualTo(configurationIntent); + } + + @Test + public void testRuleLink_appWithoutConfigActivity_hidden() { + ZenMode mode = new TestModeBuilder() + .setPackage("some.package") + .setTriggerDescription("Will not be shown :(") + .build(); + when(mConfigurationActivityHelper.getConfigurationActivityIntentForMode(any(), any())) + .thenReturn(null); + + mPrefController.updateZenMode(mPrefCategory, mode); + + assertThat(mPrefCategory.isVisible()).isFalse(); + } + @Test public void onScheduleChosen_updatesMode() { ZenMode originalMode = new TestModeBuilder() @@ -253,109 +283,4 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest { assertThat(updatedMode.getRule().getOwner()).isEqualTo( ZenModeConfig.getScheduleConditionProvider()); } - - @Test - public void testGetAppRuleIntent_configActivity() throws Exception { - ZenMode mode = new TestModeBuilder() - .setId("id") - .setPackage(mContext.getPackageName()) - .setConfigurationActivity(new ComponentName(mContext.getPackageName(), "test")) - .setType(TYPE_OTHER) - .setTriggerDescription("some rule") - .build(); - - when(mPm.getPackageUid(null, 0)).thenReturn(-1); - when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1); - - Intent res = mPrefController.getAppRuleIntent(mode); - assertThat(res).isNotNull(); - assertThat(res.getStringExtra(EXTRA_RULE_ID)).isEqualTo("id"); - assertThat(res.getStringExtra(EXTRA_AUTOMATIC_RULE_ID)).isEqualTo("id"); - assertThat(res.getComponent()).isEqualTo( - new ComponentName(mContext.getPackageName(), "test")); - } - - @Test - public void testGetAppRuleIntent_configActivity_wrongPackage() throws Exception { - ZenMode mode = new TestModeBuilder() - .setPackage(mContext.getPackageName()) - .setConfigurationActivity(new ComponentName("another", "test")) - .setType(TYPE_OTHER) - .build(); - - when(mPm.getPackageUid(null, 0)).thenReturn(-1); - when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1); - - Intent res = mPrefController.getAppRuleIntent(mode); - assertThat(res).isNull(); - } - - @Test - public void testGetAppRuleIntent_configActivity_unspecifiedOwner() throws Exception { - ZenMode mode = new TestModeBuilder() - .setId("id") - .setPackage(null) - .setConfigurationActivity(new ComponentName("another", "test")) - .setType(TYPE_OTHER) - .build(); - - when(mPm.getPackageUid(null, 0)).thenReturn(-1); - when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1); - - Intent res = mPrefController.getAppRuleIntent(mode); - assertThat(res).isNotNull(); - assertThat(res.getStringExtra(EXTRA_RULE_ID)).isEqualTo("id"); - assertThat(res.getStringExtra(EXTRA_AUTOMATIC_RULE_ID)).isEqualTo("id"); - assertThat(res.getComponent()).isEqualTo(new ComponentName("another", "test")); - } - - @Test - public void testGetAppRuleIntent_cps() throws Exception { - ZenMode mode = new TestModeBuilder() - .setId("id") - .setPackage(mContext.getPackageName()) - .setOwner(new ComponentName(mContext.getPackageName(), "service")) - .build(); - - ComponentInfo ci = new ComponentInfo(); - ci.packageName = mContext.getPackageName(); - ci.metaData = new Bundle(); - ci.metaData.putString(ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY, - ComponentName.flattenToShortString( - new ComponentName(mContext.getPackageName(), "activity"))); - - when(mServiceListing.findService(new ComponentName(mContext.getPackageName(), "service"))) - .thenReturn(ci); - when(mPm.getPackageUid(null, 0)).thenReturn(-1); - when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1); - - Intent res = mPrefController.getAppRuleIntent(mode); - assertThat(res).isNotNull(); - assertThat(res.getStringExtra(EXTRA_RULE_ID)).isEqualTo("id"); - assertThat(res.getStringExtra(EXTRA_AUTOMATIC_RULE_ID)).isEqualTo("id"); - assertThat(res.getComponent()).isEqualTo( - new ComponentName(mContext.getPackageName(), "activity")); - } - - @Test - public void testGetAppRuleIntent_cps_wrongPackage() throws Exception { - ZenMode mode = new TestModeBuilder() - .setPackage("other") - .setOwner(new ComponentName(mContext.getPackageName(), "service")) - .setType(TYPE_OTHER) - .build(); - - ComponentInfo ci = new ComponentInfo(); - ci.packageName = mContext.getPackageName(); - ci.metaData = new Bundle(); - ci.metaData.putString(ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY, - ComponentName.flattenToShortString( - new ComponentName(mContext.getPackageName(), "activity"))); - - when(mPm.getPackageUid(null, 0)).thenReturn(-1); - when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1); - - Intent res = mPrefController.getAppRuleIntent(mode); - assertThat(res).isNull(); - } } diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceControllerTest.java new file mode 100644 index 00000000000..fe530c10d18 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceControllerTest.java @@ -0,0 +1,225 @@ +/* + * 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.META_DATA_AUTOMATIC_RULE_TYPE; +import static android.app.NotificationManager.META_DATA_RULE_INSTANCE_LIMIT; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.NotificationManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.ComponentInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; + +import com.android.settings.notification.modes.ZenModesListAddModePreferenceController.ModeType; + +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.MoreExecutors; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.List; +import java.util.function.Function; + +@RunWith(RobolectricTestRunner.class) +public class ZenModesListAddModePreferenceControllerTest { + + private Context mContext; + private ZenModesListAddModePreferenceController mController; + + @Mock private ZenModesListAddModePreferenceController.OnAddModeListener mListener; + @Mock private ZenServiceListing mZenServiceListing; + @Mock private ConfigurationActivityHelper mConfigurationActivityHelper; + @Mock private NotificationManager mNm; + @Mock private PackageManager mPm; + + @Captor private ArgumentCaptor> mListenerCaptor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.getApplication(); + Function appIconRetriever = appInfo -> new ColorDrawable(); + + mController = new ZenModesListAddModePreferenceController(mContext, mListener, + mZenServiceListing, mConfigurationActivityHelper, mNm, mPm, appIconRetriever, + MoreExecutors.newDirectExecutorService(), MoreExecutors.directExecutor()); + + when(mConfigurationActivityHelper.getConfigurationActivityFromApprovedComponent(any())) + .thenAnswer((Answer) invocationOnMock -> { + // By default, assume the ComponentInfo is also the configurationActivity. + ComponentInfo ci = invocationOnMock.getArgument(0); + return ci != null ? ci.getComponentName() : null; + }); + } + + @Test + public void onClickAddMode_noAppProviders_onlyOptionIsCustom() { + when(mZenServiceListing.loadApprovedComponents()).thenReturn(ImmutableSet.of()); + + mController.onClickAddMode(); + + verify(mListener).onAvailableModeTypesForAdd(mListenerCaptor.capture()); + List options = mListenerCaptor.getValue(); + assertThat(options).hasSize(1); + assertThat(options.get(0).name()).isEqualTo("Custom"); + assertThat(options.get(0).summary()).isNull(); + assertThat(options.get(0).icon()).isNotNull(); + assertThat(options.get(0).creationActivityIntent()).isNull(); + } + + @Test + public void onClickAddMode_someAppProviders_includedInOptions() { + ImmutableSet approvedComponents = ImmutableSet.of( + newComponentInfoWithValidMetadata("pkg1"), + newComponentInfoWithValidMetadata("pkg2")); + when(mZenServiceListing.loadApprovedComponents()).thenReturn(approvedComponents); + + mController.onClickAddMode(); + + verify(mListener).onAvailableModeTypesForAdd(mListenerCaptor.capture()); + List options = mListenerCaptor.getValue(); + assertThat(options).hasSize(3); + + assertThat(options.get(1).name()).isEqualTo("Rule by pkg1"); + assertThat(options.get(1).summary()).isEqualTo("A package called pkg1"); + assertThat(options.get(1).icon()).isNotNull(); + assertThat(options.get(1).creationActivityIntent()).isNotNull(); + assertThat(options.get(1).creationActivityIntent().getComponent()).isEqualTo( + new ComponentName("pkg1", "pkg1.activity")); + + assertThat(options.get(0).name()).isEqualTo("Custom"); + assertThat(options.get(2).name()).isEqualTo("Rule by pkg2"); + } + + @Test + public void onClickAddMode_someAppProviders_optionsAreSorted() { + ImmutableSet approvedComponents = ImmutableSet.of( + newComponentInfoWithValidMetadata("pkg_Z"), + newComponentInfoWithValidMetadata("pkg_A"), + newComponentInfoWithValidMetadata("pkg_F"), + newComponentInfoWithValidMetadata("pkg_C")); + when(mZenServiceListing.loadApprovedComponents()).thenReturn(approvedComponents); + + mController.onClickAddMode(); + + verify(mListener).onAvailableModeTypesForAdd(mListenerCaptor.capture()); + List options = mListenerCaptor.getValue(); + assertThat(options).hasSize(5); + assertThat(options.stream().map(o -> o.name()).toList()) + .containsExactly("Custom", "Rule by pkg_A", "Rule by pkg_C", "Rule by pkg_F", + "Rule by pkg_Z") + .inOrder(); + } + + @Test + public void onClickAddMode_appProviderWithMissingMetadata_notAnOption() { + ComponentInfo componentWithoutRuleType = newComponentInfoWithValidMetadata("pkg1"); + componentWithoutRuleType.metaData.remove(META_DATA_AUTOMATIC_RULE_TYPE); + ImmutableSet approvedComponents = ImmutableSet.of( + componentWithoutRuleType, newComponentInfoWithValidMetadata("pkg2")); + when(mZenServiceListing.loadApprovedComponents()).thenReturn(approvedComponents); + + mController.onClickAddMode(); + + verify(mListener).onAvailableModeTypesForAdd(mListenerCaptor.capture()); + List options = mListenerCaptor.getValue(); + assertThat(options).hasSize(2); + assertThat(options.get(0).name()).isEqualTo("Custom"); + assertThat(options.get(1).name()).isEqualTo("Rule by pkg2"); + } + + @Test + public void onClickAddMode_appProviderWithRuleLimitExceeded_notAnOption() { + ComponentInfo componentWithLimitThreeRules = newComponentInfoWithValidMetadata("pkg1"); + componentWithLimitThreeRules.metaData.putInt(META_DATA_RULE_INSTANCE_LIMIT, 3); + ImmutableSet approvedComponents = ImmutableSet.of( + componentWithLimitThreeRules, newComponentInfoWithValidMetadata("pkg2")); + when(mZenServiceListing.loadApprovedComponents()).thenReturn(approvedComponents); + when(mNm.getRuleInstanceCount(any())).thenReturn(3); // Already 3 created rules. + + mController.onClickAddMode(); + + verify(mListener).onAvailableModeTypesForAdd(mListenerCaptor.capture()); + List options = mListenerCaptor.getValue(); + assertThat(options).hasSize(2); + assertThat(options.get(0).name()).isEqualTo("Custom"); + assertThat(options.get(1).name()).isEqualTo("Rule by pkg2"); + verify(mNm).getRuleInstanceCount(eq(componentWithLimitThreeRules.getComponentName())); + } + + @Test + public void onClickAddMode_appProviderWithoutConfigurationActivity_notAnOption() { + ComponentInfo componentWithoutConfigActivity = newComponentInfoWithValidMetadata("pkg2"); + ImmutableSet approvedComponents = ImmutableSet.of( + newComponentInfoWithValidMetadata("pkg1"), componentWithoutConfigActivity); + when(mZenServiceListing.loadApprovedComponents()).thenReturn(approvedComponents); + when(mConfigurationActivityHelper.getConfigurationActivityFromApprovedComponent(any())) + .thenAnswer((Answer) invocationOnMock -> { + ComponentInfo ci = invocationOnMock.getArgument(0); + if (ci == componentWithoutConfigActivity) { + return null; + } else { + return ci.getComponentName(); + } + }); + + mController.onClickAddMode(); + + verify(mListener).onAvailableModeTypesForAdd(mListenerCaptor.capture()); + List options = mListenerCaptor.getValue(); + assertThat(options).hasSize(2); + assertThat(options.get(0).name()).isEqualTo("Custom"); + assertThat(options.get(1).name()).isEqualTo("Rule by pkg1"); + } + + private ComponentInfo newComponentInfoWithValidMetadata(String pkg) { + ComponentInfo ci = new ActivityInfo(); + + ci.applicationInfo = mock(ApplicationInfo.class); + when(ci.applicationInfo.loadLabel(any())).thenReturn("A package called " + pkg); + when(ci.applicationInfo.loadUnbadgedIcon(any())).thenReturn(new ColorDrawable()); + ci.packageName = pkg; + ci.name = pkg + ".activity"; + ci.metaData = new Bundle(); + ci.metaData.putString(META_DATA_AUTOMATIC_RULE_TYPE, "Rule by " + pkg); + + return ci; + } +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListFragmentTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListFragmentTest.java new file mode 100644 index 00000000000..661f8ba1245 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListFragmentTest.java @@ -0,0 +1,149 @@ +/* + * 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.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; + +import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT; +import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS; +import static com.android.settings.notification.modes.ZenModesListFragment.REQUEST_NEW_MODE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + +import android.content.ComponentName; +import android.content.Intent; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; + +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.testing.EmptyFragmentActivity; +import androidx.test.ext.junit.rules.ActivityScenarioRule; + +import com.android.settings.notification.modes.ZenModesListAddModePreferenceController.ModeType; +import com.android.settingslib.notification.modes.ZenMode; +import com.android.settingslib.notification.modes.ZenModesBackend; + +import com.google.common.collect.ImmutableList; + +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.shadows.ShadowActivity.IntentForResult; + +@RunWith(RobolectricTestRunner.class) +public class ZenModesListFragmentTest { + + private static final ModeType APP_PROVIDED_MODE_TYPE = new ModeType("Mode", new ColorDrawable(), + "Details", new Intent().setComponent(new ComponentName("pkg", "configActivity"))); + + private static final ImmutableList EXISTING_MODES = ImmutableList.of( + new TestModeBuilder().setId("A").build(), + new TestModeBuilder().setId("B").build(), + new TestModeBuilder().setId("C").build()); + + @Rule + public ActivityScenarioRule mActivityScenario = + new ActivityScenarioRule<>(EmptyFragmentActivity.class); + + private FragmentActivity mActivity; + private ZenModesListFragment mFragment; + @Mock private ZenModesBackend mBackend; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mFragment = new ZenModesListFragment(); + mActivityScenario.getScenario().onActivity(activity -> { + activity.getSupportFragmentManager().beginTransaction() + .add(mFragment, "tag").commitNow(); + mActivity = activity; + }); + + mFragment.setBackend(mBackend); // after onAttach() + } + + @Test + public void onChosenModeTypeForAdd_appProvidedMode_startsCreationActivity() { + when(mBackend.getModes()).thenReturn(EXISTING_MODES); + + mFragment.onChosenModeTypeForAdd(APP_PROVIDED_MODE_TYPE); + + IntentForResult intent = shadowOf(mActivity).getNextStartedActivityForResult(); + assertThat(intent).isNotNull(); + assertThat(intent.intent).isEqualTo(APP_PROVIDED_MODE_TYPE.creationActivityIntent()); + } + + @Test + public void onActivityResult_modeWasCreated_opensIt() { + when(mBackend.getModes()).thenReturn(EXISTING_MODES); + mFragment.onChosenModeTypeForAdd(APP_PROVIDED_MODE_TYPE); + + // App creates the new mode. + ZenMode createdMode = new TestModeBuilder().setId("new_id").setPackage("pkg").build(); + when(mBackend.getModes()).thenReturn(new ImmutableList.Builder() + .addAll(EXISTING_MODES) + .add(createdMode) + .build()); + mFragment.onActivityResult(REQUEST_NEW_MODE, 0, new Intent()); + + Intent openModePageIntent = shadowOf(mActivity).getNextStartedActivity(); + assertThat(openModePageIntent.getStringExtra(EXTRA_SHOW_FRAGMENT)) + .isEqualTo(ZenModeFragment.class.getName()); + Bundle fragmentArgs = openModePageIntent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS); + assertThat(fragmentArgs).isNotNull(); + assertThat(fragmentArgs.getString(EXTRA_AUTOMATIC_ZEN_RULE_ID)).isEqualTo("new_id"); + } + + @Test + public void onActivityResult_secondTime_doesNothing() { + when(mBackend.getModes()).thenReturn(EXISTING_MODES); + mFragment.onChosenModeTypeForAdd(APP_PROVIDED_MODE_TYPE); + // App creates a new mode, we redirect to its page when coming back. + ZenMode createdMode = new TestModeBuilder().setId("new_id").setPackage("pkg").build(); + when(mBackend.getModes()).thenReturn(new ImmutableList.Builder() + .addAll(EXISTING_MODES) + .add(createdMode) + .build()); + mFragment.onActivityResult(REQUEST_NEW_MODE, 0, new Intent()); + shadowOf(mActivity).clearNextStartedActivities(); + + mFragment.onActivityResult(REQUEST_NEW_MODE, 0, new Intent()); + + Intent nextIntent = shadowOf(mActivity).getNextStartedActivity(); + assertThat(nextIntent).isNull(); + } + + @Test + public void onActivityResult_modeWasNotCreated_doesNothing() { + when(mBackend.getModes()).thenReturn(EXISTING_MODES); + mFragment.onChosenModeTypeForAdd(APP_PROVIDED_MODE_TYPE); + shadowOf(mActivity).clearNextStartedActivities(); + + // Returning to settings without creating a new mode. + mFragment.onActivityResult(REQUEST_NEW_MODE, 0, new Intent()); + + Intent nextIntent = shadowOf(mActivity).getNextStartedActivity(); + assertThat(nextIntent).isNull(); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListPreferenceControllerTest.java index f2624acd9e9..c0f96bea887 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListPreferenceControllerTest.java @@ -99,7 +99,7 @@ public class ZenModesListPreferenceControllerTest { PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(mContext); preferenceScreen.addPreference(mPreference); - mPrefController = new ZenModesListPreferenceController(mContext, null, mBackend); + mPrefController = new ZenModesListPreferenceController(mContext, mBackend); } @Test From 45f1e819d36043991c11d52fdcc551de777901fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Mon, 1 Jul 2024 18:25:54 +0200 Subject: [PATCH 5/7] Icon picker: Styling improvements Instead of a the plain appearance of a EntityHeaderController, make the top icon bigger and use the same circled style as the choices in the list. Also highlight the current icon in the list as selected, even if it is the default for the mode type. Also cleaned up controllers that don't need a ZenModesBackend to not receive it. (Both of these changes also line up with the "new mode" fragment that is incoming). Bug: 333901673 Bug: 326442408 Test: atest com.android.settings.notification.modes Flag: android.app.modes_ui Change-Id: I0c9f3e34019a1a6c48658933dde545ad8d7399ae --- res/layout/modes_icon_list_item.xml | 4 +- res/values/dimens.xml | 6 +- .../AbstractZenModePreferenceController.java | 35 ++++++---- .../settings/notification/modes/IconUtil.java | 65 +++++++++++++------ .../ZenModeActionsPreferenceController.java | 7 +- .../ZenModeAppsPreferenceController.java | 3 +- .../ZenModeCallsLinkPreferenceController.java | 5 +- .../modes/ZenModeDisplayFragment.java | 2 +- .../notification/modes/ZenModeFragment.java | 8 +-- .../modes/ZenModeHeaderController.java | 7 +- .../modes/ZenModeIconPickerFragment.java | 5 +- ...odeIconPickerIconPreferenceController.java | 19 ++++-- ...odeIconPickerListPreferenceController.java | 23 +++++-- ...nModeMessagesLinkPreferenceController.java | 5 +- ...nModeNotifVisLinkPreferenceController.java | 5 +- .../ZenModeOtherLinkPreferenceController.java | 5 +- .../modes/ZenModePeopleFragment.java | 4 +- ...ZenModePeopleLinkPreferenceController.java | 5 +- ...ModeCallsLinkPreferenceControllerTest.java | 5 +- ...conPickerListPreferenceControllerTest.java | 4 +- ...eMessagesLinkPreferenceControllerTest.java | 5 +- ...eNotifVisLinkPreferenceControllerTest.java | 5 +- ...ModeOtherLinkPreferenceControllerTest.java | 5 +- ...odePeopleLinkPreferenceControllerTest.java | 6 +- 24 files changed, 135 insertions(+), 108 deletions(-) diff --git a/res/layout/modes_icon_list_item.xml b/res/layout/modes_icon_list_item.xml index aa45de33b72..72aef528889 100644 --- a/res/layout/modes_icon_list_item.xml +++ b/res/layout/modes_icon_list_item.xml @@ -24,8 +24,8 @@ diff --git a/res/values/dimens.xml b/res/values/dimens.xml index d34647449df..ce48720cf78 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -502,7 +502,9 @@ 30dp + 90dp + 48dp 96dp - 56dp - 32dp + 56dp + 32dp diff --git a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java index a4bd2aac5f6..c740847af0e 100644 --- a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java +++ b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java @@ -16,6 +16,8 @@ package com.android.settings.notification.modes; +import static com.google.common.base.Preconditions.checkState; + import android.app.Flags; import android.content.Context; import android.service.notification.ZenPolicy; @@ -30,8 +32,6 @@ import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenModesBackend; -import com.google.common.base.Preconditions; - import java.util.function.Function; /** @@ -41,8 +41,8 @@ abstract class AbstractZenModePreferenceController extends AbstractPreferenceCon private static final String TAG = "AbstractZenModePreferenceController"; - @Nullable - protected ZenModesBackend mBackend; + @Nullable protected final ZenModesBackend mBackend; + @Nullable // only until setZenMode() is called private ZenMode mZenMode; @@ -50,14 +50,27 @@ abstract class AbstractZenModePreferenceController extends AbstractPreferenceCon @NonNull private final String mKey; - // ZenModesBackend should only be passed in if the preference controller may set the user's - // policy for this zen mode. Otherwise, if the preference controller is essentially read-only - // and leads to a further Settings screen, backend should be null. - AbstractZenModePreferenceController(@NonNull Context context, @NonNull String key, - @Nullable ZenModesBackend backend) { + /** + * Constructor suitable for "read-only" controllers (e.g. link to a different sub-screen. + * Controllers that call this constructor to initialize themselves cannot call + * {@link #saveMode} or {@link #savePolicy} later. + */ + AbstractZenModePreferenceController(@NonNull Context context, @NonNull String key) { super(context); - mBackend = backend; mKey = key; + mBackend = null; + } + + /** + * Constructor suitable for controllers that will update the associated {@link ZenMode}. + * Controllers that call this constructor to initialize themselves may call {@link #saveMode} or + * {@link #savePolicy} later. + */ + AbstractZenModePreferenceController(@NonNull Context context, @NonNull String key, + @NonNull ZenModesBackend backend) { + super(context); + mKey = key; + mBackend = backend; } @Override @@ -135,7 +148,7 @@ abstract class AbstractZenModePreferenceController extends AbstractPreferenceCon * instance is ok. */ protected final boolean saveMode(Function updater) { - Preconditions.checkState(mBackend != null); + checkState(mBackend != null); ZenMode mode = mZenMode; if (mode == null) { Log.wtf(TAG, "Cannot save mode, it hasn't been loaded (" + getClass() + ")"); diff --git a/src/com/android/settings/notification/modes/IconUtil.java b/src/com/android/settings/notification/modes/IconUtil.java index 56967c89d00..d07abf34b79 100644 --- a/src/com/android/settings/notification/modes/IconUtil.java +++ b/src/com/android/settings/notification/modes/IconUtil.java @@ -19,14 +19,17 @@ package com.android.settings.notification.modes; import static com.google.common.base.Preconditions.checkNotNull; import android.content.Context; +import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; +import android.view.Gravity; import androidx.annotation.AttrRes; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; +import androidx.annotation.Px; import com.android.settings.R; import com.android.settingslib.Utils; @@ -49,32 +52,52 @@ class IconUtil { } /** - * Returns a variant of the supplied {@code icon} to be used in the icon picker. The inner icon - * is 36x36dp and it's contained into a circle of diameter 54dp. It's also set up so that - * selection and pressed states are represented in the color. + * Returns a variant of the supplied {@code icon} to be used as the header in the icon picker. + * The inner icon is 48x48dp and it's contained into a circle of diameter 90dp. */ - static Drawable makeIconCircle(@NonNull Context context, @NonNull Drawable icon) { + static Drawable makeBigIconCircle(@NonNull Context context, Drawable icon) { + return composeIconCircle( + Utils.getColorAttr(context, + com.android.internal.R.attr.materialColorSecondaryContainer), + context.getResources().getDimensionPixelSize( + R.dimen.zen_mode_icon_list_header_circle_diameter), + icon, + Utils.getColorAttr(context, + com.android.internal.R.attr.materialColorOnSecondaryContainer), + context.getResources().getDimensionPixelSize( + R.dimen.zen_mode_icon_list_header_icon_size)); + } + + /** + * Returns a variant of the supplied {@code icon} to be used as an option in the icon picker. + * The inner icon is 36x36dp and it's contained into a circle of diameter 54dp. It's also set up + * so that selection and pressed states are represented in the color. + */ + static Drawable makeSmallIconCircle(@NonNull Context context, @DrawableRes int iconResId) { + return composeIconCircle( + context.getColorStateList(R.color.modes_icon_picker_item_background), + context.getResources().getDimensionPixelSize( + R.dimen.zen_mode_icon_list_item_circle_diameter), + checkNotNull(context.getDrawable(iconResId)), + context.getColorStateList(R.color.modes_icon_picker_item_icon), + context.getResources().getDimensionPixelSize( + R.dimen.zen_mode_icon_list_item_icon_size)); + } + + private static Drawable composeIconCircle(ColorStateList circleColor, @Px int circleDiameterPx, + Drawable icon, ColorStateList iconColor, @Px int iconSizePx) { ShapeDrawable background = new ShapeDrawable(new OvalShape()); - background.setTintList( - context.getColorStateList(R.color.modes_icon_picker_item_background)); - icon = icon.mutate(); - icon.setTintList( - context.getColorStateList(R.color.modes_icon_picker_item_icon)); + background.setTintList(circleColor); + Drawable foreground = checkNotNull(icon.getConstantState()).newDrawable().mutate(); + foreground.setTintList(iconColor); - LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, icon }); + LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, foreground }); - int circleDiameter = context.getResources().getDimensionPixelSize( - R.dimen.zen_mode_icon_list_circle_diameter); - int iconSize = context.getResources().getDimensionPixelSize( - R.dimen.zen_mode_icon_list_icon_size); - int iconPadding = (circleDiameter - iconSize) / 2; - layerDrawable.setBounds(0, 0, circleDiameter, circleDiameter); - layerDrawable.setLayerInset(1, iconPadding, iconPadding, iconPadding, iconPadding); + layerDrawable.setBounds(0, 0, circleDiameterPx, circleDiameterPx); + layerDrawable.setLayerSize(0, circleDiameterPx, circleDiameterPx); + layerDrawable.setLayerGravity(1, Gravity.CENTER); + layerDrawable.setLayerSize(1, iconSizePx, iconSizePx); return layerDrawable; } - - static Drawable makeIconCircle(@NonNull Context context, @DrawableRes int iconResId) { - return makeIconCircle(context, checkNotNull(context.getDrawable(iconResId))); - } } diff --git a/src/com/android/settings/notification/modes/ZenModeActionsPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeActionsPreferenceController.java index 914683fb86c..2561a7b630e 100644 --- a/src/com/android/settings/notification/modes/ZenModeActionsPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeActionsPreferenceController.java @@ -22,20 +22,17 @@ import android.content.Context; import android.os.Bundle; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.preference.Preference; import com.android.settings.R; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.notification.modes.ZenMode; -import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.widget.ActionButtonsPreference; class ZenModeActionsPreferenceController extends AbstractZenModePreferenceController { - ZenModeActionsPreferenceController(@NonNull Context context, @NonNull String key, - @Nullable ZenModesBackend backend) { - super(context, key, backend); + ZenModeActionsPreferenceController(@NonNull Context context, @NonNull String key) { + super(context, key); } @Override diff --git a/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java index 1d807ed47b4..522f191c37f 100644 --- a/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java @@ -24,7 +24,6 @@ import android.os.Bundle; import android.service.notification.ZenPolicy; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; @@ -44,7 +43,7 @@ public class ZenModeAppsPreferenceController extends String mModeId; public ZenModeAppsPreferenceController(@NonNull Context context, - @NonNull String key, @Nullable ZenModesBackend backend) { + @NonNull String key, @NonNull ZenModesBackend backend) { super(context, key, backend); } diff --git a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java index e5c1e480ee6..d8850191762 100644 --- a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java @@ -26,15 +26,14 @@ import androidx.preference.Preference; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.notification.modes.ZenMode; -import com.android.settingslib.notification.modes.ZenModesBackend; class ZenModeCallsLinkPreferenceController extends AbstractZenModePreferenceController { private final ZenModeSummaryHelper mSummaryHelper; public ZenModeCallsLinkPreferenceController(Context context, String key, - ZenModesBackend backend, ZenHelperBackend helperBackend) { - super(context, key, backend); + ZenHelperBackend helperBackend) { + super(context, key); mSummaryHelper = new ZenModeSummaryHelper(context, helperBackend); } diff --git a/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java b/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java index 8c3b8261f74..38ac8f31072 100644 --- a/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java @@ -35,7 +35,7 @@ public class ZenModeDisplayFragment extends ZenModeFragmentBase { protected List createPreferenceControllers(Context context) { List prefControllers = new ArrayList<>(); prefControllers.add(new ZenModeNotifVisLinkPreferenceController( - context, "notification_visibility", mBackend, mHelperBackend)); + context, "notification_visibility", mHelperBackend)); prefControllers.add(new ZenModeDisplayEffectPreferenceController( context, "effect_greyscale", mBackend)); prefControllers.add(new ZenModeDisplayEffectPreferenceController( diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index bb315d9cae0..5e3fc89610d 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -47,18 +47,18 @@ public class ZenModeFragment extends ZenModeFragmentBase { @Override protected List createPreferenceControllers(Context context) { List prefControllers = new ArrayList<>(); - prefControllers.add(new ZenModeHeaderController(context, "header", this, mBackend)); + prefControllers.add(new ZenModeHeaderController(context, "header", this)); prefControllers.add( new ZenModeButtonPreferenceController(context, "activate", this, mBackend)); - prefControllers.add(new ZenModeActionsPreferenceController(context, "actions", mBackend)); + prefControllers.add(new ZenModeActionsPreferenceController(context, "actions")); prefControllers.add(new ZenModePeopleLinkPreferenceController( - context, "zen_mode_people", mBackend, mHelperBackend)); + context, "zen_mode_people", mHelperBackend)); prefControllers.add(new ZenModeAppsLinkPreferenceController( context, "zen_mode_apps", this, ApplicationsState.getInstance((Application) context.getApplicationContext()), mBackend, mHelperBackend)); prefControllers.add(new ZenModeOtherLinkPreferenceController( - context, "zen_other_settings", mBackend, mHelperBackend)); + context, "zen_other_settings", mHelperBackend)); prefControllers.add(new ZenModeDisplayLinkPreferenceController( context, "mode_display_settings", mBackend, mHelperBackend)); prefControllers.add(new ZenModeSetTriggerLinkPreferenceController(context, diff --git a/src/com/android/settings/notification/modes/ZenModeHeaderController.java b/src/com/android/settings/notification/modes/ZenModeHeaderController.java index 1845ee8f190..545abffe2f1 100644 --- a/src/com/android/settings/notification/modes/ZenModeHeaderController.java +++ b/src/com/android/settings/notification/modes/ZenModeHeaderController.java @@ -19,7 +19,6 @@ import android.app.Flags; import android.content.Context; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.preference.Preference; import com.android.settings.R; @@ -27,7 +26,6 @@ import com.android.settings.dashboard.DashboardFragment; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.notification.modes.ZenIconLoader; import com.android.settingslib.notification.modes.ZenMode; -import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.widget.LayoutPreference; class ZenModeHeaderController extends AbstractZenModePreferenceController { @@ -38,9 +36,8 @@ class ZenModeHeaderController extends AbstractZenModePreferenceController { ZenModeHeaderController( @NonNull Context context, @NonNull String key, - @NonNull DashboardFragment fragment, - @Nullable ZenModesBackend backend) { - super(context, key, backend); + @NonNull DashboardFragment fragment) { + super(context, key); mFragment = fragment; } diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java b/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java index 43d9dba1b54..f065af239c4 100644 --- a/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java @@ -41,10 +41,9 @@ public class ZenModeIconPickerFragment extends ZenModeFragmentBase { @Override protected List createPreferenceControllers(Context context) { return ImmutableList.of( - new ZenModeIconPickerIconPreferenceController(context, "current_icon", this, - mBackend), + new ZenModeIconPickerIconPreferenceController(context, "current_icon", this), new ZenModeIconPickerListPreferenceController(context, "icon_list", - mIconPickerListener, new IconOptionsProviderImpl(mContext), mBackend)); + mIconPickerListener)); } private final ZenModeIconPickerListPreferenceController.IconPickerListener mIconPickerListener = diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java index d1d53af9ddf..70df9b651ee 100644 --- a/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java @@ -17,9 +17,10 @@ package com.android.settings.notification.modes; import android.content.Context; +import android.view.ViewGroup; +import android.widget.ImageView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.preference.Preference; import com.android.settings.R; @@ -27,17 +28,17 @@ import com.android.settings.dashboard.DashboardFragment; import com.android.settings.widget.EntityHeaderController; import com.android.settingslib.notification.modes.ZenIconLoader; import com.android.settingslib.notification.modes.ZenMode; -import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.widget.LayoutPreference; +/** Controller used for displaying the currently-chosen icon at the top of the icon picker. */ class ZenModeIconPickerIconPreferenceController extends AbstractZenModePreferenceController { private final DashboardFragment mFragment; private EntityHeaderController mHeaderController; ZenModeIconPickerIconPreferenceController(@NonNull Context context, @NonNull String key, - @NonNull DashboardFragment fragment, @Nullable ZenModesBackend backend) { - super(context, key, backend); + @NonNull DashboardFragment fragment) { + super(context, key); mFragment = fragment; } @@ -51,11 +52,19 @@ class ZenModeIconPickerIconPreferenceController extends AbstractZenModePreferenc mFragment.getActivity(), mFragment, pref.findViewById(R.id.entity_header)); + + ImageView iconView = pref.findViewById(R.id.entity_header_icon); + ViewGroup.LayoutParams layoutParams = iconView.getLayoutParams(); + int imageSizePx = iconView.getContext().getResources().getDimensionPixelSize( + R.dimen.zen_mode_icon_list_header_circle_diameter); + layoutParams.width = imageSizePx; + layoutParams.height = imageSizePx; + iconView.setLayoutParams(layoutParams); } FutureUtil.whenDone( zenMode.getIcon(mContext, ZenIconLoader.getInstance()), - icon -> mHeaderController.setIcon(IconUtil.applyNormalTint(mContext, icon)) + icon -> mHeaderController.setIcon(IconUtil.makeBigIconCircle(mContext, icon)) .done(/* rebindActions= */ false), mContext.getMainExecutor()); } diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java index e663354231e..512dabb4437 100644 --- a/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java @@ -26,6 +26,7 @@ import android.widget.ImageView; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import androidx.recyclerview.widget.GridLayoutManager; @@ -33,8 +34,8 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SimpleItemAnimator; import com.android.settings.R; +import com.android.settingslib.notification.modes.ZenIconLoader; import com.android.settingslib.notification.modes.ZenMode; -import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.widget.LayoutPreference; import com.google.common.collect.ImmutableList; @@ -51,9 +52,15 @@ class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenc private @DrawableRes int mCurrentIconResId; ZenModeIconPickerListPreferenceController(@NonNull Context context, @NonNull String key, - @NonNull IconPickerListener listener, @NonNull IconOptionsProvider iconOptionsProvider, - @Nullable ZenModesBackend backend) { - super(context, key, backend); + @NonNull IconPickerListener listener) { + this(context, key, listener, new IconOptionsProviderImpl(context)); + } + + @VisibleForTesting + ZenModeIconPickerListPreferenceController(@NonNull Context context, @NonNull String key, + @NonNull IconPickerListener listener, + @NonNull IconOptionsProvider iconOptionsProvider) { + super(context, key); mListener = listener; mIconOptionsProvider = iconOptionsProvider; } @@ -80,7 +87,11 @@ class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenc @Override void updateState(Preference preference, @NonNull ZenMode zenMode) { - updateIconSelection(zenMode.getRule().getIconResId()); + @DrawableRes int iconResId = zenMode.getRule().getIconResId(); + if (iconResId == 0) { + iconResId = ZenIconLoader.getIconResourceIdFromType(zenMode.getType()); + } + updateIconSelection(iconResId); } private void updateIconSelection(@DrawableRes int iconResId) { @@ -145,7 +156,7 @@ class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenc public void onBindViewHolder(@NonNull IconHolder holder, int position) { IconOptionsProvider.IconInfo iconInfo = mIconResources.get(position); Drawable iconDrawable = mIconCache.computeIfAbsent(iconInfo, - info -> IconUtil.makeIconCircle(mContext, info.resId())); + info -> IconUtil.makeSmallIconCircle(mContext, info.resId())); holder.bindIcon(iconInfo, iconDrawable); } diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java index 9b7c8a1b757..4c0b758e7cb 100644 --- a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java @@ -26,14 +26,13 @@ import androidx.preference.Preference; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.notification.modes.ZenMode; -import com.android.settingslib.notification.modes.ZenModesBackend; class ZenModeMessagesLinkPreferenceController extends AbstractZenModePreferenceController { private final ZenModeSummaryHelper mSummaryHelper; public ZenModeMessagesLinkPreferenceController(Context context, String key, - ZenModesBackend backend, ZenHelperBackend helperBackend) { - super(context, key, backend); + ZenHelperBackend helperBackend) { + super(context, key); mSummaryHelper = new ZenModeSummaryHelper(context, helperBackend); } diff --git a/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java index a2d941108ae..622c4a2db48 100644 --- a/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java @@ -27,15 +27,14 @@ import androidx.preference.Preference; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.notification.modes.ZenMode; -import com.android.settingslib.notification.modes.ZenModesBackend; class ZenModeNotifVisLinkPreferenceController extends AbstractZenModePreferenceController { private final ZenModeSummaryHelper mSummaryBuilder; public ZenModeNotifVisLinkPreferenceController(Context context, String key, - ZenModesBackend backend, ZenHelperBackend helperBackend) { - super(context, key, backend); + ZenHelperBackend helperBackend) { + super(context, key); mSummaryBuilder = new ZenModeSummaryHelper(context, helperBackend); } diff --git a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java index 99625ebc574..248ef1dd585 100644 --- a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java @@ -27,7 +27,6 @@ import androidx.preference.Preference; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.notification.modes.ZenMode; -import com.android.settingslib.notification.modes.ZenModesBackend; /** * Preference with a link and summary about what other sounds can break through the mode @@ -37,8 +36,8 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont private final ZenModeSummaryHelper mSummaryHelper; public ZenModeOtherLinkPreferenceController(Context context, String key, - ZenModesBackend backend, ZenHelperBackend helperBackend) { - super(context, key, backend); + ZenHelperBackend helperBackend) { + super(context, key); mSummaryHelper = new ZenModeSummaryHelper(mContext, helperBackend); } diff --git a/src/com/android/settings/notification/modes/ZenModePeopleFragment.java b/src/com/android/settings/notification/modes/ZenModePeopleFragment.java index 448712ce0e8..f541d132010 100644 --- a/src/com/android/settings/notification/modes/ZenModePeopleFragment.java +++ b/src/com/android/settings/notification/modes/ZenModePeopleFragment.java @@ -35,9 +35,9 @@ public class ZenModePeopleFragment extends ZenModeFragmentBase { protected List createPreferenceControllers(Context context) { List prefControllers = new ArrayList<>(); prefControllers.add(new ZenModeCallsLinkPreferenceController( - context, "zen_mode_people_calls", mBackend, mHelperBackend)); + context, "zen_mode_people_calls", mHelperBackend)); prefControllers.add(new ZenModeMessagesLinkPreferenceController( - context, "zen_mode_people_messages", mBackend, mHelperBackend)); + context, "zen_mode_people_messages", mHelperBackend)); return prefControllers; } diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java index 1613a010c35..936cea6ce5c 100644 --- a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java @@ -27,7 +27,6 @@ import androidx.preference.Preference; import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.notification.modes.ZenMode; -import com.android.settingslib.notification.modes.ZenModesBackend; /** * Preference with a link and summary about what calls and messages can break through the mode @@ -37,8 +36,8 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon private final ZenModeSummaryHelper mSummaryHelper; public ZenModePeopleLinkPreferenceController(Context context, String key, - ZenModesBackend backend, ZenHelperBackend helperBackend) { - super(context, key, backend); + ZenHelperBackend helperBackend) { + super(context, key); mSummaryHelper = new ZenModeSummaryHelper(mContext, helperBackend); } diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java index 058b2d7d566..e50d2941fc3 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java @@ -27,8 +27,6 @@ import android.platform.test.flag.junit.SetFlagsRule; import androidx.preference.Preference; -import com.android.settingslib.notification.modes.ZenModesBackend; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -47,7 +45,6 @@ public final class ZenModeCallsLinkPreferenceControllerTest { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private Context mContext; - @Mock private ZenModesBackend mBackend; @Mock private ZenHelperBackend mHelperBackend; @Before @@ -57,7 +54,7 @@ public final class ZenModeCallsLinkPreferenceControllerTest { mContext = RuntimeEnvironment.application; mController = new ZenModeCallsLinkPreferenceController( - mContext, "something", mBackend, mHelperBackend); + mContext, "something", mHelperBackend); } @Test diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java index e0ca306c71c..4d30ce95ee1 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java @@ -19,7 +19,6 @@ package com.android.settings.notification.modes; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -34,7 +33,6 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.settings.R; import com.android.settingslib.notification.modes.ZenMode; -import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.widget.LayoutPreference; import com.google.common.collect.ImmutableList; @@ -64,7 +62,7 @@ public class ZenModeIconPickerListPreferenceControllerTest { mController = new ZenModeIconPickerListPreferenceController( RuntimeEnvironment.getApplication(), "icon_list", mListener, - new TestIconOptionsProvider(), mock(ZenModesBackend.class)); + new TestIconOptionsProvider()); mRecyclerView = new RecyclerView(mContext); mRecyclerView.setId(R.id.icon_list); diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java index 288359aeb57..dda58ae7482 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java @@ -27,8 +27,6 @@ import android.platform.test.flag.junit.SetFlagsRule; import androidx.preference.Preference; -import com.android.settingslib.notification.modes.ZenModesBackend; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -47,7 +45,6 @@ public final class ZenModeMessagesLinkPreferenceControllerTest { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private Context mContext; - @Mock private ZenModesBackend mBackend; @Mock private ZenHelperBackend mHelperBackend; @Before @@ -57,7 +54,7 @@ public final class ZenModeMessagesLinkPreferenceControllerTest { mContext = RuntimeEnvironment.application; mController = new ZenModeMessagesLinkPreferenceController( - mContext, "something", mBackend, mHelperBackend); + mContext, "something", mHelperBackend); } @Test diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceControllerTest.java index ee7340bd526..bfa99a0a927 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceControllerTest.java @@ -27,8 +27,6 @@ import android.platform.test.flag.junit.SetFlagsRule; import androidx.preference.Preference; -import com.android.settingslib.notification.modes.ZenModesBackend; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -47,7 +45,6 @@ public final class ZenModeNotifVisLinkPreferenceControllerTest { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private Context mContext; - @Mock private ZenModesBackend mBackend; @Mock private ZenHelperBackend mHelperBackend; @Before @@ -57,7 +54,7 @@ public final class ZenModeNotifVisLinkPreferenceControllerTest { mContext = RuntimeEnvironment.application; mController = new ZenModeNotifVisLinkPreferenceController( - mContext, "something", mBackend, mHelperBackend); + mContext, "something", mHelperBackend); } @Test diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java index c4d03fe3360..39ba9e8aa82 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java @@ -27,8 +27,6 @@ import android.platform.test.flag.junit.SetFlagsRule; import androidx.preference.Preference; -import com.android.settingslib.notification.modes.ZenModesBackend; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -48,7 +46,6 @@ public final class ZenModeOtherLinkPreferenceControllerTest { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private Context mContext; - @Mock private ZenModesBackend mBackend; @Mock private ZenHelperBackend mHelperBackend; @Before @@ -58,7 +55,7 @@ public final class ZenModeOtherLinkPreferenceControllerTest { mContext = RuntimeEnvironment.application; mController = new ZenModeOtherLinkPreferenceController( - mContext, "something", mBackend, mHelperBackend); + mContext, "something", mHelperBackend); } @Test diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java index 6591b724c58..9d9dd9892a7 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java @@ -27,8 +27,6 @@ import android.platform.test.flag.junit.SetFlagsRule; import androidx.preference.Preference; -import com.android.settingslib.notification.modes.ZenModesBackend; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -48,8 +46,6 @@ public final class ZenModePeopleLinkPreferenceControllerTest { private Context mContext; @Mock - private ZenModesBackend mBackend; - @Mock private ZenHelperBackend mHelperBackend; @Before @@ -59,7 +55,7 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mContext = RuntimeEnvironment.application; mController = new ZenModePeopleLinkPreferenceController( - mContext, "something", mBackend, mHelperBackend); + mContext, "something", mHelperBackend); } @Test From 318636d51bd411d25b3bb897a60e7a2c9a4bc0fa Mon Sep 17 00:00:00 2001 From: Yiling Chuang Date: Tue, 18 Jun 2024 06:26:39 +0000 Subject: [PATCH 6/7] Update the conditions of power plugged determination. Under charging optimization mode, use ADAPTIVE_LONGLIFE as the additional condition to check whether a device is plugged. Bug: 349949603 Flag: EXEMPT bugfix Test: atest SettingsRoboTests Change-Id: Idbdfaaa1d5c54be325b6182bcda81d4282c21ba0 --- .../settings/fuelgauge/BatteryInfo.java | 29 ++++- .../settings/fuelgauge/BatteryInfoTest.java | 122 ++++++++++++++++-- 2 files changed, 137 insertions(+), 14 deletions(-) diff --git a/src/com/android/settings/fuelgauge/BatteryInfo.java b/src/com/android/settings/fuelgauge/BatteryInfo.java index 7cf9e44bd66..fa30253a296 100644 --- a/src/com/android/settings/fuelgauge/BatteryInfo.java +++ b/src/com/android/settings/fuelgauge/BatteryInfo.java @@ -307,12 +307,13 @@ public class BatteryInfo { info.pluggedStatus = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); info.mCharging = info.pluggedStatus != 0; info.averageTimeToDischarge = estimate.getAverageDischargeTime(); - info.isLongLife = - batteryBroadcast.getIntExtra( - BatteryManager.EXTRA_CHARGING_STATUS, - BatteryManager.CHARGING_POLICY_DEFAULT) - == BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE; + final int chargingPolicy = + batteryBroadcast.getIntExtra( + BatteryManager.EXTRA_CHARGING_STATUS, + BatteryManager.CHARGING_POLICY_DEFAULT); + + info.isLongLife = chargingPolicy == BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE; info.statusLabel = Utils.getBatteryStatus(context, batteryBroadcast, isCompactStatus); info.batteryStatus = batteryBroadcast.getIntExtra( @@ -326,7 +327,15 @@ public class BatteryInfo { .getPowerUsageFeatureProvider() .isBatteryDefend(info); } - if (!info.mCharging) { + Log.d( + TAG, + "chargingPolicy = " + + chargingPolicy + + ", pluggedStatus = " + + info.pluggedStatus + + ", batteryStatus = " + + info.batteryStatus); + if (!isPluggedIn(context, info.mCharging, chargingPolicy)) { updateBatteryInfoDischarging(context, shortString, estimate, info); } else { updateBatteryInfoCharging( @@ -556,6 +565,14 @@ public class BatteryInfo { } } + private static boolean isPluggedIn(Context context, boolean isCharging, int chargingPolicy) { + return isCharging + || (chargingPolicy == BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE + && FeatureFactory.getFeatureFactory() + .getBatterySettingsFeatureProvider() + .isChargingOptimizationMode(context)); + } + public interface BatteryDataParser { void onParsingStarted(long startTime, long endTime); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java index b7e65906fab..851dc79a03f 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java @@ -48,6 +48,7 @@ import com.android.settings.testutils.BatteryTestUtils; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.widget.UsageView; import com.android.settingslib.fuelgauge.Estimate; +import com.android.settingslib.utils.PowerUtil; import org.junit.After; import org.junit.Before; @@ -93,7 +94,8 @@ public class BatteryInfoTest { Map.of( ChargingType.WIRED, BatteryManager.BATTERY_PLUGGED_AC, ChargingType.WIRELESS, BatteryManager.BATTERY_PLUGGED_WIRELESS, - ChargingType.DOCKED, BatteryManager.BATTERY_PLUGGED_DOCK); + ChargingType.DOCKED, BatteryManager.BATTERY_PLUGGED_DOCK, + ChargingType.NONE, 0); private static final Map CHARGING_SPEED_MAP = Map.of( ChargingSpeed.FAST, 1501000, @@ -823,6 +825,92 @@ public class BatteryInfoTest { assertThat(batteryInfo.isLongLife).isFalse(); } + @Test + public void getBatteryInfo_plugTypeNoneWithLonglifeAndChargeOptimization_chargingString() { + prepareTestGetBatteryInfoEnvironment( + /* remainingTimeMs= */ Duration.ofMinutes(130).toMillis(), + /* chargingStringV2Enabled= */ false); + Intent batteryIntent = + createIntentForGetBatteryInfoTest( + ChargingType.NONE, + ChargingSpeed.REGULAR, + /* batteryLevel= */ 85, + BatteryManager.BATTERY_STATUS_DISCHARGING, + /* isLonglife= */ true); + var expectedRemainingLabel = "Expected remaining label"; + var expectedChargeLabel = "85% - " + expectedRemainingLabel; + when(mFeatureFactory.batterySettingsFeatureProvider.isChargingOptimizationMode(mContext)) + .thenReturn(true); + when(mFeatureFactory.batterySettingsFeatureProvider.getChargingOptimizationRemainingLabel( + eq(mContext), anyInt(), anyInt(), anyLong(), anyLong())) + .thenReturn(expectedRemainingLabel); + when(mFeatureFactory.batterySettingsFeatureProvider.getChargingOptimizationChargeLabel( + eq(mContext), anyInt(), anyString(), anyLong(), anyLong())) + .thenReturn(expectedChargeLabel); + var expectedStatusLabel = "Not charging"; + + assertGetBatteryInfo( + batteryIntent, + /* currentTimeMillis= */ UNUSED_TIME_MS, + expectedStatusLabel, + expectedRemainingLabel, + expectedChargeLabel); + } + + @Test + public void getBatteryInfo_plugTypeNoneNotChargeOptimizationLonglife_dischargingString() { + prepareTestGetBatteryInfoEnvironment( + /* remainingTimeMs= */ Duration.ofMinutes(130).toMillis(), + /* chargingStringV2Enabled= */ false); + Intent batteryIntent = + createIntentForGetBatteryInfoTest( + ChargingType.NONE, + ChargingSpeed.REGULAR, + /* batteryLevel= */ 85, + BatteryManager.BATTERY_STATUS_DISCHARGING, + /* isLonglife= */ true); + var expectedRemainingLabel = + PowerUtil.getBatteryRemainingShortStringFormatted( + mContext, PowerUtil.convertUsToMs(1000L)); + when(mFeatureFactory.batterySettingsFeatureProvider.isChargingOptimizationMode(mContext)) + .thenReturn(false); + var expectedStatusLabel = "Not charging"; + + assertGetBatteryInfo( + batteryIntent, + /* currentTimeMillis= */ UNUSED_TIME_MS, + expectedStatusLabel, + expectedRemainingLabel, + expectedRemainingLabel); + } + + @Test + public void getBatteryInfo_plugTypeNoneChargeOptimizationNotLonglife_dischargingString() { + prepareTestGetBatteryInfoEnvironment( + /* remainingTimeMs= */ Duration.ofMinutes(130).toMillis(), + /* chargingStringV2Enabled= */ false); + Intent batteryIntent = + createIntentForGetBatteryInfoTest( + ChargingType.NONE, + ChargingSpeed.REGULAR, + /* batteryLevel= */ 85, + BatteryManager.BATTERY_STATUS_DISCHARGING, + /* isLonglife= */ false); + var expectedRemainingLabel = + PowerUtil.getBatteryRemainingShortStringFormatted( + mContext, PowerUtil.convertUsToMs(1000L)); + when(mFeatureFactory.batterySettingsFeatureProvider.isChargingOptimizationMode(mContext)) + .thenReturn(true); + var expectedStatusLabel = "Not charging"; + + assertGetBatteryInfo( + batteryIntent, + /* currentTimeMillis= */ UNUSED_TIME_MS, + expectedStatusLabel, + expectedRemainingLabel, + expectedRemainingLabel); + } + private enum ChargingSpeed { FAST, REGULAR, @@ -832,10 +920,11 @@ public class BatteryInfoTest { private enum ChargingType { WIRED, WIRELESS, - DOCKED + DOCKED, + NONE } - private Intent createIntentForLongLifeTest(Boolean hasLongLife) { + private static Intent createIntentForLongLifeTest(Boolean hasLongLife) { return new Intent(Intent.ACTION_BATTERY_CHANGED) .putExtra( BatteryManager.EXTRA_CHARGING_STATUS, @@ -844,16 +933,33 @@ public class BatteryInfoTest { : BatteryManager.CHARGING_POLICY_DEFAULT); } - private Intent createIntentForGetBatteryInfoTest( + private static Intent createIntentForGetBatteryInfoTest( ChargingType chargingType, ChargingSpeed chargingSpeed, int batteryLevel) { + return createIntentForGetBatteryInfoTest( + chargingType, + chargingSpeed, + batteryLevel, + BatteryManager.BATTERY_STATUS_CHARGING, + /* isLonglife= */ false); + } + + private static Intent createIntentForGetBatteryInfoTest( + ChargingType chargingType, + ChargingSpeed chargingSpeed, + int batteryLevel, + int chargingStatus, + boolean isLonglife) { return createBatteryIntent( - CHARGING_TYPE_MAP.get(chargingType), - batteryLevel, - BatteryManager.BATTERY_STATUS_CHARGING) + CHARGING_TYPE_MAP.get(chargingType), batteryLevel, chargingStatus) .putExtra( BatteryManager.EXTRA_MAX_CHARGING_CURRENT, CHARGING_SPEED_MAP.get(chargingSpeed)) - .putExtra(BatteryManager.EXTRA_MAX_CHARGING_VOLTAGE, 5000000); + .putExtra(BatteryManager.EXTRA_MAX_CHARGING_VOLTAGE, 5000000) + .putExtra( + BatteryManager.EXTRA_CHARGING_STATUS, + isLonglife + ? BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE + : BatteryManager.CHARGING_POLICY_DEFAULT); } private void prepareTestGetBatteryInfoEnvironment( From 2a19cc1874f4a467355e27c6187f6e844a7b2cbd Mon Sep 17 00:00:00 2001 From: Yuri Lin Date: Tue, 2 Jul 2024 13:42:01 -0400 Subject: [PATCH 7/7] Check that modes can be deleted before adding delete menu option. Migrates to using MenuProvider instead of onCreateOptionsMenu, since the previous approach is apparently now deprecated. Also wraps usages of requireActivity() so that we're sure the activity isn't null instead of potentially throwing an exception. Flag: android.app.modes_ui Bug: 346575126 Test: manually confirmed that manual DND is no longer deletable Change-Id: I872f6054061c019db9a72028cc90cbb123a1cdce --- .../notification/modes/ZenModeFragment.java | 86 ++++++++++++------- .../modes/ZenModeFragmentBase.java | 13 --- 2 files changed, 55 insertions(+), 44 deletions(-) diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index 3a64fb2f1a0..2844f1b052e 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -24,7 +24,9 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.activity.ComponentActivity; import androidx.annotation.NonNull; +import androidx.core.view.MenuProvider; import com.android.settings.R; import com.android.settingslib.applications.ApplicationsState; @@ -39,6 +41,8 @@ public class ZenModeFragment extends ZenModeFragmentBase { // for mode deletion menu private static final int DELETE_MODE = 1; + private ModeMenuProvider mModeMenuProvider; + @Override protected int getPreferenceScreenResId() { return R.xml.modes_rule_settings; @@ -85,11 +89,22 @@ public class ZenModeFragment extends ZenModeFragmentBase { // Set title for the entire screen ZenMode mode = getMode(); - if (mode != null) { - requireActivity().setTitle(mode.getName()); + ComponentActivity activity = getActivity(); + if (mode != null && activity != null) { + activity.setTitle(mode.getName()); + mModeMenuProvider = new ModeMenuProvider(mode); + activity.addMenuProvider(mModeMenuProvider); } } + @Override + public void onStop() { + if (getActivity() != null) { + getActivity().removeMenuProvider(mModeMenuProvider); + } + super.onStop(); + } + @Override public void onDetach() { use(ManualDurationPreferenceController.class).unregisterSettingsObserver(); @@ -102,35 +117,6 @@ public class ZenModeFragment extends ZenModeFragmentBase { return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION; } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - menu.add(Menu.NONE, DELETE_MODE, Menu.NONE, R.string.zen_mode_menu_delete_mode); - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - protected boolean onOptionsItemSelected(MenuItem item, @NonNull ZenMode zenMode) { - switch (item.getItemId()) { - case DELETE_MODE: - new AlertDialog.Builder(mContext) - .setTitle(mContext.getString(R.string.zen_mode_delete_mode_confirmation, - zenMode.getRule().getName())) - .setPositiveButton(R.string.zen_mode_schedule_delete, - (dialog, which) -> { - // start finishing before calling removeMode() so that we don't - // try to update this activity with a nonexistent mode when the - // zen mode config is updated - finish(); - mBackend.removeMode(zenMode); - }) - .setNegativeButton(R.string.cancel, null) - .show(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - @Override protected void updateZenModeState() { // Because this fragment may be asked to finish by the delete menu but not be done doing @@ -140,4 +126,42 @@ public class ZenModeFragment extends ZenModeFragmentBase { } super.updateZenModeState(); } + + private class ModeMenuProvider implements MenuProvider { + private ZenMode mZenMode; + ModeMenuProvider(ZenMode mode) { + mZenMode = mode; + } + + @Override + public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { + if (mZenMode != null && mZenMode.canBeDeleted()) { + // Only deleteable modes should get a delete menu option. + menu.add(Menu.NONE, DELETE_MODE, Menu.NONE, R.string.zen_mode_menu_delete_mode); + } + } + + @Override + public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { + if (mZenMode != null) { + if (menuItem.getItemId() == DELETE_MODE) { + new AlertDialog.Builder(mContext) + .setTitle(mContext.getString(R.string.zen_mode_delete_mode_confirmation, + mZenMode.getRule().getName())) + .setPositiveButton(R.string.zen_mode_schedule_delete, + (dialog, which) -> { + // start finishing before calling removeMode() so that we + // don't try to update this activity with a nonexistent mode + // when the zen mode config is updated + finish(); + mBackend.removeMode(mZenMode); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + return true; + } + } + return false; + } + } } diff --git a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java index b0ad7956a84..f461fc3511c 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java @@ -21,7 +21,6 @@ import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; import android.content.Context; import android.os.Bundle; import android.util.Log; -import android.view.MenuItem; import android.widget.Toast; import androidx.annotation.NonNull; @@ -117,18 +116,6 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase { updateControllers(); } - @Override - public final boolean onOptionsItemSelected(MenuItem item) { - if (mZenMode != null) { - return onOptionsItemSelected(item, mZenMode); - } - return super.onOptionsItemSelected(item); - } - - protected boolean onOptionsItemSelected(MenuItem item, @NonNull ZenMode zenMode) { - return true; - } - private void updateControllers() { if (getPreferenceControllers() == null || mZenMode == null) { return;