/* * 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; 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.IAudioService; 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.ServiceManager; import android.os.UserHandle; import android.os.UserManager; 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 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 INotificationManager.Stub { private static final String TAG = "NotificationService"; private static final boolean DBG = false; private static final int MAX_PACKAGE_NOTIFICATIONS = 50; // message codes private static final int MESSAGE_TIMEOUT = 2; private static final int LONG_DELAY = 3500; // 3.5 seconds private static final int SHORT_DELAY = 2000; // 2 seconds private static final long[] DEFAULT_VIBRATE_PATTERN = {0, 250, 250, 250}; private static final int VIBRATE_PATTERN_MAXLEN = 8 * 2 + 1; // up to eight bumps private static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_NOTIFICATION; private static final boolean SCORE_ONGOING_HIGHER = false; private static final int JUNK_SCORE = -1000; private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10; private 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 private static final int SCORE_INTERRUPTION_THRESHOLD = Notification.PRIORITY_LOW * NOTIFICATION_PRIORITY_MULTIPLIER; private static final boolean ENABLE_BLOCKED_NOTIFICATIONS = true; private static final boolean ENABLE_BLOCKED_TOASTS = true; private static final String ENABLED_NOTIFICATION_LISTENERS_SEPARATOR = ":"; final Context mContext; final IActivityManager mAm; final UserManager mUserManager; final IBinder mForegroundToken = new Binder(); private WorkerHandler mHandler; private StatusBarManagerService mStatusBar; private LightsService.Light mNotificationLight; private LightsService.Light mAttentionLight; private int mDefaultNotificationColor; private int mDefaultNotificationLedOn; private int mDefaultNotificationLedOff; private long[] mDefaultVibrationPattern; private long[] mFallbackVibrationPattern; private boolean mSystemReady; private int mDisabledNotifications; private NotificationRecord mSoundNotification; private NotificationRecord mVibrateNotification; private IAudioService mAudioService; private Vibrator mVibrator; // 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 private final ArrayList mNotificationList = new ArrayList(); private ArrayList mToastQueue; private ArrayList mLights = new ArrayList(); private NotificationRecord mLedNotification; private final AppOpsManager mAppOps; // contains connections to all connected listeners, including app services // and system listeners private ArrayList mListeners = new ArrayList(); // things that will be put into mListeners as soon as they're ready private ArrayList mServicesBinding = new ArrayList(); // lists the component names of all enabled (and therefore connected) listener // app services for the current user only private HashSet mEnabledListenersForCurrentUser = new HashSet(); // Just the packages from mEnabledListenersForCurrentUser private HashSet mEnabledListenerPackageNames = new HashSet(); // Notification control database. For now just contains disabled packages. private AtomicFile mPolicyFile; private HashSet mBlockedPackages = new HashSet(); 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"; private class NotificationListenerInfo implements 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. unregisterListener(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 mBuffer = new ArrayDeque(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 descendingIterator() { return mBuffer.descendingIterator(); } public Iterator ascendingIterator() { return mBuffer.iterator(); } public Iterator filter( final Iterator iter, final String pkg, final int userId) { return new Iterator() { 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 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 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 just want to know if notifications are OK for this package. */ public boolean areNotificationsEnabledForPackage(String pkg, int uid) { checkCallerIsSystem(); return (mAppOps.checkOpNoThrow(AppOpsManager.OP_POST_NOTIFICATION, uid, pkg) == AppOpsManager.MODE_ALLOWED); } /** 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; } public void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled) { checkCallerIsSystem(); 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 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 ""; } } /** * 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 mContext.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 installedServices = pm.queryIntentServicesAsUser( new Intent(NotificationListenerService.SERVICE_INTERFACE), PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, currentUser); Set installed = new HashSet(); 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 remaining = new ArrayList(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(mContext.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( mContext.getContentResolver(), Settings.Secure.ENABLED_NOTIFICATION_LISTENERS, currentUser); NotificationListenerInfo[] toRemove = new NotificationListenerInfo[mListeners.size()]; final ArrayList toAdd; synchronized (mNotificationList) { // unbind and remove all existing listeners toRemove = mListeners.toArray(toRemove); toAdd = new ArrayList(); final HashSet newEnabled = new HashSet(); final HashSet newPackages = new HashSet(); // decode the list of components if (flat != null) { String[] components = flat.split(ENABLED_NOTIFICATION_LISTENERS_SEPARATOR); for (int i=0; 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) { mContext.unbindService(info.connection); } } } Intent intent = new Intent(NotificationListenerService.SERVICE_INTERFACE); intent.setComponent(name); intent.putExtra(Intent.EXTRA_CLIENT_LABEL, com.android.internal.R.string.notification_listener_binding_label); intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity( mContext, 0, new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS), 0)); try { if (DBG) Slog.v(TAG, "binding: " + intent); if (!mContext.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 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 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) { mContext.unbindService(info.connection); } } } } } /** * 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 { mContext.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 */ private 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 */ private 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 list = new ArrayList(); synchronized (mNotificationList) { final int N = mNotificationList.size(); for (int i=0; i 0) { pw.println(prefix + " actions={"); final int N = notification.actions.length; for (int i=0; i %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 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 StatusBarManagerService.NotificationCallbacks mNotificationCallbacks = new StatusBarManagerService.NotificationCallbacks() { 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 = mAudioService.getRingtonePlayer(); if (player != null) { player.stopAsync(); } } catch (RemoteException e) { } finally { Binder.restoreCallingIdentity(identity); } identity = Binder.clearCallingIdentity(); try { mVibrator.cancel(); } finally { Binder.restoreCallingIdentity(identity); } } } } public void onClearAll() { // XXX to be totally correct, the caller should tell us which user // this is for. cancelAll(ActivityManager.getCurrentUser()); } 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()); } 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()); } public void onPanelRevealed() { synchronized (mNotificationList) { // sound mSoundNotification = null; long identity = Binder.clearCallingIdentity(); try { final IRingtonePlayer player = mAudioService.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(); } } 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 final int enabled = mContext.getPackageManager() .getApplicationEnabledSetting(pkgName); if (enabled == PackageManager.COMPONENT_ENABLED_STATE_ENABLED || enabled == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) { cancelNotifications = false; } } 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 = mContext.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 = mContext.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(); mHandler = new WorkerHandler(); mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE); importOldBlockDb(); mStatusBar = statusBar; statusBar.setNotificationCallbacks(mNotificationCallbacks); mNotificationLight = lights.getLight(LightsService.LIGHT_ID_NOTIFICATIONS); mAttentionLight = lights.getLight(LightsService.LIGHT_ID_ATTENTION); Resources resources = mContext.getResources(); mDefaultNotificationColor = resources.getColor( com.android.internal.R.color.config_defaultNotificationColor); mDefaultNotificationLedOn = resources.getInteger( com.android.internal.R.integer.config_defaultNotificationLedOn); mDefaultNotificationLedOff = resources.getInteger( com.android.internal.R.integer.config_defaultNotificationLedOff); mDefaultVibrationPattern = getLongArray(resources, com.android.internal.R.array.config_defaultNotificationVibePattern, VIBRATE_PATTERN_MAXLEN, DEFAULT_VIBRATE_PATTERN); mFallbackVibrationPattern = getLongArray(resources, com.android.internal.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(mContext.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); mContext.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"); mContext.registerReceiver(mIntentReceiver, pkgFilter); IntentFilter sdFilter = new IntentFilter(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); mContext.registerReceiver(mIntentReceiver, sdFilter); mSettingsObserver = new SettingsObserver(mHandler); mSettingsObserver.observe(); } /** * Read the old XML-based app block database and import those blockages into the AppOps system. */ private void importOldBlockDb() { loadBlockDb(); PackageManager pm = mContext.getPackageManager(); for (String pkg : mBlockedPackages) { PackageInfo info = null; try { info = pm.getPackageInfo(pkg, 0); setNotificationsEnabledForPackage(pkg, info.applicationInfo.uid, false); } catch (NameNotFoundException e) { // forget you } } mBlockedPackages.clear(); if (mPolicyFile != null) { mPolicyFile.delete(); } } void systemReady() { mAudioService = IAudioService.Stub.asInterface( ServiceManager.getService(Context.AUDIO_SERVICE)); // no beeping until we're basically done booting mSystemReady = true; // make sure our listener services are properly bound rebindListenerServices(); } // Toasts // ============================================================================ 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= 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); } } } 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); } } } private 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, false); 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; } } } } private 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, boolean immediate) { Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); long delay = immediate ? 0 : (r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY); mHandler.removeCallbacksAndMessages(r); 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 private int indexOfToastLocked(String pkg, ITransientNotification callback) { IBinder cbak = callback.asBinder(); ArrayList list = mToastQueue; int len = list.size(); for (int i=0; i list = mToastQueue; int N = list.size(); for (int i=0; i 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 // ============================================================================ public void enqueueNotificationWithTag(String pkg, String basePkg, String tag, int id, Notification notification, int[] idOut, int userId) { enqueueNotificationInternal(pkg, basePkg, Binder.getCallingUid(), Binder.getCallingPid(), tag, id, notification, idOut, userId); } private final static int clamp(int x, int low, int high) { return (x < low) ? low : ((x > high) ? high : x); } // Not exposed via Binder; for system use only (otherwise malicious apps could spoof the // uid/pid of another application) public void enqueueNotificationInternal(String pkg, String basePkg, int callingUid, int callingPid, String tag, int id, Notification notification, int[] idOut, int userId) { if (DBG) { Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id + " notification=" + notification); } checkCallerIsSystemOrSameApp(pkg); final boolean isSystemNotification = isCallerSystem() || ("android".equals(pkg)); userId = ActivityManager.handleIncomingUser(callingPid, callingUid, userId, 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= 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); } } // === 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 // 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; long identity = Binder.clearCallingIdentity(); try { mStatusBar.updateNotification(r.statusBarKey, n); } finally { Binder.restoreCallingIdentity(identity); } } else { 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) { 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) { final AudioManager audioManager = (AudioManager) mContext .getSystemService(Context.AUDIO_SERVICE); // 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 = mContext.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 speech recognition is active. if ((audioManager.getStreamVolume(audioStreamType) != 0) && !audioManager.isSpeechRecognitionActive()) { final long identity = Binder.clearCallingIdentity(); try { final IRingtonePlayer player = mAudioService.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 && (audioManager.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) && !(audioManager.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; } private void sendAccessibilityEvent(Notification notification, CharSequence packageName) { AccessibilityManager manager = AccessibilityManager.getInstance(mContext); 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) { 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 = mAudioService.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}. */ private void cancelNotification(String pkg, String tag, int id, int mustHaveFlags, int mustNotHaveFlags, boolean sendDelete, int userId) { 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; } } 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); } 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); } // Return true if the caller is a system or phone UID and therefore should not have // any notifications or toasts blocked. boolean isCallerSystem() { final int uid = Binder.getCallingUid(); final int appid = UserHandle.getAppId(uid); return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0); } 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 private 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, LightsService.LIGHT_FLASH_TIMED, ledOnMS, ledOffMS); } } } // lock on mNotificationList private int indexOfNotificationLocked(String pkg, String tag, int id, int userId) { ArrayList list = mNotificationList; final int len = list.size(); for (int i=0; i 0) { pw.println(" Toast Queue:"); for (int i=0; i 0) { pw.println(" Notification List:"); for (int i=0; i 0) { pw.println(" Lights List:"); for (int i=0; i= 5) { if (iter.hasNext()) pw.println(" ..."); break; } } } } }