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

Commit 25cf5547 authored by Julia Reynolds's avatar Julia Reynolds
Browse files

Hold classifications for noisy notifications

If we recieve a classification adjustment after the notification
has been posted, and it made noisy, delay classifying the notification
for 30 seconds so users can easily see what notification made a sound.

Test: NotificationAdjustmentExtractorTest
Test: NotificationRecordTest
Test: manual with delayed adjustment
Bug: 430573835
Flag: com.android.server.notification.show_noisy_bundled_notifications

Change-Id: I85a34a268ce31fc9c8c3cdf954dd89855fc693a4
parent f9b4eeb8
Loading
Loading
Loading
Loading
+87 −19
Original line number Diff line number Diff line
@@ -20,8 +20,14 @@ import static android.service.notification.Adjustment.KEY_UNCLASSIFY;
import static android.service.notification.Flags.notificationForceGrouping;

import android.content.Context;
import android.util.ArraySet;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.List;

/**
 * Applies adjustments from the group helper and notification assistant
 */
@@ -30,6 +36,14 @@ public class NotificationAdjustmentExtractor implements NotificationSignalExtrac
    private static final boolean DBG = false;
    private GroupHelper mGroupHelper;

    /** Length of time (in milliseconds) that a noisy notification will stay in its non-bundled
     * classification.
     */
    @VisibleForTesting
    static final long HANG_TIME_MS = 30000;

    @VisibleForTesting
    InjectedTime mInjectedTimeMs = null;

    public void initialize(Context ctx, NotificationUsageStats usageStats) {
        if (DBG) Slog.d(TAG, "Initializing  " + getClass().getSimpleName() + ".");
@@ -43,12 +57,73 @@ public class NotificationAdjustmentExtractor implements NotificationSignalExtrac

        final boolean hasAdjustedClassification = record.hasAdjustment(KEY_TYPE);
        final boolean removedClassification = record.hasAdjustment(KEY_UNCLASSIFY);

        if (Flags.showNoisyBundledNotifications()
                && android.service.notification.Flags.notificationClassification()
                && hasAdjustedClassification && record.getLastAudiblyAlertedMs() > 0) {
            record.applyAdjustments(new ArraySet<>(new String[] {KEY_TYPE}));

            return getClassificationReconsideration(record);
        }

        record.applyAdjustments();

        if (notificationForceGrouping()
                && android.service.notification.Flags.notificationClassification()) {
            // Classification adjustments trigger regrouping
            if (mGroupHelper != null && (hasAdjustedClassification || removedClassification)) {
                return getRegroupReconsideration(
                        record, hasAdjustedClassification, removedClassification);
            }
        }

        return null;
    }

    @Override
    public void setConfig(RankingConfig config) {
        // config is not used
    }

    @Override
    public void setZenHelper(ZenModeHelper helper) {

    }

    @Override
    public void setGroupHelper(GroupHelper groupHelper) {
        mGroupHelper = groupHelper;
    }

    private long getCurrentTime() {
        if (mInjectedTimeMs != null) {
            return mInjectedTimeMs.getCurrentTimeMillis();
        }
        return System.currentTimeMillis();
    }

    private RankingReconsideration getClassificationReconsideration(NotificationRecord record) {
        return new RankingReconsideration(record.getKey(), HANG_TIME_MS) {
            @Override
            public void work() {
                // pass
            }

            @Override
            public void applyChangesLocked(NotificationRecord record) {
                if ((getCurrentTime() - record.getLastAudiblyAlertedMs()) >= HANG_TIME_MS) {
                    record.applyAdjustments();
                    getRegroupReconsideration(record, true, false).applyChangesLocked(record);
                }
            }
        };
    }

    // The notification channel of the record has changed such that it's now moving to a new
    // UI section. We need to change the record's grouping to make sure it's not in a group
    // for the wrong section
    private RankingReconsideration getRegroupReconsideration(NotificationRecord record,
            boolean hasAdjustedClassification, boolean removedClassification) {
        return new RankingReconsideration(record.getKey(), 0) {
            @Override
            public void work() {
@@ -69,23 +144,16 @@ public class NotificationAdjustmentExtractor implements NotificationSignalExtrac
            }
        };
    }
        }

        return null;
    }
    static class InjectedTime {
        private final long mCurrentTimeMillis;

    @Override
    public void setConfig(RankingConfig config) {
        // config is not used
        InjectedTime(long time) {
            mCurrentTimeMillis = time;
        }

    @Override
    public void setZenHelper(ZenModeHelper helper) {

        long getCurrentTimeMillis() {
            return mCurrentTimeMillis;
        }

    @Override
    public void setGroupHelper(GroupHelper groupHelper) {
        mGroupHelper = groupHelper;
    }
}
+128 −51
Original line number Diff line number Diff line
@@ -21,7 +21,20 @@ import static android.app.NotificationManager.IMPORTANCE_HIGH;
import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.NotificationManager.IMPORTANCE_MIN;
import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
import static android.service.notification.Adjustment.KEY_CONTEXTUAL_ACTIONS;
import static android.service.notification.Adjustment.KEY_GROUP_KEY;
import static android.service.notification.Adjustment.KEY_IMPORTANCE;
import static android.service.notification.Adjustment.KEY_IMPORTANCE_PROPOSAL;
import static android.service.notification.Adjustment.KEY_NOT_CONVERSATION;
import static android.service.notification.Adjustment.KEY_PEOPLE;
import static android.service.notification.Adjustment.KEY_RANKING_SCORE;
import static android.service.notification.Adjustment.KEY_SENSITIVE_CONTENT;
import static android.service.notification.Adjustment.KEY_SNOOZE_CRITERIA;
import static android.service.notification.Adjustment.KEY_SUMMARIZATION;
import static android.service.notification.Adjustment.KEY_TEXT_REPLIES;
import static android.service.notification.Adjustment.KEY_TYPE;
import static android.service.notification.Adjustment.KEY_UNCLASSIFY;
import static android.service.notification.Adjustment.KEY_USER_SENTIMENT;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE;

@@ -708,6 +721,13 @@ public final class NotificationRecord {
                this.getSbn().getNotification());
    }

    @VisibleForTesting
    int getPendingAdjustmentCount() {
        synchronized (mAdjustments) {
            return mAdjustments.size();
        }
    }

    public boolean hasAdjustment(String key) {
        synchronized (mAdjustments) {
            for (Adjustment adjustment : mAdjustments) {
@@ -726,113 +746,160 @@ public final class NotificationRecord {
    }

    public void applyAdjustments() {
        long now = System.currentTimeMillis();
        applyAdjustments(new ArraySet<>());
    }

    public void applyAdjustments(@NonNull ArraySet<String> keysToSkip) {
        synchronized (mAdjustments) {
            for (Adjustment adjustment: mAdjustments) {
            for (int i = mAdjustments.size() - 1; i >= 0; i--) {
                Adjustment adjustment = mAdjustments.get(i);
                Bundle signals = adjustment.getSignals();
                if (signals.containsKey(Adjustment.KEY_PEOPLE)) {
                if (signals.containsKey(KEY_PEOPLE) && !keysToSkip.contains(KEY_PEOPLE)) {
                    final ArrayList<String> people =
                            adjustment.getSignals().getStringArrayList(Adjustment.KEY_PEOPLE);
                            adjustment.getSignals().getStringArrayList(KEY_PEOPLE);
                    setPeopleOverride(people);
                    EventLogTags.writeNotificationAdjusted(
                            getKey(), Adjustment.KEY_PEOPLE, people.toString());
                    EventLogTags.writeNotificationAdjusted(getKey(), KEY_PEOPLE, people.toString());
                    if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                        signals.remove(KEY_PEOPLE);
                    }
                if (signals.containsKey(Adjustment.KEY_SNOOZE_CRITERIA)) {
                }
                if (signals.containsKey(KEY_SNOOZE_CRITERIA)
                        && !keysToSkip.contains(KEY_SNOOZE_CRITERIA)) {
                    final ArrayList<SnoozeCriterion> snoozeCriterionList =
                            adjustment.getSignals().getParcelableArrayList(
                                    Adjustment.KEY_SNOOZE_CRITERIA,
                                    KEY_SNOOZE_CRITERIA,
                                    android.service.notification.SnoozeCriterion.class);
                    setSnoozeCriteria(snoozeCriterionList);
                    EventLogTags.writeNotificationAdjusted(getKey(), Adjustment.KEY_SNOOZE_CRITERIA,
                    EventLogTags.writeNotificationAdjusted(getKey(), KEY_SNOOZE_CRITERIA,
                            snoozeCriterionList.toString());
                    if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                        signals.remove(KEY_SNOOZE_CRITERIA);
                    }
                }
                if (signals.containsKey(Adjustment.KEY_GROUP_KEY)) {
                if (signals.containsKey(KEY_GROUP_KEY) && !keysToSkip.contains(KEY_GROUP_KEY)) {
                    final String groupOverrideKey =
                            adjustment.getSignals().getString(Adjustment.KEY_GROUP_KEY);
                            adjustment.getSignals().getString(KEY_GROUP_KEY);
                    setOverrideGroupKey(groupOverrideKey);
                    EventLogTags.writeNotificationAdjusted(getKey(), Adjustment.KEY_GROUP_KEY,
                    EventLogTags.writeNotificationAdjusted(getKey(), KEY_GROUP_KEY,
                            groupOverrideKey);
                    if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                        signals.remove(KEY_GROUP_KEY);
                    }
                if (signals.containsKey(Adjustment.KEY_USER_SENTIMENT)) {
                }
                if (signals.containsKey(KEY_USER_SENTIMENT)
                        && !keysToSkip.contains(KEY_USER_SENTIMENT)) {
                    // Only allow user sentiment update from assistant if user hasn't already
                    // expressed a preference for this channel
                    if (!mIsAppImportanceLocked
                            && (getChannel().getUserLockedFields() & USER_LOCKED_IMPORTANCE) == 0) {
                        setUserSentiment(adjustment.getSignals().getInt(
                                Adjustment.KEY_USER_SENTIMENT, USER_SENTIMENT_NEUTRAL));
                                KEY_USER_SENTIMENT, USER_SENTIMENT_NEUTRAL));
                        EventLogTags.writeNotificationAdjusted(getKey(),
                                Adjustment.KEY_USER_SENTIMENT,
                                KEY_USER_SENTIMENT,
                                Integer.toString(getUserSentiment()));
                        if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                            signals.remove(KEY_USER_SENTIMENT);
                        }
                    }
                }
                if (signals.containsKey(Adjustment.KEY_CONTEXTUAL_ACTIONS)) {
                if (signals.containsKey(KEY_CONTEXTUAL_ACTIONS)
                        && !keysToSkip.contains(KEY_CONTEXTUAL_ACTIONS)) {
                    setSystemGeneratedSmartActions(
                            signals.getParcelableArrayList(Adjustment.KEY_CONTEXTUAL_ACTIONS,
                            signals.getParcelableArrayList(KEY_CONTEXTUAL_ACTIONS,
                                    android.app.Notification.Action.class));
                    EventLogTags.writeNotificationAdjusted(getKey(),
                            Adjustment.KEY_CONTEXTUAL_ACTIONS,
                            getSystemGeneratedSmartActions().toString());
                            KEY_CONTEXTUAL_ACTIONS, getSystemGeneratedSmartActions().toString());
                    if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                        signals.remove(KEY_CONTEXTUAL_ACTIONS);
                    }
                }
                if (signals.containsKey(Adjustment.KEY_TEXT_REPLIES)) {
                    setSmartReplies(signals.getCharSequenceArrayList(Adjustment.KEY_TEXT_REPLIES));
                    EventLogTags.writeNotificationAdjusted(getKey(), Adjustment.KEY_TEXT_REPLIES,
                if (signals.containsKey(KEY_TEXT_REPLIES)
                        && !keysToSkip.contains(KEY_TEXT_REPLIES)) {
                    setSmartReplies(signals.getCharSequenceArrayList(KEY_TEXT_REPLIES));
                    EventLogTags.writeNotificationAdjusted(getKey(), KEY_TEXT_REPLIES,
                            getSmartReplies().toString());
                    if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                        signals.remove(KEY_TEXT_REPLIES);
                    }
                if (signals.containsKey(Adjustment.KEY_IMPORTANCE)) {
                    int importance = signals.getInt(Adjustment.KEY_IMPORTANCE);
                }
                if (signals.containsKey(KEY_IMPORTANCE) && !keysToSkip.contains(KEY_IMPORTANCE)) {
                    int importance = signals.getInt(KEY_IMPORTANCE);
                    importance = Math.max(IMPORTANCE_UNSPECIFIED, importance);
                    importance = Math.min(IMPORTANCE_HIGH, importance);
                    setAssistantImportance(importance);
                    EventLogTags.writeNotificationAdjusted(getKey(), Adjustment.KEY_IMPORTANCE,
                    EventLogTags.writeNotificationAdjusted(getKey(), KEY_IMPORTANCE,
                            Integer.toString(importance));
                    if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                        signals.remove(KEY_IMPORTANCE);
                    }
                }
                if (signals.containsKey(Adjustment.KEY_RANKING_SCORE)) {
                    mRankingScore = signals.getFloat(Adjustment.KEY_RANKING_SCORE);
                    EventLogTags.writeNotificationAdjusted(getKey(), Adjustment.KEY_RANKING_SCORE,
                if (signals.containsKey(KEY_RANKING_SCORE)
                        && !keysToSkip.contains(KEY_RANKING_SCORE)) {
                    mRankingScore = signals.getFloat(KEY_RANKING_SCORE);
                    EventLogTags.writeNotificationAdjusted(getKey(), KEY_RANKING_SCORE,
                            Float.toString(mRankingScore));
                    if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                        signals.remove(KEY_RANKING_SCORE);
                    }
                if (signals.containsKey(Adjustment.KEY_NOT_CONVERSATION)) {
                    mIsNotConversationOverride = signals.getBoolean(
                            Adjustment.KEY_NOT_CONVERSATION);
                }
                if (signals.containsKey(KEY_NOT_CONVERSATION)
                        && !keysToSkip.contains(KEY_NOT_CONVERSATION)) {
                    mIsNotConversationOverride = signals.getBoolean(KEY_NOT_CONVERSATION);
                    EventLogTags.writeNotificationAdjusted(getKey(),
                            Adjustment.KEY_NOT_CONVERSATION,
                            Boolean.toString(mIsNotConversationOverride));
                            KEY_NOT_CONVERSATION, Boolean.toString(mIsNotConversationOverride));
                    if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                        signals.remove(KEY_NOT_CONVERSATION);
                    }
                }
                if (signals.containsKey(Adjustment.KEY_IMPORTANCE_PROPOSAL)) {
                    mProposedImportance = signals.getInt(Adjustment.KEY_IMPORTANCE_PROPOSAL);
                if (signals.containsKey(KEY_IMPORTANCE_PROPOSAL)
                        && !keysToSkip.contains(KEY_IMPORTANCE_PROPOSAL)) {
                    mProposedImportance = signals.getInt(KEY_IMPORTANCE_PROPOSAL);
                    EventLogTags.writeNotificationAdjusted(getKey(),
                            Adjustment.KEY_IMPORTANCE_PROPOSAL,
                            KEY_IMPORTANCE_PROPOSAL,
                            Integer.toString(mProposedImportance));
                    if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                        signals.remove(KEY_IMPORTANCE_PROPOSAL);
                    }
                if (signals.containsKey(Adjustment.KEY_SENSITIVE_CONTENT)) {
                    mSensitiveContent = signals.getBoolean(Adjustment.KEY_SENSITIVE_CONTENT);
                }
                if (signals.containsKey(KEY_SENSITIVE_CONTENT)
                        && !keysToSkip.contains(KEY_SENSITIVE_CONTENT)) {
                    mSensitiveContent = signals.getBoolean(KEY_SENSITIVE_CONTENT);
                    EventLogTags.writeNotificationAdjusted(getKey(),
                            Adjustment.KEY_SENSITIVE_CONTENT,
                            Boolean.toString(mSensitiveContent));
                            KEY_SENSITIVE_CONTENT, Boolean.toString(mSensitiveContent));
                    if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                        signals.remove(KEY_SENSITIVE_CONTENT);
                    }
                }
                if (android.service.notification.Flags.notificationClassification()) {
                    if (signals.containsKey(Adjustment.KEY_TYPE)) {
                    if (signals.containsKey(KEY_TYPE) && !keysToSkip.contains(KEY_TYPE)) {
                        // Store original channel visibility before re-assigning channel
                        if (!NotificationChannel.SYSTEM_RESERVED_IDS.contains(mChannel.getId())) {
                            setOriginalChannelVisibility(mChannel.getLockscreenVisibility());
                        }
                        updateNotificationChannel(signals.getParcelable(Adjustment.KEY_TYPE,
                        updateNotificationChannel(signals.getParcelable(KEY_TYPE,
                                NotificationChannel.class));
                        EventLogTags.writeNotificationAdjusted(getKey(),
                                Adjustment.KEY_TYPE,
                                mChannel.getId());
                        EventLogTags.writeNotificationAdjusted(
                                getKey(), KEY_TYPE, mChannel.getId());
                        if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                            signals.remove(KEY_TYPE);
                        }
                    }
                    if (signals.containsKey(Adjustment.KEY_UNCLASSIFY)) {
                    if (signals.containsKey(KEY_UNCLASSIFY)
                            && !keysToSkip.contains(KEY_UNCLASSIFY)) {
                        // reset original channel visibility as we're returning to the original
                        setOriginalChannelVisibility(NotificationManager.VISIBILITY_NO_OVERRIDE);
                        updateNotificationChannel(signals.getParcelable(Adjustment.KEY_UNCLASSIFY,
                        updateNotificationChannel(signals.getParcelable(KEY_UNCLASSIFY,
                                NotificationChannel.class));
                        EventLogTags.writeNotificationAdjusted(getKey(),
                                Adjustment.KEY_UNCLASSIFY, mChannel.getId());
                                KEY_UNCLASSIFY, mChannel.getId());
                        if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                            signals.remove(KEY_UNCLASSIFY);
                        }
                    }
                }
                if ((android.app.Flags.nmSummarizationUi() || android.app.Flags.nmSummarization())
                        && signals.containsKey(KEY_SUMMARIZATION)) {
                        && signals.containsKey(KEY_SUMMARIZATION)
                        && !keysToSkip.contains(KEY_SUMMARIZATION)) {
                    CharSequence summary = signals.getCharSequence(KEY_SUMMARIZATION,
                            signals.getString(KEY_SUMMARIZATION));
                    if (summary != null) {
@@ -842,15 +909,25 @@ public final class NotificationRecord {
                    }
                    EventLogTags.writeNotificationAdjusted(getKey(),
                            KEY_SUMMARIZATION, Boolean.toString(mSummarization != null));
                    if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                        signals.remove(KEY_SUMMARIZATION);
                    }
                }
                if (!signals.isEmpty() && adjustment.getIssuer() != null) {
                    mAdjustmentIssuer = adjustment.getIssuer();
                }
                if (com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                    if (adjustment.getSignals().isEmpty()) {
                        mAdjustments.remove(i);
                    }
            // We have now gotten all the information out of the adjustments and can forget them.
                }
            }
            if (!com.android.server.notification.Flags.showNoisyBundledNotifications()) {
                // We have now gotten all the information out of the adjustments and can forget them
                mAdjustments.clear();
            }
        }
    }

    String getAdjustmentIssuer() {
        return mAdjustmentIssuer;
+2 −3
Original line number Diff line number Diff line
@@ -20,9 +20,8 @@ import android.content.Context;
import com.android.internal.compat.IPlatformCompat;

/**
 * Extracts signals that will be useful to the {@link NotificationComparator} and caches them
 *  on the {@link NotificationRecord} object. These annotations will
 *  not be passed on to {@link android.service.notification.NotificationListenerService}s.
 * Extracts signals that will be useful to provide to notification listeners and caches them
 * on the {@link NotificationRecord} object.
 *
 *  If you add a new Extractor be sure to add it to R.array.config_notificationSignalExtractors.
 */
+0 −1
Original line number Diff line number Diff line
@@ -15,7 +15,6 @@
 */
package com.android.server.notification;

import static android.app.NotificationManager.IMPORTANCE_MIN;
import static android.text.TextUtils.formatSimple;

import android.annotation.NonNull;
+10 −0
Original line number Diff line number Diff line
@@ -230,3 +230,13 @@ flag {
    purpose: PURPOSE_BUGFIX
  }
}

flag {
  name: "show_noisy_bundled_notifications"
  namespace: "notifications"
  description: "If a classification arrived late and the notification made noise, show the notification as unclassified temporarily"
  bug: "430573835"
  metadata {
      purpose: PURPOSE_BUGFIX
    }
}
Loading