From a7ba45acb1e18e654d9861ee57c0ae1e6ebfbef9 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Wed, 29 Aug 2018 09:07:52 -0400 Subject: [PATCH] Allow apps to proxy notifications for other apps This will allow apps to delegate posting to persistently running apps, to decrease the numbers of times apps need to wake up just to post a notification. Bug: 111452544 Test: runtest systemui-notification Change-Id: I1ead239747f2871f222d0ce6a971d1448a0766ad --- api/current.txt | 7 + .../android/app/INotificationManager.aidl | 5 + .../java/android/app/NotificationManager.java | 128 ++++++++++++- .../notification/StatusBarNotification.java | 16 +- .../accounts/AccountManagerService.java | 4 +- .../NotificationManagerService.java | 161 ++++++++++++---- .../notification/PreferencesHelper.java | 151 +++++++++++++-- .../NotificationManagerServiceTest.java | 178 +++++++++++++----- .../notification/PreferencesHelperTest.java | 161 +++++++++++++++- 9 files changed, 694 insertions(+), 117 deletions(-) diff --git a/api/current.txt b/api/current.txt index f8825d9924d3e..d445b576f2cd9 100755 --- a/api/current.txt +++ b/api/current.txt @@ -5680,6 +5680,7 @@ package android.app { public class NotificationManager { method public java.lang.String addAutomaticZenRule(android.app.AutomaticZenRule); method public boolean areNotificationsEnabled(); + method public boolean canNotifyAsPackage(java.lang.String); method public void cancel(int); method public void cancel(java.lang.String, int); method public void cancelAll(); @@ -5698,13 +5699,17 @@ package android.app { method public android.app.NotificationChannelGroup getNotificationChannelGroup(java.lang.String); method public java.util.List getNotificationChannelGroups(); method public java.util.List getNotificationChannels(); + method public java.lang.String getNotificationDelegate(); method public android.app.NotificationManager.Policy getNotificationPolicy(); method public boolean isNotificationListenerAccessGranted(android.content.ComponentName); method public boolean isNotificationPolicyAccessGranted(); method public void notify(int, android.app.Notification); method public void notify(java.lang.String, int, android.app.Notification); + method public void notifyAsPackage(java.lang.String, java.lang.String, int, android.app.Notification); method public boolean removeAutomaticZenRule(java.lang.String); + method public void revokeNotificationDelegate(); method public final void setInterruptionFilter(int); + method public void setNotificationDelegate(java.lang.String); method public void setNotificationPolicy(android.app.NotificationManager.Policy); method public boolean updateAutomaticZenRule(java.lang.String, android.app.AutomaticZenRule); field public static final java.lang.String ACTION_APP_BLOCK_STATE_CHANGED = "android.app.action.APP_BLOCK_STATE_CHANGED"; @@ -39644,10 +39649,12 @@ package android.service.notification { method public int getId(); method public java.lang.String getKey(); method public android.app.Notification getNotification(); + method public java.lang.String getOpPkg(); method public java.lang.String getOverrideGroupKey(); method public java.lang.String getPackageName(); method public long getPostTime(); method public java.lang.String getTag(); + method public int getUid(); method public android.os.UserHandle getUser(); method public deprecated int getUserId(); method public boolean isClearable(); diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 3171e3e3b992a..4f004d93e3bf6 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -165,4 +165,9 @@ interface INotificationManager void applyRestore(in byte[] payload, int user); ParceledListSlice getAppActiveNotifications(String callingPkg, int userId); + + void setNotificationDelegate(String callingPkg, String delegate); + void revokeNotificationDelegate(String callingPkg); + String getNotificationDelegate(String callingPkg); + boolean canNotifyAsPackage(String callingPkg, String targetPkg); } diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 4b25b8b6e1e00..b96b39df9aa0c 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -18,6 +18,7 @@ package android.app; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SystemService; import android.annotation.TestApi; @@ -352,7 +353,7 @@ public class NotificationManager { } /** - * Post a notification to be shown in the status bar. If a notification with + * Posts a notification to be shown in the status bar. If a notification with * the same tag and id has already been posted by your application and has not yet been * canceled, it will be replaced by the updated information. * @@ -375,6 +376,42 @@ public class NotificationManager { notifyAsUser(tag, id, notification, mContext.getUser()); } + /** + * Posts a notification as a specified package to be shown in the status bar. If a notification + * with the same tag and id has already been posted for that package and has not yet been + * canceled, it will be replaced by the updated information. + * + * All {@link android.service.notification.NotificationListenerService listener services} will + * be granted {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} access to any {@link Uri uris} + * provided on this notification or the + * {@link NotificationChannel} this notification is posted to using + * {@link Context#grantUriPermission(String, Uri, int)}. Permission will be revoked when the + * notification is canceled, or you can revoke permissions with + * {@link Context#revokeUriPermission(Uri, int)}. + * + * @param targetPackage The package to post the notification as. The package must have granted + * you access to post notifications on their behalf with + * {@link #setNotificationDelegate(String)}. + * @param tag A string identifier for this notification. May be {@code null}. + * @param id An identifier for this notification. The pair (tag, id) must be unique + * within your application. + * @param notification A {@link Notification} object describing what to + * show the user. Must not be null. + */ + public void notifyAsPackage(@NonNull String targetPackage, @NonNull String tag, int id, + Notification notification) { + INotificationManager service = getService(); + String sender = mContext.getPackageName(); + + try { + if (localLOGV) Log.v(TAG, sender + ": notify(" + id + ", " + notification + ")"); + service.enqueueNotificationWithTag(targetPackage, sender, tag, id, + fixNotification(notification), mContext.getUser().getIdentifier()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * @hide */ @@ -383,6 +420,18 @@ public class NotificationManager { { INotificationManager service = getService(); String pkg = mContext.getPackageName(); + + try { + if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")"); + service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, + fixNotification(notification), user.getIdentifier()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private Notification fixNotification(Notification notification) { + String pkg = mContext.getPackageName(); // Fix the notification as best we can. Notification.addFieldsFromContext(mContext, notification); @@ -400,19 +449,12 @@ public class NotificationManager { + notification); } } - if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")"); + notification.reduceImageSizes(mContext); ActivityManager am = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); boolean isLowRam = am.isLowRamDevice(); - final Notification copy = Builder.maybeCloneStrippedForDelivery(notification, isLowRam, - mContext); - try { - service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, - copy, user.getIdentifier()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return Builder.maybeCloneStrippedForDelivery(notification, isLowRam, mContext); } private void fixLegacySmallIcon(Notification n, String pkg) { @@ -473,6 +515,72 @@ public class NotificationManager { } } + /** + * Allows a package to post notifications on your behalf using + * {@link #notifyAsPackage(String, String, int, Notification)}. + * + * This can be used to allow persistent processes to post notifications based on messages + * received on your behalf from the cloud, without your process having to wake up. + * + * You can check if you have an allowed delegate with {@link #getNotificationDelegate()} and + * revoke your delegate with {@link #revokeNotificationDelegate()}. + * + * @param delegate Package name of the app which can send notifications on your behalf. + */ + public void setNotificationDelegate(@NonNull String delegate) { + INotificationManager service = getService(); + String pkg = mContext.getPackageName(); + if (localLOGV) Log.v(TAG, pkg + ": cancelAll()"); + try { + service.setNotificationDelegate(pkg, delegate); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Revokes permission for your {@link #setNotificationDelegate(String) notification delegate} + * to post notifications on your behalf. + */ + public void revokeNotificationDelegate() { + INotificationManager service = getService(); + String pkg = mContext.getPackageName(); + try { + service.revokeNotificationDelegate(pkg); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the {@link #setNotificationDelegate(String) delegate} that can post notifications on + * your behalf, if there currently is one. + */ + public @Nullable String getNotificationDelegate() { + INotificationManager service = getService(); + String pkg = mContext.getPackageName(); + try { + return service.getNotificationDelegate(pkg); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns whether you are allowed to post notifications on behalf of a given package, with + * {@link #notifyAsPackage(String, String, int, Notification)}. + * + * See {@link #setNotificationDelegate(String)}. + */ + public boolean canNotifyAsPackage(String pkg) { + INotificationManager service = getService(); + try { + return service.canNotifyAsPackage(mContext.getPackageName(), pkg); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Creates a group container for {@link NotificationChannel} objects. * diff --git a/core/java/android/service/notification/StatusBarNotification.java b/core/java/android/service/notification/StatusBarNotification.java index dd97d524d8297..84826e0f68248 100644 --- a/core/java/android/service/notification/StatusBarNotification.java +++ b/core/java/android/service/notification/StatusBarNotification.java @@ -18,7 +18,7 @@ package android.service.notification; import android.annotation.UnsupportedAppUsage; import android.app.Notification; -import android.app.NotificationChannel; +import android.app.NotificationManager; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -261,7 +261,7 @@ public class StatusBarNotification implements Parcelable { return this.user.getIdentifier(); } - /** The package of the app that posted the notification. */ + /** The package that the notification belongs to. */ public String getPackageName() { return pkg; } @@ -277,14 +277,18 @@ public class StatusBarNotification implements Parcelable { return tag; } - /** The notifying app's calling uid. @hide */ - @UnsupportedAppUsage + /** + * The notifying app's ({@link #getPackageName()}'s) uid. + */ public int getUid() { return uid; } - /** The package used for AppOps tracking. @hide */ - @UnsupportedAppUsage + /** The package that posted the notification. + *

+ * Might be different from {@link #getPackageName()} if the app owning the notification has + * a {@link NotificationManager#setNotificationDelegate(String) notification delegate}. + */ public String getOpPkg() { return opPkg; } diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 426a0c157aec5..fd32b5a76f156 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -5296,7 +5296,9 @@ public class AccountManagerService try { INotificationManager notificationManager = mInjector.getNotificationManager(); try { - notificationManager.enqueueNotificationWithTag(packageName, packageName, + // The calling uid must match either the package or op package, so use an op + // package that matches the cleared calling identity. + notificationManager.enqueueNotificationWithTag(packageName, "android", id.mTag, id.mId, notification, userId); } catch (RemoteException e) { /* ignore - local call */ diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index ce71dd2ec9ad1..03b7652e32eca 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -35,6 +35,8 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_TELEVISION; +import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; +import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL; @@ -203,6 +205,7 @@ import com.android.server.lights.Light; import com.android.server.lights.LightsManager; import com.android.server.notification.ManagedServices.ManagedServiceInfo; import com.android.server.notification.ManagedServices.UserProfiles; +import com.android.server.pm.PackageManagerService; import com.android.server.policy.PhoneWindowManager; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.uri.UriGrantsManagerInternal; @@ -470,8 +473,8 @@ public class NotificationManagerService extends SystemService { // Gather all notification listener components for candidate pkgs. Set approvedListeners = mListeners.queryPackageForServices(whitelisted, - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); + MATCH_DIRECT_BOOT_AWARE + | MATCH_DIRECT_BOOT_UNAWARE, userId); for (ComponentName cn : approvedListeners) { try { getBinderService().setNotificationListenerAccessGrantedForUser(cn, @@ -507,8 +510,8 @@ public class NotificationManagerService extends SystemService { // only be one Set approvedAssistants = mAssistants.queryPackageForServices(defaultAssistantAccess, - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); + MATCH_DIRECT_BOOT_AWARE + | MATCH_DIRECT_BOOT_UNAWARE, userId); for (ComponentName cn : approvedAssistants) { try { getBinderService().setNotificationAssistantAccessGrantedForUser( @@ -1377,7 +1380,7 @@ public class NotificationManagerService extends SystemService { NotificationUsageStats usageStats, AtomicFile policyFile, ActivityManager activityManager, GroupHelper groupHelper, IActivityManager am, UsageStatsManagerInternal appUsageStats, DevicePolicyManagerInternal dpm, - IUriGrantsManager ugm, UriGrantsManagerInternal ugmInternal) { + IUriGrantsManager ugm, UriGrantsManagerInternal ugmInternal, AppOpsManager appOps) { Resources resources = getContext().getResources(); mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(), Settings.Global.MAX_NOTIFICATION_ENQUEUE_RATE, @@ -1390,7 +1393,7 @@ public class NotificationManagerService extends SystemService { mUgmInternal = ugmInternal; mPackageManager = packageManager; mPackageManagerClient = packageManagerClient; - mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE); + mAppOps = appOps; mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE); mAppUsageStats = appUsageStats; mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); @@ -1544,7 +1547,8 @@ public class NotificationManagerService extends SystemService { LocalServices.getService(UsageStatsManagerInternal.class), LocalServices.getService(DevicePolicyManagerInternal.class), UriGrantsManager.getService(), - LocalServices.getService(UriGrantsManagerInternal.class)); + LocalServices.getService(UriGrantsManagerInternal.class), + (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE)); // register for various Intents IntentFilter filter = new IntentFilter(); @@ -2233,6 +2237,60 @@ public class NotificationManagerService extends SystemService { savePolicyFile(); } + @Override + public void setNotificationDelegate(String callingPkg, String delegate) { + checkCallerIsSameApp(callingPkg); + final int callingUid = Binder.getCallingUid(); + UserHandle user = UserHandle.getUserHandleForUid(callingUid); + try { + ApplicationInfo info = + mPackageManager.getApplicationInfo(delegate, + MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE, + user.getIdentifier()); + if (info != null) { + mPreferencesHelper.setNotificationDelegate( + callingPkg, callingUid, delegate, info.uid); + savePolicyFile(); + } + } catch (RemoteException e) { + // :( + } + } + + @Override + public void revokeNotificationDelegate(String callingPkg) { + checkCallerIsSameApp(callingPkg); + mPreferencesHelper.revokeNotificationDelegate(callingPkg, Binder.getCallingUid()); + savePolicyFile(); + } + + @Override + public String getNotificationDelegate(String callingPkg) { + // callable by Settings also + checkCallerIsSystemOrSameApp(callingPkg); + return mPreferencesHelper.getNotificationDelegate(callingPkg, Binder.getCallingUid()); + } + + @Override + public boolean canNotifyAsPackage(String callingPkg, String targetPkg) { + checkCallerIsSameApp(callingPkg); + final int callingUid = Binder.getCallingUid(); + UserHandle user = UserHandle.getUserHandleForUid(callingUid); + try { + ApplicationInfo info = + mPackageManager.getApplicationInfo(targetPkg, + MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE, + user.getIdentifier()); + if (info != null) { + return mPreferencesHelper.isDelegateAllowed( + targetPkg, info.uid, callingPkg, callingUid); + } + } catch (RemoteException e) { + // :( + } + return false; + } + @Override public void updateNotificationChannelGroupForPackage(String pkg, int uid, NotificationChannelGroup group) throws RemoteException { @@ -4053,20 +4111,21 @@ public class NotificationManagerService extends SystemService { Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id + " notification=" + notification); } - checkCallerIsSystemOrSameApp(pkg); - checkRestrictedCategories(notification); - - final int userId = ActivityManager.handleIncomingUser(callingPid, - callingUid, incomingUserId, true, false, "enqueueNotification", pkg); - final UserHandle user = new UserHandle(userId); if (pkg == null || notification == null) { throw new IllegalArgumentException("null not allowed: pkg=" + pkg + " id=" + id + " notification=" + notification); } - // The system can post notifications for any package, let us resolve that. - final int notificationUid = resolveNotificationUid(opPkg, callingUid, userId); + final int userId = ActivityManager.handleIncomingUser(callingPid, + callingUid, incomingUserId, true, false, "enqueueNotification", pkg); + final UserHandle user = UserHandle.of(userId); + + // Can throw a SecurityException if the calling uid doesn't have permission to post + // as "pkg" + final int notificationUid = resolveNotificationUid(opPkg, pkg, callingUid, userId); + + checkRestrictedCategories(notification); // Fix the notification as best we can. try { @@ -4193,17 +4252,28 @@ public class NotificationManagerService extends SystemService { } } - private int resolveNotificationUid(String opPackageName, int callingUid, int userId) { - // The system can post notifications on behalf of any package it wants - if (isCallerSystemOrPhone() && opPackageName != null && !"android".equals(opPackageName)) { - try { - return getContext().getPackageManager() - .getPackageUidAsUser(opPackageName, userId); - } catch (NameNotFoundException e) { - /* ignore */ - } + @VisibleForTesting + int resolveNotificationUid(String callingPkg, String targetPkg, + int callingUid, int userId) { + // posted from app A on behalf of app A + if (isCallerSameApp(targetPkg, callingUid) && TextUtils.equals(callingPkg, targetPkg)) { + return callingUid; } - return callingUid; + + int targetUid = -1; + try { + targetUid = mPackageManagerClient.getPackageUidAsUser(targetPkg, userId); + } catch (NameNotFoundException e) { + /* ignore */ + } + // posted from app A on behalf of app B + if (targetUid != -1 && (isCallerAndroid(callingPkg, callingUid) + || mPreferencesHelper.isDelegateAllowed( + targetPkg, targetUid, callingPkg, callingUid))) { + return targetUid; + } + + throw new SecurityException("Caller " + callingUid + " cannot post for pkg " + targetPkg); } /** @@ -4222,7 +4292,8 @@ public class NotificationManagerService extends SystemService { // package or a registered listener can enqueue. Prevents DOS attacks and deals with leaks. if (!isSystemNotification && !isNotificationFromListener) { synchronized (mNotificationLock) { - if (mNotificationsByKey.get(r.sbn.getKey()) == null && isCallerInstantApp(pkg)) { + if (mNotificationsByKey.get(r.sbn.getKey()) == null + && isCallerInstantApp(pkg, callingUid)) { // Ephemeral apps have some special constraints for notifications. // They are not allowed to create new notifications however they are allowed to // update notifications created by the system (e.g. a foreground service @@ -5149,11 +5220,11 @@ public class NotificationManagerService extends SystemService { try { Thread.sleep(waitMs); } catch (InterruptedException e) { } - mVibrator.vibrate(record.sbn.getUid(), record.sbn.getOpPkg(), + mVibrator.vibrate(record.sbn.getUid(), record.sbn.getPackageName(), effect, "Notification (delayed)", record.getAudioAttributes()); }).start(); } else { - mVibrator.vibrate(record.sbn.getUid(), record.sbn.getOpPkg(), + mVibrator.vibrate(record.sbn.getUid(), record.sbn.getPackageName(), effect, "Notification", record.getAudioAttributes()); } return true; @@ -6282,6 +6353,11 @@ public class NotificationManagerService extends SystemService { checkCallerIsSameApp(pkg); } + private boolean isCallerAndroid(String callingPkg, int uid) { + return isUidSystemOrPhone(uid) && callingPkg != null + && PackageManagerService.PLATFORM_PACKAGE_NAME.equals(callingPkg); + } + /** * Check if the notification is of a category type that is restricted to system use only, * if so throw SecurityException @@ -6302,13 +6378,13 @@ public class NotificationManagerService extends SystemService { } } - private boolean isCallerInstantApp(String pkg) { + private boolean isCallerInstantApp(String pkg, int callingUid) { // System is always allowed to act for ephemeral apps. - if (isCallerSystemOrPhone()) { + if (isUidSystemOrPhone(callingUid)) { return false; } - mAppOps.checkPackage(Binder.getCallingUid(), pkg); + mAppOps.checkPackage(callingUid, pkg); try { ApplicationInfo ai = mPackageManager.getApplicationInfo(pkg, 0, @@ -6324,7 +6400,10 @@ public class NotificationManagerService extends SystemService { } private void checkCallerIsSameApp(String pkg) { - final int uid = Binder.getCallingUid(); + checkCallerIsSameApp(pkg, Binder.getCallingUid()); + } + + private void checkCallerIsSameApp(String pkg, int uid) { try { ApplicationInfo ai = mPackageManager.getApplicationInfo( pkg, 0, UserHandle.getCallingUserId()); @@ -6340,6 +6419,24 @@ public class NotificationManagerService extends SystemService { } } + private boolean isCallerSameApp(String pkg) { + try { + checkCallerIsSameApp(pkg); + return true; + } catch (SecurityException e) { + return false; + } + } + + private boolean isCallerSameApp(String pkg, int uid) { + try { + checkCallerIsSameApp(pkg, uid); + return true; + } catch (SecurityException e) { + return false; + } + } + private static String callStateToString(int state) { switch (state) { case TelephonyManager.CALL_STATE_IDLE: return "CALL_STATE_IDLE"; diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 432d17c821f23..593e7cdf4d666 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -20,6 +20,7 @@ import static android.app.NotificationManager.IMPORTANCE_NONE; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; @@ -66,12 +67,14 @@ import java.util.concurrent.ConcurrentHashMap; public class PreferencesHelper implements RankingConfig { private static final String TAG = "NotificationPrefHelper"; private static final int XML_VERSION = 1; + private static final int UNKNOWN_UID = UserHandle.USER_NULL; @VisibleForTesting static final String TAG_RANKING = "ranking"; private static final String TAG_PACKAGE = "package"; private static final String TAG_CHANNEL = "channel"; private static final String TAG_GROUP = "channelGroup"; + private static final String TAG_DELEGATE = "delegate"; private static final String ATT_VERSION = "version"; private static final String ATT_NAME = "name"; @@ -82,6 +85,8 @@ public class PreferencesHelper implements RankingConfig { private static final String ATT_IMPORTANCE = "importance"; private static final String ATT_SHOW_BADGE = "show_badge"; private static final String ATT_APP_USER_LOCKED_FIELDS = "app_user_locked_fields"; + private static final String ATT_ENABLED = "enabled"; + private static final String ATT_USER_ALLOWED = "allowed"; private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT; private static final int DEFAULT_VISIBILITY = NotificationManager.VISIBILITY_NO_OVERRIDE; @@ -147,8 +152,7 @@ public class PreferencesHelper implements RankingConfig { } if (type == XmlPullParser.START_TAG) { if (TAG_PACKAGE.equals(tag)) { - int uid = XmlUtils.readIntAttribute(parser, ATT_UID, - PackagePreferences.UNKNOWN_UID); + int uid = XmlUtils.readIntAttribute(parser, ATT_UID, UNKNOWN_UID); String name = parser.getAttributeValue(null, ATT_NAME); if (!TextUtils.isEmpty(name)) { if (forRestore) { @@ -217,6 +221,24 @@ public class PreferencesHelper implements RankingConfig { r.channels.put(id, channel); } } + // Delegate + if (TAG_DELEGATE.equals(tagName)) { + int delegateId = + XmlUtils.readIntAttribute(parser, ATT_UID, UNKNOWN_UID); + String delegateName = + XmlUtils.readStringAttribute(parser, ATT_NAME); + boolean delegateEnabled = XmlUtils.readBooleanAttribute( + parser, ATT_ENABLED, Delegate.DEFAULT_ENABLED); + boolean userAllowed = XmlUtils.readBooleanAttribute( + parser, ATT_USER_ALLOWED, Delegate.DEFAULT_USER_ALLOWED); + Delegate d = null; + if (delegateId != UNKNOWN_UID && !TextUtils.isEmpty(delegateName)) { + d = new Delegate( + delegateName, delegateId, delegateEnabled, userAllowed); + } + r.delegate = d; + } + } try { @@ -248,7 +270,7 @@ public class PreferencesHelper implements RankingConfig { final String key = packagePreferencesKey(pkg, uid); synchronized (mPackagePreferencess) { PackagePreferences - r = (uid == PackagePreferences.UNKNOWN_UID) ? mRestoredWithoutUids.get(pkg) + r = (uid == UNKNOWN_UID) ? mRestoredWithoutUids.get(pkg) : mPackagePreferencess.get(key); if (r == null) { r = new PackagePreferences(); @@ -265,7 +287,7 @@ public class PreferencesHelper implements RankingConfig { Slog.e(TAG, "createDefaultChannelIfNeeded - Exception: " + e); } - if (r.uid == PackagePreferences.UNKNOWN_UID) { + if (r.uid == UNKNOWN_UID) { mRestoredWithoutUids.put(pkg, r); } else { mPackagePreferencess.put(key, r); @@ -357,7 +379,8 @@ public class PreferencesHelper implements RankingConfig { || r.showBadge != DEFAULT_SHOW_BADGE || r.lockedAppFields != DEFAULT_LOCKED_APP_FIELDS || r.channels.size() > 0 - || r.groups.size() > 0; + || r.groups.size() > 0 + || r.delegate != null; if (hasNonDefaultSettings) { out.startTag(null, TAG_PACKAGE); out.attribute(null, ATT_NAME, r.pkg); @@ -378,6 +401,21 @@ public class PreferencesHelper implements RankingConfig { out.attribute(null, ATT_UID, Integer.toString(r.uid)); } + if (r.delegate != null) { + out.startTag(null, TAG_DELEGATE); + + out.attribute(null, ATT_NAME, r.delegate.mPkg); + out.attribute(null, ATT_UID, Integer.toString(r.delegate.mUid)); + if (r.delegate.mEnabled != Delegate.DEFAULT_ENABLED) { + out.attribute(null, ATT_ENABLED, Boolean.toString(r.delegate.mEnabled)); + } + if (r.delegate.mUserAllowed != Delegate.DEFAULT_USER_ALLOWED) { + out.attribute(null, ATT_USER_ALLOWED, + Boolean.toString(r.delegate.mUserAllowed)); + } + out.endTag(null, TAG_DELEGATE); + } + for (NotificationChannelGroup group : r.groups.values()) { group.writeXml(out); } @@ -923,16 +961,76 @@ public class PreferencesHelper implements RankingConfig { * considered for sentiment adjustments (and thus never show a blocking helper). */ public void setAppImportanceLocked(String packageName, int uid) { - PackagePreferences PackagePreferences = getOrCreatePackagePreferences(packageName, uid); - if ((PackagePreferences.lockedAppFields & LockableAppFields.USER_LOCKED_IMPORTANCE) != 0) { + PackagePreferences prefs = getOrCreatePackagePreferences(packageName, uid); + if ((prefs.lockedAppFields & LockableAppFields.USER_LOCKED_IMPORTANCE) != 0) { return; } - PackagePreferences.lockedAppFields = - PackagePreferences.lockedAppFields | LockableAppFields.USER_LOCKED_IMPORTANCE; + prefs.lockedAppFields = prefs.lockedAppFields | LockableAppFields.USER_LOCKED_IMPORTANCE; updateConfig(); } + /** + * Returns the delegate for a given package, if it's allowed by the package and the user. + */ + public @Nullable String getNotificationDelegate(String sourcePkg, int sourceUid) { + PackagePreferences prefs = getPackagePreferences(sourcePkg, sourceUid); + + if (prefs == null || prefs.delegate == null) { + return null; + } + if (!prefs.delegate.mUserAllowed || !prefs.delegate.mEnabled) { + return null; + } + return prefs.delegate.mPkg; + } + + /** + * Used by an app to delegate notification posting privileges to another apps. + */ + public void setNotificationDelegate(String sourcePkg, int sourceUid, + String delegatePkg, int delegateUid) { + PackagePreferences prefs = getOrCreatePackagePreferences(sourcePkg, sourceUid); + + boolean userAllowed = prefs.delegate == null || prefs.delegate.mUserAllowed; + Delegate delegate = new Delegate(delegatePkg, delegateUid, true, userAllowed); + prefs.delegate = delegate; + updateConfig(); + } + + /** + * Used by an app to turn off its notification delegate. + */ + public void revokeNotificationDelegate(String sourcePkg, int sourceUid) { + PackagePreferences prefs = getPackagePreferences(sourcePkg, sourceUid); + if (prefs != null && prefs.delegate != null) { + prefs.delegate.mEnabled = false; + updateConfig(); + } + } + + /** + * Toggles whether an app can have a notification delegate on behalf of a user. + */ + public void toggleNotificationDelegate(String sourcePkg, int sourceUid, boolean userAllowed) { + PackagePreferences prefs = getPackagePreferences(sourcePkg, sourceUid); + if (prefs != null && prefs.delegate != null) { + prefs.delegate.mUserAllowed = userAllowed; + updateConfig(); + } + } + + /** + * Returns whether the given app is allowed on post notifications on behalf of the other given + * app. + */ + public boolean isDelegateAllowed(String sourcePkg, int sourceUid, + String potentialDelegatePkg, int potentialDelegateUid) { + PackagePreferences prefs = getPackagePreferences(sourcePkg, sourceUid); + + return prefs != null && prefs.isValidDelegate(potentialDelegatePkg, potentialDelegateUid); + } + @VisibleForTesting void lockFieldsForUpdate(NotificationChannel original, NotificationChannel update) { if (original.canBypassDnd() != update.canBypassDnd()) { @@ -994,8 +1092,7 @@ public class PreferencesHelper implements RankingConfig { pw.print(" AppSettings: "); pw.print(r.pkg); pw.print(" ("); - pw.print(r.uid == PackagePreferences.UNKNOWN_UID ? "UNKNOWN_UID" - : Integer.toString(r.uid)); + pw.print(r.uid == UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid)); pw.print(')'); if (r.importance != DEFAULT_IMPORTANCE) { pw.print(" importance="); @@ -1356,8 +1453,6 @@ public class PreferencesHelper implements RankingConfig { } private static class PackagePreferences { - static int UNKNOWN_UID = UserHandle.USER_NULL; - String pkg; int uid = UNKNOWN_UID; int importance = DEFAULT_IMPORTANCE; @@ -1366,7 +1461,37 @@ public class PreferencesHelper implements RankingConfig { boolean showBadge = DEFAULT_SHOW_BADGE; int lockedAppFields = DEFAULT_LOCKED_APP_FIELDS; + Delegate delegate = null; ArrayMap channels = new ArrayMap<>(); Map groups = new ConcurrentHashMap<>(); + + public boolean isValidDelegate(String pkg, int uid) { + return delegate != null && delegate.isAllowed(pkg, uid); + } + } + + private static class Delegate { + static final boolean DEFAULT_ENABLED = true; + static final boolean DEFAULT_USER_ALLOWED = true; + String mPkg; + int mUid = UNKNOWN_UID; + boolean mEnabled = DEFAULT_ENABLED; + boolean mUserAllowed = DEFAULT_USER_ALLOWED; + + Delegate(String pkg, int uid, boolean enabled, boolean userAllowed) { + mPkg = pkg; + mUid = uid; + mEnabled = enabled; + mUserAllowed = userAllowed; + } + + public boolean isAllowed(String pkg, int uid) { + if (pkg == null || uid == UNKNOWN_UID) { + return false; + } + return pkg.equals(mPkg) + && uid == mUid + && (mUserAllowed && mEnabled); + } } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 0ff124e4ce7a0..a1b3b988397c4 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -65,6 +65,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.AppOpsManager; +import android.app.Application; import android.app.IActivityManager; import android.app.INotificationManager; import android.app.Notification; @@ -195,6 +197,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { IUriGrantsManager mUgm; @Mock UriGrantsManagerInternal mUgmInternal; + @Mock + AppOpsManager mAppOpsManager; // Use a Testable subclass so we can simulate calls from the system without failing. private static class TestableNotificationManagerService extends NotificationManagerService { @@ -295,7 +299,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mListeners, mAssistants, mConditionProviders, mCompanionMgr, mSnoozeHelper, mUsageStats, mPolicyFile, mActivityManager, mGroupHelper, mAm, mAppUsageStats, - mock(DevicePolicyManagerInternal.class), mUgm, mUgmInternal); + mock(DevicePolicyManagerInternal.class), mUgm, mUgmInternal, + mAppOpsManager); } catch (SecurityException e) { if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) { throw e; @@ -531,7 +536,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.createNotificationChannels( PKG, new ParceledListSlice(Arrays.asList(channel))); final StatusBarNotification sbn = generateNotificationRecord(channel).sbn; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); waitForIdle(); assertEquals(0, mBinderService.getActiveNotifications(sbn.getPackageName()).length); @@ -549,7 +554,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final StatusBarNotification sbn = generateNotificationRecord(channel).sbn; sbn.getNotification().flags |= FLAG_FOREGROUND_SERVICE; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); waitForIdle(); assertEquals(1, mBinderService.getActiveNotifications(sbn.getPackageName()).length); @@ -578,7 +583,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { StatusBarNotification sbn = generateNotificationRecord(channel).sbn; sbn.getNotification().flags |= FLAG_FOREGROUND_SERVICE; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); waitForIdle(); // The first time a foreground service notification is shown, we allow the channel @@ -600,7 +605,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { sbn = generateNotificationRecord(channel).sbn; sbn.getNotification().flags |= FLAG_FOREGROUND_SERVICE; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); waitForIdle(); // The second time it is shown, we keep the user's preference. @@ -631,7 +636,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.setNotificationsEnabledForPackage(PKG, mUid, false); final StatusBarNotification sbn = generateNotificationRecord(null).sbn; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); waitForIdle(); assertEquals(0, mBinderService.getActiveNotifications(sbn.getPackageName()).length); @@ -645,7 +650,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final StatusBarNotification sbn = generateNotificationRecord(null).sbn; sbn.getNotification().flags |= FLAG_FOREGROUND_SERVICE; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); waitForIdle(); assertEquals(0, mBinderService.getActiveNotifications(sbn.getPackageName()).length); @@ -667,7 +672,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final StatusBarNotification sbn = generateNotificationRecord(mTestNotificationChannel, ++id, "", false).sbn; sbn.getNotification().category = category; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); } waitForIdle(); @@ -691,7 +696,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final StatusBarNotification sbn = generateNotificationRecord(mTestNotificationChannel, ++id, "", false).sbn; sbn.getNotification().category = category; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); } waitForIdle(); @@ -714,7 +719,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final StatusBarNotification sbn = generateNotificationRecord(null).sbn; sbn.getNotification().category = category; try { - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); fail("Calls from non system apps should not allow use of restricted categories"); } catch (SecurityException e) { @@ -746,7 +751,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testEnqueueNotificationWithTag_PopulatesGetActiveNotifications() throws Exception { - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", 0, + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, generateNotificationRecord(null).getNotification(), 0); waitForIdle(); StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG); @@ -756,7 +761,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelNotificationImmediatelyAfterEnqueue() throws Exception { - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", 0, + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, generateNotificationRecord(null).getNotification(), 0); mBinderService.cancelNotificationWithTag(PKG, "tag", 0, 0); waitForIdle(); @@ -768,10 +773,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelNotificationWhilePostedAndEnqueued() throws Exception { - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", 0, + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, generateNotificationRecord(null).getNotification(), 0); waitForIdle(); - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", 0, + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, generateNotificationRecord(null).getNotification(), 0); mBinderService.cancelNotificationWithTag(PKG, "tag", 0, 0); waitForIdle(); @@ -788,7 +793,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testCancelNotificationsFromListenerImmediatelyAfterEnqueue() throws Exception { NotificationRecord r = generateNotificationRecord(null); final StatusBarNotification sbn = r.sbn; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); mBinderService.cancelNotificationsFromListener(null, null); waitForIdle(); @@ -801,7 +806,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelAllNotificationsImmediatelyAfterEnqueue() throws Exception { final StatusBarNotification sbn = generateNotificationRecord(null).sbn; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); mBinderService.cancelAllNotifications(PKG, sbn.getUserId()); waitForIdle(); @@ -816,7 +821,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final NotificationRecord n = generateNotificationRecord( mTestNotificationChannel, 1, "group", true); - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", n.sbn.getId(), n.sbn.getNotification(), n.sbn.getUserId()); waitForIdle(); @@ -839,9 +844,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final NotificationRecord child = generateNotificationRecord( mTestNotificationChannel, 2, "group1", false); - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", parent.sbn.getId(), parent.sbn.getNotification(), parent.sbn.getUserId()); - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", child.sbn.getId(), child.sbn.getNotification(), child.sbn.getUserId()); waitForIdle(); @@ -854,7 +859,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testCancelAllNotificationsMultipleEnqueuedDoesNotCrash() throws Exception { final StatusBarNotification sbn = generateNotificationRecord(null).sbn; for (int i = 0; i < 10; i++) { - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); } mBinderService.cancelAllNotifications(PKG, sbn.getUserId()); @@ -873,17 +878,17 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mTestNotificationChannel, 2, "group1", false); // fully post parent notification - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", parent.sbn.getId(), parent.sbn.getNotification(), parent.sbn.getUserId()); waitForIdle(); // enqueue the child several times for (int i = 0; i < 10; i++) { - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", child.sbn.getId(), child.sbn.getNotification(), child.sbn.getUserId()); } // make the parent a child, which will cancel the child notification - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", parentAsChild.sbn.getId(), parentAsChild.sbn.getNotification(), parentAsChild.sbn.getUserId()); waitForIdle(); @@ -895,7 +900,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testCancelAllNotifications_IgnoreForegroundService() throws Exception { final StatusBarNotification sbn = generateNotificationRecord(null).sbn; sbn.getNotification().flags |= FLAG_FOREGROUND_SERVICE; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); mBinderService.cancelAllNotifications(PKG, sbn.getUserId()); waitForIdle(); @@ -909,7 +914,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testCancelAllNotifications_IgnoreOtherPackages() throws Exception { final StatusBarNotification sbn = generateNotificationRecord(null).sbn; sbn.getNotification().flags |= FLAG_FOREGROUND_SERVICE; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); mBinderService.cancelAllNotifications("other_pkg_name", sbn.getUserId()); waitForIdle(); @@ -922,7 +927,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelAllNotifications_NullPkgRemovesAll() throws Exception { final StatusBarNotification sbn = generateNotificationRecord(null).sbn; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); mBinderService.cancelAllNotifications(null, sbn.getUserId()); waitForIdle(); @@ -935,7 +940,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCancelAllNotifications_NullPkgIgnoresUserAllNotifications() throws Exception { final StatusBarNotification sbn = generateNotificationRecord(null).sbn; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), UserHandle.USER_ALL); // Null pkg is how we signal a user switch. mBinderService.cancelAllNotifications(null, sbn.getUserId()); @@ -950,7 +955,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testAppInitiatedCancelAllNotifications_CancelsNoClearFlag() throws Exception { final StatusBarNotification sbn = generateNotificationRecord(null).sbn; sbn.getNotification().flags |= Notification.FLAG_NO_CLEAR; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); mBinderService.cancelAllNotifications(PKG, sbn.getUserId()); waitForIdle(); @@ -1037,7 +1042,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testRemoveForegroundServiceFlag_ImmediatelyAfterEnqueue() throws Exception { final StatusBarNotification sbn = generateNotificationRecord(null).sbn; sbn.getNotification().flags |= FLAG_FOREGROUND_SERVICE; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", null, + mBinderService.enqueueNotificationWithTag(PKG, PKG, null, sbn.getId(), sbn.getNotification(), sbn.getUserId()); mInternalService.removeForegroundServiceFlagFromNotification(PKG, sbn.getId(), sbn.getUserId()); @@ -1052,10 +1057,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final StatusBarNotification sbn = generateNotificationRecord(null).sbn; sbn.getNotification().flags = Notification.FLAG_ONGOING_EVENT | FLAG_FOREGROUND_SERVICE; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); sbn.getNotification().flags = Notification.FLAG_ONGOING_EVENT; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); mBinderService.cancelNotificationWithTag(PKG, "tag", sbn.getId(), sbn.getUserId()); waitForIdle(); @@ -1145,21 +1150,21 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // should not be returned final NotificationRecord group2 = generateNotificationRecord( mTestNotificationChannel, 2, "group2", true); - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", null, + mBinderService.enqueueNotificationWithTag(PKG, PKG, null, group2.sbn.getId(), group2.sbn.getNotification(), group2.sbn.getUserId()); waitForIdle(); // should not be returned final NotificationRecord nonGroup = generateNotificationRecord( mTestNotificationChannel, 3, null, false); - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", null, + mBinderService.enqueueNotificationWithTag(PKG, PKG, null, nonGroup.sbn.getId(), nonGroup.sbn.getNotification(), nonGroup.sbn.getUserId()); waitForIdle(); // same group, child, should be returned final NotificationRecord group1Child = generateNotificationRecord( mTestNotificationChannel, 4, "group1", false); - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", null, group1Child.sbn.getId(), + mBinderService.enqueueNotificationWithTag(PKG, PKG, null, group1Child.sbn.getId(), group1Child.sbn.getNotification(), group1Child.sbn.getUserId()); waitForIdle(); @@ -1216,7 +1221,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testAppInitiatedCancelAllNotifications_CancelsOnGoingFlag() throws Exception { final StatusBarNotification sbn = generateNotificationRecord(null).sbn; sbn.getNotification().flags |= Notification.FLAG_ONGOING_EVENT; - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); mBinderService.cancelAllNotifications(PKG, sbn.getUserId()); waitForIdle(); @@ -1333,7 +1338,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { new NotificationChannel("foo", "foo", IMPORTANCE_HIGH)); Notification.TvExtender tv = new Notification.TvExtender().setChannelId("foo"); - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", 0, + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, generateNotificationRecord(null, tv).getNotification(), 0); verify(mPreferencesHelper, times(1)).getNotificationChannel( anyString(), anyInt(), eq("foo"), anyBoolean()); @@ -1348,7 +1353,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mTestNotificationChannel); Notification.TvExtender tv = new Notification.TvExtender().setChannelId("foo"); - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", 0, + mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", 0, generateNotificationRecord(null, tv).getNotification(), 0); verify(mPreferencesHelper, times(1)).getNotificationChannel( anyString(), anyInt(), eq(mTestNotificationChannel.getId()), anyBoolean()); @@ -1879,7 +1884,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final NotificationRecord child = generateNotificationRecord( mTestNotificationChannel, 2, "group", false); - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", null, + mBinderService.enqueueNotificationWithTag(PKG, PKG, null, child.sbn.getId(), child.sbn.getNotification(), child.sbn.getUserId()); waitForIdle(); @@ -1892,7 +1897,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final NotificationRecord record = generateNotificationRecord( mTestNotificationChannel, 2, null, false); - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", null, + mBinderService.enqueueNotificationWithTag(PKG, PKG, null, record.sbn.getId(), record.sbn.getNotification(), record.sbn.getUserId()); waitForIdle(); @@ -1904,7 +1909,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final NotificationRecord parent = generateNotificationRecord( mTestNotificationChannel, 2, "group", true); - mBinderService.enqueueNotificationWithTag(PKG, "opPkg", null, + mBinderService.enqueueNotificationWithTag(PKG, PKG, null, parent.sbn.getId(), parent.sbn.getNotification(), parent.sbn.getUserId()); waitForIdle(); @@ -2378,12 +2383,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testBumpFGImportance_noChannelChangePreOApp() throws Exception { String preOPkg = PKG_N_MR1; - int preOUid = 145; final ApplicationInfo legacy = new ApplicationInfo(); legacy.targetSdkVersion = Build.VERSION_CODES.N_MR1; when(mPackageManagerClient.getApplicationInfoAsUser(eq(preOPkg), anyInt(), anyInt())) .thenReturn(legacy); - when(mPackageManagerClient.getPackageUidAsUser(eq(preOPkg), anyInt())).thenReturn(preOUid); + when(mPackageManagerClient.getPackageUidAsUser(eq(preOPkg), anyInt())) + .thenReturn(Binder.getCallingUid()); getContext().setMockPackageManager(mPackageManagerClient); Notification.Builder nb = new Notification.Builder(mContext, @@ -2393,12 +2398,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .setFlag(FLAG_FOREGROUND_SERVICE, true) .setPriority(Notification.PRIORITY_MIN); - StatusBarNotification sbn = new StatusBarNotification(preOPkg, preOPkg, 9, "tag", preOUid, - 0, nb.build(), new UserHandle(preOUid), null, 0); + StatusBarNotification sbn = new StatusBarNotification(preOPkg, preOPkg, 9, "tag", + Binder.getCallingUid(), 0, nb.build(), new UserHandle(Binder.getCallingUid()), null, 0); - mBinderService.enqueueNotificationWithTag(preOPkg, preOPkg, "tag", - sbn.getId(), sbn.getNotification(), sbn.getUserId()); + mBinderService.enqueueNotificationWithTag(sbn.getPackageName(), sbn.getOpPkg(), + sbn.getTag(), sbn.getId(), sbn.getNotification(), sbn.getUserId()); waitForIdle(); + assertEquals(IMPORTANCE_LOW, mService.getNotificationRecord(sbn.getKey()).getImportance()); @@ -2408,8 +2414,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .setFlag(FLAG_FOREGROUND_SERVICE, true) .setPriority(Notification.PRIORITY_MIN); - sbn = new StatusBarNotification(preOPkg, preOPkg, 9, "tag", preOUid, - 0, nb.build(), new UserHandle(preOUid), null, 0); + sbn = new StatusBarNotification(preOPkg, preOPkg, 9, "tag", Binder.getCallingUid(), + 0, nb.build(), new UserHandle(Binder.getCallingUid()), null, 0); mBinderService.enqueueNotificationWithTag(preOPkg, preOPkg, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); @@ -3360,7 +3366,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void testMybeRecordInterruptionLocked_doesNotRecordTwice() + public void testMaybeRecordInterruptionLocked_doesNotRecordTwice() throws RemoteException { final NotificationRecord r = generateNotificationRecord( mTestNotificationChannel, 1, null, true); @@ -3373,4 +3379,78 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mAppUsageStats, times(1)).reportInterruptiveNotification( anyString(), anyString(), anyInt()); } + + @Test + public void testResolveNotificationUid_sameApp() throws Exception { + ApplicationInfo info = new ApplicationInfo(); + info.uid = Binder.getCallingUid(); + when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt())).thenReturn(info); + + int actualUid = mService.resolveNotificationUid("caller", "caller", info.uid, 0); + + assertEquals(info.uid, actualUid); + } + + @Test + public void testResolveNotificationUid_sameAppWrongPkg() throws Exception { + ApplicationInfo info = new ApplicationInfo(); + info.uid = Binder.getCallingUid(); + when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt())).thenReturn(info); + + try { + mService.resolveNotificationUid("caller", "other", info.uid, 0); + fail("Incorrect pkg didn't throw security exception"); + } catch (SecurityException e) { + // yay + } + } + + @Test + public void testResolveNotificationUid_sameAppWrongUid() throws Exception { + ApplicationInfo info = new ApplicationInfo(); + info.uid = 1356347; + when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt())).thenReturn(info); + + try { + mService.resolveNotificationUid("caller", "caller", 9, 0); + fail("Incorrect uid didn't throw security exception"); + } catch (SecurityException e) { + // yay + } + } + + @Test + public void testResolveNotificationUid_delegateAllowed() throws Exception { + int expectedUid = 123; + + when(mPackageManagerClient.getPackageUidAsUser("target", 0)).thenReturn(expectedUid); + mService.setPreferencesHelper(mPreferencesHelper); + when(mPreferencesHelper.isDelegateAllowed(anyString(), anyInt(), anyString(), anyInt())) + .thenReturn(true); + + assertEquals(expectedUid, mService.resolveNotificationUid("caller", "target", 9, 0)); + } + + @Test + public void testResolveNotificationUid_androidAllowed() throws Exception { + int expectedUid = 123; + + when(mPackageManagerClient.getPackageUidAsUser("target", 0)).thenReturn(expectedUid); + // no delegate + + assertEquals(expectedUid, mService.resolveNotificationUid("android", "target", 0, 0)); + } + + @Test + public void testResolveNotificationUid_delegateNotAllowed() throws Exception { + when(mPackageManagerClient.getPackageUidAsUser("target", 0)).thenReturn(123); + // no delegate + + try { + mService.resolveNotificationUid("caller", "target", 9, 0); + fail("Incorrect uid didn't throw security exception"); + } catch (SecurityException e) { + // yay + } + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 73adf25cb3ec8..750345be1c1dd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -123,7 +123,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - UserHandle user = UserHandle.ALL; final ApplicationInfo legacy = new ApplicationInfo(); legacy.targetSdkVersion = Build.VERSION_CODES.N_MR1; @@ -176,11 +175,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { .build(); } - private NotificationChannel getDefaultChannel() { - return new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID, "name", - IMPORTANCE_LOW); - } - private ByteArrayOutputStream writeXmlAndPurge(String pkg, int uid, boolean forBackup, String... channelIds) throws Exception { @@ -1787,4 +1781,159 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.setEnabled(PKG_N_MR1, 1000, true); assertEquals(3, mHelper.getBlockedAppCount(0)); } + + @Test + public void testSetNotificationDelegate() { + mHelper.setNotificationDelegate(PKG_O, UID_O, "other", 53); + assertEquals("other", mHelper.getNotificationDelegate(PKG_O, UID_O)); + } + + @Test + public void testRevokeNotificationDelegate() { + mHelper.setNotificationDelegate(PKG_O, UID_O, "other", 53); + mHelper.revokeNotificationDelegate(PKG_O, UID_O); + + assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O)); + } + + @Test + public void testRevokeNotificationDelegate_noDelegateExistsNoCrash() { + mHelper.revokeNotificationDelegate(PKG_O, UID_O); + + assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O)); + } + + @Test + public void testToggleNotificationDelegate() { + mHelper.setNotificationDelegate(PKG_O, UID_O, "other", 53); + mHelper.toggleNotificationDelegate(PKG_O, UID_O, false); + + assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O)); + + mHelper.toggleNotificationDelegate(PKG_O, UID_O, true); + assertEquals("other", mHelper.getNotificationDelegate(PKG_O, UID_O)); + } + + @Test + public void testToggleNotificationDelegate_noDelegateExistsNoCrash() { + mHelper.toggleNotificationDelegate(PKG_O, UID_O, false); + assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O)); + + mHelper.toggleNotificationDelegate(PKG_O, UID_O, true); + assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O)); + } + + @Test + public void testIsDelegateAllowed_noSource() { + assertFalse(mHelper.isDelegateAllowed("does not exist", -1, "whatever", 0)); + } + + @Test + public void testIsDelegateAllowed_noDelegate() { + mHelper.setImportance(PKG_O, UID_O, IMPORTANCE_UNSPECIFIED); + + assertFalse(mHelper.isDelegateAllowed(PKG_O, UID_O, "whatever", 0)); + } + + @Test + public void testIsDelegateAllowed_delegateDisabledByApp() { + mHelper.setNotificationDelegate(PKG_O, UID_O, "other", 53); + mHelper.revokeNotificationDelegate(PKG_O, UID_O); + + assertFalse(mHelper.isDelegateAllowed(PKG_O, UID_O, "other", 53)); + } + + @Test + public void testIsDelegateAllowed_wrongDelegate() { + mHelper.setNotificationDelegate(PKG_O, UID_O, "other", 53); + mHelper.revokeNotificationDelegate(PKG_O, UID_O); + + assertFalse(mHelper.isDelegateAllowed(PKG_O, UID_O, "banana", 27)); + } + + @Test + public void testIsDelegateAllowed_delegateDisabledByUser() { + mHelper.setNotificationDelegate(PKG_O, UID_O, "other", 53); + mHelper.toggleNotificationDelegate(PKG_O, UID_O, false); + + assertFalse(mHelper.isDelegateAllowed(PKG_O, UID_O, "other", 53)); + } + + @Test + public void testIsDelegateAllowed() { + mHelper.setNotificationDelegate(PKG_O, UID_O, "other", 53); + + assertTrue(mHelper.isDelegateAllowed(PKG_O, UID_O, "other", 53)); + } + + @Test + public void testDelegateXml_noDelegate() throws Exception { + mHelper.setImportance(PKG_O, UID_O, IMPORTANCE_UNSPECIFIED); + + ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false); + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper); + loadStreamXml(baos, false); + + assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O)); + } + + @Test + public void testDelegateXml_delegate() throws Exception { + mHelper.setNotificationDelegate(PKG_O, UID_O, "other", 53); + + ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false); + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper); + loadStreamXml(baos, false); + + assertEquals("other", mHelper.getNotificationDelegate(PKG_O, UID_O)); + } + + @Test + public void testDelegateXml_disabledDelegate() throws Exception { + mHelper.setNotificationDelegate(PKG_O, UID_O, "other", 53); + mHelper.revokeNotificationDelegate(PKG_O, UID_O); + + ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false); + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper); + loadStreamXml(baos, false); + + assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O)); + } + + @Test + public void testDelegateXml_userDisabledDelegate() throws Exception { + mHelper.setNotificationDelegate(PKG_O, UID_O, "other", 53); + mHelper.toggleNotificationDelegate(PKG_O, UID_O, false); + + ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false); + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper); + loadStreamXml(baos, false); + + // appears disabled + assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O)); + + // but was loaded and can be toggled back on + mHelper.toggleNotificationDelegate(PKG_O, UID_O, true); + assertEquals("other", mHelper.getNotificationDelegate(PKG_O, UID_O)); + } + + @Test + public void testDelegateXml_entirelyDisabledDelegate() throws Exception { + mHelper.setNotificationDelegate(PKG_O, UID_O, "other", 53); + mHelper.toggleNotificationDelegate(PKG_O, UID_O, false); + mHelper.revokeNotificationDelegate(PKG_O, UID_O); + + ByteArrayOutputStream baos = writeXmlAndPurge(PKG_O, UID_O, false); + mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper); + loadStreamXml(baos, false); + + // appears disabled + assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O)); + + mHelper.setNotificationDelegate(PKG_O, UID_O, "other", 53); + assertNull(mHelper.getNotificationDelegate(PKG_O, UID_O)); + + mHelper.toggleNotificationDelegate(PKG_O, UID_O, true); + assertEquals("other", mHelper.getNotificationDelegate(PKG_O, UID_O)); + } }