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
diff --git a/api/current.txt b/api/current.txt
index f8825d9..d445b57 100755
--- a/api/current.txt
+++ b/api/current.txt
@@ -5680,6 +5680,7 @@
   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 @@
     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";
@@ -39644,10 +39649,12 @@
     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 3171e3e..4f004d93 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -165,4 +165,9 @@
     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 4b25b8b..b96b39d 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -18,6 +18,7 @@
 
 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 @@
     }
 
     /**
-     * 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 @@
     }
 
     /**
+     * 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 @@
     {
         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 @@
                         + 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 @@
     }
 
     /**
+     * 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 dd97d52..84826e0 100644
--- a/core/java/android/service/notification/StatusBarNotification.java
+++ b/core/java/android/service/notification/StatusBarNotification.java
@@ -18,7 +18,7 @@
 
 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 @@
         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 @@
         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 426a0c15..fd32b5a 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 @@
         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 ce71dd2..03b7652 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_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.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 @@
                 // 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 @@
             // 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 @@
             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 @@
         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 @@
                 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 @@
         }
 
         @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 @@
             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 @@
         }
     }
 
-    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 @@
         // 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 @@
                     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 @@
         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 @@
         }
     }
 
-    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 @@
     }
 
     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 @@
         }
     }
 
+    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 432d17c..593e7cd 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 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 @@
 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 @@
     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 @@
             }
             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 @@
                                     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 @@
         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 @@
                     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 @@
                                 || 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 @@
                         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 @@
      * 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 @@
                 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 @@
     }
 
     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 @@
         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 0ff124e..a1b3b98 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.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 @@
     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 @@
                     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 @@
         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 @@
 
         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 @@
 
         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 @@
 
         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 @@
         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 @@
 
         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 @@
             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 @@
             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 @@
             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 @@
 
     @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 @@
 
     @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 @@
 
     @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 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 @@
     @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 @@
         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 @@
         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 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 @@
                 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 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 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 @@
     @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 @@
     @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 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 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 @@
         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 @@
         // 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 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 @@
                         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 @@
                 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 @@
         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 @@
         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 @@
         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 @@
     @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 @@
                 .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 @@
                 .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 @@
     }
 
     @Test
-    public void testMybeRecordInterruptionLocked_doesNotRecordTwice()
+    public void testMaybeRecordInterruptionLocked_doesNotRecordTwice()
             throws RemoteException {
         final NotificationRecord r = generateNotificationRecord(
                 mTestNotificationChannel, 1, null, true);
@@ -3373,4 +3379,78 @@
         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 73adf25..750345b 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 @@
     @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 @@
                 .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 @@
         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));
+    }
 }