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