blob: 8bd0f4ded5514aecdbe7fb55f0eb78afd0aae753 [file] [log] [blame]
/*
* 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.SystemApi;
import android.annotation.SdkConstant;
import android.app.INotificationManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.ArrayMap;
import android.util.Log;
import java.util.List;
/**
* A service that receives calls from the system when new notifications are
* posted or removed, or their ranking changed.
* <p>To extend this class, you must declare the service in your manifest file with
* the {@link android.Manifest.permission#BIND_NOTIFICATION_LISTENER_SERVICE} permission
* and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p>
* <pre>
* &lt;service android:name=".NotificationListener"
* android:label="&#64;string/service_name"
* android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
* &lt;intent-filter>
* &lt;action android:name="android.service.notification.NotificationListenerService" />
* &lt;/intent-filter>
* &lt;/service></pre>
*/
public abstract class NotificationListenerService extends Service {
// TAG = "NotificationListenerService[MySubclass]"
private final String TAG = NotificationListenerService.class.getSimpleName()
+ "[" + getClass().getSimpleName() + "]";
private INotificationListenerWrapper mWrapper = null;
private RankingMap mRankingMap;
private INotificationManager mNoMan;
/** Only valid after a successful call to (@link registerAsService}. */
private int mCurrentUser;
/**
* 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 void onNotificationPosted(StatusBarNotification sbn) {
// optional
}
/**
* 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).
* @param rankingMap The current ranking map that can be used to retrieve ranking information
* for active notifications, including the newly posted one.
*/
public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
onNotificationPosted(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.
* <P>
* NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
* result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
* fields such as {@link android.app.Notification#contentView} and
* {@link android.app.Notification#largeIcon}. However, all other fields on
* {@link StatusBarNotification}, sufficient to match this call with a prior call to
* {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
*
* @param sbn A data structure encapsulating at least the original information (tag and id)
* and source (package name) used to post the {@link android.app.Notification} that
* was just removed.
*/
public void onNotificationRemoved(StatusBarNotification sbn) {
// optional
}
/**
* 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.
* <P>
* NOTE: The {@link StatusBarNotification} object you receive will be "light"; that is, the
* result from {@link StatusBarNotification#getNotification} may be missing some heavyweight
* fields such as {@link android.app.Notification#contentView} and
* {@link android.app.Notification#largeIcon}. However, all other fields on
* {@link StatusBarNotification}, sufficient to match this call with a prior call to
* {@link #onNotificationPosted(StatusBarNotification)}, will be intact.
*
* @param sbn A data structure encapsulating at least the original information (tag and id)
* and source (package name) used to post the {@link android.app.Notification} that
* was just removed.
* @param rankingMap The current ranking map that can be used to retrieve ranking information
* for active notifications.
*
*/
public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap) {
onNotificationRemoved(sbn);
}
/**
* Implement this method to learn about when the listener is enabled and connected to
* the notification manager. You are safe to call {@link #getActiveNotifications()}
* at this time.
*/
public void onListenerConnected() {
// optional
}
/**
* Implement this method to be notified when the notification ranking changes.
*
* @param rankingMap The current ranking map that can be used to retrieve ranking information
* for active notifications.
*/
public void onNotificationRankingUpdate(RankingMap rankingMap) {
// optional
}
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)}.
* <p>
* @deprecated Use {@link #cancelNotification(String key)}
* instead. Beginning with {@link android.os.Build.VERSION_CODES#L} this method will no longer
* cancel the notification. It will continue to cancel the notification for applications
* whose {@code targetSdkVersion} is earlier than {@link android.os.Build.VERSION_CODES#L}.
*/
public final void cancelNotification(String pkg, String tag, int id) {
if (!isBound()) return;
try {
getNotificationInterface().cancelNotificationFromListener(
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 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.
* <p>
* @param key Notification to dismiss from {@link StatusBarNotification#getKey()}.
*/
public final void cancelNotification(String key) {
if (!isBound()) return;
try {
getNotificationInterface().cancelNotificationsFromListener(mWrapper,
new String[] {key});
} 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 #cancelNotification(String, String, int)}
*/
public final void cancelAllNotifications() {
cancelNotifications(null /*all*/);
}
/**
* Inform the notification manager about dismissal of specific notifications.
* <p>
* Use this if your listener has a user interface that allows the user to dismiss
* multiple notifications at once.
*
* @param keys Notifications to dismiss, or {@code null} to dismiss all.
*
* {@see #cancelNotification(String, String, int)}
*/
public final void cancelNotifications(String[] keys) {
if (!isBound()) return;
try {
getNotificationInterface().cancelNotificationsFromListener(mWrapper, keys);
} catch (android.os.RemoteException ex) {
Log.v(TAG, "Unable to contact notification manager", ex);
}
}
/**
* Request the list of outstanding notifications (that is, those that are visible to the
* current user). Useful when you don't know what's already been posted.
*
* @return An array of active notifications, sorted in natural order.
*/
public StatusBarNotification[] getActiveNotifications() {
if (!isBound()) return null;
try {
ParceledListSlice<StatusBarNotification> parceledList =
getNotificationInterface().getActiveNotificationsFromListener(mWrapper);
List<StatusBarNotification> list = parceledList.getList();
return list.toArray(new StatusBarNotification[list.size()]);
} catch (android.os.RemoteException ex) {
Log.v(TAG, "Unable to contact notification manager", ex);
}
return null;
}
/**
* Returns current ranking information.
*
* <p>
* The returned object represents the current ranking snapshot and only
* applies for currently active notifications.
* <p>
* Generally you should use the RankingMap that is passed with events such
* as {@link #onNotificationPosted(StatusBarNotification, RankingMap)},
* {@link #onNotificationRemoved(StatusBarNotification, RankingMap)}, and
* so on. This method should only be used when needing access outside of
* such events, for example to retrieve the RankingMap right after
* initialization.
*
* @return A {@link RankingMap} object providing access to ranking information
*/
public RankingMap getCurrentRanking() {
return mRankingMap;
}
@Override
public IBinder onBind(Intent intent) {
if (mWrapper == null) {
mWrapper = new INotificationListenerWrapper();
}
return mWrapper;
}
private boolean isBound() {
if (mWrapper == null) {
Log.w(TAG, "Notification listener service not yet bound.");
return false;
}
return true;
}
/**
* Directly register this service with the Notification Manager.
*
* <p>Only system services may use this call. It will fail for non-system callers.
* Apps should ask the user to add their listener in Settings.
*
* @param componentName the component that will consume the notification information
* @param currentUser the user to use as the stream filter
* @hide
*/
@SystemApi
public void registerAsSystemService(ComponentName componentName, int currentUser)
throws RemoteException {
if (mWrapper == null) {
mWrapper = new INotificationListenerWrapper();
}
INotificationManager noMan = getNotificationInterface();
noMan.registerListener(mWrapper, componentName, currentUser);
mCurrentUser = currentUser;
}
/**
* Directly unregister this service from the Notification Manager.
*
* <P>This method will fail for listeners that were not registered
* with (@link registerAsService).
* @hide
*/
@SystemApi
public void unregisterAsSystemService() throws RemoteException {
if (mWrapper != null) {
INotificationManager noMan = getNotificationInterface();
noMan.unregisterListener(mWrapper, mCurrentUser);
}
}
private class INotificationListenerWrapper extends INotificationListener.Stub {
@Override
public void onNotificationPosted(StatusBarNotification sbn,
NotificationRankingUpdate update) {
// protect subclass from concurrent modifications of (@link mNotificationKeys}.
synchronized (mWrapper) {
applyUpdate(update);
try {
NotificationListenerService.this.onNotificationPosted(sbn, mRankingMap);
} catch (Throwable t) {
Log.w(TAG, "Error running onNotificationPosted", t);
}
}
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn,
NotificationRankingUpdate update) {
// protect subclass from concurrent modifications of (@link mNotificationKeys}.
synchronized (mWrapper) {
applyUpdate(update);
try {
NotificationListenerService.this.onNotificationRemoved(sbn, mRankingMap);
} catch (Throwable t) {
Log.w(TAG, "Error running onNotificationRemoved", t);
}
}
}
@Override
public void onListenerConnected(NotificationRankingUpdate update) {
// protect subclass from concurrent modifications of (@link mNotificationKeys}.
synchronized (mWrapper) {
applyUpdate(update);
try {
NotificationListenerService.this.onListenerConnected();
} catch (Throwable t) {
Log.w(TAG, "Error running onListenerConnected", t);
}
}
}
@Override
public void onNotificationRankingUpdate(NotificationRankingUpdate update)
throws RemoteException {
// protect subclass from concurrent modifications of (@link mNotificationKeys}.
synchronized (mWrapper) {
applyUpdate(update);
try {
NotificationListenerService.this.onNotificationRankingUpdate(mRankingMap);
} catch (Throwable t) {
Log.w(TAG, "Error running onNotificationRankingUpdate", t);
}
}
}
}
private void applyUpdate(NotificationRankingUpdate update) {
mRankingMap = new RankingMap(update);
}
/**
* Provides access to ranking information on a currently active
* notification.
*
* <p>
* Note that this object is not updated on notification events (such as
* {@link #onNotificationPosted(StatusBarNotification, RankingMap)},
* {@link #onNotificationRemoved(StatusBarNotification)}, etc.). Make sure
* to retrieve a new Ranking from the current {@link RankingMap} whenever
* a notification event occurs.
*/
public static class Ranking {
private final String mKey;
private final int mRank;
private final boolean mIsAmbient;
private final boolean mIsInterceptedByDnd;
private Ranking(String key, int rank, boolean isAmbient, boolean isInterceptedByDnd) {
mKey = key;
mRank = rank;
mIsAmbient = isAmbient;
mIsInterceptedByDnd = isInterceptedByDnd;
}
/**
* Returns the key of the notification this Ranking applies to.
*/
public String getKey() {
return mKey;
}
/**
* Returns the rank of the notification.
*
* @return the rank of the notification, that is the 0-based index in
* the list of active notifications.
*/
public int getRank() {
return mRank;
}
/**
* Returns whether the notification is an ambient notification, that is
* a notification that doesn't require the user's immediate attention.
*/
public boolean isAmbient() {
return mIsAmbient;
}
/**
* Returns whether the notification was intercepted by
* &quot;Do not disturb&quot;.
*/
public boolean isInterceptedByDoNotDisturb() {
return mIsInterceptedByDnd;
}
}
/**
* Provides access to ranking information on currently active
* notifications.
*
* <p>
* Note that this object represents a ranking snapshot that only applies to
* notifications active at the time of retrieval.
*/
public static class RankingMap implements Parcelable {
private final NotificationRankingUpdate mRankingUpdate;
private final ArrayMap<String, Ranking> mRankingCache;
private boolean mRankingCacheInitialized;
private RankingMap(NotificationRankingUpdate rankingUpdate) {
mRankingUpdate = rankingUpdate;
mRankingCache = new ArrayMap<>(rankingUpdate.getOrderedKeys().length);
}
/**
* Request the list of notification keys in their current ranking
* order.
*
* @return An array of active notification keys, in their ranking order.
*/
public String[] getOrderedKeys() {
return mRankingUpdate.getOrderedKeys();
}
/**
* Returns the Ranking for the notification with the given key.
*
* @return the Ranking of the notification with the given key;
* <code>null</code> when the key is unknown.
*/
public Ranking getRanking(String key) {
synchronized (mRankingCache) {
if (!mRankingCacheInitialized) {
initializeRankingCache();
mRankingCacheInitialized = true;
}
}
return mRankingCache.get(key);
}
private void initializeRankingCache() {
String[] orderedKeys = mRankingUpdate.getOrderedKeys();
int firstAmbientIndex = mRankingUpdate.getFirstAmbientIndex();
for (int i = 0; i < orderedKeys.length; i++) {
String key = orderedKeys[i];
boolean isAmbient = firstAmbientIndex > -1 && firstAmbientIndex <= i;
boolean isInterceptedByDnd = false;
// TODO: Optimize.
for (String s : mRankingUpdate.getDndInterceptedKeys()) {
if (s.equals(key)) {
isInterceptedByDnd = true;
break;
}
}
mRankingCache.put(key, new Ranking(key, i, isAmbient, isInterceptedByDnd));
}
}
// ----------- Parcelable
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(mRankingUpdate, flags);
}
public static final Creator<RankingMap> CREATOR = new Creator<RankingMap>() {
@Override
public RankingMap createFromParcel(Parcel source) {
NotificationRankingUpdate rankingUpdate = source.readParcelable(null);
return new RankingMap(rankingUpdate);
}
@Override
public RankingMap[] newArray(int size) {
return new RankingMap[size];
}
};
}
}