From 2956baaf51e9347bb970e4dc861b5ef1f806be0f Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Wed, 26 Jun 2024 14:23:38 -0400 Subject: [PATCH] Show bundles separately from other channels Fixes: 346612561 Test: BundleListPreferenceControllerTest Flag: android.service.notification.notification_classification Change-Id: I2371219822f6a777788147e8249ca1e3b29d40ba --- res/values/strings.xml | 3 + res/xml/app_notification_settings.xml | 6 + .../app/AppNotificationSettings.java | 1 + .../app/BundleListPreferenceController.java | 171 ++++++++++++++++++ .../BundleListPreferenceControllerTest.java | 160 ++++++++++++++++ 5 files changed, 341 insertions(+) create mode 100644 src/com/android/settings/notification/app/BundleListPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/notification/app/BundleListPreferenceControllerTest.java 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); + } +}