From 55441003d3c6d172f4712c71f41965097857c683 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Mon, 3 Feb 2025 10:52:53 -0500 Subject: [PATCH] Add app level settings for classification and summarization Test: AdjustmentKeyPreferenceControllerTest Flag: android.app.nm_summarization Flag: android.app.notification_classification_ui Flag: android.app.nm_summarization_ui Bug: 377697346 Bug: 390412878 Change-Id: I85b67b5c0376ee4cd962e26bf178aae6fa712212 --- res/values/strings.xml | 13 +- res/xml/app_notification_settings.xml | 10 ++ .../notification/NotificationBackend.java | 20 ++- .../AdjustmentKeyPreferenceController.java | 98 ++++++++++++ .../app/AppNotificationSettings.java | 5 + ...AdjustmentKeyPreferenceControllerTest.java | 149 ++++++++++++++++++ 6 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 src/com/android/settings/notification/app/AdjustmentKeyPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/notification/app/AdjustmentKeyPreferenceControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 80b0e8aad8e..25ec499400f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8858,7 +8858,13 @@ On Off Use notification summaries - Automatically summarize conversation notifications from any app + Automatically summarize conversation notifications from apps + Conversation notifications from these apps will not be summarized + Manage apps + Allow notification summaries for apps + App exceptions + Summarize notifications + Summarize conversation notifications from this app Live notifications @@ -9282,6 +9288,11 @@ Use notification bundling Notifications with similar themes will be silenced and grouped together for a quieter experience. Bundling will override an app\'s own notification settings. + Bundle notifications + Notifications with similar themes will be silenced and grouped together for a quieter experience + Notifications from these apps will not be bundled + Allow notification bundling for apps + VR helper services diff --git a/res/xml/app_notification_settings.xml b/res/xml/app_notification_settings.xml index b91aea9a618..901a0c900df 100644 --- a/res/xml/app_notification_settings.xml +++ b/res/xml/app_notification_settings.xml @@ -89,6 +89,16 @@ android:title="@string/live_notifications_switch" android:summary="@string/live_notifications_desc" /> + + + + diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java index d7a747ce004..3ce377ecb19 100644 --- a/src/com/android/settings/notification/NotificationBackend.java +++ b/src/com/android/settings/notification/NotificationBackend.java @@ -54,6 +54,7 @@ import android.text.format.DateUtils; import android.util.IconDrawableFactory; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.util.CollectionUtils; @@ -436,7 +437,7 @@ public class NotificationBackend { } } - public List getAssistantAdjustments(String pkg) { + public List getAllowedAssistantAdjustments(String pkg) { try { return sINM.getAllowedAssistantAdjustments(pkg); } catch (Exception e) { @@ -769,6 +770,23 @@ public class NotificationBackend { } } + public @NonNull String[] getAdjustmentDeniedPackages(String key) { + try { + return sINM.getAdjustmentDeniedPackages(key); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return new String[]{}; + } + } + + public @NonNull void setAdjustmentSupportedForPackage(String key, String pkg, boolean enabled) { + try { + sINM.setAdjustmentSupportedForPackage(key, pkg, enabled); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + } + } + @VisibleForTesting void setNm(INotificationManager inm) { sINM = inm; diff --git a/src/com/android/settings/notification/app/AdjustmentKeyPreferenceController.java b/src/com/android/settings/notification/app/AdjustmentKeyPreferenceController.java new file mode 100644 index 00000000000..acde2daf294 --- /dev/null +++ b/src/com/android/settings/notification/app/AdjustmentKeyPreferenceController.java @@ -0,0 +1,98 @@ +/* + * 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.app; + +import android.app.Flags; +import android.content.Context; +import android.service.notification.Adjustment; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; + +import com.android.settings.notification.NotificationBackend; +import com.android.settingslib.RestrictedSwitchPreference; + +/** + * Used for the app-level preference screen to opt the app in or out of a provided Adjustment key. + * E.g. to say an app can or cannot be classified by the NotificationAssistantService. + */ +public class AdjustmentKeyPreferenceController extends + NotificationPreferenceController implements Preference.OnPreferenceChangeListener { + private String mKey; + + public AdjustmentKeyPreferenceController(@NonNull Context context, + @NonNull NotificationBackend backend, String key) { + super(context, backend); + mKey = key; + } + + @Override + @NonNull + public String getPreferenceKey() { + return mKey; + } + + @Override + public boolean isAvailable() { + if (!(Flags.notificationClassificationUi() || Flags.nmSummarizationUi() + || Flags.nmSummarization())) { + return false; + } + boolean isBundlePref = Adjustment.KEY_TYPE.equals(mKey); + boolean isSummarizePref = Adjustment.KEY_SUMMARIZATION.equals(mKey); + if (!Flags.notificationClassificationUi() && isBundlePref) { + return false; + } + if (!(Flags.nmSummarizationUi() || Flags.nmSummarization()) && isSummarizePref) { + return false; + } + if (!isSummarizePref && !isBundlePref) { + return false; + } + if (isSummarizePref && !(mBackend.hasSentValidMsg(mAppRow.pkg, mAppRow.uid) + || mBackend.isInInvalidMsgState(mAppRow.pkg, mAppRow.uid))) { + return false; + } + return super.isAvailable(); + } + + @Override + boolean isIncludedInFilter() { + // not a channel-specific preference; only at the app level + return false; + } + + public void updateState(@NonNull Preference preference) { + RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference; + if (pref.getParent() != null) { + pref.getParent().setVisible(true); + } + + if (pref != null && mAppRow != null) { + pref.setDisabledByAdmin(mAdmin); + pref.setEnabled(!pref.isDisabledByAdmin()); + pref.setChecked(mBackend.getAllowedAssistantAdjustments(mAppRow.pkg).contains(mKey)); + pref.setOnPreferenceChangeListener(this); + } + } + + @Override + public boolean onPreferenceChange(@NonNull Preference preference, @NonNull Object newValue) { + final boolean allowedForPkg = (Boolean) newValue; + mBackend.setAdjustmentSupportedForPackage(mKey, mAppRow.pkg, allowedForPkg); + return true; + } +} diff --git a/src/com/android/settings/notification/app/AppNotificationSettings.java b/src/com/android/settings/notification/app/AppNotificationSettings.java index e0bcad37fc3..1fde8fa1de3 100644 --- a/src/com/android/settings/notification/app/AppNotificationSettings.java +++ b/src/com/android/settings/notification/app/AppNotificationSettings.java @@ -21,6 +21,7 @@ import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; import android.provider.Settings; +import android.service.notification.Adjustment; import android.text.TextUtils; import android.util.Log; @@ -132,6 +133,10 @@ public class AppNotificationSettings extends NotificationSettings { context, mDependentFieldListener, mBackend)); mControllers.add(new BundleListPreferenceController(context, mBackend)); mControllers.add(new PromotedNotificationsPreferenceController(context, mBackend)); + mControllers.add(new AdjustmentKeyPreferenceController(context, mBackend, + Adjustment.KEY_SUMMARIZATION)); + mControllers.add(new AdjustmentKeyPreferenceController(context, mBackend, + Adjustment.KEY_TYPE)); return new ArrayList<>(mControllers); } } diff --git a/tests/robotests/src/com/android/settings/notification/app/AdjustmentKeyPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/app/AdjustmentKeyPreferenceControllerTest.java new file mode 100644 index 00000000000..f458769e19e --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/app/AdjustmentKeyPreferenceControllerTest.java @@ -0,0 +1,149 @@ +/* + * 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.app; + +import static android.service.notification.Adjustment.KEY_IMPORTANCE; +import static android.service.notification.Adjustment.KEY_SUMMARIZATION; +import static android.service.notification.Adjustment.KEY_TYPE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +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.Flags; +import android.content.Context; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.preference.PreferenceManager; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.notification.NotificationBackend; +import com.android.settingslib.RestrictedSwitchPreference; + +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 java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class AdjustmentKeyPreferenceControllerTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + private NotificationBackend.AppRow mAppRow; + @Mock + private NotificationBackend mBackend; + private RestrictedSwitchPreference mSwitch; + + private AdjustmentKeyPreferenceController mPrefController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = ApplicationProvider.getApplicationContext(); + mSwitch = new RestrictedSwitchPreference(mContext); + new PreferenceManager(mContext).createPreferenceScreen(mContext).addPreference(mSwitch); + when(mBackend.hasSentValidMsg(anyString(), anyInt())).thenReturn(true); + + mPrefController = new AdjustmentKeyPreferenceController(mContext, mBackend, KEY_TYPE); + + mAppRow = new NotificationBackend.AppRow(); + mAppRow.pkg = "pkg.name"; + mAppRow.uid = 12345; + mPrefController.onResume(mAppRow, null, null, null, null, null, null); + } + + @Test + @DisableFlags({Flags.FLAG_NM_SUMMARIZATION, Flags.FLAG_NM_SUMMARIZATION_UI, + Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI}) + public void testIsAvailable_flagOff() { + assertThat(mPrefController.isAvailable()).isFalse(); + } + + @Test + @EnableFlags({Flags.FLAG_NM_SUMMARIZATION, Flags.FLAG_NM_SUMMARIZATION_UI, + Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI}) + public void testIsAvailable_flagOn() { + assertThat(mPrefController.isAvailable()).isTrue(); + } + + @Test + @EnableFlags({Flags.FLAG_NM_SUMMARIZATION, Flags.FLAG_NM_SUMMARIZATION_UI, + Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI}) + public void testIsAvailable_summarization_notMsgApp() { + when(mBackend.hasSentValidMsg(anyString(), anyInt())).thenReturn(false); + + mPrefController = new AdjustmentKeyPreferenceController( + mContext, mBackend, KEY_SUMMARIZATION); + mPrefController.onResume(mAppRow, null, null, null, null, null, null); + + assertThat(mPrefController.isAvailable()).isFalse(); + } + + @Test + @EnableFlags({Flags.FLAG_NM_SUMMARIZATION, Flags.FLAG_NM_SUMMARIZATION_UI, + Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI}) + public void testChecked_adjustmentAllowed() { + when(mBackend.getAllowedAssistantAdjustments(mAppRow.pkg)).thenReturn( + List.of(KEY_TYPE, KEY_IMPORTANCE)); + mPrefController.onResume(mAppRow, null, null, null, null, null, null); + + mPrefController.updateState(mSwitch); + assertThat(mSwitch.isChecked()).isTrue(); + + when(mBackend.getAllowedAssistantAdjustments(mAppRow.pkg)).thenReturn( + List.of(KEY_SUMMARIZATION, KEY_IMPORTANCE)); + mPrefController.onResume(mAppRow, null, null, null, null, null, null); + mPrefController.updateState(mSwitch); + assertThat(mSwitch.isChecked()).isFalse(); + } + + @Test + @EnableFlags({Flags.FLAG_NM_SUMMARIZATION, Flags.FLAG_NM_SUMMARIZATION_UI, + Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI}) + public void testOnPreferenceChange_changeOnAndOff() { + when(mBackend.getAllowedAssistantAdjustments(mAppRow.pkg)).thenReturn( + List.of(KEY_TYPE, KEY_IMPORTANCE)); + mPrefController.onResume(mAppRow, null, null, null, null, null, null); + + // when the switch value changes to false + mPrefController.onPreferenceChange(mSwitch, false); + + verify(mBackend, times(1)) + .setAdjustmentSupportedForPackage(eq(KEY_TYPE), eq(mAppRow.pkg), eq(false)); + + // same as above but now from false -> true + mPrefController.onPreferenceChange(mSwitch, true); + verify(mBackend, times(1)) + .setAdjustmentSupportedForPackage(eq(KEY_TYPE), eq(mAppRow.pkg), eq(true)); + } +}