notification ranking infrastructure
Added an ordered list of notifications (n.b. a complete ordering). Added a mechanism for ranking to be updated asynchronously Added onNotificationRankingUpdate to NotificationListeners Added an opaque order update object and a convenience comparator that uses it to sort notifications for listeners Repurpose scorers to be ranking preprocessors. The preprocessors will perform heavy-weight validation of the notification object and memoize the results to improve efficiency of the ranking comparator. Current internal comparator implements status quo ordering, except that notes with a valid contact sort to the top of their priority bucket. Change-Id: I7244c65944a9657df41fb313b3cb5a52e149709d
This commit is contained in:
@@ -25161,16 +25161,24 @@ package android.service.notification {
|
||||
method public final deprecated void cancelNotification(java.lang.String, java.lang.String, int);
|
||||
method public final void cancelNotification(java.lang.String);
|
||||
method public final void cancelNotifications(java.lang.String[]);
|
||||
method public java.lang.String[] getActiveNotificationKeys();
|
||||
method public android.service.notification.StatusBarNotification[] getActiveNotifications();
|
||||
method public android.service.notification.StatusBarNotification[] getActiveNotifications(java.lang.String[]);
|
||||
method public java.lang.String[] getOrderedNotificationKeys();
|
||||
method public android.os.IBinder onBind(android.content.Intent);
|
||||
method public void onListenerConnected(java.lang.String[]);
|
||||
method public void onNotificationOrderUpdate();
|
||||
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 NotificationOrderUpdate implements android.os.Parcelable {
|
||||
ctor public NotificationOrderUpdate(android.os.Parcel);
|
||||
method public int describeContents();
|
||||
method public void writeToParcel(android.os.Parcel, int);
|
||||
field public static final android.os.Parcelable.Creator CREATOR;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -659,8 +659,8 @@ public class Notification implements Parcelable
|
||||
|
||||
/**
|
||||
* @hide
|
||||
* Extra added by NotificationManagerService to indicate whether a NotificationScorer
|
||||
* modified the Notifications's score.
|
||||
* Extra added by NotificationManagerService to indicate whether
|
||||
* the Notifications's score has been modified.
|
||||
*/
|
||||
public static final String EXTRA_SCORE_MODIFIED = "android.scoreModified";
|
||||
|
||||
|
||||
@@ -17,11 +17,15 @@
|
||||
package android.service.notification;
|
||||
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.service.notification.NotificationOrderUpdate;
|
||||
|
||||
/** @hide */
|
||||
oneway interface INotificationListener
|
||||
{
|
||||
void onListenerConnected(in String[] notificationKeys);
|
||||
void onNotificationPosted(in StatusBarNotification notification);
|
||||
void onNotificationRemoved(in StatusBarNotification notification);
|
||||
void onListenerConnected(in NotificationOrderUpdate update);
|
||||
void onNotificationPosted(in StatusBarNotification notification,
|
||||
in NotificationOrderUpdate update);
|
||||
void onNotificationRemoved(in StatusBarNotification notification,
|
||||
in NotificationOrderUpdate update);
|
||||
void onNotificationOrderUpdate(in NotificationOrderUpdate update);
|
||||
}
|
||||
@@ -22,10 +22,13 @@ import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
import android.os.UserHandle;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* A service that receives calls from the system when new notifications are posted or removed.
|
||||
* <p>To extend this class, you must declare the service in your manifest file with
|
||||
@@ -46,6 +49,7 @@ public abstract class NotificationListenerService extends Service {
|
||||
+ "[" + getClass().getSimpleName() + "]";
|
||||
|
||||
private INotificationListenerWrapper mWrapper = null;
|
||||
private String[] mNotificationKeys;
|
||||
|
||||
private INotificationManager mNoMan;
|
||||
|
||||
@@ -95,6 +99,15 @@ public abstract class NotificationListenerService extends Service {
|
||||
// optional
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement this method to be notified when the notification order cahnges.
|
||||
*
|
||||
* Call {@link #getOrderedNotificationKeys()} to retrieve the new order.
|
||||
*/
|
||||
public void onNotificationOrderUpdate() {
|
||||
// optional
|
||||
}
|
||||
|
||||
private final INotificationManager getNotificationInterface() {
|
||||
if (mNoMan == null) {
|
||||
mNoMan = INotificationManager.Stub.asInterface(
|
||||
@@ -202,7 +215,7 @@ public abstract class NotificationListenerService extends Service {
|
||||
* 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.
|
||||
* @return An array of active notifications, sorted in natural order.
|
||||
*/
|
||||
public StatusBarNotification[] getActiveNotifications() {
|
||||
return getActiveNotifications(null /*all*/);
|
||||
@@ -213,7 +226,8 @@ public abstract class NotificationListenerService extends Service {
|
||||
* current user). Useful when you don't know what's already been posted.
|
||||
*
|
||||
* @param keys A specific list of notification keys, or {@code null} for all.
|
||||
* @return An array of active notifications.
|
||||
* @return An array of active notifications, sorted in natural order
|
||||
* if {@code keys} is {@code null}.
|
||||
*/
|
||||
public StatusBarNotification[] getActiveNotifications(String[] keys) {
|
||||
if (!isBound()) return null;
|
||||
@@ -226,21 +240,15 @@ public abstract class NotificationListenerService extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the list of outstanding notification keys(that is, those that are visible to the
|
||||
* current user). You can use the notification keys for subsequent retrieval via
|
||||
* Request the list of notification keys in their current natural order.
|
||||
* You can use the notification keys for subsequent retrieval via
|
||||
* {@link #getActiveNotifications(String[]) or dismissal via
|
||||
* {@link #cancelNotifications(String[]).
|
||||
*
|
||||
* @return An array of active notification keys.
|
||||
* @return An array of active notification keys, in their natural order.
|
||||
*/
|
||||
public String[] getActiveNotificationKeys() {
|
||||
if (!isBound()) return null;
|
||||
try {
|
||||
return getNotificationInterface().getActiveNotificationKeysFromListener(mWrapper);
|
||||
} catch (android.os.RemoteException ex) {
|
||||
Log.v(TAG, "Unable to contact notification manager", ex);
|
||||
}
|
||||
return null;
|
||||
public String[] getOrderedNotificationKeys() {
|
||||
return mNotificationKeys;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -261,28 +269,60 @@ public abstract class NotificationListenerService extends Service {
|
||||
|
||||
private class INotificationListenerWrapper extends INotificationListener.Stub {
|
||||
@Override
|
||||
public void onNotificationPosted(StatusBarNotification sbn) {
|
||||
public void onNotificationPosted(StatusBarNotification sbn,
|
||||
NotificationOrderUpdate update) {
|
||||
try {
|
||||
NotificationListenerService.this.onNotificationPosted(sbn);
|
||||
// protect subclass from concurrent modifications of (@link mNotificationKeys}.
|
||||
synchronized (mWrapper) {
|
||||
updateNotificationKeys(update);
|
||||
NotificationListenerService.this.onNotificationPosted(sbn);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "Error running onNotificationPosted", t);
|
||||
Log.w(TAG, "Error running onOrderedNotificationPosted", t);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onNotificationRemoved(StatusBarNotification sbn) {
|
||||
public void onNotificationRemoved(StatusBarNotification sbn,
|
||||
NotificationOrderUpdate update) {
|
||||
try {
|
||||
NotificationListenerService.this.onNotificationRemoved(sbn);
|
||||
// protect subclass from concurrent modifications of (@link mNotificationKeys}.
|
||||
synchronized (mWrapper) {
|
||||
updateNotificationKeys(update);
|
||||
NotificationListenerService.this.onNotificationRemoved(sbn);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "Error running onNotificationRemoved", t);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onListenerConnected(String[] notificationKeys) {
|
||||
public void onListenerConnected(NotificationOrderUpdate update) {
|
||||
try {
|
||||
NotificationListenerService.this.onListenerConnected(notificationKeys);
|
||||
// protect subclass from concurrent modifications of (@link mNotificationKeys}.
|
||||
synchronized (mWrapper) {
|
||||
updateNotificationKeys(update);
|
||||
NotificationListenerService.this.onListenerConnected(mNotificationKeys);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "Error running onListenerConnected", t);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onNotificationOrderUpdate(NotificationOrderUpdate update)
|
||||
throws RemoteException {
|
||||
try {
|
||||
// protect subclass from concurrent modifications of (@link mNotificationKeys}.
|
||||
synchronized (mWrapper) {
|
||||
updateNotificationKeys(update);
|
||||
NotificationListenerService.this.onNotificationOrderUpdate();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "Error running onNotificationOrderUpdate", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateNotificationKeys(NotificationOrderUpdate update) {
|
||||
// TODO: avoid garbage by comparing the lists
|
||||
mNotificationKeys = update.getOrderedKeys();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2014, 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;
|
||||
|
||||
parcelable NotificationOrderUpdate;
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
public class NotificationOrderUpdate implements Parcelable {
|
||||
// TODO replace this with an update instead of the whole array
|
||||
private final String[] mKeys;
|
||||
|
||||
/** @hide */
|
||||
public NotificationOrderUpdate(String[] keys) {
|
||||
this.mKeys = keys;
|
||||
}
|
||||
|
||||
public NotificationOrderUpdate(Parcel in) {
|
||||
this.mKeys = in.readStringArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeStringArray(this.mKeys);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<NotificationOrderUpdate> CREATOR
|
||||
= new Parcelable.Creator<NotificationOrderUpdate>() {
|
||||
public NotificationOrderUpdate createFromParcel(Parcel parcel) {
|
||||
return new NotificationOrderUpdate(parcel);
|
||||
}
|
||||
|
||||
public NotificationOrderUpdate[] newArray(int size) {
|
||||
return new NotificationOrderUpdate[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @hide
|
||||
* @return ordered list of keys
|
||||
*/
|
||||
String[] getOrderedKeys() {
|
||||
return mKeys;
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/*
|
||||
* 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 com.android.internal.notification;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.Settings;
|
||||
import android.text.SpannableString;
|
||||
import android.util.Slog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This NotificationScorer bumps up the priority of notifications that contain references to the
|
||||
* display names of starred contacts. The references it picks up are spannable strings which, in
|
||||
* their entirety, match the display name of some starred contact. The magnitude of the bump ranges
|
||||
* from 0 to 15 (assuming NOTIFICATION_PRIORITY_MULTIPLIER = 10) depending on the initial score, and
|
||||
* the mapping is defined by priorityBumpMap. In a production version of this scorer, a notification
|
||||
* extra will be used to specify contact identifiers.
|
||||
*/
|
||||
|
||||
public class DemoContactNotificationScorer implements NotificationScorer {
|
||||
private static final String TAG = "DemoContactNotificationScorer";
|
||||
private static final boolean DBG = false;
|
||||
|
||||
protected static final boolean ENABLE_CONTACT_SCORER = true;
|
||||
private static final String SETTING_ENABLE_SCORER = "contact_scorer_enabled";
|
||||
protected boolean mEnabled;
|
||||
|
||||
// see NotificationManagerService
|
||||
private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10;
|
||||
|
||||
private Context mContext;
|
||||
|
||||
private static final List<String> RELEVANT_KEYS_LIST = Arrays.asList(
|
||||
Notification.EXTRA_INFO_TEXT, Notification.EXTRA_TEXT, Notification.EXTRA_TEXT_LINES,
|
||||
Notification.EXTRA_SUB_TEXT, Notification.EXTRA_TITLE
|
||||
);
|
||||
|
||||
private static final String[] PROJECTION = new String[] {
|
||||
ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME
|
||||
};
|
||||
|
||||
private static final Uri CONTACTS_URI = ContactsContract.Contacts.CONTENT_URI;
|
||||
|
||||
private static List<String> extractSpannedStrings(CharSequence charSequence) {
|
||||
if (charSequence == null) return Collections.emptyList();
|
||||
if (!(charSequence instanceof SpannableString)) {
|
||||
return Arrays.asList(charSequence.toString());
|
||||
}
|
||||
SpannableString spannableString = (SpannableString)charSequence;
|
||||
// get all spans
|
||||
Object[] ssArr = spannableString.getSpans(0, spannableString.length(), Object.class);
|
||||
// spanned string sequences
|
||||
ArrayList<String> sss = new ArrayList<String>();
|
||||
for (Object spanObj : ssArr) {
|
||||
try {
|
||||
sss.add(spannableString.subSequence(spannableString.getSpanStart(spanObj),
|
||||
spannableString.getSpanEnd(spanObj)).toString());
|
||||
} catch(StringIndexOutOfBoundsException e) {
|
||||
Slog.e(TAG, "Bad indices when extracting spanned subsequence", e);
|
||||
}
|
||||
}
|
||||
return sss;
|
||||
};
|
||||
|
||||
private static String getQuestionMarksInParens(int n) {
|
||||
StringBuilder sb = new StringBuilder("(");
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (sb.length() > 1) sb.append(',');
|
||||
sb.append('?');
|
||||
}
|
||||
sb.append(")");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private boolean hasStarredContact(Bundle extras) {
|
||||
if (extras == null) return false;
|
||||
ArrayList<String> qStrings = new ArrayList<String>();
|
||||
// build list to query against the database for display names.
|
||||
for (String rk: RELEVANT_KEYS_LIST) {
|
||||
if (extras.get(rk) == null) {
|
||||
continue;
|
||||
} else if (extras.get(rk) instanceof CharSequence) {
|
||||
qStrings.addAll(extractSpannedStrings((CharSequence) extras.get(rk)));
|
||||
} else if (extras.get(rk) instanceof CharSequence[]) {
|
||||
// this is intended for Notification.EXTRA_TEXT_LINES
|
||||
for (CharSequence line: (CharSequence[]) extras.get(rk)){
|
||||
qStrings.addAll(extractSpannedStrings(line));
|
||||
}
|
||||
} else {
|
||||
Slog.w(TAG, "Strange, the extra " + rk + " is of unexpected type.");
|
||||
}
|
||||
}
|
||||
if (qStrings.isEmpty()) return false;
|
||||
String[] qStringsArr = qStrings.toArray(new String[qStrings.size()]);
|
||||
|
||||
String selection = ContactsContract.Contacts.DISPLAY_NAME + " IN "
|
||||
+ getQuestionMarksInParens(qStringsArr.length) + " AND "
|
||||
+ ContactsContract.Contacts.STARRED+" ='1'";
|
||||
|
||||
Cursor c = null;
|
||||
try {
|
||||
c = mContext.getContentResolver().query(
|
||||
CONTACTS_URI, PROJECTION, selection, qStringsArr, null);
|
||||
if (c != null) return c.getCount() > 0;
|
||||
} catch(Throwable t) {
|
||||
Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private final static int clamp(int x, int low, int high) {
|
||||
return (x < low) ? low : ((x > high) ? high : x);
|
||||
}
|
||||
|
||||
private static int priorityBumpMap(int incomingScore) {
|
||||
//assumption is that scale runs from [-2*pm, 2*pm]
|
||||
int pm = NOTIFICATION_PRIORITY_MULTIPLIER;
|
||||
int theScore = incomingScore;
|
||||
// enforce input in range
|
||||
theScore = clamp(theScore, -2 * pm, 2 * pm);
|
||||
if (theScore != incomingScore) return incomingScore;
|
||||
// map -20 -> -20 and -10 -> 5 (when pm = 10)
|
||||
if (theScore <= -pm) {
|
||||
theScore += 1.5 * (theScore + 2 * pm);
|
||||
} else {
|
||||
// map 0 -> 10, 10 -> 15, 20 -> 20;
|
||||
theScore += 0.5 * (2 * pm - theScore);
|
||||
}
|
||||
if (DBG) Slog.v(TAG, "priorityBumpMap: score before: " + incomingScore
|
||||
+ ", score after " + theScore + ".");
|
||||
return theScore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Context context) {
|
||||
if (DBG) Slog.v(TAG, "Initializing " + getClass().getSimpleName() + ".");
|
||||
mContext = context;
|
||||
mEnabled = ENABLE_CONTACT_SCORER && 1 == Settings.Global.getInt(
|
||||
mContext.getContentResolver(), SETTING_ENABLE_SCORER, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScore(Notification notification, int score) {
|
||||
if (notification == null || !mEnabled) {
|
||||
if (DBG) Slog.w(TAG, "empty notification? scorer disabled?");
|
||||
return score;
|
||||
}
|
||||
boolean hasStarredPriority = hasStarredContact(notification.extras);
|
||||
|
||||
if (DBG) {
|
||||
if (hasStarredPriority) {
|
||||
Slog.v(TAG, "Notification references starred contact. Promoted!");
|
||||
} else {
|
||||
Slog.v(TAG, "Notification lacks any starred contact reference. Not promoted!");
|
||||
}
|
||||
}
|
||||
if (hasStarredPriority) score = priorityBumpMap(score);
|
||||
return score;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* 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 com.android.internal.notification;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
|
||||
public interface NotificationScorer {
|
||||
|
||||
public void initialize(Context context);
|
||||
public int getScore(Notification notification, int score);
|
||||
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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.internal.notification;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.ContactsContract.Contacts;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.LruCache;
|
||||
import android.util.Slog;
|
||||
|
||||
/**
|
||||
* This {@link NotificationScorer} attempts to validate people references.
|
||||
* Also elevates the priority of real people.
|
||||
*/
|
||||
public class PeopleNotificationScorer implements NotificationScorer {
|
||||
private static final String TAG = "PeopleNotificationScorer";
|
||||
private static final boolean DBG = false;
|
||||
|
||||
private static final boolean ENABLE_PEOPLE_SCORER = true;
|
||||
private static final String SETTING_ENABLE_PEOPLE_SCORER = "people_scorer_enabled";
|
||||
private static final String[] LOOKUP_PROJECTION = { Contacts._ID };
|
||||
private static final int MAX_PEOPLE = 10;
|
||||
private static final int PEOPLE_CACHE_SIZE = 200;
|
||||
// see NotificationManagerService
|
||||
private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10;
|
||||
|
||||
protected boolean mEnabled;
|
||||
private Context mContext;
|
||||
|
||||
// maps raw person handle to resolved person object
|
||||
private LruCache<String, LookupResult> mPeopleCache;
|
||||
|
||||
private float findMaxContactScore(Bundle extras) {
|
||||
if (extras == null) {
|
||||
return 0f;
|
||||
}
|
||||
|
||||
final String[] people = extras.getStringArray(Notification.EXTRA_PEOPLE);
|
||||
if (people == null || people.length == 0) {
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float rank = 0f;
|
||||
for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) {
|
||||
final String handle = people[personIdx];
|
||||
if (TextUtils.isEmpty(handle)) continue;
|
||||
|
||||
LookupResult lookupResult = mPeopleCache.get(handle);
|
||||
if (lookupResult == null || lookupResult.isExpired()) {
|
||||
final Uri uri = Uri.parse(handle);
|
||||
if ("tel".equals(uri.getScheme())) {
|
||||
if (DBG) Slog.w(TAG, "checking telephone URI: " + handle);
|
||||
lookupResult = lookupPhoneContact(handle, uri.getSchemeSpecificPart());
|
||||
} else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
|
||||
if (DBG) Slog.w(TAG, "checking lookup URI: " + handle);
|
||||
lookupResult = resolveContactsUri(handle, uri);
|
||||
} else {
|
||||
if (DBG) Slog.w(TAG, "unsupported URI " + handle);
|
||||
}
|
||||
} else {
|
||||
if (DBG) Slog.w(TAG, "using cached lookupResult: " + lookupResult.mId);
|
||||
}
|
||||
if (lookupResult != null) {
|
||||
rank = Math.max(rank, lookupResult.getRank());
|
||||
}
|
||||
}
|
||||
return rank;
|
||||
}
|
||||
|
||||
private LookupResult lookupPhoneContact(final String handle, final String number) {
|
||||
LookupResult lookupResult = null;
|
||||
Cursor c = null;
|
||||
try {
|
||||
Uri numberUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
|
||||
Uri.encode(number));
|
||||
c = mContext.getContentResolver().query(numberUri, LOOKUP_PROJECTION, null, null, null);
|
||||
if (c != null && c.getCount() > 0) {
|
||||
c.moveToFirst();
|
||||
final int idIdx = c.getColumnIndex(Contacts._ID);
|
||||
final int id = c.getInt(idIdx);
|
||||
if (DBG) Slog.w(TAG, "is valid: " + id);
|
||||
lookupResult = new LookupResult(id);
|
||||
}
|
||||
} catch(Throwable t) {
|
||||
Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
if (lookupResult == null) {
|
||||
lookupResult = new LookupResult(LookupResult.INVALID_ID);
|
||||
}
|
||||
mPeopleCache.put(handle, lookupResult);
|
||||
return lookupResult;
|
||||
}
|
||||
|
||||
private LookupResult resolveContactsUri(String handle, final Uri personUri) {
|
||||
LookupResult lookupResult = null;
|
||||
Cursor c = null;
|
||||
try {
|
||||
c = mContext.getContentResolver().query(personUri, LOOKUP_PROJECTION, null, null, null);
|
||||
if (c != null && c.getCount() > 0) {
|
||||
c.moveToFirst();
|
||||
final int idIdx = c.getColumnIndex(Contacts._ID);
|
||||
final int id = c.getInt(idIdx);
|
||||
if (DBG) Slog.w(TAG, "is valid: " + id);
|
||||
lookupResult = new LookupResult(id);
|
||||
}
|
||||
} catch(Throwable t) {
|
||||
Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
if (lookupResult == null) {
|
||||
lookupResult = new LookupResult(LookupResult.INVALID_ID);
|
||||
}
|
||||
mPeopleCache.put(handle, lookupResult);
|
||||
return lookupResult;
|
||||
}
|
||||
|
||||
private final static int clamp(int x, int low, int high) {
|
||||
return (x < low) ? low : ((x > high) ? high : x);
|
||||
}
|
||||
|
||||
// TODO: rework this function before shipping
|
||||
private static int priorityBumpMap(int incomingScore) {
|
||||
//assumption is that scale runs from [-2*pm, 2*pm]
|
||||
int pm = NOTIFICATION_PRIORITY_MULTIPLIER;
|
||||
int theScore = incomingScore;
|
||||
// enforce input in range
|
||||
theScore = clamp(theScore, -2 * pm, 2 * pm);
|
||||
if (theScore != incomingScore) return incomingScore;
|
||||
// map -20 -> -20 and -10 -> 5 (when pm = 10)
|
||||
if (theScore <= -pm) {
|
||||
theScore += 1.5 * (theScore + 2 * pm);
|
||||
} else {
|
||||
// map 0 -> 10, 10 -> 15, 20 -> 20;
|
||||
theScore += 0.5 * (2 * pm - theScore);
|
||||
}
|
||||
if (DBG) Slog.v(TAG, "priorityBumpMap: score before: " + incomingScore
|
||||
+ ", score after " + theScore + ".");
|
||||
return theScore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Context context) {
|
||||
if (DBG) Slog.v(TAG, "Initializing " + getClass().getSimpleName() + ".");
|
||||
mContext = context;
|
||||
mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE);
|
||||
mEnabled = ENABLE_PEOPLE_SCORER && 1 == Settings.Global.getInt(
|
||||
mContext.getContentResolver(), SETTING_ENABLE_PEOPLE_SCORER, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScore(Notification notification, int score) {
|
||||
if (notification == null || !mEnabled) {
|
||||
if (DBG) Slog.w(TAG, "empty notification? scorer disabled?");
|
||||
return score;
|
||||
}
|
||||
float contactScore = findMaxContactScore(notification.extras);
|
||||
if (contactScore > 0f) {
|
||||
if (DBG) Slog.v(TAG, "Notification references a real contact. Promoted!");
|
||||
score = priorityBumpMap(score);
|
||||
} else {
|
||||
if (DBG) Slog.v(TAG, "Notification lacks any valid contact reference. Not promoted!");
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
private static class LookupResult {
|
||||
private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr
|
||||
public static final int INVALID_ID = -1;
|
||||
|
||||
private final long mExpireMillis;
|
||||
private int mId;
|
||||
|
||||
public LookupResult(int id) {
|
||||
mId = id;
|
||||
mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return mExpireMillis < System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public boolean isInvalid() {
|
||||
return mId == INVALID_ID || isExpired();
|
||||
}
|
||||
|
||||
public float getRank() {
|
||||
if (isInvalid()) {
|
||||
return 0f;
|
||||
} else {
|
||||
return 1f; // TODO: finer grained score
|
||||
}
|
||||
}
|
||||
|
||||
public LookupResult setId(int id) {
|
||||
mId = id;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1393,8 +1393,10 @@
|
||||
<item>com.android.inputmethod.latin</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="config_notificationScorers">
|
||||
<item>com.android.internal.notification.PeopleNotificationScorer</item>
|
||||
<!-- The list of classes that should be added to the notification ranking pipline.
|
||||
See {@link com.android.server.notification.NotificationSignalExtractortor} -->
|
||||
<string-array name="config_notificationSignalExtractors">
|
||||
<item>com.android.server.notification.ValidateNotificationPeople</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Flag indicating that this device does not rotate and will always remain in its default
|
||||
|
||||
@@ -1653,7 +1653,7 @@
|
||||
<java-symbol type="id" name="button_always" />
|
||||
<java-symbol type="integer" name="config_globalActionsKeyTimeout" />
|
||||
<java-symbol type="integer" name="config_maxResolverActivityColumns" />
|
||||
<java-symbol type="array" name="config_notificationScorers" />
|
||||
<java-symbol type="array" name="config_notificationSignalExtractors" />
|
||||
|
||||
<java-symbol type="layout" name="notification_quantum_action" />
|
||||
<java-symbol type="layout" name="notification_quantum_action_list" />
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.server.notification;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Sorts notificaitons into attention-relelvant order.
|
||||
*/
|
||||
public class NotificationComparator
|
||||
implements Comparator<NotificationManagerService.NotificationRecord> {
|
||||
|
||||
@Override
|
||||
public int compare(NotificationManagerService.NotificationRecord lhs,
|
||||
NotificationManagerService.NotificationRecord rhs) {
|
||||
final int leftScore = lhs.sbn.getScore();
|
||||
final int rightScore = rhs.sbn.getScore();
|
||||
if (leftScore != rightScore) {
|
||||
// by priority, high to low
|
||||
return -1 * Integer.compare(leftScore, rightScore);
|
||||
}
|
||||
final float leftPeple = lhs.getContactAffinity();
|
||||
final float rightPeople = rhs.getContactAffinity();
|
||||
if (leftPeple != rightPeople) {
|
||||
// by contact proximity, close to far
|
||||
return -1 * Float.compare(leftPeple, rightPeople);
|
||||
}
|
||||
// then break ties by time, most recent first
|
||||
return -1 * Long.compare(lhs.sbn.getPostTime(), rhs.sbn.getPostTime());
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,10 @@ import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
@@ -61,6 +63,7 @@ import android.service.notification.INotificationListener;
|
||||
import android.service.notification.IConditionListener;
|
||||
import android.service.notification.IConditionProvider;
|
||||
import android.service.notification.NotificationListenerService;
|
||||
import android.service.notification.NotificationOrderUpdate;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.service.notification.Condition;
|
||||
import android.service.notification.ZenModeConfig;
|
||||
@@ -76,7 +79,6 @@ import android.view.accessibility.AccessibilityManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.internal.R;
|
||||
import com.android.internal.notification.NotificationScorer;
|
||||
import com.android.internal.util.FastXmlSerializer;
|
||||
import com.android.server.EventLogTags;
|
||||
import com.android.server.SystemService;
|
||||
@@ -104,9 +106,12 @@ import java.lang.reflect.Array;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** {@hide} */
|
||||
public class NotificationManagerService extends SystemService {
|
||||
@@ -118,6 +123,8 @@ public class NotificationManagerService extends SystemService {
|
||||
// message codes
|
||||
static final int MESSAGE_TIMEOUT = 2;
|
||||
static final int MESSAGE_SAVE_POLICY_FILE = 3;
|
||||
static final int MESSAGE_RECONSIDER_RANKING = 4;
|
||||
static final int MESSAGE_SEND_RANKING_UPDATE = 5;
|
||||
|
||||
static final int LONG_DELAY = 3500; // 3.5 seconds
|
||||
static final int SHORT_DELAY = 2000; // 2 seconds
|
||||
@@ -147,6 +154,9 @@ public class NotificationManagerService extends SystemService {
|
||||
|
||||
final IBinder mForegroundToken = new Binder();
|
||||
private WorkerHandler mHandler;
|
||||
private final HandlerThread mRankingThread = new HandlerThread("ranker",
|
||||
Process.THREAD_PRIORITY_BACKGROUND);
|
||||
private Handler mRankingHandler = null;
|
||||
|
||||
private Light mNotificationLight;
|
||||
Light mAttentionLight;
|
||||
@@ -171,6 +181,7 @@ public class NotificationManagerService extends SystemService {
|
||||
// used as a mutex for access to all active notifications & listeners
|
||||
final ArrayList<NotificationRecord> mNotificationList =
|
||||
new ArrayList<NotificationRecord>();
|
||||
final NotificationComparator mRankingComparator = new NotificationComparator();
|
||||
final ArrayMap<String, NotificationRecord> mNotificationsByKey =
|
||||
new ArrayMap<String, NotificationRecord>();
|
||||
final ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>();
|
||||
@@ -193,7 +204,7 @@ public class NotificationManagerService extends SystemService {
|
||||
private static final String TAG_PACKAGE = "package";
|
||||
private static final String ATTR_NAME = "name";
|
||||
|
||||
final ArrayList<NotificationScorer> mScorers = new ArrayList<NotificationScorer>();
|
||||
final ArrayList<NotificationSignalExtractor> mSignalExtractors = new ArrayList<NotificationSignalExtractor>();
|
||||
|
||||
private final UserProfiles mUserProfiles = new UserProfiles();
|
||||
private NotificationListeners mListeners;
|
||||
@@ -446,6 +457,7 @@ public class NotificationManagerService extends SystemService {
|
||||
final StatusBarNotification sbn;
|
||||
final SingleNotificationStats stats = new SingleNotificationStats();
|
||||
IBinder statusBarKey;
|
||||
private float mContactAffinity;
|
||||
|
||||
NotificationRecord(StatusBarNotification sbn)
|
||||
{
|
||||
@@ -528,6 +540,14 @@ public class NotificationManagerService extends SystemService {
|
||||
this.sbn.getTag(), this.sbn.getScore(), this.sbn.getKey(),
|
||||
this.sbn.getNotification());
|
||||
}
|
||||
|
||||
public void setContactAffinity(float contactAffinity) {
|
||||
mContactAffinity = contactAffinity;
|
||||
}
|
||||
|
||||
public float getContactAffinity() {
|
||||
return mContactAffinity;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ToastRecord
|
||||
@@ -707,7 +727,7 @@ public class NotificationManagerService extends SystemService {
|
||||
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)
|
||||
@@ -849,6 +869,8 @@ public class NotificationManagerService extends SystemService {
|
||||
mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
|
||||
|
||||
mHandler = new WorkerHandler();
|
||||
mRankingThread.start();
|
||||
mRankingHandler = new RankingWorkerHandler(mRankingThread.getLooper());
|
||||
mZenModeHelper = new ZenModeHelper(getContext(), mHandler);
|
||||
mZenModeHelper.addCallback(new ZenModeHelper.Callback() {
|
||||
@Override
|
||||
@@ -925,21 +947,22 @@ public class NotificationManagerService extends SystemService {
|
||||
|
||||
mSettingsObserver = new SettingsObserver(mHandler);
|
||||
|
||||
// spin up NotificationScorers
|
||||
String[] notificationScorerNames = resources.getStringArray(
|
||||
R.array.config_notificationScorers);
|
||||
for (String scorerName : notificationScorerNames) {
|
||||
// spin up NotificationSignalExtractors
|
||||
String[] extractorNames = resources.getStringArray(
|
||||
R.array.config_notificationSignalExtractors);
|
||||
for (String extractorName : extractorNames) {
|
||||
try {
|
||||
Class<?> scorerClass = getContext().getClassLoader().loadClass(scorerName);
|
||||
NotificationScorer scorer = (NotificationScorer) scorerClass.newInstance();
|
||||
scorer.initialize(getContext());
|
||||
mScorers.add(scorer);
|
||||
Class<?> extractorClass = getContext().getClassLoader().loadClass(extractorName);
|
||||
NotificationSignalExtractor extractor =
|
||||
(NotificationSignalExtractor) extractorClass.newInstance();
|
||||
extractor.initialize(getContext());
|
||||
mSignalExtractors.add(extractor);
|
||||
} catch (ClassNotFoundException e) {
|
||||
Slog.w(TAG, "Couldn't find scorer " + scorerName + ".", e);
|
||||
Slog.w(TAG, "Couldn't find extractor " + extractorName + ".", e);
|
||||
} catch (InstantiationException e) {
|
||||
Slog.w(TAG, "Couldn't instantiate scorer " + scorerName + ".", e);
|
||||
Slog.w(TAG, "Couldn't instantiate extractor " + extractorName + ".", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
Slog.w(TAG, "Problem accessing scorer " + scorerName + ".", e);
|
||||
Slog.w(TAG, "Problem accessing extractor " + extractorName + ".", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1150,6 +1173,7 @@ public class NotificationManagerService extends SystemService {
|
||||
* System-only API for getting a list of current (i.e. not cleared) notifications.
|
||||
*
|
||||
* Requires ACCESS_NOTIFICATIONS which is signature|system.
|
||||
* @returns A list of all the notifications, in natural order.
|
||||
*/
|
||||
@Override
|
||||
public StatusBarNotification[] getActiveNotifications(String callingPkg) {
|
||||
@@ -1306,6 +1330,9 @@ public class NotificationManagerService extends SystemService {
|
||||
* should be used.
|
||||
*
|
||||
* @param token The binder for the listener, to check that the caller is allowed
|
||||
* @param keys the notification keys to fetch, or null for all active notifications.
|
||||
* @returns The return value will contain the notifications specified in keys, in that
|
||||
* order, or if keys is null, all the notifications, in natural order.
|
||||
*/
|
||||
@Override
|
||||
public StatusBarNotification[] getActiveNotificationsFromListener(
|
||||
@@ -1337,7 +1364,7 @@ public class NotificationManagerService extends SystemService {
|
||||
|
||||
@Override
|
||||
public String[] getActiveNotificationKeysFromListener(INotificationListener token) {
|
||||
return NotificationManagerService.this.getActiveNotificationKeysFromListener(token);
|
||||
return NotificationManagerService.this.getActiveNotificationKeys(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1409,19 +1436,21 @@ public class NotificationManagerService extends SystemService {
|
||||
}
|
||||
};
|
||||
|
||||
private String[] getActiveNotificationKeysFromListener(INotificationListener token) {
|
||||
synchronized (mNotificationList) {
|
||||
final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
|
||||
final ArrayList<String> keys = new ArrayList<String>();
|
||||
final int N = mNotificationList.size();
|
||||
for (int i=0; i<N; i++) {
|
||||
final StatusBarNotification sbn = mNotificationList.get(i).sbn;
|
||||
if (info.enabledAndUserMatches(sbn.getUserId())) {
|
||||
keys.add(sbn.getKey());
|
||||
private String[] getActiveNotificationKeys(INotificationListener token) {
|
||||
final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
|
||||
final ArrayList<String> keys = new ArrayList<String>();
|
||||
if (info.isEnabledForCurrentProfiles()) {
|
||||
synchronized (mNotificationList) {
|
||||
final int N = mNotificationList.size();
|
||||
for (int i = 0; i < N; i++) {
|
||||
final StatusBarNotification sbn = mNotificationList.get(i).sbn;
|
||||
if (info.enabledAndUserMatches(sbn.getUserId())) {
|
||||
keys.add(sbn.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys.toArray(new String[keys.size()]);
|
||||
}
|
||||
return keys.toArray(new String[keys.size()]);
|
||||
}
|
||||
|
||||
void dumpImpl(PrintWriter pw) {
|
||||
@@ -1578,26 +1607,23 @@ public class NotificationManagerService extends SystemService {
|
||||
// 1. initial score: buckets of 10, around the app
|
||||
int score = notification.priority * NOTIFICATION_PRIORITY_MULTIPLIER; //[-20..20]
|
||||
|
||||
// 2. Consult external heuristics (TBD)
|
||||
|
||||
// 3. Apply local rules
|
||||
|
||||
int initialScore = score;
|
||||
if (!mScorers.isEmpty()) {
|
||||
if (DBG) Slog.v(TAG, "Initial score is " + score + ".");
|
||||
for (NotificationScorer scorer : mScorers) {
|
||||
// 2. extract ranking signals from the notification data
|
||||
final StatusBarNotification n = new StatusBarNotification(
|
||||
pkg, opPkg, id, tag, callingUid, callingPid, score, notification,
|
||||
user);
|
||||
NotificationRecord r = new NotificationRecord(n);
|
||||
if (!mSignalExtractors.isEmpty()) {
|
||||
for (NotificationSignalExtractor extractor : mSignalExtractors) {
|
||||
try {
|
||||
score = scorer.getScore(notification, score);
|
||||
RankingFuture future = extractor.process(r);
|
||||
scheduleRankingReconsideration(future);
|
||||
} catch (Throwable t) {
|
||||
Slog.w(TAG, "Scorer threw on .getScore.", t);
|
||||
Slog.w(TAG, "NotificationSignalExtractor failed.", t);
|
||||
}
|
||||
}
|
||||
if (DBG) Slog.v(TAG, "Final score is " + score + ".");
|
||||
}
|
||||
|
||||
// add extra to indicate score modified by NotificationScorer
|
||||
notification.extras.putBoolean(Notification.EXTRA_SCORE_MODIFIED,
|
||||
score != initialScore);
|
||||
// 3. Apply local rules
|
||||
|
||||
// blocked apps
|
||||
if (ENABLE_BLOCKED_NOTIFICATIONS && !noteNotificationOp(pkg, callingUid)) {
|
||||
@@ -1608,10 +1634,6 @@ public class NotificationManagerService extends SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1626,12 +1648,7 @@ public class NotificationManagerService extends SystemService {
|
||||
if (DBG || intercept) Slog.v(TAG,
|
||||
"pkg=" + pkg + " canInterrupt=" + canInterrupt + " intercept=" + intercept);
|
||||
synchronized (mNotificationList) {
|
||||
final StatusBarNotification n = new StatusBarNotification(
|
||||
pkg, opPkg, 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);
|
||||
@@ -1651,6 +1668,8 @@ public class NotificationManagerService extends SystemService {
|
||||
}
|
||||
mNotificationsByKey.put(n.getKey(), r);
|
||||
|
||||
Collections.sort(mNotificationList, mRankingComparator);
|
||||
|
||||
// Ensure if this is a foreground service that the proper additional
|
||||
// flags are set.
|
||||
if ((notification.flags&Notification.FLAG_FOREGROUND_SERVICE) != 0) {
|
||||
@@ -1948,6 +1967,57 @@ public class NotificationManagerService extends SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleRankingReconsideration(RankingFuture future) {
|
||||
if (future != null) {
|
||||
Message m = Message.obtain(mRankingHandler, MESSAGE_RECONSIDER_RANKING, future);
|
||||
long delay = future.getDelay(TimeUnit.MILLISECONDS);
|
||||
mRankingHandler.sendMessageDelayed(m, delay);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRankingReconsideration(Message message) {
|
||||
if (!(message.obj instanceof RankingFuture)) return;
|
||||
|
||||
RankingFuture future = (RankingFuture) message.obj;
|
||||
future.run();
|
||||
try {
|
||||
NotificationRecord record = future.get();
|
||||
synchronized (mNotificationList) {
|
||||
int before = mNotificationList.indexOf(record);
|
||||
if (before != -1) {
|
||||
Collections.sort(mNotificationList, mRankingComparator);
|
||||
int after = mNotificationList.indexOf(record);
|
||||
|
||||
if (before != after) {
|
||||
scheduleSendRankingUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// we're running the future explicitly, so this should never happen
|
||||
} catch (ExecutionException e) {
|
||||
// we're running the future explicitly, so this should never happen
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleSendRankingUpdate() {
|
||||
mHandler.removeMessages(MESSAGE_SEND_RANKING_UPDATE);
|
||||
Message m = Message.obtain(mHandler, MESSAGE_SEND_RANKING_UPDATE);
|
||||
mHandler.sendMessage(m);
|
||||
}
|
||||
|
||||
private void handleSendRankingUpdate() {
|
||||
synchronized (mNotificationList) {
|
||||
final int N = mNotificationList.size();
|
||||
ArrayList<StatusBarNotification> sbns =
|
||||
new ArrayList<StatusBarNotification>(N);
|
||||
for (int i = 0; i < N; i++ ) {
|
||||
sbns.add(mNotificationList.get(i).sbn);
|
||||
}
|
||||
mListeners.notifyOrderUpdateLocked(sbns);
|
||||
}
|
||||
}
|
||||
|
||||
private final class WorkerHandler extends Handler
|
||||
{
|
||||
@Override
|
||||
@@ -1961,11 +2031,30 @@ public class NotificationManagerService extends SystemService {
|
||||
case MESSAGE_SAVE_POLICY_FILE:
|
||||
handleSavePolicyFile();
|
||||
break;
|
||||
case MESSAGE_SEND_RANKING_UPDATE:
|
||||
handleSendRankingUpdate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class RankingWorkerHandler extends Handler
|
||||
{
|
||||
public RankingWorkerHandler(Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MESSAGE_RECONSIDER_RANKING:
|
||||
handleRankingReconsideration(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Notifications
|
||||
// ============================================================================
|
||||
static int clamp(int x, int low, int high) {
|
||||
@@ -2346,9 +2435,9 @@ public class NotificationManagerService extends SystemService {
|
||||
@Override
|
||||
public void onServiceAdded(ManagedServiceInfo info) {
|
||||
final INotificationListener listener = (INotificationListener) info.service;
|
||||
final String[] keys = getActiveNotificationKeysFromListener(listener);
|
||||
final String[] keys = getActiveNotificationKeys(listener);
|
||||
try {
|
||||
listener.onListenerConnected(keys);
|
||||
listener.onListenerConnected(new NotificationOrderUpdate(keys));
|
||||
} catch (RemoteException e) {
|
||||
// we tried
|
||||
}
|
||||
@@ -2361,12 +2450,18 @@ public class NotificationManagerService extends SystemService {
|
||||
// make a copy in case changes are made to the underlying Notification object
|
||||
final StatusBarNotification sbnClone = sbn.clone();
|
||||
for (final ManagedServiceInfo info : mServices) {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyPostedIfUserMatch(info, sbnClone);
|
||||
if (info.isEnabledForCurrentProfiles()) {
|
||||
final INotificationListener listener = (INotificationListener) info.service;
|
||||
final String[] keys = getActiveNotificationKeys(listener);
|
||||
if (keys.length > 0) {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyPostedIfUserMatch(info, sbnClone, keys);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2378,39 +2473,83 @@ public class NotificationManagerService extends SystemService {
|
||||
// NOTE: this copy is lightweight: it doesn't include heavyweight parts of the
|
||||
// notification
|
||||
final StatusBarNotification sbnLight = sbn.cloneLight();
|
||||
for (ManagedServiceInfo serviceInfo : mServices) {
|
||||
final ManagedServiceInfo info = (ManagedServiceInfo) serviceInfo;
|
||||
for (final ManagedServiceInfo info : mServices) {
|
||||
if (info.isEnabledForCurrentProfiles()) {
|
||||
final INotificationListener listener = (INotificationListener) info.service;
|
||||
final String[] keys = getActiveNotificationKeys(listener);
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyRemovedIfUserMatch(info, sbnLight, keys);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* asynchronously notify all listeners about a reordering of notifications
|
||||
* @param sbns an array of {@link StatusBarNotification}s to consider. This code
|
||||
* must not rely on mutable members of these objects, such as the
|
||||
* {@link Notification}.
|
||||
*/
|
||||
public void notifyOrderUpdateLocked(final ArrayList<StatusBarNotification> sbns) {
|
||||
for (final ManagedServiceInfo serviceInfo : mServices) {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyRemovedIfUserMatch(info, sbnLight);
|
||||
notifyOrderUpdateIfUserMatch(serviceInfo, sbns);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyPostedIfUserMatch(ManagedServiceInfo info, StatusBarNotification sbn) {
|
||||
private void notifyPostedIfUserMatch(final ManagedServiceInfo info,
|
||||
final StatusBarNotification sbn, String[] keys) {
|
||||
if (!info.enabledAndUserMatches(sbn.getUserId())) {
|
||||
return;
|
||||
}
|
||||
final INotificationListener listener = (INotificationListener)info.service;
|
||||
try {
|
||||
listener.onNotificationPosted(sbn);
|
||||
listener.onNotificationPosted(sbn, new NotificationOrderUpdate(keys));
|
||||
} catch (RemoteException ex) {
|
||||
Log.e(TAG, "unable to notify listener (posted): " + listener, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyRemovedIfUserMatch(ManagedServiceInfo info, StatusBarNotification sbn) {
|
||||
private void notifyRemovedIfUserMatch(ManagedServiceInfo info, StatusBarNotification sbn,
|
||||
String[] keys) {
|
||||
if (!info.enabledAndUserMatches(sbn.getUserId())) {
|
||||
return;
|
||||
}
|
||||
final INotificationListener listener = (INotificationListener)info.service;
|
||||
try {
|
||||
listener.onNotificationRemoved(sbn);
|
||||
listener.onNotificationRemoved(sbn, new NotificationOrderUpdate(keys));
|
||||
} catch (RemoteException ex) {
|
||||
Log.e(TAG, "unable to notify listener (removed): " + listener, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sbns an array of {@link StatusBarNotification}s to consider. This code
|
||||
* must not rely on mutable members of these objects, such as the
|
||||
* {@link Notification}.
|
||||
*/
|
||||
public void notifyOrderUpdateIfUserMatch(ManagedServiceInfo info,
|
||||
ArrayList<StatusBarNotification> sbns) {
|
||||
ArrayList<String> keys = new ArrayList<String>(sbns.size());
|
||||
for (StatusBarNotification sbn: sbns) {
|
||||
if (info.enabledAndUserMatches(sbn.getUserId())) {
|
||||
keys.add(sbn.getKey());
|
||||
}
|
||||
}
|
||||
final INotificationListener listener = (INotificationListener)info.service;
|
||||
try {
|
||||
listener.onNotificationOrderUpdate(
|
||||
new NotificationOrderUpdate(keys.toArray(new String[keys.size()])));
|
||||
} catch (RemoteException ex) {
|
||||
Log.e(TAG, "unable to notify listener (ranking update): " + listener, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.notification;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Extracts signals that will be useful to the {@link NotificationComparator} and caches them
|
||||
* on the {@link NotificationManagerService.NotificationRecord} object. These annotations will
|
||||
* not be passed on to {@link android.service.notification.NotificationListenerService}s.
|
||||
*/
|
||||
public interface NotificationSignalExtractor {
|
||||
|
||||
/** One-time initialization. */
|
||||
public void initialize(Context context);
|
||||
|
||||
/**
|
||||
* Called once per notification that is posted or updated.
|
||||
*
|
||||
* @return null if the work is done, or a future if there is more to do. The
|
||||
* {@link RankingFuture} will be run on a worker thread, and if notifications are re-ordered
|
||||
* by that execution, the {@link NotificationManagerService} may send order update
|
||||
* events to the {@link android.service.notification.NotificationListenerService}s.
|
||||
*/
|
||||
public RankingFuture process(NotificationManagerService.NotificationRecord notification);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.server.notification;
|
||||
|
||||
import java.util.concurrent.Delayed;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public abstract class RankingFuture
|
||||
implements ScheduledFuture<NotificationManagerService.NotificationRecord> {
|
||||
private static final long IMMEDIATE = 0l;
|
||||
|
||||
private static final int START = 0;
|
||||
private static final int RUNNING = 1;
|
||||
private static final int DONE = 2;
|
||||
private static final int CANCELLED = 3;
|
||||
|
||||
private int mState;
|
||||
private long mDelay;
|
||||
protected NotificationManagerService.NotificationRecord mRecord;
|
||||
|
||||
public RankingFuture(NotificationManagerService.NotificationRecord record) {
|
||||
this(record, IMMEDIATE);
|
||||
}
|
||||
|
||||
public RankingFuture(NotificationManagerService.NotificationRecord record, long delay) {
|
||||
mDelay = delay;
|
||||
mRecord = record;
|
||||
mState = START;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
if (mState == START) {
|
||||
mState = RUNNING;
|
||||
|
||||
work();
|
||||
|
||||
mState = DONE;
|
||||
synchronized (this) {
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDelay(TimeUnit unit) {
|
||||
return unit.convert(mDelay, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Delayed another) {
|
||||
return Long.compare(getDelay(TimeUnit.MICROSECONDS),
|
||||
another.getDelay(TimeUnit.MICROSECONDS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel(boolean mayInterruptIfRunning) {
|
||||
if (mState == START) { // can't cancel if running or done
|
||||
mState = CANCELLED;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return mState == CANCELLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
return mState == DONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NotificationManagerService.NotificationRecord get()
|
||||
throws InterruptedException, ExecutionException {
|
||||
while (!isDone()) {
|
||||
synchronized (this) {
|
||||
this.wait();
|
||||
}
|
||||
}
|
||||
return mRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NotificationManagerService.NotificationRecord get(long timeout, TimeUnit unit)
|
||||
throws InterruptedException, ExecutionException, TimeoutException {
|
||||
long timeoutMillis = unit.convert(timeout, TimeUnit.MILLISECONDS);
|
||||
long start = System.currentTimeMillis();
|
||||
long now = System.currentTimeMillis();
|
||||
while (!isDone() && (now - start) < timeoutMillis) {
|
||||
try {
|
||||
wait(timeoutMillis - (now - start));
|
||||
} catch (InterruptedException e) {
|
||||
now = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
return mRecord;
|
||||
}
|
||||
|
||||
public abstract void work();
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.notification;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.ContactsContract.Contacts;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.LruCache;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.server.notification.NotificationManagerService.NotificationRecord;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* This {@link NotificationSignalExtractor} attempts to validate
|
||||
* people references. Also elevates the priority of real people.
|
||||
*/
|
||||
public class ValidateNotificationPeople implements NotificationSignalExtractor {
|
||||
private static final String TAG = "ValidateNotificationPeople";
|
||||
private static final boolean INFO = true;
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final boolean ENABLE_PEOPLE_VALIDATOR = true;
|
||||
private static final String SETTING_ENABLE_PEOPLE_VALIDATOR =
|
||||
"validate_notification_people_enabled";
|
||||
private static final String[] LOOKUP_PROJECTION = { Contacts._ID };
|
||||
private static final int MAX_PEOPLE = 10;
|
||||
private static final int PEOPLE_CACHE_SIZE = 200;
|
||||
|
||||
private static final float NONE = 0f;
|
||||
private static final float VALID_CONTACT = 0.5f;
|
||||
// TODO private static final float STARRED_CONTACT = 1f;
|
||||
|
||||
protected boolean mEnabled;
|
||||
private Context mContext;
|
||||
|
||||
// maps raw person handle to resolved person object
|
||||
private LruCache<String, LookupResult> mPeopleCache;
|
||||
|
||||
private RankingFuture validatePeople(NotificationRecord record) {
|
||||
float affinity = NONE;
|
||||
Bundle extras = record.getNotification().extras;
|
||||
if (extras == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String[] people = getExtraPeople(extras);
|
||||
if (people == null || people.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (INFO) Slog.i(TAG, "Validating: " + record.sbn.getKey());
|
||||
final LinkedList<String> pendingLookups = new LinkedList<String>();
|
||||
for (int personIdx = 0; personIdx < people.length && personIdx < MAX_PEOPLE; personIdx++) {
|
||||
final String handle = people[personIdx];
|
||||
if (TextUtils.isEmpty(handle)) continue;
|
||||
|
||||
synchronized (mPeopleCache) {
|
||||
LookupResult lookupResult = mPeopleCache.get(handle);
|
||||
if (lookupResult == null || lookupResult.isExpired()) {
|
||||
pendingLookups.add(handle);
|
||||
} else {
|
||||
if (DEBUG) Slog.d(TAG, "using cached lookupResult: " + lookupResult.mId);
|
||||
}
|
||||
if (lookupResult != null) {
|
||||
affinity = Math.max(affinity, lookupResult.getAffinity());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// record the best available data, so far:
|
||||
record.setContactAffinity(affinity);
|
||||
|
||||
if (pendingLookups.isEmpty()) {
|
||||
if (INFO) Slog.i(TAG, "final affinity: " + affinity);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (DEBUG) Slog.d(TAG, "Pending: future work scheduled for: " + record.sbn.getKey());
|
||||
return new RankingFuture(record) {
|
||||
@Override
|
||||
public void work() {
|
||||
if (INFO) Slog.i(TAG, "Executing: validation for: " + mRecord.sbn.getKey());
|
||||
float affinity = NONE;
|
||||
LookupResult lookupResult = null;
|
||||
for (final String handle: pendingLookups) {
|
||||
final Uri uri = Uri.parse(handle);
|
||||
if ("tel".equals(uri.getScheme())) {
|
||||
if (DEBUG) Slog.d(TAG, "checking telephone URI: " + handle);
|
||||
lookupResult = resolvePhoneContact(handle, uri.getSchemeSpecificPart());
|
||||
} else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
|
||||
if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle);
|
||||
lookupResult = resolveContactsUri(handle, uri);
|
||||
} else {
|
||||
Slog.w(TAG, "unsupported URI " + handle);
|
||||
}
|
||||
}
|
||||
if (lookupResult != null) {
|
||||
affinity = Math.max(affinity, lookupResult.getAffinity());
|
||||
}
|
||||
|
||||
float affinityBound = mRecord.getContactAffinity();
|
||||
affinity = Math.max(affinity, affinityBound);
|
||||
mRecord.setContactAffinity(affinity);
|
||||
if (INFO) Slog.i(TAG, "final affinity: " + affinity);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private String[] getExtraPeople(Bundle extras) {
|
||||
String[] people = extras.getStringArray(Notification.EXTRA_PEOPLE);
|
||||
if (people != null) {
|
||||
return people;
|
||||
}
|
||||
|
||||
ArrayList<String> stringArray = extras.getStringArrayList(Notification.EXTRA_PEOPLE);
|
||||
if (stringArray != null) {
|
||||
return (String[]) stringArray.toArray();
|
||||
}
|
||||
|
||||
String string = extras.getString(Notification.EXTRA_PEOPLE);
|
||||
if (string != null) {
|
||||
people = new String[1];
|
||||
people[0] = string;
|
||||
return people;
|
||||
}
|
||||
char[] charArray = extras.getCharArray(Notification.EXTRA_PEOPLE);
|
||||
if (charArray != null) {
|
||||
people = new String[1];
|
||||
people[0] = new String(charArray);
|
||||
return people;
|
||||
}
|
||||
|
||||
CharSequence charSeq = extras.getCharSequence(Notification.EXTRA_PEOPLE);
|
||||
if (charSeq != null) {
|
||||
people = new String[1];
|
||||
people[0] = charSeq.toString();
|
||||
return people;
|
||||
}
|
||||
|
||||
CharSequence[] charSeqArray = extras.getCharSequenceArray(Notification.EXTRA_PEOPLE);
|
||||
if (charSeqArray != null) {
|
||||
final int N = charSeqArray.length;
|
||||
people = new String[N];
|
||||
for (int i = 0; i < N; i++) {
|
||||
people[i] = charSeqArray[i].toString();
|
||||
}
|
||||
return people;
|
||||
}
|
||||
|
||||
ArrayList<CharSequence> charSeqList =
|
||||
extras.getCharSequenceArrayList(Notification.EXTRA_PEOPLE);
|
||||
if (charSeqList != null) {
|
||||
final int N = charSeqList.size();
|
||||
people = new String[N];
|
||||
for (int i = 0; i < N; i++) {
|
||||
people[i] = charSeqList.get(i).toString();
|
||||
}
|
||||
return people;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private LookupResult resolvePhoneContact(final String handle, final String number) {
|
||||
LookupResult lookupResult = null;
|
||||
Cursor c = null;
|
||||
try {
|
||||
Uri numberUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
|
||||
Uri.encode(number));
|
||||
c = mContext.getContentResolver().query(numberUri, LOOKUP_PROJECTION, null, null, null);
|
||||
if (c != null && c.getCount() > 0) {
|
||||
c.moveToFirst();
|
||||
final int idIdx = c.getColumnIndex(Contacts._ID);
|
||||
final int id = c.getInt(idIdx);
|
||||
if (DEBUG) Slog.d(TAG, "is valid: " + id);
|
||||
lookupResult = new LookupResult(id);
|
||||
}
|
||||
} catch(Throwable t) {
|
||||
Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
if (lookupResult == null) {
|
||||
lookupResult = new LookupResult(LookupResult.INVALID_ID);
|
||||
}
|
||||
synchronized (mPeopleCache) {
|
||||
mPeopleCache.put(handle, lookupResult);
|
||||
}
|
||||
return lookupResult;
|
||||
}
|
||||
|
||||
private LookupResult resolveContactsUri(String handle, final Uri personUri) {
|
||||
LookupResult lookupResult = null;
|
||||
Cursor c = null;
|
||||
try {
|
||||
c = mContext.getContentResolver().query(personUri, LOOKUP_PROJECTION, null, null, null);
|
||||
if (c != null && c.getCount() > 0) {
|
||||
c.moveToFirst();
|
||||
final int idIdx = c.getColumnIndex(Contacts._ID);
|
||||
final int id = c.getInt(idIdx);
|
||||
if (DEBUG) Slog.d(TAG, "is valid: " + id);
|
||||
lookupResult = new LookupResult(id);
|
||||
}
|
||||
} catch(Throwable t) {
|
||||
Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t);
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
if (lookupResult == null) {
|
||||
lookupResult = new LookupResult(LookupResult.INVALID_ID);
|
||||
}
|
||||
synchronized (mPeopleCache) {
|
||||
mPeopleCache.put(handle, lookupResult);
|
||||
}
|
||||
return lookupResult;
|
||||
}
|
||||
|
||||
public void initialize(Context context) {
|
||||
if (DEBUG) Slog.d(TAG, "Initializing " + getClass().getSimpleName() + ".");
|
||||
mContext = context;
|
||||
mPeopleCache = new LruCache<String, LookupResult>(PEOPLE_CACHE_SIZE);
|
||||
mEnabled = ENABLE_PEOPLE_VALIDATOR && 1 == Settings.Global.getInt(
|
||||
mContext.getContentResolver(), SETTING_ENABLE_PEOPLE_VALIDATOR, 1);
|
||||
}
|
||||
|
||||
public RankingFuture process(NotificationManagerService.NotificationRecord record) {
|
||||
if (!mEnabled) {
|
||||
if (INFO) Slog.i(TAG, "disabled");
|
||||
return null;
|
||||
}
|
||||
if (record == null || record.getNotification() == null) {
|
||||
if (INFO) Slog.i(TAG, "skipping empty notification");
|
||||
return null;
|
||||
}
|
||||
return validatePeople(record);
|
||||
}
|
||||
|
||||
private static class LookupResult {
|
||||
private static final long CONTACT_REFRESH_MILLIS = 60 * 60 * 1000; // 1hr
|
||||
public static final int INVALID_ID = -1;
|
||||
|
||||
private final long mExpireMillis;
|
||||
private int mId;
|
||||
|
||||
public LookupResult(int id) {
|
||||
mId = id;
|
||||
mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS;
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return mExpireMillis < System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public boolean isInvalid() {
|
||||
return mId == INVALID_ID || isExpired();
|
||||
}
|
||||
|
||||
public float getAffinity() {
|
||||
if (isInvalid()) {
|
||||
return NONE;
|
||||
} else {
|
||||
return VALID_CONTACT; // TODO: finer grained result: stars
|
||||
}
|
||||
}
|
||||
|
||||
public LookupResult setId(int id) {
|
||||
mId = id;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user