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