New API to request a list of current notifications.
The ACCESS_NOTIFICATIONS permission is signature|system only.
Change-Id: I41338230aee9611117cbdac251c1b6b6c3cebf00
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 241a9ae..34708ab 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -40,6 +40,10 @@
public static final int MODE_IGNORED = 1;
public static final int MODE_ERRORED = 2;
+ // when adding one of these:
+ // - increment _NUM_OP
+ // - add rows to sOpToSwitch, sOpNames, sOpPerms
+ // - add descriptive strings to Settings/res/values/arrays.xml
public static final int OP_NONE = -1;
public static final int OP_COARSE_LOCATION = 0;
public static final int OP_FINE_LOCATION = 1;
@@ -66,8 +70,9 @@
public static final int OP_WRITE_ICC_SMS = 22;
public static final int OP_WRITE_SETTINGS = 23;
public static final int OP_SYSTEM_ALERT_WINDOW = 24;
+ public static final int OP_ACCESS_NOTIFICATIONS = 25;
/** @hide */
- public static final int _NUM_OP = 25;
+ public static final int _NUM_OP = 26;
/**
* This maps each operation to the operation that serves as the
@@ -103,6 +108,7 @@
OP_WRITE_SMS,
OP_WRITE_SETTINGS,
OP_SYSTEM_ALERT_WINDOW,
+ OP_ACCESS_NOTIFICATIONS,
};
/**
@@ -135,6 +141,7 @@
"WRITE_ICC_SMS",
"WRITE_SETTINGS",
"SYSTEM_ALERT_WINDOW",
+ "ACCESS_NOTIFICATIONS",
};
/**
@@ -167,6 +174,7 @@
android.Manifest.permission.WRITE_SMS,
android.Manifest.permission.WRITE_SETTINGS,
android.Manifest.permission.SYSTEM_ALERT_WINDOW,
+ android.Manifest.permission.ACCESS_NOTIFICATIONS,
};
public static int opToSwitch(int op) {
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index d400eba..bb10f62 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -21,6 +21,8 @@
import android.app.Notification;
import android.content.Intent;
+import com.android.internal.statusbar.StatusBarNotification;
+
/** {@hide} */
interface INotificationManager
{
@@ -34,5 +36,7 @@
void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled);
boolean areNotificationsEnabledForPackage(String pkg, int uid);
+
+ StatusBarNotification[] getActiveNotifications(String callingPkg);
}
diff --git a/core/java/com/android/internal/statusbar/StatusBarNotification.java b/core/java/com/android/internal/statusbar/StatusBarNotification.java
index a91aa3c..23e87fc 100644
--- a/core/java/com/android/internal/statusbar/StatusBarNotification.java
+++ b/core/java/com/android/internal/statusbar/StatusBarNotification.java
@@ -21,23 +21,13 @@
import android.os.Parcelable;
import android.os.UserHandle;
-/*
-boolean clearable = !n.ongoingEvent && ((notification.flags & Notification.FLAG_NO_CLEAR) == 0);
-
-
-// TODO: make this restriction do something smarter like never fill
-// more than two screens. "Why would anyone need more than 80 characters." :-/
-final int maxTickerLen = 80;
-if (truncatedTicker != null && truncatedTicker.length() > maxTickerLen) {
- truncatedTicker = truncatedTicker.subSequence(0, maxTickerLen);
-}
-*/
-
/**
- * Class encapsulating a Notification. Sent by the NotificationManagerService to the IStatusBar (in System UI).
+ * Class encapsulating a Notification. Sent by the NotificationManagerService to clients including
+ * the IStatusBar (in System UI).
*/
public class StatusBarNotification implements Parcelable {
public final String pkg;
+ public final String basePkg;
public final int id;
public final String tag;
public final int uid;
@@ -47,6 +37,7 @@
public final Notification notification;
public final int score;
public final UserHandle user;
+ public final long postTime;
/** This is temporarily needed for the JB MR1 PDK. */
@Deprecated
@@ -57,10 +48,23 @@
public StatusBarNotification(String pkg, int id, String tag, int uid, int initialPid, int score,
Notification notification, UserHandle user) {
+ this(pkg, null, id, tag, uid, initialPid, score, notification, user);
+ }
+
+ public StatusBarNotification(String pkg, String basePkg, int id, String tag, int uid,
+ int initialPid, int score, Notification notification, UserHandle user) {
+ this(pkg, basePkg, id, tag, uid, initialPid, score, notification, user,
+ System.currentTimeMillis());
+ }
+
+ public StatusBarNotification(String pkg, String basePkg, int id, String tag, int uid,
+ int initialPid, int score, Notification notification, UserHandle user,
+ long postTime) {
if (pkg == null) throw new NullPointerException();
if (notification == null) throw new NullPointerException();
this.pkg = pkg;
+ this.basePkg = pkg;
this.id = id;
this.tag = tag;
this.uid = uid;
@@ -69,10 +73,13 @@
this.notification = notification;
this.user = user;
this.notification.setUser(user);
+
+ this.postTime = postTime;
}
public StatusBarNotification(Parcel in) {
this.pkg = in.readString();
+ this.basePkg = in.readString();
this.id = in.readInt();
if (in.readInt() != 0) {
this.tag = in.readString();
@@ -84,11 +91,13 @@
this.score = in.readInt();
this.notification = new Notification(in);
this.user = UserHandle.readFromParcel(in);
- this.notification.setUser(user);
+ this.notification.setUser(this.user);
+ this.postTime = in.readLong();
}
public void writeToParcel(Parcel out, int flags) {
out.writeString(this.pkg);
+ out.writeString(this.basePkg);
out.writeInt(this.id);
if (this.tag != null) {
out.writeInt(1);
@@ -101,6 +110,8 @@
out.writeInt(this.score);
this.notification.writeToParcel(out, flags);
user.writeToParcel(out, flags);
+
+ out.writeLong(this.postTime);
}
public int describeContents() {
@@ -123,14 +134,17 @@
@Override
public StatusBarNotification clone() {
- return new StatusBarNotification(this.pkg, this.id, this.tag, this.uid, this.initialPid,
- this.score, this.notification.clone(), this.user);
+ return new StatusBarNotification(this.pkg, this.basePkg,
+ this.id, this.tag, this.uid, this.initialPid,
+ this.score, this.notification.clone(), this.user, this.postTime);
}
@Override
public String toString() {
- return "StatusBarNotification(pkg=" + pkg + " id=" + id + " tag=" + tag + " score=" + score
- + " notn=" + notification + " user=" + user + ")";
+ return String.format(
+ "StatusBarNotification(pkg=%s user=%s id=%d tag=%s score=%d: %s)",
+ this.pkg, this.user, this.id, this.tag,
+ this.score, this.notification);
}
public boolean isOngoing() {
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 5783bf6..5d0614c 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2175,6 +2175,14 @@
android:description="@string/permdesc_updateLock"
android:protectionLevel="signatureOrSystem" />
+ <!-- Allows an application to read the current set of notifications, including
+ any metadata and intents attached.
+ @hide -->
+ <permission android:name="android.permission.ACCESS_NOTIFICATIONS"
+ android:label="@string/permlab_accessNotifications"
+ android:description="@string/permdesc_accessNotifications"
+ android:protectionLevel="signature|system" />
+
<!-- The system process is explicitly the only one allowed to launch the
confirmation UI for full backup/restore -->
<uses-permission android:name="android.permission.CONFIRM_FULL_BACKUP"/>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 22f4e2e..00c6f6d 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1806,6 +1806,11 @@
<!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
<string name="permdesc_modifyNetworkAccounting">Allows the app to modify how network usage is accounted against apps. Not for use by normal apps.</string>
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_accessNotifications">access notifications</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_accessNotifications">Allows the app to retrieve, examine, and clear notifications, including those posted by other apps.</string>
+
<!-- Policy administration -->
<!-- Title of policy access to limiting the user's password choices -->
diff --git a/services/java/com/android/server/NotificationManagerService.java b/services/java/com/android/server/NotificationManagerService.java
index d121653..1a2c3de 100644
--- a/services/java/com/android/server/NotificationManagerService.java
+++ b/services/java/com/android/server/NotificationManagerService.java
@@ -49,6 +49,8 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -77,9 +79,12 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
import libcore.io.IoUtils;
@@ -169,6 +174,71 @@
private static final String TAG_PACKAGE = "package";
private static final String ATTR_NAME = "name";
+ private static class Archive {
+ static final int BUFFER_SIZE = 1000;
+ ArrayDeque<StatusBarNotification> mBuffer = new ArrayDeque<StatusBarNotification>(BUFFER_SIZE);
+
+ public Archive() {
+
+ }
+ public void record(StatusBarNotification nr) {
+ if (mBuffer.size() == BUFFER_SIZE) {
+ mBuffer.removeFirst();
+ }
+ mBuffer.addLast(nr);
+ }
+
+ public void clear() {
+ mBuffer.clear();
+ }
+
+ public Iterator<StatusBarNotification> descendingIterator() {
+ return mBuffer.descendingIterator();
+ }
+ public Iterator<StatusBarNotification> ascendingIterator() {
+ return mBuffer.iterator();
+ }
+ public Iterator<StatusBarNotification> filter(
+ final Iterator<StatusBarNotification> iter, final String pkg, final int userId) {
+ return new Iterator<StatusBarNotification>() {
+ StatusBarNotification mNext = findNext();
+
+ private StatusBarNotification findNext() {
+ while (iter.hasNext()) {
+ StatusBarNotification nr = iter.next();
+ if ((pkg == null || nr.pkg == pkg)
+ && (userId == UserHandle.USER_ALL || nr.getUserId() == userId)) {
+ return nr;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return mNext == null;
+ }
+
+ @Override
+ public StatusBarNotification next() {
+ StatusBarNotification next = mNext;
+ if (next == null) {
+ throw new NoSuchElementException();
+ }
+ mNext = findNext();
+ return next;
+ }
+
+ @Override
+ public void remove() {
+ iter.remove();
+ }
+ };
+ }
+ }
+
+ Archive mArchive = new Archive();
+
private void loadBlockDb() {
synchronized(mBlockedPackages) {
if (mPolicyFile == null) {
@@ -275,44 +345,53 @@
}
}
- private static final class NotificationRecord
+ public StatusBarNotification[] getActiveNotifications(String callingPkg) {
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS,
+ "NotificationManagerService");
+
+ StatusBarNotification[] tmp = null;
+ int userId = UserHandle.getCallingUserId();
+ int uid = Binder.getCallingUid();
+
+ if (mAppOps.noteOpNoThrow(AppOpsManager.OP_ACCESS_NOTIFICATIONS, uid, callingPkg)
+ == AppOpsManager.MODE_ALLOWED) {
+ synchronized (mNotificationList) {
+ tmp = new StatusBarNotification[mNotificationList.size()];
+ final int N = mNotificationList.size();
+ for (int i=0; i<N; i++) {
+ tmp[i] = mNotificationList.get(i).sbn;
+ }
+ }
+ }
+ return tmp;
+ }
+
+ public static final class NotificationRecord
{
- final String pkg;
- final String basePkg;
- final String tag;
- final int id;
- final int uid;
- final int initialPid;
- final int userId;
- final Notification notification;
- final int score;
+ final StatusBarNotification sbn;
IBinder statusBarKey;
- NotificationRecord(String pkg, String basePkg, String tag, int id, int uid, int initialPid,
- int userId, int score, Notification notification)
+ NotificationRecord(StatusBarNotification sbn)
{
- this.pkg = pkg;
- this.basePkg = basePkg;
- this.tag = tag;
- this.id = id;
- this.uid = uid;
- this.initialPid = initialPid;
- this.userId = userId;
- this.score = score;
- this.notification = notification;
+ this.sbn = sbn;
}
+ public Notification getNotification() { return sbn.notification; }
+ public int getFlags() { return sbn.notification.flags; }
+ public int getUserId() { return sbn.getUserId(); }
+
void dump(PrintWriter pw, String prefix, Context baseContext) {
+ final Notification notification = sbn.notification;
pw.println(prefix + this);
pw.println(prefix + " icon=0x" + Integer.toHexString(notification.icon)
- + " / " + idDebugString(baseContext, this.pkg, notification.icon));
+ + " / " + idDebugString(baseContext, this.sbn.pkg, notification.icon));
pw.println(prefix + " pri=" + notification.priority);
- pw.println(prefix + " score=" + this.score);
+ pw.println(prefix + " score=" + this.sbn.score);
pw.println(prefix + " contentIntent=" + notification.contentIntent);
pw.println(prefix + " deleteIntent=" + notification.deleteIntent);
pw.println(prefix + " tickerText=" + notification.tickerText);
pw.println(prefix + " contentView=" + notification.contentView);
- pw.println(prefix + " uid=" + uid + " userId=" + userId);
+ pw.println(prefix + " uid=" + this.sbn.uid + " userId=" + this.sbn.getUserId());
pw.println(prefix + " defaults=0x" + Integer.toHexString(notification.defaults));
pw.println(prefix + " flags=0x" + Integer.toHexString(notification.flags));
pw.println(prefix + " sound=" + notification.sound);
@@ -323,15 +402,12 @@
}
@Override
- public final String toString()
- {
- return "NotificationRecord{"
- + Integer.toHexString(System.identityHashCode(this))
- + " pkg=" + pkg
- + " id=" + Integer.toHexString(id)
- + " tag=" + tag
- + " score=" + score
- + "}";
+ public final String toString() {
+ return String.format(
+ "NotificationRecord(0x%08x: pkg=%s user=%s id=%d tag=%s score=%d: %s)",
+ System.identityHashCode(this),
+ this.sbn.pkg, this.sbn.user, this.sbn.id, this.sbn.tag,
+ this.sbn.score, this.sbn.notification);
}
}
@@ -916,7 +992,7 @@
final int N = mNotificationList.size();
for (int i=0; i<N; i++) {
final NotificationRecord r = mNotificationList.get(i);
- if (r.pkg.equals(pkg) && r.userId == userId) {
+ if (r.sbn.pkg.equals(pkg) && r.sbn.getUserId() == userId) {
count++;
if (count >= MAX_PACKAGE_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count
@@ -986,10 +1062,9 @@
final boolean canInterrupt = (score >= SCORE_INTERRUPTION_THRESHOLD);
synchronized (mNotificationList) {
- NotificationRecord r = new NotificationRecord(pkg, basePkg, tag, id,
- callingUid, callingPid, userId,
- score,
- notification);
+ final StatusBarNotification n = new StatusBarNotification(
+ pkg, id, tag, callingUid, callingPid, score, notification, user);
+ NotificationRecord r = new NotificationRecord(n);
NotificationRecord old = null;
int index = indexOfNotificationLocked(pkg, tag, id, userId);
@@ -1001,7 +1076,7 @@
// Make sure we don't lose the foreground service state.
if (old != null) {
notification.flags |=
- old.notification.flags&Notification.FLAG_FOREGROUND_SERVICE;
+ old.getNotification().flags&Notification.FLAG_FOREGROUND_SERVICE;
}
}
@@ -1021,8 +1096,6 @@
}
if (notification.icon != 0) {
- final StatusBarNotification n = new StatusBarNotification(
- pkg, id, tag, r.uid, r.initialPid, score, notification, user);
if (old != null && old.statusBarKey != null) {
r.statusBarKey = old.statusBarKey;
long identity = Binder.clearCallingIdentity();
@@ -1049,6 +1122,9 @@
if (currentUser == userId) {
sendAccessibilityEvent(notification, pkg);
}
+
+ // finally, keep some of this information around for later use
+ mArchive.record(n);
} else {
Slog.e(TAG, "Ignoring notification with icon==0: " + notification);
if (old != null && old.statusBarKey != null) {
@@ -1060,14 +1136,15 @@
Binder.restoreCallingIdentity(identity);
}
}
+ return; // do not play sounds, show lights, etc. for invalid notifications
}
// If we're not supposed to beep, vibrate, etc. then don't.
if (((mDisabledNotifications & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) == 0)
&& (!(old != null
&& (notification.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0 ))
- && (r.userId == UserHandle.USER_ALL ||
- (r.userId == userId && r.userId == currentUser))
+ && (r.getUserId() == UserHandle.USER_ALL ||
+ (r.getUserId() == userId && r.getUserId() == currentUser))
&& canInterrupt
&& mSystemReady) {
@@ -1143,7 +1220,7 @@
// does not have the VIBRATE permission.
long identity = Binder.clearCallingIdentity();
try {
- mVibrator.vibrate(r.uid, r.basePkg,
+ mVibrator.vibrate(r.sbn.uid, r.sbn.basePkg,
useDefaultVibrate ? mDefaultVibrationPattern
: mFallbackVibrationPattern,
((notification.flags & Notification.FLAG_INSISTENT) != 0) ? 0: -1);
@@ -1152,14 +1229,12 @@
}
} else if (notification.vibrate.length > 1) {
// If you want your own vibration pattern, you need the VIBRATE permission
- mVibrator.vibrate(r.uid, r.basePkg, notification.vibrate,
+ mVibrator.vibrate(r.sbn.uid, r.sbn.basePkg, notification.vibrate,
((notification.flags & Notification.FLAG_INSISTENT) != 0) ? 0: -1);
}
}
}
- // this option doesn't shut off the lights
-
// light
// the most recent thing gets the light
mLights.remove(old);
@@ -1174,7 +1249,7 @@
updateLightsLocked();
} else {
if (old != null
- && ((old.notification.flags & Notification.FLAG_SHOW_LIGHTS) != 0)) {
+ && ((old.getFlags() & Notification.FLAG_SHOW_LIGHTS) != 0)) {
updateLightsLocked();
}
}
@@ -1205,19 +1280,19 @@
private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete) {
// tell the app
if (sendDelete) {
- if (r.notification.deleteIntent != null) {
+ if (r.getNotification().deleteIntent != null) {
try {
- r.notification.deleteIntent.send();
+ r.getNotification().deleteIntent.send();
} catch (PendingIntent.CanceledException ex) {
// do nothing - there's no relevant way to recover, and
// no reason to let this propagate
- Slog.w(TAG, "canceled PendingIntent for " + r.pkg, ex);
+ Slog.w(TAG, "canceled PendingIntent for " + r.sbn.pkg, ex);
}
}
}
// status bar
- if (r.notification.icon != 0) {
+ if (r.getNotification().icon != 0) {
long identity = Binder.clearCallingIdentity();
try {
mStatusBar.removeNotification(r.statusBarKey);
@@ -1276,10 +1351,10 @@
if (index >= 0) {
NotificationRecord r = mNotificationList.get(index);
- if ((r.notification.flags & mustHaveFlags) != mustHaveFlags) {
+ if ((r.getNotification().flags & mustHaveFlags) != mustHaveFlags) {
return;
}
- if ((r.notification.flags & mustNotHaveFlags) != 0) {
+ if ((r.getNotification().flags & mustNotHaveFlags) != 0) {
return;
}
@@ -1300,9 +1375,9 @@
// looking for USER_ALL notifications? match everything
userId == UserHandle.USER_ALL
// a notification sent to USER_ALL matches any query
- || r.userId == UserHandle.USER_ALL
+ || r.getUserId() == UserHandle.USER_ALL
// an exact user match
- || r.userId == userId;
+ || r.getUserId() == userId;
}
/**
@@ -1323,16 +1398,16 @@
continue;
}
// Don't remove notifications to all, if there's no package name specified
- if (r.userId == UserHandle.USER_ALL && pkg == null) {
+ if (r.getUserId() == UserHandle.USER_ALL && pkg == null) {
continue;
}
- if ((r.notification.flags & mustHaveFlags) != mustHaveFlags) {
+ if ((r.getFlags() & mustHaveFlags) != mustHaveFlags) {
continue;
}
- if ((r.notification.flags & mustNotHaveFlags) != 0) {
+ if ((r.getFlags() & mustNotHaveFlags) != 0) {
continue;
}
- if (pkg != null && !r.pkg.equals(pkg)) {
+ if (pkg != null && !r.sbn.pkg.equals(pkg)) {
continue;
}
canceledSomething = true;
@@ -1405,7 +1480,7 @@
continue;
}
- if ((r.notification.flags & (Notification.FLAG_ONGOING_EVENT
+ if ((r.getFlags() & (Notification.FLAG_ONGOING_EVENT
| Notification.FLAG_NO_CLEAR)) == 0) {
mNotificationList.remove(i);
cancelNotificationLocked(r, true);
@@ -1432,10 +1507,11 @@
if (mLedNotification == null || mInCall || mScreenOn) {
mNotificationLight.turnOff();
} else {
- int ledARGB = mLedNotification.notification.ledARGB;
- int ledOnMS = mLedNotification.notification.ledOnMS;
- int ledOffMS = mLedNotification.notification.ledOffMS;
- if ((mLedNotification.notification.defaults & Notification.DEFAULT_LIGHTS) != 0) {
+ final Notification ledno = mLedNotification.sbn.notification;
+ int ledARGB = ledno.ledARGB;
+ int ledOnMS = ledno.ledOnMS;
+ int ledOffMS = ledno.ledOffMS;
+ if ((ledno.defaults & Notification.DEFAULT_LIGHTS) != 0) {
ledARGB = mDefaultNotificationColor;
ledOnMS = mDefaultNotificationLedOn;
ledOffMS = mDefaultNotificationLedOff;
@@ -1455,19 +1531,19 @@
final int len = list.size();
for (int i=0; i<len; i++) {
NotificationRecord r = list.get(i);
- if (!notificationMatchesUserId(r, userId) || r.id != id) {
+ if (!notificationMatchesUserId(r, userId) || r.sbn.id != id) {
continue;
}
if (tag == null) {
- if (r.tag != null) {
+ if (r.sbn.tag != null) {
continue;
}
} else {
- if (!tag.equals(r.tag)) {
+ if (!tag.equals(r.sbn.tag)) {
continue;
}
}
- if (r.pkg.equals(pkg)) {
+ if (r.sbn.pkg.equals(pkg)) {
return i;
}
}