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      */
cloneShallow(Notification notification)276     StatusBarNotification cloneShallow(Notification notification) {
277         StatusBarNotification result = new StatusBarNotification(this.pkg, this.opPkg,
278                 this.id, this.tag, this.uid, this.initialPid,
279                 notification, this.user, this.overrideGroupKey, this.postTime);
280         result.setInstanceId(this.mInstanceId);
281         return result;
282     }
283 
284     @Override
toString()285     public String toString() {
286         return formatSimple(
287                 "StatusBarNotification(pkg=%s user=%s id=%d tag=%s key=%s: %s)",
288                 this.pkg, this.user, this.id, this.tag,
289                 this.key, this.notification);
290     }
291 
292     /**
293      * Convenience method to check the notification's flags for
294      * {@link Notification#FLAG_ONGOING_EVENT}.
295      */
isOngoing()296     public boolean isOngoing() {
297         return (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0;
298     }
299 
300     /**
301      * @hide
302      *
303      * Convenience method to check the notification's flags for
304      * {@link Notification#FLAG_NO_DISMISS}.
305      */
isNonDismissable()306     public boolean isNonDismissable() {
307         return (notification.flags & Notification.FLAG_NO_DISMISS) != 0;
308     }
309 
310     /**
311      * Convenience method to check the notification's flags for
312      * either {@link Notification#FLAG_ONGOING_EVENT} or
313      * {@link Notification#FLAG_NO_CLEAR}.
314      */
isClearable()315     public boolean isClearable() {
316         return ((notification.flags & Notification.FLAG_ONGOING_EVENT) == 0)
317                 && ((notification.flags & Notification.FLAG_NO_CLEAR) == 0);
318     }
319 
320     /**
321      * Returns a userid for whom this notification is intended.
322      *
323      * @deprecated Use {@link #getUser()} instead.
324      */
325     @Deprecated
getUserId()326     public int getUserId() {
327         return this.user.getIdentifier();
328     }
329 
330     /**
331      * Like {@link #getUserId()} but handles special users.
332      * @hide
333      */
getNormalizedUserId()334     public int getNormalizedUserId() {
335         int userId = getUserId();
336         if (userId == UserHandle.USER_ALL) {
337             userId = UserHandle.USER_SYSTEM;
338         }
339         return userId;
340     }
341 
342     /** The package that the notification belongs to. */
getPackageName()343     public String getPackageName() {
344         return pkg;
345     }
346 
347     /** The id supplied to {@link android.app.NotificationManager#notify(int, Notification)}. */
getId()348     public int getId() {
349         return id;
350     }
351 
352     /**
353      * The tag supplied to {@link android.app.NotificationManager#notify(int, Notification)},
354      * or null if no tag was specified.
355      */
getTag()356     public String getTag() {
357         return tag;
358     }
359 
360     /**
361      * The notifying app's ({@link #getPackageName()}'s) uid.
362      */
getUid()363     public int getUid() {
364         return uid;
365     }
366 
367     /**
368      * The package that posted the notification.
369      * <p> Might be different from {@link #getPackageName()} if the app owning the notification has
370      * a {@link NotificationManager#setNotificationDelegate(String) notification delegate}.
371      */
getOpPkg()372     public @NonNull String getOpPkg() {
373         return opPkg;
374     }
375 
376     /** @hide */
377     @UnsupportedAppUsage
getInitialPid()378     public int getInitialPid() {
379         return initialPid;
380     }
381 
382     /**
383      * The {@link android.app.Notification} supplied to
384      * {@link android.app.NotificationManager#notify(int, Notification)}.
385      */
getNotification()386     public Notification getNotification() {
387         return notification;
388     }
389 
390     /**
391      * The {@link android.os.UserHandle} for whom this notification is intended.
392      */
getUser()393     public UserHandle getUser() {
394         return user;
395     }
396 
397     /**
398      * The time (in {@link System#currentTimeMillis} time) the notification was posted,
399      * which may be different than {@link android.app.Notification#when}.
400      */
getPostTime()401     public long getPostTime() {
402         return postTime;
403     }
404 
405     /**
406      * A unique instance key for this notification record.
407      */
getKey()408     public String getKey() {
409         return key;
410     }
411 
412     /**
413      * A key that indicates the group with which this message ranks.
414      */
getGroupKey()415     public String getGroupKey() {
416         return groupKey;
417     }
418 
419     /**
420      * The ID passed to setGroup(), or the override, or null.
421      *
422      * @hide
423      */
getGroup()424     public String getGroup() {
425         if (overrideGroupKey != null) {
426             return overrideGroupKey;
427         }
428         return getNotification().getGroup();
429     }
430 
431     /**
432      * Sets the override group key.
433      */
setOverrideGroupKey(String overrideGroupKey)434     public void setOverrideGroupKey(String overrideGroupKey) {
435         this.overrideGroupKey = overrideGroupKey;
436         groupKey = groupKey();
437     }
438 
439     /**
440      * Returns the override group key.
441      */
getOverrideGroupKey()442     public String getOverrideGroupKey() {
443         return overrideGroupKey;
444     }
445 
446     /**
447      * @hide
448      */
clearPackageContext()449     public void clearPackageContext() {
450         mContext = null;
451     }
452 
453     /**
454      * @hide
455      */
getInstanceId()456     public InstanceId getInstanceId() {
457         return mInstanceId;
458     }
459 
460     /**
461      * @hide
462      */
setInstanceId(InstanceId instanceId)463     public void setInstanceId(InstanceId instanceId) {
464         mInstanceId = instanceId;
465     }
466 
467     /**
468      * @hide
469      */
470     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getPackageContext(Context context)471     public Context getPackageContext(Context context) {
472         if (mContext == null) {
473             try {
474                 ApplicationInfo ai = context.getPackageManager()
475                         .getApplicationInfoAsUser(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES,
476                                 getNormalizedUserId());
477                 mContext = context.createApplicationContext(ai,
478                         Context.CONTEXT_RESTRICTED);
479             } catch (PackageManager.NameNotFoundException e) {
480                 mContext = null;
481             }
482         }
483         if (mContext == null) {
484             mContext = context;
485         }
486         return mContext;
487     }
488 
489     /**
490      * Returns a LogMaker that contains all basic information of the notification.
491      *
492      * @hide
493      */
getLogMaker()494     public LogMaker getLogMaker() {
495         LogMaker logMaker = new LogMaker(MetricsEvent.VIEW_UNKNOWN).setPackageName(getPackageName())
496                 .addTaggedData(MetricsEvent.NOTIFICATION_ID, getId())
497                 .addTaggedData(MetricsEvent.NOTIFICATION_TAG, getTag())
498                 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID, getChannelIdLogTag())
499                 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_GROUP_ID, getGroupLogTag())
500                 .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_GROUP_SUMMARY,
501                         getNotification().isGroupSummary() ? 1 : 0)
502                 .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CATEGORY,
503                         getNotification().category);
504         if (getNotification().extras != null) {
505             // Log the style used, if present.  We only log the hash here, as notification log
506             // events are frequent, while there are few styles (hence low chance of collisions).
507             String template = getNotification().extras.getString(Notification.EXTRA_TEMPLATE);
508             if (template != null && !template.isEmpty()) {
509                 logMaker.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_STYLE,
510                         template.hashCode());
511             }
512             ArrayList<Person> people = getNotification().extras.getParcelableArrayList(
513                     Notification.EXTRA_PEOPLE_LIST, android.app.Person.class);
514             if (people != null && !people.isEmpty()) {
515                 logMaker.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_PEOPLE, people.size());
516             }
517         }
518         return logMaker;
519     }
520 
521     /**
522      * @hide
523      */
getShortcutId()524     public String getShortcutId() {
525         return getNotification().getShortcutId();
526     }
527 
528     /**
529      *  Returns a probably-unique string based on the notification's group name,
530      *  with no more than MAX_LOG_TAG_LENGTH characters.
531      * @return String based on group name of notification.
532      * @hide
533      */
getGroupLogTag()534     public String getGroupLogTag() {
535         return shortenTag(getGroup());
536     }
537 
538     /**
539      *  Returns a probably-unique string based on the notification's channel ID,
540      *  with no more than MAX_LOG_TAG_LENGTH characters.
541      * @return String based on channel ID of notification.
542      * @hide
543      */
getChannelIdLogTag()544     public String getChannelIdLogTag() {
545         if (notification.getChannelId() == null) {
546             return null;
547         }
548         return shortenTag(notification.getChannelId());
549     }
550 
551     // Make logTag with max size MAX_LOG_TAG_LENGTH.
552     // For shorter or equal tags, returns the tag.
553     // For longer tags, truncate the tag and append a hash of the full tag to
554     // fill the maximum size.
shortenTag(String logTag)555     private String shortenTag(String logTag) {
556         if (logTag == null || logTag.length() <= MAX_LOG_TAG_LENGTH) {
557             return logTag;
558         }
559         String hash = Integer.toHexString(logTag.hashCode());
560         return logTag.substring(0, MAX_LOG_TAG_LENGTH - hash.length() - 1) + "-"
561                 + hash;
562     }
563 }
564