diff options
9 files changed, 694 insertions, 117 deletions
diff --git a/api/current.txt b/api/current.txt index d3ef74fcde96..d82dcad9f7bc 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<android.app.NotificationChannelGroup> getNotificationChannelGroups(); method public java.util.List<android.app.NotificationChannel> 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"; @@ -39652,10 +39657,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 3171e3e3b992..4f004d93e3bf 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 4b25b8b6e1e0..b96b39df9aa0 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. * @@ -376,6 +377,42 @@ public class NotificationManager { } /** + * 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 */ @UnsupportedAppUsage @@ -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) { @@ -474,6 +516,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. * * This can be used to rename an existing group. diff --git a/core/java/android/service/notification/StatusBarNotification.java b/core/java/android/service/notification/StatusBarNotification.java index dd97d524d829..84826e0f6824 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. + *<p> + * 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 426a0c157aec..fd32b5a76f15 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 ce71dd2ec9ad..03b7652e32ec 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<ComponentName> 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<ComponentName> 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(); @@ -2234,6 +2238,60 @@ public class NotificationManagerService extends SystemService { } @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 { enforceSystemOrSystemUI("Caller not system or systemui"); @@ -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; + } + + 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; } - return callingUid; + + 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 432d17c821f2..593e7cdf4d66 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<String, NotificationChannel> channels = new ArrayMap<>(); Map<String, NotificationChannelGroup> 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 0ff124e4ce7a..a1b3b988397c 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 73adf25cb3ec..750345be1c1d 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)); + } } |