Add granular control over disabled effects.

Currently, we can only wholesale disable effects outside of zen mode.
There are some devices that require long running suppression of features.
Effects disabling was added previously to support this. However, calling
suppression was broken recently on a device where we needed notification
suppression, but not calling suppression.

This CL remedies this problem by adding more granular disabled effects
hints. The NotificationManagerService has been updated to handle these
request types and present ZenModeHelper with a unified map of what should
be suppressed.

Bug: 27992763
Change-Id: I38b4706446dfd6e1c3eb97266dbc3332b250adda
diff --git a/api/current.txt b/api/current.txt
index 07a4f6d..6097a27 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -34693,7 +34693,9 @@
     method public static void requestRebind(android.content.ComponentName) throws android.os.RemoteException;
     method public final void requestUnbind() throws android.os.RemoteException;
     method public final void setNotificationsShown(java.lang.String[]);
+    field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4
     field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1
+    field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2
     field public static final int INTERRUPTION_FILTER_ALARMS = 4; // 0x4
     field public static final int INTERRUPTION_FILTER_ALL = 1; // 0x1
     field public static final int INTERRUPTION_FILTER_NONE = 3; // 0x3
diff --git a/api/system-current.txt b/api/system-current.txt
index e1e73e9..bbdc48c 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -37176,7 +37176,9 @@
     method public final void setNotificationsShown(java.lang.String[]);
     method public final void setOnNotificationPostedTrim(int);
     method public void unregisterAsSystemService() throws android.os.RemoteException;
+    field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4
     field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1
+    field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2
     field public static final int INTERRUPTION_FILTER_ALARMS = 4; // 0x4
     field public static final int INTERRUPTION_FILTER_ALL = 1; // 0x1
     field public static final int INTERRUPTION_FILTER_NONE = 3; // 0x3
diff --git a/api/test-current.txt b/api/test-current.txt
index 09c55b9..2a42177 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -34765,7 +34765,9 @@
     method public static void requestRebind(android.content.ComponentName) throws android.os.RemoteException;
     method public final void requestUnbind() throws android.os.RemoteException;
     method public final void setNotificationsShown(java.lang.String[]);
+    field public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 4; // 0x4
     field public static final int HINT_HOST_DISABLE_EFFECTS = 1; // 0x1
+    field public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 2; // 0x2
     field public static final int INTERRUPTION_FILTER_ALARMS = 4; // 0x4
     field public static final int INTERRUPTION_FILTER_ALL = 1; // 0x1
     field public static final int INTERRUPTION_FILTER_NONE = 3; // 0x3
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index afdd1d4..9a41edf 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -117,6 +117,16 @@
      * This does not change the interruption filter, only the effects. **/
     public static final int HINT_HOST_DISABLE_EFFECTS = 1;
 
+    /** {@link #getCurrentListenerHints() Listener hints} constant - the primary device UI
+     * should disable notification sound, but not phone calls.
+     * This does not change the interruption filter, only the effects. **/
+    public static final int HINT_HOST_DISABLE_NOTIFICATION_EFFECTS = 1 << 1;
+
+    /** {@link #getCurrentListenerHints() Listener hints} constant - the primary device UI
+     * should disable phone call sounds, buyt not notification sound.
+     * This does not change the interruption filter, only the effects. **/
+    public static final int HINT_HOST_DISABLE_CALL_EFFECTS = 1 << 2;
+
     /**
      * Whether notification suppressed by DND should not interruption visually when the screen is
      * off.
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 124d7f1..19a1805 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -32,6 +32,8 @@
 import static android.service.notification.NotificationRankerService.REASON_PROFILE_TURNED_OFF;
 import static android.service.notification.NotificationRankerService.REASON_USER_STOPPED;
 import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_EFFECTS;
+import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS;
+import static android.service.notification.NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS;
 import static android.service.notification.NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_OFF;
 import static android.service.notification.NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON;
 import static android.service.notification.NotificationListenerService.TRIM_FULL;
@@ -114,6 +116,7 @@
 import android.util.AtomicFile;
 import android.util.Log;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.util.Xml;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
@@ -248,8 +251,9 @@
     private String mSoundNotificationKey;
     private String mVibrateNotificationKey;
 
-    private final ArraySet<ManagedServiceInfo> mListenersDisablingEffects = new ArraySet<>();
-    private ComponentName mEffectsSuppressor;
+    private final SparseArray<ArraySet<ManagedServiceInfo>> mListenersDisablingEffects =
+            new SparseArray<ArraySet<ManagedServiceInfo>>();
+    private List<ComponentName> mEffectsSuppressors = new ArrayList<ComponentName>();
     private int mListenerHints;  // right now, all hints are global
     private int mInterruptionFilter = NotificationListenerService.INTERRUPTION_FILTER_UNKNOWN;
 
@@ -1119,23 +1123,112 @@
     }
 
     private void updateListenerHintsLocked() {
-        final int hints = mListenersDisablingEffects.isEmpty() ? 0 : HINT_HOST_DISABLE_EFFECTS;
+        final int hints = calculateHints();
         if (hints == mListenerHints) return;
-        ZenLog.traceListenerHintsChanged(mListenerHints, hints, mListenersDisablingEffects.size());
+        ZenLog.traceListenerHintsChanged(mListenerHints, hints, mEffectsSuppressors.size());
         mListenerHints = hints;
         scheduleListenerHintsChanged(hints);
     }
 
     private void updateEffectsSuppressorLocked() {
-        final ComponentName suppressor = !mListenersDisablingEffects.isEmpty()
-                ? mListenersDisablingEffects.valueAt(0).component : null;
-        if (Objects.equals(suppressor, mEffectsSuppressor)) return;
-        ZenLog.traceEffectsSuppressorChanged(mEffectsSuppressor, suppressor);
-        mEffectsSuppressor = suppressor;
-        mZenModeHelper.setEffectsSuppressed(suppressor != null);
+        final long updatedSuppressedEffects = calculateSuppressedEffects();
+        if (updatedSuppressedEffects == mZenModeHelper.getSuppressedEffects()) return;
+        final List<ComponentName> suppressors = getSuppressors();
+        ZenLog.traceEffectsSuppressorChanged(mEffectsSuppressors, suppressors, updatedSuppressedEffects);
+        mEffectsSuppressors = suppressors;
+        mZenModeHelper.setSuppressedEffects(updatedSuppressedEffects);
         sendRegisteredOnlyBroadcast(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED);
     }
 
+    private ArrayList<ComponentName> getSuppressors() {
+        ArrayList<ComponentName> names = new ArrayList<ComponentName>();
+        for (int i = mListenersDisablingEffects.size() - 1; i >= 0; --i) {
+            ArraySet<ManagedServiceInfo> serviceInfoList = mListenersDisablingEffects.valueAt(i);
+
+            for (ManagedServiceInfo info : serviceInfoList) {
+                names.add(info.component);
+            }
+        }
+
+        return names;
+    }
+
+    private boolean removeDisabledHints(ManagedServiceInfo info) {
+        return removeDisabledHints(info, 0);
+    }
+
+    private boolean removeDisabledHints(ManagedServiceInfo info, int hints) {
+        boolean removed = false;
+
+        for (int i = mListenersDisablingEffects.size() - 1; i >= 0; --i) {
+            final int hint = mListenersDisablingEffects.keyAt(i);
+            final ArraySet<ManagedServiceInfo> listeners =
+                    mListenersDisablingEffects.valueAt(i);
+
+            if (hints == 0 || (hint & hints) == hint) {
+                removed = removed || listeners.remove(info);
+            }
+        }
+
+        return removed;
+    }
+
+    private void addDisabledHints(ManagedServiceInfo info, int hints) {
+        if ((hints & HINT_HOST_DISABLE_EFFECTS) != 0) {
+            addDisabledHint(info, HINT_HOST_DISABLE_EFFECTS);
+        }
+
+        if ((hints & HINT_HOST_DISABLE_NOTIFICATION_EFFECTS) != 0) {
+            addDisabledHint(info, HINT_HOST_DISABLE_NOTIFICATION_EFFECTS);
+        }
+
+        if ((hints & HINT_HOST_DISABLE_CALL_EFFECTS) != 0) {
+            addDisabledHint(info, HINT_HOST_DISABLE_CALL_EFFECTS);
+        }
+    }
+
+    private void addDisabledHint(ManagedServiceInfo info, int hint) {
+        if (mListenersDisablingEffects.indexOfKey(hint) < 0) {
+            mListenersDisablingEffects.put(hint, new ArraySet<ManagedServiceInfo>());
+        }
+
+        ArraySet<ManagedServiceInfo> hintListeners = mListenersDisablingEffects.get(hint);
+        hintListeners.add(info);
+    }
+
+    private int calculateHints() {
+        int hints = 0;
+        for (int i = mListenersDisablingEffects.size() - 1; i >= 0; --i) {
+            int hint = mListenersDisablingEffects.keyAt(i);
+            ArraySet<ManagedServiceInfo> serviceInfoList = mListenersDisablingEffects.valueAt(i);
+
+            if (!serviceInfoList.isEmpty()) {
+                hints |= hint;
+            }
+        }
+
+        return hints;
+    }
+
+    private long calculateSuppressedEffects() {
+        int hints = calculateHints();
+        long suppressedEffects = 0;
+
+        if ((hints & HINT_HOST_DISABLE_EFFECTS) != 0) {
+            suppressedEffects |= ZenModeHelper.SUPPRESSED_EFFECT_ALL;
+        }
+
+        if ((hints & HINT_HOST_DISABLE_NOTIFICATION_EFFECTS) != 0) {
+            suppressedEffects |= ZenModeHelper.SUPPRESSED_EFFECT_NOTIFICATIONS;
+        }
+
+        if ((hints & HINT_HOST_DISABLE_CALL_EFFECTS) != 0) {
+            suppressedEffects |= ZenModeHelper.SUPPRESSED_EFFECT_CALLS;
+        }
+
+        return suppressedEffects;
+    }
+
     private void updateInterruptionFilterLocked() {
         int interruptionFilter = mZenModeHelper.getZenModeListenerInterruptionFilter();
         if (interruptionFilter == mInterruptionFilter) return;
@@ -1652,11 +1745,14 @@
             try {
                 synchronized (mNotificationList) {
                     final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
-                    final boolean disableEffects = (hints & HINT_HOST_DISABLE_EFFECTS) != 0;
+                    final int disableEffectsMask = HINT_HOST_DISABLE_EFFECTS
+                            | HINT_HOST_DISABLE_NOTIFICATION_EFFECTS
+                            | HINT_HOST_DISABLE_CALL_EFFECTS;
+                    final boolean disableEffects = (hints & disableEffectsMask) != 0;
                     if (disableEffects) {
-                        mListenersDisablingEffects.add(info);
+                        addDisabledHints(info, hints);
                     } else {
-                        mListenersDisablingEffects.remove(info);
+                        removeDisabledHints(info, hints);
                     }
                     updateListenerHintsLocked();
                     updateEffectsSuppressorLocked();
@@ -1914,7 +2010,7 @@
         @Override
         public ComponentName getEffectsSuppressor() {
             enforceSystemOrSystemUIOrVolume("INotificationManager.getEffectsSuppressor");
-            return mEffectsSuppressor;
+            return mEffectsSuppressors.get(0);
         }
 
         @Override
@@ -2176,9 +2272,19 @@
                 pw.print("    mListenersDisablingEffects: (");
                 N = mListenersDisablingEffects.size();
                 for (int i = 0; i < N; i++) {
-                    final ManagedServiceInfo listener = mListenersDisablingEffects.valueAt(i);
-                    if (i > 0) pw.print(',');
-                    pw.print(listener.component);
+                    final int hint = mListenersDisablingEffects.keyAt(i);
+                    if (i > 0) pw.print(';');
+                    pw.print("hint[" + hint + "]:");
+
+                    final ArraySet<ManagedServiceInfo> listeners =
+                            mListenersDisablingEffects.valueAt(i);
+                    final int listenerSize = listeners.size();
+
+                    for (int j = 0; j < listenerSize; j++) {
+                        if (i > 0) pw.print(',');
+                        final ManagedServiceInfo listener = listeners.valueAt(i);
+                        pw.print(listener.component);
+                    }
                 }
                 pw.println(')');
                 pw.println("\n  mRankerServicePackageName: " + mRankerServicePackageName);
@@ -3690,7 +3796,7 @@
 
         @Override
         protected void onServiceRemovedLocked(ManagedServiceInfo removed) {
-            if (mListenersDisablingEffects.remove(removed)) {
+            if (removeDisabledHints(removed)) {
                 updateListenerHintsLocked();
                 updateEffectsSuppressorLocked();
             }
diff --git a/services/core/java/com/android/server/notification/ZenLog.java b/services/core/java/com/android/server/notification/ZenLog.java
index c45071b..207bdba 100644
--- a/services/core/java/com/android/server/notification/ZenLog.java
+++ b/services/core/java/com/android/server/notification/ZenLog.java
@@ -31,6 +31,7 @@
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
 import java.util.Date;
+import java.util.List;
 
 public class ZenLog {
     private static final String TAG = "ZenLog";
@@ -126,10 +127,11 @@
         append(TYPE_DISABLE_EFFECTS, record.getKey() + "," + reason);
     }
 
-    public static void traceEffectsSuppressorChanged(ComponentName oldSuppressor,
-            ComponentName newSuppressor) {
-        append(TYPE_SUPPRESSOR_CHANGED, componentToString(oldSuppressor) + "->"
-            + componentToString(newSuppressor));
+    public static void traceEffectsSuppressorChanged(List<ComponentName> oldSuppressors,
+            List<ComponentName> newSuppressors, long suppressedEffects) {
+        append(TYPE_SUPPRESSOR_CHANGED, "suppressed effects:" + suppressedEffects + ","
+                + componentListToString(oldSuppressors) + "->"
+                + componentListToString(newSuppressors));
     }
 
     public static void traceListenerHintsChanged(int oldHints, int newHints, int listenerCount) {
@@ -193,6 +195,19 @@
         return component != null ? component.toShortString() : null;
     }
 
+    private static String componentListToString(List<ComponentName> components) {
+        StringBuilder stringBuilder = new StringBuilder();
+
+        for (int i = 0; i < components.size(); ++i) {
+            if (i > 0) {
+                stringBuilder.append(", ");
+            }
+            stringBuilder.append(componentToString(components.get(i)));
+        }
+
+        return stringBuilder.toString();
+    }
+
     private static void append(int type, String msg) {
         synchronized(MSGS) {
             TIMES[sNext] = System.currentTimeMillis();
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 5c5c8f8..eb49e9f 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -102,7 +102,12 @@
     private ZenModeConfig mConfig;
     private AudioManagerInternal mAudioManager;
     private PackageManager mPm;
-    private boolean mEffectsSuppressed;
+    private long mSuppressedEffects;
+
+    public static final long SUPPRESSED_EFFECT_NOTIFICATIONS = 1;
+    public static final long SUPPRESSED_EFFECT_CALLS = 1 << 1;
+    public static final long SUPPRESSED_EFFECT_ALL = SUPPRESSED_EFFECT_CALLS
+            | SUPPRESSED_EFFECT_NOTIFICATIONS;
 
     public ZenModeHelper(Context context, Looper looper, ConditionProviders conditionProviders) {
         mContext = context;
@@ -228,12 +233,16 @@
         }
     }
 
-    public void setEffectsSuppressed(boolean effectsSuppressed) {
-        if (mEffectsSuppressed == effectsSuppressed) return;
-        mEffectsSuppressed = effectsSuppressed;
+    public void setSuppressedEffects(long suppressedEffects) {
+        if (mSuppressedEffects == suppressedEffects) return;
+        mSuppressedEffects = suppressedEffects;
         applyRestrictions();
     }
 
+    public long getSuppressedEffects() {
+        return mSuppressedEffects;
+    }
+
     public int getZenMode() {
         return mZenMode;
     }
@@ -484,7 +493,8 @@
         synchronized (mConfig) {
             dump(pw, prefix, "mConfig", mConfig);
         }
-        pw.print(prefix); pw.print("mEffectsSuppressed="); pw.println(mEffectsSuppressed);
+
+        pw.print(prefix); pw.print("mSuppressedEffects="); pw.println(mSuppressedEffects);
         mFiltering.dump(pw, prefix);
         mConditions.dump(pw, prefix);
     }
@@ -708,9 +718,11 @@
         final boolean zen = mZenMode != Global.ZEN_MODE_OFF;
 
         // notification restrictions
-        final boolean muteNotifications = mEffectsSuppressed;
+        final boolean muteNotifications =
+                (mSuppressedEffects & SUPPRESSED_EFFECT_NOTIFICATIONS) != 0;
         // call restrictions
-        final boolean muteCalls = zen && !mConfig.allowCalls && !mConfig.allowRepeatCallers;
+        final boolean muteCalls = zen && !mConfig.allowCalls && !mConfig.allowRepeatCallers
+                || (mSuppressedEffects & SUPPRESSED_EFFECT_CALLS) != 0;
         // total silence restrictions
         final boolean muteEverything = mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS;