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.content.pm.PackageManager;
     21 import android.content.pm.PackageManager.NameNotFoundException;
     22 import android.os.UserHandle;
     23 import android.service.notification.NotificationListenerService.Ranking;
     24 import android.text.TextUtils;
     25 import android.util.ArrayMap;
     26 import android.util.Slog;
     27 
     28 import com.android.server.notification.NotificationManagerService.DumpFilter;
     29 
     30 import org.json.JSONArray;
     31 import org.json.JSONException;
     32 import org.json.JSONObject;
     33 import org.xmlpull.v1.XmlPullParser;
     34 import org.xmlpull.v1.XmlPullParserException;
     35 import org.xmlpull.v1.XmlSerializer;
     36 
     37 import java.io.IOException;
     38 import java.io.PrintWriter;
     39 import java.util.ArrayList;
     40 import java.util.Collections;
     41 import java.util.Map;
     42 import java.util.Map.Entry;
     43 
     44 public class RankingHelper implements RankingConfig {
     45     private static final String TAG = "RankingHelper";
     46 
     47     private static final int XML_VERSION = 1;
     48 
     49     private static final String TAG_RANKING = "ranking";
     50     private static final String TAG_PACKAGE = "package";
     51     private static final String ATT_VERSION = "version";
     52 
     53     private static final String ATT_NAME = "name";
     54     private static final String ATT_UID = "uid";
     55     private static final String ATT_PRIORITY = "priority";
     56     private static final String ATT_VISIBILITY = "visibility";
     57     private static final String ATT_IMPORTANCE = "importance";
     58     private static final String ATT_TOPIC_ID = "id";
     59     private static final String ATT_TOPIC_LABEL = "label";
     60 
     61     private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT;
     62     private static final int DEFAULT_VISIBILITY = Ranking.VISIBILITY_NO_OVERRIDE;
     63     private static final int DEFAULT_IMPORTANCE = Ranking.IMPORTANCE_UNSPECIFIED;
     64 
     65     private final NotificationSignalExtractor[] mSignalExtractors;
     66     private final NotificationComparator mPreliminaryComparator = new NotificationComparator();
     67     private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
     68 
     69     private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record
     70     private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>();
     71     private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record
     72 
     73     private final Context mContext;
     74     private final RankingHandler mRankingHandler;
     75 
     76     public RankingHelper(Context context, RankingHandler rankingHandler,
     77             NotificationUsageStats usageStats, String[] extractorNames) {
     78         mContext = context;
     79         mRankingHandler = rankingHandler;
     80 
     81         final int N = extractorNames.length;
     82         mSignalExtractors = new NotificationSignalExtractor[N];
     83         for (int i = 0; i < N; i++) {
     84             try {
     85                 Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
     86                 NotificationSignalExtractor extractor =
     87                         (NotificationSignalExtractor) extractorClass.newInstance();
     88                 extractor.initialize(mContext, usageStats);
     89                 extractor.setConfig(this);
     90                 mSignalExtractors[i] = extractor;
     91             } catch (ClassNotFoundException e) {
     92                 Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
     93             } catch (InstantiationException e) {
     94                 Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e);
     95             } catch (IllegalAccessException e) {
     96                 Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
     97             }
     98         }
     99     }
    100 
    101     @SuppressWarnings("unchecked")
    102     public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) {
    103         final int N = mSignalExtractors.length;
    104         for (int i = 0; i < N; i++) {
    105             final NotificationSignalExtractor extractor = mSignalExtractors[i];
    106             if (extractorClass.equals(extractor.getClass())) {
    107                 return (T) extractor;
    108             }
    109         }
    110         return null;
    111     }
    112 
    113     public void extractSignals(NotificationRecord r) {
    114         final int N = mSignalExtractors.length;
    115         for (int i = 0; i < N; i++) {
    116             NotificationSignalExtractor extractor = mSignalExtractors[i];
    117             try {
    118                 RankingReconsideration recon = extractor.process(r);
    119                 if (recon != null) {
    120                     mRankingHandler.requestReconsideration(recon);
    121                 }
    122             } catch (Throwable t) {
    123                 Slog.w(TAG, "NotificationSignalExtractor failed.", t);
    124             }
    125         }
    126     }
    127 
    128     public void readXml(XmlPullParser parser, boolean forRestore)
    129             throws XmlPullParserException, IOException {
    130         final PackageManager pm = mContext.getPackageManager();
    131         int type = parser.getEventType();
    132         if (type != XmlPullParser.START_TAG) return;
    133         String tag = parser.getName();
    134         if (!TAG_RANKING.equals(tag)) return;
    135         mRecords.clear();
    136         mRestoredWithoutUids.clear();
    137         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
    138             tag = parser.getName();
    139             if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
    140                 return;
    141             }
    142             if (type == XmlPullParser.START_TAG) {
    143                 if (TAG_PACKAGE.equals(tag)) {
    144                     int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID);
    145                     String name = parser.getAttributeValue(null, ATT_NAME);
    146 
    147                     if (!TextUtils.isEmpty(name)) {
    148                         if (forRestore) {
    149                             try {
    150                                 //TODO: http://b/22388012
    151                                 uid = pm.getPackageUidAsUser(name, UserHandle.USER_SYSTEM);
    152                             } catch (NameNotFoundException e) {
    153                                 // noop
    154                             }
    155                         }
    156                         Record r = null;
    157                         if (uid == Record.UNKNOWN_UID) {
    158                             r = mRestoredWithoutUids.get(name);
    159                             if (r == null) {
    160                                 r = new Record();
    161                                 mRestoredWithoutUids.put(name, r);
    162                             }
    163                         } else {
    164                             r = getOrCreateRecord(name, uid);
    165                         }
    166                         r.importance = safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
    167                         r.priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY);
    168                         r.visibility = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY);
    169                     }
    170                 }
    171             }
    172         }
    173         throw new IllegalStateException("Failed to reach END_DOCUMENT");
    174     }
    175 
    176     private static String recordKey(String pkg, int uid) {
    177         return pkg + "|" + uid;
    178     }
    179 
    180     private Record getOrCreateRecord(String pkg, int uid) {
    181         final String key = recordKey(pkg, uid);
    182         Record r = mRecords.get(key);
    183         if (r == null) {
    184             r = new Record();
    185             r.pkg = pkg;
    186             r.uid = uid;
    187             mRecords.put(key, r);
    188         }
    189         return r;
    190     }
    191 
    192     public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
    193         out.startTag(null, TAG_RANKING);
    194         out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
    195 
    196         final int N = mRecords.size();
    197         for (int i = 0; i < N; i++) {
    198             final Record r = mRecords.valueAt(i);
    199             //TODO: http://b/22388012
    200             if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
    201                 continue;
    202             }
    203             final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE
    204                     || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY;
    205             if (hasNonDefaultSettings) {
    206                 out.startTag(null, TAG_PACKAGE);
    207                 out.attribute(null, ATT_NAME, r.pkg);
    208                 if (r.importance != DEFAULT_IMPORTANCE) {
    209                     out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance));
    210                 }
    211                 if (r.priority != DEFAULT_PRIORITY) {
    212                     out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority));
    213                 }
    214                 if (r.visibility != DEFAULT_VISIBILITY) {
    215                     out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
    216                 }
    217 
    218                 if (!forBackup) {
    219                     out.attribute(null, ATT_UID, Integer.toString(r.uid));
    220                 }
    221 
    222                 out.endTag(null, TAG_PACKAGE);
    223             }
    224         }
    225         out.endTag(null, TAG_RANKING);
    226     }
    227 
    228     private void updateConfig() {
    229         final int N = mSignalExtractors.length;
    230         for (int i = 0; i < N; i++) {
    231             mSignalExtractors[i].setConfig(this);
    232         }
    233         mRankingHandler.requestSort();
    234     }
    235 
    236     public void sort(ArrayList<NotificationRecord> notificationList) {
    237         final int N = notificationList.size();
    238         // clear global sort keys
    239         for (int i = N - 1; i >= 0; i--) {
    240             notificationList.get(i).setGlobalSortKey(null);
    241         }
    242 
    243         // rank each record individually
    244         Collections.sort(notificationList, mPreliminaryComparator);
    245 
    246         synchronized (mProxyByGroupTmp) {
    247             // record individual ranking result and nominate proxies for each group
    248             for (int i = N - 1; i >= 0; i--) {
    249                 final NotificationRecord record = notificationList.get(i);
    250                 record.setAuthoritativeRank(i);
    251                 final String groupKey = record.getGroupKey();
    252                 boolean isGroupSummary = record.getNotification().isGroupSummary();
    253                 if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) {
    254                     mProxyByGroupTmp.put(groupKey, record);
    255                 }
    256             }
    257             // assign global sort key:
    258             //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
    259             for (int i = 0; i < N; i++) {
    260                 final NotificationRecord record = notificationList.get(i);
    261                 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
    262                 String groupSortKey = record.getNotification().getSortKey();
    263 
    264                 // We need to make sure the developer provided group sort key (gsk) is handled
    265                 // correctly:
    266                 //   gsk="" < gsk=non-null-string < gsk=null
    267                 //
    268                 // We enforce this by using different prefixes for these three cases.
    269                 String groupSortKeyPortion;
    270                 if (groupSortKey == null) {
    271                     groupSortKeyPortion = "nsk";
    272                 } else if (groupSortKey.equals("")) {
    273                     groupSortKeyPortion = "esk";
    274                 } else {
    275                     groupSortKeyPortion = "gsk=" + groupSortKey;
    276                 }
    277 
    278                 boolean isGroupSummary = record.getNotification().isGroupSummary();
    279                 record.setGlobalSortKey(
    280                         String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
    281                         record.isRecentlyIntrusive() ? '0' : '1',
    282                         groupProxy.getAuthoritativeRank(),
    283                         isGroupSummary ? '0' : '1',
    284                         groupSortKeyPortion,
    285                         record.getAuthoritativeRank()));
    286             }
    287             mProxyByGroupTmp.clear();
    288         }
    289 
    290         // Do a second ranking pass, using group proxies
    291         Collections.sort(notificationList, mFinalComparator);
    292     }
    293 
    294     public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
    295         return Collections.binarySearch(notificationList, target, mFinalComparator);
    296     }
    297 
    298     private static int safeInt(XmlPullParser parser, String att, int defValue) {
    299         final String val = parser.getAttributeValue(null, att);
    300         return tryParseInt(val, defValue);
    301     }
    302 
    303     private static int tryParseInt(String value, int defValue) {
    304         if (TextUtils.isEmpty(value)) return defValue;
    305         try {
    306             return Integer.parseInt(value);
    307         } catch (NumberFormatException e) {
    308             return defValue;
    309         }
    310     }
    311 
    312     private static boolean tryParseBool(String value, boolean defValue) {
    313         if (TextUtils.isEmpty(value)) return defValue;
    314         return Boolean.valueOf(value);
    315     }
    316 
    317     /**
    318      * Gets priority.
    319      */
    320     @Override
    321     public int getPriority(String packageName, int uid) {
    322         return getOrCreateRecord(packageName, uid).priority;
    323     }
    324 
    325     /**
    326      * Sets priority.
    327      */
    328     @Override
    329     public void setPriority(String packageName, int uid, int priority) {
    330         getOrCreateRecord(packageName, uid).priority = priority;
    331         updateConfig();
    332     }
    333 
    334     /**
    335      * Gets visual override.
    336      */
    337     @Override
    338     public int getVisibilityOverride(String packageName, int uid) {
    339         return getOrCreateRecord(packageName, uid).visibility;
    340     }
    341 
    342     /**
    343      * Sets visibility override.
    344      */
    345     @Override
    346     public void setVisibilityOverride(String pkgName, int uid, int visibility) {
    347         getOrCreateRecord(pkgName, uid).visibility = visibility;
    348         updateConfig();
    349     }
    350 
    351     /**
    352      * Gets importance.
    353      */
    354     @Override
    355     public int getImportance(String packageName, int uid) {
    356         return getOrCreateRecord(packageName, uid).importance;
    357     }
    358 
    359     /**
    360      * Sets importance.
    361      */
    362     @Override
    363     public void setImportance(String pkgName, int uid, int importance) {
    364         getOrCreateRecord(pkgName, uid).importance = importance;
    365         updateConfig();
    366     }
    367 
    368     public void setEnabled(String packageName, int uid, boolean enabled) {
    369         boolean wasEnabled = getImportance(packageName, uid) != Ranking.IMPORTANCE_NONE;
    370         if (wasEnabled == enabled) {
    371             return;
    372         }
    373         setImportance(packageName, uid, enabled ? DEFAULT_IMPORTANCE : Ranking.IMPORTANCE_NONE);
    374     }
    375 
    376     public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
    377         if (filter == null) {
    378             final int N = mSignalExtractors.length;
    379             pw.print(prefix);
    380             pw.print("mSignalExtractors.length = ");
    381             pw.println(N);
    382             for (int i = 0; i < N; i++) {
    383                 pw.print(prefix);
    384                 pw.print("  ");
    385                 pw.println(mSignalExtractors[i]);
    386             }
    387         }
    388         if (filter == null) {
    389             pw.print(prefix);
    390             pw.println("per-package config:");
    391         }
    392         pw.println("Records:");
    393         dumpRecords(pw, prefix, filter, mRecords);
    394         pw.println("Restored without uid:");
    395         dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
    396     }
    397 
    398     private static void dumpRecords(PrintWriter pw, String prefix,
    399             NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
    400         final int N = records.size();
    401         for (int i = 0; i < N; i++) {
    402             final Record r = records.valueAt(i);
    403             if (filter == null || filter.matches(r.pkg)) {
    404                 pw.print(prefix);
    405                 pw.print("  ");
    406                 pw.print(r.pkg);
    407                 pw.print(" (");
    408                 pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
    409                 pw.print(')');
    410                 if (r.importance != DEFAULT_IMPORTANCE) {
    411                     pw.print(" importance=");
    412                     pw.print(Ranking.importanceToString(r.importance));
    413                 }
    414                 if (r.priority != DEFAULT_PRIORITY) {
    415                     pw.print(" priority=");
    416                     pw.print(Notification.priorityToString(r.priority));
    417                 }
    418                 if (r.visibility != DEFAULT_VISIBILITY) {
    419                     pw.print(" visibility=");
    420                     pw.print(Notification.visibilityToString(r.visibility));
    421                 }
    422                 pw.println();
    423             }
    424         }
    425     }
    426 
    427     public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) {
    428         JSONObject ranking = new JSONObject();
    429         JSONArray records = new JSONArray();
    430         try {
    431             ranking.put("noUid", mRestoredWithoutUids.size());
    432         } catch (JSONException e) {
    433            // pass
    434         }
    435         final int N = mRecords.size();
    436         for (int i = 0; i < N; i++) {
    437             final Record r = mRecords.valueAt(i);
    438             if (filter == null || filter.matches(r.pkg)) {
    439                 JSONObject record = new JSONObject();
    440                 try {
    441                     record.put("userId", UserHandle.getUserId(r.uid));
    442                     record.put("packageName", r.pkg);
    443                     if (r.importance != DEFAULT_IMPORTANCE) {
    444                         record.put("importance", Ranking.importanceToString(r.importance));
    445                     }
    446                     if (r.priority != DEFAULT_PRIORITY) {
    447                         record.put("priority", Notification.priorityToString(r.priority));
    448                     }
    449                     if (r.visibility != DEFAULT_VISIBILITY) {
    450                         record.put("visibility", Notification.visibilityToString(r.visibility));
    451                     }
    452                 } catch (JSONException e) {
    453                    // pass
    454                 }
    455                 records.put(record);
    456             }
    457         }
    458         try {
    459             ranking.put("records", records);
    460         } catch (JSONException e) {
    461             // pass
    462         }
    463         return ranking;
    464     }
    465 
    466     /**
    467      * Dump only the ban information as structured JSON for the stats collector.
    468      *
    469      * This is intentionally redundant with {#link dumpJson} because the old
    470      * scraper will expect this format.
    471      *
    472      * @param filter
    473      * @return
    474      */
    475     public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) {
    476         JSONArray bans = new JSONArray();
    477         Map<Integer, String> packageBans = getPackageBans();
    478         for(Entry<Integer, String> ban : packageBans.entrySet()) {
    479             final int userId = UserHandle.getUserId(ban.getKey());
    480             final String packageName = ban.getValue();
    481             if (filter == null || filter.matches(packageName)) {
    482                 JSONObject banJson = new JSONObject();
    483                 try {
    484                     banJson.put("userId", userId);
    485                     banJson.put("packageName", packageName);
    486                 } catch (JSONException e) {
    487                     e.printStackTrace();
    488                 }
    489                 bans.put(banJson);
    490             }
    491         }
    492         return bans;
    493     }
    494 
    495     public Map<Integer, String> getPackageBans() {
    496         final int N = mRecords.size();
    497         ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
    498         for (int i = 0; i < N; i++) {
    499             final Record r = mRecords.valueAt(i);
    500             if (r.importance == Ranking.IMPORTANCE_NONE) {
    501                 packageBans.put(r.uid, r.pkg);
    502             }
    503         }
    504         return packageBans;
    505     }
    506 
    507     public void onPackagesChanged(boolean removingPackage, String[] pkgList) {
    508         if (!removingPackage || pkgList == null || pkgList.length == 0
    509                 || mRestoredWithoutUids.isEmpty()) {
    510             return; // nothing to do
    511         }
    512         final PackageManager pm = mContext.getPackageManager();
    513         boolean updated = false;
    514         for (String pkg : pkgList) {
    515             final Record r = mRestoredWithoutUids.get(pkg);
    516             if (r != null) {
    517                 try {
    518                     //TODO: http://b/22388012
    519                     r.uid = pm.getPackageUidAsUser(r.pkg, UserHandle.USER_SYSTEM);
    520                     mRestoredWithoutUids.remove(pkg);
    521                     mRecords.put(recordKey(r.pkg, r.uid), r);
    522                     updated = true;
    523                 } catch (NameNotFoundException e) {
    524                     // noop
    525                 }
    526             }
    527         }
    528         if (updated) {
    529             updateConfig();
    530         }
    531     }
    532 
    533     private static class Record {
    534         static int UNKNOWN_UID = UserHandle.USER_NULL;
    535 
    536         String pkg;
    537         int uid = UNKNOWN_UID;
    538         int importance = DEFAULT_IMPORTANCE;
    539         int priority = DEFAULT_PRIORITY;
    540         int visibility = DEFAULT_VISIBILITY;
    541    }
    542 }
    543