1 /*
2  * Copyright (C) 2008 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 
17 package android.service.notification;
18 
19 import static android.app.NotificationChannel.PLACEHOLDER_CONVERSATION_ID;
20 
21 import android.annotation.NonNull;
22 import android.app.Notification;
23 import android.app.NotificationManager;
24 import android.app.Person;
25 import android.compat.annotation.UnsupportedAppUsage;
26 import android.content.Context;
27 import android.content.pm.ApplicationInfo;
28 import android.content.pm.PackageManager;
29 import android.metrics.LogMaker;
30 import android.os.Build;
31 import android.os.Parcel;
32 import android.os.Parcelable;
33 import android.os.UserHandle;
34 import android.provider.Settings;
35 import android.text.TextUtils;
36 
37 import com.android.internal.logging.InstanceId;
38 import com.android.internal.logging.nano.MetricsProto;
39 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
40 
41 import java.util.ArrayList;
42 
43 /**
44  * Class encapsulating a Notification. Sent by the NotificationManagerService to clients including
45  * the status bar and any {@link android.service.notification.NotificationListenerService}s.
46  */
47 public class StatusBarNotification implements Parcelable {
48     static final int MAX_LOG_TAG_LENGTH = 36;
49 
50     @UnsupportedAppUsage
51     private final String pkg;
52     @UnsupportedAppUsage
53     private final int id;
54     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
55     private final String tag;
56     private final String key;
57     private String groupKey;
58     private String overrideGroupKey;
59 
60     @UnsupportedAppUsage
61     private final int uid;
62     private final String opPkg;
63     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
64     private final int initialPid;
65     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
66     private final Notification notification;
67     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
68     private final UserHandle user;
69     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
70     private final long postTime;
71     // A small per-notification ID, used for statsd logging.
72     private InstanceId mInstanceId;  // Not final, see setInstanceId()
73 
74     private Context mContext; // used for inflation & icon expansion
75 
76     /** @hide */
StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid, int initialPid, Notification notification, UserHandle user, String overrideGroupKey, long postTime)77     public StatusBarNotification(String pkg, String opPkg, int id,
78             String tag, int uid, int initialPid, Notification notification, UserHandle user,
79             String overrideGroupKey, long postTime) {
80         if (pkg == null) throw new NullPointerException();
81         if (notification == null) throw new NullPointerException();
82 
83         this.pkg = pkg;
84         this.opPkg = opPkg;
85         this.id = id;
86         this.tag = tag;
87         this.uid = uid;
88         this.initialPid = initialPid;
89         this.notification = notification;
90         this.user = user;
91         this.postTime = postTime;
92         this.overrideGroupKey = overrideGroupKey;
93         this.key = key();
94         this.groupKey = groupKey();
95     }
96 
97     /**
98      * @deprecated Non-system apps should not need to create StatusBarNotifications.
99      */
100     @Deprecated
StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid, int initialPid, int score, Notification notification, UserHandle user, long postTime)101     public StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid,
102             int initialPid, int score, Notification notification, UserHandle user,
103             long postTime) {
104         if (pkg == null) throw new NullPointerException();
105         if (notification == null) throw new NullPointerException();
106 
107         this.pkg = pkg;
108         this.opPkg = opPkg;
109         this.id = id;
110         this.tag = tag;
111         this.uid = uid;
112         this.initialPid = initialPid;
113         this.notification = notification;
114         this.user = user;
115         this.postTime = postTime;
116         this.key = key();
117         this.groupKey = groupKey();
118     }
119 
StatusBarNotification(Parcel in)120     public StatusBarNotification(Parcel in) {
121         this.pkg = in.readString();
122         this.opPkg = in.readString();
123         this.id = in.readInt();
124         if (in.readInt() != 0) {
125             this.tag = in.readString();
126         } else {
127             this.tag = null;
128         }
129         this.uid = in.readInt();
130         this.initialPid = in.readInt();
131         this.notification = new Notification(in);
132         this.user = UserHandle.readFromParcel(in);
133         this.postTime = in.readLong();
134         if (in.readInt() != 0) {
135             this.overrideGroupKey = in.readString();
136         }
137         if (in.readInt() != 0) {
138             this.mInstanceId = InstanceId.CREATOR.createFromParcel(in);
139         }
140         this.key = key();
141         this.groupKey = groupKey();
142     }
143 
key()144     private String key() {
145         String sbnKey = user.getIdentifier() + "|" + pkg + "|" + id + "|" + tag + "|" + uid;
146         if (overrideGroupKey != null && getNotification().isGroupSummary()) {
147             sbnKey = sbnKey + "|" + overrideGroupKey;
148         }
149         return sbnKey;
150     }
151 
groupKey()152     private String groupKey() {
153         if (overrideGroupKey != null) {
154             return user.getIdentifier() + "|" + pkg + "|" + "g:" + overrideGroupKey;
155         }
156         final String group = getNotification().getGroup();
157         final String sortKey = getNotification().getSortKey();
158         if (group == null && sortKey == null) {
159             // a group of one
160             return key;
161         }
162         return user.getIdentifier() + "|" + pkg + "|" +
163                 (group == null
164                         ? "c:" + notification.getChannelId()
165                         : "g:" + group);
166     }
167 
168     /**
169      * Returns true if this notification is part of a group.
170      */
isGroup()171     public boolean isGroup() {
172         if (overrideGroupKey != null || isAppGroup()) {
173             return true;
174         }
175         return false;
176     }
177 
178     /**
179      * Returns true if application asked that this notification be part of a group.
180      */
isAppGroup()181     public boolean isAppGroup() {
182         if (getNotification().getGroup() != null || getNotification().getSortKey() != null) {
183             return true;
184         }
185         return false;
186     }
187 
writeToParcel(Parcel out, int flags)188     public void writeToParcel(Parcel out, int flags) {
189         out.writeString(this.pkg);
190         out.writeString(this.opPkg);
191         out.writeInt(this.id);
192         if (this.tag != null) {
193             out.writeInt(1);
194             out.writeString(this.tag);
195         } else {
196             out.writeInt(0);
197         }
198         out.writeInt(this.uid);
199         out.writeInt(this.initialPid);
200         this.notification.writeToParcel(out, flags);
201         user.writeToParcel(out, flags);
202         out.writeLong(this.postTime);
203         if (this.overrideGroupKey != null) {
204             out.writeInt(1);
205             out.writeString(this.overrideGroupKey);
206         } else {
207             out.writeInt(0);
208         }
209         if (this.mInstanceId != null) {
210             out.writeInt(1);
211             mInstanceId.writeToParcel(out, flags);
212         } else {
213             out.writeInt(0);
214         }
215     }
216 
describeContents()217     public int describeContents() {
218         return 0;
219     }
220 
221     public static final @android.annotation.NonNull
222             Parcelable.Creator<StatusBarNotification> CREATOR =
223             new Parcelable.Creator<StatusBarNotification>() {
224                 public StatusBarNotification createFromParcel(Parcel parcel) {
225                     return new StatusBarNotification(parcel);
226                 }
227 
228             public StatusBarNotification[] newArray(int size) {
229                 return new StatusBarNotification[size];
230             }
231     };
232 
233     /**
234      * @hide
235      */
cloneLight()236     public StatusBarNotification cloneLight() {
237         final Notification no = new Notification();
238         this.notification.cloneInto(no, false); // light copy
239         return cloneShallow(no);
240     }
241 
242     @Override
clone()243     public StatusBarNotification clone() {
244         return cloneShallow(this.notification.clone());
245     }
246 
247     /**
248      * @param notification Some kind of clone of this.notification.
249      * @return A shallow copy of self, with notification in place of this.notification.
250      */
cloneShallow(Notification notification)251     StatusBarNotification cloneShallow(Notification notification) {
252         StatusBarNotification result = new StatusBarNotification(this.pkg, this.opPkg,
253                 this.id, this.tag, this.uid, this.initialPid,
254                 notification, this.user, this.overrideGroupKey, this.postTime);
255         result.setInstanceId(this.mInstanceId);
256         return result;
257     }
258 
259     @Override
toString()260     public String toString() {
261         return String.format(
262                 "StatusBarNotification(pkg=%s user=%s id=%d tag=%s key=%s: %s)",
263                 this.pkg, this.user, this.id, this.tag,
264                 this.key, this.notification);
265     }
266 
267     /**
268      * Convenience method to check the notification's flags for
269      * {@link Notification#FLAG_ONGOING_EVENT}.
270      */
isOngoing()271     public boolean isOngoing() {
272         return (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0;
273     }
274 
275     /**
276      * Convenience method to check the notification's flags for
277      * either {@link Notification#FLAG_ONGOING_EVENT} or
278      * {@link Notification#FLAG_NO_CLEAR}.
279      */
isClearable()280     public boolean isClearable() {
281         return ((notification.flags & Notification.FLAG_ONGOING_EVENT) == 0)
282                 && ((notification.flags & Notification.FLAG_NO_CLEAR) == 0);
283     }
284 
285     /**
286      * Returns a userid for whom this notification is intended.
287      *
288      * @deprecated Use {@link #getUser()} instead.
289      */
290     @Deprecated
getUserId()291     public int getUserId() {
292         return this.user.getIdentifier();
293     }
294 
295     /**
296      * Like {@link #getUserId()} but handles special users.
297      * @hide
298      */
getNormalizedUserId()299     public int getNormalizedUserId() {
300         int userId = getUserId();
301         if (userId == UserHandle.USER_ALL) {
302             userId = UserHandle.USER_SYSTEM;
303         }
304         return userId;
305     }
306 
307     /** The package that the notification belongs to. */
getPackageName()308     public String getPackageName() {
309         return pkg;
310     }
311 
312     /** The id supplied to {@link android.app.NotificationManager#notify(int, Notification)}. */
getId()313     public int getId() {
314         return id;
315     }
316 
317     /**
318      * The tag supplied to {@link android.app.NotificationManager#notify(int, Notification)},
319      * or null if no tag was specified.
320      */
getTag()321     public String getTag() {
322         return tag;
323     }
324 
325     /**
326      * The notifying app's ({@link #getPackageName()}'s) uid.
327      */
getUid()328     public int getUid() {
329         return uid;
330     }
331 
332     /**
333      * The package that posted the notification.
334      * <p> Might be different from {@link #getPackageName()} if the app owning the notification has
335      * a {@link NotificationManager#setNotificationDelegate(String) notification delegate}.
336      */
getOpPkg()337     public @NonNull String getOpPkg() {
338         return opPkg;
339     }
340 
341     /** @hide */
342     @UnsupportedAppUsage
getInitialPid()343     public int getInitialPid() {
344         return initialPid;
345     }
346 
347     /**
348      * The {@link android.app.Notification} supplied to
349      * {@link android.app.NotificationManager#notify(int, Notification)}.
350      */
getNotification()351     public Notification getNotification() {
352         return notification;
353     }
354 
355     /**
356      * The {@link android.os.UserHandle} for whom this notification is intended.
357      */
getUser()358     public UserHandle getUser() {
359         return user;
360     }
361 
362     /**
363      * The time (in {@link System#currentTimeMillis} time) the notification was posted,
364      * which may be different than {@link android.app.Notification#when}.
365      */
getPostTime()366     public long getPostTime() {
367         return postTime;
368     }
369 
370     /**
371      * A unique instance key for this notification record.
372      */
getKey()373     public String getKey() {
374         return key;
375     }
376 
377     /**
378      * A key that indicates the group with which this message ranks.
379      */
getGroupKey()380     public String getGroupKey() {
381         return groupKey;
382     }
383 
384     /**
385      * The ID passed to setGroup(), or the override, or null.
386      *
387      * @hide
388      */
getGroup()389     public String getGroup() {
390         if (overrideGroupKey != null) {
391             return overrideGroupKey;
392         }
393         return getNotification().getGroup();
394     }
395 
396     /**
397      * Sets the override group key.
398      */
setOverrideGroupKey(String overrideGroupKey)399     public void setOverrideGroupKey(String overrideGroupKey) {
400         this.overrideGroupKey = overrideGroupKey;
401         groupKey = groupKey();
402     }
403 
404     /**
405      * Returns the override group key.
406      */
getOverrideGroupKey()407     public String getOverrideGroupKey() {
408         return overrideGroupKey;
409     }
410 
411     /**
412      * @hide
413      */
clearPackageContext()414     public void clearPackageContext() {
415         mContext = null;
416     }
417 
418     /**
419      * @hide
420      */
getInstanceId()421     public InstanceId getInstanceId() {
422         return mInstanceId;
423     }
424 
425     /**
426      * @hide
427      */
setInstanceId(InstanceId instanceId)428     public void setInstanceId(InstanceId instanceId) {
429         mInstanceId = instanceId;
430     }
431 
432     /**
433      * @hide
434      */
435     @UnsupportedAppUsage
getPackageContext(Context context)436     public Context getPackageContext(Context context) {
437         if (mContext == null) {
438             try {
439                 ApplicationInfo ai = context.getPackageManager()
440                         .getApplicationInfoAsUser(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES,
441                                 getUserId());
442                 mContext = context.createApplicationContext(ai,
443                         Context.CONTEXT_RESTRICTED);
444             } catch (PackageManager.NameNotFoundException e) {
445                 mContext = null;
446             }
447         }
448         if (mContext == null) {
449             mContext = context;
450         }
451         return mContext;
452     }
453 
454     /**
455      * Returns a LogMaker that contains all basic information of the notification.
456      *
457      * @hide
458      */
getLogMaker()459     public LogMaker getLogMaker() {
460         LogMaker logMaker = new LogMaker(MetricsEvent.VIEW_UNKNOWN).setPackageName(getPackageName())
461                 .addTaggedData(MetricsEvent.NOTIFICATION_ID, getId())
462                 .addTaggedData(MetricsEvent.NOTIFICATION_TAG, getTag())
463                 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID, getChannelIdLogTag())
464                 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_GROUP_ID, getGroupLogTag())
465                 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_GROUP_SUMMARY,
466                         getNotification().isGroupSummary() ? 1 : 0)
467                 .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CATEGORY,
468                         getNotification().category);
469         if (getNotification().extras != null) {
470             // Log the style used, if present.  We only log the hash here, as notification log
471             // events are frequent, while there are few styles (hence low chance of collisions).
472             String template = getNotification().extras.getString(Notification.EXTRA_TEMPLATE);
473             if (template != null && !template.isEmpty()) {
474                 logMaker.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_STYLE,
475                         template.hashCode());
476             }
477             ArrayList<Person> people = getNotification().extras.getParcelableArrayList(
478                     Notification.EXTRA_PEOPLE_LIST);
479             if (people != null && !people.isEmpty()) {
480                 logMaker.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_PEOPLE, people.size());
481             }
482         }
483         return logMaker;
484     }
485 
486     /**
487      * @hide
488      */
getShortcutId()489     public String getShortcutId() {
490         return getNotification().getShortcutId();
491     }
492 
493     /**
494      *  Returns a probably-unique string based on the notification's group name,
495      *  with no more than MAX_LOG_TAG_LENGTH characters.
496      * @return String based on group name of notification.
497      * @hide
498      */
getGroupLogTag()499     public String getGroupLogTag() {
500         return shortenTag(getGroup());
501     }
502 
503     /**
504      *  Returns a probably-unique string based on the notification's channel ID,
505      *  with no more than MAX_LOG_TAG_LENGTH characters.
506      * @return String based on channel ID of notification.
507      * @hide
508      */
getChannelIdLogTag()509     public String getChannelIdLogTag() {
510         if (notification.getChannelId() == null) {
511             return null;
512         }
513         return shortenTag(notification.getChannelId());
514     }
515 
516     // Make logTag with max size MAX_LOG_TAG_LENGTH.
517     // For shorter or equal tags, returns the tag.
518     // For longer tags, truncate the tag and append a hash of the full tag to
519     // fill the maximum size.
shortenTag(String logTag)520     private String shortenTag(String logTag) {
521         if (logTag == null || logTag.length() <= MAX_LOG_TAG_LENGTH) {
522             return logTag;
523         }
524         String hash = Integer.toHexString(logTag.hashCode());
525         return logTag.substring(0, MAX_LOG_TAG_LENGTH - hash.length() - 1) + "-"
526                 + hash;
527     }
528 }
529