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