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 static android.text.TextUtils.formatSimple;
19 
20 import android.annotation.NonNull;
21 import android.app.NotificationManager;
22 import android.content.Context;
23 import android.service.notification.RankingHelperProto;
24 import android.util.ArrayMap;
25 import android.util.Slog;
26 import android.util.proto.ProtoOutputStream;
27 
28 import java.io.PrintWriter;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 
32 public class RankingHelper {
33     private static final String TAG = "RankingHelper";
34 
35     private final NotificationSignalExtractor[] mSignalExtractors;
36     private final NotificationComparator mPreliminaryComparator;
37     private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
38 
39     private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>();
40 
41     private final Context mContext;
42     private final RankingHandler mRankingHandler;
43 
44 
RankingHelper(Context context, RankingHandler rankingHandler, RankingConfig config, ZenModeHelper zenHelper, NotificationUsageStats usageStats, String[] extractorNames)45     public RankingHelper(Context context, RankingHandler rankingHandler, RankingConfig config,
46             ZenModeHelper zenHelper, NotificationUsageStats usageStats, String[] extractorNames) {
47         mContext = context;
48         mRankingHandler = rankingHandler;
49         mPreliminaryComparator = new NotificationComparator(mContext);
50 
51         final int N = extractorNames.length;
52         mSignalExtractors = new NotificationSignalExtractor[N];
53         for (int i = 0; i < N; i++) {
54             try {
55                 Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
56                 NotificationSignalExtractor extractor =
57                         (NotificationSignalExtractor) extractorClass.newInstance();
58                 extractor.initialize(mContext, usageStats);
59                 extractor.setConfig(config);
60                 extractor.setZenHelper(zenHelper);
61                 mSignalExtractors[i] = extractor;
62             } catch (ClassNotFoundException e) {
63                 Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
64             } catch (InstantiationException e) {
65                 Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e);
66             } catch (IllegalAccessException e) {
67                 Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
68             }
69         }
70     }
71 
72     @SuppressWarnings("unchecked")
findExtractor(Class<T> extractorClass)73     public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) {
74         final int N = mSignalExtractors.length;
75         for (int i = 0; i < N; i++) {
76             final NotificationSignalExtractor extractor = mSignalExtractors[i];
77             if (extractorClass.equals(extractor.getClass())) {
78                 return (T) extractor;
79             }
80         }
81         return null;
82     }
83 
extractSignals(NotificationRecord r)84     public void extractSignals(NotificationRecord r) {
85         final int N = mSignalExtractors.length;
86         for (int i = 0; i < N; i++) {
87             NotificationSignalExtractor extractor = mSignalExtractors[i];
88             try {
89                 RankingReconsideration recon = extractor.process(r);
90                 if (recon != null) {
91                     mRankingHandler.requestReconsideration(recon);
92                 }
93             } catch (Throwable t) {
94                 Slog.w(TAG, "NotificationSignalExtractor failed.", t);
95             }
96         }
97     }
98 
sort(ArrayList<NotificationRecord> notificationList)99     public void sort(ArrayList<NotificationRecord> notificationList) {
100         final int N = notificationList.size();
101         // clear global sort keys
102         for (int i = N - 1; i >= 0; i--) {
103             notificationList.get(i).setGlobalSortKey(null);
104         }
105 
106         // rank each record individually
107         Collections.sort(notificationList, mPreliminaryComparator);
108 
109         synchronized (mProxyByGroupTmp) {
110             // record individual ranking result and nominate proxies for each group
111             for (int i = 0; i < N; i++) {
112                 final NotificationRecord record = notificationList.get(i);
113                 record.setAuthoritativeRank(i);
114                 final String groupKey = record.getGroupKey();
115                 NotificationRecord existingProxy = mProxyByGroupTmp.get(groupKey);
116                 if (existingProxy == null) {
117                     mProxyByGroupTmp.put(groupKey, record);
118                 }
119             }
120             // assign global sort key:
121             //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
122             for (int i = 0; i < N; i++) {
123                 final NotificationRecord record = notificationList.get(i);
124                 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
125                 String groupSortKey = record.getNotification().getSortKey();
126 
127                 // We need to make sure the developer provided group sort key (gsk) is handled
128                 // correctly:
129                 //   gsk="" < gsk=non-null-string < gsk=null
130                 //
131                 // We enforce this by using different prefixes for these three cases.
132                 String groupSortKeyPortion;
133                 if (groupSortKey == null) {
134                     groupSortKeyPortion = "nsk";
135                 } else if (groupSortKey.equals("")) {
136                     groupSortKeyPortion = "esk";
137                 } else {
138                     groupSortKeyPortion = "gsk=" + groupSortKey;
139                 }
140 
141                 boolean isGroupSummary = record.getNotification().isGroupSummary();
142                 record.setGlobalSortKey(
143                         formatSimple("crtcl=0x%04x:intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
144                         record.getCriticality(),
145                         record.isRecentlyIntrusive()
146                                 && record.getImportance() > NotificationManager.IMPORTANCE_MIN
147                                 ? '0' : '1',
148                         groupProxy.getAuthoritativeRank(),
149                         isGroupSummary ? '0' : '1',
150                         groupSortKeyPortion,
151                         record.getAuthoritativeRank()));
152             }
153             mProxyByGroupTmp.clear();
154         }
155 
156         // Do a second ranking pass, using group proxies
157         Collections.sort(notificationList, mFinalComparator);
158     }
159 
indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target)160     public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
161         return Collections.binarySearch(notificationList, target, mFinalComparator);
162     }
163 
dump(PrintWriter pw, String prefix, @NonNull NotificationManagerService.DumpFilter filter)164     public void dump(PrintWriter pw, String prefix,
165             @NonNull NotificationManagerService.DumpFilter filter) {
166         final int N = mSignalExtractors.length;
167         pw.print(prefix);
168         pw.print("mSignalExtractors.length = ");
169         pw.println(N);
170         for (int i = 0; i < N; i++) {
171             pw.print(prefix);
172             pw.print("  ");
173             pw.println(mSignalExtractors[i].getClass().getSimpleName());
174         }
175     }
176 
dump(ProtoOutputStream proto, @NonNull NotificationManagerService.DumpFilter filter)177     public void dump(ProtoOutputStream proto,
178             @NonNull NotificationManagerService.DumpFilter filter) {
179         final int N = mSignalExtractors.length;
180         for (int i = 0; i < N; i++) {
181             proto.write(RankingHelperProto.NOTIFICATION_SIGNAL_EXTRACTORS,
182                     mSignalExtractors[i].getClass().getSimpleName());
183         }
184     }
185 }
186