Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bac3f7da authored by Yuri Lin's avatar Yuri Lin
Browse files

Log a notification update when fields such as importance or ranking score are updated.

This update happens in the function performing the actual sort because while adjustments are queued, the actual updates to fields in the NotificationRecord don't happen until sorting time. To avoid spurious extra logging, we only log an update to notifications that were updated via assistant adjustments (by tracking those keys in a set).

Note that there are two ways adjustments can be applied to notifications: synchronously to an enqueued notification or asynchronously (prompting the sort). Given that enqueued notification (posts or updates) are logged once the post or update goes through, this change only logs the updates to the ones updated asynchronously. (Otherwise, we may see a notification "update" before the notification is ever even visible to the user.)

Bug: 195579280
Test: manual via statsd_testdrive, atest NotificationManagerServiceTest
Change-Id: I97ef8865e31653f0fee0ac943251fecc21e5e05f
Merged-In: I97ef8865e31653f0fee0ac943251fecc21e5e05f
parent b53041d1
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -92,6 +92,8 @@ option java_package com.android.server
27533 notification_autogrouped (key|3)
# notification was removed from an autogroup
275534 notification_unautogrouped (key|3)
# when a notification is adjusted via assistant
27535 notification_adjusted (key|3),(adjustment_type|3),(new_value|3)

# ---------------------------
# Watchdog.java
+133 −45
Original line number Diff line number Diff line
@@ -5397,6 +5397,7 @@ public class NotificationManagerService extends SystemService {
                                    == IMPORTANCE_NONE) {
                                cancelNotificationsFromListener(token, new String[]{r.getKey()});
                            } else {
                                r.setPendingLogUpdate(true);
                                needsSort = true;
                            }
                        }
@@ -8057,64 +8058,151 @@ public class NotificationManagerService extends SystemService {
        }
    }

    static class NotificationRecordExtractorData {
        // Class that stores any field in a NotificationRecord that can change via an extractor.
        // Used to cache previous data used in a sort.
        int mPosition;
        int mVisibility;
        boolean mShowBadge;
        boolean mAllowBubble;
        boolean mIsBubble;
        NotificationChannel mChannel;
        String mGroupKey;
        ArrayList<String> mOverridePeople;
        ArrayList<SnoozeCriterion> mSnoozeCriteria;
        Integer mUserSentiment;
        Integer mSuppressVisually;
        ArrayList<Notification.Action> mSystemSmartActions;
        ArrayList<CharSequence> mSmartReplies;
        int mImportance;

        // These fields may not trigger a reranking but diffs here may be logged.
        float mRankingScore;
        boolean mIsConversation;

        NotificationRecordExtractorData(int position, int visibility, boolean showBadge,
                boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey,
                ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria,
                Integer userSentiment, Integer suppressVisually,
                ArrayList<Notification.Action> systemSmartActions,
                ArrayList<CharSequence> smartReplies, int importance, float rankingScore,
                boolean isConversation) {
            mPosition = position;
            mVisibility = visibility;
            mShowBadge = showBadge;
            mAllowBubble = allowBubble;
            mIsBubble = isBubble;
            mChannel = channel;
            mGroupKey = groupKey;
            mOverridePeople = overridePeople;
            mSnoozeCriteria = snoozeCriteria;
            mUserSentiment = userSentiment;
            mSuppressVisually = suppressVisually;
            mSystemSmartActions = systemSmartActions;
            mSmartReplies = smartReplies;
            mImportance = importance;
            mRankingScore = rankingScore;
            mIsConversation = isConversation;
        }

        // Returns whether the provided NotificationRecord differs from the cached data in any way.
        // Should be guarded by mNotificationLock; not annotated here as this class is static.
        boolean hasDiffForRankingLocked(NotificationRecord r, int newPosition) {
            return mPosition != newPosition
                    || mVisibility != r.getPackageVisibilityOverride()
                    || mShowBadge != r.canShowBadge()
                    || mAllowBubble != r.canBubble()
                    || mIsBubble != r.getNotification().isBubbleNotification()
                    || !Objects.equals(mChannel, r.getChannel())
                    || !Objects.equals(mGroupKey, r.getGroupKey())
                    || !Objects.equals(mOverridePeople, r.getPeopleOverride())
                    || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
                    || !Objects.equals(mUserSentiment, r.getUserSentiment())
                    || !Objects.equals(mSuppressVisually, r.getSuppressedVisualEffects())
                    || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
                    || !Objects.equals(mSmartReplies, r.getSmartReplies())
                    || mImportance != r.getImportance();
        }

        // Returns whether the NotificationRecord has a change from this data for which we should
        // log an update. This method specifically targets fields that may be changed via
        // adjustments from the assistant.
        //
        // Fields here are the union of things in NotificationRecordLogger.shouldLogReported
        // and NotificationRecord.applyAdjustments.
        //
        // Should be guarded by mNotificationLock; not annotated here as this class is static.
        boolean hasDiffForLoggingLocked(NotificationRecord r, int newPosition) {
            return mPosition != newPosition
                    || !Objects.equals(mChannel, r.getChannel())
                    || !Objects.equals(mGroupKey, r.getGroupKey())
                    || !Objects.equals(mOverridePeople, r.getPeopleOverride())
                    || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria())
                    || !Objects.equals(mUserSentiment, r.getUserSentiment())
                    || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions())
                    || !Objects.equals(mSmartReplies, r.getSmartReplies())
                    || mImportance != r.getImportance()
                    || !r.rankingScoreMatches(mRankingScore)
                    || mIsConversation != r.isConversation();
        }
    }

    void handleRankingSort() {
        if (mRankingHelper == null) return;
        synchronized (mNotificationLock) {
            final int N = mNotificationList.size();
            // Any field that can change via one of the extractors needs to be added here.
            ArrayList<String> orderBefore = new ArrayList<>(N);
            int[] visibilities = new int[N];
            boolean[] showBadges = new boolean[N];
            boolean[] allowBubbles = new boolean[N];
            boolean[] isBubble = new boolean[N];
            ArrayList<NotificationChannel> channelBefore = new ArrayList<>(N);
            ArrayList<String> groupKeyBefore = new ArrayList<>(N);
            ArrayList<ArrayList<String>> overridePeopleBefore = new ArrayList<>(N);
            ArrayList<ArrayList<SnoozeCriterion>> snoozeCriteriaBefore = new ArrayList<>(N);
            ArrayList<Integer> userSentimentBefore = new ArrayList<>(N);
            ArrayList<Integer> suppressVisuallyBefore = new ArrayList<>(N);
            ArrayList<ArrayList<Notification.Action>> systemSmartActionsBefore = new ArrayList<>(N);
            ArrayList<ArrayList<CharSequence>> smartRepliesBefore = new ArrayList<>(N);
            int[] importancesBefore = new int[N];
            ArrayMap<String, NotificationRecordExtractorData> extractorDataBefore =
                    new ArrayMap<>(N);
            for (int i = 0; i < N; i++) {
                final NotificationRecord r = mNotificationList.get(i);
                orderBefore.add(r.getKey());
                visibilities[i] = r.getPackageVisibilityOverride();
                showBadges[i] = r.canShowBadge();
                allowBubbles[i] = r.canBubble();
                isBubble[i] = r.getNotification().isBubbleNotification();
                channelBefore.add(r.getChannel());
                groupKeyBefore.add(r.getGroupKey());
                overridePeopleBefore.add(r.getPeopleOverride());
                snoozeCriteriaBefore.add(r.getSnoozeCriteria());
                userSentimentBefore.add(r.getUserSentiment());
                suppressVisuallyBefore.add(r.getSuppressedVisualEffects());
                systemSmartActionsBefore.add(r.getSystemGeneratedSmartActions());
                smartRepliesBefore.add(r.getSmartReplies());
                importancesBefore[i] = r.getImportance();
                NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData(
                        i,
                        r.getPackageVisibilityOverride(),
                        r.canShowBadge(),
                        r.canBubble(),
                        r.getNotification().isBubbleNotification(),
                        r.getChannel(),
                        r.getGroupKey(),
                        r.getPeopleOverride(),
                        r.getSnoozeCriteria(),
                        r.getUserSentiment(),
                        r.getSuppressedVisualEffects(),
                        r.getSystemGeneratedSmartActions(),
                        r.getSmartReplies(),
                        r.getImportance(),
                        r.getRankingScore(),
                        r.isConversation());
                extractorDataBefore.put(r.getKey(), extractorData);
                mRankingHelper.extractSignals(r);
            }
            mRankingHelper.sort(mNotificationList);
            for (int i = 0; i < N; i++) {
                final NotificationRecord r = mNotificationList.get(i);
                if (!orderBefore.get(i).equals(r.getKey())
                        || visibilities[i] != r.getPackageVisibilityOverride()
                        || showBadges[i] != r.canShowBadge()
                        || allowBubbles[i] != r.canBubble()
                        || isBubble[i] != r.getNotification().isBubbleNotification()
                        || !Objects.equals(channelBefore.get(i), r.getChannel())
                        || !Objects.equals(groupKeyBefore.get(i), r.getGroupKey())
                        || !Objects.equals(overridePeopleBefore.get(i), r.getPeopleOverride())
                        || !Objects.equals(snoozeCriteriaBefore.get(i), r.getSnoozeCriteria())
                        || !Objects.equals(userSentimentBefore.get(i), r.getUserSentiment())
                        || !Objects.equals(suppressVisuallyBefore.get(i),
                        r.getSuppressedVisualEffects())
                        || !Objects.equals(systemSmartActionsBefore.get(i),
                                r.getSystemGeneratedSmartActions())
                        || !Objects.equals(smartRepliesBefore.get(i), r.getSmartReplies())
                        || importancesBefore[i] != r.getImportance()) {
                if (!extractorDataBefore.containsKey(r.getKey())) {
                    // This shouldn't happen given that we just built this with all the
                    // notifications, but check just to be safe.
                    continue;
                }
                if (extractorDataBefore.get(r.getKey()).hasDiffForRankingLocked(r, i)) {
                    mHandler.scheduleSendRankingUpdate();
                    return;
                }

                // If this notification is one for which we wanted to log an update, and
                // sufficient relevant bits are different, log update.
                if (r.hasPendingLogUpdate()) {
                    // We need to acquire the previous data associated with this specific
                    // notification, as the one at the current index may be unrelated if
                    // notification order has changed.
                    NotificationRecordExtractorData prevData = extractorDataBefore.get(r.getKey());
                    if (prevData.hasDiffForLoggingLocked(r, i)) {
                        mNotificationRecordLogger.logNotificationAdjusted(r, i, 0,
                                getGroupInstanceId(r.getSbn().getGroupKey()));
                    }

                    // Remove whether there was a diff or not; we've sorted the key, so if it
                    // turns out there was nothing to log, that's fine too.
                    r.setPendingLogUpdate(false);
                }
            }
        }
+43 −0
Original line number Diff line number Diff line
@@ -200,6 +200,10 @@ public final class NotificationRecord {
    private boolean mIsAppImportanceLocked;
    private ArraySet<Uri> mGrantableUris;

    // Whether this notification record should have an update logged the next time notifications
    // are sorted.
    private boolean mPendingLogUpdate = false;

    public NotificationRecord(Context context, StatusBarNotification sbn,
            NotificationChannel channel) {
        this.sbn = sbn;
@@ -648,17 +652,23 @@ public final class NotificationRecord {
                    final ArrayList<String> people =
                            adjustment.getSignals().getStringArrayList(Adjustment.KEY_PEOPLE);
                    setPeopleOverride(people);
                    EventLogTags.writeNotificationAdjusted(
                            getKey(), Adjustment.KEY_PEOPLE, people.toString());
                }
                if (signals.containsKey(Adjustment.KEY_SNOOZE_CRITERIA)) {
                    final ArrayList<SnoozeCriterion> snoozeCriterionList =
                            adjustment.getSignals().getParcelableArrayList(
                                    Adjustment.KEY_SNOOZE_CRITERIA);
                    setSnoozeCriteria(snoozeCriterionList);
                    EventLogTags.writeNotificationAdjusted(getKey(), Adjustment.KEY_SNOOZE_CRITERIA,
                            snoozeCriterionList.toString());
                }
                if (signals.containsKey(Adjustment.KEY_GROUP_KEY)) {
                    final String groupOverrideKey =
                            adjustment.getSignals().getString(Adjustment.KEY_GROUP_KEY);
                    setOverrideGroupKey(groupOverrideKey);
                    EventLogTags.writeNotificationAdjusted(getKey(), Adjustment.KEY_GROUP_KEY,
                            groupOverrideKey);
                }
                if (signals.containsKey(Adjustment.KEY_USER_SENTIMENT)) {
                    // Only allow user sentiment update from assistant if user hasn't already
@@ -667,27 +677,42 @@ public final class NotificationRecord {
                            && (getChannel().getUserLockedFields() & USER_LOCKED_IMPORTANCE) == 0) {
                        setUserSentiment(adjustment.getSignals().getInt(
                                Adjustment.KEY_USER_SENTIMENT, USER_SENTIMENT_NEUTRAL));
                        EventLogTags.writeNotificationAdjusted(getKey(),
                                Adjustment.KEY_USER_SENTIMENT,
                                Integer.toString(getUserSentiment()));
                    }
                }
                if (signals.containsKey(Adjustment.KEY_CONTEXTUAL_ACTIONS)) {
                    setSystemGeneratedSmartActions(
                            signals.getParcelableArrayList(Adjustment.KEY_CONTEXTUAL_ACTIONS));
                    EventLogTags.writeNotificationAdjusted(getKey(),
                            Adjustment.KEY_CONTEXTUAL_ACTIONS,
                            getSystemGeneratedSmartActions().toString());
                }
                if (signals.containsKey(Adjustment.KEY_TEXT_REPLIES)) {
                    setSmartReplies(signals.getCharSequenceArrayList(Adjustment.KEY_TEXT_REPLIES));
                    EventLogTags.writeNotificationAdjusted(getKey(), Adjustment.KEY_TEXT_REPLIES,
                            getSmartReplies().toString());
                }
                if (signals.containsKey(Adjustment.KEY_IMPORTANCE)) {
                    int importance = signals.getInt(Adjustment.KEY_IMPORTANCE);
                    importance = Math.max(IMPORTANCE_UNSPECIFIED, importance);
                    importance = Math.min(IMPORTANCE_HIGH, importance);
                    setAssistantImportance(importance);
                    EventLogTags.writeNotificationAdjusted(getKey(), Adjustment.KEY_IMPORTANCE,
                            Integer.toString(importance));
                }
                if (signals.containsKey(Adjustment.KEY_RANKING_SCORE)) {
                    mRankingScore = signals.getFloat(Adjustment.KEY_RANKING_SCORE);
                    EventLogTags.writeNotificationAdjusted(getKey(), Adjustment.KEY_RANKING_SCORE,
                            Float.toString(mRankingScore));
                }
                if (signals.containsKey(Adjustment.KEY_NOT_CONVERSATION)) {
                    mIsNotConversationOverride = signals.getBoolean(
                            Adjustment.KEY_NOT_CONVERSATION);
                    EventLogTags.writeNotificationAdjusted(getKey(),
                            Adjustment.KEY_NOT_CONVERSATION,
                            Boolean.toString(mIsNotConversationOverride));
                }
                if (!signals.isEmpty() && adjustment.getIssuer() != null) {
                    mAdjustmentIssuer = adjustment.getIssuer();
@@ -1478,6 +1503,24 @@ public final class NotificationRecord {
        return sbn;
    }

    /**
     * Returns whether this record's ranking score is approximately equal to otherScore
     * (the difference must be within 0.0001).
     */
    public boolean rankingScoreMatches(float otherScore) {
        return Math.abs(mRankingScore - otherScore) < 0.0001;
    }

    protected void setPendingLogUpdate(boolean pendingLogUpdate) {
        mPendingLogUpdate = pendingLogUpdate;
    }

    // If a caller of this function subsequently logs the update, they should also call
    // setPendingLogUpdate to false to make sure other callers don't also do so.
    protected boolean hasPendingLogUpdate() {
        return mPendingLogUpdate;
    }

    @VisibleForTesting
    static final class Light {
        public final int color;
+19 −2
Original line number Diff line number Diff line
@@ -58,6 +58,20 @@ public interface NotificationRecordLogger {
            int position, int buzzBeepBlink,
            InstanceId groupId);

    /**
     * Logs a NotificationReported atom reflecting an adjustment to a notification.
     * Unlike maybeLogNotificationPosted, this method is guaranteed to log a notification update,
     * so the caller must take responsibility for checking that that logging update is necessary,
     * and that the notification is meaningfully changed.
     * @param r The NotificationRecord. If null, no action is taken.
     * @param position The position at which this notification is ranked.
     * @param buzzBeepBlink Logging code reflecting whether this notification alerted the user.
     * @param groupId The instance Id of the group summary notification, or null.
     */
    void logNotificationAdjusted(@Nullable NotificationRecord r,
            int position, int buzzBeepBlink,
            InstanceId groupId);

    /**
     * Logs a notification cancel / dismiss event using UiEventReported (event ids from the
     * NotificationCancelledEvents enum).
@@ -96,7 +110,9 @@ public interface NotificationRecordLogger {
        @UiEvent(doc = "New notification enqueued to post")
        NOTIFICATION_POSTED(162),
        @UiEvent(doc = "Notification substantially updated, or alerted again.")
        NOTIFICATION_UPDATED(163);
        NOTIFICATION_UPDATED(163),
        @UiEvent(doc = "Notification adjusted by assistant.")
        NOTIFICATION_ADJUSTED(908);

        private final int mId;
        NotificationReportedEvent(int id) {
@@ -349,7 +365,8 @@ public interface NotificationRecordLogger {
                    && Objects.equals(r.getSbn().getNotification().category,
                        old.getSbn().getNotification().category)
                    && (r.getImportance() == old.getImportance())
                    && (getLoggingImportance(r) == getLoggingImportance(old)));
                    && (getLoggingImportance(r) == getLoggingImportance(old))
                    && r.rankingScoreMatches(old.getRankingScore()));
        }

        /**
+29 −11
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.server.notification;

import android.annotation.Nullable;

import com.android.internal.logging.InstanceId;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLoggerImpl;
@@ -37,33 +39,49 @@ public class NotificationRecordLoggerImpl implements NotificationRecordLogger {
        if (!p.shouldLogReported(buzzBeepBlink)) {
            return;
        }
        writeNotificationReportedAtom(p, NotificationReportedEvent.fromRecordPair(p),
                position, buzzBeepBlink, groupId);
    }

    @Override
    public void logNotificationAdjusted(@Nullable NotificationRecord r,
            int position, int buzzBeepBlink,
            InstanceId groupId) {
        NotificationRecordPair p = new NotificationRecordPair(r, null);
        writeNotificationReportedAtom(p, NotificationReportedEvent.NOTIFICATION_ADJUSTED,
                position, buzzBeepBlink, groupId);
    }

    private void writeNotificationReportedAtom(NotificationRecordPair p,
            NotificationReportedEvent eventType, int position, int buzzBeepBlink,
            InstanceId groupId) {
        FrameworkStatsLog.write(FrameworkStatsLog.NOTIFICATION_REPORTED,
                /* int32 event_id = 1 */ NotificationReportedEvent.fromRecordPair(p).getId(),
                /* int32 uid = 2 */ r.getUid(),
                /* string package_name = 3 */ r.getSbn().getPackageName(),
                /* int32 event_id = 1 */ eventType.getId(),
                /* int32 uid = 2 */ p.r.getUid(),
                /* string package_name = 3 */ p.r.getSbn().getPackageName(),
                /* int32 instance_id = 4 */ p.getInstanceId(),
                /* int32 notification_id_hash = 5 */ p.getNotificationIdHash(),
                /* int32 channel_id_hash = 6 */ p.getChannelIdHash(),
                /* string group_id_hash = 7 */ p.getGroupIdHash(),
                /* int32 group_instance_id = 8 */ (groupId == null) ? 0 : groupId.getId(),
                /* bool is_group_summary = 9 */ r.getSbn().getNotification().isGroupSummary(),
                /* string category = 10 */ r.getSbn().getNotification().category,
                /* bool is_group_summary = 9 */ p.r.getSbn().getNotification().isGroupSummary(),
                /* string category = 10 */ p.r.getSbn().getNotification().category,
                /* int32 style = 11 */ p.getStyle(),
                /* int32 num_people = 12 */ p.getNumPeople(),
                /* int32 position = 13 */ position,
                /* android.stats.sysui.NotificationImportance importance = 14 */
                NotificationRecordLogger.getLoggingImportance(r),
                NotificationRecordLogger.getLoggingImportance(p.r),
                /* int32 alerting = 15 */ buzzBeepBlink,
                /* NotificationImportanceExplanation importance_source = 16 */
                r.getImportanceExplanationCode(),
                p.r.getImportanceExplanationCode(),
                /* android.stats.sysui.NotificationImportance importance_initial = 17 */
                r.getInitialImportance(),
                p.r.getInitialImportance(),
                /* NotificationImportanceExplanation importance_initial_source = 18 */
                r.getInitialImportanceExplanationCode(),
                p.r.getInitialImportanceExplanationCode(),
                /* android.stats.sysui.NotificationImportance importance_asst = 19 */
                r.getAssistantImportance(),
                p.r.getAssistantImportance(),
                /* int32 assistant_hash = 20 */ p.getAssistantHash(),
                /* float assistant_ranking_score = 21 */ r.getRankingScore()
                /* float assistant_ranking_score = 21 */ p.r.getRankingScore()
        );
    }

Loading