Display on-going notification for apps using alert windows.

Allows the user to associate alert windows with specific apps
and revoke the permission if they want.

Test: manual
Bug: 33256752
Change-Id: Ie28325b6bb799b3df253770ebe655f97ebbadd90
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 97fbfa5..2f4b74e 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -520,6 +520,7 @@
     <!-- TODO: temporary broadcast used by AutoFillManagerServiceImpl; will be removed -->
     <protected-broadcast android:name="com.android.internal.autofill.action.REQUEST_AUTOFILL" />
     <protected-broadcast android:name="android.app.action.APPLICATION_DELEGATION_SCOPES_CHANGED" />
+    <protected-broadcast android:name="com.android.server.wm.ACTION_REVOKE_SYSTEM_ALERT_WINDOW_PERMISSION" />
 
     <!-- ====================================================================== -->
     <!--                          RUNTIME PERMISSIONS                           -->
diff --git a/core/res/res/drawable-nodpi/alert_window_layer.xml b/core/res/res/drawable-nodpi/alert_window_layer.xml
new file mode 100644
index 0000000..f9b38c8
--- /dev/null
+++ b/core/res/res/drawable-nodpi/alert_window_layer.xml
@@ -0,0 +1,24 @@
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+<path
+        android:fillColor="#FF000000"
+        android:pathData="M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27 -7.38,5.74zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16z"/>
+</vector>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 8faa76c..7069d16 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3116,6 +3116,21 @@
     <string name="fast_scroll_alphabet">\u0020ABCDEFGHIJKLMNOPQRSTUVWXYZ</string>
     <string name="fast_scroll_numeric_alphabet">\u00200123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ</string>
 
+    <!-- Alert windows notification strings -->
+    <skip />
+    <!-- Name of notification channel the system post notification to inform the use about apps
+         that are drawing ui on-top of other apps (alert-windows) [CHAR LIMIT=NONE] -->
+    <string name="alert_windows_notification_channel_name"><xliff:g id="name" example="Google Maps">%s</xliff:g> draw over other apps</string>
+    <!-- Notification title when an application is displaying ui on-top of other apps
+         [CHAR LIMIT=30] -->
+    <string name="alert_windows_notification_title"><xliff:g id="name" example="Google Maps">%s</xliff:g> app displaying on top.</string>
+    <!-- Notification body when an application is displaying ui on-top of other apps
+         [CHAR LIMIT=NONE] -->
+    <string name="alert_windows_notification_message">Parts of this app may remain visible at all times. If this feature isn\'t working correctly, turn it off.</string>
+    <!-- Notification action to turn-off app displaying on-top of other apps. [CHAR LIMIT=20] -->
+    <string name="alert_windows_notification_turn_off_action">TURN OFF</string>
+
+
     <!-- External media notification strings -->
     <skip />
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 0d63a1e..a4605c3 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2867,4 +2867,12 @@
 
   <!-- resolver activity -->
   <java-symbol type="drawable" name="resolver_icon_placeholder" />
+
+  <!-- Alert windows notification -->
+  <java-symbol type="string" name="alert_windows_notification_channel_name" />
+  <java-symbol type="string" name="alert_windows_notification_title" />
+  <java-symbol type="string" name="alert_windows_notification_message" />
+  <java-symbol type="string" name="alert_windows_notification_turn_off_action" />
+  <java-symbol type="drawable" name="alert_window_layer" />
+
 </resources>
diff --git a/services/core/java/com/android/server/wm/AlertWindowNotification.java b/services/core/java/com/android/server/wm/AlertWindowNotification.java
new file mode 100644
index 0000000..0d282ef
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AlertWindowNotification.java
@@ -0,0 +1,144 @@
+/*
+ * 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.server.wm;
+
+import static android.app.Notification.VISIBILITY_PRIVATE;
+import static android.app.NotificationManager.IMPORTANCE_MIN;
+import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
+import static android.content.Context.NOTIFICATION_SERVICE;
+import static android.content.Intent.EXTRA_PACKAGE_NAME;
+import static android.content.Intent.EXTRA_UID;
+import static com.android.server.wm.WindowManagerService.ACTION_REVOKE_SYSTEM_ALERT_WINDOW_PERMISSION;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import com.android.internal.R;
+
+/** Displays an ongoing notification for a process displaying an alert window */
+class AlertWindowNotification {
+    private static final String CHANNEL_PREFIX = "com.android.server.wm.AlertWindowNotification - ";
+    private static final int NOTIFICATION_ID = 0;
+
+    private static int sNextRequestCode = 0;
+    private final int mRequestCode;
+    private final WindowManagerService mService;
+    private String mNotificationTag;
+    private final NotificationManager mNotificationManager;
+    private final String mPackageName;
+    private final int mUid;
+    private boolean mCancelled;
+
+    AlertWindowNotification(WindowManagerService service, String packageName, int uid) {
+        mService = service;
+        mPackageName = packageName;
+        mUid = uid;
+        mNotificationManager =
+                (NotificationManager) mService.mContext.getSystemService(NOTIFICATION_SERVICE);
+        mNotificationTag = CHANNEL_PREFIX + mPackageName;
+        mRequestCode = sNextRequestCode++;
+
+        // We can't create/post the notification while the window manager lock is held since it will
+        // end up calling into activity manager. So, we post a message to do it later.
+        mService.mH.post(this::postNotification);
+    }
+
+    /** Cancels the notification */
+    void cancel() {
+        mNotificationManager.cancel(mNotificationTag, NOTIFICATION_ID);
+        mCancelled = true;
+    }
+
+    /** Don't call with the window manager lock held! */
+    private void postNotification() {
+        final Context context = mService.mContext;
+        final PackageManager pm = context.getPackageManager();
+        final ApplicationInfo aInfo = getApplicationInfo(pm, mPackageName);
+        final String appName = (aInfo != null)
+                ? pm.getApplicationLabel(aInfo).toString() : mPackageName;
+
+        createNotificationChannelIfNeeded(context, appName);
+
+        final String message = context.getString(R.string.alert_windows_notification_message);
+        final Notification.Builder builder = new Notification.Builder(context, mNotificationTag)
+                .setOngoing(true)
+                .setContentTitle(
+                        context.getString(R.string.alert_windows_notification_title, appName))
+                .setContentText(message)
+                .setSmallIcon(R.drawable.alert_window_layer)
+                .setColor(context.getColor(R.color.system_notification_accent_color))
+                .setStyle(new Notification.BigTextStyle().bigText(message))
+                .setLocalOnly(true)
+                .addAction(getTurnOffAction(context, mPackageName, mUid));
+
+        if (aInfo != null) {
+            final Bitmap bitmap = ((BitmapDrawable) pm.getApplicationIcon(aInfo)).getBitmap();
+            builder.setLargeIcon(bitmap);
+        }
+
+        synchronized (mService.mWindowMap) {
+            if (mCancelled) {
+                // Notification was cancelled, so nothing more to do...
+                return;
+            }
+            mNotificationManager.notify(mNotificationTag, NOTIFICATION_ID, builder.build());
+        }
+    }
+
+    private Notification.Action getTurnOffAction(Context context, String packageName, int uid) {
+        final Intent intent = new Intent(ACTION_REVOKE_SYSTEM_ALERT_WINDOW_PERMISSION);
+        intent.putExtra(EXTRA_PACKAGE_NAME, packageName);
+        intent.putExtra(EXTRA_UID, uid);
+        // Calls into activity manager...
+        final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, mRequestCode,
+                intent, FLAG_CANCEL_CURRENT);
+        return new Notification.Action.Builder(R.drawable.alert_window_layer,
+                context.getString(R.string.alert_windows_notification_turn_off_action),
+                pendingIntent).build();
+
+    }
+
+    private void createNotificationChannelIfNeeded(Context context, String appName) {
+        if (mNotificationManager.getNotificationChannel(mNotificationTag) != null) {
+            return;
+        }
+        final String nameChannel =
+                context.getString(R.string.alert_windows_notification_channel_name, appName);
+        final NotificationChannel channel =
+                new NotificationChannel(mNotificationTag, nameChannel, IMPORTANCE_MIN);
+        channel.enableLights(false);
+        channel.enableVibration(false);
+        mNotificationManager.createNotificationChannel(channel);
+    }
+
+
+    private ApplicationInfo getApplicationInfo(PackageManager pm, String packageName) {
+        try {
+            return pm.getApplicationInfo(packageName, 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            return null;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index e6fd0ab..5355f31 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -80,8 +80,10 @@
     // Set of visible alert window surfaces connected to this session.
     private final Set<WindowSurfaceController> mAlertWindowSurfaces = new HashSet<>();
     final boolean mCanAddInternalSystemWindow;
+    private AlertWindowNotification mAlertWindowNotification;
     private boolean mClientDead = false;
     private float mLastReportedAnimatorScale;
+    private String mPackageName;
 
     public Session(WindowManagerService service, IWindowSessionCallback callback,
             IInputMethodClient client, IInputContext inputContext) {
@@ -555,7 +557,8 @@
         }
     }
 
-    void windowAddedLocked() {
+    void windowAddedLocked(String packageName) {
+        mPackageName = packageName;
         if (mSurfaceSession == null) {
             if (WindowManagerService.localLOGV) Slog.v(
                 TAG_WM, "First window added to " + this + ", creating SurfaceSession");
@@ -595,7 +598,12 @@
             }
 
             if (changed) {
-                // TODO: Update notification.
+                if (mAlertWindowSurfaces.isEmpty()) {
+                    cancelAlertWindowNotification();
+                } else if (mAlertWindowNotification == null){
+                    mAlertWindowNotification = new AlertWindowNotification(
+                            mService, mPackageName, mUid);
+                }
             }
         }
 
@@ -636,14 +644,24 @@
                     + " in session " + this + ": " + e.toString());
         }
         mSurfaceSession = null;
+        mAlertWindowSurfaces.clear();
+        mAppOverlaySurfaces.clear();
         setHasOverlayUi(false);
-        // TODO: Update notification
+        cancelAlertWindowNotification();
     }
 
     private void setHasOverlayUi(boolean hasOverlayUi) {
         mService.mH.obtainMessage(H.SET_HAS_OVERLAY_UI, mPid, hasOverlayUi ? 1 : 0).sendToTarget();
     }
 
+    private void cancelAlertWindowNotification() {
+        if (mAlertWindowNotification == null) {
+            return;
+        }
+        mAlertWindowNotification.cancel();
+        mAlertWindowNotification = null;
+    }
+
     void dump(PrintWriter pw, String prefix) {
         pw.print(prefix); pw.print("mNumWindow="); pw.print(mNumWindow);
                 pw.print(" mAppOverlaySurfaces="); pw.print(mAppOverlaySurfaces);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 1df9e43..02c52a7 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -21,8 +21,13 @@
 import static android.Manifest.permission.REGISTER_WINDOW_MANAGER_LISTENERS;
 import static android.app.ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
 import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
+import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
 import static android.app.StatusBarManager.DISABLE_MASK;
+import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED;
 import static android.content.Intent.ACTION_USER_REMOVED;
+import static android.content.Intent.EXTRA_PACKAGE_NAME;
+import static android.content.Intent.EXTRA_UID;
 import static android.content.Intent.EXTRA_USER_HANDLE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
@@ -66,6 +71,7 @@
 import static com.android.server.wm.AppWindowAnimator.PROLONG_ANIMATION_AT_START;
 import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_DOCKED_DIVIDER;
 import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_FREEFORM;
+import static com.android.server.wm.KeyguardDisableHandler.KEYGUARD_POLICY_CHANGED;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ADD_REMOVE;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ANIM;
@@ -343,20 +349,34 @@
 
     final private KeyguardDisableHandler mKeyguardDisableHandler;
 
-    final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+    static final String ACTION_REVOKE_SYSTEM_ALERT_WINDOW_PERMISSION =
+            "com.android.server.wm.ACTION_REVOKE_SYSTEM_ALERT_WINDOW_PERMISSION";
+
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            final String action = intent.getAction();
-            if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals(action)) {
-                mKeyguardDisableHandler.sendEmptyMessage(
-                    KeyguardDisableHandler.KEYGUARD_POLICY_CHANGED);
-            } else if (ACTION_USER_REMOVED.equals(action)) {
-                final int userId = intent.getIntExtra(EXTRA_USER_HANDLE, USER_NULL);
-                if (userId != USER_NULL) {
-                    synchronized (mWindowMap) {
-                        mScreenCaptureDisabled.remove(userId);
+            switch (intent.getAction()) {
+                case ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED:
+                    mKeyguardDisableHandler.sendEmptyMessage(KEYGUARD_POLICY_CHANGED);
+                    break;
+                case ACTION_USER_REMOVED:
+                    final int userId = intent.getIntExtra(EXTRA_USER_HANDLE, USER_NULL);
+                    if (userId != USER_NULL) {
+                        synchronized (mWindowMap) {
+                            mScreenCaptureDisabled.remove(userId);
+                        }
                     }
-                }
+                    break;
+                case ACTION_REVOKE_SYSTEM_ALERT_WINDOW_PERMISSION:
+                    final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
+                    final int uid = intent.getIntExtra(EXTRA_UID, -1);
+                    if (packageName != null && uid != -1) {
+                        synchronized (mWindowMap) {
+                            // Revoke permission.
+                            mAppOps.setMode(OP_SYSTEM_ALERT_WINDOW, uid, packageName, MODE_IGNORED);
+                        }
+                    }
+                    break;
             }
         }
     };
@@ -1018,7 +1038,7 @@
                         updateAppOpsState();
                     }
                 };
-        mAppOps.startWatchingMode(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, null, opListener);
+        mAppOps.startWatchingMode(OP_SYSTEM_ALERT_WINDOW, null, opListener);
         mAppOps.startWatchingMode(AppOpsManager.OP_TOAST_WINDOW, null, opListener);
 
         // Get persisted window scale setting
@@ -1034,9 +1054,10 @@
 
         IntentFilter filter = new IntentFilter();
         // Track changes to DevicePolicyManager state so we can enable/disable keyguard.
-        filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
+        filter.addAction(ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
         // Listen to user removal broadcasts so that we can remove the user-specific data.
         filter.addAction(Intent.ACTION_USER_REMOVED);
+        filter.addAction(ACTION_REVOKE_SYSTEM_ALERT_WINDOW_PERMISSION);
         mContext.registerReceiver(mBroadcastReceiver, filter);
 
         mSettingsObserver = new SettingsObserver();
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 867080e..0337ba4 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -665,7 +665,7 @@
 
     void attach() {
         if (localLOGV) Slog.v(TAG, "Attaching " + this + " token=" + mToken);
-        mSession.windowAddedLocked();
+        mSession.windowAddedLocked(mAttrs.packageName);
     }
 
     @Override