diff --git a/res/xml/bundle_notifications_settings.xml b/res/xml/bundle_notifications_settings.xml
index 34efd7dd49b..3ff8c053273 100644
--- a/res/xml/bundle_notifications_settings.xml
+++ b/res/xml/bundle_notifications_settings.xml
@@ -56,4 +56,27 @@
android:key="recs"
android:title="@*android:string/recs_notification_channel_label"
settings:controller="com.android.settings.notification.BundleTypePreferenceController"/>
+
+
+
+
+
+
+
+
+
+
diff --git a/res/xml/summarization_notifications_settings.xml b/res/xml/summarization_notifications_settings.xml
index c6de6265709..af9598042ac 100644
--- a/res/xml/summarization_notifications_settings.xml
+++ b/res/xml/summarization_notifications_settings.xml
@@ -36,4 +36,29 @@
android:key="global_pref"
android:title="@string/notification_summarization_main_control_title"
settings:controller="com.android.settings.notification.SummarizationGlobalPreferenceController" />
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index ee866babffc..c8b0783ece5 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -358,6 +358,8 @@ public class Settings extends SettingsActivity {
public static class AppBubbleNotificationSettingsActivity extends SettingsActivity { /* empty */ }
public static class NotificationAssistantSettingsActivity extends SettingsActivity{ /* empty */ }
public static class NotificationAppListActivity extends SettingsActivity { /* empty */ }
+ public static class NotificationExcludeSummarizationActivity extends SettingsActivity { /* empty */ }
+ public static class NotificationExcludeClassificationActivity extends SettingsActivity { /* empty */ }
/** Activity to manage Cloned Apps page */
public static class ClonedAppsListActivity extends SettingsActivity { /* empty */ }
/** Activity to manage Aspect Ratio app list page */
diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java
index b837e1e9c5d..d3d532f2dd4 100644
--- a/src/com/android/settings/applications/manageapplications/ManageApplications.java
+++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java
@@ -270,6 +270,8 @@ public class ManageApplications extends InstrumentedFragment
public static final int LIST_TYPE_NFC_TAG_APPS = 18;
public static final int LIST_TYPE_TURN_SCREEN_ON = 19;
public static final int LIST_TYPE_USER_ASPECT_RATIO_APPS = 20;
+ public static final int LIST_TYPE_NOTIFICATION_EXCLUDE_SUMMARIZATION = 21;
+ public static final int LIST_TYPE_NOTIFICATION_EXCLUDE_CLASSIFICATION = 22;
// List types that should show instant apps.
public static final Set LIST_TYPES_WITH_INSTANT = new ArraySet<>(Arrays.asList(
diff --git a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
index dca115b97c0..41b92dde868 100644
--- a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
+++ b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
@@ -31,6 +31,8 @@ import com.android.settings.Settings.ManageExternalSourcesActivity
import com.android.settings.Settings.ManageExternalStorageActivity
import com.android.settings.Settings.MediaManagementAppsActivity
import com.android.settings.Settings.NotificationAppListActivity
+import com.android.settings.Settings.NotificationExcludeClassificationActivity
+import com.android.settings.Settings.NotificationExcludeSummarizationActivity
import com.android.settings.Settings.NotificationReviewPermissionsActivity
import com.android.settings.Settings.OverlaySettingsActivity
import com.android.settings.Settings.StorageUseActivity
@@ -44,6 +46,8 @@ import com.android.settings.applications.manageapplications.ManageApplications.L
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_USER_ASPECT_RATIO_APPS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_BATTERY_OPTIMIZATION
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_CLONED_APPS
+import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_NOTIFICATION_EXCLUDE_CLASSIFICATION
+import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_NOTIFICATION_EXCLUDE_SUMMARIZATION
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_GAMES
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_HIGH_POWER
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_LONG_BACKGROUND_TASKS
@@ -99,6 +103,9 @@ object ManageApplicationsUtil {
ChangeNfcTagAppsActivity::class to LIST_TYPE_NFC_TAG_APPS,
TurnScreenOnSettingsActivity::class to LIST_TYPE_TURN_SCREEN_ON,
UserAspectRatioAppListActivity::class to LIST_TYPE_USER_ASPECT_RATIO_APPS,
+ NotificationExcludeSummarizationActivity::class to LIST_TYPE_NOTIFICATION_EXCLUDE_SUMMARIZATION,
+ NotificationExcludeClassificationActivity::class to LIST_TYPE_NOTIFICATION_EXCLUDE_CLASSIFICATION,
+
)
@JvmField
@@ -117,7 +124,7 @@ object ManageApplicationsUtil {
LIST_TYPE_MEDIA_MANAGEMENT_APPS -> MediaManagementAppsAppListProvider.getAppListRoute()
LIST_TYPE_ALARMS_AND_REMINDERS -> AlarmsAndRemindersAppListProvider.getAppListRoute()
LIST_TYPE_WIFI_ACCESS -> WifiControlAppListProvider.getAppListRoute()
- LIST_TYPE_NOTIFICATION -> AppListNotificationsPageProvider.name
+ LIST_TYPE_NOTIFICATION -> AppListNotificationsPageProvider.AllApps.name
LIST_TYPE_APPS_LOCALE -> AppLanguagesPageProvider.name
LIST_TYPE_MAIN -> AllAppListPageProvider.name
LIST_TYPE_NFC_TAG_APPS -> NfcTagAppsSettingsProvider.getAppListRoute()
@@ -128,6 +135,8 @@ object ManageApplicationsUtil {
//LIST_TYPE_STORAGE -> StorageAppListPageProvider.Apps.name
//LIST_TYPE_GAMES -> StorageAppListPageProvider.Games.name
LIST_TYPE_BATTERY_OPTIMIZATION -> BatteryOptimizationModeAppListPageProvider.name
+ LIST_TYPE_NOTIFICATION_EXCLUDE_SUMMARIZATION -> AppListNotificationsPageProvider.ExcludeSummarization.name
+ LIST_TYPE_NOTIFICATION_EXCLUDE_CLASSIFICATION -> AppListNotificationsPageProvider.ExcludeClassification.name
else -> null
}
}
diff --git a/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceController.java b/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceController.java
new file mode 100644
index 00000000000..b9cf9737be6
--- /dev/null
+++ b/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceController.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2025 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;
+
+import static android.service.notification.Adjustment.KEY_SUMMARIZATION;
+import static android.service.notification.Adjustment.KEY_TYPE;
+
+import android.app.Flags;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.service.notification.Adjustment;
+
+import androidx.annotation.NonNull;
+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.core.BasePreferenceController;
+import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settingslib.applications.AppUtils;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.utils.ThreadUtils;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Adds a preference to the PreferenceCategory for every app excluded from an adjustment key
+ */
+public class AdjustmentExcludedAppsPreferenceController extends BasePreferenceController
+ implements PreferenceControllerMixin {
+
+ @NonNull private NotificationBackend mBackend;
+
+ @Nullable String mAdjustmentKey;
+ @Nullable @VisibleForTesting ApplicationsState mApplicationsState;
+ @VisibleForTesting PreferenceCategory mPreferenceCategory;
+ @VisibleForTesting Context mPrefContext;
+
+ private ApplicationsState.Session mAppSession;
+
+ public AdjustmentExcludedAppsPreferenceController(@NonNull Context context,
+ @NonNull String preferenceKey) {
+ super(context, preferenceKey);
+ mBackend = new NotificationBackend();
+ }
+
+ protected void onAttach(@Nullable ApplicationsState appState, @Nullable Fragment host,
+ @NonNull NotificationBackend helperBackend, @Adjustment.Keys String adjustment) {
+ mApplicationsState = appState;
+ mBackend = helperBackend;
+ mAdjustmentKey = adjustment;
+
+ if (mApplicationsState != null && host != null) {
+ mAppSession = mApplicationsState.newSession(mAppSessionCallbacks, host.getLifecycle());
+ }
+ }
+
+ @Override
+ public void displayPreference(@NonNull PreferenceScreen screen) {
+ mPreferenceCategory = screen.findPreference(getPreferenceKey());
+ mPrefContext = screen.getContext();
+ updateAppList();
+ super.displayPreference(screen);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ if (!(Flags.nmSummarization() || Flags.nmSummarizationUi()
+ || Flags.notificationClassificationUi())) {
+ return CONDITIONALLY_UNAVAILABLE;
+ }
+ if (KEY_SUMMARIZATION.equals(mAdjustmentKey)
+ && mBackend.isNotificationSummarizationSupported()) {
+ return AVAILABLE;
+ }
+ if (KEY_TYPE.equals(mAdjustmentKey) && mBackend.isNotificationBundlingSupported()) {
+ return AVAILABLE;
+ }
+ return CONDITIONALLY_UNAVAILABLE;
+ }
+
+ /**
+ * 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 {
+ ListenableFuture unused = ThreadUtils.postOnBackgroundThread(() -> {
+ final Drawable icon = AppUtils.getIcon(mPrefContext, entry);
+ if (icon != null) {
+ ThreadUtils.postOnMainThread(() -> pref.setIcon(icon));
+ }
+ });
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void updateAppList(List apps) {
+ if (mPreferenceCategory == null || apps == null) {
+ return;
+ }
+
+ List excludedApps = List.of(mBackend.getAdjustmentDeniedPackages(mAdjustmentKey));
+
+ for (ApplicationsState.AppEntry app : apps) {
+ String pkg = app.info.packageName;
+ final String key = getKey(pkg, app.info.uid);
+ boolean doesAppPassCriteria = false;
+
+ if (excludedApps.contains(pkg)) {
+ doesAppPassCriteria = true;
+ }
+ Preference pref = mPreferenceCategory.findPreference(key);
+ if (pref == null) {
+ if (doesAppPassCriteria) {
+ // does not exist but should
+ pref = new Preference(mPrefContext);
+ pref.setKey(key);
+ pref.setTitle(BidiFormatter.getInstance().unicodeWrap(app.label));
+ updateIcon(pref, app);
+ mPreferenceCategory.addPreference(pref);
+ }
+ } else if (!doesAppPassCriteria) {
+ // exists but shouldn't anymore
+ mPreferenceCategory.removePreference(pref);
+ }
+ }
+ }
+
+ /**
+ * Create a unique key to identify 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(@NonNull ArrayList apps) {
+ updateAppList(apps);
+ }
+
+ @Override
+ public void onPackageIconChanged() {
+ }
+
+ @Override
+ public void onPackageSizeChanged(@NonNull String packageName) {
+ }
+
+ @Override
+ public void onAllSizesComputed() { }
+
+ @Override
+ public void onLauncherInfoChanged() {
+ }
+
+ @Override
+ public void onLoadEntriesCompleted() {
+ updateAppList();
+ }
+ };
+}
diff --git a/src/com/android/settings/notification/BundleManageAppsPreferenceController.java b/src/com/android/settings/notification/BundleManageAppsPreferenceController.java
new file mode 100644
index 00000000000..6c096598af9
--- /dev/null
+++ b/src/com/android/settings/notification/BundleManageAppsPreferenceController.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2025 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;
+
+import android.app.Flags;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.core.BasePreferenceController;
+
+public class BundleManageAppsPreferenceController extends
+ BasePreferenceController {
+
+ NotificationBackend mBackend;
+
+ public BundleManageAppsPreferenceController(@NonNull Context context,
+ @NonNull String preferenceKey) {
+ super(context, preferenceKey);
+ mBackend = new NotificationBackend();
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ if (Flags.notificationClassificationUi() && mBackend.isNotificationBundlingSupported()) {
+ return AVAILABLE;
+ }
+ return CONDITIONALLY_UNAVAILABLE;
+ }
+}
diff --git a/src/com/android/settings/notification/BundlePreferenceFragment.java b/src/com/android/settings/notification/BundlePreferenceFragment.java
index 14de2c26d1c..4a61f9e54af 100644
--- a/src/com/android/settings/notification/BundlePreferenceFragment.java
+++ b/src/com/android/settings/notification/BundlePreferenceFragment.java
@@ -16,6 +16,11 @@
package com.android.settings.notification;
+import static android.service.notification.Adjustment.KEY_SUMMARIZATION;
+import static android.service.notification.Adjustment.KEY_TYPE;
+
+import android.app.Activity;
+import android.app.Application;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.app.Flags;
@@ -25,6 +30,7 @@ import androidx.lifecycle.Lifecycle;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.search.BaseSearchIndexProvider;
+import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.search.SearchIndexable;
import org.jetbrains.annotations.NotNull;
@@ -49,6 +55,26 @@ public class BundlePreferenceFragment extends DashboardFragment {
return "BundlePreferenceFragment";
}
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (use(AdjustmentExcludedAppsPreferenceController.class) != null) {
+ final Activity activity = getActivity();
+ Application app = null;
+ ApplicationsState appState = null;
+ if (activity != null) {
+ app = activity.getApplication();
+ } else {
+ app = null;
+ }
+ if (app != null) {
+ appState = ApplicationsState.getInstance(app);
+ }
+ use(AdjustmentExcludedAppsPreferenceController.class).onAttach(
+ appState, this, new NotificationBackend(), KEY_TYPE);
+ }
+ }
+
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(R.xml.bundle_notifications_settings) {
diff --git a/src/com/android/settings/notification/SummarizationManageAppsPreferenceController.java b/src/com/android/settings/notification/SummarizationManageAppsPreferenceController.java
new file mode 100644
index 00000000000..0a19c020fb5
--- /dev/null
+++ b/src/com/android/settings/notification/SummarizationManageAppsPreferenceController.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2025 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;
+
+import android.app.Flags;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.core.BasePreferenceController;
+
+public class SummarizationManageAppsPreferenceController extends
+ BasePreferenceController {
+
+ NotificationBackend mBackend;
+
+ public SummarizationManageAppsPreferenceController(@NonNull Context context,
+ @NonNull String preferenceKey) {
+ super(context, preferenceKey);
+ mBackend = new NotificationBackend();
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ if ((Flags.nmSummarization() || Flags.nmSummarizationUi())
+ && mBackend.isNotificationSummarizationSupported()) {
+ return AVAILABLE;
+ }
+ return CONDITIONALLY_UNAVAILABLE;
+ }
+}
diff --git a/src/com/android/settings/notification/SummarizationPreferenceFragment.java b/src/com/android/settings/notification/SummarizationPreferenceFragment.java
index 1c8f5e3059d..893383a3b8c 100644
--- a/src/com/android/settings/notification/SummarizationPreferenceFragment.java
+++ b/src/com/android/settings/notification/SummarizationPreferenceFragment.java
@@ -16,13 +16,19 @@
package com.android.settings.notification;
+import static android.service.notification.Adjustment.KEY_SUMMARIZATION;
+
+import android.app.Activity;
+import android.app.Application;
import android.app.Flags;
import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.notification.app.HeaderPreferenceController;
import com.android.settings.search.BaseSearchIndexProvider;
+import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.search.SearchIndexable;
/**
@@ -45,6 +51,26 @@ public class SummarizationPreferenceFragment extends DashboardFragment {
return "SummarizationPreferenceFragment";
}
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (use(AdjustmentExcludedAppsPreferenceController.class) != null) {
+ final Activity activity = getActivity();
+ Application app = null;
+ ApplicationsState appState = null;
+ if (activity != null) {
+ app = activity.getApplication();
+ } else {
+ app = null;
+ }
+ if (app != null) {
+ appState = ApplicationsState.getInstance(app);
+ }
+ use(AdjustmentExcludedAppsPreferenceController.class).onAttach(
+ appState, this, new NotificationBackend(), KEY_SUMMARIZATION);
+ }
+ }
+
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(R.xml.summarization_notifications_settings) {
diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
index 754b30b5ab7..0419d3b7017 100644
--- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt
+++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
@@ -45,7 +45,6 @@ import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
import com.android.settings.spa.app.specialaccess.WriteSystemPreferencesAppListProvider
import com.android.settings.spa.app.storage.StorageAppListPageProvider
import com.android.settings.spa.core.instrumentation.SpaLogMetricsProvider
-import com.android.settings.spa.core.instrumentation.SpaLogProvider
import com.android.settings.spa.development.UsageStatsPageProvider
import com.android.settings.spa.development.compat.PlatformCompatAppListPageProvider
import com.android.settings.spa.home.HomePageProvider
@@ -107,7 +106,9 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) {
AppInfoSettingsProvider,
SpecialAppAccessPageProvider,
NotificationMainPageProvider,
- AppListNotificationsPageProvider,
+ AppListNotificationsPageProvider.AllApps,
+ AppListNotificationsPageProvider.ExcludeClassification,
+ AppListNotificationsPageProvider.ExcludeSummarization,
SystemMainPageProvider,
LanguageAndInputPageProvider,
AppLanguagesPageProvider,
diff --git a/src/com/android/settings/spa/notification/AppListNotifications.kt b/src/com/android/settings/spa/notification/AppListNotifications.kt
index 00e439495a4..7aa46206f4d 100644
--- a/src/com/android/settings/spa/notification/AppListNotifications.kt
+++ b/src/com/android/settings/spa/notification/AppListNotifications.kt
@@ -17,34 +17,72 @@
package com.android.settings.spa.notification
import android.os.Bundle
+import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.android.settings.R
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.compose.navigator
-import com.android.settingslib.spa.framework.compose.rememberContext
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spaprivileged.template.app.AppList
+import com.android.settingslib.spaprivileged.template.app.AppListInput
import com.android.settingslib.spaprivileged.template.app.AppListPage
-object AppListNotificationsPageProvider : SettingsPageProvider {
- override val name = "AppListNotifications"
-
+sealed class AppListNotificationsPageProvider(private val type: ListType) : SettingsPageProvider {
@Composable
override fun Page(arguments: Bundle?) {
- AppListPage(
- title = stringResource(R.string.app_notifications_title),
- listModel = rememberContext(::AppNotificationsListModel),
- )
+ NotificationsAppListPage(type)
}
- @Composable
- fun EntryItem() {
- val summary = stringResource(R.string.app_notification_field_summary)
- Preference(object : PreferenceModel {
- override val title = stringResource(R.string.app_notifications_title)
- override val summary = { summary }
- override val onClick = navigator(name)
- })
+ object AllApps : AppListNotificationsPageProvider(ListType.Apps) {
+ override val name = "AppListNotifications"
+
+ @Composable
+ fun EntryItem() {
+ val summary = stringResource(R.string.app_notification_field_summary)
+ Preference(object : PreferenceModel {
+ override val title = stringResource(ListType.Apps.titleResource)
+ override val summary = { summary }
+ override val onClick = navigator(name)
+ })
+ }
+ }
+
+ object ExcludeSummarization : AppListNotificationsPageProvider(ListType.ExcludeSummarization) {
+ override val name = "NotificationsExcludeSummarization"
+ }
+
+ object ExcludeClassification : AppListNotificationsPageProvider(ListType.ExcludeClassification) {
+ override val name = "NotificationsExcludeClassification"
}
}
+
+@Composable
+fun NotificationsAppListPage(
+ type: ListType,
+ appList: @Composable AppListInput.() -> Unit = { AppList() }
+) {
+ val context = LocalContext.current
+ AppListPage(
+ title = stringResource(type.titleResource),
+ listModel = remember(context) { AppNotificationsListModel(context, type) },
+ appList = appList,
+ )
+}
+
+sealed class ListType(
+ @StringRes val titleResource: Int
+) {
+ object Apps : ListType(
+ titleResource = R.string.app_notifications_title,
+ )
+ object ExcludeSummarization : ListType(
+ titleResource = R.string.notification_summarization_manage_excluded_apps_title,
+ )
+ object ExcludeClassification : ListType(
+ titleResource = R.string.notification_bundle_manage_excluded_apps_title,
+ )
+}
\ No newline at end of file
diff --git a/src/com/android/settings/spa/notification/AppNotificationController.kt b/src/com/android/settings/spa/notification/AppNotificationController.kt
index 1ce72e01f72..fd35427d5c2 100644
--- a/src/com/android/settings/spa/notification/AppNotificationController.kt
+++ b/src/com/android/settings/spa/notification/AppNotificationController.kt
@@ -17,12 +17,16 @@
package com.android.settings.spa.notification
import android.content.pm.ApplicationInfo
+import android.service.notification.Adjustment
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import com.android.settings.spa.app.storage.StorageAppListModel
+import com.android.settings.spa.app.storage.StorageType
class AppNotificationController(
private val repository: AppNotificationRepository,
private val app: ApplicationInfo,
+ private val listType: ListType,
) {
val isEnabled: LiveData
get() = _isEnabled
@@ -47,4 +51,62 @@ class AppNotificationController(
postValue(it)
}
}
+
+ val isAllowed: LiveData
+ get() = _isAllowed
+
+ fun getAllowed() = _isAllowed.get()
+
+ fun setAllowed(enabled: Boolean) {
+ when (listType) {
+ ListType.ExcludeSummarization -> {
+ if (repository.setAdjustmentSupportedForPackage(
+ app, Adjustment.KEY_SUMMARIZATION, enabled)) {
+ _isAllowed.postValue(enabled)
+ }
+ }
+ ListType.ExcludeClassification -> {
+ if (repository.setAdjustmentSupportedForPackage(
+ app, Adjustment.KEY_TYPE, enabled)) {
+ _isAllowed.postValue(enabled)
+ }
+ }
+ else -> {}
+ }
+ }
+
+ private val _isAllowed = object : MutableLiveData() {
+ override fun onActive() {
+ when (listType) {
+ ListType.ExcludeSummarization -> {
+ postValue(repository.isAdjustmentSupportedForPackage(
+ app, Adjustment.KEY_SUMMARIZATION))
+ }
+ ListType.ExcludeClassification -> {
+ postValue(repository.isAdjustmentSupportedForPackage(
+ app, Adjustment.KEY_TYPE))
+ }
+ else -> {}
+ }
+ }
+
+ override fun onInactive() {
+ }
+
+ fun get(): Boolean = when (listType) {
+ ListType.ExcludeSummarization -> {
+ value ?: repository.isAdjustmentSupportedForPackage(
+ app, Adjustment.KEY_SUMMARIZATION).also {
+ postValue(it)
+ }
+ }
+ ListType.ExcludeClassification -> {
+ value ?: repository.isAdjustmentSupportedForPackage(
+ app, Adjustment.KEY_TYPE).also {
+ postValue(it)
+ }
+ }
+ else -> false
+ }
+ }
}
diff --git a/src/com/android/settings/spa/notification/AppNotificationRepository.kt b/src/com/android/settings/spa/notification/AppNotificationRepository.kt
index d0e700a9e34..fddbf1aae04 100644
--- a/src/com/android/settings/spa/notification/AppNotificationRepository.kt
+++ b/src/com/android/settings/spa/notification/AppNotificationRepository.kt
@@ -126,6 +126,20 @@ class AppNotificationRepository(
}
}
+ fun isAdjustmentSupportedForPackage(app: ApplicationInfo, key: String): Boolean =
+ notificationManager.isAdjustmentSupportedForPackage(key, app.packageName)
+
+ fun setAdjustmentSupportedForPackage(app: ApplicationInfo, key: String, enabled: Boolean):
+ Boolean {
+ return try {
+ notificationManager.setAdjustmentSupportedForPackage(key, app.packageName, enabled)
+ true
+ } catch (e: Exception) {
+ Log.w(TAG, "Error calling INotificationManager", e)
+ false
+ }
+ }
+
fun isUserUnlocked(user: Int): Boolean {
return try {
userManager.isUserUnlocked(user)
diff --git a/src/com/android/settings/spa/notification/AppNotificationsListModel.kt b/src/com/android/settings/spa/notification/AppNotificationsListModel.kt
index 8a534c8b14d..d979918dac4 100644
--- a/src/com/android/settings/spa/notification/AppNotificationsListModel.kt
+++ b/src/com/android/settings/spa/notification/AppNotificationsListModel.kt
@@ -36,6 +36,7 @@ import com.android.settingslib.spaprivileged.model.app.AppListModel
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.userId
import com.android.settingslib.spaprivileged.template.app.AppListItemModel
+import com.android.settingslib.spaprivileged.template.app.AppListSwitchItem
import com.android.settingslib.spaprivileged.template.app.AppListTwoTargetSwitchItem
import com.android.settingslib.utils.StringUtil
import kotlinx.coroutines.Dispatchers
@@ -52,6 +53,7 @@ data class AppNotificationsRecord(
class AppNotificationsListModel(
private val context: Context,
+ private val listType: ListType
) : AppListModel {
private val repository = AppNotificationRepository(context)
private val now = System.currentTimeMillis()
@@ -64,7 +66,7 @@ class AppNotificationsListModel(
AppNotificationsRecord(
app = app,
sentState = usageEvents[app.packageName],
- controller = AppNotificationController(repository, app),
+ controller = AppNotificationController(repository, app, listType),
)
}
}
@@ -129,17 +131,35 @@ class AppNotificationsListModel(
@Composable
override fun AppListItemModel.AppItem() {
- val changeable by produceState(initialValue = false) {
- withContext(Dispatchers.Default) {
- value = repository.isChangeable(record.app)
+ when (listType) {
+ ListType.ExcludeSummarization -> {
+ AppListSwitchItem(
+ checked = record.controller.isAllowed.observeAsCallback(),
+ changeable = { true },
+ onCheckedChange = record.controller::setAllowed,
+ )
+ }
+ ListType.ExcludeClassification -> {
+ AppListSwitchItem(
+ checked = record.controller.isAllowed.observeAsCallback(),
+ changeable = { true },
+ onCheckedChange = record.controller::setAllowed,
+ )
+ }
+ else -> {
+ val changeable by produceState(initialValue = false) {
+ withContext(Dispatchers.Default) {
+ value = repository.isChangeable(record.app)
+ }
+ }
+ AppListTwoTargetSwitchItem(
+ onClick = { navigateToAppNotificationSettings(app = record.app) },
+ checked = record.controller.isEnabled.observeAsCallback(),
+ changeable = { changeable },
+ onCheckedChange = record.controller::setEnabled,
+ )
}
}
- AppListTwoTargetSwitchItem(
- onClick = { navigateToAppNotificationSettings(app = record.app) },
- checked = record.controller.isEnabled.observeAsCallback(),
- changeable = { changeable },
- onCheckedChange = record.controller::setEnabled,
- )
}
private fun navigateToAppNotificationSettings(app: ApplicationInfo) {
diff --git a/src/com/android/settings/spa/notification/NotificationMain.kt b/src/com/android/settings/spa/notification/NotificationMain.kt
index b3c7a55465c..6859aa916f6 100644
--- a/src/com/android/settings/spa/notification/NotificationMain.kt
+++ b/src/com/android/settings/spa/notification/NotificationMain.kt
@@ -41,7 +41,7 @@ object NotificationMainPageProvider : SettingsPageProvider {
@Composable
override fun Page(arguments: Bundle?) {
RegularScaffold(title = getTitle(arguments)) {
- AppListNotificationsPageProvider.EntryItem()
+ AppListNotificationsPageProvider.AllApps.EntryItem()
}
}
diff --git a/tests/robotests/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceControllerTest.java
new file mode 100644
index 00000000000..ec68a7a14d4
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/AdjustmentExcludedAppsPreferenceControllerTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2025 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;
+
+import static android.service.notification.Adjustment.KEY_SUMMARIZATION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.Flags;
+import android.app.INotificationManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.fragment.app.Fragment;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settingslib.applications.ApplicationsState;
+
+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;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@EnableFlags({Flags.FLAG_NM_SUMMARIZATION_UI, Flags.FLAG_NM_SUMMARIZATION,
+ Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI})
+public class AdjustmentExcludedAppsPreferenceControllerTest {
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ @Mock
+ private NotificationBackend mBackend;
+ @Mock
+ private ApplicationsState mApplicationState;
+ private AdjustmentExcludedAppsPreferenceController mController;
+ private Context mContext;
+ @Mock
+ INotificationManager mInm;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+
+ mController = new AdjustmentExcludedAppsPreferenceController(mContext, "key");
+ mController.onAttach(null, mock(Fragment.class), mBackend, KEY_SUMMARIZATION);
+ PreferenceScreen screen = new PreferenceManager(mContext).createPreferenceScreen(mContext);
+ mController.mPreferenceCategory = new PreferenceCategory(mContext);
+ screen.addPreference(mController.mPreferenceCategory);
+
+ mController.mApplicationsState = mApplicationState;
+ mController.mPrefContext = mContext;
+ }
+
+ @Test
+ public void testIsAvailable() {
+ when(mBackend.isNotificationBundlingSupported()).thenReturn(true);
+ when(mBackend.isNotificationSummarizationSupported()).thenReturn(true);
+ assertThat(mController.isAvailable()).isTrue();
+ }
+
+ @Test
+ public void isAvailable_flagEnabledNasDoesNotSupport_shouldReturnFalse() throws Exception {
+ when(mInm.getUnsupportedAdjustmentTypes()).thenReturn(List.of(KEY_SUMMARIZATION));
+ assertThat(mController.isAvailable()).isFalse();
+ }
+
+ @Test
+ public void testUpdateAppList() throws Exception {
+ when(mBackend.getAdjustmentDeniedPackages(KEY_SUMMARIZATION)).thenReturn(
+ new String[] {"cannot", "cannot2"});
+
+ // GIVEN there are four apps, and two have KEY_SUMMARIZATION off
+ ApplicationsState.AppEntry canSummarize =
+ mock(ApplicationsState.AppEntry.class);
+ canSummarize.info = new ApplicationInfo();
+ canSummarize.info.packageName = "canSummarize";
+ canSummarize.info.uid = 0;
+
+ ApplicationsState.AppEntry canSummarize2 = mock(ApplicationsState.AppEntry.class);
+ canSummarize2.info = new ApplicationInfo();
+ canSummarize2.info.packageName = "canSummarizeTwo";
+ canSummarize2.info.uid = 0;
+
+ ApplicationsState.AppEntry cannot =
+ mock(ApplicationsState.AppEntry.class);
+ cannot.info = new ApplicationInfo();
+ cannot.info.packageName = "cannot";
+ cannot.info.uid = 0;
+
+ ApplicationsState.AppEntry cannot2 =
+ mock(ApplicationsState.AppEntry.class);
+ cannot2.info = new ApplicationInfo();
+ cannot2.info.packageName = "cannot2";
+ cannot2.info.uid = 0;
+
+ List appEntries = new ArrayList<>();
+ appEntries.add(canSummarize);
+ appEntries.add(canSummarize2);
+ appEntries.add(cannot);
+ appEntries.add(cannot2);
+
+ // WHEN the controller updates the app list with the app entries
+ mController.updateAppList(appEntries);
+
+ // THEN only the 'cannot' entries make it to the app list
+ assertThat(mController.mPreferenceCategory.getPreferenceCount()).isEqualTo(2);
+ assertThat((Preference) mController.mPreferenceCategory.findPreference(
+ AdjustmentExcludedAppsPreferenceController.getKey(
+ cannot.info.packageName,cannot.info.uid))).isNotNull();
+ assertThat((Preference) mController.mPreferenceCategory.findPreference(
+ AdjustmentExcludedAppsPreferenceController.getKey(
+ cannot2.info.packageName,cannot2.info.uid))).isNotNull();
+ }
+
+ @Test
+ public void testUpdateAppList_nullApps() {
+ mController.updateAppList(null);
+ assertThat(mController.mPreferenceCategory.getPreferenceCount()).isEqualTo(0);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/BundleManageAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/BundleManageAppsPreferenceControllerTest.java
new file mode 100644
index 00000000000..7f595b5c7e0
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/BundleManageAppsPreferenceControllerTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 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;
+
+import static android.service.notification.Adjustment.KEY_SUMMARIZATION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.app.Flags;
+import android.app.INotificationManager;
+import android.content.Context;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.service.notification.Adjustment;
+
+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;
+
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public class BundleManageAppsPreferenceControllerTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private static final String PREFERENCE_KEY = "preference_key";
+
+ private Context mContext;
+ BundleManageAppsPreferenceController mController;
+ @Mock
+ INotificationManager mInm;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI);
+ mController = new BundleManageAppsPreferenceController(mContext, PREFERENCE_KEY);
+ mController.mBackend.setNm(mInm);
+ }
+
+ @Test
+ public void isAvailable_flagEnabledNasSupports_shouldReturnTrue() {
+ assertThat(mController.isAvailable()).isTrue();
+ }
+
+ @Test
+ public void isAvailable_flagEnabledNasDoesNotSupport_shouldReturnFalse() throws Exception {
+ when(mInm.getUnsupportedAdjustmentTypes()).thenReturn(List.of(Adjustment.KEY_TYPE));
+ assertThat(mController.isAvailable()).isFalse();
+ }
+
+ @Test
+ public void isAvailable_flagDisabledNasSupports_shouldReturnFalse() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI);
+ assertThat(mController.isAvailable()).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/SummarizationManageAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/SummarizationManageAppsPreferenceControllerTest.java
new file mode 100644
index 00000000000..b6cedb86585
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/SummarizationManageAppsPreferenceControllerTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2025 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;
+
+import static android.service.notification.Adjustment.KEY_IMPORTANCE;
+import static android.service.notification.Adjustment.KEY_SUMMARIZATION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.app.Flags;
+import android.app.INotificationManager;
+import android.content.Context;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+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;
+
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public class SummarizationManageAppsPreferenceControllerTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private static final String PREFERENCE_KEY = "preference_key";
+
+ private Context mContext;
+ SummarizationManageAppsPreferenceController mController;
+ @Mock
+ INotificationManager mInm;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = RuntimeEnvironment.application;
+ mSetFlagsRule.enableFlags(Flags.FLAG_NM_SUMMARIZATION, Flags.FLAG_NM_SUMMARIZATION_UI);
+ mController = new SummarizationManageAppsPreferenceController(mContext, PREFERENCE_KEY);
+ mController.mBackend.setNm(mInm);
+ }
+
+ @Test
+ public void isAvailable_flagEnabledNasSupports_shouldReturnTrue() {
+ assertThat(mController.isAvailable()).isTrue();
+ }
+
+ @Test
+ public void isAvailable_flagEnabledNasDoesNotSupport_shouldReturnFalse() throws Exception {
+ when(mInm.getUnsupportedAdjustmentTypes()).thenReturn(List.of(KEY_SUMMARIZATION));
+ assertThat(mController.isAvailable()).isFalse();
+ }
+
+ @Test
+ public void isAvailable_flagDisabledNasSupports_shouldReturnFalse() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_NM_SUMMARIZATION);
+ mSetFlagsRule.disableFlags(Flags.FLAG_NM_SUMMARIZATION_UI);
+ assertThat(mController.isAvailable()).isFalse();
+ }
+}