Merge changes from topic "b308819928-ModesSettingsAppBreakthroughPage" into main

* changes:
  Moves ZenModeBypassingAppsSettings into modes dir
  Adds modes settings page for bypassing apps
This commit is contained in:
Alexander Roederer
2024-05-29 14:20:02 +00:00
committed by Android (Google) Code Review
17 changed files with 1708 additions and 0 deletions

View File

@@ -8020,6 +8020,7 @@
} }
</string> </string>
<!-- Do not disturb: restrict notifications settings title [CHAR LIMIT=80] --> <!-- Do not disturb: restrict notifications settings title [CHAR LIMIT=80] -->
<string name="zen_mode_restrict_notifications_title">Display options for filtered <string name="zen_mode_restrict_notifications_title">Display options for filtered
notifications</string> notifications</string>
@@ -9175,6 +9176,17 @@
<!-- [CHAR LIMIT=50] Zen mode settings: Events (ie: calendar events) category in a list of sounds when events is the first or only element in the list. For example “Events can interrupt" or “Events and touch sounds can interrupt" --> <!-- [CHAR LIMIT=50] Zen mode settings: Events (ie: calendar events) category in a list of sounds when events is the first or only element in the list. For example “Events can interrupt" or “Events and touch sounds can interrupt" -->
<string name="zen_mode_events_list_first">Events</string> <string name="zen_mode_events_list_first">Events</string>
<!-- [CHAR LIMIT=80] Zen mode settings: Title for page to select which apps allowed to interrupt dnd -->
<string name="zen_mode_apps_title">Apps</string>
<!-- [CHAR LIMIT=100] Zen mode settings: subtitle for page to select which apps allowed to interrupt dnd -->
<string name="zen_mode_apps_category">Apps that can interrupt</string>
<!-- [CHAR LIMIT=60] Zen mode settings: selected apps will be able to bypass dnd -->
<string name="zen_mode_apps_priority_apps">Selected apps</string>
<!-- [CHAR LIMIT=60] Zen mode settings: no apps will be able to bypass dnd -->
<string name="zen_mode_apps_none_apps">None</string>
<!-- [CHAR LIMIT=60] Zen mode settings: all apps will be able to bypass dnd -->
<string name="zen_mode_apps_all_apps">All</string>
<!-- [CHAR LIMIT=100] Zen mode settings: Allow apps to bypass DND --> <!-- [CHAR LIMIT=100] Zen mode settings: Allow apps to bypass DND -->
<string name="zen_mode_bypassing_apps">Allow apps to override</string> <string name="zen_mode_bypassing_apps">Allow apps to override</string>
<!-- [CHAR LIMIT=100] Zen mode settings: Allow apps to bypass DND header --> <!-- [CHAR LIMIT=100] Zen mode settings: Allow apps to bypass DND header -->

View File

@@ -34,6 +34,10 @@
android:key="zen_mode_people" android:key="zen_mode_people"
android:title="@string/zen_category_people"/> android:title="@string/zen_category_people"/>
<Preference
android:key="zen_mode_apps"
android:title="@string/zen_category_apps" />
<Preference <Preference
android:key="zen_other_settings" android:key="zen_other_settings"
android:title="@string/zen_category_exceptions" /> android:title="@string/zen_category_exceptions" />

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/zen_mode_apps_title" >
<PreferenceCategory
android:key="zen_mode_apps_category"
android:title="@string/zen_mode_apps_category">
<com.android.settingslib.widget.SelectorWithWidgetPreference
android:key="zen_mode_apps_priority"
android:title="@string/zen_mode_apps_priority_apps"
settings:searchable="false"/>
<com.android.settingslib.widget.SelectorWithWidgetPreference
android:key="zen_mode_apps_none"
android:title="@string/zen_mode_apps_none_apps"
settings:searchable="false"/>
<com.android.settingslib.widget.SelectorWithWidgetPreference
android:key="zen_mode_apps_all"
android:title="@string/zen_mode_apps_all_apps"
settings:searchable="false"/>
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/zen_mode_bypassing_apps_title">
<PreferenceCategory
android:key="zen_mode_bypassing_apps_list"
android:title="@string/zen_mode_bypassing_apps_header">
<!-- apps that have notifications that can bypass DND are added here -->
</PreferenceCategory>
<Preference
android:key="zen_mode_bypassing_apps_add"
android:title="@string/zen_mode_bypassing_apps_add"
android:icon="@drawable/ic_add_24dp"
settings:allowDividerAbove="true" />
<com.android.settingslib.widget.FooterPreference
android:title="@string/zen_mode_bypassing_apps_footer" />
</PreferenceScreen>

View File

@@ -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<ApplicationsState.AppEntry> 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<ApplicationsState.AppEntry> 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();
}
};
}

View File

@@ -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<ApplicationsState.AppEntry> 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<ApplicationsState.AppEntry> 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();
}
};
}

View File

@@ -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<AbstractPreferenceController> createPreferenceControllers(Context context) {
List<AbstractPreferenceController> 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;
}
}

View File

@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -40,6 +40,8 @@ public class ZenModeFragment extends ZenModeFragmentBase {
prefControllers.add(new ZenModeButtonPreferenceController(context, "activate", mBackend)); prefControllers.add(new ZenModeButtonPreferenceController(context, "activate", mBackend));
prefControllers.add(new ZenModePeopleLinkPreferenceController( prefControllers.add(new ZenModePeopleLinkPreferenceController(
context, "zen_mode_people", mBackend)); context, "zen_mode_people", mBackend));
prefControllers.add(new ZenModeAppsLinkPreferenceController(
context, "zen_mode_apps", mBackend));
prefControllers.add(new ZenModeOtherLinkPreferenceController( prefControllers.add(new ZenModeOtherLinkPreferenceController(
context, "zen_other_settings", mBackend)); context, "zen_other_settings", mBackend));
prefControllers.add(new ZenModeDisplayLinkPreferenceController( prefControllers.add(new ZenModeDisplayLinkPreferenceController(

View File

@@ -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<AbstractPreferenceController> 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<AbstractPreferenceController> buildPreferenceControllers(Context context,
@Nullable Application app, @Nullable Fragment host,
@Nullable NotificationBackend notificationBackend) {
final List<AbstractPreferenceController> 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<AbstractPreferenceController> createPreferenceControllers(
Context context) {
return buildPreferenceControllers(context, null, null, null);
}
};
}

View File

@@ -395,4 +395,19 @@ class ZenModeSummaryHelper {
return mContext.getResources().getString(R.string.zen_mode_people_some); 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 "";
}
} }

View File

@@ -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<ApplicationsState.AppEntry> 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<Preference> 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<Preference> 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
}

View File

@@ -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<ApplicationsState.AppEntry> appEntries = new ArrayList<>();
appEntries.add(entry1);
appEntries.add(entry2);
List<NotificationChannel> 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<Preference> prefCaptor = ArgumentCaptor.forClass(Preference.class);
verify(mPreferenceCategory).addPreference(prefCaptor.capture());
Preference pref = prefCaptor.getValue();
assertThat(pref.getKey()).isEqualTo(
ZenModeAllBypassingAppsPreferenceController.KEY_NO_APPS);
}
}

View File

@@ -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());
}
}

View File

@@ -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<ZenMode> 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<ZenMode> 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<ZenMode> 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<ZenMode> 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());
}
}

View File

@@ -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.PEOPLE_TYPE_CONTACTS;
import static android.service.notification.ZenPolicy.VISUAL_EFFECT_AMBIENT; import static android.service.notification.ZenPolicy.VISUAL_EFFECT_AMBIENT;
import static android.service.notification.ZenPolicy.VISUAL_EFFECT_LIGHTS; import static android.service.notification.ZenPolicy.VISUAL_EFFECT_LIGHTS;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.app.AutomaticZenRule; import android.app.AutomaticZenRule;
@@ -29,6 +30,7 @@ import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenDeviceEffects;
import android.service.notification.ZenPolicy; import android.service.notification.ZenPolicy;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -325,4 +327,46 @@ public class ZenModesSummaryHelperTest {
assertThat(mSummaryHelper.getDisplayEffectsSummary(zenMode)).isEqualTo( assertThat(mSummaryHelper.getDisplayEffectsSummary(zenMode)).isEqualTo(
"Notifications partially hidden, grayscale, and 2 more"); "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");
}
} }