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