diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1913bfbedb4..ea0981b0aaa 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -8447,6 +8447,9 @@
Notification dot on app icon
+
+ Notification bundles
+
Bubbles
diff --git a/res/xml/app_notification_settings.xml b/res/xml/app_notification_settings.xml
index 091de7536da..1eee0cb8a77 100644
--- a/res/xml/app_notification_settings.xml
+++ b/res/xml/app_notification_settings.xml
@@ -50,6 +50,12 @@
settings:controller="com.android.settings.notification.app.BubbleSummaryPreferenceController">
+
+
+
(mControllers);
}
}
diff --git a/src/com/android/settings/notification/app/BundleListPreferenceController.java b/src/com/android/settings/notification/app/BundleListPreferenceController.java
new file mode 100644
index 00000000000..82e910cea8f
--- /dev/null
+++ b/src/com/android/settings/notification/app/BundleListPreferenceController.java
@@ -0,0 +1,171 @@
+/*
+ * 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.app;
+
+import static android.app.NotificationChannel.NEWS_ID;
+import static android.app.NotificationChannel.PROMOTIONS_ID;
+import static android.app.NotificationChannel.RECS_ID;
+import static android.app.NotificationChannel.SOCIAL_MEDIA_ID;
+import static android.app.NotificationManager.IMPORTANCE_LOW;
+import static android.app.NotificationManager.IMPORTANCE_NONE;
+
+import static com.android.server.notification.Flags.notificationHideUnusedChannels;
+
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.service.notification.Flags;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.TwoStatePreference;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settings.applications.AppInfoBase;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.notification.NotificationBackend;
+import com.android.settingslib.PrimarySwitchPreference;
+import com.android.settingslib.RestrictedSwitchPreference;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class BundleListPreferenceController extends NotificationPreferenceController {
+
+ private static final String KEY = "bundles";
+
+ public BundleListPreferenceController(Context context, NotificationBackend backend) {
+ super(context, backend);
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY;
+ }
+
+ @Override
+ public boolean isAvailable() {
+ if (!Flags.notificationClassification()) {
+ return false;
+ }
+ if (mAppRow == null) {
+ return false;
+ }
+ if (mAppRow.banned || mAppRow.lockedImportance || mAppRow.systemApp) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ boolean isIncludedInFilter() {
+ return false;
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ PreferenceCategory category = (PreferenceCategory) preference;
+
+ createOrUpdatePrefForChannel(category,
+ mBackend.getChannel(mAppRow.pkg, mAppRow.uid, PROMOTIONS_ID));
+ createOrUpdatePrefForChannel(category,
+ mBackend.getChannel(mAppRow.pkg, mAppRow.uid, RECS_ID));
+ createOrUpdatePrefForChannel(category,
+ mBackend.getChannel(mAppRow.pkg, mAppRow.uid, SOCIAL_MEDIA_ID));
+ createOrUpdatePrefForChannel(category,
+ mBackend.getChannel(mAppRow.pkg, mAppRow.uid, NEWS_ID));
+ }
+
+ @NonNull
+ private void createOrUpdatePrefForChannel(
+ @NonNull PreferenceGroup groupPrefGroup, NotificationChannel channel) {
+ int preferenceCount = groupPrefGroup.getPreferenceCount();
+ for (int i = 0; i < preferenceCount; i++) {
+ Preference preference = groupPrefGroup.getPreference(i);
+ if (channel.getId().equals(preference.getKey())) {
+ updateSingleChannelPrefs((PrimarySwitchPreference) preference, channel);
+ return;
+ }
+ }
+ PrimarySwitchPreference channelPref = new PrimarySwitchPreference(mContext);
+ channelPref.setKey(channel.getId());
+ updateSingleChannelPrefs(channelPref, channel);
+ groupPrefGroup.addPreference(channelPref);
+ }
+
+ /** Update the properties of the channel preference with the values from the channel object. */
+ private void updateSingleChannelPrefs(@NonNull final PrimarySwitchPreference channelPref,
+ @NonNull final NotificationChannel channel) {
+ channelPref.setSwitchEnabled(mAdmin == null);
+ if (channel.getImportance() > IMPORTANCE_LOW) {
+ channelPref.setIcon(getAlertingIcon());
+ } else {
+ channelPref.setIcon(mContext.getDrawable(R.drawable.empty_icon));
+ }
+ channelPref.setIconSize(PrimarySwitchPreference.ICON_SIZE_SMALL);
+ channelPref.setTitle(channel.getName());
+ channelPref.setSummary(NotificationBackend.getSentSummary(
+ mContext, mAppRow.sentByChannel.get(channel.getId()), false));
+ channelPref.setChecked(channel.getImportance() != IMPORTANCE_NONE);
+ Bundle channelArgs = new Bundle();
+ channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mAppRow.uid);
+ channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mAppRow.pkg);
+ channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId());
+ channelPref.setIntent(new SubSettingLauncher(mContext)
+ .setDestination(ChannelNotificationSettings.class.getName())
+ .setArguments(channelArgs)
+ .setTitleRes(R.string.notification_channel_title)
+ .setSourceMetricsCategory(SettingsEnums.NOTIFICATION_APP_NOTIFICATION)
+ .toIntent());
+
+ channelPref.setOnPreferenceChangeListener(
+ (preference, o) -> {
+ boolean value = (Boolean) o;
+ int importance = value
+ ? Math.max(channel.getOriginalImportance(), IMPORTANCE_LOW)
+ : IMPORTANCE_NONE;
+ channel.setImportance(importance);
+ channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
+ PrimarySwitchPreference channelPref1 = (PrimarySwitchPreference) preference;
+ channelPref1.setIcon(R.drawable.empty_icon);
+ if (channel.getImportance() > IMPORTANCE_LOW) {
+ channelPref1.setIcon(getAlertingIcon());
+ }
+ mBackend.updateChannel(mAppRow.pkg, mAppRow.uid, channel);
+
+ return true;
+ });
+ }
+
+ private Drawable getAlertingIcon() {
+ Drawable icon = mContext.getDrawable(R.drawable.ic_notifications_alert);
+ icon.setTintList(Utils.getColorAccent(mContext));
+ return icon;
+ }
+
+}
diff --git a/tests/robotests/src/com/android/settings/notification/app/BundleListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/app/BundleListPreferenceControllerTest.java
new file mode 100644
index 00000000000..8b8c77e9f30
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/app/BundleListPreferenceControllerTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2023 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.app;
+
+import static android.app.NotificationChannel.NEWS_ID;
+import static android.app.NotificationChannel.PROMOTIONS_ID;
+import static android.app.NotificationChannel.RECS_ID;
+import static android.app.NotificationChannel.SOCIAL_MEDIA_ID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.app.NotificationChannel;
+import android.content.Context;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.service.notification.Flags;
+
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.android.settings.notification.NotificationBackend;
+import com.android.settingslib.PrimarySwitchPreference;
+
+import com.google.common.collect.ImmutableMap;
+
+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;
+
+@RunWith(RobolectricTestRunner.class)
+@SmallTest
+@EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+public class BundleListPreferenceControllerTest {
+ private Context mContext;
+ @Mock
+ private NotificationBackend mBackend;
+ private NotificationBackend.AppRow mAppRow;
+ private BundleListPreferenceController mController;
+ private PreferenceManager mPreferenceManager;
+ private PreferenceScreen mPreferenceScreen;
+ private PreferenceCategory mGroupList;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mContext = ApplicationProvider.getApplicationContext();
+
+ mAppRow = new NotificationBackend.AppRow();
+ mAppRow.pkg = "pkg";
+ mAppRow.uid = 1111111;
+ NotificationBackend.NotificationsSentState
+ sentA = new NotificationBackend.NotificationsSentState();
+ sentA.avgSentDaily = 2;
+ sentA.avgSentWeekly = 10;
+ NotificationBackend.NotificationsSentState
+ sentB = new NotificationBackend.NotificationsSentState();
+ sentB.avgSentDaily = 0;
+ sentB.avgSentWeekly = 2;
+ mAppRow.sentByChannel = ImmutableMap.of(
+ PROMOTIONS_ID, sentA, NEWS_ID, sentA, SOCIAL_MEDIA_ID, sentB, RECS_ID, sentB);
+ mController = new BundleListPreferenceController(mContext, mBackend);
+ mController.onResume(mAppRow, null, null, null, null, null, null);
+ mPreferenceManager = new PreferenceManager(mContext);
+ mPreferenceScreen = mPreferenceManager.createPreferenceScreen(mContext);
+ mGroupList = new PreferenceCategory(mContext);
+ mPreferenceScreen.addPreference(mGroupList);
+
+ when(mBackend.getChannel(mAppRow.pkg, mAppRow.uid, PROMOTIONS_ID)).thenReturn(
+ new NotificationChannel(PROMOTIONS_ID, PROMOTIONS_ID, 2));
+ when(mBackend.getChannel(mAppRow.pkg, mAppRow.uid, NEWS_ID)).thenReturn(
+ new NotificationChannel(NEWS_ID, NEWS_ID, 2));
+ when(mBackend.getChannel(mAppRow.pkg, mAppRow.uid, SOCIAL_MEDIA_ID)).thenReturn(
+ new NotificationChannel(SOCIAL_MEDIA_ID, SOCIAL_MEDIA_ID, 2));
+ when(mBackend.getChannel(mAppRow.pkg, mAppRow.uid, RECS_ID)).thenReturn(
+ new NotificationChannel(RECS_ID, RECS_ID, 2));
+ }
+
+ @Test
+ public void isAvailable_null() {
+ mController.onResume(null, null, null, null, null, null, null);
+ assertThat(mController.isAvailable()).isFalse();
+ mAppRow.banned = true;
+ }
+
+ @Test
+ public void isAvailable_banned() {
+ mAppRow.banned = true;
+ assertThat(mController.isAvailable()).isFalse();
+ }
+
+ @Test
+ public void isAvailable_locked() {
+ mAppRow.lockedImportance = true;
+ assertThat(mController.isAvailable()).isFalse();
+ }
+
+ @Test
+ public void isAvailable_system() {
+ mAppRow.systemApp = true;
+ assertThat(mController.isAvailable()).isFalse();
+ }
+
+ @Test
+ public void isAvailable() {
+ assertThat(mController.isAvailable()).isTrue();
+ }
+
+ @Test
+ public void updateState() {
+ mController.updateState(mGroupList);
+ assertThat(mGroupList.getPreferenceCount()).isEqualTo(4);
+ assertThat(mGroupList.findPreference(PROMOTIONS_ID).getTitle()).isEqualTo(PROMOTIONS_ID);
+ assertThat(mGroupList.findPreference(NEWS_ID).getTitle()).isEqualTo(NEWS_ID);
+ assertThat(mGroupList.findPreference(SOCIAL_MEDIA_ID).getTitle())
+ .isEqualTo(SOCIAL_MEDIA_ID);
+ assertThat(mGroupList.findPreference(RECS_ID).getTitle()).isEqualTo(RECS_ID);
+ }
+
+ @Test
+ public void updateState_updateChildren() {
+ mController.updateState(mGroupList);
+ assertThat(mGroupList.getPreferenceCount()).isEqualTo(4);
+
+ when(mBackend.getChannel(mAppRow.pkg, mAppRow.uid, PROMOTIONS_ID)).thenReturn(
+ new NotificationChannel(PROMOTIONS_ID, PROMOTIONS_ID, 2));
+
+ mController.updateState(mGroupList);
+ assertThat(mGroupList.getPreferenceCount()).isEqualTo(4);
+
+ assertThat(((PrimarySwitchPreference) mGroupList.findPreference(NEWS_ID)).isChecked())
+ .isEqualTo(false);
+ assertThat(((PrimarySwitchPreference) mGroupList.findPreference(NEWS_ID)).isChecked())
+ .isEqualTo(false);
+ }
+}