Tag foreground notis that use certain services

- Draw over other apps
- Camera
- Microphone

The icons are not yet clickable, and the system 'drawing over
other apps' notification still appears even when the app's
notification is tagged.

Test: runtest systemui
Bug: 64085448
Change-Id: Ib3b0cdd9adced82f562f256cb81af80dc395440d
diff --git a/packages/SystemUI/res/layout/status_bar_notification_row.xml b/packages/SystemUI/res/layout/status_bar_notification_row.xml
index 4614999..2e7ab7f 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_row.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_row.xml
@@ -15,6 +15,7 @@
     limitations under the License.
 -->
 
+<!-- extends FrameLayout -->
 <com.android.systemui.statusbar.ExpandableNotificationRow
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
@@ -54,6 +55,7 @@
         android:paddingStart="8dp"
         />
 
+    <!-- TODO: remove -->
     <ImageButton
         android:id="@+id/helper"
         android:layout_width="48dp"
@@ -64,7 +66,7 @@
         android:tint="#FF0000"
         android:background="@drawable/ripple_drawable"
         android:visibility="visible"
-        />
+    />
 
     <ViewStub
         android:layout="@layout/notification_children_container"
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 7403ddc..cad155c 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -44,6 +44,7 @@
 import com.android.systemui.power.EnhancedEstimatesImpl;
 import com.android.systemui.power.PowerNotificationWarnings;
 import com.android.systemui.power.PowerUI;
+import com.android.systemui.statusbar.AppOpsListener;
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
 import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl;
 import com.android.systemui.statusbar.phone.LightBarController;
@@ -314,6 +315,8 @@
 
         mProviders.put(EnhancedEstimates.class, () -> new EnhancedEstimatesImpl());
 
+        mProviders.put(AppOpsListener.class, () -> new AppOpsListener(mContext));
+
         // Put all dependencies above here so the factory can override them if it wants.
         SystemUIFactory.getInstance().injectDependencies(mProviders, mContext);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
index a2c9ab4..5a2263c 100644
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
@@ -14,7 +14,9 @@
 
 package com.android.systemui;
 
+import android.annotation.Nullable;
 import android.service.notification.StatusBarNotification;
+import android.util.ArraySet;
 
 public interface ForegroundServiceController {
     /**
@@ -46,4 +48,32 @@
      * @return true if sbn is the system-provided "dungeon" (list of running foreground services).
      */
     boolean isDungeonNotification(StatusBarNotification sbn);
+
+    /**
+     * @return true if sbn is one of the window manager "drawing over other apps" notifications
+     */
+    boolean isSystemAlertNotification(StatusBarNotification sbn);
+
+    /**
+     * Returns the key of the foreground service from this package using the standard template,
+     * if one exists.
+     */
+    @Nullable String getStandardLayoutKey(int userId, String pkg);
+
+    /**
+     * @return true if this user/pkg has a missing or custom layout notification and therefore needs
+     * a disclosure notification for system alert windows.
+     */
+    boolean isSystemAlertWarningNeeded(int userId, String pkg);
+
+    /**
+     * Records active app ops. App Ops are stored in FSC in addition to NotificationData in
+     * case they change before we have a notification to tag.
+     */
+    void onAppOpChanged(int code, int uid, String packageName, boolean active);
+
+    /**
+     * Gets active app ops for this user and package.
+     */
+    @Nullable ArraySet<Integer> getAppOps(int userId, String packageName);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java
index 3714c4e..fc2b5b4 100644
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java
@@ -18,13 +18,13 @@
 import android.app.NotificationManager;
 import android.content.Context;
 import android.os.Bundle;
+import android.os.UserHandle;
 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;
@@ -34,17 +34,19 @@
  */
 public class ForegroundServiceControllerImpl
         implements ForegroundServiceController {
-  
+
     // shelf life of foreground services before they go bad
     public static final long FG_SERVICE_GRACE_MILLIS = 5000;
 
     private static final String TAG = "FgServiceController";
     private static final boolean DBG = false;
 
+    private final Context mContext;
     private final SparseArray<UserServices> mUserServices = new SparseArray<>();
     private final Object mMutex = new Object();
 
     public ForegroundServiceControllerImpl(Context context) {
+        mContext = context;
     }
 
     @Override
@@ -57,6 +59,52 @@
     }
 
     @Override
+    public boolean isSystemAlertWarningNeeded(int userId, String pkg) {
+        synchronized (mMutex) {
+            final UserServices services = mUserServices.get(userId);
+            if (services == null) return false;
+            return services.getStandardLayoutKey(pkg) == null;
+        }
+    }
+
+    @Override
+    public String getStandardLayoutKey(int userId, String pkg) {
+        synchronized (mMutex) {
+            final UserServices services = mUserServices.get(userId);
+            if (services == null) return null;
+            return services.getStandardLayoutKey(pkg);
+        }
+    }
+
+    @Override
+    public ArraySet<Integer> getAppOps(int userId, String pkg) {
+        synchronized (mMutex) {
+            final UserServices services = mUserServices.get(userId);
+            if (services == null) {
+                return null;
+            }
+            return services.getFeatures(pkg);
+        }
+    }
+
+    @Override
+    public void onAppOpChanged(int code, int uid, String packageName, boolean active) {
+        int userId = UserHandle.getUserId(uid);
+        synchronized (mMutex) {
+            UserServices userServices = mUserServices.get(userId);
+            if (userServices == null) {
+                userServices = new UserServices();
+                mUserServices.put(userId, userServices);
+            }
+            if (active) {
+                userServices.addOp(packageName, code);
+            } else {
+                userServices.removeOp(packageName, code);
+            }
+        }
+    }
+
+    @Override
     public void addNotification(StatusBarNotification sbn, int importance) {
         updateNotification(sbn, importance);
     }
@@ -102,9 +150,16 @@
                 }
             } 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());
+                if (0 != (sbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE)) {
+                    if (newImportance > NotificationManager.IMPORTANCE_MIN) {
+                        userServices.addImportantNotification(sbn.getPackageName(), sbn.getKey());
+                    }
+                    final Notification.Builder builder = Notification.Builder.recoverBuilder(
+                            mContext, sbn.getNotification());
+                    if (builder.usesStandardHeader()) {
+                        userServices.addStandardLayoutNotification(
+                                sbn.getPackageName(), sbn.getKey());
+                    }
                 }
             }
         }
@@ -117,42 +172,105 @@
                 && sbn.getPackageName().equals("android");
     }
 
+    @Override
+    public boolean isSystemAlertNotification(StatusBarNotification sbn) {
+        // TODO: tag system alert notifications so they can be suppressed if app's notification
+        // is tagged
+        return false;
+    }
+
     /**
      * Struct to track relevant packages and notifications for a userid's foreground services.
      */
     private static class UserServices {
         private String[] mRunning = null;
         private long mServiceStartTime = 0;
-        private ArrayMap<String, ArraySet<String>> mNotifications = new ArrayMap<>(1);
+        // package -> sufficiently important posted notification keys
+        private ArrayMap<String, ArraySet<String>> mImportantNotifications = new ArrayMap<>(1);
+        // package -> standard layout posted notification keys
+        private ArrayMap<String, ArraySet<String>> mStandardLayoutNotifications = new ArrayMap<>(1);
+
+        // package -> app ops
+        private ArrayMap<String, ArraySet<Integer>> mAppOps = new ArrayMap<>(1);
+
         public void setRunningServices(String[] pkgs, long serviceStartTime) {
             mRunning = pkgs != null ? Arrays.copyOf(pkgs, pkgs.length) : null;
             mServiceStartTime = serviceStartTime;
         }
-        public void addNotification(String pkg, String key) {
-            if (mNotifications.get(pkg) == null) {
-                mNotifications.put(pkg, new ArraySet<String>());
+
+        public void addOp(String pkg, int op) {
+            if (mAppOps.get(pkg) == null) {
+                mAppOps.put(pkg, new ArraySet<>(3));
             }
-            mNotifications.get(pkg).add(key);
+            mAppOps.get(pkg).add(op);
         }
-        public boolean removeNotification(String pkg, String key) {
+
+        public boolean removeOp(String pkg, int op) {
             final boolean found;
-            final ArraySet<String> keys = mNotifications.get(pkg);
+            final ArraySet<Integer> keys = mAppOps.get(pkg);
+            if (keys == null) {
+                found = false;
+            } else {
+                found = keys.remove(op);
+                if (keys.size() == 0) {
+                    mAppOps.remove(pkg);
+                }
+            }
+            return found;
+        }
+
+        public void addImportantNotification(String pkg, String key) {
+            addNotification(mImportantNotifications, pkg, key);
+        }
+
+        public boolean removeImportantNotification(String pkg, String key) {
+            return removeNotification(mImportantNotifications, pkg, key);
+        }
+
+        public void addStandardLayoutNotification(String pkg, String key) {
+            addNotification(mStandardLayoutNotifications, pkg, key);
+        }
+
+        public boolean removeStandardLayoutNotification(String pkg, String key) {
+            return removeNotification(mStandardLayoutNotifications, pkg, key);
+        }
+
+        public boolean removeNotification(String pkg, String key) {
+            boolean removed = false;
+            removed |= removeImportantNotification(pkg, key);
+            removed |= removeStandardLayoutNotification(pkg, key);
+            return removed;
+        }
+
+        public void addNotification(ArrayMap<String, ArraySet<String>> map, String pkg,
+                String key) {
+            if (map.get(pkg) == null) {
+                map.put(pkg, new ArraySet<>());
+            }
+            map.get(pkg).add(key);
+        }
+
+        public boolean removeNotification(ArrayMap<String, ArraySet<String>> map,
+                String pkg, String key) {
+            final boolean found;
+            final ArraySet<String> keys = map.get(pkg);
             if (keys == null) {
                 found = false;
             } else {
                 found = keys.remove(key);
                 if (keys.size() == 0) {
-                    mNotifications.remove(pkg);
+                    map.remove(pkg);
                 }
             }
             return found;
         }
+
         public boolean isDungeonNeeded() {
             if (mRunning != null
                 && System.currentTimeMillis() - mServiceStartTime >= FG_SERVICE_GRACE_MILLIS) {
 
                 for (String pkg : mRunning) {
-                    final ArraySet<String> set = mNotifications.get(pkg);
+                    final ArraySet<String> set = mImportantNotifications.get(pkg);
                     if (set == null || set.size() == 0) {
                         return true;
                     }
@@ -160,5 +278,27 @@
             }
             return false;
         }
+
+        public ArraySet<Integer> getFeatures(String pkg) {
+            return mAppOps.get(pkg);
+        }
+
+        public String getStandardLayoutKey(String pkg) {
+            final ArraySet<String> set = mStandardLayoutNotifications.get(pkg);
+            if (set == null || set.size() == 0) {
+                return null;
+            }
+            return set.valueAt(0);
+        }
+
+        @Override
+        public String toString() {
+            return "UserServices{" +
+                    "mRunning=" + Arrays.toString(mRunning) +
+                    ", mServiceStartTime=" + mServiceStartTime +
+                    ", mImportantNotifications=" + mImportantNotifications +
+                    ", mStandardLayoutNotifications=" + mStandardLayoutNotifications +
+                    '}';
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AppOpsListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/AppOpsListener.java
new file mode 100644
index 0000000..2ec78cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/AppOpsListener.java
@@ -0,0 +1,68 @@
+/*
+ * 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.statusbar;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.ForegroundServiceController;
+
+/**
+ * This class handles listening to notification updates and passing them along to
+ * NotificationPresenter to be displayed to the user.
+ */
+public class AppOpsListener implements AppOpsManager.OnOpActiveChangedListener {
+    private static final String TAG = "NotificationListener";
+
+    // Dependencies:
+    private final ForegroundServiceController mFsc =
+            Dependency.get(ForegroundServiceController.class);
+
+    private final Context mContext;
+    protected NotificationPresenter mPresenter;
+    protected NotificationEntryManager mEntryManager;
+    protected final AppOpsManager mAppOps;
+
+    protected static final int[] OPS = new int[] {AppOpsManager.OP_CAMERA,
+            AppOpsManager.OP_SYSTEM_ALERT_WINDOW,
+            AppOpsManager.OP_RECORD_AUDIO};
+
+    public AppOpsListener(Context context) {
+        mContext = context;
+        mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+    }
+
+    public void setUpWithPresenter(NotificationPresenter presenter,
+            NotificationEntryManager entryManager) {
+        mPresenter = presenter;
+        mEntryManager = entryManager;
+        mAppOps.startWatchingActive(OPS, this);
+    }
+
+    public void destroy() {
+        mAppOps.stopWatchingActive(this);
+    }
+
+    @Override
+    public void onOpActiveChanged(int code, int uid, String packageName, boolean active) {
+        mFsc.onAppOpChanged(code, uid, packageName, active);
+        mPresenter.getHandler().post(() -> {
+          mEntryManager.updateNotificationsForAppOps(code, uid, packageName, active);
+        });
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
index bc2dff9..785fc1c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -36,6 +36,7 @@
 import android.os.Bundle;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
+import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.MathUtils;
@@ -1354,6 +1355,14 @@
         mHelperButton.setVisibility(show ? View.VISIBLE : View.GONE);
     }
 
+    public void showAppOpsIcons(ArraySet<Integer> activeOps) {
+        if (mIsSummaryWithChildren && mChildrenContainer.getHeaderView() != null) {
+            mChildrenContainer.getHeaderView().showAppOpsIcons(activeOps);
+        }
+        mPrivateLayout.showAppOpsIcons(activeOps);
+        mPublicLayout.showAppOpsIcons(activeOps);
+    }
+
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
@@ -2629,6 +2638,16 @@
         mChildrenContainer = childrenContainer;
     }
 
+    @VisibleForTesting
+    protected void setPrivateLayout(NotificationContentView privateLayout) {
+        mPrivateLayout = privateLayout;
+    }
+
+    @VisibleForTesting
+    protected void setPublicLayout(NotificationContentView publicLayout) {
+        mPublicLayout = publicLayout;
+    }
+
     /**
      * Equivalent to View.OnLongClickListener with coordinates
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
index 91960df..73c8795 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
@@ -23,6 +23,7 @@
 import android.graphics.Rect;
 import android.os.Build;
 import android.service.notification.StatusBarNotification;
+import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.NotificationHeaderView;
@@ -1423,6 +1424,17 @@
         return header;
     }
 
+    public void showAppOpsIcons(ArraySet<Integer> activeOps) {
+        if (mContractedChild != null && mContractedWrapper.getNotificationHeader() != null) {
+            mContractedWrapper.getNotificationHeader().showAppOpsIcons(activeOps);
+        }
+        if (mExpandedChild != null && mExpandedWrapper.getNotificationHeader() != null) {
+            mExpandedWrapper.getNotificationHeader().showAppOpsIcons(activeOps);
+        }
+        if (mHeadsUpChild != null && mHeadsUpWrapper.getNotificationHeader() != null) {
+            mHeadsUpWrapper.getNotificationHeader().showAppOpsIcons(activeOps);
+        }
+    }
 
     public NotificationHeaderView getContractedNotificationHeader() {
         if (mContractedChild != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
index 127f3f9..d53cb03 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar;
 
 import android.app.AppGlobals;
+import android.app.AppOpsManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
@@ -34,6 +35,7 @@
 import android.service.notification.SnoozeCriterion;
 import android.service.notification.StatusBarNotification;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.RemoteViews;
@@ -65,6 +67,8 @@
     private final Environment mEnvironment;
     private HeadsUpManager mHeadsUpManager;
 
+    final ForegroundServiceController mFsc = Dependency.get(ForegroundServiceController.class);
+
     public static final class Entry {
         private static final long LAUNCH_COOLDOWN = 2000;
         private static final long REMOTE_INPUT_COOLDOWN = 500;
@@ -95,6 +99,7 @@
         private Throwable mDebugThrowable;
         public CharSequence remoteInputTextWhenReset;
         public long lastRemoteInputSent = NOT_LAUNCHED_YET;
+        public ArraySet<Integer> mActiveAppOps = new ArraySet<>(3);
 
         public Entry(StatusBarNotification n) {
             this.key = n.getKey();
@@ -194,7 +199,7 @@
         /**
          * Update the notification icons.
          * @param context the context to create the icons with.
-         * @param n the notification to read the icon from.
+         * @param sbn the notification to read the icon from.
          * @throws InflationException
          */
         public void updateIcons(Context context, StatusBarNotification sbn)
@@ -375,6 +380,8 @@
         }
         mGroupManager.onEntryAdded(entry);
 
+        updateAppOps(entry);
+
         updateRankingAndSort(mRankingMap);
     }
 
@@ -393,6 +400,35 @@
         updateRankingAndSort(ranking);
     }
 
+    private void updateAppOps(Entry entry) {
+        final int uid = entry.notification.getUid();
+        final String pkg = entry.notification.getPackageName();
+        ArraySet<Integer> activeOps = mFsc.getAppOps(entry.notification.getUserId(), pkg);
+        if (activeOps != null) {
+            int N = activeOps.size();
+            for (int i = 0; i < N; i++) {
+                updateAppOp(activeOps.valueAt(i), uid, pkg, true);
+            }
+        }
+    }
+
+    public void updateAppOp(int appOp, int uid, String pkg, boolean showIcon) {
+        synchronized (mEntries) {
+            final int N = mEntries.size();
+            for (int i = 0; i < N; i++) {
+                Entry entry = mEntries.valueAt(i);
+                if (uid == entry.notification.getUid()
+                    && pkg.equals(entry.notification.getPackageName())) {
+                    if (showIcon) {
+                        entry.mActiveAppOps.add(appOp);
+                    } else {
+                        entry.mActiveAppOps.remove(appOp);
+                    }
+                }
+            }
+        }
+    }
+
     public boolean isAmbient(String key) {
         if (mRankingMap != null) {
             getRanking(key, mTmpRanking);
@@ -545,11 +581,14 @@
             return true;
         }
 
-        final ForegroundServiceController fsc = Dependency.get(ForegroundServiceController.class);
-        if (fsc.isDungeonNotification(sbn) && !fsc.isDungeonNeededForUser(sbn.getUserId())) {
+        if (mFsc.isDungeonNotification(sbn) && !mFsc.isDungeonNeededForUser(sbn.getUserId())) {
             // this is a foreground-service disclosure for a user that does not need to show one
             return true;
         }
+        if (mFsc.isSystemAlertNotification(sbn) && !mFsc.isSystemAlertWarningNeeded(
+                sbn.getUserId(), sbn.getPackageName())) {
+            return true;
+        }
 
         return false;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java
index 7360486..71f7911 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java
@@ -31,6 +31,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.provider.Settings;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationStats;
@@ -77,7 +78,7 @@
 public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback,
         ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler,
         VisualStabilityManager.Callback {
-    private static final String TAG = "NotificationEntryManager";
+    private static final String TAG = "NotificationEntryMgr";
     protected static final boolean DEBUG = false;
     protected static final boolean ENABLE_HEADS_UP = true;
     protected static final String SETTING_HEADS_UP_TICKER = "ticker_gets_heads_up";
@@ -734,6 +735,14 @@
         }
     }
 
+    public void updateNotificationsForAppOps(int appOp, int uid, String pkg, boolean showIcon) {
+        if (mForegroundServiceController.getStandardLayoutKey(
+                UserHandle.getUserId(uid), pkg) != null) {
+            mNotificationData.updateAppOp(appOp, uid, pkg, showIcon);
+            updateNotifications();
+        }
+    }
+
     private boolean alertAgain(NotificationData.Entry oldEntry, Notification newNotification) {
         return oldEntry == null || !oldEntry.hasInterrupted()
                 || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
index cd4c7ae..75b8b37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
@@ -342,6 +342,8 @@
 
             row.showBlockingHelper(entry.userSentiment ==
                     NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE);
+
+            row.showAppOpsIcons(entry.mActiveAppOps);
         }
 
         mPresenter.onUpdateRowStates();
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 a31727e..2f05726 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -180,6 +180,7 @@
 import com.android.systemui.stackdivider.Divider;
 import com.android.systemui.stackdivider.WindowManagerProxy;
 import com.android.systemui.statusbar.ActivatableNotificationView;
+import com.android.systemui.statusbar.AppOpsListener;
 import com.android.systemui.statusbar.BackDropView;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.CrossFadeHelper;
@@ -405,6 +406,7 @@
     protected NotificationLogger mNotificationLogger;
     protected NotificationEntryManager mEntryManager;
     protected NotificationViewHierarchyManager mViewHierarchyManager;
+    protected AppOpsListener mAppOpsListener;
 
     /**
      * Helper that is responsible for showing the right toast when a disallowed activity operation
@@ -622,6 +624,8 @@
         mMediaManager = Dependency.get(NotificationMediaManager.class);
         mEntryManager = Dependency.get(NotificationEntryManager.class);
         mViewHierarchyManager = Dependency.get(NotificationViewHierarchyManager.class);
+        mAppOpsListener = Dependency.get(AppOpsListener.class);
+        mAppOpsListener.setUpWithPresenter(this, mEntryManager);
 
         mColorExtractor = Dependency.get(SysuiColorExtractor.class);
         mColorExtractor.addOnColorsChangedListener(this);
@@ -3293,6 +3297,7 @@
         Dependency.get(ActivityStarterDelegate.class).setActivityStarterImpl(null);
         mDeviceProvisionedController.removeCallback(mUserSetupObserver);
         Dependency.get(ConfigurationController.class).removeCallback(this);
+        mAppOpsListener.destroy();
     }
 
     private boolean mDemoModeAllowed;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
index 943020c..18dd3c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
@@ -16,6 +16,14 @@
 
 package com.android.systemui;
 
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNull;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
 import android.annotation.UserIdInt;
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -24,17 +32,14 @@
 import android.service.notification.StatusBarNotification;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
+import android.widget.RemoteViews;
+
 import com.android.internal.messages.nano.SystemMessageProto;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class ForegroundServiceControllerTest extends SysuiTestCase {
@@ -49,7 +54,7 @@
     }
 
     @Test
-    public void testNotificationCRUD() {
+    public void testNotificationCRUD_dungeon() {
         StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, "com.example.app1");
         StatusBarNotification sbn_user2_app2_fg = makeMockFgSBN(USERID_TWO, "com.example.app2");
         StatusBarNotification sbn_user1_app3_fg = makeMockFgSBN(USERID_ONE, "com.example.app3");
@@ -98,6 +103,101 @@
     }
 
     @Test
+    public void testNotificationCRUD_stdLayout() {
+        StatusBarNotification sbn_user1_app1_fg =
+                makeMockFgSBN(USERID_ONE, "com.example.app1", 0, true);
+        StatusBarNotification sbn_user2_app2_fg =
+                makeMockFgSBN(USERID_TWO, "com.example.app2", 1, true);
+        StatusBarNotification sbn_user1_app3_fg =
+                makeMockFgSBN(USERID_ONE, "com.example.app3", 2, true);
+        StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, "com.example.app1",
+                5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
+        StatusBarNotification sbn_user2_app1 = makeMockSBN(USERID_TWO, "com.example.app1",
+                5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
+
+        assertFalse(fsc.removeNotification(sbn_user1_app3_fg));
+        assertFalse(fsc.removeNotification(sbn_user2_app2_fg));
+        assertFalse(fsc.removeNotification(sbn_user1_app1_fg));
+        assertFalse(fsc.removeNotification(sbn_user1_app1));
+        assertFalse(fsc.removeNotification(sbn_user2_app1));
+
+        fsc.addNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN);
+        fsc.addNotification(sbn_user2_app2_fg, NotificationManager.IMPORTANCE_MIN);
+        fsc.addNotification(sbn_user1_app3_fg, NotificationManager.IMPORTANCE_MIN);
+        fsc.addNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_MIN);
+        fsc.addNotification(sbn_user2_app1, NotificationManager.IMPORTANCE_MIN);
+
+        // these are never added to the tracker
+        assertFalse(fsc.removeNotification(sbn_user1_app1));
+        assertFalse(fsc.removeNotification(sbn_user2_app1));
+
+        fsc.updateNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_MIN);
+        fsc.updateNotification(sbn_user2_app1, NotificationManager.IMPORTANCE_MIN);
+        // should still not be there
+        assertFalse(fsc.removeNotification(sbn_user1_app1));
+        assertFalse(fsc.removeNotification(sbn_user2_app1));
+
+        fsc.updateNotification(sbn_user2_app2_fg, NotificationManager.IMPORTANCE_MIN);
+        fsc.updateNotification(sbn_user1_app3_fg, NotificationManager.IMPORTANCE_MIN);
+        fsc.updateNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN);
+
+        assertTrue(fsc.removeNotification(sbn_user1_app3_fg));
+        assertFalse(fsc.removeNotification(sbn_user1_app3_fg));
+
+        assertTrue(fsc.removeNotification(sbn_user2_app2_fg));
+        assertFalse(fsc.removeNotification(sbn_user2_app2_fg));
+
+        assertTrue(fsc.removeNotification(sbn_user1_app1_fg));
+        assertFalse(fsc.removeNotification(sbn_user1_app1_fg));
+
+        assertFalse(fsc.removeNotification(sbn_user1_app1));
+        assertFalse(fsc.removeNotification(sbn_user2_app1));
+    }
+
+    @Test
+    public void testAppOpsCRUD() {
+        // no crash on remove that doesn't exist
+        fsc.onAppOpChanged(9, 1000, "pkg1", false);
+        assertNull(fsc.getAppOps(0, "pkg1"));
+
+        // multiuser & multipackage
+        fsc.onAppOpChanged(8, 50, "pkg1", true);
+        fsc.onAppOpChanged(1, 60, "pkg3", true);
+        fsc.onAppOpChanged(7, 500000, "pkg2", true);
+
+        assertEquals(1, fsc.getAppOps(0, "pkg1").size());
+        assertTrue(fsc.getAppOps(0, "pkg1").contains(8));
+
+        assertEquals(1, fsc.getAppOps(UserHandle.getUserId(500000), "pkg2").size());
+        assertTrue(fsc.getAppOps(UserHandle.getUserId(500000), "pkg2").contains(7));
+
+        assertEquals(1, fsc.getAppOps(0, "pkg3").size());
+        assertTrue(fsc.getAppOps(0, "pkg3").contains(1));
+
+        // multiple ops for the same package
+        fsc.onAppOpChanged(9, 50, "pkg1", true);
+        fsc.onAppOpChanged(5, 50, "pkg1", true);
+
+        assertEquals(3, fsc.getAppOps(0, "pkg1").size());
+        assertTrue(fsc.getAppOps(0, "pkg1").contains(8));
+        assertTrue(fsc.getAppOps(0, "pkg1").contains(9));
+        assertTrue(fsc.getAppOps(0, "pkg1").contains(5));
+
+        assertEquals(1, fsc.getAppOps(UserHandle.getUserId(500000), "pkg2").size());
+        assertTrue(fsc.getAppOps(UserHandle.getUserId(500000), "pkg2").contains(7));
+
+        // remove one of the multiples
+        fsc.onAppOpChanged(9, 50, "pkg1", false);
+        assertEquals(2, fsc.getAppOps(0, "pkg1").size());
+        assertTrue(fsc.getAppOps(0, "pkg1").contains(8));
+        assertTrue(fsc.getAppOps(0, "pkg1").contains(5));
+
+        // remove last op
+        fsc.onAppOpChanged(1, 60, "pkg3", false);
+        assertNull(fsc.getAppOps(0, "pkg3"));
+    }
+
+    @Test
     public void testDungeonPredicate() {
         StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, "com.example.app1",
                 5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
@@ -252,6 +352,14 @@
         assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
         assertTrue(fsc.isDungeonNeededForUser(USERID_ONE));
 
+        // importance upgrade
+        fsc.addNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN);
+        assertTrue(fsc.isDungeonNeededForUser(USERID_ONE));
+        assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+        sbn_user1_app1.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE;
+        fsc.updateNotification(sbn_user1_app1_fg,
+                NotificationManager.IMPORTANCE_DEFAULT); // this is now a fg notification
+
         // finally, let's turn off the service
         fsc.addNotification(makeMockDungeon(USERID_ONE, null),
                 NotificationManager.IMPORTANCE_DEFAULT);
@@ -260,12 +368,71 @@
         assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
     }
 
+    @Test
+    public void testStdLayoutBasic() {
+        final String PKG1 = "com.example.app0";
+
+        StatusBarNotification sbn_user1_app1 = makeMockFgSBN(USERID_ONE, PKG1, 0, true);
+        sbn_user1_app1.getNotification().flags = 0;
+        StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, PKG1, 1, true);
+        fsc.addNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_MIN); // not fg
+        assertTrue(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); // should be required!
+        fsc.addNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN);
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); // app1 has got it covered
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "otherpkg"));
+        // let's take out the non-fg notification and see what happens.
+        fsc.removeNotification(sbn_user1_app1);
+        // still covered by sbn_user1_app1_fg
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1));
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "anyPkg"));
+
+        // let's attempt to downgrade the notification from FLAG_FOREGROUND and see what we get
+        StatusBarNotification sbn_user1_app1_fg_sneaky = makeMockFgSBN(USERID_ONE, PKG1, 1, true);
+        sbn_user1_app1_fg_sneaky.getNotification().flags = 0;
+        fsc.updateNotification(sbn_user1_app1_fg_sneaky, NotificationManager.IMPORTANCE_MIN);
+        assertTrue(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); // should be required!
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "anything"));
+        // ok, ok, we'll put it back
+        sbn_user1_app1_fg_sneaky.getNotification().flags = Notification.FLAG_FOREGROUND_SERVICE;
+        fsc.updateNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_MIN);
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1));
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "whatever"));
+
+        assertTrue(fsc.removeNotification(sbn_user1_app1_fg_sneaky));
+        assertTrue(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); // should be required!
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "a"));
+
+        // let's try a custom layout
+        sbn_user1_app1_fg_sneaky = makeMockFgSBN(USERID_ONE, PKG1, 1, false);
+        fsc.updateNotification(sbn_user1_app1_fg_sneaky, NotificationManager.IMPORTANCE_MIN);
+        assertTrue(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1)); // should be required!
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "anything"));
+        // now let's test an upgrade (non fg to fg)
+        fsc.addNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_MIN);
+        assertTrue(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1));
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, "b"));
+        sbn_user1_app1.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE;
+        fsc.updateNotification(sbn_user1_app1,
+                NotificationManager.IMPORTANCE_MIN); // this is now a fg notification
+
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, PKG1));
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1));
+
+        // remove it, make sure we're out of compliance again
+        assertTrue(fsc.removeNotification(sbn_user1_app1)); // was fg, should return true
+        assertFalse(fsc.removeNotification(sbn_user1_app1));
+        assertFalse(fsc.isSystemAlertWarningNeeded(USERID_TWO, PKG1));
+        assertTrue(fsc.isSystemAlertWarningNeeded(USERID_ONE, PKG1));
+    }
+
     private StatusBarNotification makeMockSBN(int userid, String pkg, int id, String tag,
             int flags) {
         final Notification n = mock(Notification.class);
+        n.extras = new Bundle();
         n.flags = flags;
         return makeMockSBN(userid, pkg, id, tag, n);
     }
+
     private StatusBarNotification makeMockSBN(int userid, String pkg, int id, String tag,
             Notification n) {
         final StatusBarNotification sbn = mock(StatusBarNotification.class);
@@ -278,9 +445,25 @@
         when(sbn.getKey()).thenReturn("MOCK:"+userid+"|"+pkg+"|"+id+"|"+tag);
         return sbn;
     }
+
+    private StatusBarNotification makeMockFgSBN(int userid, String pkg, int id,
+            boolean usesStdLayout) {
+        StatusBarNotification sbn =
+                makeMockSBN(userid, pkg, id, "foo", Notification.FLAG_FOREGROUND_SERVICE);
+        if (usesStdLayout) {
+            sbn.getNotification().contentView = null;
+            sbn.getNotification().headsUpContentView = null;
+            sbn.getNotification().bigContentView = null;
+        } else {
+            sbn.getNotification().contentView = mock(RemoteViews.class);
+        }
+        return sbn;
+    }
+
     private StatusBarNotification makeMockFgSBN(int userid, String pkg) {
         return makeMockSBN(userid, pkg, 1000, "foo", Notification.FLAG_FOREGROUND_SERVICE);
     }
+
     private StatusBarNotification makeMockDungeon(int userid, String[] pkgs) {
         final Notification n = mock(Notification.class);
         n.flags = Notification.FLAG_ONGOING_EVENT;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AppOpsListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AppOpsListenerTest.java
new file mode 100644
index 0000000..2a48c4b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AppOpsListenerTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 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.statusbar;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.systemui.ForegroundServiceController;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class AppOpsListenerTest extends SysuiTestCase {
+    private static final String TEST_PACKAGE_NAME = "test";
+    private static final int TEST_UID = 0;
+
+    @Mock private NotificationPresenter mPresenter;
+    @Mock private AppOpsManager mAppOpsManager;
+
+    // Dependency mocks:
+    @Mock private NotificationEntryManager mEntryManager;
+    @Mock private ForegroundServiceController mFsc;
+
+    private AppOpsListener mListener;
+    private Handler mHandler;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager);
+        mDependency.injectTestDependency(ForegroundServiceController.class, mFsc);
+        getContext().addMockSystemService(AppOpsManager.class, mAppOpsManager);
+        mHandler = new Handler(Looper.getMainLooper());
+        when(mPresenter.getHandler()).thenReturn(mHandler);
+
+        mListener = new AppOpsListener(mContext);
+    }
+
+    @Test
+    public void testOnlyListenForFewOps() {
+        mListener.setUpWithPresenter(mPresenter, mEntryManager);
+
+        verify(mAppOpsManager, times(1)).startWatchingActive(AppOpsListener.OPS, mListener);
+    }
+
+    @Test
+    public void testStopListening() {
+        mListener.destroy();
+        verify(mAppOpsManager, times(1)).stopWatchingActive(mListener);
+    }
+
+    @Test
+    public void testInformEntryMgrOnAppOpsChange() {
+        mListener.setUpWithPresenter(mPresenter, mEntryManager);
+        mListener.onOpActiveChanged(
+                AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
+        waitForIdleSync(mHandler);
+        verify(mEntryManager, times(1)).updateNotificationsForAppOps(
+                AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
+    }
+
+    @Test
+    public void testInformFscOnAppOpsChange() {
+        mListener.setUpWithPresenter(mPresenter, mEntryManager);
+        mListener.onOpActiveChanged(
+                AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
+        waitForIdleSync(mHandler);
+        verify(mFsc, times(1)).onAppOpChanged(
+                AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java
index 544585a..ce629bb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ExpandableNotificationRowTest.java
@@ -19,10 +19,15 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.app.AppOpsManager;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
+import android.util.ArraySet;
+import android.view.NotificationHeaderView;
 import android.view.View;
 
 import com.android.systemui.SysuiTestCase;
@@ -146,4 +151,34 @@
         Assert.assertTrue("Should always play sounds when not trusted.",
                 mGroup.isSoundEffectsEnabled());
     }
+
+    @Test
+    public void testShowAppOpsIcons_noHeader() {
+        // public notification is custom layout - no header
+        mGroup.setSensitive(true, true);
+        mGroup.showAppOpsIcons(new ArraySet<>());
+    }
+
+    @Test
+    public void testShowAppOpsIcons_header() throws Exception {
+        NotificationHeaderView mockHeader = mock(NotificationHeaderView.class);
+
+        NotificationContentView publicLayout = mock(NotificationContentView.class);
+        mGroup.setPublicLayout(publicLayout);
+        NotificationContentView privateLayout = mock(NotificationContentView.class);
+        mGroup.setPrivateLayout(privateLayout);
+        NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class);
+        when(mockContainer.getNotificationChildCount()).thenReturn(1);
+        when(mockContainer.getHeaderView()).thenReturn(mockHeader);
+        mGroup.setChildrenContainer(mockContainer);
+
+        ArraySet<Integer> ops = new ArraySet<>();
+        ops.add(AppOpsManager.OP_ANSWER_PHONE_CALLS);
+        mGroup.showAppOpsIcons(ops);
+
+        verify(mockHeader, times(1)).showAppOpsIcons(ops);
+        verify(privateLayout, times(1)).showAppOpsIcons(ops);
+        verify(publicLayout, times(1)).showAppOpsIcons(ops);
+
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationContentViewTest.java
index 436849c..1fb4c37 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationContentViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationContentViewTest.java
@@ -16,14 +16,23 @@
 
 package com.android.systemui.statusbar;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.app.AppOpsManager;
 import android.support.test.annotation.UiThreadTest;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
+import android.util.ArraySet;
+import android.view.NotificationHeaderView;
 import android.view.View;
 
 import com.android.systemui.SysuiTestCase;
@@ -75,4 +84,35 @@
         mView.setHeadsUpAnimatingAway(true);
         Assert.assertFalse(mView.isAnimatingVisibleType());
     }
+
+    @Test
+    @UiThreadTest
+    public void testShowAppOpsIcons() {
+        NotificationHeaderView mockContracted = mock(NotificationHeaderView.class);
+        when(mockContracted.findViewById(com.android.internal.R.id.notification_header))
+                .thenReturn(mockContracted);
+        NotificationHeaderView mockExpanded = mock(NotificationHeaderView.class);
+        when(mockExpanded.findViewById(com.android.internal.R.id.notification_header))
+                .thenReturn(mockExpanded);
+        NotificationHeaderView mockHeadsUp = mock(NotificationHeaderView.class);
+        when(mockHeadsUp.findViewById(com.android.internal.R.id.notification_header))
+                .thenReturn(mockHeadsUp);
+        NotificationHeaderView mockAmbient = mock(NotificationHeaderView.class);
+        when(mockAmbient.findViewById(com.android.internal.R.id.notification_header))
+                .thenReturn(mockAmbient);
+
+        mView.setContractedChild(mockContracted);
+        mView.setExpandedChild(mockExpanded);
+        mView.setHeadsUpChild(mockHeadsUp);
+        mView.setAmbientChild(mockAmbient);
+
+        ArraySet<Integer> ops = new ArraySet<>();
+        ops.add(AppOpsManager.OP_ANSWER_PHONE_CALLS);
+        mView.showAppOpsIcons(ops);
+
+        verify(mockContracted, times(1)).showAppOpsIcons(ops);
+        verify(mockExpanded, times(1)).showAppOpsIcons(ops);
+        verify(mockAmbient, never()).showAppOpsIcons(ops);
+        verify(mockHeadsUp, times(1)).showAppOpsIcons(any());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java
index 972eddb..b1e1c02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java
@@ -16,8 +16,16 @@
 
 package com.android.systemui.statusbar;
 
+import static android.app.AppOpsManager.OP_ACCEPT_HANDOVER;
+import static android.app.AppOpsManager.OP_CAMERA;
+
+import static junit.framework.Assert.assertEquals;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -33,7 +41,9 @@
 import android.support.test.annotation.UiThreadTest;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
+import android.util.ArraySet;
 
+import com.android.systemui.ForegroundServiceController;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 
@@ -41,6 +51,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -51,6 +63,10 @@
 
     private final StatusBarNotification mMockStatusBarNotification =
             mock(StatusBarNotification.class);
+    @Mock
+    ForegroundServiceController mFsc;
+    @Mock
+    NotificationData.Environment mEnvironment;
 
     private final IPackageManager mMockPackageManager = mock(IPackageManager.class);
     private NotificationData mNotificationData;
@@ -58,6 +74,7 @@
 
     @Before
     public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
         when(mMockStatusBarNotification.getUid()).thenReturn(UID_NORMAL);
 
         when(mMockPackageManager.checkUidPermission(
@@ -69,9 +86,11 @@
                 eq(UID_ALLOW_DURING_SETUP)))
                 .thenReturn(PackageManager.PERMISSION_GRANTED);
 
-        NotificationData.Environment mock = mock(NotificationData.Environment.class);
-        when(mock.getGroupManager()).thenReturn(new NotificationGroupManager());
-        mNotificationData = new TestableNotificationData(mock);
+        mDependency.injectTestDependency(ForegroundServiceController.class, mFsc);
+        when(mEnvironment.getGroupManager()).thenReturn(new NotificationGroupManager());
+        when(mEnvironment.isDeviceProvisioned()).thenReturn(true);
+        when(mEnvironment.isNotificationForCurrentProfiles(any())).thenReturn(true);
+        mNotificationData = new TestableNotificationData(mEnvironment);
         mNotificationData.updateRanking(mock(NotificationListenerService.RankingMap.class));
         mRow = new NotificationTestHelper(getContext()).createRow();
     }
@@ -117,6 +136,117 @@
         Assert.assertTrue(mRow.getEntry().channel != null);
     }
 
+    @Test
+    public void testAdd_appOpsAdded() {
+        ArraySet<Integer> expected = new ArraySet<>();
+        expected.add(3);
+        expected.add(235);
+        expected.add(1);
+        when(mFsc.getAppOps(mRow.getEntry().notification.getUserId(),
+                mRow.getEntry().notification.getPackageName())).thenReturn(expected);
+
+        mNotificationData.add(mRow.getEntry());
+        assertEquals(expected.size(),
+                mNotificationData.get(mRow.getEntry().key).mActiveAppOps.size());
+        for (int op : expected) {
+            assertTrue(" entry missing op " + op,
+                    mNotificationData.get(mRow.getEntry().key).mActiveAppOps.contains(op));
+        }
+    }
+
+    @Test
+    public void testAdd_noExistingAppOps() {
+        when(mFsc.getAppOps(mRow.getEntry().notification.getUserId(),
+                mRow.getEntry().notification.getPackageName())).thenReturn(null);
+
+        mNotificationData.add(mRow.getEntry());
+        assertEquals(0, mNotificationData.get(mRow.getEntry().key).mActiveAppOps.size());
+    }
+
+    @Test
+    public void testAllRelevantNotisTaggedWithAppOps() throws Exception {
+        mNotificationData.add(mRow.getEntry());
+        ExpandableNotificationRow row2 = new NotificationTestHelper(getContext()).createRow();
+        mNotificationData.add(row2.getEntry());
+        ExpandableNotificationRow diffPkg =
+                new NotificationTestHelper(getContext()).createRow("pkg", 4000);
+        mNotificationData.add(diffPkg.getEntry());
+
+        ArraySet<Integer> expectedOps = new ArraySet<>();
+        expectedOps.add(OP_CAMERA);
+        expectedOps.add(OP_ACCEPT_HANDOVER);
+
+        for (int op : expectedOps) {
+            mNotificationData.updateAppOp(op, NotificationTestHelper.UID,
+                    NotificationTestHelper.PKG, true);
+        }
+        for (int op : expectedOps) {
+            assertTrue(mRow.getEntry().key + " doesn't have op " + op,
+                    mNotificationData.get(mRow.getEntry().key).mActiveAppOps.contains(op));
+            assertTrue(row2.getEntry().key + " doesn't have op " + op,
+                    mNotificationData.get(row2.getEntry().key).mActiveAppOps.contains(op));
+            assertFalse(diffPkg.getEntry().key + " has op " + op,
+                    mNotificationData.get(diffPkg.getEntry().key).mActiveAppOps.contains(op));
+        }
+    }
+
+    @Test
+    public void testAppOpsRemoval() throws Exception {
+        mNotificationData.add(mRow.getEntry());
+        ExpandableNotificationRow row2 = new NotificationTestHelper(getContext()).createRow();
+        mNotificationData.add(row2.getEntry());
+
+        ArraySet<Integer> expectedOps = new ArraySet<>();
+        expectedOps.add(OP_CAMERA);
+        expectedOps.add(OP_ACCEPT_HANDOVER);
+
+        for (int op : expectedOps) {
+            mNotificationData.updateAppOp(op, NotificationTestHelper.UID,
+                    NotificationTestHelper.PKG, true);
+        }
+
+        expectedOps.remove(OP_ACCEPT_HANDOVER);
+        mNotificationData.updateAppOp(OP_ACCEPT_HANDOVER, NotificationTestHelper.UID,
+                NotificationTestHelper.PKG, false);
+
+        assertTrue(mRow.getEntry().key + " doesn't have op " + OP_CAMERA,
+                mNotificationData.get(mRow.getEntry().key).mActiveAppOps.contains(OP_CAMERA));
+        assertTrue(row2.getEntry().key + " doesn't have op " + OP_CAMERA,
+                mNotificationData.get(row2.getEntry().key).mActiveAppOps.contains(OP_CAMERA));
+        assertFalse(mRow.getEntry().key + " has op " + OP_ACCEPT_HANDOVER,
+                mNotificationData.get(mRow.getEntry().key)
+                        .mActiveAppOps.contains(OP_ACCEPT_HANDOVER));
+        assertFalse(row2.getEntry().key + " has op " + OP_ACCEPT_HANDOVER,
+                mNotificationData.get(row2.getEntry().key)
+                        .mActiveAppOps.contains(OP_ACCEPT_HANDOVER));
+    }
+
+    @Test
+    public void testSuppressSystemAlertNotification() {
+        when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(false);
+        when(mFsc.isSystemAlertNotification(any())).thenReturn(true);
+
+        assertTrue(mNotificationData.shouldFilterOut(mRow.getEntry().notification));
+    }
+
+    @Test
+    public void testDoNotSuppressSystemAlertNotification() {
+        when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(true);
+        when(mFsc.isSystemAlertNotification(any())).thenReturn(true);
+
+        assertFalse(mNotificationData.shouldFilterOut(mRow.getEntry().notification));
+
+        when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(true);
+        when(mFsc.isSystemAlertNotification(any())).thenReturn(false);
+
+        assertFalse(mNotificationData.shouldFilterOut(mRow.getEntry().notification));
+
+        when(mFsc.isSystemAlertWarningNeeded(anyInt(), anyString())).thenReturn(false);
+        when(mFsc.isSystemAlertNotification(any())).thenReturn(false);
+
+        assertFalse(mNotificationData.shouldFilterOut(mRow.getEntry().notification));
+    }
+
     private void initStatusBarNotification(boolean allowDuringSetup) {
         Bundle bundle = new Bundle();
         bundle.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, allowDuringSetup);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java
index f9ec3f92..37dd939 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java
@@ -23,14 +23,17 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
+import android.app.AppOpsManager;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.content.Context;
@@ -274,4 +277,40 @@
 
         assertNull(mEntryManager.getNotificationData().get(mSbn.getKey()));
     }
+
+    @Test
+    public void testUpdateAppOps_foregroundNoti() {
+        com.android.systemui.util.Assert.isNotMainThread();
+
+        when(mForegroundServiceController.getStandardLayoutKey(anyInt(), anyString()))
+                .thenReturn("something");
+        mEntry.row = mRow;
+        mEntryManager.getNotificationData().add(mEntry);
+
+
+        mHandler.post(() -> {
+            mEntryManager.updateNotificationsForAppOps(
+                    AppOpsManager.OP_CAMERA, mEntry.notification.getUid(),
+                    mEntry.notification.getPackageName(), true);
+        });
+        waitForIdleSync(mHandler);
+
+        verify(mPresenter, times(1)).updateNotificationViews();
+        assertTrue(mEntryManager.getNotificationData().get(mEntry.key).mActiveAppOps.contains(
+                AppOpsManager.OP_CAMERA));
+    }
+
+    @Test
+    public void testUpdateAppOps_otherNoti() {
+        com.android.systemui.util.Assert.isNotMainThread();
+
+        when(mForegroundServiceController.getStandardLayoutKey(anyInt(), anyString()))
+                .thenReturn(null);
+        mHandler.post(() -> {
+            mEntryManager.updateNotificationsForAppOps(AppOpsManager.OP_CAMERA, 1000, "pkg", true);
+        });
+        waitForIdleSync(mHandler);
+
+        verify(mPresenter, never()).updateNotificationViews();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java
index f3c1171..2764254 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationTestHelper.java
@@ -48,6 +48,8 @@
     private ExpandableNotificationRow mRow;
     private InflationException mException;
     private HeadsUpManager mHeadsUpManager;
+    protected static final String PKG = "com.android.systemui";
+    protected static final int UID = 1000;
 
     public NotificationTestHelper(Context context) {
         mContext = context;
@@ -55,7 +57,7 @@
         mHeadsUpManager = new HeadsUpManagerPhone(mContext, null, mGroupManager, null, null);
     }
 
-    public ExpandableNotificationRow createRow() throws Exception {
+    public ExpandableNotificationRow createRow(String pkg, int uid) throws Exception {
         Notification publicVersion = new Notification.Builder(mContext).setSmallIcon(
                 R.drawable.ic_person)
                 .setCustomContentView(new RemoteViews(mContext.getPackageName(),
@@ -67,10 +69,19 @@
                 .setContentText("Text")
                 .setPublicVersion(publicVersion)
                 .build();
-        return createRow(notification);
+        return createRow(notification, pkg, uid);
+    }
+
+    public ExpandableNotificationRow createRow() throws Exception {
+        return createRow(PKG, UID);
     }
 
     public ExpandableNotificationRow createRow(Notification notification) throws Exception {
+        return createRow(notification, PKG, UID);
+    }
+
+    public ExpandableNotificationRow createRow(Notification notification, String pkg, int uid)
+            throws Exception {
         LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
                 mContext.LAYOUT_INFLATER_SERVICE);
         mInstrumentation.runOnMainSync(() -> {
@@ -83,8 +94,7 @@
         row.setHeadsUpManager(mHeadsUpManager);
         row.setAboveShelfChangedListener(aboveShelf -> {});
         UserHandle mUser = UserHandle.of(ActivityManager.getCurrentUser());
-        StatusBarNotification sbn = new StatusBarNotification("com.android.systemui",
-                "com.android.systemui", mId++, null, 1000,
+        StatusBarNotification sbn = new StatusBarNotification(pkg, pkg, mId++, null, uid,
                 2000, notification, mUser, null, System.currentTimeMillis());
         NotificationData.Entry entry = new NotificationData.Entry(sbn);
         entry.row = row;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
index fbe730a..76ed452 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
@@ -19,6 +19,9 @@
 import static junit.framework.Assert.assertTrue;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -170,6 +173,19 @@
         assertEquals(View.VISIBLE, entry1.row.getVisibility());
     }
 
+    @Test
+    public void testUpdateNotificationViews_appOps() throws Exception {
+        NotificationData.Entry entry0 = createEntry();
+        entry0.row = spy(entry0.row);
+        when(mNotificationData.getActiveNotifications()).thenReturn(
+                Lists.newArrayList(entry0));
+        mListContainer.addContainerView(entry0.row);
+
+        mViewHierarchyManager.updateNotificationViews();
+
+        verify(entry0.row, times(1)).showAppOpsIcons(any());
+    }
+
     private class FakeListContainer implements NotificationListContainer {
         final LinearLayout mLayout = new LinearLayout(mContext);
         final List<View> mRows = Lists.newArrayList();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index 31442af..ff545f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -69,6 +69,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.statusbar.ActivatableNotificationView;
+import com.android.systemui.statusbar.AppOpsListener;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.NotificationData;
@@ -145,6 +146,7 @@
         mDependency.injectTestDependency(VisualStabilityManager.class, mVisualStabilityManager);
         mDependency.injectTestDependency(NotificationListener.class, mNotificationListener);
         mDependency.injectTestDependency(KeyguardMonitor.class, mock(KeyguardMonitorImpl.class));
+        mDependency.injectTestDependency(AppOpsListener.class, mock(AppOpsListener.class));
 
         mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
         mContext.addMockSystemService(FingerprintManager.class, mock(FingerprintManager.class));