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