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] 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