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 */
16package com.android.server.notification;
17
18import android.app.Notification;
19import android.content.Context;
20import android.content.pm.PackageManager;
21import android.content.pm.PackageManager.NameNotFoundException;
22import android.os.UserHandle;
23import android.service.notification.NotificationListenerService.Ranking;
24import android.text.TextUtils;
25import android.util.ArrayMap;
26import android.util.Slog;
27
28import com.android.server.notification.NotificationManagerService.DumpFilter;
29
30import org.json.JSONArray;
31import org.json.JSONException;
32import org.json.JSONObject;
33import org.xmlpull.v1.XmlPullParser;
34import org.xmlpull.v1.XmlPullParserException;
35import org.xmlpull.v1.XmlSerializer;
36
37import java.io.IOException;
38import java.io.PrintWriter;
39import java.util.ArrayList;
40import java.util.Collections;
41import java.util.Map;
42import java.util.Map.Entry;
43
44public 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 queryReplace, String[] pkgList) {
508        if (queryReplace || 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