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