Hide redundant foreground service notifications.

If an app with a foreground service has (at least one)
FLAG_FOREGROUND notification shown to the user, we allow
that to satisfy the requirement that the user be informed
about such things. But if the fg notification or its channel
is blocked by the user, we show the NOTE_FOREGROUND_SERVICES
notification (a/k/a Dianne's Dungeon) provided to us by the
activity manager.

Note that if even one of the foreground processes for the
current user is missing its disclosure notification, the
user will see the whole dungeon.

Bug: 36891897
Test: runtest -x frameworks/base/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
Change-Id: I435b7c01e60f99da162c9ea61656e2991d2cb87c
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 5197c95..73a2a43 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -272,6 +272,9 @@
 
         mProviders.put(TunablePaddingService.class, () -> new TunablePaddingService());
 
+        mProviders.put(ForegroundServiceController.class,
+                () -> new ForegroundServiceControllerImpl(mContext));
+
         mProviders.put(UiOffloadThread.class, UiOffloadThread::new);
 
         // Put all dependencies above here so the factory can override them if it wants.
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
new file mode 100644
index 0000000..a2c9ab4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 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.systemui;
+
+import android.service.notification.StatusBarNotification;
+
+public interface ForegroundServiceController {
+    /**
+     * @param sbn notification that was just posted
+     * @param importance
+     */
+    void addNotification(StatusBarNotification sbn, int importance);
+
+    /**
+     * @param sbn notification that was just changed in some way
+     * @param newImportance
+     */
+    void updateNotification(StatusBarNotification sbn, int newImportance);
+
+    /**
+     * @param sbn notification that was just canceled
+     */
+    boolean removeNotification(StatusBarNotification sbn);
+
+    /**
+     * @param userId
+     * @return true if this user has services missing notifications and therefore needs a
+     * disclosure notification.
+     */
+    boolean isDungeonNeededForUser(int userId);
+
+    /**
+     * @param sbn
+     * @return true if sbn is the system-provided "dungeon" (list of running foreground services).
+     */
+    boolean isDungeonNotification(StatusBarNotification sbn);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java
new file mode 100644
index 0000000..c930d56
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2017 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.systemui;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto;
+
+import java.util.Arrays;
+
+/**
+ * Foreground service controller, a/k/a Dianne's Dungeon.
+ */
+public class ForegroundServiceControllerImpl
+        implements ForegroundServiceController {
+    private static final String TAG = "FgServiceController";
+    private static final boolean DBG = false;
+
+    private final SparseArray<UserServices> mUserServices = new SparseArray<>();
+    private final Object mMutex = new Object();
+
+    public ForegroundServiceControllerImpl(Context context) {
+    }
+
+    @Override
+    public boolean isDungeonNeededForUser(int userId) {
+        synchronized (mMutex) {
+            final UserServices services = mUserServices.get(userId);
+            if (services == null) return false;
+            return services.isDungeonNeeded();
+        }
+    }
+
+    @Override
+    public void addNotification(StatusBarNotification sbn, int importance) {
+        updateNotification(sbn, importance);
+    }
+
+    @Override
+    public boolean removeNotification(StatusBarNotification sbn) {
+        synchronized (mMutex) {
+            final UserServices userServices = mUserServices.get(sbn.getUserId());
+            if (userServices == null) {
+                if (DBG) {
+                    Log.w(TAG, String.format(
+                            "user %d with no known notifications got removeNotification for %s",
+                            sbn.getUserId(), sbn));
+                }
+                return false;
+            }
+            if (isDungeonNotification(sbn)) {
+                // if you remove the dungeon entirely, we take that to mean there are
+                // no running services
+                userServices.setRunningServices(null);
+                return true;
+            } else {
+                // this is safe to call on any notification, not just FLAG_FOREGROUND_SERVICE
+                return userServices.removeNotification(sbn.getPackageName(), sbn.getKey());
+            }
+        }
+    }
+
+    @Override
+    public void updateNotification(StatusBarNotification sbn, int newImportance) {
+        synchronized (mMutex) {
+            UserServices userServices = mUserServices.get(sbn.getUserId());
+            if (userServices == null) {
+                userServices = new UserServices();
+                mUserServices.put(sbn.getUserId(), userServices);
+            }
+
+            if (isDungeonNotification(sbn)) {
+                final Bundle extras = sbn.getNotification().extras;
+                if (extras != null) {
+                    final String[] svcs = extras.getStringArray(Notification.EXTRA_FOREGROUND_APPS);
+                    userServices.setRunningServices(svcs); // null ok
+                }
+            } else {
+                userServices.removeNotification(sbn.getPackageName(), sbn.getKey());
+                if (0 != (sbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE)
+                        && newImportance > NotificationManager.IMPORTANCE_MIN) {
+                    userServices.addNotification(sbn.getPackageName(), sbn.getKey());
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean isDungeonNotification(StatusBarNotification sbn) {
+        return sbn.getId() == SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES
+                && sbn.getTag() == null
+                && sbn.getPackageName().equals("android");
+    }
+
+    /**
+     * Struct to track relevant packages and notifications for a userid's foreground services.
+     */
+    private static class UserServices {
+        private String[] mRunning = null;
+        private ArrayMap<String, ArraySet<String>> mNotifications = new ArrayMap<>(1);
+        public void setRunningServices(String[] pkgs) {
+            mRunning = pkgs != null ? Arrays.copyOf(pkgs, pkgs.length) : null;
+        }
+        public void addNotification(String pkg, String key) {
+            if (mNotifications.get(pkg) == null) {
+                mNotifications.put(pkg, new ArraySet<String>());
+            }
+            mNotifications.get(pkg).add(key);
+        }
+        public boolean removeNotification(String pkg, String key) {
+            final boolean found;
+            final ArraySet<String> keys = mNotifications.get(pkg);
+            if (keys == null) {
+                found = false;
+            } else {
+                found = keys.remove(key);
+                if (keys.size() == 0) {
+                    mNotifications.remove(pkg);
+                }
+            }
+            return found;
+        }
+        public boolean isDungeonNeeded() {
+            if (mRunning != null) {
+                for (String pkg : mRunning) {
+                    final ArraySet<String> set = mNotifications.get(pkg);
+                    if (set == null || set.size() == 0) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
index 1844946..531437d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
@@ -24,6 +24,8 @@
 import android.content.pm.PackageManager;
 import android.content.Context;
 import android.graphics.drawable.Icon;
+import android.os.AsyncTask;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.service.notification.NotificationListenerService;
@@ -38,8 +40,11 @@
 import android.Manifest;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.Dependency;
+import com.android.systemui.ForegroundServiceController;
 import com.android.systemui.statusbar.notification.InflationException;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.StatusBar;
@@ -339,6 +344,7 @@
             mEntries.put(entry.notification.getKey(), entry);
         }
         mGroupManager.onEntryAdded(entry);
+
         updateRankingAndSort(mRankingMap);
     }
 
@@ -466,6 +472,10 @@
         Collections.sort(mSortedAndFiltered, mRankingComparator);
     }
 
+    /**
+     * @param sbn
+     * @return true if this notification should NOT be shown right now
+     */
     public boolean shouldFilterOut(StatusBarNotification sbn) {
         if (!(mEnvironment.isDeviceProvisioned() ||
                 showNotificationEvenIfUnprovisioned(sbn))) {
@@ -487,6 +497,13 @@
                 && mGroupManager.isChildInGroupWithSummary(sbn)) {
             return true;
         }
+
+        final ForegroundServiceController fsc = Dependency.get(ForegroundServiceController.class);
+        if (fsc.isDungeonNotification(sbn) && !fsc.isDungeonNeededForUser(sbn.getUserId())) {
+            // this is a foreground-service disclosure for a user that does not need to show one
+            return true;
+        }
+
         return false;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index a7e95d7..59c32a8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -157,6 +157,7 @@
 import com.android.systemui.DemoMode;
 import com.android.systemui.Dependency;
 import com.android.systemui.EventLogTags;
+import com.android.systemui.ForegroundServiceController;
 import com.android.systemui.Interpolators;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
@@ -732,6 +733,7 @@
     private boolean mClearAllEnabled;
     @Nullable private View mAmbientIndicationContainer;
     private ColorExtractor mColorExtractor;
+    private ForegroundServiceController mForegroundServiceController;
 
     private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) {
         final int N = array.size();
@@ -781,6 +783,9 @@
         mColorExtractor.addOnColorsChangedListener(this);
 
         mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+
+        mForegroundServiceController = Dependency.get(ForegroundServiceController.class);
+
         mDisplay = mWindowManager.getDefaultDisplay();
         updateDisplaySize();
 
@@ -1614,6 +1619,10 @@
             }
         }
         abortExistingInflation(key);
+
+        mForegroundServiceController.addNotification(notification,
+                mNotificationData.getImportance(key));
+
         mPendingNotifications.put(key, shadeEntry);
     }
 
@@ -1752,6 +1761,10 @@
             return;
         }
 
+        if (entry != null) {
+            mForegroundServiceController.removeNotification(entry.notification);
+        }
+
         if (entry != null && entry.row != null) {
             entry.row.setRemoved();
             mStackScroller.cleanUpViewState(entry.row);
@@ -6942,6 +6955,9 @@
         entry.updateIcons(mContext, n);
         inflateViews(entry, mStackScroller);
 
+        mForegroundServiceController.updateNotification(notification,
+                mNotificationData.getImportance(key));
+
         boolean shouldPeek = shouldPeek(entry, notification);
         boolean alertAgain = alertAgain(entry, n);
 
@@ -6959,6 +6975,7 @@
             boolean isForCurrentUser = isNotificationForCurrentProfiles(notification);
             Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
         }
+
         setAreThereNotifications();
     }