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         // Lock comparator state for consistent compare() results.
108         synchronized (mPreliminaryComparator.mStateLock) {
109             notificationList.sort(mPreliminaryComparator);
110         }
111 
112         synchronized (mProxyByGroupTmp) {
113             // record individual ranking result and nominate proxies for each group
114             for (int i = 0; i < N; i++) {
115                 final NotificationRecord record = notificationList.get(i);
116                 record.setAuthoritativeRank(i);
117                 final String groupKey = record.getGroupKey();
118                 NotificationRecord existingProxy = mProxyByGroupTmp.get(groupKey);
119                 if (existingProxy == null) {
120                     mProxyByGroupTmp.put(groupKey, record);
121                 }
122             }
123             // assign global sort key:
124             //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
125             for (int i = 0; i < N; i++) {
126                 final NotificationRecord record = notificationList.get(i);
127                 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
128                 String groupSortKey = record.getNotification().getSortKey();
129 
130                 // We need to make sure the developer provided group sort key (gsk) is handled
131                 // correctly:
132                 //   gsk="" < gsk=non-null-string < gsk=null
133                 //
134                 // We enforce this by using different prefixes for these three cases.
135                 String groupSortKeyPortion;
136                 if (groupSortKey == null) {
137                     groupSortKeyPortion = "nsk";
138                 } else if (groupSortKey.equals("")) {
139                     groupSortKeyPortion = "esk";
140                 } else {
141                     groupSortKeyPortion = "gsk=" + groupSortKey;
142                 }
143 
144                 boolean isGroupSummary = record.getNotification().isGroupSummary();
145                 record.setGlobalSortKey(
146                         formatSimple("crtcl=0x%04x:intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
147                         record.getCriticality(),
148                         record.isRecentlyIntrusive()
149                                 && record.getImportance() > NotificationManager.IMPORTANCE_MIN
150                                 ? '0' : '1',
151                         groupProxy.getAuthoritativeRank(),
152                         isGroupSummary ? '0' : '1',
153                         groupSortKeyPortion,
154                         record.getAuthoritativeRank()));
155             }
156             mProxyByGroupTmp.clear();
157         }
158 
159         // Do a second ranking pass, using group proxies
160         Collections.sort(notificationList, mFinalComparator);
161     }
162 
indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target)163     public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
164         return Collections.binarySearch(notificationList, target, mFinalComparator);
165     }
166 
dump(PrintWriter pw, String prefix, @NonNull NotificationManagerService.DumpFilter filter)167     public void dump(PrintWriter pw, String prefix,
168             @NonNull NotificationManagerService.DumpFilter filter) {
169         final int N = mSignalExtractors.length;
170         pw.print(prefix);
171         pw.print("mSignalExtractors.length = ");
172         pw.println(N);
173         for (int i = 0; i < N; i++) {
174             pw.print(prefix);
175             pw.print("  ");
176             pw.println(mSignalExtractors[i].getClass().getSimpleName());
177         }
178     }
179 
dump(ProtoOutputStream proto, @NonNull NotificationManagerService.DumpFilter filter)180     public void dump(ProtoOutputStream proto,
181             @NonNull NotificationManagerService.DumpFilter filter) {
182         final int N = mSignalExtractors.length;
183         for (int i = 0; i < N; i++) {
184             proto.write(RankingHelperProto.NOTIFICATION_SIGNAL_EXTRACTORS,
185                     mSignalExtractors[i].getClass().getSimpleName());
186         }
187     }
188 }
189