diff --git a/res/values/strings.xml b/res/values/strings.xml index ce2e3985b4e..83a537d8f90 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8847,6 +8847,13 @@ Notification bundles + + Notification summaries + On + Off + Use notification summaries + Automatically summarize conversation notifications from any app + Live notifications @@ -9269,7 +9276,6 @@ 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. - VR helper services diff --git a/res/xml/configure_notification_settings.xml b/res/xml/configure_notification_settings.xml index e5fddc4e58e..a46769a6032 100644 --- a/res/xml/configure_notification_settings.xml +++ b/res/xml/configure_notification_settings.xml @@ -25,7 +25,7 @@ @@ -35,7 +35,7 @@ + + diff --git a/res/xml/summarization_notifications_settings.xml b/res/xml/summarization_notifications_settings.xml new file mode 100644 index 00000000000..c6de6265709 --- /dev/null +++ b/res/xml/summarization_notifications_settings.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java index 5b1dbde910a..d7a747ce004 100644 --- a/src/com/android/settings/notification/NotificationBackend.java +++ b/src/com/android/settings/notification/NotificationBackend.java @@ -687,6 +687,37 @@ public class NotificationBackend { } } + public boolean isNotificationSummarizationSupported() { + try { + return !sINM.getUnsupportedAdjustmentTypes().contains(Adjustment.KEY_SUMMARIZATION); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + } + return false; + } + + public boolean isNotificationSummarizationEnabled(Context context) { + try { + return sINM.getAllowedAssistantAdjustments(context.getPackageName()) + .contains(Adjustment.KEY_SUMMARIZATION); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + } + return false; + } + + public void setNotificationSummarizationEnabled(boolean enabled) { + try { + if (enabled) { + sINM.allowAssistantAdjustment(Adjustment.KEY_SUMMARIZATION); + } else { + sINM.disallowAssistantAdjustment(Adjustment.KEY_SUMMARIZATION); + } + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + } + } + public boolean isBundleTypeApproved(@Adjustment.Types int type) { try { int[] approved = sINM.getAllowedAdjustmentKeyTypes(); diff --git a/src/com/android/settings/notification/SummarizationGlobalPreferenceController.java b/src/com/android/settings/notification/SummarizationGlobalPreferenceController.java new file mode 100644 index 00000000000..0d66f37a91b --- /dev/null +++ b/src/com/android/settings/notification/SummarizationGlobalPreferenceController.java @@ -0,0 +1,62 @@ +/* + * 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.widget.SettingsMainSwitchPreferenceController; + +public class SummarizationGlobalPreferenceController extends + SettingsMainSwitchPreferenceController { + + NotificationBackend mBackend; + + public SummarizationGlobalPreferenceController(@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; + } + + @Override + public boolean isChecked() { + return mBackend.isNotificationSummarizationEnabled(mContext); + } + + @Override + public boolean setChecked(boolean isChecked) { + mBackend.setNotificationSummarizationEnabled(isChecked); + return true; + } + + @Override + public int getSliceHighlightMenuRes() { + // not needed since it's not sliceable + return NO_RES; + } +} diff --git a/src/com/android/settings/notification/SummarizationPreferenceController.java b/src/com/android/settings/notification/SummarizationPreferenceController.java new file mode 100644 index 00000000000..8857944c794 --- /dev/null +++ b/src/com/android/settings/notification/SummarizationPreferenceController.java @@ -0,0 +1,50 @@ +/* + * 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 com.android.settings.R; +import com.android.settings.core.BasePreferenceController; + +/** + * Controller for the summarized notifications settings page. + */ +public class SummarizationPreferenceController extends BasePreferenceController { + + NotificationBackend mBackend; + + public SummarizationPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mBackend = new NotificationBackend(); + } + + @Override + public int getAvailabilityStatus() { + return (Flags.nmSummarization() || Flags.nmSummarizationUi()) + && mBackend.isNotificationSummarizationSupported() + ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public CharSequence getSummary() { + return mBackend.isNotificationSummarizationEnabled(mContext) + ? mContext.getString(R.string.notification_summarization_on) + : mContext.getString(R.string.notification_summarization_off); + } +} diff --git a/src/com/android/settings/notification/SummarizationPreferenceFragment.java b/src/com/android/settings/notification/SummarizationPreferenceFragment.java new file mode 100644 index 00000000000..1c8f5e3059d --- /dev/null +++ b/src/com/android/settings/notification/SummarizationPreferenceFragment.java @@ -0,0 +1,56 @@ +/* + * 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.app.settings.SettingsEnums; +import android.content.Context; + +import com.android.settings.R; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.search.SearchIndexable; + +/** + * Fragment for summarized notifications. + */ +@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) +public class SummarizationPreferenceFragment extends DashboardFragment { + + @Override + public int getMetricsCategory() { + return SettingsEnums.SUMMARIZED_NOTIFICATIONS; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.summarization_notifications_settings; + } + @Override + protected String getLogTag() { + return "SummarizationPreferenceFragment"; + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.summarization_notifications_settings) { + + @Override + protected boolean isPageSearchEnabled(Context context) { + return Flags.notificationClassificationUi(); + } + }; +} diff --git a/tests/robotests/src/com/android/settings/notification/SummarizationGlobalPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/SummarizationGlobalPreferenceControllerTest.java new file mode 100644 index 00000000000..b0bf5db6cb8 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/SummarizationGlobalPreferenceControllerTest.java @@ -0,0 +1,100 @@ +/* + * 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.verify; +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 SummarizationGlobalPreferenceControllerTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private static final String PREFERENCE_KEY = "preference_key"; + + private Context mContext; + SummarizationGlobalPreferenceController 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 SummarizationGlobalPreferenceController(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(); + } + + @Test + public void isChecked() throws Exception { + when(mInm.getAllowedAssistantAdjustments(any())).thenReturn(List.of(KEY_SUMMARIZATION)); + assertThat(mController.isChecked()).isTrue(); + + when(mInm.getAllowedAssistantAdjustments(any())).thenReturn(List.of(KEY_IMPORTANCE)); + assertThat(mController.isChecked()).isFalse(); + } + + @Test + public void setChecked() throws Exception { + mController.setChecked(false); + verify(mInm).disallowAssistantAdjustment(KEY_SUMMARIZATION); + + mController.setChecked(true); + verify(mInm).allowAssistantAdjustment(KEY_SUMMARIZATION); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/SummarizationPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/SummarizationPreferenceControllerTest.java new file mode 100644 index 00000000000..ed93290696e --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/SummarizationPreferenceControllerTest.java @@ -0,0 +1,90 @@ +/* + * 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 SummarizationPreferenceControllerTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private static final String PREFERENCE_KEY = "preference_key"; + + private Context mContext; + SummarizationPreferenceController 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 SummarizationPreferenceController(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(); + } + + @Test + public void getSummary() throws Exception { + when(mInm.getAllowedAssistantAdjustments(any())).thenReturn(List.of(KEY_SUMMARIZATION)); + assertThat(mController.getSummary()).isEqualTo("On"); + + when(mInm.getAllowedAssistantAdjustments(any())).thenReturn(List.of(KEY_IMPORTANCE)); + assertThat(mController.getSummary()).isEqualTo("Off"); + } +}