/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.service.notification; import static android.text.TextUtils.formatSimple; import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationManager; import android.app.Person; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.metrics.LogMaker; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; import com.android.internal.logging.InstanceId; import com.android.internal.logging.nano.MetricsProto; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.util.ArrayList; /** * Class encapsulating a Notification. Sent by the NotificationManagerService to clients including * the status bar and any {@link android.service.notification.NotificationListenerService}s. */ public class StatusBarNotification implements Parcelable { static final int MAX_LOG_TAG_LENGTH = 36; @UnsupportedAppUsage private final String pkg; @UnsupportedAppUsage private final int id; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private final String tag; private final String key; private String groupKey; private String overrideGroupKey; @UnsupportedAppUsage private final int uid; private final String opPkg; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private final int initialPid; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private final Notification notification; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private final UserHandle user; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private final long postTime; // A small per-notification ID, used for statsd logging. private InstanceId mInstanceId; // Not final, see setInstanceId() private Context mContext; // used for inflation & icon expansion /** @hide */ public StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid, int initialPid, Notification notification, UserHandle user, String overrideGroupKey, long postTime) { if (pkg == null) throw new NullPointerException(); if (notification == null) throw new NullPointerException(); this.pkg = pkg; this.opPkg = opPkg; this.id = id; this.tag = tag; this.uid = uid; this.initialPid = initialPid; this.notification = notification; this.user = user; this.postTime = postTime; this.overrideGroupKey = overrideGroupKey; this.key = key(); this.groupKey = groupKey(); } /** * @deprecated Non-system apps should not need to create StatusBarNotifications. */ @Deprecated public StatusBarNotification(String pkg, String opPkg, int id, String tag, int uid, int initialPid, int score, Notification notification, UserHandle user, long postTime) { if (pkg == null) throw new NullPointerException(); if (notification == null) throw new NullPointerException(); this.pkg = pkg; this.opPkg = opPkg; this.id = id; this.tag = tag; this.uid = uid; this.initialPid = initialPid; this.notification = notification; this.user = user; this.postTime = postTime; this.key = key(); this.groupKey = groupKey(); } public StatusBarNotification(Parcel in) { this.pkg = in.readString(); this.opPkg = in.readString(); this.id = in.readInt(); if (in.readInt() != 0) { this.tag = in.readString(); } else { this.tag = null; } this.uid = in.readInt(); this.initialPid = in.readInt(); this.notification = new Notification(in); this.user = UserHandle.readFromParcel(in); this.postTime = in.readLong(); if (in.readInt() != 0) { this.overrideGroupKey = in.readString(); } if (in.readInt() != 0) { this.mInstanceId = InstanceId.CREATOR.createFromParcel(in); } this.key = key(); this.groupKey = groupKey(); } /** * @hide */ public static int getUidFromKey(@NonNull String key) { String[] parts = key.split("\\|"); if (parts.length >= 5) { try { int uid = Integer.parseInt(parts[4]); return uid; } catch (NumberFormatException e) { return -1; } } return -1; } /** * @hide */ public static String getPkgFromKey(@NonNull String key) { String[] parts = key.split("\\|"); if (parts.length >= 2) { return parts[1]; } return null; } private String key() { String sbnKey = user.getIdentifier() + "|" + pkg + "|" + id + "|" + tag + "|" + uid; if (overrideGroupKey != null && getNotification().isGroupSummary()) { sbnKey = sbnKey + "|" + overrideGroupKey; } return sbnKey; } private String groupKey() { if (overrideGroupKey != null) { return user.getIdentifier() + "|" + pkg + "|" + "g:" + overrideGroupKey; } final String group = getNotification().getGroup(); final String sortKey = getNotification().getSortKey(); if (group == null && sortKey == null) { // a group of one return key; } return user.getIdentifier() + "|" + pkg + "|" + (group == null ? "c:" + notification.getChannelId() : "g:" + group); } /** * Returns true if this notification is part of a group. */ public boolean isGroup() { if (overrideGroupKey != null || isAppGroup()) { return true; } return false; } /** * Returns true if application asked that this notification be part of a group. */ public boolean isAppGroup() { if (getNotification().getGroup() != null || getNotification().getSortKey() != null) { return true; } return false; } public void writeToParcel(Parcel out, int flags) { out.writeString(this.pkg); out.writeString(this.opPkg); out.writeInt(this.id); if (this.tag != null) { out.writeInt(1); out.writeString(this.tag); } else { out.writeInt(0); } out.writeInt(this.uid); out.writeInt(this.initialPid); this.notification.writeToParcel(out, flags); user.writeToParcel(out, flags); out.writeLong(this.postTime); if (this.overrideGroupKey != null) { out.writeInt(1); out.writeString(this.overrideGroupKey); } else { out.writeInt(0); } if (this.mInstanceId != null) { out.writeInt(1); mInstanceId.writeToParcel(out, flags); } else { out.writeInt(0); } } public int describeContents() { return 0; } public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { public StatusBarNotification createFromParcel(Parcel parcel) { return new StatusBarNotification(parcel); } public StatusBarNotification[] newArray(int size) { return new StatusBarNotification[size]; } }; /** * @hide */ public StatusBarNotification cloneLight() { final Notification no = new Notification(); this.notification.cloneInto(no, false); // light copy return cloneShallow(no); } @Override public StatusBarNotification clone() { return cloneShallow(this.notification.clone()); } /** * @param notification Some kind of clone of this.notification. * @return A shallow copy of self, with notification in place of this.notification. * * @hide */ public StatusBarNotification cloneShallow(Notification notification) { StatusBarNotification result = new StatusBarNotification(this.pkg, this.opPkg, this.id, this.tag, this.uid, this.initialPid, notification, this.user, this.overrideGroupKey, this.postTime); result.setInstanceId(this.mInstanceId); return result; } @Override public String toString() { return formatSimple( "StatusBarNotification(pkg=%s user=%s id=%d tag=%s key=%s: %s)", this.pkg, this.user, this.id, this.tag, this.key, this.notification); } /** * Convenience method to check the notification's flags for * {@link Notification#FLAG_ONGOING_EVENT}. */ public boolean isOngoing() { return (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0; } /** * @hide * * Convenience method to check the notification's flags for * {@link Notification#FLAG_NO_DISMISS}. */ public boolean isNonDismissable() { return (notification.flags & Notification.FLAG_NO_DISMISS) != 0; } /** * Convenience method to check the notification's flags for * either {@link Notification#FLAG_ONGOING_EVENT} or * {@link Notification#FLAG_NO_CLEAR}. */ public boolean isClearable() { return ((notification.flags & Notification.FLAG_ONGOING_EVENT) == 0) && ((notification.flags & Notification.FLAG_NO_CLEAR) == 0); } /** * Returns a userid for whom this notification is intended. * * @deprecated Use {@link #getUser()} instead. */ @Deprecated public int getUserId() { return this.user.getIdentifier(); } /** * Like {@link #getUserId()} but handles special users. * @hide */ public int getNormalizedUserId() { int userId = getUserId(); if (userId == UserHandle.USER_ALL) { userId = UserHandle.USER_SYSTEM; } return userId; } /** The package that the notification belongs to. */ public String getPackageName() { return pkg; } /** The id supplied to {@link android.app.NotificationManager#notify(int, Notification)}. */ public int getId() { return id; } /** * The tag supplied to {@link android.app.NotificationManager#notify(int, Notification)}, * or null if no tag was specified. */ public String getTag() { return tag; } /** * The notifying app's ({@link #getPackageName()}'s) uid. */ public int getUid() { return uid; } /** * The package that posted the notification. *

Might be different from {@link #getPackageName()} if the app owning the notification has * a {@link NotificationManager#setNotificationDelegate(String) notification delegate}. */ public @NonNull String getOpPkg() { return opPkg; } /** @hide */ @UnsupportedAppUsage public int getInitialPid() { return initialPid; } /** * The {@link android.app.Notification} supplied to * {@link android.app.NotificationManager#notify(int, Notification)}. */ public Notification getNotification() { return notification; } /** * The {@link android.os.UserHandle} for whom this notification is intended. */ public UserHandle getUser() { return user; } /** * The time (in {@link System#currentTimeMillis} time) the notification was posted, * which may be different than {@link android.app.Notification#when}. */ public long getPostTime() { return postTime; } /** * A unique instance key for this notification record. */ public String getKey() { return key; } /** * A key that indicates the group with which this message ranks. */ public String getGroupKey() { return groupKey; } /** * The ID passed to setGroup(), or the override, or null. * * @hide */ public String getGroup() { if (overrideGroupKey != null) { return overrideGroupKey; } return getNotification().getGroup(); } /** * Sets the override group key. */ public void setOverrideGroupKey(String overrideGroupKey) { this.overrideGroupKey = overrideGroupKey; groupKey = groupKey(); } /** * Returns the override group key. */ public String getOverrideGroupKey() { return overrideGroupKey; } /** * @hide */ public void clearPackageContext() { mContext = null; } /** * @hide */ public InstanceId getInstanceId() { return mInstanceId; } /** * @hide */ public void setInstanceId(InstanceId instanceId) { mInstanceId = instanceId; } /** * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public Context getPackageContext(Context context) { if (mContext == null) { try { ApplicationInfo ai = context.getPackageManager() .getApplicationInfoAsUser(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES, getNormalizedUserId()); mContext = context.createApplicationContext(ai, Context.CONTEXT_RESTRICTED); } catch (PackageManager.NameNotFoundException e) { mContext = null; } } if (mContext == null) { mContext = context; } return mContext; } /** * Returns a LogMaker that contains all basic information of the notification. * * @hide */ public LogMaker getLogMaker() { LogMaker logMaker = new LogMaker(MetricsEvent.VIEW_UNKNOWN).setPackageName(getPackageName()) .addTaggedData(MetricsEvent.NOTIFICATION_ID, getId()) .addTaggedData(MetricsEvent.NOTIFICATION_TAG, getTag()) .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID, getChannelIdLogTag()) .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_GROUP_ID, getGroupLogTag()) .addTaggedData(MetricsEvent.FIELD_NOTIFICATION_GROUP_SUMMARY, getNotification().isGroupSummary() ? 1 : 0) .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CATEGORY, getNotification().category); if (getNotification().extras != null) { // Log the style used, if present. We only log the hash here, as notification log // events are frequent, while there are few styles (hence low chance of collisions). String template = getNotification().extras.getString(Notification.EXTRA_TEMPLATE); if (template != null && !template.isEmpty()) { logMaker.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_STYLE, template.hashCode()); } ArrayList people = getNotification().extras.getParcelableArrayList( Notification.EXTRA_PEOPLE_LIST, android.app.Person.class); if (people != null && !people.isEmpty()) { logMaker.addTaggedData(MetricsEvent.FIELD_NOTIFICATION_PEOPLE, people.size()); } } return logMaker; } /** * @hide */ public String getShortcutId() { return getNotification().getShortcutId(); } /** * Returns a probably-unique string based on the notification's group name, * with no more than MAX_LOG_TAG_LENGTH characters. * @return String based on group name of notification. * @hide */ public String getGroupLogTag() { return shortenTag(getGroup()); } /** * Returns a probably-unique string based on the notification's channel ID, * with no more than MAX_LOG_TAG_LENGTH characters. * @return String based on channel ID of notification. * @hide */ public String getChannelIdLogTag() { if (notification.getChannelId() == null) { return null; } return shortenTag(notification.getChannelId()); } // Make logTag with max size MAX_LOG_TAG_LENGTH. // For shorter or equal tags, returns the tag. // For longer tags, truncate the tag and append a hash of the full tag to // fill the maximum size. private String shortenTag(String logTag) { if (logTag == null || logTag.length() <= MAX_LOG_TAG_LENGTH) { return logTag; } String hash = Integer.toHexString(logTag.hashCode()); return logTag.substring(0, MAX_LOG_TAG_LENGTH - hash.length() - 1) + "-" + hash; } }