Update channelBypassingDnd on user unlock + switch

Also add method that returns the number of apps
that are bypassing dnd

Test: atest PreferencesHelperTest
Fixes: 115972200
Bug: 111475013
Change-Id: Id75093f9f42d00d05cca7700f64493d702c6a518
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index bd9cf6d..362f4ae 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -88,6 +88,8 @@
     ParceledListSlice getRecentNotifyingAppsForUser(int userId);
     int getBlockedAppCount(int userId);
     boolean areChannelsBypassingDnd();
+    int getAppsBypassingDndCount(int uid);
+    ParceledListSlice getNotificationChannelsBypassingDnd(String pkg, int userId);
 
     // TODO: Remove this when callers have been migrated to the equivalent
     // INotificationListener method.
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 1c7572e..7bd52bc 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1160,6 +1160,7 @@
                     mConditionProviders.onUserSwitched(userId);
                     mListeners.onUserSwitched(userId);
                     mZenModeHelper.onUserSwitched(userId);
+                    mPreferencesHelper.onUserSwitched(userId);
                 }
                 // assistant is the only thing that cares about managed profiles specifically
                 mAssistants.onUserSwitched(userId);
@@ -1188,6 +1189,7 @@
                     mConditionProviders.onUserUnlocked(userId);
                     mListeners.onUserUnlocked(userId);
                     mZenModeHelper.onUserUnlocked(userId);
+                    mPreferencesHelper.onUserUnlocked(userId);
                 }
             }
         }
@@ -2525,6 +2527,19 @@
         }
 
         @Override
+        public int getAppsBypassingDndCount(int userId) {
+            checkCallerIsSystem();
+            return mPreferencesHelper.getAppsBypassingDndCount(userId);
+        }
+
+        @Override
+        public ParceledListSlice<NotificationChannel> getNotificationChannelsBypassingDnd(
+                String pkg, int userId) {
+            checkCallerIsSystem();
+            return mPreferencesHelper.getNotificationChannelsBypassingDnd(pkg, userId);
+        }
+
+        @Override
         public boolean areChannelsBypassingDnd() {
             return mPreferencesHelper.areChannelsBypassingDnd();
         }
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 8fce5e3..fd65ebe 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -111,7 +111,6 @@
     // pkg => PackagePreferences
     private final ArrayMap<String, PackagePreferences> mRestoredWithoutUids = new ArrayMap<>();
 
-
     private final Context mContext;
     private final PackageManager mPm;
     private final RankingHandler mRankingHandler;
@@ -120,7 +119,6 @@
     private SparseBooleanArray mBadgingEnabled;
     private boolean mAreChannelsBypassingDnd;
 
-
     public PreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
             ZenModeHelper zenHelper) {
         mContext = context;
@@ -129,11 +127,7 @@
         mPm = pm;
 
         updateBadgingEnabled();
-
-        mAreChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state &
-                NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) == 1;
-        updateChannelsBypassingDnd();
-
+        syncChannelsBypassingDnd(mContext.getUserId());
     }
 
     public void readXml(XmlPullParser parser, boolean forRestore)
@@ -525,6 +519,7 @@
                 // but the system can
                 if (group.isBlocked() != oldGroup.isBlocked()) {
                     group.lockFields(NotificationChannelGroup.USER_LOCKED_BLOCKED_STATE);
+                    updateChannelsBypassingDnd(mContext.getUserId());
                 }
                 if (group.canOverlayApps() != oldGroup.canOverlayApps()) {
                     group.lockFields(NotificationChannelGroup.USER_LOCKED_ALLOW_APP_OVERLAY);
@@ -571,6 +566,7 @@
 
             // Apps are allowed to downgrade channel importance if the user has not changed any
             // fields on this channel yet.
+            final int previousExistingImportance = existing.getImportance();
             if (existing.getUserLockedFields() == 0 &&
                     channel.getImportance() < existing.getImportance()) {
                 existing.setImportance(channel.getImportance());
@@ -582,8 +578,9 @@
                 boolean bypassDnd = channel.canBypassDnd();
                 existing.setBypassDnd(bypassDnd);
 
-                if (bypassDnd != mAreChannelsBypassingDnd) {
-                    updateChannelsBypassingDnd();
+                if (bypassDnd != mAreChannelsBypassingDnd
+                        || previousExistingImportance != existing.getImportance()) {
+                    updateChannelsBypassingDnd(mContext.getUserId());
                 }
             }
 
@@ -613,7 +610,7 @@
 
         r.channels.put(channel.getId(), channel);
         if (channel.canBypassDnd() != mAreChannelsBypassingDnd) {
-            updateChannelsBypassingDnd();
+            updateChannelsBypassingDnd(mContext.getUserId());
         }
         MetricsLogger.action(getChannelLog(channel, pkg).setType(
                 com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_OPEN));
@@ -663,8 +660,9 @@
             MetricsLogger.action(getChannelLog(updatedChannel, pkg));
         }
 
-        if (updatedChannel.canBypassDnd() != mAreChannelsBypassingDnd) {
-            updateChannelsBypassingDnd();
+        if (updatedChannel.canBypassDnd() != mAreChannelsBypassingDnd
+                || channel.getImportance() != updatedChannel.getImportance()) {
+            updateChannelsBypassingDnd(mContext.getUserId());
         }
         updateConfig();
     }
@@ -701,7 +699,7 @@
             MetricsLogger.action(lm);
 
             if (mAreChannelsBypassingDnd && channel.canBypassDnd()) {
-                updateChannelsBypassingDnd();
+                updateChannelsBypassingDnd(mContext.getUserId());
             }
         }
     }
@@ -859,6 +857,27 @@
     }
 
     /**
+     * Gets all notification channels associated with the given pkg and userId that can bypass dnd
+     */
+    public ParceledListSlice<NotificationChannel> getNotificationChannelsBypassingDnd(String pkg,
+            int userId) {
+        List<NotificationChannel> channels = new ArrayList<>();
+        synchronized (mPackagePreferences) {
+            final PackagePreferences r = mPackagePreferences.get(
+                    packagePreferencesKey(pkg, userId));
+            // notifications from this package aren't blocked
+            if (r != null && r.importance != IMPORTANCE_NONE) {
+                for (NotificationChannel channel : r.channels.values()) {
+                    if (channelIsLive(r, channel) && channel.canBypassDnd()) {
+                        channels.add(channel);
+                    }
+                }
+            }
+        }
+        return new ParceledListSlice<>(channels);
+    }
+
+    /**
      * True for pre-O apps that only have the default channel, or pre O apps that have no
      * channels yet. This method will create the default channel for pre-O apps that don't have it.
      * Should never be true for O+ targeting apps, but that's enforced on boot/when an app
@@ -922,18 +941,62 @@
         return count;
     }
 
-    public void updateChannelsBypassingDnd() {
+    /**
+     * Returns the number of apps that have at least one notification channel that can bypass DND
+     * for given particular user
+     */
+    public int getAppsBypassingDndCount(int userId) {
+        int count = 0;
         synchronized (mPackagePreferences) {
-            final int numPackagePreferencess = mPackagePreferences.size();
-            for (int PackagePreferencesIndex = 0; PackagePreferencesIndex < numPackagePreferencess;
-                    PackagePreferencesIndex++) {
-                final PackagePreferences r = mPackagePreferences.valueAt(PackagePreferencesIndex);
-                final int numChannels = r.channels.size();
+            final int numPackagePreferences = mPackagePreferences.size();
+            for (int i = 0; i < numPackagePreferences; i++) {
+                final PackagePreferences r = mPackagePreferences.valueAt(i);
+                // Package isn't associated with this userId or notifications from this package are
+                // blocked
+                if (userId != UserHandle.getUserId(r.uid) || r.importance == IMPORTANCE_NONE) {
+                    continue;
+                }
 
-                for (int channelIndex = 0; channelIndex < numChannels; channelIndex++) {
-                    NotificationChannel channel = r.channels.valueAt(channelIndex);
-                    if (!channel.isDeleted() && channel.canBypassDnd()) {
-                        // If any channel bypasses DND, synchronize state and return early.
+                for (NotificationChannel channel : r.channels.values()) {
+                    if (channelIsLive(r, channel) && channel.canBypassDnd()) {
+                        count++;
+                        break;
+                    }
+                }
+            }
+        }
+        return count;
+    }
+
+    /**
+     * Syncs {@link #mAreChannelsBypassingDnd} with the user's notification policy before
+     * updating
+     * @param userId
+     */
+    private void syncChannelsBypassingDnd(int userId) {
+        mAreChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state
+                & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) == 1;
+        updateChannelsBypassingDnd(userId);
+    }
+
+    /**
+     * Updates the user's NotificationPolicy based on whether the given userId
+     * has channels bypassing DND
+     * @param userId
+     */
+    private void updateChannelsBypassingDnd(int userId) {
+        synchronized (mPackagePreferences) {
+            final int numPackagePreferences = mPackagePreferences.size();
+            for (int i = 0; i < numPackagePreferences; i++) {
+                final PackagePreferences r = mPackagePreferences.valueAt(i);
+                // Package isn't associated with this userId or notifications from this package are
+                // blocked
+                if (userId != UserHandle.getUserId(r.uid) || r.importance == IMPORTANCE_NONE) {
+                    continue;
+                }
+
+                for (NotificationChannel channel : r.channels.values()) {
+                    if (channelIsLive(r, channel) && channel.canBypassDnd()) {
                         if (!mAreChannelsBypassingDnd) {
                             mAreChannelsBypassingDnd = true;
                             updateZenPolicy(true);
@@ -943,7 +1006,6 @@
                 }
             }
         }
-
         // If no channels bypass DND, update the zen policy once to disable DND bypass.
         if (mAreChannelsBypassingDnd) {
             mAreChannelsBypassingDnd = false;
@@ -951,6 +1013,22 @@
         }
     }
 
+    private boolean channelIsLive(PackagePreferences pkgPref, NotificationChannel channel) {
+        // Channel is in a group that's blocked
+        if (!TextUtils.isEmpty(channel.getGroup())) {
+            if (pkgPref.groups.get(channel.getGroup()).isBlocked()) {
+                return false;
+            }
+        }
+
+        // Channel is deleted or is blocked
+        if (channel.isDeleted() || channel.getImportance() == IMPORTANCE_NONE) {
+            return false;
+        }
+
+        return true;
+    }
+
     public void updateZenPolicy(boolean areChannelsBypassingDnd) {
         NotificationManager.Policy policy = mZenModeHelper.getNotificationPolicy();
         mZenModeHelper.setNotificationPolicy(new NotificationManager.Policy(
@@ -1329,6 +1407,20 @@
         return packageChannels;
     }
 
+    /**
+     * Called when user switches
+     */
+    public void onUserSwitched(int userId) {
+        syncChannelsBypassingDnd(userId);
+    }
+
+    /**
+     * Called when user is unlocked
+     */
+    public void onUserUnlocked(int userId) {
+        syncChannelsBypassingDnd(userId);
+    }
+
     public void onUserRemoved(int userId) {
         synchronized (mPackagePreferences) {
             int N = mPackagePreferences.size();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 3fe381b..1a218b2 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -27,7 +27,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -98,7 +97,7 @@
     private static final UserHandle USER = UserHandle.of(0);
     private static final int UID_O = 1111;
     private static final String SYSTEM_PKG = "android";
-    private static final int SYSTEM_UID= 1000;
+    private static final int SYSTEM_UID = 1000;
     private static final UserHandle USER2 = UserHandle.of(10);
     private static final String TEST_CHANNEL_ID = "test_channel_id";
     private static final String TEST_AUTHORITY = "test";
@@ -1091,6 +1090,158 @@
     }
 
     @Test
+    public void testGetChannelsBypassingDndCount_noChannelsBypassing() throws Exception {
+        assertEquals(0, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1,
+                USER.getIdentifier()).getList().size());
+    }
+
+    @Test
+    public void testGetChannelsBypassingDnd_noChannelsForUserIdBypassing()
+            throws Exception {
+        int user = 9;
+        NotificationChannel channel = new NotificationChannel("id", "name",
+                NotificationManager.IMPORTANCE_MAX);
+        channel.setBypassDnd(true);
+        mHelper.createNotificationChannel(PKG_N_MR1, 111, channel, true, true);
+
+        assertEquals(0, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1,
+                user).getList().size());
+    }
+
+    @Test
+    public void testGetChannelsBypassingDndCount_oneChannelBypassing_groupBlocked() {
+        int user = USER.getIdentifier();
+        NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1");
+        NotificationChannel channel1 = new NotificationChannel("id1", "name1",
+                NotificationManager.IMPORTANCE_MAX);
+        channel1.setBypassDnd(true);
+        channel1.setGroup(ncg.getId());
+        mHelper.createNotificationChannelGroup(PKG_N_MR1, user, ncg,  /* fromTargetApp */ true);
+        mHelper.createNotificationChannel(PKG_N_MR1, user, channel1, true, /*has DND access*/ true);
+
+        assertEquals(1, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1,
+                user).getList().size());
+
+        // disable group
+        ncg.setBlocked(true);
+        mHelper.createNotificationChannelGroup(PKG_N_MR1, user, ncg,  /* fromTargetApp */ false);
+        assertEquals(0, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1,
+                user).getList().size());
+    }
+
+    @Test
+    public void testGetChannelsBypassingDndCount_multipleChannelsBypassing() {
+        int user = USER.getIdentifier();
+        NotificationChannel channel1 = new NotificationChannel("id1", "name1",
+                NotificationManager.IMPORTANCE_MAX);
+        NotificationChannel channel2 = new NotificationChannel("id2", "name2",
+                NotificationManager.IMPORTANCE_MAX);
+        NotificationChannel channel3 = new NotificationChannel("id3", "name3",
+                NotificationManager.IMPORTANCE_MAX);
+        channel1.setBypassDnd(true);
+        channel2.setBypassDnd(true);
+        channel3.setBypassDnd(true);
+        // has DND access, so can set bypassDnd attribute
+        mHelper.createNotificationChannel(PKG_N_MR1, user, channel1, true, /*has DND access*/ true);
+        mHelper.createNotificationChannel(PKG_N_MR1, user, channel2, true, true);
+        mHelper.createNotificationChannel(PKG_N_MR1, user, channel3, true, true);
+        assertEquals(3, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1,
+                user).getList().size());
+
+        // block notifications from this app
+        mHelper.setEnabled(PKG_N_MR1, user, false);
+        assertEquals(0, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1,
+                user).getList().size());
+
+        // re-enable notifications from this app
+        mHelper.setEnabled(PKG_N_MR1, user, true);
+        assertEquals(3, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1,
+                user).getList().size());
+
+        // setBypassDnd false for some channels
+        channel1.setBypassDnd(false);
+        channel2.setBypassDnd(false);
+        assertEquals(1, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1,
+                user).getList().size());
+
+        // setBypassDnd false for rest of the channels
+        channel3.setBypassDnd(false);
+        assertEquals(0, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1,
+                user).getList().size());
+    }
+
+    @Test
+    public void testGetAppsBypassingDndCount_noAppsBypassing() throws Exception {
+        assertEquals(0, mHelper.getAppsBypassingDndCount(USER.getIdentifier()));
+    }
+
+    @Test
+    public void testGetAppsBypassingDndCount_noAppsForUserIdBypassing() throws Exception {
+        int user = 9;
+        NotificationChannel channel = new NotificationChannel("id", "name",
+                NotificationManager.IMPORTANCE_MAX);
+        channel.setBypassDnd(true);
+        mHelper.createNotificationChannel(PKG_N_MR1, 111, channel, true, true);
+
+        assertEquals(0, mHelper.getAppsBypassingDndCount(user));
+    }
+
+    @Test
+    public void testGetAppsBypassingDndCount_oneChannelBypassing_groupBlocked() {
+        int user = USER.getIdentifier();
+        NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1");
+        NotificationChannel channel1 = new NotificationChannel("id1", "name1",
+                NotificationManager.IMPORTANCE_MAX);
+        channel1.setBypassDnd(true);
+        channel1.setGroup(ncg.getId());
+        mHelper.createNotificationChannelGroup(PKG_N_MR1, user, ncg,  /* fromTargetApp */ true);
+        mHelper.createNotificationChannel(PKG_N_MR1, user, channel1, true, /*has DND access*/ true);
+
+        assertEquals(1, mHelper.getAppsBypassingDndCount(user));
+
+        // disable group
+        ncg.setBlocked(true);
+        mHelper.createNotificationChannelGroup(PKG_N_MR1, user, ncg,  /* fromTargetApp */ false);
+        assertEquals(0, mHelper.getAppsBypassingDndCount(user));
+    }
+
+    @Test
+    public void testGetAppsBypassingDndCount_oneAppBypassing() {
+        int user = USER.getIdentifier();
+        NotificationChannel channel1 = new NotificationChannel("id1", "name1",
+                NotificationManager.IMPORTANCE_MAX);
+        NotificationChannel channel2 = new NotificationChannel("id2", "name2",
+                NotificationManager.IMPORTANCE_MAX);
+        NotificationChannel channel3 = new NotificationChannel("id3", "name3",
+                NotificationManager.IMPORTANCE_MAX);
+        channel1.setBypassDnd(true);
+        channel2.setBypassDnd(true);
+        channel3.setBypassDnd(true);
+        // has DND access, so can set bypassDnd attribute
+        mHelper.createNotificationChannel(PKG_N_MR1, user, channel1, true, /*has DND access*/ true);
+        mHelper.createNotificationChannel(PKG_N_MR1, user, channel2, true, true);
+        mHelper.createNotificationChannel(PKG_N_MR1, user, channel3, true, true);
+        assertEquals(1, mHelper.getAppsBypassingDndCount(user));
+
+        // block notifications from this app
+        mHelper.setEnabled(PKG_N_MR1, user, false);
+        assertEquals(0, mHelper.getAppsBypassingDndCount(user)); // no apps can bypass dnd
+
+        // re-enable notifications from this app
+        mHelper.setEnabled(PKG_N_MR1, user, true);
+        assertEquals(1, mHelper.getAppsBypassingDndCount(user));
+
+        // setBypassDnd false for some channels
+        channel1.setBypassDnd(false);
+        channel2.setBypassDnd(false);
+        assertEquals(1, mHelper.getAppsBypassingDndCount(user));
+
+        // setBypassDnd false for rest of the channels
+        channel3.setBypassDnd(false);
+        assertEquals(0, mHelper.getAppsBypassingDndCount(user));
+    }
+
+    @Test
     public void testCreateAndDeleteCanChannelsBypassDnd() throws Exception {
         // create notification channel that can't bypass dnd
         // expected result: areChannelsBypassingDnd = false