New NotificationListenerService.

This is the best and only way for apps to listen for
notifications: create a NotificationListenerService, wait
for the NoMan to bind to you (as a result of the user
checking a box somewhere in Settings and agreeing to a
scary dialog box), and you'll start receiving notification
posted and dismissed callbacks. Your service, while enabled,
will also be able to clear one or all notifications.

Use this power wisely.

This change moves StatusBarNotification out of
com.android.internal into android.service.notification.
[Internal customers, including System UI and early users of
the system-only listener binder API, will need to be
updated.]

Bug: 8199624
Change-Id: I1be46f823d4b3ddc901109ec1e085cd6deb740c2
This commit is contained in:
Daniel Sandler
2013-03-22 18:29:23 -07:00
committed by Android (Google) Code Review
parent bab9687e64
commit 5feceebb89
26 changed files with 573 additions and 93 deletions

View File

@ -69,7 +69,6 @@ LOCAL_SRC_FILES += \
core/java/android/app/IAlarmManager.aidl \
core/java/android/app/IBackupAgent.aidl \
core/java/android/app/IInstrumentationWatcher.aidl \
core/java/android/app/INotificationListener.aidl \
core/java/android/app/INotificationManager.aidl \
core/java/android/app/IProcessObserver.aidl \
core/java/android/app/ISearchManager.aidl \
@ -148,6 +147,7 @@ LOCAL_SRC_FILES += \
core/java/android/os/IUpdateLock.aidl \
core/java/android/os/IUserManager.aidl \
core/java/android/os/IVibratorService.aidl \
core/java/android/service/notification/INotificationListener.aidl \
core/java/android/service/dreams/IDreamManager.aidl \
core/java/android/service/dreams/IDreamService.aidl \
core/java/android/service/wallpaper/IWallpaperConnection.aidl \

View File

@ -159,6 +159,7 @@ $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framew
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/com/android/internal/view/IInputMethodCallback.*)
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/com/android/internal/view/IInputMethodSession.*)
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/com/android/internal/view/IInputMethodCallback.*)
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/framework_intermediates)
# ************************************************
# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
# ************************************************

View File

@ -22,6 +22,7 @@ package android {
field public static final java.lang.String BIND_APPWIDGET = "android.permission.BIND_APPWIDGET";
field public static final java.lang.String BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN";
field public static final java.lang.String BIND_INPUT_METHOD = "android.permission.BIND_INPUT_METHOD";
field public static final java.lang.String BIND_NOTIFICATION_LISTENER_SERVICE = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE";
field public static final java.lang.String BIND_REMOTEVIEWS = "android.permission.BIND_REMOTEVIEWS";
field public static final java.lang.String BIND_TEXT_SERVICE = "android.permission.BIND_TEXT_SERVICE";
field public static final java.lang.String BIND_VPN_SERVICE = "android.permission.BIND_VPN_SERVICE";
@ -20839,6 +20840,38 @@ package android.service.dreams {
}
package android.service.notification {
public abstract class NotificationListenerService extends android.app.Service {
ctor public NotificationListenerService();
method public final void clearAllNotifications();
method public final void clearNotification(java.lang.String, java.lang.String, int);
method public android.os.IBinder onBind(android.content.Intent);
method public abstract void onNotificationPosted(android.service.notification.StatusBarNotification);
method public abstract void onNotificationRemoved(android.service.notification.StatusBarNotification);
field public static final java.lang.String SERVICE_INTERFACE = "android.service.notification.NotificationListenerService";
}
public class StatusBarNotification implements android.os.Parcelable {
ctor public StatusBarNotification(java.lang.String, java.lang.String, int, java.lang.String, int, int, int, android.app.Notification, android.os.UserHandle, long);
ctor public StatusBarNotification(android.os.Parcel);
method public android.service.notification.StatusBarNotification clone();
method public int describeContents();
method public int getUserId();
method public boolean isClearable();
method public boolean isOngoing();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator CREATOR;
field public final int id;
field public final android.app.Notification notification;
field public final java.lang.String pkg;
field public final long postTime;
field public final java.lang.String tag;
field public final android.os.UserHandle user;
}
}
package android.service.textservice {
public abstract class SpellCheckerService extends android.app.Service {

View File

@ -17,12 +17,12 @@
package android.app;
import android.app.INotificationListener;
import android.app.ITransientNotification;
import android.service.notification.StatusBarNotification;
import android.app.Notification;
import android.content.ComponentName;
import android.content.Intent;
import com.android.internal.statusbar.StatusBarNotification;
import android.service.notification.INotificationListener;
/** {@hide} */
interface INotificationManager
@ -41,7 +41,9 @@ interface INotificationManager
StatusBarNotification[] getActiveNotifications(String callingPkg);
StatusBarNotification[] getHistoricalNotifications(String callingPkg, int count);
void registerListener(in INotificationListener listener, String pkg, int userid);
void registerListener(in INotificationListener listener, in ComponentName component, int userid);
void unregisterListener(in INotificationListener listener, int userid);
}
void clearNotificationFromListener(in INotificationListener token, String pkg, String tag, int id);
void clearAllNotificationsFromListener(in INotificationListener token);
}

View File

@ -655,6 +655,22 @@ public final class Settings {
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_DREAM_SETTINGS = "android.settings.DREAM_SETTINGS";
/**
* Activity Action: Show Notification listener settings.
* <p>
* In some cases, a matching Activity may not exist, so ensure you
* safeguard against this.
* <p>
* Input: Nothing.
* <p>
* Output: Nothing.
* @see android.service.notification.NotificationListenerService
* @hide
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_NOTIFICATION_LISTENER_SETTINGS
= "android.settings.NOTIFICATION_LISTENER_SETTINGS";
// End of Intent actions for Settings
/**

View File

@ -14,9 +14,9 @@
* limitations under the License.
*/
package android.app;
package android.service.notification;
import com.android.internal.statusbar.StatusBarNotification;
import android.service.notification.StatusBarNotification;
/** @hide */
oneway interface INotificationListener

View File

@ -0,0 +1,138 @@
/*
* Copyright (C) 2013 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 android.service.notification;
import android.annotation.SdkConstant;
import android.app.INotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.ServiceManager;
import android.util.Log;
public abstract class NotificationListenerService extends Service {
// TAG = "NotificationListenerService[MySubclass]"
private final String TAG = NotificationListenerService.class.getSimpleName()
+ "[" + getClass().getSimpleName() + "]";
private INotificationListenerWrapper mWrapper = null;
private INotificationManager mNoMan;
/**
* The {@link Intent} that must be declared as handled by the service.
*/
@SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
public static final String SERVICE_INTERFACE
= "android.service.notification.NotificationListenerService";
/**
* Implement this method to learn about new notifications as they are posted by apps.
*
* @param sbn A data structure encapsulating the original {@link android.app.Notification}
* object as well as its identifying information (tag and id) and source
* (package name).
*/
public abstract void onNotificationPosted(StatusBarNotification sbn);
/**
* Implement this method to learn when notifications are removed.
* <P>
* This might occur because the user has dismissed the notification using system UI (or another
* notification listener) or because the app has withdrawn the notification.
*
* @param sbn A data structure encapsulating the original {@link android.app.Notification}
* object as well as its identifying information (tag and id) and source
* (package name).
*/
public abstract void onNotificationRemoved(StatusBarNotification sbn);
private final INotificationManager getNotificationInterface() {
if (mNoMan == null) {
mNoMan = INotificationManager.Stub.asInterface(
ServiceManager.getService(Context.NOTIFICATION_SERVICE));
}
return mNoMan;
}
/**
* Inform the notification manager about dismissal of a single notification.
* <p>
* Use this if your listener has a user interface that allows the user to dismiss individual
* notifications, similar to the behavior of Android's status bar and notification panel.
* It should be called after the user dismisses a single notification using your UI;
* upon being informed, the notification manager will actually remove the notification
* and you will get an {@link #onNotificationRemoved(StatusBarNotification)} callback.
* <P>
* <b>Note:</b> If your listener allows the user to fire a notification's
* {@link android.app.Notification#contentIntent} by tapping/clicking/etc., you should call
* this method at that time <i>if</i> the Notification in question has the
* {@link android.app.Notification#FLAG_AUTO_CANCEL} flag set.
*
* @param pkg Package of the notifying app.
* @param tag Tag of the notification as specified by the notifying app in
* {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}.
* @param id ID of the notification as specified by the notifying app in
* {@link android.app.NotificationManager#notify(String, int, android.app.Notification)}.
*/
public final void clearNotification(String pkg, String tag, int id) {
try {
getNotificationInterface().clearNotificationFromListener(mWrapper, pkg, tag, id);
} catch (android.os.RemoteException ex) {
Log.v(TAG, "Unable to contact notification manager", ex);
}
}
/**
* Inform the notification manager about dismissal of all notifications.
* <p>
* Use this if your listener has a user interface that allows the user to dismiss all
* notifications, similar to the behavior of Android's status bar and notification panel.
* It should be called after the user invokes the "dismiss all" function of your UI;
* upon being informed, the notification manager will actually remove all active notifications
* and you will get multiple {@link #onNotificationRemoved(StatusBarNotification)} callbacks.
*
* {@see #clearNotification(String, String, int)}
*/
public final void clearAllNotifications() {
try {
getNotificationInterface().clearAllNotificationsFromListener(mWrapper);
} catch (android.os.RemoteException ex) {
Log.v(TAG, "Unable to contact notification manager", ex);
}
}
@Override
public IBinder onBind(Intent intent) {
if (mWrapper == null) {
mWrapper = new INotificationListenerWrapper();
}
return mWrapper;
}
private class INotificationListenerWrapper extends INotificationListener.Stub {
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
NotificationListenerService.this.onNotificationPosted(sbn);
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
NotificationListenerService.this.onNotificationRemoved(sbn);
}
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.internal.statusbar;
package android.service.notification;
parcelable StatusBarNotification;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.internal.statusbar;
package android.service.notification;
import android.app.Notification;
import android.os.Parcel;
@ -23,34 +23,54 @@ import android.os.UserHandle;
/**
* Class encapsulating a Notification. Sent by the NotificationManagerService to clients including
* the IStatusBar (in System UI).
* the status bar and any {@link android.service.notification.NotificationListenerService}s.
*/
public class StatusBarNotification implements Parcelable {
/** The package of the app that posted the notification. */
public final String pkg;
public final String basePkg;
/** The id supplied to {@link android.app.NotificationManager#notify}. */
public final int id;
/** The tag supplied to {@link android.app.NotificationManager#notify}, or null if no tag
* was specified. */
public final String tag;
/** The notifying app's calling uid. @hide */
public final int uid;
/** The notifying app's base package. @hide */
public final String basePkg;
/** @hide */
public final int initialPid;
// TODO: make this field private and move callers to an accessor that
// ensures sourceUser is applied.
/** The {@link android.app.Notification} supplied to
* {@link android.app.NotificationManager#notify}. */
public final Notification notification;
public final int score;
/** The {@link android.os.UserHandle} for whom this notification is intended. */
public final UserHandle user;
/** The time (in {@link System#currentTimeMillis} time) the notification was posted,
* which may be different than {@link android.app.Notification#when}.
*/
public final long postTime;
/** This is temporarily needed for the JB MR1 PDK. */
/** @hide */
public final int score;
/** This is temporarily needed for the JB MR1 PDK.
* @hide */
@Deprecated
public StatusBarNotification(String pkg, int id, String tag, int uid, int initialPid, int score,
Notification notification) {
this(pkg, id, tag, uid, initialPid, score, notification, UserHandle.OWNER);
}
/** @hide */
public StatusBarNotification(String pkg, int id, String tag, int uid, int initialPid, int score,
Notification notification, UserHandle user) {
this(pkg, null, id, tag, uid, initialPid, score, notification, user);
}
/** @hide */
public StatusBarNotification(String pkg, String basePkg, int id, String tag, int uid,
int initialPid, int score, Notification notification, UserHandle user) {
this(pkg, basePkg, id, tag, uid, initialPid, score, notification, user,
@ -147,10 +167,17 @@ public class StatusBarNotification implements Parcelable {
this.score, this.notification);
}
/** Convenience method to check the notification's flags for
* {@link Notification#FLAG_ONGOING_EVENT}.
*/
public boolean isOngoing() {
return (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0;
}
/** Convenience method to check the notification's flags for
* either {@link Notification#FLAG_ONGOING_EVENT} or
* {@link Notification#FLAG_NO_CLEAR}.
*/
public boolean isClearable() {
return ((notification.flags & Notification.FLAG_ONGOING_EVENT) == 0)
&& ((notification.flags & Notification.FLAG_NO_CLEAR) == 0);

View File

@ -17,7 +17,7 @@
package com.android.internal.statusbar;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarNotification;
import android.service.notification.StatusBarNotification;
/** @hide */
oneway interface IStatusBar

View File

@ -19,7 +19,7 @@ package com.android.internal.statusbar;
import com.android.internal.statusbar.IStatusBar;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarIconList;
import com.android.internal.statusbar.StatusBarNotification;
import android.service.notification.StatusBarNotification;
/** @hide */
interface IStatusBarService

View File

@ -2193,6 +2193,14 @@
android:description="@string/permdesc_accessNotifications"
android:protectionLevel="signature|system" />
<!-- Must be required by an {@link
android.service.notification.NotificationListenerService},
to ensure that only the system can bind to it. -->
<permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:label="@string/permlab_bindNotificationListenerService"
android:description="@string/permdesc_bindNotificationListenerService"
android:protectionLevel="signature" />
<!-- The system process is explicitly the only one allowed to launch the
confirmation UI for full backup/restore -->
<uses-permission android:name="android.permission.CONFIRM_FULL_BACKUP"/>

View File

@ -1816,6 +1816,11 @@
<!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
<string name="permdesc_accessNotifications">Allows the app to retrieve, examine, and clear notifications, including those posted by other apps.</string>
<!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
<string name="permlab_bindNotificationListenerService">bind to a notification listener service</string>
<!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
<string name="permdesc_bindNotificationListenerService">Allows the holder to bind to the top-level interface of a notification listener service. Should never be needed for normal apps.</string>
<!-- Policy administration -->
<!-- Title of policy access to limiting the user's password choices -->
@ -3508,6 +3513,9 @@
<string name="wallpaper_binding_label">Wallpaper</string>
<!-- Dialog title for user to select a different wallpaper from service list -->
<string name="chooser_wallpaper">Change wallpaper</string>
<!-- Label to show for a service that is running because it is observing
the user's notifications. -->
<string name="notification_listener_binding_label">Notification listener</string>
<!-- Do Not Translate: Alternate eri.xml -->
<string name="alternate_eri_file">/data/eri.xml</string>

View File

@ -1638,6 +1638,7 @@
<java-symbol type="string" name="launch_warning_title" />
<java-symbol type="string" name="low_internal_storage_view_text" />
<java-symbol type="string" name="low_internal_storage_view_title" />
<java-symbol type="string" name="notification_listener_binding_label" />
<java-symbol type="string" name="report" />
<java-symbol type="string" name="select_input_method" />
<java-symbol type="string" name="select_keyboard_layout_notification_title" />

View File

@ -17,11 +17,11 @@
package com.android.systemui.statusbar;
import android.service.notification.StatusBarNotification;
import android.content.res.Configuration;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarIconList;
import com.android.internal.statusbar.StatusBarNotification;
import com.android.internal.widget.SizeAdaptiveLayout;
import com.android.systemui.R;
import com.android.systemui.SearchPanelView;

View File

@ -20,10 +20,10 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.service.notification.StatusBarNotification;
import com.android.internal.statusbar.IStatusBar;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarIconList;
import com.android.internal.statusbar.StatusBarNotification;
/**
* This class takes the functions from IStatusBar that come in on

View File

@ -16,12 +16,11 @@
package com.android.systemui.statusbar;
import android.app.Notification;
import android.service.notification.StatusBarNotification;
import android.os.IBinder;
import android.view.View;
import android.widget.ImageView;
import com.android.internal.statusbar.StatusBarNotification;
import com.android.systemui.R;
import java.util.Comparator;

View File

@ -26,6 +26,7 @@ import android.app.ActivityManagerNative;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.StatusBarManager;
import android.service.notification.StatusBarNotification;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -76,7 +77,6 @@ import android.widget.ScrollView;
import android.widget.TextView;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarNotification;
import com.android.systemui.EventLogTags;
import com.android.systemui.R;
import com.android.systemui.statusbar.BaseStatusBar;

View File

@ -16,6 +16,7 @@
package com.android.systemui.statusbar.phone;
import android.service.notification.StatusBarNotification;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
@ -23,10 +24,7 @@ import android.os.Handler;
import android.text.StaticLayout;
import android.text.Layout.Alignment;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.Slog;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.TextSwitcher;
import android.widget.TextView;
@ -35,7 +33,6 @@ import android.widget.ImageSwitcher;
import java.util.ArrayList;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarNotification;
import com.android.internal.util.CharSequences;
import com.android.systemui.R;

View File

@ -28,16 +28,11 @@ import android.content.IntentFilter;
import android.location.LocationManager;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Slog;
import android.view.View;
import android.widget.ImageView;
// private NM API
import android.app.INotificationManager;
import com.android.internal.statusbar.StatusBarNotification;
import com.android.systemui.R;
import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
public class LocationController extends BroadcastReceiver {
private static final String TAG = "StatusBar.LocationController";

View File

@ -23,6 +23,7 @@ import android.app.ActivityManagerNative;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.StatusBarManager;
import android.service.notification.StatusBarNotification;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -58,7 +59,6 @@ import android.widget.ScrollView;
import android.widget.TextView;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarNotification;
import com.android.systemui.R;
import com.android.systemui.statusbar.BaseStatusBar;
import com.android.systemui.statusbar.CommandQueue;

View File

@ -21,9 +21,9 @@ import java.util.Arrays;
import android.animation.LayoutTransition;
import android.app.Notification;
import android.app.PendingIntent;
import android.service.notification.StatusBarNotification;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import android.os.Handler;
@ -37,11 +37,9 @@ import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarNotification;
import com.android.systemui.R;
import com.android.systemui.statusbar.StatusBarIconView;

View File

@ -16,8 +16,8 @@
package com.android.systemui.statusbar.tv;
import android.service.notification.StatusBarNotification;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarNotification;
import com.android.systemui.statusbar.BaseStatusBar;
import android.os.IBinder;

View File

@ -26,16 +26,17 @@ import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.IActivityManager;
import android.app.INotificationManager;
import android.app.INotificationListener;
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;
@ -57,6 +58,9 @@ 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;
@ -68,8 +72,6 @@ import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.Toast;
import com.android.internal.statusbar.StatusBarNotification;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@ -121,6 +123,8 @@ public class NotificationManagerService extends INotificationManager.Stub
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;
@ -163,8 +167,18 @@ public class NotificationManagerService extends INotificationManager.Stub
private final AppOpsManager mAppOps;
private ArrayList<NotificationListenerInfo> mListeners = new ArrayList<NotificationListenerInfo>();
private ArrayList<String> mEnabledListenersForCurrentUser = new ArrayList<String>();
// 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;
@ -181,27 +195,42 @@ public class NotificationManagerService extends INotificationManager.Stub
private class NotificationListenerInfo implements DeathRecipient {
INotificationListener listener;
String pkg;
ComponentName component;
int userid;
boolean isSystem;
ServiceConnection connection;
public NotificationListenerInfo(INotificationListener listener, String pkg, int userid,
boolean isSystem) {
public NotificationListenerInfo(INotificationListener listener, ComponentName component,
int userid, boolean isSystem) {
this.listener = listener;
this.pkg = pkg;
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 (!(isSystem || isEnabledForUser(nid))) return false;
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;
if (!enabledAndUserMatches(sbn)) {
return;
}
try {
listener.onNotificationPosted(sbn);
} catch (RemoteException ex) {
@ -220,15 +249,17 @@ public class NotificationManagerService extends INotificationManager.Stub
@Override
public void binderDied() {
unregisterListener(this.listener, this.userid);
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 isEnabledForUser(int userid) {
for (int i=0; i<mEnabledListenersForCurrentUser.size(); i++) {
if (this.pkg.equals(mEnabledListenersForCurrentUser.get(i))) return true;
}
return false;
public boolean isEnabledForCurrentUser() {
if (this.isSystem) return true;
if (this.connection == null) return false;
return mEnabledListenersForCurrentUser.contains(this.component);
}
}
@ -434,6 +465,12 @@ public class NotificationManagerService extends INotificationManager.Stub
}
}
/**
* 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,
@ -456,6 +493,12 @@ public class NotificationManagerService extends INotificationManager.Stub
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
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS,
@ -474,27 +517,76 @@ public class NotificationManagerService extends INotificationManager.Stub
return tmp;
}
boolean packageCanTapNotificationsForUser(final int uid, final String pkg) {
// Make sure the package and uid match, and that the package is allowed access
return (AppOpsManager.MODE_ALLOWED
== mAppOps.checkOpNoThrow(AppOpsManager.OP_ACCESS_NOTIFICATIONS, uid, pkg));
/**
* 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() {
String flat = Settings.Secure.getString(
mContext.getContentResolver(),
Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
NotificationListenerInfo[] toRemove = new NotificationListenerInfo[mListeners.size()];
final ArrayList<ComponentName> toAdd;
final int currentUser = ActivityManager.getCurrentUser();
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);
}
}
/**
* 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 String pkg, final int userid) {
// ensure system or allowed pkg
int uid = Binder.getCallingUid();
boolean isSystem = (UserHandle.getAppId(uid) == Process.SYSTEM_UID || uid == 0);
if (!(isSystem || packageCanTapNotificationsForUser(uid, pkg))) {
throw new SecurityException("Package " + pkg
+ " may not listen for notifications");
}
final ComponentName component, final int userid) {
checkCallerIsSystem();
synchronized (mNotificationList) {
try {
NotificationListenerInfo info
= new NotificationListenerInfo(listener, pkg, userid, isSystem);
= new NotificationListenerInfo(listener, component, userid, true);
listener.asBinder().linkToDeath(info, 0);
mListeners.add(info);
} catch (RemoteException e) {
@ -503,6 +595,90 @@ public class NotificationManagerService extends INotificationManager.Stub
}
}
/**
* 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) {
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,
@ -513,12 +689,39 @@ public class NotificationManagerService extends INotificationManager.Stub
for (int i=N-1; i>=0; i--) {
final NotificationListenerInfo info = mListeners.get(i);
if (info.listener == listener && info.userid == userid) {
mListeners.remove(listener);
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) {
mContext.unbindService(info.connection);
}
}
}
}
}
/**
* asynchronously notify all listeners about a new notification
*/
private void notifyPostedLocked(NotificationRecord n) {
final StatusBarNotification sbn = n.sbn;
for (final NotificationListenerInfo info : mListeners) {
@ -530,6 +733,9 @@ public class NotificationManagerService extends INotificationManager.Stub
}
}
/**
* asynchronously notify all listeners about a removed notification
*/
private void notifyRemovedLocked(NotificationRecord n) {
final StatusBarNotification sbn = n.sbn;
for (final NotificationListenerInfo info : mListeners) {
@ -541,6 +747,57 @@ public class NotificationManagerService extends INotificationManager.Stub
}
}
// -- 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);
}
/**
* 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
*/
public void clearAllNotificationsFromListener(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
*/
public void clearNotificationFromListener(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);
}
}
// -- end of listener APIs --
public static final class NotificationRecord
{
final StatusBarNotification sbn;
@ -759,12 +1016,23 @@ public class NotificationManagerService extends INotificationManager.Stub
}
pkgList = new String[]{pkgName};
}
boolean anyListenersInvolved = false;
if (pkgList != null && (pkgList.length > 0)) {
for (String pkgName : pkgList) {
cancelAllNotificationsInt(pkgName, 0, 0, !queryRestart,
UserHandle.USER_ALL);
if (mEnabledListenerPackageNames.contains(pkgName)) {
anyListenersInvolved = true;
}
}
}
if (anyListenersInvolved) {
// 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.
@ -795,7 +1063,7 @@ public class NotificationManagerService extends INotificationManager.Stub
= Settings.System.getUriFor(Settings.System.NOTIFICATION_LIGHT_PULSE);
private final Uri ENABLED_NOTIFICATION_LISTENERS_URI
= Settings.System.getUriFor(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
= Settings.Secure.getUriFor(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
SettingsObserver(Handler handler) {
super(handler);
@ -804,9 +1072,9 @@ public class NotificationManagerService extends INotificationManager.Stub
void observe() {
ContentResolver resolver = mContext.getContentResolver();
resolver.registerContentObserver(NOTIFICATION_LIGHT_PULSE_URI,
false, this);
false, this, UserHandle.USER_ALL);
resolver.registerContentObserver(ENABLED_NOTIFICATION_LISTENERS_URI,
false, this);
false, this, UserHandle.USER_ALL);
update(null);
}
@ -825,19 +1093,7 @@ public class NotificationManagerService extends INotificationManager.Stub
}
}
if (uri == null || ENABLED_NOTIFICATION_LISTENERS_URI.equals(uri)) {
String pkglist = Settings.Secure.getString(
mContext.getContentResolver(),
Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
mEnabledListenersForCurrentUser.clear();
if (pkglist != null) {
String[] pkgs = pkglist.split(";");
for (int i=0; i<pkgs.length; i++) {
final String pkg = pkgs[i];
if (pkg != null && ! "".equals(pkg)) {
mEnabledListenersForCurrentUser.add(pkgs[i]);
}
}
}
rebindListenerServices();
}
}
}
@ -956,6 +1212,9 @@ public class NotificationManagerService extends INotificationManager.Stub
// no beeping until we're basically done booting
mSystemReady = true;
// make sure our listener services are properly bound
rebindListenerServices();
}
// Toasts
@ -1781,16 +2040,17 @@ public class NotificationManagerService extends INotificationManager.Stub
pw.println("Current Notification Manager state:");
pw.print(" Enabled listeners: [");
for (String pkg : mEnabledListenersForCurrentUser) {
pw.print(" " + pkg);
pw.println(" Listeners (" + mEnabledListenersForCurrentUser.size()
+ ") enabled for current user:");
for (ComponentName cmpt : mEnabledListenersForCurrentUser) {
pw.println(" " + cmpt);
}
pw.println(" ]");
pw.println(" Live listeners:");
pw.println(" Live listeners (" + mListeners.size() + "):");
for (NotificationListenerInfo info : mListeners) {
pw.println(" " + info.pkg + " (user " + info.userid + "): " + info.listener
+ (info.isSystem?" SYSTEM":""));
pw.println(" " + info.component
+ " (user " + info.userid + "): " + info.listener
+ (info.isSystem?" SYSTEM":""));
}
int N;

View File

@ -17,6 +17,7 @@
package com.android.server;
import android.app.StatusBarManager;
import android.service.notification.StatusBarNotification;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -33,7 +34,6 @@ import com.android.internal.statusbar.IStatusBar;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.statusbar.StatusBarIconList;
import com.android.internal.statusbar.StatusBarNotification;
import com.android.server.wm.WindowManagerService;
import java.io.FileDescriptor;

View File

@ -33,13 +33,10 @@ import android.util.Log;
import android.net.Uri;
import android.os.SystemClock;
import android.widget.RemoteViews;
import android.widget.TextView;
import android.widget.ProgressBar;
import android.os.PowerManager;
// private NM API
import android.app.INotificationManager;
import com.android.internal.statusbar.StatusBarNotification;
public class NotificationTestList extends TestActivity
{