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/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);
+ }
}
}