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