summaryrefslogtreecommitdiffstats
path: root/services/core/java/com/android/server/notification/NotificationManagerService.java
diff options
context:
space:
mode:
Diffstat (limited to 'services/core/java/com/android/server/notification/NotificationManagerService.java')
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java2427
1 files changed, 2427 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
new file mode 100644
index 0000000..db4cf31d
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -0,0 +1,2427 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
+import static org.xmlpull.v1.XmlPullParser.END_TAG;
+import static org.xmlpull.v1.XmlPullParser.START_TAG;
+
+import android.app.ActivityManager;
+import android.app.ActivityManagerNative;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.app.IActivityManager;
+import android.app.INotificationManager;
+import android.app.ITransientNotification;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+import android.media.AudioManager;
+import android.media.IRingtonePlayer;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.service.notification.INotificationListener;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.AtomicFile;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.Slog;
+import android.util.Xml;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.Toast;
+
+import com.android.internal.R;
+
+import com.android.internal.notification.NotificationScorer;
+import com.android.server.EventLogTags;
+import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.server.SystemService;
+import com.android.server.lights.Light;
+import com.android.server.lights.LightsManager;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.Array;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+import libcore.io.IoUtils;
+
+/** {@hide} */
+public class NotificationManagerService extends SystemService {
+ static final String TAG = "NotificationService";
+ static final boolean DBG = false;
+
+ static final int MAX_PACKAGE_NOTIFICATIONS = 50;
+
+ // message codes
+ static final int MESSAGE_TIMEOUT = 2;
+
+ static final int LONG_DELAY = 3500; // 3.5 seconds
+ static final int SHORT_DELAY = 2000; // 2 seconds
+
+ static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250};
+ static final int VIBRATE_PATTERN_MAXLEN = 8 * 2 + 1; // up to eight bumps
+
+ static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_NOTIFICATION;
+ static final boolean SCORE_ONGOING_HIGHER = false;
+
+ static final int JUNK_SCORE = -1000;
+ static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10;
+ static final int SCORE_DISPLAY_THRESHOLD = Notification.PRIORITY_MIN * NOTIFICATION_PRIORITY_MULTIPLIER;
+
+ // Notifications with scores below this will not interrupt the user, either via LED or
+ // sound or vibration
+ static final int SCORE_INTERRUPTION_THRESHOLD =
+ Notification.PRIORITY_LOW * NOTIFICATION_PRIORITY_MULTIPLIER;
+
+ static final boolean ENABLE_BLOCKED_NOTIFICATIONS = true;
+ static final boolean ENABLE_BLOCKED_TOASTS = true;
+
+ static final String ENABLED_NOTIFICATION_LISTENERS_SEPARATOR = ":";
+
+ private IActivityManager mAm;
+ AudioManager mAudioManager;
+ StatusBarManagerInternal mStatusBar;
+ Vibrator mVibrator;
+
+ final IBinder mForegroundToken = new Binder();
+ private WorkerHandler mHandler;
+
+ private Light mNotificationLight;
+ Light mAttentionLight;
+ private int mDefaultNotificationColor;
+ private int mDefaultNotificationLedOn;
+
+ private int mDefaultNotificationLedOff;
+ private long[] mDefaultVibrationPattern;
+
+ private long[] mFallbackVibrationPattern;
+ boolean mSystemReady;
+
+ int mDisabledNotifications;
+ NotificationRecord mSoundNotification;
+ NotificationRecord mVibrateNotification;
+
+ // for enabling and disabling notification pulse behavior
+ private boolean mScreenOn = true;
+ private boolean mInCall = false;
+ private boolean mNotificationPulseEnabled;
+
+ // used as a mutex for access to all active notifications & listeners
+ final ArrayList<NotificationRecord> mNotificationList =
+ new ArrayList<NotificationRecord>();
+
+ final ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>();
+
+ ArrayList<NotificationRecord> mLights = new ArrayList<NotificationRecord>();
+ NotificationRecord mLedNotification;
+
+ private AppOpsManager mAppOps;
+
+ // contains connections to all connected listeners, including app services
+ // and system listeners
+ private ArrayList<NotificationListenerInfo> mListeners
+ = new ArrayList<NotificationListenerInfo>();
+ // things that will be put into mListeners as soon as they're ready
+ private ArrayList<String> mServicesBinding = new ArrayList<String>();
+ // lists the component names of all enabled (and therefore connected) listener
+ // app services for the current user only
+ private HashSet<ComponentName> mEnabledListenersForCurrentUser
+ = new HashSet<ComponentName>();
+ // Just the packages from mEnabledListenersForCurrentUser
+ private HashSet<String> mEnabledListenerPackageNames = new HashSet<String>();
+
+ // Notification control database. For now just contains disabled packages.
+ private AtomicFile mPolicyFile;
+ private HashSet<String> mBlockedPackages = new HashSet<String>();
+
+ private static final int DB_VERSION = 1;
+
+ private static final String TAG_BODY = "notification-policy";
+ private static final String ATTR_VERSION = "version";
+
+ private static final String TAG_BLOCKED_PKGS = "blocked-packages";
+ private static final String TAG_PACKAGE = "package";
+ private static final String ATTR_NAME = "name";
+
+ final ArrayList<NotificationScorer> mScorers = new ArrayList<NotificationScorer>();
+
+ private class NotificationListenerInfo implements IBinder.DeathRecipient {
+ INotificationListener listener;
+ ComponentName component;
+ int userid;
+ boolean isSystem;
+ ServiceConnection connection;
+
+ public NotificationListenerInfo(INotificationListener listener, ComponentName component,
+ int userid, boolean isSystem) {
+ this.listener = listener;
+ this.component = component;
+ this.userid = userid;
+ this.isSystem = isSystem;
+ this.connection = null;
+ }
+
+ public NotificationListenerInfo(INotificationListener listener, ComponentName component,
+ int userid, ServiceConnection connection) {
+ this.listener = listener;
+ this.component = component;
+ this.userid = userid;
+ this.isSystem = false;
+ this.connection = connection;
+ }
+
+ boolean enabledAndUserMatches(StatusBarNotification sbn) {
+ final int nid = sbn.getUserId();
+ if (!isEnabledForCurrentUser()) {
+ return false;
+ }
+ if (this.userid == UserHandle.USER_ALL) return true;
+ return (nid == UserHandle.USER_ALL || nid == this.userid);
+ }
+
+ public void notifyPostedIfUserMatch(StatusBarNotification sbn) {
+ if (!enabledAndUserMatches(sbn)) {
+ return;
+ }
+ try {
+ listener.onNotificationPosted(sbn);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "unable to notify listener (posted): " + listener, ex);
+ }
+ }
+
+ public void notifyRemovedIfUserMatch(StatusBarNotification sbn) {
+ if (!enabledAndUserMatches(sbn)) return;
+ try {
+ listener.onNotificationRemoved(sbn);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "unable to notify listener (removed): " + listener, ex);
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ if (connection == null) {
+ // This is not a service; it won't be recreated. We can give up this connection.
+ unregisterListenerImpl(this.listener, this.userid);
+ }
+ }
+
+ /** convenience method for looking in mEnabledListenersForCurrentUser */
+ public boolean isEnabledForCurrentUser() {
+ if (this.isSystem) return true;
+ if (this.connection == null) return false;
+ return mEnabledListenersForCurrentUser.contains(this.component);
+ }
+ }
+
+ private static class Archive {
+ static final int BUFFER_SIZE = 250;
+ ArrayDeque<StatusBarNotification> mBuffer = new ArrayDeque<StatusBarNotification>(BUFFER_SIZE);
+
+ public Archive() {
+ }
+
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ final int N = mBuffer.size();
+ sb.append("Archive (");
+ sb.append(N);
+ sb.append(" notification");
+ sb.append((N==1)?")":"s)");
+ return sb.toString();
+ }
+
+ public void record(StatusBarNotification nr) {
+ if (mBuffer.size() == BUFFER_SIZE) {
+ mBuffer.removeFirst();
+ }
+
+ // We don't want to store the heavy bits of the notification in the archive,
+ // but other clients in the system process might be using the object, so we
+ // store a (lightened) copy.
+ mBuffer.addLast(nr.cloneLight());
+ }
+
+
+ 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.getPackageName() == 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();
+ }
+ };
+ }
+
+ public StatusBarNotification[] getArray(int count) {
+ if (count == 0) count = Archive.BUFFER_SIZE;
+ final StatusBarNotification[] a
+ = new StatusBarNotification[Math.min(count, mBuffer.size())];
+ Iterator<StatusBarNotification> iter = descendingIterator();
+ int i=0;
+ while (iter.hasNext() && i < count) {
+ a[i++] = iter.next();
+ }
+ return a;
+ }
+
+ public StatusBarNotification[] getArray(int count, String pkg, int userId) {
+ if (count == 0) count = Archive.BUFFER_SIZE;
+ final StatusBarNotification[] a
+ = new StatusBarNotification[Math.min(count, mBuffer.size())];
+ Iterator<StatusBarNotification> iter = filter(descendingIterator(), pkg, userId);
+ int i=0;
+ while (iter.hasNext() && i < count) {
+ a[i++] = iter.next();
+ }
+ return a;
+ }
+
+ }
+
+ Archive mArchive = new Archive();
+
+ private void loadBlockDb() {
+ synchronized(mBlockedPackages) {
+ if (mPolicyFile == null) {
+ File dir = new File("/data/system");
+ mPolicyFile = new AtomicFile(new File(dir, "notification_policy.xml"));
+
+ mBlockedPackages.clear();
+
+ FileInputStream infile = null;
+ try {
+ infile = mPolicyFile.openRead();
+ final XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(infile, null);
+
+ int type;
+ String tag;
+ int version = DB_VERSION;
+ while ((type = parser.next()) != END_DOCUMENT) {
+ tag = parser.getName();
+ if (type == START_TAG) {
+ if (TAG_BODY.equals(tag)) {
+ version = Integer.parseInt(
+ parser.getAttributeValue(null, ATTR_VERSION));
+ } else if (TAG_BLOCKED_PKGS.equals(tag)) {
+ while ((type = parser.next()) != END_DOCUMENT) {
+ tag = parser.getName();
+ if (TAG_PACKAGE.equals(tag)) {
+ mBlockedPackages.add(
+ parser.getAttributeValue(null, ATTR_NAME));
+ } else if (TAG_BLOCKED_PKGS.equals(tag) && type == END_TAG) {
+ break;
+ }
+ }
+ }
+ }
+ }
+ } catch (FileNotFoundException e) {
+ // No data yet
+ } catch (IOException e) {
+ Log.wtf(TAG, "Unable to read blocked notifications database", e);
+ } catch (NumberFormatException e) {
+ Log.wtf(TAG, "Unable to parse blocked notifications database", e);
+ } catch (XmlPullParserException e) {
+ Log.wtf(TAG, "Unable to parse blocked notifications database", e);
+ } finally {
+ IoUtils.closeQuietly(infile);
+ }
+ }
+ }
+ }
+
+ /** Use this when you actually want to post a notification or toast.
+ *
+ * Unchecked. Not exposed via Binder, but can be called in the course of enqueue*().
+ */
+ private boolean noteNotificationOp(String pkg, int uid) {
+ if (mAppOps.noteOpNoThrow(AppOpsManager.OP_POST_NOTIFICATION, uid, pkg)
+ != AppOpsManager.MODE_ALLOWED) {
+ Slog.v(TAG, "notifications are disabled by AppOps for " + pkg);
+ return false;
+ }
+ return true;
+ }
+
+ private static String idDebugString(Context baseContext, String packageName, int id) {
+ Context c = null;
+
+ if (packageName != null) {
+ try {
+ c = baseContext.createPackageContext(packageName, 0);
+ } catch (NameNotFoundException e) {
+ c = baseContext;
+ }
+ } else {
+ c = baseContext;
+ }
+
+ String pkg;
+ String type;
+ String name;
+
+ Resources r = c.getResources();
+ try {
+ return r.getResourceName(id);
+ } catch (Resources.NotFoundException e) {
+ return "<name unknown>";
+ }
+ }
+
+
+ /**
+ * Remove notification access for any services that no longer exist.
+ */
+ void disableNonexistentListeners() {
+ int currentUser = ActivityManager.getCurrentUser();
+ String flatIn = Settings.Secure.getStringForUser(
+ getContext().getContentResolver(),
+ Settings.Secure.ENABLED_NOTIFICATION_LISTENERS,
+ currentUser);
+ if (!TextUtils.isEmpty(flatIn)) {
+ if (DBG) Slog.v(TAG, "flat before: " + flatIn);
+ PackageManager pm = getContext().getPackageManager();
+ List<ResolveInfo> installedServices = pm.queryIntentServicesAsUser(
+ new Intent(NotificationListenerService.SERVICE_INTERFACE),
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
+ currentUser);
+
+ Set<ComponentName> installed = new HashSet<ComponentName>();
+ for (int i = 0, count = installedServices.size(); i < count; i++) {
+ ResolveInfo resolveInfo = installedServices.get(i);
+ ServiceInfo info = resolveInfo.serviceInfo;
+
+ if (!android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE.equals(
+ info.permission)) {
+ Slog.w(TAG, "Skipping notification listener service "
+ + info.packageName + "/" + info.name
+ + ": it does not require the permission "
+ + android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE);
+ continue;
+ }
+ installed.add(new ComponentName(info.packageName, info.name));
+ }
+
+ String flatOut = "";
+ if (!installed.isEmpty()) {
+ String[] enabled = flatIn.split(ENABLED_NOTIFICATION_LISTENERS_SEPARATOR);
+ ArrayList<String> remaining = new ArrayList<String>(enabled.length);
+ for (int i = 0; i < enabled.length; i++) {
+ ComponentName enabledComponent = ComponentName.unflattenFromString(enabled[i]);
+ if (installed.contains(enabledComponent)) {
+ remaining.add(enabled[i]);
+ }
+ }
+ flatOut = TextUtils.join(ENABLED_NOTIFICATION_LISTENERS_SEPARATOR, remaining);
+ }
+ if (DBG) Slog.v(TAG, "flat after: " + flatOut);
+ if (!flatIn.equals(flatOut)) {
+ Settings.Secure.putStringForUser(getContext().getContentResolver(),
+ Settings.Secure.ENABLED_NOTIFICATION_LISTENERS,
+ flatOut, currentUser);
+ }
+ }
+ }
+
+ /**
+ * Called whenever packages change, the user switches, or ENABLED_NOTIFICATION_LISTENERS
+ * is altered. (For example in response to USER_SWITCHED in our broadcast receiver)
+ */
+ void rebindListenerServices() {
+ final int currentUser = ActivityManager.getCurrentUser();
+ String flat = Settings.Secure.getStringForUser(
+ getContext().getContentResolver(),
+ Settings.Secure.ENABLED_NOTIFICATION_LISTENERS,
+ currentUser);
+
+ NotificationListenerInfo[] toRemove = new NotificationListenerInfo[mListeners.size()];
+ final ArrayList<ComponentName> toAdd;
+
+ synchronized (mNotificationList) {
+ // unbind and remove all existing listeners
+ toRemove = mListeners.toArray(toRemove);
+
+ toAdd = new ArrayList<ComponentName>();
+ final HashSet<ComponentName> newEnabled = new HashSet<ComponentName>();
+ final HashSet<String> newPackages = new HashSet<String>();
+
+ // decode the list of components
+ if (flat != null) {
+ String[] components = flat.split(ENABLED_NOTIFICATION_LISTENERS_SEPARATOR);
+ for (int i=0; i<components.length; i++) {
+ final ComponentName component
+ = ComponentName.unflattenFromString(components[i]);
+ if (component != null) {
+ newEnabled.add(component);
+ toAdd.add(component);
+ newPackages.add(component.getPackageName());
+ }
+ }
+
+ mEnabledListenersForCurrentUser = newEnabled;
+ mEnabledListenerPackageNames = newPackages;
+ }
+ }
+
+ for (NotificationListenerInfo info : toRemove) {
+ final ComponentName component = info.component;
+ final int oldUser = info.userid;
+ Slog.v(TAG, "disabling notification listener for user " + oldUser + ": " + component);
+ unregisterListenerService(component, info.userid);
+ }
+
+ final int N = toAdd.size();
+ for (int i=0; i<N; i++) {
+ final ComponentName component = toAdd.get(i);
+ Slog.v(TAG, "enabling notification listener for user " + currentUser + ": "
+ + component);
+ registerListenerService(component, currentUser);
+ }
+ }
+
+
+ /**
+ * Version of registerListener that takes the name of a
+ * {@link android.service.notification.NotificationListenerService} to bind to.
+ *
+ * This is the mechanism by which third parties may subscribe to notifications.
+ */
+ private void registerListenerService(final ComponentName name, final int userid) {
+ checkCallerIsSystem();
+
+ if (DBG) Slog.v(TAG, "registerListenerService: " + name + " u=" + userid);
+
+ synchronized (mNotificationList) {
+ final String servicesBindingTag = name.toString() + "/" + userid;
+ if (mServicesBinding.contains(servicesBindingTag)) {
+ // stop registering this thing already! we're working on it
+ return;
+ }
+ mServicesBinding.add(servicesBindingTag);
+
+ final int N = mListeners.size();
+ for (int i=N-1; i>=0; i--) {
+ final NotificationListenerInfo info = mListeners.get(i);
+ if (name.equals(info.component)
+ && info.userid == userid) {
+ // cut old connections
+ if (DBG) Slog.v(TAG, " disconnecting old listener: " + info.listener);
+ mListeners.remove(i);
+ if (info.connection != null) {
+ getContext().unbindService(info.connection);
+ }
+ }
+ }
+
+ Intent intent = new Intent(NotificationListenerService.SERVICE_INTERFACE);
+ intent.setComponent(name);
+
+ intent.putExtra(Intent.EXTRA_CLIENT_LABEL,
+ R.string.notification_listener_binding_label);
+
+ final PendingIntent pendingIntent = PendingIntent.getActivity(
+ getContext(), 0, new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS), 0);
+ intent.putExtra(Intent.EXTRA_CLIENT_INTENT, pendingIntent);
+
+ try {
+ if (DBG) Slog.v(TAG, "binding: " + intent);
+ if (!getContext().bindServiceAsUser(intent,
+ new ServiceConnection() {
+ INotificationListener mListener;
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ synchronized (mNotificationList) {
+ mServicesBinding.remove(servicesBindingTag);
+ try {
+ mListener = INotificationListener.Stub.asInterface(service);
+ NotificationListenerInfo info
+ = new NotificationListenerInfo(
+ mListener, name, userid, this);
+ service.linkToDeath(info, 0);
+ mListeners.add(info);
+ } catch (RemoteException e) {
+ // already dead
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ Slog.v(TAG, "notification listener connection lost: " + name);
+ }
+ },
+ Context.BIND_AUTO_CREATE,
+ new UserHandle(userid)))
+ {
+ mServicesBinding.remove(servicesBindingTag);
+ Slog.w(TAG, "Unable to bind listener service: " + intent);
+ return;
+ }
+ } catch (SecurityException ex) {
+ Slog.e(TAG, "Unable to bind listener service: " + intent, ex);
+ return;
+ }
+ }
+ }
+
+
+ /**
+ * Remove a listener service for the given user by ComponentName
+ */
+ private void unregisterListenerService(ComponentName name, int userid) {
+ checkCallerIsSystem();
+
+ synchronized (mNotificationList) {
+ final int N = mListeners.size();
+ for (int i=N-1; i>=0; i--) {
+ final NotificationListenerInfo info = mListeners.get(i);
+ if (name.equals(info.component)
+ && info.userid == userid) {
+ mListeners.remove(i);
+ if (info.connection != null) {
+ try {
+ getContext().unbindService(info.connection);
+ } catch (IllegalArgumentException ex) {
+ // something happened to the service: we think we have a connection
+ // but it's bogus.
+ Slog.e(TAG, "Listener " + name + " could not be unbound: " + ex);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * asynchronously notify all listeners about a new notification
+ */
+ void notifyPostedLocked(NotificationRecord n) {
+ // make a copy in case changes are made to the underlying Notification object
+ final StatusBarNotification sbn = n.sbn.clone();
+ for (final NotificationListenerInfo info : mListeners) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ info.notifyPostedIfUserMatch(sbn);
+ }});
+ }
+ }
+
+ /**
+ * asynchronously notify all listeners about a removed notification
+ */
+ void notifyRemovedLocked(NotificationRecord n) {
+ // make a copy in case changes are made to the underlying Notification object
+ // NOTE: this copy is lightweight: it doesn't include heavyweight parts of the notification
+ final StatusBarNotification sbn_light = n.sbn.cloneLight();
+
+ for (final NotificationListenerInfo info : mListeners) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ info.notifyRemovedIfUserMatch(sbn_light);
+ }});
+ }
+ }
+
+ // -- APIs to support listeners clicking/clearing notifications --
+
+ private NotificationListenerInfo checkListenerToken(INotificationListener listener) {
+ final IBinder token = listener.asBinder();
+ final int N = mListeners.size();
+ for (int i=0; i<N; i++) {
+ final NotificationListenerInfo info = mListeners.get(i);
+ if (info.listener.asBinder() == token) return info;
+ }
+ throw new SecurityException("Disallowed call from unknown listener: " + listener);
+ }
+
+
+
+ // -- end of listener APIs --
+
+ public static final class NotificationRecord
+ {
+ final StatusBarNotification sbn;
+ IBinder statusBarKey;
+
+ NotificationRecord(StatusBarNotification sbn)
+ {
+ this.sbn = sbn;
+ }
+
+ public Notification getNotification() { return sbn.getNotification(); }
+ public int getFlags() { return sbn.getNotification().flags; }
+ public int getUserId() { return sbn.getUserId(); }
+
+ void dump(PrintWriter pw, String prefix, Context baseContext) {
+ final Notification notification = sbn.getNotification();
+ pw.println(prefix + this);
+ pw.println(prefix + " uid=" + sbn.getUid() + " userId=" + sbn.getUserId());
+ pw.println(prefix + " icon=0x" + Integer.toHexString(notification.icon)
+ + " / " + idDebugString(baseContext, sbn.getPackageName(), notification.icon));
+ pw.println(prefix + " pri=" + notification.priority + " score=" + sbn.getScore());
+ 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 + String.format(" defaults=0x%08x flags=0x%08x",
+ notification.defaults, notification.flags));
+ pw.println(prefix + " sound=" + notification.sound);
+ pw.println(prefix + " vibrate=" + Arrays.toString(notification.vibrate));
+ pw.println(prefix + String.format(" led=0x%08x onMs=%d offMs=%d",
+ notification.ledARGB, notification.ledOnMS, notification.ledOffMS));
+ if (notification.actions != null && notification.actions.length > 0) {
+ pw.println(prefix + " actions={");
+ final int N = notification.actions.length;
+ for (int i=0; i<N; i++) {
+ final Notification.Action action = notification.actions[i];
+ pw.println(String.format("%s [%d] \"%s\" -> %s",
+ prefix,
+ i,
+ action.title,
+ action.actionIntent.toString()
+ ));
+ }
+ pw.println(prefix + " }");
+ }
+ if (notification.extras != null && notification.extras.size() > 0) {
+ pw.println(prefix + " extras={");
+ for (String key : notification.extras.keySet()) {
+ pw.print(prefix + " " + key + "=");
+ Object val = notification.extras.get(key);
+ if (val == null) {
+ pw.println("null");
+ } else {
+ pw.print(val.toString());
+ if (val instanceof Bitmap) {
+ pw.print(String.format(" (%dx%d)",
+ ((Bitmap) val).getWidth(),
+ ((Bitmap) val).getHeight()));
+ } else if (val.getClass().isArray()) {
+ pw.println(" {");
+ final int N = Array.getLength(val);
+ for (int i=0; i<N; i++) {
+ if (i > 0) pw.println(",");
+ pw.print(prefix + " " + Array.get(val, i));
+ }
+ pw.print("\n" + prefix + " }");
+ }
+ pw.println();
+ }
+ }
+ pw.println(prefix + " }");
+ }
+ }
+
+ @Override
+ 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.getPackageName(), this.sbn.getUser(), this.sbn.getId(),
+ this.sbn.getTag(), this.sbn.getScore(), this.sbn.getNotification());
+ }
+ }
+
+ private static final class ToastRecord
+ {
+ final int pid;
+ final String pkg;
+ final ITransientNotification callback;
+ int duration;
+
+ ToastRecord(int pid, String pkg, ITransientNotification callback, int duration)
+ {
+ this.pid = pid;
+ this.pkg = pkg;
+ this.callback = callback;
+ this.duration = duration;
+ }
+
+ void update(int duration) {
+ this.duration = duration;
+ }
+
+ void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + this);
+ }
+
+ @Override
+ public final String toString()
+ {
+ return "ToastRecord{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " pkg=" + pkg
+ + " callback=" + callback
+ + " duration=" + duration;
+ }
+ }
+
+ private final NotificationDelegate mNotificationDelegate = new NotificationDelegate() {
+
+ @Override
+ public void onSetDisabled(int status) {
+ synchronized (mNotificationList) {
+ mDisabledNotifications = status;
+ if ((mDisabledNotifications & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) {
+ // cancel whatever's going on
+ long identity = Binder.clearCallingIdentity();
+ try {
+ final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
+ if (player != null) {
+ player.stopAsync();
+ }
+ } catch (RemoteException e) {
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+
+ identity = Binder.clearCallingIdentity();
+ try {
+ mVibrator.cancel();
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onClearAll() {
+ // XXX to be totally correct, the caller should tell us which user
+ // this is for.
+ cancelAll(ActivityManager.getCurrentUser());
+ }
+
+ @Override
+ public void onNotificationClick(String pkg, String tag, int id) {
+ // XXX to be totally correct, the caller should tell us which user
+ // this is for.
+ cancelNotification(pkg, tag, id, Notification.FLAG_AUTO_CANCEL,
+ Notification.FLAG_FOREGROUND_SERVICE, false,
+ ActivityManager.getCurrentUser());
+ }
+
+ @Override
+ public void onNotificationClear(String pkg, String tag, int id) {
+ // XXX to be totally correct, the caller should tell us which user
+ // this is for.
+ cancelNotification(pkg, tag, id, 0,
+ Notification.FLAG_ONGOING_EVENT | Notification.FLAG_FOREGROUND_SERVICE,
+ true, ActivityManager.getCurrentUser());
+ }
+
+ @Override
+ public void onPanelRevealed() {
+ synchronized (mNotificationList) {
+ // sound
+ mSoundNotification = null;
+
+ long identity = Binder.clearCallingIdentity();
+ try {
+ final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
+ if (player != null) {
+ player.stopAsync();
+ }
+ } catch (RemoteException e) {
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+
+ // vibrate
+ mVibrateNotification = null;
+ identity = Binder.clearCallingIdentity();
+ try {
+ mVibrator.cancel();
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+
+ // light
+ mLights.clear();
+ mLedNotification = null;
+ updateLightsLocked();
+ }
+ }
+
+ @Override
+ public void onNotificationError(String pkg, String tag, int id,
+ int uid, int initialPid, String message) {
+ Slog.d(TAG, "onNotification error pkg=" + pkg + " tag=" + tag + " id=" + id
+ + "; will crashApplication(uid=" + uid + ", pid=" + initialPid + ")");
+ // XXX to be totally correct, the caller should tell us which user
+ // this is for.
+ cancelNotification(pkg, tag, id, 0, 0, false, UserHandle.getUserId(uid));
+ long ident = Binder.clearCallingIdentity();
+ try {
+ ActivityManagerNative.getDefault().crashApplication(uid, initialPid, pkg,
+ "Bad notification posted from package " + pkg
+ + ": " + message);
+ } catch (RemoteException e) {
+ }
+ Binder.restoreCallingIdentity(ident);
+ }
+ };
+
+ private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+
+ boolean queryRestart = false;
+ boolean queryRemove = false;
+ boolean packageChanged = false;
+ boolean cancelNotifications = true;
+
+ if (action.equals(Intent.ACTION_PACKAGE_ADDED)
+ || (queryRemove=action.equals(Intent.ACTION_PACKAGE_REMOVED))
+ || action.equals(Intent.ACTION_PACKAGE_RESTARTED)
+ || (packageChanged=action.equals(Intent.ACTION_PACKAGE_CHANGED))
+ || (queryRestart=action.equals(Intent.ACTION_QUERY_PACKAGE_RESTART))
+ || action.equals(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE)) {
+ String pkgList[] = null;
+ boolean queryReplace = queryRemove &&
+ intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
+ if (DBG) Slog.i(TAG, "queryReplace=" + queryReplace);
+ if (action.equals(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE)) {
+ pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+ } else if (queryRestart) {
+ pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
+ } else {
+ Uri uri = intent.getData();
+ if (uri == null) {
+ return;
+ }
+ String pkgName = uri.getSchemeSpecificPart();
+ if (pkgName == null) {
+ return;
+ }
+ if (packageChanged) {
+ // We cancel notifications for packages which have just been disabled
+ try {
+ final int enabled = getContext().getPackageManager()
+ .getApplicationEnabledSetting(pkgName);
+ if (enabled == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ || enabled == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
+ cancelNotifications = false;
+ }
+ } catch (IllegalArgumentException e) {
+ // Package doesn't exist; probably racing with uninstall.
+ // cancelNotifications is already true, so nothing to do here.
+ if (DBG) {
+ Slog.i(TAG, "Exception trying to look up app enabled setting", e);
+ }
+ }
+ }
+ pkgList = new String[]{pkgName};
+ }
+
+ boolean anyListenersInvolved = false;
+ if (pkgList != null && (pkgList.length > 0)) {
+ for (String pkgName : pkgList) {
+ if (cancelNotifications) {
+ cancelAllNotificationsInt(pkgName, 0, 0, !queryRestart,
+ UserHandle.USER_ALL);
+ }
+ if (mEnabledListenerPackageNames.contains(pkgName)) {
+ anyListenersInvolved = true;
+ }
+ }
+ }
+
+ if (anyListenersInvolved) {
+ // if we're not replacing a package, clean up orphaned bits
+ if (!queryReplace) {
+ disableNonexistentListeners();
+ }
+ // make sure we're still bound to any of our
+ // listeners who may have just upgraded
+ rebindListenerServices();
+ }
+ } else if (action.equals(Intent.ACTION_SCREEN_ON)) {
+ // Keep track of screen on/off state, but do not turn off the notification light
+ // until user passes through the lock screen or views the notification.
+ mScreenOn = true;
+ } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
+ mScreenOn = false;
+ } else if (action.equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) {
+ mInCall = (intent.getStringExtra(TelephonyManager.EXTRA_STATE).equals(
+ TelephonyManager.EXTRA_STATE_OFFHOOK));
+ updateNotificationPulse();
+ } else if (action.equals(Intent.ACTION_USER_STOPPED)) {
+ int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (userHandle >= 0) {
+ cancelAllNotificationsInt(null, 0, 0, true, userHandle);
+ }
+ } else if (action.equals(Intent.ACTION_USER_PRESENT)) {
+ // turn off LED when user passes through lock screen
+ mNotificationLight.turnOff();
+ } else if (action.equals(Intent.ACTION_USER_SWITCHED)) {
+ // reload per-user settings
+ mSettingsObserver.update(null);
+ }
+ }
+ };
+
+ class SettingsObserver extends ContentObserver {
+ private final Uri NOTIFICATION_LIGHT_PULSE_URI
+ = Settings.System.getUriFor(Settings.System.NOTIFICATION_LIGHT_PULSE);
+
+ private final Uri ENABLED_NOTIFICATION_LISTENERS_URI
+ = Settings.Secure.getUriFor(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
+
+ SettingsObserver(Handler handler) {
+ super(handler);
+ }
+
+ void observe() {
+ ContentResolver resolver = getContext().getContentResolver();
+ resolver.registerContentObserver(NOTIFICATION_LIGHT_PULSE_URI,
+ false, this, UserHandle.USER_ALL);
+ resolver.registerContentObserver(ENABLED_NOTIFICATION_LISTENERS_URI,
+ false, this, UserHandle.USER_ALL);
+ update(null);
+ }
+
+ @Override public void onChange(boolean selfChange, Uri uri) {
+ update(uri);
+ }
+
+ public void update(Uri uri) {
+ ContentResolver resolver = getContext().getContentResolver();
+ if (uri == null || NOTIFICATION_LIGHT_PULSE_URI.equals(uri)) {
+ boolean pulseEnabled = Settings.System.getInt(resolver,
+ Settings.System.NOTIFICATION_LIGHT_PULSE, 0) != 0;
+ if (mNotificationPulseEnabled != pulseEnabled) {
+ mNotificationPulseEnabled = pulseEnabled;
+ updateNotificationPulse();
+ }
+ }
+ if (uri == null || ENABLED_NOTIFICATION_LISTENERS_URI.equals(uri)) {
+ rebindListenerServices();
+ }
+ }
+ }
+
+ private SettingsObserver mSettingsObserver;
+
+ static long[] getLongArray(Resources r, int resid, int maxlen, long[] def) {
+ int[] ar = r.getIntArray(resid);
+ if (ar == null) {
+ return def;
+ }
+ final int len = ar.length > maxlen ? maxlen : ar.length;
+ long[] out = new long[len];
+ for (int i=0; i<len; i++) {
+ out[i] = ar[i];
+ }
+ return out;
+ }
+
+ @Override
+ public void onStart() {
+ mAm = ActivityManagerNative.getDefault();
+ mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
+ mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
+
+ mHandler = new WorkerHandler();
+
+ importOldBlockDb();
+
+ mStatusBar = getLocalService(StatusBarManagerInternal.class);
+ mStatusBar.setNotificationDelegate(mNotificationDelegate);
+
+ final LightsManager lights = getLocalService(LightsManager.class);
+ mNotificationLight = lights.getLight(LightsManager.LIGHT_ID_NOTIFICATIONS);
+ mAttentionLight = lights.getLight(LightsManager.LIGHT_ID_ATTENTION);
+
+ Resources resources = getContext().getResources();
+ mDefaultNotificationColor = resources.getColor(
+ R.color.config_defaultNotificationColor);
+ mDefaultNotificationLedOn = resources.getInteger(
+ R.integer.config_defaultNotificationLedOn);
+ mDefaultNotificationLedOff = resources.getInteger(
+ R.integer.config_defaultNotificationLedOff);
+
+ mDefaultVibrationPattern = getLongArray(resources,
+ R.array.config_defaultNotificationVibePattern,
+ VIBRATE_PATTERN_MAXLEN,
+ DEFAULT_VIBRATE_PATTERN);
+
+ mFallbackVibrationPattern = getLongArray(resources,
+ R.array.config_notificationFallbackVibePattern,
+ VIBRATE_PATTERN_MAXLEN,
+ DEFAULT_VIBRATE_PATTERN);
+
+ // Don't start allowing notifications until the setup wizard has run once.
+ // After that, including subsequent boots, init with notifications turned on.
+ // This works on the first boot because the setup wizard will toggle this
+ // flag at least once and we'll go back to 0 after that.
+ if (0 == Settings.Global.getInt(getContext().getContentResolver(),
+ Settings.Global.DEVICE_PROVISIONED, 0)) {
+ mDisabledNotifications = StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
+ }
+
+ // register for various Intents
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
+ filter.addAction(Intent.ACTION_USER_PRESENT);
+ filter.addAction(Intent.ACTION_USER_STOPPED);
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+ getContext().registerReceiver(mIntentReceiver, filter);
+ IntentFilter pkgFilter = new IntentFilter();
+ pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ pkgFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ pkgFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+ pkgFilter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART);
+ pkgFilter.addDataScheme("package");
+ getContext().registerReceiver(mIntentReceiver, pkgFilter);
+ IntentFilter sdFilter = new IntentFilter(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+ getContext().registerReceiver(mIntentReceiver, sdFilter);
+
+ mSettingsObserver = new SettingsObserver(mHandler);
+ mSettingsObserver.observe();
+
+ // spin up NotificationScorers
+ String[] notificationScorerNames = resources.getStringArray(
+ R.array.config_notificationScorers);
+ for (String scorerName : notificationScorerNames) {
+ try {
+ Class<?> scorerClass = getContext().getClassLoader().loadClass(scorerName);
+ NotificationScorer scorer = (NotificationScorer) scorerClass.newInstance();
+ scorer.initialize(getContext());
+ mScorers.add(scorer);
+ } catch (ClassNotFoundException e) {
+ Slog.w(TAG, "Couldn't find scorer " + scorerName + ".", e);
+ } catch (InstantiationException e) {
+ Slog.w(TAG, "Couldn't instantiate scorer " + scorerName + ".", e);
+ } catch (IllegalAccessException e) {
+ Slog.w(TAG, "Problem accessing scorer " + scorerName + ".", e);
+ }
+ }
+
+ publishBinderService(Context.NOTIFICATION_SERVICE, mService);
+ publishLocalService(NotificationManagerInternal.class, mInternalService);
+ }
+
+ /**
+ * Read the old XML-based app block database and import those blockages into the AppOps system.
+ */
+ private void importOldBlockDb() {
+ loadBlockDb();
+
+ PackageManager pm = getContext().getPackageManager();
+ for (String pkg : mBlockedPackages) {
+ PackageInfo info = null;
+ try {
+ info = pm.getPackageInfo(pkg, 0);
+ setNotificationsEnabledForPackageImpl(pkg, info.applicationInfo.uid, false);
+ } catch (NameNotFoundException e) {
+ // forget you
+ }
+ }
+ mBlockedPackages.clear();
+ if (mPolicyFile != null) {
+ mPolicyFile.delete();
+ }
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
+ // no beeping until we're basically done booting
+ mSystemReady = true;
+
+ // Grab our optional AudioService
+ mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+
+ // make sure our listener services are properly bound
+ rebindListenerServices();
+ }
+ }
+
+ void setNotificationsEnabledForPackageImpl(String pkg, int uid, boolean enabled) {
+ Slog.v(TAG, (enabled?"en":"dis") + "abling notifications for " + pkg);
+
+ mAppOps.setMode(AppOpsManager.OP_POST_NOTIFICATION, uid, pkg,
+ enabled ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
+
+ // Now, cancel any outstanding notifications that are part of a just-disabled app
+ if (ENABLE_BLOCKED_NOTIFICATIONS && !enabled) {
+ cancelAllNotificationsInt(pkg, 0, 0, true, UserHandle.getUserId(uid));
+ }
+ }
+
+ private final IBinder mService = new INotificationManager.Stub() {
+ // Toasts
+ // ============================================================================
+
+ @Override
+ public void enqueueToast(String pkg, ITransientNotification callback, int duration)
+ {
+ if (DBG) {
+ Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
+ + " duration=" + duration);
+ }
+
+ if (pkg == null || callback == null) {
+ Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
+ return ;
+ }
+
+ final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
+
+ if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {
+ if (!isSystemToast) {
+ Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
+ return;
+ }
+ }
+
+ synchronized (mToastQueue) {
+ int callingPid = Binder.getCallingPid();
+ long callingId = Binder.clearCallingIdentity();
+ try {
+ ToastRecord record;
+ int index = indexOfToastLocked(pkg, callback);
+ // If it's already in the queue, we update it in place, we don't
+ // move it to the end of the queue.
+ if (index >= 0) {
+ record = mToastQueue.get(index);
+ record.update(duration);
+ } else {
+ // Limit the number of toasts that any given package except the android
+ // package can enqueue. Prevents DOS attacks and deals with leaks.
+ if (!isSystemToast) {
+ int count = 0;
+ final int N = mToastQueue.size();
+ for (int i=0; i<N; i++) {
+ final ToastRecord r = mToastQueue.get(i);
+ if (r.pkg.equals(pkg)) {
+ count++;
+ if (count >= MAX_PACKAGE_NOTIFICATIONS) {
+ Slog.e(TAG, "Package has already posted " + count
+ + " toasts. Not showing more. Package=" + pkg);
+ return;
+ }
+ }
+ }
+ }
+
+ record = new ToastRecord(callingPid, pkg, callback, duration);
+ mToastQueue.add(record);
+ index = mToastQueue.size() - 1;
+ keepProcessAliveLocked(callingPid);
+ }
+ // If it's at index 0, it's the current toast. It doesn't matter if it's
+ // new or just been updated. Call back and tell it to show itself.
+ // If the callback fails, this will remove it from the list, so don't
+ // assume that it's valid after this.
+ if (index == 0) {
+ showNextToastLocked();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ }
+ }
+
+ @Override
+ public void cancelToast(String pkg, ITransientNotification callback) {
+ Slog.i(TAG, "cancelToast pkg=" + pkg + " callback=" + callback);
+
+ if (pkg == null || callback == null) {
+ Slog.e(TAG, "Not cancelling notification. pkg=" + pkg + " callback=" + callback);
+ return ;
+ }
+
+ synchronized (mToastQueue) {
+ long callingId = Binder.clearCallingIdentity();
+ try {
+ int index = indexOfToastLocked(pkg, callback);
+ if (index >= 0) {
+ cancelToastLocked(index);
+ } else {
+ Slog.w(TAG, "Toast already cancelled. pkg=" + pkg
+ + " callback=" + callback);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ }
+ }
+
+ @Override
+ public void enqueueNotificationWithTag(String pkg, String basePkg, String tag, int id,
+ Notification notification, int[] idOut, int userId) throws RemoteException {
+ enqueueNotificationInternal(pkg, basePkg, Binder.getCallingUid(),
+ Binder.getCallingPid(), tag, id, notification, idOut, userId);
+ }
+
+ @Override
+ public void cancelNotificationWithTag(String pkg, String tag, int id, int userId) {
+ checkCallerIsSystemOrSameApp(pkg);
+ userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, true, false, "cancelNotificationWithTag", pkg);
+ // Don't allow client applications to cancel foreground service notis.
+ cancelNotification(pkg, tag, id, 0,
+ Binder.getCallingUid() == Process.SYSTEM_UID
+ ? 0 : Notification.FLAG_FOREGROUND_SERVICE, false, userId);
+ }
+
+ @Override
+ public void cancelAllNotifications(String pkg, int userId) {
+ checkCallerIsSystemOrSameApp(pkg);
+
+ userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, true, false, "cancelAllNotifications", pkg);
+
+ // Calling from user space, don't allow the canceling of actively
+ // running foreground services.
+ cancelAllNotificationsInt(pkg, 0, Notification.FLAG_FOREGROUND_SERVICE, true, userId);
+ }
+
+ @Override
+ public void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled) {
+ checkCallerIsSystem();
+
+ setNotificationsEnabledForPackageImpl(pkg, uid, enabled);
+ }
+
+ /**
+ * Use this when you just want to know if notifications are OK for this package.
+ */
+ @Override
+ public boolean areNotificationsEnabledForPackage(String pkg, int uid) {
+ checkCallerIsSystem();
+ return (mAppOps.checkOpNoThrow(AppOpsManager.OP_POST_NOTIFICATION, uid, pkg)
+ == AppOpsManager.MODE_ALLOWED);
+ }
+
+ /**
+ * System-only API for getting a list of current (i.e. not cleared) notifications.
+ *
+ * Requires ACCESS_NOTIFICATIONS which is signature|system.
+ */
+ @Override
+ public StatusBarNotification[] getActiveNotifications(String callingPkg) {
+ // enforce() will ensure the calling uid has the correct permission
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_NOTIFICATIONS,
+ "NotificationManagerService.getActiveNotifications");
+
+ StatusBarNotification[] tmp = null;
+ int uid = Binder.getCallingUid();
+
+ // noteOp will check to make sure the callingPkg matches the uid
+ 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;
+ }
+
+ /**
+ * System-only API for getting a list of recent (cleared, no longer shown) notifications.
+ *
+ * Requires ACCESS_NOTIFICATIONS which is signature|system.
+ */
+ @Override
+ public StatusBarNotification[] getHistoricalNotifications(String callingPkg, int count) {
+ // enforce() will ensure the calling uid has the correct permission
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_NOTIFICATIONS,
+ "NotificationManagerService.getHistoricalNotifications");
+
+ StatusBarNotification[] tmp = null;
+ int uid = Binder.getCallingUid();
+
+ // noteOp will check to make sure the callingPkg matches the uid
+ if (mAppOps.noteOpNoThrow(AppOpsManager.OP_ACCESS_NOTIFICATIONS, uid, callingPkg)
+ == AppOpsManager.MODE_ALLOWED) {
+ synchronized (mArchive) {
+ tmp = mArchive.getArray(count);
+ }
+ }
+ return tmp;
+ }
+
+ /**
+ * Register a listener binder directly with the notification manager.
+ *
+ * Only works with system callers. Apps should extend
+ * {@link android.service.notification.NotificationListenerService}.
+ */
+ @Override
+ public void registerListener(final INotificationListener listener,
+ final ComponentName component, final int userid) {
+ checkCallerIsSystem();
+ registerListenerImpl(listener, component, userid);
+ }
+
+ /**
+ * Remove a listener binder directly
+ */
+ @Override
+ public void unregisterListener(INotificationListener listener, int userid) {
+ // no need to check permissions; if your listener binder is in the list,
+ // that's proof that you had permission to add it in the first place
+ unregisterListenerImpl(listener, userid);
+ }
+
+ /**
+ * Allow an INotificationListener to simulate a "clear all" operation.
+ *
+ * {@see com.android.server.StatusBarManagerService.NotificationCallbacks#onClearAllNotifications}
+ *
+ * @param token The binder for the listener, to check that the caller is allowed
+ */
+ @Override
+ public void cancelAllNotificationsFromListener(INotificationListener token) {
+ NotificationListenerInfo info = checkListenerToken(token);
+ long identity = Binder.clearCallingIdentity();
+ try {
+ cancelAll(info.userid);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Allow an INotificationListener to simulate clearing (dismissing) a single notification.
+ *
+ * {@see com.android.server.StatusBarManagerService.NotificationCallbacks#onNotificationClear}
+ *
+ * @param token The binder for the listener, to check that the caller is allowed
+ */
+ @Override
+ public void cancelNotificationFromListener(INotificationListener token, String pkg,
+ String tag, int id) {
+ NotificationListenerInfo info = checkListenerToken(token);
+ long identity = Binder.clearCallingIdentity();
+ try {
+ cancelNotification(pkg, tag, id, 0,
+ Notification.FLAG_ONGOING_EVENT | Notification.FLAG_FOREGROUND_SERVICE,
+ true,
+ info.userid);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Allow an INotificationListener to request the list of outstanding notifications seen by
+ * the current user. Useful when starting up, after which point the listener callbacks
+ * should be used.
+ *
+ * @param token The binder for the listener, to check that the caller is allowed
+ */
+ @Override
+ public StatusBarNotification[] getActiveNotificationsFromListener(
+ INotificationListener token) {
+ NotificationListenerInfo info = checkListenerToken(token);
+
+ StatusBarNotification[] result = new StatusBarNotification[0];
+ ArrayList<StatusBarNotification> list = new ArrayList<StatusBarNotification>();
+ synchronized (mNotificationList) {
+ final int N = mNotificationList.size();
+ for (int i=0; i<N; i++) {
+ StatusBarNotification sbn = mNotificationList.get(i).sbn;
+ if (info.enabledAndUserMatches(sbn)) {
+ list.add(sbn);
+ }
+ }
+ }
+ return list.toArray(result);
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+ pw.println("Permission Denial: can't dump NotificationManager from from pid="
+ + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid());
+ return;
+ }
+
+ dumpImpl(pw);
+ }
+ };
+
+ void dumpImpl(PrintWriter pw) {
+ pw.println("Current Notification Manager state:");
+
+ pw.println(" Listeners (" + mEnabledListenersForCurrentUser.size()
+ + ") enabled for current user:");
+ for (ComponentName cmpt : mEnabledListenersForCurrentUser) {
+ pw.println(" " + cmpt);
+ }
+
+ pw.println(" Live listeners (" + mListeners.size() + "):");
+ for (NotificationListenerInfo info : mListeners) {
+ pw.println(" " + info.component
+ + " (user " + info.userid + "): " + info.listener
+ + (info.isSystem?" SYSTEM":""));
+ }
+
+ int N;
+
+ synchronized (mToastQueue) {
+ N = mToastQueue.size();
+ if (N > 0) {
+ pw.println(" Toast Queue:");
+ for (int i=0; i<N; i++) {
+ mToastQueue.get(i).dump(pw, " ");
+ }
+ pw.println(" ");
+ }
+
+ }
+
+ synchronized (mNotificationList) {
+ N = mNotificationList.size();
+ if (N > 0) {
+ pw.println(" Notification List:");
+ for (int i=0; i<N; i++) {
+ mNotificationList.get(i).dump(pw, " ", getContext());
+ }
+ pw.println(" ");
+ }
+
+ N = mLights.size();
+ if (N > 0) {
+ pw.println(" Lights List:");
+ for (int i=0; i<N; i++) {
+ pw.println(" " + mLights.get(i));
+ }
+ pw.println(" ");
+ }
+
+ pw.println(" mSoundNotification=" + mSoundNotification);
+ pw.println(" mVibrateNotification=" + mVibrateNotification);
+ pw.println(" mDisabledNotifications=0x"
+ + Integer.toHexString(mDisabledNotifications));
+ pw.println(" mSystemReady=" + mSystemReady);
+ pw.println(" mArchive=" + mArchive.toString());
+ Iterator<StatusBarNotification> iter = mArchive.descendingIterator();
+ int i=0;
+ while (iter.hasNext()) {
+ pw.println(" " + iter.next());
+ if (++i >= 5) {
+ if (iter.hasNext()) pw.println(" ...");
+ break;
+ }
+ }
+
+ }
+ }
+
+ /**
+ * The private API only accessible to the system process.
+ */
+ private final NotificationManagerInternal mInternalService = new NotificationManagerInternal() {
+ @Override
+ public void enqueueNotification(String pkg, String basePkg, int callingUid, int callingPid,
+ String tag, int id, Notification notification, int[] idReceived, int userId) {
+ enqueueNotificationInternal(pkg, basePkg, callingUid, callingPid, tag, id, notification,
+ idReceived, userId);
+ }
+ };
+
+ void enqueueNotificationInternal(final String pkg, String basePkg, final int callingUid,
+ final int callingPid, final String tag, final int id, final Notification notification,
+ int[] idOut, int incomingUserId) {
+ if (DBG) {
+ Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id
+ + " notification=" + notification);
+ }
+ checkCallerIsSystemOrSameApp(pkg);
+ final boolean isSystemNotification = isUidSystem(callingUid) || ("android".equals(pkg));
+
+ final int userId = ActivityManager.handleIncomingUser(callingPid,
+ callingUid, incomingUserId, true, false, "enqueueNotification", pkg);
+ final UserHandle user = new UserHandle(userId);
+
+ // Limit the number of notifications that any given package except the android
+ // package can enqueue. Prevents DOS attacks and deals with leaks.
+ if (!isSystemNotification) {
+ synchronized (mNotificationList) {
+ int count = 0;
+ final int N = mNotificationList.size();
+ for (int i=0; i<N; i++) {
+ final NotificationRecord r = mNotificationList.get(i);
+ if (r.sbn.getPackageName().equals(pkg) && r.sbn.getUserId() == userId) {
+ count++;
+ if (count >= MAX_PACKAGE_NOTIFICATIONS) {
+ Slog.e(TAG, "Package has already posted " + count
+ + " notifications. Not showing more. package=" + pkg);
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ // This conditional is a dirty hack to limit the logging done on
+ // behalf of the download manager without affecting other apps.
+ if (!pkg.equals("com.android.providers.downloads")
+ || Log.isLoggable("DownloadManager", Log.VERBOSE)) {
+ EventLog.writeEvent(EventLogTags.NOTIFICATION_ENQUEUE, pkg, id, tag, userId,
+ notification.toString());
+ }
+
+ if (pkg == null || notification == null) {
+ throw new IllegalArgumentException("null not allowed: pkg=" + pkg
+ + " id=" + id + " notification=" + notification);
+ }
+ if (notification.icon != 0) {
+ if (notification.contentView == null) {
+ throw new IllegalArgumentException("contentView required: pkg=" + pkg
+ + " id=" + id + " notification=" + notification);
+ }
+ }
+
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+
+ // === Scoring ===
+
+ // 0. Sanitize inputs
+ notification.priority = clamp(notification.priority, Notification.PRIORITY_MIN,
+ Notification.PRIORITY_MAX);
+ // Migrate notification flags to scores
+ if (0 != (notification.flags & Notification.FLAG_HIGH_PRIORITY)) {
+ if (notification.priority < Notification.PRIORITY_MAX) {
+ notification.priority = Notification.PRIORITY_MAX;
+ }
+ } else if (SCORE_ONGOING_HIGHER &&
+ 0 != (notification.flags & Notification.FLAG_ONGOING_EVENT)) {
+ if (notification.priority < Notification.PRIORITY_HIGH) {
+ notification.priority = Notification.PRIORITY_HIGH;
+ }
+ }
+
+ // 1. initial score: buckets of 10, around the app
+ int score = notification.priority * NOTIFICATION_PRIORITY_MULTIPLIER; //[-20..20]
+
+ // 2. Consult external heuristics (TBD)
+
+ // 3. Apply local rules
+
+ int initialScore = score;
+ if (!mScorers.isEmpty()) {
+ if (DBG) Slog.v(TAG, "Initial score is " + score + ".");
+ for (NotificationScorer scorer : mScorers) {
+ try {
+ score = scorer.getScore(notification, score);
+ } catch (Throwable t) {
+ Slog.w(TAG, "Scorer threw on .getScore.", t);
+ }
+ }
+ if (DBG) Slog.v(TAG, "Final score is " + score + ".");
+ }
+
+ // add extra to indicate score modified by NotificationScorer
+ notification.extras.putBoolean(Notification.EXTRA_SCORE_MODIFIED,
+ score != initialScore);
+
+ // blocked apps
+ if (ENABLE_BLOCKED_NOTIFICATIONS && !noteNotificationOp(pkg, callingUid)) {
+ if (!isSystemNotification) {
+ score = JUNK_SCORE;
+ Slog.e(TAG, "Suppressing notification from package " + pkg
+ + " by user request.");
+ }
+ }
+
+ if (DBG) {
+ Slog.v(TAG, "Assigned score=" + score + " to " + notification);
+ }
+
+ if (score < SCORE_DISPLAY_THRESHOLD) {
+ // Notification will be blocked because the score is too low.
+ return;
+ }
+
+ // Should this notification make noise, vibe, or use the LED?
+ final boolean canInterrupt = (score >= SCORE_INTERRUPTION_THRESHOLD);
+
+ synchronized (mNotificationList) {
+ 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);
+ if (index < 0) {
+ mNotificationList.add(r);
+ } else {
+ old = mNotificationList.remove(index);
+ mNotificationList.add(index, r);
+ // Make sure we don't lose the foreground service state.
+ if (old != null) {
+ notification.flags |=
+ old.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE;
+ }
+ }
+
+ // Ensure if this is a foreground service that the proper additional
+ // flags are set.
+ if ((notification.flags&Notification.FLAG_FOREGROUND_SERVICE) != 0) {
+ notification.flags |= Notification.FLAG_ONGOING_EVENT
+ | Notification.FLAG_NO_CLEAR;
+ }
+
+ final int currentUser;
+ final long token = Binder.clearCallingIdentity();
+ try {
+ currentUser = ActivityManager.getCurrentUser();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ if (notification.icon != 0) {
+ if (old != null && old.statusBarKey != null) {
+ r.statusBarKey = old.statusBarKey;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mStatusBar.updateNotification(r.statusBarKey, n);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ } else {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ r.statusBarKey = mStatusBar.addNotification(n);
+ if ((n.getNotification().flags & Notification.FLAG_SHOW_LIGHTS) != 0
+ && canInterrupt) {
+ mAttentionLight.pulse();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ // Send accessibility events only for the current user.
+ if (currentUser == userId) {
+ sendAccessibilityEvent(notification, pkg);
+ }
+
+ notifyPostedLocked(r);
+ } else {
+ Slog.e(TAG, "Not posting notification with icon==0: " + notification);
+ if (old != null && old.statusBarKey != null) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mStatusBar.removeNotification(old.statusBarKey);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+
+ notifyRemovedLocked(r);
+ }
+ // ATTENTION: in a future release we will bail out here
+ // so that we do not play sounds, show lights, etc. for invalid
+ // notifications
+ Slog.e(TAG, "WARNING: In a future release this will crash the app: "
+ + n.getPackageName());
+ }
+
+ // 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.getUserId() == UserHandle.USER_ALL ||
+ (r.getUserId() == userId && r.getUserId() == currentUser))
+ && canInterrupt
+ && mSystemReady
+ && mAudioManager != null) {
+
+ // sound
+
+ // should we use the default notification sound? (indicated either by
+ // DEFAULT_SOUND or because notification.sound is pointing at
+ // Settings.System.NOTIFICATION_SOUND)
+ final boolean useDefaultSound =
+ (notification.defaults & Notification.DEFAULT_SOUND) != 0 ||
+ Settings.System.DEFAULT_NOTIFICATION_URI
+ .equals(notification.sound);
+
+ Uri soundUri = null;
+ boolean hasValidSound = false;
+
+ if (useDefaultSound) {
+ soundUri = Settings.System.DEFAULT_NOTIFICATION_URI;
+
+ // check to see if the default notification sound is silent
+ ContentResolver resolver = getContext().getContentResolver();
+ hasValidSound = Settings.System.getString(resolver,
+ Settings.System.NOTIFICATION_SOUND) != null;
+ } else if (notification.sound != null) {
+ soundUri = notification.sound;
+ hasValidSound = (soundUri != null);
+ }
+
+ if (hasValidSound) {
+ boolean looping =
+ (notification.flags & Notification.FLAG_INSISTENT) != 0;
+ int audioStreamType;
+ if (notification.audioStreamType >= 0) {
+ audioStreamType = notification.audioStreamType;
+ } else {
+ audioStreamType = DEFAULT_STREAM_TYPE;
+ }
+ mSoundNotification = r;
+ // do not play notifications if stream volume is 0 (typically because
+ // ringer mode is silent) or if there is a user of exclusive audio focus
+ if ((mAudioManager.getStreamVolume(audioStreamType) != 0)
+ && !mAudioManager.isAudioFocusExclusive()) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final IRingtonePlayer player =
+ mAudioManager.getRingtonePlayer();
+ if (player != null) {
+ player.playAsync(soundUri, user, looping, audioStreamType);
+ }
+ } catch (RemoteException e) {
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
+ // vibrate
+ // Does the notification want to specify its own vibration?
+ final boolean hasCustomVibrate = notification.vibrate != null;
+
+ // new in 4.2: if there was supposed to be a sound and we're in vibrate
+ // mode, and no other vibration is specified, we fall back to vibration
+ final boolean convertSoundToVibration =
+ !hasCustomVibrate
+ && hasValidSound
+ && (mAudioManager.getRingerMode()
+ == AudioManager.RINGER_MODE_VIBRATE);
+
+ // The DEFAULT_VIBRATE flag trumps any custom vibration AND the fallback.
+ final boolean useDefaultVibrate =
+ (notification.defaults & Notification.DEFAULT_VIBRATE) != 0;
+
+ if ((useDefaultVibrate || convertSoundToVibration || hasCustomVibrate)
+ && !(mAudioManager.getRingerMode()
+ == AudioManager.RINGER_MODE_SILENT)) {
+ mVibrateNotification = r;
+
+ if (useDefaultVibrate || convertSoundToVibration) {
+ // Escalate privileges so we can use the vibrator even if the
+ // notifying app does not have the VIBRATE permission.
+ long identity = Binder.clearCallingIdentity();
+ try {
+ mVibrator.vibrate(r.sbn.getUid(), r.sbn.getBasePkg(),
+ useDefaultVibrate ? mDefaultVibrationPattern
+ : mFallbackVibrationPattern,
+ ((notification.flags & Notification.FLAG_INSISTENT) != 0)
+ ? 0: -1);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ } else if (notification.vibrate.length > 1) {
+ // If you want your own vibration pattern, you need the VIBRATE
+ // permission
+ mVibrator.vibrate(r.sbn.getUid(), r.sbn.getBasePkg(),
+ notification.vibrate,
+ ((notification.flags & Notification.FLAG_INSISTENT) != 0)
+ ? 0: -1);
+ }
+ }
+ }
+
+ // light
+ // the most recent thing gets the light
+ mLights.remove(old);
+ if (mLedNotification == old) {
+ mLedNotification = null;
+ }
+ //Slog.i(TAG, "notification.lights="
+ // + ((old.notification.lights.flags & Notification.FLAG_SHOW_LIGHTS)
+ // != 0));
+ if ((notification.flags & Notification.FLAG_SHOW_LIGHTS) != 0
+ && canInterrupt) {
+ mLights.add(r);
+ updateLightsLocked();
+ } else {
+ if (old != null
+ && ((old.getFlags() & Notification.FLAG_SHOW_LIGHTS) != 0)) {
+ updateLightsLocked();
+ }
+ }
+ }
+ }
+ });
+
+ idOut[0] = id;
+ }
+
+ void registerListenerImpl(final INotificationListener listener,
+ final ComponentName component, final int userid) {
+ synchronized (mNotificationList) {
+ try {
+ NotificationListenerInfo info
+ = new NotificationListenerInfo(listener, component, userid, true);
+ listener.asBinder().linkToDeath(info, 0);
+ mListeners.add(info);
+ } catch (RemoteException e) {
+ // already dead
+ }
+ }
+ }
+
+ void unregisterListenerImpl(final INotificationListener listener, final int userid) {
+ synchronized (mNotificationList) {
+ final int N = mListeners.size();
+ for (int i=N-1; i>=0; i--) {
+ final NotificationListenerInfo info = mListeners.get(i);
+ if (info.listener.asBinder() == listener.asBinder()
+ && info.userid == userid) {
+ mListeners.remove(i);
+ if (info.connection != null) {
+ getContext().unbindService(info.connection);
+ }
+ }
+ }
+ }
+ }
+
+ void showNextToastLocked() {
+ ToastRecord record = mToastQueue.get(0);
+ while (record != null) {
+ if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
+ try {
+ record.callback.show();
+ scheduleTimeoutLocked(record);
+ return;
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Object died trying to show notification " + record.callback
+ + " in package " + record.pkg);
+ // remove it from the list and let the process die
+ int index = mToastQueue.indexOf(record);
+ if (index >= 0) {
+ mToastQueue.remove(index);
+ }
+ keepProcessAliveLocked(record.pid);
+ if (mToastQueue.size() > 0) {
+ record = mToastQueue.get(0);
+ } else {
+ record = null;
+ }
+ }
+ }
+ }
+
+ void cancelToastLocked(int index) {
+ ToastRecord record = mToastQueue.get(index);
+ try {
+ record.callback.hide();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Object died trying to hide notification " + record.callback
+ + " in package " + record.pkg);
+ // don't worry about this, we're about to remove it from
+ // the list anyway
+ }
+ mToastQueue.remove(index);
+ keepProcessAliveLocked(record.pid);
+ if (mToastQueue.size() > 0) {
+ // Show the next one. If the callback fails, this will remove
+ // it from the list, so don't assume that the list hasn't changed
+ // after this point.
+ showNextToastLocked();
+ }
+ }
+
+ private void scheduleTimeoutLocked(ToastRecord r)
+ {
+ mHandler.removeCallbacksAndMessages(r);
+ Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
+ long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
+ mHandler.sendMessageDelayed(m, delay);
+ }
+
+ private void handleTimeout(ToastRecord record)
+ {
+ if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
+ synchronized (mToastQueue) {
+ int index = indexOfToastLocked(record.pkg, record.callback);
+ if (index >= 0) {
+ cancelToastLocked(index);
+ }
+ }
+ }
+
+ // lock on mToastQueue
+ int indexOfToastLocked(String pkg, ITransientNotification callback)
+ {
+ IBinder cbak = callback.asBinder();
+ ArrayList<ToastRecord> list = mToastQueue;
+ int len = list.size();
+ for (int i=0; i<len; i++) {
+ ToastRecord r = list.get(i);
+ if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ // lock on mToastQueue
+ void keepProcessAliveLocked(int pid)
+ {
+ int toastCount = 0; // toasts from this pid
+ ArrayList<ToastRecord> list = mToastQueue;
+ int N = list.size();
+ for (int i=0; i<N; i++) {
+ ToastRecord r = list.get(i);
+ if (r.pid == pid) {
+ toastCount++;
+ }
+ }
+ try {
+ mAm.setProcessForeground(mForegroundToken, pid, toastCount > 0);
+ } catch (RemoteException e) {
+ // Shouldn't happen.
+ }
+ }
+
+ private final class WorkerHandler extends Handler
+ {
+ @Override
+ public void handleMessage(Message msg)
+ {
+ switch (msg.what)
+ {
+ case MESSAGE_TIMEOUT:
+ handleTimeout((ToastRecord)msg.obj);
+ break;
+ }
+ }
+ }
+
+
+ // Notifications
+ // ============================================================================
+ static int clamp(int x, int low, int high) {
+ return (x < low) ? low : ((x > high) ? high : x);
+ }
+
+ void sendAccessibilityEvent(Notification notification, CharSequence packageName) {
+ AccessibilityManager manager = AccessibilityManager.getInstance(getContext());
+ if (!manager.isEnabled()) {
+ return;
+ }
+
+ AccessibilityEvent event =
+ AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
+ event.setPackageName(packageName);
+ event.setClassName(Notification.class.getName());
+ event.setParcelableData(notification);
+ CharSequence tickerText = notification.tickerText;
+ if (!TextUtils.isEmpty(tickerText)) {
+ event.getText().add(tickerText);
+ }
+
+ manager.sendAccessibilityEvent(event);
+ }
+
+ private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete) {
+ // tell the app
+ if (sendDelete) {
+ if (r.getNotification().deleteIntent != null) {
+ try {
+ 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.sbn.getPackageName(), ex);
+ }
+ }
+ }
+
+ // status bar
+ if (r.getNotification().icon != 0) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mStatusBar.removeNotification(r.statusBarKey);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ r.statusBarKey = null;
+ notifyRemovedLocked(r);
+ }
+
+ // sound
+ if (mSoundNotification == r) {
+ mSoundNotification = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final IRingtonePlayer player = mAudioManager.getRingtonePlayer();
+ if (player != null) {
+ player.stopAsync();
+ }
+ } catch (RemoteException e) {
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ // vibrate
+ if (mVibrateNotification == r) {
+ mVibrateNotification = null;
+ long identity = Binder.clearCallingIdentity();
+ try {
+ mVibrator.cancel();
+ }
+ finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ // light
+ mLights.remove(r);
+ if (mLedNotification == r) {
+ mLedNotification = null;
+ }
+
+ // Save it for users of getHistoricalNotifications()
+ mArchive.record(r.sbn);
+ }
+
+ /**
+ * Cancels a notification ONLY if it has all of the {@code mustHaveFlags}
+ * and none of the {@code mustNotHaveFlags}.
+ */
+ void cancelNotification(final String pkg, final String tag, final int id,
+ final int mustHaveFlags, final int mustNotHaveFlags, final boolean sendDelete,
+ final int userId) {
+ // In enqueueNotificationInternal notifications are added by scheduling the
+ // work on the worker handler. Hence, we also schedule the cancel on this
+ // handler to avoid a scenario where an add notification call followed by a
+ // remove notification call ends up in not removing the notification.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ EventLog.writeEvent(EventLogTags.NOTIFICATION_CANCEL, pkg, id, tag, userId,
+ mustHaveFlags, mustNotHaveFlags);
+
+ synchronized (mNotificationList) {
+ int index = indexOfNotificationLocked(pkg, tag, id, userId);
+ if (index >= 0) {
+ NotificationRecord r = mNotificationList.get(index);
+
+ if ((r.getNotification().flags & mustHaveFlags) != mustHaveFlags) {
+ return;
+ }
+ if ((r.getNotification().flags & mustNotHaveFlags) != 0) {
+ return;
+ }
+
+ mNotificationList.remove(index);
+
+ cancelNotificationLocked(r, sendDelete);
+ updateLightsLocked();
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Determine whether the userId applies to the notification in question, either because
+ * they match exactly, or one of them is USER_ALL (which is treated as a wildcard).
+ */
+ private boolean notificationMatchesUserId(NotificationRecord r, int userId) {
+ return
+ // looking for USER_ALL notifications? match everything
+ userId == UserHandle.USER_ALL
+ // a notification sent to USER_ALL matches any query
+ || r.getUserId() == UserHandle.USER_ALL
+ // an exact user match
+ || r.getUserId() == userId;
+ }
+
+ /**
+ * Cancels all notifications from a given package that have all of the
+ * {@code mustHaveFlags}.
+ */
+ boolean cancelAllNotificationsInt(String pkg, int mustHaveFlags,
+ int mustNotHaveFlags, boolean doit, int userId) {
+ EventLog.writeEvent(EventLogTags.NOTIFICATION_CANCEL_ALL, pkg, userId,
+ mustHaveFlags, mustNotHaveFlags);
+
+ synchronized (mNotificationList) {
+ final int N = mNotificationList.size();
+ boolean canceledSomething = false;
+ for (int i = N-1; i >= 0; --i) {
+ NotificationRecord r = mNotificationList.get(i);
+ if (!notificationMatchesUserId(r, userId)) {
+ continue;
+ }
+ // Don't remove notifications to all, if there's no package name specified
+ if (r.getUserId() == UserHandle.USER_ALL && pkg == null) {
+ continue;
+ }
+ if ((r.getFlags() & mustHaveFlags) != mustHaveFlags) {
+ continue;
+ }
+ if ((r.getFlags() & mustNotHaveFlags) != 0) {
+ continue;
+ }
+ if (pkg != null && !r.sbn.getPackageName().equals(pkg)) {
+ continue;
+ }
+ canceledSomething = true;
+ if (!doit) {
+ return true;
+ }
+ mNotificationList.remove(i);
+ cancelNotificationLocked(r, false);
+ }
+ if (canceledSomething) {
+ updateLightsLocked();
+ }
+ return canceledSomething;
+ }
+ }
+
+
+
+ // Return true if the UID is a system or phone UID and therefore should not have
+ // any notifications or toasts blocked.
+ boolean isUidSystem(int uid) {
+ final int appid = UserHandle.getAppId(uid);
+ return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);
+ }
+
+ // same as isUidSystem(int, int) for the Binder caller's UID.
+ boolean isCallerSystem() {
+ return isUidSystem(Binder.getCallingUid());
+ }
+
+ void checkCallerIsSystem() {
+ if (isCallerSystem()) {
+ return;
+ }
+ throw new SecurityException("Disallowed call for uid " + Binder.getCallingUid());
+ }
+
+ void checkCallerIsSystemOrSameApp(String pkg) {
+ if (isCallerSystem()) {
+ return;
+ }
+ final int uid = Binder.getCallingUid();
+ try {
+ ApplicationInfo ai = AppGlobals.getPackageManager().getApplicationInfo(
+ pkg, 0, UserHandle.getCallingUserId());
+ if (!UserHandle.isSameApp(ai.uid, uid)) {
+ throw new SecurityException("Calling uid " + uid + " gave package"
+ + pkg + " which is owned by uid " + ai.uid);
+ }
+ } catch (RemoteException re) {
+ throw new SecurityException("Unknown package " + pkg + "\n" + re);
+ }
+ }
+
+ void cancelAll(int userId) {
+ synchronized (mNotificationList) {
+ final int N = mNotificationList.size();
+ for (int i=N-1; i>=0; i--) {
+ NotificationRecord r = mNotificationList.get(i);
+
+ if (!notificationMatchesUserId(r, userId)) {
+ continue;
+ }
+
+ if ((r.getFlags() & (Notification.FLAG_ONGOING_EVENT
+ | Notification.FLAG_NO_CLEAR)) == 0) {
+ mNotificationList.remove(i);
+ cancelNotificationLocked(r, true);
+ }
+ }
+
+ updateLightsLocked();
+ }
+ }
+
+ // lock on mNotificationList
+ void updateLightsLocked()
+ {
+ // handle notification lights
+ if (mLedNotification == null) {
+ // get next notification, if any
+ int n = mLights.size();
+ if (n > 0) {
+ mLedNotification = mLights.get(n-1);
+ }
+ }
+
+ // Don't flash while we are in a call or screen is on
+ if (mLedNotification == null || mInCall || mScreenOn) {
+ mNotificationLight.turnOff();
+ } else {
+ final Notification ledno = mLedNotification.sbn.getNotification();
+ 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;
+ }
+ if (mNotificationPulseEnabled) {
+ // pulse repeatedly
+ mNotificationLight.setFlashing(ledARGB, Light.LIGHT_FLASH_TIMED,
+ ledOnMS, ledOffMS);
+ }
+ }
+ }
+
+ // lock on mNotificationList
+ int indexOfNotificationLocked(String pkg, String tag, int id, int userId)
+ {
+ ArrayList<NotificationRecord> list = mNotificationList;
+ final int len = list.size();
+ for (int i=0; i<len; i++) {
+ NotificationRecord r = list.get(i);
+ if (!notificationMatchesUserId(r, userId) || r.sbn.getId() != id) {
+ continue;
+ }
+ if (tag == null) {
+ if (r.sbn.getTag() != null) {
+ continue;
+ }
+ } else {
+ if (!tag.equals(r.sbn.getTag())) {
+ continue;
+ }
+ }
+ if (r.sbn.getPackageName().equals(pkg)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void updateNotificationPulse() {
+ synchronized (mNotificationList) {
+ updateLightsLocked();
+ }
+ }
+}