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