Moves ZenModeBypassingAppsSettings into modes dir

Copies these files into modes dir, so we can continue to deprecate the
old zen directory

Bug: 308819928
Test: flash+test, atest ZenModeAllBypassingAppsPreferenceControllerTest,
atestZenModeAddBypassingAppsPreferenceControllerTest
Flag: android.app.modes_ui
Change-Id: Ib18930bb16a3d675e460028f5c52ef32060541ad

Change-Id: Id6c8d834bbd106fef068c5f04acf8cd71229a5e3
This commit is contained in:
Alexander Roederer
2024-05-24 22:00:55 +00:00
parent c512046eb9
commit c50457613e
7 changed files with 943 additions and 2 deletions

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

@@ -16,8 +16,11 @@
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;
@@ -28,7 +31,6 @@ import androidx.preference.PreferenceScreen;
import androidx.preference.TwoStatePreference;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.notification.zen.ZenModeBypassingAppsSettings;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
public class ZenModeAppsPreferenceController extends
@@ -38,6 +40,8 @@ public class ZenModeAppsPreferenceController extends
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) {
@@ -62,6 +66,7 @@ public class ZenModeAppsPreferenceController extends
@Override
public void updateState(Preference preference, @NonNull ZenMode zenMode) {
mModeId = zenMode.getId();
TwoStatePreference pref = (TwoStatePreference) preference;
switch (getPreferenceKey()) {
case KEY_PRIORITY:
@@ -107,10 +112,15 @@ public class ZenModeAppsPreferenceController extends
};
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(ZenModeBypassingAppsSettings.class.getName())
.setDestination(ZenModeSelectBypassingAppsFragment.class.getName())
.setSourceMetricsCategory(SettingsEnums.SETTINGS_ZEN_NOTIFICATIONS)
.setArguments(bundle)
.launch();
}
}

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

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