Home | History | Annotate | Download | only in notification
      1 /**
      2  * Copyright (c) 2014, The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.server.notification;
     17 
     18 import android.app.Notification;
     19 import android.content.Context;
     20 import android.os.Handler;
     21 import android.os.Message;
     22 import android.os.UserHandle;
     23 import android.service.notification.NotificationListenerService;
     24 import android.text.TextUtils;
     25 import android.util.ArrayMap;
     26 import android.util.ArraySet;
     27 import android.util.Log;
     28 import android.util.Slog;
     29 import android.util.SparseIntArray;
     30 import org.xmlpull.v1.XmlPullParser;
     31 import org.xmlpull.v1.XmlPullParserException;
     32 import org.xmlpull.v1.XmlSerializer;
     33 
     34 import java.io.IOException;
     35 import java.io.PrintWriter;
     36 import java.util.ArrayList;
     37 import java.util.Collections;
     38 import java.util.Set;
     39 import java.util.concurrent.TimeUnit;
     40 
     41 public class RankingHelper implements RankingConfig {
     42     private static final String TAG = "RankingHelper";
     43     private static final boolean DEBUG = false;
     44 
     45     private static final int XML_VERSION = 1;
     46 
     47     private static final String TAG_RANKING = "ranking";
     48     private static final String TAG_PACKAGE = "package";
     49     private static final String ATT_VERSION = "version";
     50 
     51     private static final String ATT_NAME = "name";
     52     private static final String ATT_UID = "uid";
     53     private static final String ATT_PRIORITY = "priority";
     54     private static final String ATT_VISIBILITY = "visibility";
     55 
     56     private final NotificationSignalExtractor[] mSignalExtractors;
     57     private final NotificationComparator mPreliminaryComparator = new NotificationComparator();
     58     private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
     59 
     60     // Package name to uid, to priority. Would be better as Table<String, Int, Int>
     61     private final ArrayMap<String, SparseIntArray> mPackagePriorities;
     62     private final ArrayMap<String, SparseIntArray> mPackageVisibilities;
     63     private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp;
     64 
     65     private final Context mContext;
     66     private final Handler mRankingHandler;
     67 
     68     public RankingHelper(Context context, Handler rankingHandler, String[] extractorNames) {
     69         mContext = context;
     70         mRankingHandler = rankingHandler;
     71         mPackagePriorities = new ArrayMap<String, SparseIntArray>();
     72         mPackageVisibilities = new ArrayMap<String, SparseIntArray>();
     73 
     74         final int N = extractorNames.length;
     75         mSignalExtractors = new NotificationSignalExtractor[N];
     76         for (int i = 0; i < N; i++) {
     77             try {
     78                 Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
     79                 NotificationSignalExtractor extractor =
     80                         (NotificationSignalExtractor) extractorClass.newInstance();
     81                 extractor.initialize(mContext);
     82                 extractor.setConfig(this);
     83                 mSignalExtractors[i] = extractor;
     84             } catch (ClassNotFoundException e) {
     85                 Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
     86             } catch (InstantiationException e) {
     87                 Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e);
     88             } catch (IllegalAccessException e) {
     89                 Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
     90             }
     91         }
     92         mProxyByGroupTmp = new ArrayMap<String, NotificationRecord>();
     93     }
     94 
     95     public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) {
     96         final int N = mSignalExtractors.length;
     97         for (int i = 0; i < N; i++) {
     98             final NotificationSignalExtractor extractor = mSignalExtractors[i];
     99             if (extractorClass.equals(extractor.getClass())) {
    100                 return (T) extractor;
    101             }
    102         }
    103         return null;
    104     }
    105 
    106     public void extractSignals(NotificationRecord r) {
    107         final int N = mSignalExtractors.length;
    108         for (int i = 0; i < N; i++) {
    109             NotificationSignalExtractor extractor = mSignalExtractors[i];
    110             try {
    111                 RankingReconsideration recon = extractor.process(r);
    112                 if (recon != null) {
    113                     Message m = Message.obtain(mRankingHandler,
    114                             NotificationManagerService.MESSAGE_RECONSIDER_RANKING, recon);
    115                     long delay = recon.getDelay(TimeUnit.MILLISECONDS);
    116                     mRankingHandler.sendMessageDelayed(m, delay);
    117                 }
    118             } catch (Throwable t) {
    119                 Slog.w(TAG, "NotificationSignalExtractor failed.", t);
    120             }
    121         }
    122     }
    123 
    124     public void readXml(XmlPullParser parser) throws XmlPullParserException, IOException {
    125         int type = parser.getEventType();
    126         if (type != XmlPullParser.START_TAG) return;
    127         String tag = parser.getName();
    128         if (!TAG_RANKING.equals(tag)) return;
    129         mPackagePriorities.clear();
    130         final int version = safeInt(parser, ATT_VERSION, XML_VERSION);
    131         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
    132             tag = parser.getName();
    133             if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
    134                 return;
    135             }
    136             if (type == XmlPullParser.START_TAG) {
    137                 if (TAG_PACKAGE.equals(tag)) {
    138                     int uid = safeInt(parser, ATT_UID, UserHandle.USER_ALL);
    139                     int priority = safeInt(parser, ATT_PRIORITY, Notification.PRIORITY_DEFAULT);
    140                     int vis = safeInt(parser, ATT_VISIBILITY,
    141                             NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE);
    142                     String name = parser.getAttributeValue(null, ATT_NAME);
    143 
    144                     if (!TextUtils.isEmpty(name)) {
    145                         if (priority != Notification.PRIORITY_DEFAULT) {
    146                             SparseIntArray priorityByUid = mPackagePriorities.get(name);
    147                             if (priorityByUid == null) {
    148                                 priorityByUid = new SparseIntArray();
    149                                 mPackagePriorities.put(name, priorityByUid);
    150                             }
    151                             priorityByUid.put(uid, priority);
    152                         }
    153                         if (vis != NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE) {
    154                             SparseIntArray visibilityByUid = mPackageVisibilities.get(name);
    155                             if (visibilityByUid == null) {
    156                                 visibilityByUid = new SparseIntArray();
    157                                 mPackageVisibilities.put(name, visibilityByUid);
    158                             }
    159                             visibilityByUid.put(uid, vis);
    160                         }
    161                     }
    162                 }
    163             }
    164         }
    165         throw new IllegalStateException("Failed to reach END_DOCUMENT");
    166     }
    167 
    168     public void writeXml(XmlSerializer out) throws IOException {
    169         out.startTag(null, TAG_RANKING);
    170         out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
    171 
    172         final Set<String> packageNames = new ArraySet<>(mPackagePriorities.size()
    173                 + mPackageVisibilities.size());
    174         packageNames.addAll(mPackagePriorities.keySet());
    175         packageNames.addAll(mPackageVisibilities.keySet());
    176         final Set<Integer> packageUids = new ArraySet<>();
    177         for (String packageName : packageNames) {
    178             packageUids.clear();
    179             SparseIntArray priorityByUid = mPackagePriorities.get(packageName);
    180             SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName);
    181             if (priorityByUid != null) {
    182                 final int M = priorityByUid.size();
    183                 for (int j = 0; j < M; j++) {
    184                     packageUids.add(priorityByUid.keyAt(j));
    185                 }
    186             }
    187             if (visibilityByUid != null) {
    188                 final int M = visibilityByUid.size();
    189                 for (int j = 0; j < M; j++) {
    190                     packageUids.add(visibilityByUid.keyAt(j));
    191                 }
    192             }
    193             for (Integer uid : packageUids) {
    194                 out.startTag(null, TAG_PACKAGE);
    195                 out.attribute(null, ATT_NAME, packageName);
    196                 if (priorityByUid != null) {
    197                     final int priority = priorityByUid.get(uid);
    198                     if (priority != Notification.PRIORITY_DEFAULT) {
    199                         out.attribute(null, ATT_PRIORITY, Integer.toString(priority));
    200                     }
    201                 }
    202                 if (visibilityByUid != null) {
    203                     final int visibility = visibilityByUid.get(uid);
    204                     if (visibility != NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE) {
    205                         out.attribute(null, ATT_VISIBILITY, Integer.toString(visibility));
    206                     }
    207                 }
    208                 out.attribute(null, ATT_UID, Integer.toString(uid));
    209                 out.endTag(null, TAG_PACKAGE);
    210             }
    211         }
    212         out.endTag(null, TAG_RANKING);
    213     }
    214 
    215     private void updateConfig() {
    216         final int N = mSignalExtractors.length;
    217         for (int i = 0; i < N; i++) {
    218             mSignalExtractors[i].setConfig(this);
    219         }
    220         mRankingHandler.sendEmptyMessage(NotificationManagerService.MESSAGE_RANKING_CONFIG_CHANGE);
    221     }
    222 
    223     public void sort(ArrayList<NotificationRecord> notificationList) {
    224         final int N = notificationList.size();
    225         // clear global sort keys
    226         for (int i = N - 1; i >= 0; i--) {
    227             notificationList.get(i).setGlobalSortKey(null);
    228         }
    229 
    230         // rank each record individually
    231         Collections.sort(notificationList, mPreliminaryComparator);
    232 
    233         synchronized (mProxyByGroupTmp) {
    234             // record individual ranking result and nominate proxies for each group
    235             for (int i = N - 1; i >= 0; i--) {
    236                 final NotificationRecord record = notificationList.get(i);
    237                 record.setAuthoritativeRank(i);
    238                 final String groupKey = record.getGroupKey();
    239                 boolean isGroupSummary = record.getNotification().isGroupSummary();
    240                 if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) {
    241                     mProxyByGroupTmp.put(groupKey, record);
    242                 }
    243             }
    244             // assign global sort key:
    245             //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
    246             for (int i = 0; i < N; i++) {
    247                 final NotificationRecord record = notificationList.get(i);
    248                 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
    249                 String groupSortKey = record.getNotification().getSortKey();
    250 
    251                 // We need to make sure the developer provided group sort key (gsk) is handled
    252                 // correctly:
    253                 //   gsk="" < gsk=non-null-string < gsk=null
    254                 //
    255                 // We enforce this by using different prefixes for these three cases.
    256                 String groupSortKeyPortion;
    257                 if (groupSortKey == null) {
    258                     groupSortKeyPortion = "nsk";
    259                 } else if (groupSortKey.equals("")) {
    260                     groupSortKeyPortion = "esk";
    261                 } else {
    262                     groupSortKeyPortion = "gsk=" + groupSortKey;
    263                 }
    264 
    265                 boolean isGroupSummary = record.getNotification().isGroupSummary();
    266                 record.setGlobalSortKey(
    267                         String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
    268                         record.isRecentlyIntrusive() ? '0' : '1',
    269                         groupProxy.getAuthoritativeRank(),
    270                         isGroupSummary ? '0' : '1',
    271                         groupSortKeyPortion,
    272                         record.getAuthoritativeRank()));
    273             }
    274             mProxyByGroupTmp.clear();
    275         }
    276 
    277         // Do a second ranking pass, using group proxies
    278         Collections.sort(notificationList, mFinalComparator);
    279     }
    280 
    281     public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
    282         return Collections.binarySearch(notificationList, target, mFinalComparator);
    283     }
    284 
    285     private static int safeInt(XmlPullParser parser, String att, int defValue) {
    286         final String val = parser.getAttributeValue(null, att);
    287         return tryParseInt(val, defValue);
    288     }
    289 
    290     private static int tryParseInt(String value, int defValue) {
    291         if (TextUtils.isEmpty(value)) return defValue;
    292         try {
    293             return Integer.valueOf(value);
    294         } catch (NumberFormatException e) {
    295             return defValue;
    296         }
    297     }
    298 
    299     @Override
    300     public int getPackagePriority(String packageName, int uid) {
    301         int priority = Notification.PRIORITY_DEFAULT;
    302         SparseIntArray priorityByUid = mPackagePriorities.get(packageName);
    303         if (priorityByUid != null) {
    304             priority = priorityByUid.get(uid, Notification.PRIORITY_DEFAULT);
    305         }
    306         return priority;
    307     }
    308 
    309     @Override
    310     public void setPackagePriority(String packageName, int uid, int priority) {
    311         if (priority == getPackagePriority(packageName, uid)) {
    312             return;
    313         }
    314         SparseIntArray priorityByUid = mPackagePriorities.get(packageName);
    315         if (priorityByUid == null) {
    316             priorityByUid = new SparseIntArray();
    317             mPackagePriorities.put(packageName, priorityByUid);
    318         }
    319         priorityByUid.put(uid, priority);
    320         updateConfig();
    321     }
    322 
    323     @Override
    324     public int getPackageVisibilityOverride(String packageName, int uid) {
    325         int visibility = NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE;
    326         SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName);
    327         if (visibilityByUid != null) {
    328             visibility = visibilityByUid.get(uid,
    329                     NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE);
    330         }
    331         return visibility;
    332     }
    333 
    334     @Override
    335     public void setPackageVisibilityOverride(String packageName, int uid, int visibility) {
    336         if (visibility == getPackageVisibilityOverride(packageName, uid)) {
    337             return;
    338         }
    339         SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName);
    340         if (visibilityByUid == null) {
    341             visibilityByUid = new SparseIntArray();
    342             mPackageVisibilities.put(packageName, visibilityByUid);
    343         }
    344         visibilityByUid.put(uid, visibility);
    345         updateConfig();
    346     }
    347 
    348     public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
    349         if (filter == null) {
    350             final int N = mSignalExtractors.length;
    351             pw.print(prefix);
    352             pw.print("mSignalExtractors.length = ");
    353             pw.println(N);
    354             for (int i = 0; i < N; i++) {
    355                 pw.print(prefix);
    356                 pw.print("  ");
    357                 pw.println(mSignalExtractors[i]);
    358             }
    359         }
    360         final int N = mPackagePriorities.size();
    361         if (filter == null) {
    362             pw.print(prefix);
    363             pw.println("package priorities:");
    364         }
    365         for (int i = 0; i < N; i++) {
    366             String name = mPackagePriorities.keyAt(i);
    367             if (filter == null || filter.matches(name)) {
    368                 SparseIntArray priorityByUid = mPackagePriorities.get(name);
    369                 final int M = priorityByUid.size();
    370                 for (int j = 0; j < M; j++) {
    371                     int uid = priorityByUid.keyAt(j);
    372                     int priority = priorityByUid.get(uid);
    373                     pw.print(prefix);
    374                     pw.print("  ");
    375                     pw.print(name);
    376                     pw.print(" (");
    377                     pw.print(uid);
    378                     pw.print(") has priority: ");
    379                     pw.println(priority);
    380                 }
    381             }
    382         }
    383     }
    384 }
    385