diff --git a/res/values/strings.xml b/res/values/strings.xml index a1a067774d6..650bc36d5a0 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8020,6 +8020,7 @@ } + Display options for filtered notifications @@ -9175,6 +9176,17 @@ Events + + Apps + + Apps that can interrupt + + Selected apps + + None + + All + Allow apps to override diff --git a/res/xml/modes_rule_settings.xml b/res/xml/modes_rule_settings.xml index f1ff9772c9b..df560957036 100644 --- a/res/xml/modes_rule_settings.xml +++ b/res/xml/modes_rule_settings.xml @@ -34,6 +34,10 @@ android:key="zen_mode_people" android:title="@string/zen_category_people"/> + + diff --git a/res/xml/zen_mode_apps_settings.xml b/res/xml/zen_mode_apps_settings.xml new file mode 100644 index 00000000000..4ee14e41c5e --- /dev/null +++ b/res/xml/zen_mode_apps_settings.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/zen_mode_select_bypassing_apps.xml b/res/xml/zen_mode_select_bypassing_apps.xml new file mode 100644 index 00000000000..7b5a84d131f --- /dev/null +++ b/res/xml/zen_mode_select_bypassing_apps.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/src/com/android/settings/notification/modes/ZenModeAddBypassingAppsPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAddBypassingAppsPreferenceController.java new file mode 100644 index 00000000000..ccd35eca1d5 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeAddBypassingAppsPreferenceController.java @@ -0,0 +1,260 @@ +/* + * 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.Application; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.UserHandle; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.core.text.BidiFormatter; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.notification.NotificationBackend; +import com.android.settings.notification.app.AppChannelsBypassingDndSettings; +import com.android.settingslib.applications.AppUtils; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.utils.ThreadUtils; +import com.android.settingslib.widget.AppPreference; + +import java.util.ArrayList; +import java.util.List; + + +/** + * When clicked, populates the PreferenceScreen with apps that aren't already bypassing DND. The + * user can click on these Preferences to allow notification channels from the app to bypass DND. + */ +public class ZenModeAddBypassingAppsPreferenceController extends AbstractPreferenceController + implements PreferenceControllerMixin { + + public static final String KEY_NO_APPS = "add_none"; + private static final String KEY = "zen_mode_non_bypassing_apps_list"; + private static final String KEY_ADD = "zen_mode_bypassing_apps_add"; + @Nullable private final NotificationBackend mNotificationBackend; + + @Nullable @VisibleForTesting ApplicationsState mApplicationsState; + @VisibleForTesting PreferenceScreen mPreferenceScreen; + @VisibleForTesting PreferenceCategory mPreferenceCategory; + @VisibleForTesting Context mPrefContext; + + private Preference mAddPreference; + private ApplicationsState.Session mAppSession; + @Nullable private Fragment mHostFragment; + + public ZenModeAddBypassingAppsPreferenceController(Context context, @Nullable Application app, + @Nullable Fragment host, @Nullable NotificationBackend notificationBackend) { + this(context, app == null ? null : ApplicationsState.getInstance(app), host, + notificationBackend); + } + + private ZenModeAddBypassingAppsPreferenceController(Context context, + @Nullable ApplicationsState appState, @Nullable Fragment host, + @Nullable NotificationBackend notificationBackend) { + super(context); + mNotificationBackend = notificationBackend; + mApplicationsState = appState; + mHostFragment = host; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + mPreferenceScreen = screen; + mAddPreference = screen.findPreference(KEY_ADD); + mAddPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + mAddPreference.setVisible(false); + if (mApplicationsState != null && mHostFragment != null) { + mAppSession = mApplicationsState.newSession(mAppSessionCallbacks, + mHostFragment.getLifecycle()); + } + return true; + } + }); + mPrefContext = screen.getContext(); + super.displayPreference(screen); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + /** + * Call this method to trigger the app list to refresh. + */ + public void updateAppList() { + if (mAppSession == null) { + return; + } + + ApplicationsState.AppFilter filter = android.multiuser.Flags.enablePrivateSpaceFeatures() + && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace() + ? ApplicationsState.FILTER_ENABLED_NOT_QUIET + : ApplicationsState.FILTER_ALL_ENABLED; + mAppSession.rebuild(filter, ApplicationsState.ALPHA_COMPARATOR); + } + + // Set the icon for the given preference to the entry icon from cache if available, or look + // it up. + private void updateIcon(Preference pref, ApplicationsState.AppEntry entry) { + synchronized (entry) { + final Drawable cachedIcon = AppUtils.getIconFromCache(entry); + if (cachedIcon != null && entry.mounted) { + pref.setIcon(cachedIcon); + } else { + ThreadUtils.postOnBackgroundThread(() -> { + final Drawable icon = AppUtils.getIcon(mPrefContext, entry); + if (icon != null) { + ThreadUtils.postOnMainThread(() -> pref.setIcon(icon)); + } + }); + } + } + } + + @VisibleForTesting + void updateAppList(List apps) { + if (apps == null) { + return; + } + + if (mPreferenceCategory == null) { + mPreferenceCategory = new PreferenceCategory(mPrefContext); + mPreferenceCategory.setTitle(R.string.zen_mode_bypassing_apps_add_header); + mPreferenceScreen.addPreference(mPreferenceCategory); + } + + boolean doAnyAppsPassCriteria = false; + for (ApplicationsState.AppEntry app : apps) { + String pkg = app.info.packageName; + final String key = getKey(pkg, app.info.uid); + final int appChannels = mNotificationBackend.getChannelCount(pkg, app.info.uid); + final int appChannelsBypassingDnd = mNotificationBackend + .getNotificationChannelsBypassingDnd(pkg, app.info.uid).getList().size(); + if (appChannelsBypassingDnd == 0 && appChannels > 0) { + doAnyAppsPassCriteria = true; + } + + Preference pref = mPreferenceCategory.findPreference(key); + + if (pref == null) { + if (appChannelsBypassingDnd == 0 && appChannels > 0) { + // does not exist but should + pref = new AppPreference(mPrefContext); + pref.setKey(key); + pref.setOnPreferenceClickListener(preference -> { + Bundle args = new Bundle(); + args.putString(AppInfoBase.ARG_PACKAGE_NAME, app.info.packageName); + args.putInt(AppInfoBase.ARG_PACKAGE_UID, app.info.uid); + new SubSettingLauncher(mContext) + .setDestination(AppChannelsBypassingDndSettings.class.getName()) + .setArguments(args) + .setResultListener(mHostFragment, 0) + .setUserHandle(new UserHandle(UserHandle.getUserId(app.info.uid))) + .setSourceMetricsCategory( + SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APP) + .launch(); + return true; + }); + pref.setTitle(BidiFormatter.getInstance().unicodeWrap(app.label)); + updateIcon(pref, app); + mPreferenceCategory.addPreference(pref); + } + } else if (appChannelsBypassingDnd != 0 || appChannels == 0) { + // exists but shouldn't anymore + mPreferenceCategory.removePreference(pref); + } + } + + Preference pref = mPreferenceCategory.findPreference(KEY_NO_APPS); + if (!doAnyAppsPassCriteria) { + if (pref == null) { + pref = new Preference(mPrefContext); + pref.setKey(KEY_NO_APPS); + pref.setTitle(R.string.zen_mode_bypassing_apps_none); + } + mPreferenceCategory.addPreference(pref); + } else if (pref != null) { + mPreferenceCategory.removePreference(pref); + } + } + + static String getKey(String pkg, int uid) { + return "add|" + pkg + "|" + uid; + } + + private final ApplicationsState.Callbacks mAppSessionCallbacks = + new ApplicationsState.Callbacks() { + + @Override + public void onRunningStateChanged(boolean running) { + + } + + @Override + public void onPackageListChanged() { + + } + + @Override + public void onRebuildComplete(ArrayList apps) { + updateAppList(apps); + } + + @Override + public void onPackageIconChanged() { + updateAppList(); + } + + @Override + public void onPackageSizeChanged(String packageName) { + + } + + @Override + public void onAllSizesComputed() { } + + @Override + public void onLauncherInfoChanged() { + + } + + @Override + public void onLoadEntriesCompleted() { + updateAppList(); + } + }; +} diff --git a/src/com/android/settings/notification/modes/ZenModeAllBypassingAppsPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAllBypassingAppsPreferenceController.java new file mode 100644 index 00000000000..922ac5ecf5b --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeAllBypassingAppsPreferenceController.java @@ -0,0 +1,244 @@ +/* + * 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.Application; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.UserHandle; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.core.text.BidiFormatter; +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.notification.NotificationBackend; +import com.android.settings.notification.app.AppChannelsBypassingDndSettings; +import com.android.settingslib.applications.AppUtils; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.utils.ThreadUtils; +import com.android.settingslib.widget.AppPreference; + +import java.util.ArrayList; +import java.util.List; + +/** + * Adds a preference to the PreferenceScreen for each notification channel that can bypass DND. + */ +public class ZenModeAllBypassingAppsPreferenceController extends AbstractPreferenceController + implements PreferenceControllerMixin { + public static final String KEY_NO_APPS = "all_none"; + private static final String KEY = "zen_mode_bypassing_apps_list"; + + @Nullable private final NotificationBackend mNotificationBackend; + + @Nullable @VisibleForTesting ApplicationsState mApplicationsState; + @VisibleForTesting PreferenceCategory mPreferenceCategory; + @VisibleForTesting Context mPrefContext; + + private ApplicationsState.Session mAppSession; + @Nullable private Fragment mHostFragment; + + public ZenModeAllBypassingAppsPreferenceController(Context context, @Nullable Application app, + @Nullable Fragment host, @Nullable NotificationBackend notificationBackend) { + this(context, app == null ? null : ApplicationsState.getInstance(app), host, + notificationBackend); + } + + private ZenModeAllBypassingAppsPreferenceController(Context context, + @Nullable ApplicationsState appState, @Nullable Fragment host, + @Nullable NotificationBackend notificationBackend) { + super(context); + mNotificationBackend = notificationBackend; + mApplicationsState = appState; + mHostFragment = host; + + if (mApplicationsState != null && host != null) { + mAppSession = mApplicationsState.newSession(mAppSessionCallbacks, host.getLifecycle()); + } + } + + @Override + public void displayPreference(PreferenceScreen screen) { + mPreferenceCategory = screen.findPreference(KEY); + mPrefContext = screen.getContext(); + updateAppList(); + super.displayPreference(screen); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + /** + * Call this method to trigger the app list to refresh. + */ + public void updateAppList() { + if (mAppSession == null) { + return; + } + + ApplicationsState.AppFilter filter = android.multiuser.Flags.enablePrivateSpaceFeatures() + && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace() + ? ApplicationsState.FILTER_ENABLED_NOT_QUIET + : ApplicationsState.FILTER_ALL_ENABLED; + mAppSession.rebuild(filter, ApplicationsState.ALPHA_COMPARATOR); + } + + // Set the icon for the given preference to the entry icon from cache if available, or look + // it up. + private void updateIcon(Preference pref, ApplicationsState.AppEntry entry) { + synchronized (entry) { + final Drawable cachedIcon = AppUtils.getIconFromCache(entry); + if (cachedIcon != null && entry.mounted) { + pref.setIcon(cachedIcon); + } else { + ThreadUtils.postOnBackgroundThread(() -> { + final Drawable icon = AppUtils.getIcon(mPrefContext, entry); + if (icon != null) { + ThreadUtils.postOnMainThread(() -> pref.setIcon(icon)); + } + }); + } + } + } + + @VisibleForTesting + void updateAppList(List apps) { + if (mPreferenceCategory == null || apps == null) { + return; + } + + boolean doAnyAppsPassCriteria = false; + for (ApplicationsState.AppEntry app : apps) { + String pkg = app.info.packageName; + final String key = getKey(pkg, app.info.uid); + final int appChannels = mNotificationBackend.getChannelCount(pkg, app.info.uid); + final int appChannelsBypassingDnd = mNotificationBackend + .getNotificationChannelsBypassingDnd(pkg, app.info.uid).getList().size(); + if (appChannelsBypassingDnd > 0) { + doAnyAppsPassCriteria = true; + } + + Preference pref = mPreferenceCategory.findPreference(key); + if (pref == null) { + if (appChannelsBypassingDnd > 0) { + // does not exist but should + pref = new AppPreference(mPrefContext); + pref.setKey(key); + pref.setOnPreferenceClickListener(preference -> { + Bundle args = new Bundle(); + args.putString(AppInfoBase.ARG_PACKAGE_NAME, app.info.packageName); + args.putInt(AppInfoBase.ARG_PACKAGE_UID, app.info.uid); + new SubSettingLauncher(mContext) + .setDestination(AppChannelsBypassingDndSettings.class.getName()) + .setArguments(args) + .setUserHandle(UserHandle.getUserHandleForUid(app.info.uid)) + .setResultListener(mHostFragment, 0) + .setSourceMetricsCategory( + SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APP) + .launch(); + return true; + }); + pref.setTitle(BidiFormatter.getInstance().unicodeWrap(app.label)); + updateIcon(pref, app); + if (appChannels > appChannelsBypassingDnd) { + pref.setSummary(R.string.zen_mode_bypassing_apps_summary_some); + } else { + pref.setSummary(R.string.zen_mode_bypassing_apps_summary_all); + } + mPreferenceCategory.addPreference(pref); + } + } else if (appChannelsBypassingDnd == 0) { + // exists but shouldn't anymore + mPreferenceCategory.removePreference(pref); + } + } + + Preference pref = mPreferenceCategory.findPreference(KEY_NO_APPS); + if (!doAnyAppsPassCriteria) { + if (pref == null) { + pref = new Preference(mPrefContext); + pref.setKey(KEY_NO_APPS); + pref.setTitle(R.string.zen_mode_bypassing_apps_none); + } + mPreferenceCategory.addPreference(pref); + } else if (pref != null) { + mPreferenceCategory.removePreference(pref); + } + } + + /** + * Create a unique key to idenfity an AppPreference + */ + static String getKey(String pkg, int uid) { + return "all|" + pkg + "|" + uid; + } + + private final ApplicationsState.Callbacks mAppSessionCallbacks = + new ApplicationsState.Callbacks() { + + @Override + public void onRunningStateChanged(boolean running) { + } + + @Override + public void onPackageListChanged() { + } + + @Override + public void onRebuildComplete(ArrayList apps) { + updateAppList(apps); + } + + @Override + public void onPackageIconChanged() { + } + + @Override + public void onPackageSizeChanged(String packageName) { + } + + @Override + public void onAllSizesComputed() { } + + @Override + public void onLauncherInfoChanged() { + } + + @Override + public void onLoadEntriesCompleted() { + updateAppList(); + } + }; +} diff --git a/src/com/android/settings/notification/modes/ZenModeAppsFragment.java b/src/com/android/settings/notification/modes/ZenModeAppsFragment.java new file mode 100644 index 00000000000..73329a242fd --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeAppsFragment.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import android.app.settings.SettingsEnums; +import android.content.Context; + +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +/** + * Mode > Apps + */ +public class ZenModeAppsFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List controllers = new ArrayList<>(); + controllers.add(new ZenModeAppsPreferenceController( + context, ZenModeAppsPreferenceController.KEY_PRIORITY, mBackend)); + controllers.add(new ZenModeAppsPreferenceController( + context, ZenModeAppsPreferenceController.KEY_NONE, mBackend)); + // TODO: b/308819928 - The manual DND mode cannot have the ALL type; + // unify the controllers into one and only create a preference if isManualDnd is false. + controllers.add(new ZenModeAppsPreferenceController( + context, ZenModeAppsPreferenceController.KEY_ALL, mBackend)); + return controllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.zen_mode_apps_settings; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.NOTIFICATION_ZEN_MODE_PRIORITY; + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java new file mode 100644 index 00000000000..42b58b1346e --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; + +import com.android.settings.core.SubSettingLauncher; + +/** + * Preference with a link and summary about what apps can break through the mode + */ +public class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceController { + + private final ZenModeSummaryHelper mSummaryHelper; + + public ZenModeAppsLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); + } + + @Override + public void updateState(Preference preference, @NonNull ZenMode zenMode) { + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, zenMode.getId()); + // TODO(b/332937635): Update metrics category + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModeAppsFragment.class.getName()) + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + preference.setSummary(mSummaryHelper.getAppsSummary(zenMode)); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java new file mode 100644 index 00000000000..704bce04504 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.app.settings.SettingsEnums; +import android.content.Context; +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; +import androidx.preference.TwoStatePreference; + +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +public class ZenModeAppsPreferenceController extends + AbstractZenModePreferenceController implements Preference.OnPreferenceChangeListener { + + static final String KEY_PRIORITY = "zen_mode_apps_priority"; + static final String KEY_NONE = "zen_mode_apps_none"; + static final String KEY_ALL = "zen_mode_apps_all"; + + String mModeId; + + + public ZenModeAppsPreferenceController(@NonNull Context context, + @NonNull String key, @Nullable ZenModesBackend backend) { + super(context, key, backend); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + SelectorWithWidgetPreference pref = screen.findPreference(getPreferenceKey()); + if (pref != null) { + pref.setOnClickListener(mSelectorClickListener); + + // Adds the widget to only the priority category. + if (getPreferenceKey().equals(KEY_PRIORITY)) { + pref.setExtraWidgetOnClickListener(p -> { + launchPrioritySettings(); + }); + } + } + super.displayPreference(screen); + } + + @Override + public void updateState(Preference preference, @NonNull ZenMode zenMode) { + mModeId = zenMode.getId(); + TwoStatePreference pref = (TwoStatePreference) preference; + switch (getPreferenceKey()) { + case KEY_PRIORITY: + boolean policy_priority = zenMode.getPolicy().getAllowedChannels() + == ZenPolicy.CHANNEL_POLICY_PRIORITY; + pref.setChecked(policy_priority); + break; + case KEY_NONE: + boolean policy_none = zenMode.getPolicy().getAllowedChannels() + == ZenPolicy.CHANNEL_POLICY_NONE; + pref.setChecked(policy_none); + break; + case KEY_ALL: + // A UI-only setting; the underlying policy never actually has this value, + // but ZenMode acts as though it does for the sake of UI consistency. + boolean policy_all = zenMode.getPolicy().getAllowedChannels() + == ZenMode.CHANNEL_POLICY_ALL; + pref.setChecked(policy_all); + break; + } + } + + @Override + public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) { + switch (getPreferenceKey()) { + case KEY_PRIORITY: + return savePolicy(p -> p.allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)); + case KEY_NONE: + return savePolicy(p -> p.allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)); + case KEY_ALL: + return savePolicy(p -> p.allowChannels(ZenMode.CHANNEL_POLICY_ALL)); + } + return true; + } + + @VisibleForTesting + SelectorWithWidgetPreference.OnClickListener mSelectorClickListener = + new SelectorWithWidgetPreference.OnClickListener() { + @Override + public void onRadioButtonClicked(SelectorWithWidgetPreference preference) { + onPreferenceChange(preference, true); + } + }; + + private void launchPrioritySettings() { + Bundle bundle = new Bundle(); + if (mModeId != null) { + bundle.putString(MODE_ID, mModeId); + } + // TODO(b/332937635): Update metrics category + new SubSettingLauncher(mContext) + .setDestination(ZenModeSelectBypassingAppsFragment.class.getName()) + .setSourceMetricsCategory(SettingsEnums.SETTINGS_ZEN_NOTIFICATIONS) + .setArguments(bundle) + .launch(); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index b8666bdabc2..1f6ae45c462 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -40,6 +40,8 @@ public class ZenModeFragment extends ZenModeFragmentBase { prefControllers.add(new ZenModeButtonPreferenceController(context, "activate", mBackend)); prefControllers.add(new ZenModePeopleLinkPreferenceController( context, "zen_mode_people", mBackend)); + prefControllers.add(new ZenModeAppsLinkPreferenceController( + context, "zen_mode_apps", mBackend)); prefControllers.add(new ZenModeOtherLinkPreferenceController( context, "zen_other_settings", mBackend)); prefControllers.add(new ZenModeDisplayLinkPreferenceController( diff --git a/src/com/android/settings/notification/modes/ZenModeSelectBypassingAppsFragment.java b/src/com/android/settings/notification/modes/ZenModeSelectBypassingAppsFragment.java new file mode 100644 index 00000000000..8b682b92f1a --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeSelectBypassingAppsFragment.java @@ -0,0 +1,93 @@ +/* + * 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.Activity; +import android.app.Application; +import android.app.settings.SettingsEnums; +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.android.settings.R; +import com.android.settings.notification.NotificationBackend; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.search.Indexable; +import com.android.settingslib.search.SearchIndexable; + +import java.util.ArrayList; +import java.util.List; + +@SearchIndexable +public class ZenModeSelectBypassingAppsFragment extends ZenModeFragmentBase implements + Indexable { + private static final String TAG = "ZenBypassingApps"; + + @Override + protected List createPreferenceControllers(Context context) { + final Activity activity = getActivity(); + final Application app; + if (activity != null) { + app = activity.getApplication(); + } else { + app = null; + } + return buildPreferenceControllers(context, app, this, new NotificationBackend()); + } + + private static List buildPreferenceControllers(Context context, + @Nullable Application app, @Nullable Fragment host, + @Nullable NotificationBackend notificationBackend) { + final List controllers = new ArrayList<>(); + controllers.add(new ZenModeAllBypassingAppsPreferenceController(context, app, host, + notificationBackend)); + controllers.add(new ZenModeAddBypassingAppsPreferenceController(context, app, host, + notificationBackend)); + return controllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.zen_mode_select_bypassing_apps; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public int getMetricsCategory() { + // TODO(b/332937635): Update metrics category + return SettingsEnums.NOTIFICATION_ZEN_MODE_OVERRIDING_APPS; + } + + /** + * For Search. + */ + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.zen_mode_select_bypassing_apps) { + + @Override + public List createPreferenceControllers( + Context context) { + return buildPreferenceControllers(context, null, null, null); + } + }; +} diff --git a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java index 41a3d20d0a3..b4075cde656 100644 --- a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java +++ b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java @@ -395,4 +395,19 @@ class ZenModeSummaryHelper { return mContext.getResources().getString(R.string.zen_mode_people_some); } } + + /** + * Generates a summary to display under the top level "Apps" preference for a mode. + */ + public String getAppsSummary(ZenMode zenMode) { + // TODO: b/308819928 - Set summary using priority app list if Selected Apps Chosen. + if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_PRIORITY) { + return mContext.getResources().getString(R.string.zen_mode_apps_priority_apps); + } else if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) { + return mContext.getResources().getString(R.string.zen_mode_apps_none_apps); + } else if (zenMode.getPolicy().getAllowedChannels() == ZenMode.CHANNEL_POLICY_ALL) { + return mContext.getResources().getString(R.string.zen_mode_apps_all_apps); + } + return ""; + } } diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAddBypassingAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAddBypassingAppsPreferenceControllerTest.java new file mode 100644 index 00000000000..bca1ccf58c6 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAddBypassingAppsPreferenceControllerTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.NotificationChannel; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.ParceledListSlice; + +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + +import com.android.settings.notification.NotificationBackend; +import com.android.settingslib.applications.ApplicationsState; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class ZenModeAddBypassingAppsPreferenceControllerTest { + + @Mock + private NotificationBackend mBackend; + @Mock + private PreferenceCategory mPreferenceCategory; + @Mock + private ApplicationsState mApplicationState; + private ZenModeAddBypassingAppsPreferenceController mController; + private Context mContext; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + + mController = new ZenModeAddBypassingAppsPreferenceController( + mContext, null, mock(Fragment.class), mBackend); + mController.mPreferenceCategory = mPreferenceCategory; + mController.mApplicationsState = mApplicationState; + mController.mPrefContext = mContext; + } + + @Test + public void testIsAvailable() { + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void testUpdateAppList() { + // GIVEN there's an app with bypassing channels, app without any channels, and then an app + // with notification channels but none that can bypass DND + ApplicationsState.AppEntry appWithBypassingChannels = + mock(ApplicationsState.AppEntry.class); + appWithBypassingChannels.info = new ApplicationInfo(); + appWithBypassingChannels.info.packageName = "appWithBypassingChannels"; + appWithBypassingChannels.info.uid = 0; + when(mBackend.getNotificationChannelsBypassingDnd( + appWithBypassingChannels.info.packageName, + appWithBypassingChannels.info.uid)) + .thenReturn(new ParceledListSlice<>( + Arrays.asList(mock(NotificationChannel.class)))); + when(mBackend.getChannelCount( + appWithBypassingChannels.info.packageName, + appWithBypassingChannels.info.uid)) + .thenReturn(5); + + ApplicationsState.AppEntry appWithoutChannels = mock(ApplicationsState.AppEntry.class); + appWithoutChannels.info = new ApplicationInfo(); + appWithoutChannels.info.packageName = "appWithoutChannels"; + appWithoutChannels.info.uid = 0; + when(mBackend.getChannelCount( + appWithoutChannels.info.packageName, + appWithoutChannels.info.uid)) + .thenReturn(0); + when(mBackend.getNotificationChannelsBypassingDnd( + appWithoutChannels.info.packageName, + appWithoutChannels.info.uid)) + .thenReturn(new ParceledListSlice<>(new ArrayList<>())); + + ApplicationsState.AppEntry appWithChannelsNoneBypassing = + mock(ApplicationsState.AppEntry.class); + appWithChannelsNoneBypassing.info = new ApplicationInfo(); + appWithChannelsNoneBypassing.info.packageName = "appWithChannelsNoneBypassing"; + appWithChannelsNoneBypassing.info.uid = 0; + when(mBackend.getChannelCount( + appWithChannelsNoneBypassing.info.packageName, + appWithChannelsNoneBypassing.info.uid)) + .thenReturn(5); + when(mBackend.getNotificationChannelsBypassingDnd( + appWithChannelsNoneBypassing.info.packageName, + appWithChannelsNoneBypassing.info.uid)) + .thenReturn(new ParceledListSlice<>(new ArrayList<>())); + + List appEntries = new ArrayList<>(); + appEntries.add(appWithBypassingChannels); + appEntries.add(appWithoutChannels); + appEntries.add(appWithChannelsNoneBypassing); + + // WHEN the controller updates the app list with the app entries + mController.updateAppList(appEntries); + + // THEN only the appWithChannelsNoneBypassing makes it to the app list + ArgumentCaptor prefCaptor = ArgumentCaptor.forClass(Preference.class); + verify(mPreferenceCategory).addPreference(prefCaptor.capture()); + + Preference pref = prefCaptor.getValue(); + assertThat(pref.getKey()).isEqualTo( + ZenModeAddBypassingAppsPreferenceController.getKey( + appWithChannelsNoneBypassing.info.packageName, + appWithChannelsNoneBypassing.info.uid)); + } + + @Test + public void testUpdateAppList_nullApps() { + mController.updateAppList(null); + verify(mPreferenceCategory, never()).addPreference(any()); + } + + @Test + public void testUpdateAppList_emptyAppList() { + // WHEN there are no apps + mController.updateAppList(new ArrayList<>()); + + // THEN only the appWithChannelsNoneBypassing makes it to the app list + ArgumentCaptor prefCaptor = ArgumentCaptor.forClass(Preference.class); + verify(mPreferenceCategory).addPreference(prefCaptor.capture()); + + Preference pref = prefCaptor.getValue(); + assertThat(pref.getKey()).isEqualTo( + ZenModeAddBypassingAppsPreferenceController.KEY_NO_APPS); + } + + // TODO(b/331624810): Add tests to verify updateAppList() when the filter is + // ApplicationsState.FILTER_ENABLED_NOT_QUIET +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAllBypassingAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAllBypassingAppsPreferenceControllerTest.java new file mode 100644 index 00000000000..3114a2d01ca --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAllBypassingAppsPreferenceControllerTest.java @@ -0,0 +1,129 @@ +/* + * 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.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.NotificationChannel; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.ParceledListSlice; + +import androidx.fragment.app.Fragment; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + +import com.android.settings.notification.NotificationBackend; +import com.android.settingslib.applications.ApplicationsState; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class ZenModeAllBypassingAppsPreferenceControllerTest { + private ZenModeAllBypassingAppsPreferenceController mController; + + private Context mContext; + @Mock + private NotificationBackend mBackend; + @Mock + private PreferenceCategory mPreferenceCategory; + @Mock + private ApplicationsState mApplicationState; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + + mController = new ZenModeAllBypassingAppsPreferenceController( + mContext, null, mock(Fragment.class), mBackend); + mController.mPreferenceCategory = mPreferenceCategory; + mController.mApplicationsState = mApplicationState; + mController.mPrefContext = mContext; + } + + @Test + public void testIsAvailable() { + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void testUpdateAppList() { + // WHEN there's two apps with notification channels that bypass DND + ApplicationsState.AppEntry entry1 = mock(ApplicationsState.AppEntry.class); + entry1.info = new ApplicationInfo(); + entry1.info.packageName = "test"; + entry1.info.uid = 0; + + ApplicationsState.AppEntry entry2 = mock(ApplicationsState.AppEntry.class); + entry2.info = new ApplicationInfo(); + entry2.info.packageName = "test2"; + entry2.info.uid = 0; + + List appEntries = new ArrayList<>(); + appEntries.add(entry1); + appEntries.add(entry2); + List channelsBypassing = new ArrayList<>(); + channelsBypassing.add(mock(NotificationChannel.class)); + channelsBypassing.add(mock(NotificationChannel.class)); + when(mBackend.getNotificationChannelsBypassingDnd(anyString(), + anyInt())).thenReturn(new ParceledListSlice<>(channelsBypassing)); + + // THEN there's are two preferences + mController.updateAppList(appEntries); + verify(mPreferenceCategory, times(2)).addPreference(any()); + } + + @Test + public void testUpdateAppList_nullApps() { + mController.updateAppList(null); + verify(mPreferenceCategory, never()).addPreference(any()); + } + + @Test + public void testUpdateAppList_emptyAppList() { + // WHEN there are no apps + mController.updateAppList(new ArrayList<>()); + + // THEN only the appWithChannelsNoneBypassing makes it to the app list + ArgumentCaptor prefCaptor = ArgumentCaptor.forClass(Preference.class); + verify(mPreferenceCategory).addPreference(prefCaptor.capture()); + + Preference pref = prefCaptor.getValue(); + assertThat(pref.getKey()).isEqualTo( + ZenModeAllBypassingAppsPreferenceController.KEY_NO_APPS); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..67e1f9f919a --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java @@ -0,0 +1,76 @@ +/* + * 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 org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; + +import androidx.preference.Preference; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public final class ZenModeAppsLinkPreferenceControllerTest { + + private ZenModeAppsLinkPreferenceController mController; + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mController = new ZenModeAppsLinkPreferenceController( + mContext, "controller_key", mBackend); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testHasSummary() { + Preference pref = mock(Preference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(), true); + mController.updateZenMode(pref, zenMode); + verify(pref).setSummary(any()); + } + +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsPreferenceControllerTest.java new file mode 100644 index 00000000000..750453dabe6 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsPreferenceControllerTest.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; +import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE; +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; + +import static com.android.settings.notification.modes.ZenModeAppsPreferenceController.KEY_ALL; +import static com.android.settings.notification.modes.ZenModeAppsPreferenceController.KEY_NONE; +import static com.android.settings.notification.modes.ZenModeAppsPreferenceController.KEY_PRIORITY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; + +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public final class ZenModeAppsPreferenceControllerTest { + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + private ZenModeAppsPreferenceController mPriorityController; + private ZenModeAppsPreferenceController mAllController; + private ZenModeAppsPreferenceController mNoneController; + + private SelectorWithWidgetPreference mPriorityPref; + private SelectorWithWidgetPreference mAllPref; + private SelectorWithWidgetPreference mNonePref; + private PreferenceCategory mPrefCategory; + private PreferenceScreen mPreferenceScreen; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + + mPriorityController = new ZenModeAppsPreferenceController(mContext, KEY_PRIORITY, mBackend); + mNoneController = new ZenModeAppsPreferenceController(mContext, KEY_NONE, mBackend); + mAllController = new ZenModeAppsPreferenceController(mContext, KEY_ALL, mBackend); + + mPriorityPref = makePreference(KEY_PRIORITY, mPriorityController); + mAllPref = makePreference(KEY_ALL, mAllController); + mNonePref = makePreference(KEY_NONE, mNoneController); + + mPrefCategory = new PreferenceCategory(mContext); + mPrefCategory.setKey("zen_mode_apps_category"); + + PreferenceManager preferenceManager = new PreferenceManager(mContext); + mPreferenceScreen = preferenceManager.createPreferenceScreen(mContext); + + mPreferenceScreen.addPreference(mPrefCategory); + mPrefCategory.addPreference(mPriorityPref); + mPrefCategory.addPreference(mAllPref); + mPrefCategory.addPreference(mNonePref); + + mAllController.displayPreference(mPreferenceScreen); + mPriorityController.displayPreference(mPreferenceScreen); + mNoneController.displayPreference(mPreferenceScreen); + } + + private SelectorWithWidgetPreference makePreference(String key, + ZenModeAppsPreferenceController controller) { + final SelectorWithWidgetPreference pref = new SelectorWithWidgetPreference(mContext, false); + pref.setKey(key); + pref.setOnClickListener(controller.mSelectorClickListener); + return pref; + } + + @Test + public void testUpdateState_All() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenMode.CHANNEL_POLICY_ALL) + .build()) + .build(), true); + mAllController.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testUpdateState_All_Unchecked() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE) + .build()) + .build(), true); + mAllController.updateZenMode(preference, zenMode); + + verify(preference).setChecked(false); + } + + @Test + public void testUpdateState_None() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE) + .build()) + .build(), true); + mNoneController.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testUpdateState_None_Unchecked() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenMode.CHANNEL_POLICY_ALL) + .build()) + .build(), true); + mNoneController.updateZenMode(preference, zenMode); + + verify(preference).setChecked(false); + } + + @Test + public void testUpdateState_Priority() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY) + .build()) + .build(), true); + mPriorityController.updateZenMode(preference, zenMode); + + verify(preference).setChecked(true); + } + + @Test + public void testUpdateState_Priority_Unchecked() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE) + .build()) + .build(), true); + mPriorityController.updateZenMode(preference, zenMode); + + verify(preference).setChecked(false); + } + + @Test + public void testOnPreferenceChange_All() { + TwoStatePreference preference = mock(TwoStatePreference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenMode.CHANNEL_POLICY_ALL) + .build()) + .build(), true); + + mAllController.updateZenMode(preference, zenMode); + mAllController.onPreferenceChange(preference, true); + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + + assertThat(captor.getValue().getPolicy().getAllowedChannels()) + .isEqualTo(ZenMode.CHANNEL_POLICY_ALL); + } + + @Test + public void testPreferenceClick_passesCorrectCheckedState_All() { + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE) + .build()) + .build(), true); + + + mAllController.updateZenMode(mAllPref, zenMode); + mNoneController.updateZenMode(mNonePref, zenMode); + mPriorityController.updateZenMode(mPriorityPref, zenMode); + + // MPME is checked; ALL and PRIORITY are unchecked. + assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE)) + .isChecked()); + assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_ALL)) + .isChecked()); + assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY)) + .isChecked()); + + mPrefCategory.findPreference(KEY_ALL).performClick(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + // Checks the policy value for ALL is set. + // The important part is that the interruption filter is propagated to the backend. + assertThat(captor.getValue().getRule().getInterruptionFilter()) + .isEqualTo(INTERRUPTION_FILTER_ALL); + // ALL is now checked; others are unchecked. + assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_ALL)) + .isChecked()); + assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE)) + .isChecked()); + assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY)) + .isChecked()); + } + + @Test + public void testPreferenceClick_passesCorrectCheckedState_None() { + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY) + .build()) + .build(), true); + + mAllController.updateZenMode(mAllPref, zenMode); + mNoneController.updateZenMode(mNonePref, zenMode); + mPriorityController.updateZenMode(mPriorityPref, zenMode); + + assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_ALL)) + .isChecked()); + assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE)) + .isChecked()); + assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY)) + .isChecked()); + + // Click on NONE + mPrefCategory.findPreference(KEY_NONE).performClick(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + // NONE is not actually propagated to the backend as an interruption filter; + // the filter is set to priority, and sounds and visual effects are disallowed. + // See AbstractZenModePreferenceController. + assertThat(captor.getValue().getRule().getInterruptionFilter()) + .isEqualTo(INTERRUPTION_FILTER_PRIORITY); + // NONE is now checked; others are unchecked. + assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE)) + .isChecked()); + assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_ALL)) + .isChecked()); + assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY)) + .isChecked()); + } + + @Test + public void testPreferenceClick_passesCorrectCheckedState_Priority() { + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE) + .build()) + .build(), true); + + mAllController.updateZenMode(mAllPref, zenMode); + mNoneController.updateZenMode(mNonePref, zenMode); + mPriorityController.updateZenMode(mPriorityPref, zenMode); + + assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE)) + .isChecked()); + assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_ALL)) + .isChecked()); + assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY)) + .isChecked()); + + // Click on PRIORITY + mPrefCategory.findPreference(KEY_PRIORITY).performClick(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); + verify(mBackend).updateMode(captor.capture()); + // Checks the policy value for PRIORITY is propagated to the backend. + assertThat(captor.getValue().getRule().getInterruptionFilter()) + .isEqualTo(INTERRUPTION_FILTER_PRIORITY); + // PRIORITY is now checked; others are unchecked. + assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY)) + .isChecked()); + assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_ALL)) + .isChecked()); + assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE)) + .isChecked()); + } + +} diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java index 3e41778cdc0..d8c8bf0bfb5 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java @@ -22,6 +22,7 @@ import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; import static android.service.notification.ZenPolicy.VISUAL_EFFECT_AMBIENT; import static android.service.notification.ZenPolicy.VISUAL_EFFECT_LIGHTS; + import static com.google.common.truth.Truth.assertThat; import android.app.AutomaticZenRule; @@ -29,6 +30,7 @@ import android.content.Context; import android.net.Uri; import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenPolicy; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -325,4 +327,46 @@ public class ZenModesSummaryHelperTest { assertThat(mSummaryHelper.getDisplayEffectsSummary(zenMode)).isEqualTo( "Notifications partially hidden, grayscale, and 2 more"); } + + @Test + public void getAppsSummary_all() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenMode.CHANNEL_POLICY_ALL) + .build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getAppsSummary(zenMode)).isEqualTo("All"); + } + + @Test + public void getAppsSummary_none() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE) + .build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getAppsSummary(zenMode)).isEqualTo("None"); + } + + @Test + public void getAppsSummary_priorityApps() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder() + .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY) + .build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getAppsSummary(zenMode)).isEqualTo("Selected apps"); + } }