1 /* 2 * Copyright (C) 2018 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 android.ext.services.notification; 17 18 import static android.app.Notification.CATEGORY_MESSAGE; 19 import static android.app.NotificationManager.IMPORTANCE_DEFAULT; 20 import static android.app.NotificationManager.IMPORTANCE_HIGH; 21 import static android.app.NotificationManager.IMPORTANCE_LOW; 22 import static android.app.NotificationManager.IMPORTANCE_MIN; 23 import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; 24 25 import android.app.Notification; 26 import android.app.NotificationChannel; 27 import android.app.Person; 28 import android.app.RemoteInput; 29 import android.content.Context; 30 import android.content.pm.ApplicationInfo; 31 import android.content.pm.PackageManager; 32 import android.graphics.drawable.Icon; 33 import android.media.AudioAttributes; 34 import android.os.Build; 35 import android.os.Parcelable; 36 import android.service.notification.StatusBarNotification; 37 import android.util.Log; 38 import android.util.SparseArray; 39 40 import java.util.ArrayList; 41 import java.util.Objects; 42 import java.util.Set; 43 44 /** 45 * Holds data about notifications. 46 */ 47 public class NotificationEntry { 48 static final String TAG = "NotificationEntry"; 49 50 // Copied from hidden definitions in Notification.TvExtender 51 private static final String EXTRA_TV_EXTENDER = "android.tv.EXTENSIONS"; 52 53 private final Context mContext; 54 private final StatusBarNotification mSbn; 55 private final PackageManager mPackageManager; 56 private int mTargetSdkVersion = Build.VERSION_CODES.N_MR1; 57 private final boolean mPreChannelsNotification; 58 private final AudioAttributes mAttributes; 59 private final NotificationChannel mChannel; 60 private final int mImportance; 61 private boolean mSeen; 62 private boolean mIsShowActionEventLogged; 63 private final SmsHelper mSmsHelper; 64 65 private final Object mLock = new Object(); 66 NotificationEntry(Context applicationContext, PackageManager packageManager, StatusBarNotification sbn, NotificationChannel channel, SmsHelper smsHelper)67 public NotificationEntry(Context applicationContext, PackageManager packageManager, 68 StatusBarNotification sbn, NotificationChannel channel, SmsHelper smsHelper) { 69 mContext = applicationContext; 70 mSbn = cloneStatusBarNotificationLight(sbn); 71 mChannel = channel; 72 mPackageManager = packageManager; 73 mPreChannelsNotification = isPreChannelsNotification(); 74 mAttributes = calculateAudioAttributes(); 75 mImportance = calculateInitialImportance(); 76 mSmsHelper = smsHelper; 77 } 78 79 /** Adapted from {@code Notification.lightenPayload}. */ 80 @SuppressWarnings("nullness") lightenNotificationPayload(Notification notification)81 private static void lightenNotificationPayload(Notification notification) { 82 notification.tickerView = null; 83 notification.contentView = null; 84 notification.bigContentView = null; 85 notification.headsUpContentView = null; 86 notification.largeIcon = null; 87 if (notification.extras != null && !notification.extras.isEmpty()) { 88 final Set<String> keyset = notification.extras.keySet(); 89 final int keysetSize = keyset.size(); 90 final String[] keys = keyset.toArray(new String[keysetSize]); 91 for (int i = 0; i < keysetSize; i++) { 92 final String key = keys[i]; 93 if (EXTRA_TV_EXTENDER.equals(key) 94 || Notification.EXTRA_MESSAGES.equals(key) 95 || Notification.EXTRA_MESSAGING_PERSON.equals(key) 96 || Notification.EXTRA_PEOPLE_LIST.equals(key)) { 97 continue; 98 } 99 final Object obj = notification.extras.get(key); 100 if (obj != null 101 && (obj instanceof Parcelable 102 || obj instanceof Parcelable[] 103 || obj instanceof SparseArray 104 || obj instanceof ArrayList)) { 105 notification.extras.remove(key); 106 } 107 } 108 } 109 } 110 111 /** An interpretation of {@code Notification.cloneInto} with heavy=false. */ cloneNotificationLight(Notification notification)112 private Notification cloneNotificationLight(Notification notification) { 113 // We can't just use clone() here because the only way to remove the icons is with the 114 // builder, which we can only create with a Context. 115 Notification lightNotification = 116 Notification.Builder.recoverBuilder(mContext, notification) 117 .setSmallIcon(0) 118 .setLargeIcon((Icon) null) 119 .build(); 120 lightenNotificationPayload(lightNotification); 121 return lightNotification; 122 } 123 124 /** Adapted from {@code StatusBarNotification.cloneLight}. */ cloneStatusBarNotificationLight(StatusBarNotification sbn)125 public StatusBarNotification cloneStatusBarNotificationLight(StatusBarNotification sbn) { 126 return new StatusBarNotification( 127 sbn.getPackageName(), 128 sbn.getOpPkg(), 129 sbn.getId(), 130 sbn.getTag(), 131 sbn.getUid(), 132 /*initialPid=*/ 0, 133 /*score=*/ 0, 134 cloneNotificationLight(sbn.getNotification()), 135 sbn.getUser(), 136 sbn.getPostTime()); 137 } 138 isPreChannelsNotification()139 private boolean isPreChannelsNotification() { 140 try { 141 ApplicationInfo info = mPackageManager.getApplicationInfoAsUser( 142 mSbn.getPackageName(), PackageManager.MATCH_ALL, 143 mSbn.getUser()); 144 if (info != null) { 145 mTargetSdkVersion = info.targetSdkVersion; 146 } 147 } catch (PackageManager.NameNotFoundException e) { 148 Log.w(TAG, "Couldn't look up " + mSbn.getPackageName()); 149 } 150 if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(getChannel().getId())) { 151 if (mTargetSdkVersion < Build.VERSION_CODES.O) { 152 return true; 153 } 154 } 155 return false; 156 } 157 calculateAudioAttributes()158 private AudioAttributes calculateAudioAttributes() { 159 final Notification n = getNotification(); 160 AudioAttributes attributes = getChannel().getAudioAttributes(); 161 if (attributes == null) { 162 attributes = Notification.AUDIO_ATTRIBUTES_DEFAULT; 163 } 164 165 if (mPreChannelsNotification && !getChannel().hasUserSetSound()) { 166 if (n.audioAttributes != null) { 167 // prefer audio attributes to stream type 168 attributes = n.audioAttributes; 169 } else if (n.audioStreamType >= 0) { 170 // the stream type is valid, use it 171 attributes = new AudioAttributes.Builder() 172 .setLegacyStreamType(n.audioStreamType) 173 .build(); 174 } else { 175 Log.w(TAG, String.format("Invalid stream type: %d", n.audioStreamType)); 176 } 177 } 178 return attributes; 179 } 180 calculateInitialImportance()181 private int calculateInitialImportance() { 182 final Notification n = getNotification(); 183 int importance = getChannel().getImportance(); 184 int requestedImportance = IMPORTANCE_DEFAULT; 185 186 // Migrate notification flags to scores 187 if ((n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) { 188 n.priority = Notification.PRIORITY_MAX; 189 } 190 191 n.priority = clamp(n.priority, Notification.PRIORITY_MIN, 192 Notification.PRIORITY_MAX); 193 switch (n.priority) { 194 case Notification.PRIORITY_MIN: 195 requestedImportance = IMPORTANCE_MIN; 196 break; 197 case Notification.PRIORITY_LOW: 198 requestedImportance = IMPORTANCE_LOW; 199 break; 200 case Notification.PRIORITY_DEFAULT: 201 requestedImportance = IMPORTANCE_DEFAULT; 202 break; 203 case Notification.PRIORITY_HIGH: 204 case Notification.PRIORITY_MAX: 205 requestedImportance = IMPORTANCE_HIGH; 206 break; 207 } 208 209 if (mPreChannelsNotification 210 && (importance == IMPORTANCE_UNSPECIFIED 211 || (getChannel().hasUserSetImportance()))) { 212 if (n.fullScreenIntent != null) { 213 requestedImportance = IMPORTANCE_HIGH; 214 } 215 importance = requestedImportance; 216 } 217 218 return importance; 219 } 220 isCategory(String category)221 public boolean isCategory(String category) { 222 return Objects.equals(getNotification().category, category); 223 } 224 225 /** 226 * Similar to {@link #isCategory(String)}, but checking the public version of the notification, 227 * if available. 228 */ isPublicVersionCategory(String category)229 public boolean isPublicVersionCategory(String category) { 230 Notification publicVersion = getNotification().publicVersion; 231 if (publicVersion == null) { 232 return false; 233 } 234 return Objects.equals(publicVersion.category, category); 235 } 236 isAudioAttributesUsage(int usage)237 public boolean isAudioAttributesUsage(int usage) { 238 return mAttributes != null && mAttributes.getUsage() == usage; 239 } 240 hasPerson()241 private boolean hasPerson() { 242 // TODO: cache favorite and recent contacts to check contact affinity 243 ArrayList<Person> people = getNotification().extras.getParcelableArrayList( 244 Notification.EXTRA_PEOPLE_LIST); 245 return people != null && !people.isEmpty(); 246 } 247 hasStyle(Class targetStyle)248 protected boolean hasStyle(Class targetStyle) { 249 String templateClass = getNotification().extras.getString(Notification.EXTRA_TEMPLATE); 250 return targetStyle.getName().equals(templateClass); 251 } 252 isOngoing()253 protected boolean isOngoing() { 254 return (getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0; 255 } 256 involvesPeople()257 protected boolean involvesPeople() { 258 return isMessaging() 259 || hasStyle(Notification.InboxStyle.class) 260 || hasPerson() 261 || isDefaultSmsApp(); 262 } 263 isDefaultSmsApp()264 private boolean isDefaultSmsApp() { 265 String defaultSmsApp = mSmsHelper.getDefaultSmsPackage(); 266 if (defaultSmsApp == null) { 267 return false; 268 } 269 return mSbn.getPackageName().equals(defaultSmsApp); 270 } 271 isMessaging()272 protected boolean isMessaging() { 273 return isCategory(CATEGORY_MESSAGE) 274 || isPublicVersionCategory(CATEGORY_MESSAGE) 275 || hasStyle(Notification.MessagingStyle.class); 276 } 277 hasInlineReply()278 public boolean hasInlineReply() { 279 Notification.Action[] actions = getNotification().actions; 280 if (actions == null) { 281 return false; 282 } 283 for (Notification.Action action : actions) { 284 RemoteInput[] remoteInputs = action.getRemoteInputs(); 285 if (remoteInputs == null) { 286 continue; 287 } 288 for (RemoteInput remoteInput : remoteInputs) { 289 if (remoteInput.getAllowFreeFormInput()) { 290 return true; 291 } 292 } 293 } 294 return false; 295 } 296 setSeen()297 public void setSeen() { 298 synchronized (mLock) { 299 mSeen = true; 300 } 301 } 302 setShowActionEventLogged()303 public void setShowActionEventLogged() { 304 synchronized (mLock) { 305 mIsShowActionEventLogged = true; 306 } 307 } 308 hasSeen()309 public boolean hasSeen() { 310 synchronized (mLock) { 311 return mSeen; 312 } 313 } 314 isShowActionEventLogged()315 public boolean isShowActionEventLogged() { 316 synchronized (mLock) { 317 return mIsShowActionEventLogged; 318 } 319 } 320 getSbn()321 public StatusBarNotification getSbn() { 322 return mSbn; 323 } 324 getNotification()325 public Notification getNotification() { 326 return getSbn().getNotification(); 327 } 328 getChannel()329 public NotificationChannel getChannel() { 330 return mChannel; 331 } 332 getImportance()333 public int getImportance() { 334 return mImportance; 335 } 336 clamp(int x, int low, int high)337 private int clamp(int x, int low, int high) { 338 return (x < low) ? low : ((x > high) ? high : x); 339 } 340 } 341