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

Commit d411e94a authored by Julia Reynolds's avatar Julia Reynolds Committed by Android (Google) Code Review
Browse files

Merge "DO NOT MERGE: Associate notif cancels with notif posts" into rvc-qpr-dev

parents 7585f8c4 d96f50a8
Loading
Loading
Loading
Loading
+44 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 com.android.server.notification;

/**
 * Testable wrapper around {@link android.os.SystemClock}.
 *
 * The default implementation at InjectableSystemClockImpl just proxies calls to the real
 * SystemClock
 *
 * In tests, pass an instance of FakeSystemClock, which allows you to control the values returned by
 * the various getters below.
 */
public interface InjectableSystemClock {
    /** @see android.os.SystemClock#uptimeMillis() */
    long uptimeMillis();

    /** @see android.os.SystemClock#elapsedRealtime() */
    long elapsedRealtime();

    /** @see android.os.SystemClock#elapsedRealtimeNanos() */
    long elapsedRealtimeNanos();

    /** @see android.os.SystemClock#currentThreadTimeMillis() */
    long currentThreadTimeMillis();

    /** @see System#currentTimeMillis()  */
    long currentTimeMillis();
}
+51 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 com.android.server.notification;

/**
 * Default implementation of {@link InjectableSystemClock}.
 *
 * @hide
 */
public class InjectableSystemClockImpl implements InjectableSystemClock {
    public InjectableSystemClockImpl() {}

    @Override
    public long uptimeMillis() {
        return android.os.SystemClock.uptimeMillis();
    }

    @Override
    public long elapsedRealtime() {
        return android.os.SystemClock.elapsedRealtime();
    }

    @Override
    public long elapsedRealtimeNanos() {
        return android.os.SystemClock.elapsedRealtimeNanos();
    }

    @Override
    public long currentThreadTimeMillis() {
        return android.os.SystemClock.currentThreadTimeMillis();
    }

    @Override
    public long currentTimeMillis() {
        return System.currentTimeMillis();
    }
}
+197 −103
Original line number Diff line number Diff line
@@ -187,7 +187,6 @@ import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
@@ -473,6 +472,11 @@ public class NotificationManagerService extends SystemService {
    final ArrayMap<Integer, ArrayMap<String, String>> mAutobundledSummaries = new ArrayMap<>();
    final ArrayList<ToastRecord> mToastQueue = new ArrayList<>();
    final ArrayMap<String, NotificationRecord> mSummaryByGroupKey = new ArrayMap<>();
    // Keep track of `CancelNotificationRunnable`s which have been delayed due to awaiting
    // enqueued notifications to post
    @GuardedBy("mNotificationLock")
    final ArrayMap<NotificationRecord, ArrayList<CancelNotificationRunnable>> mDelayedCancelations =
            new ArrayMap<>();

    // The last key in this list owns the hardware.
    ArrayList<String> mLights = new ArrayList<>();
@@ -535,6 +539,7 @@ public class NotificationManagerService extends SystemService {
    private NotificationRecordLogger mNotificationRecordLogger;
    private InstanceIdSequence mNotificationInstanceIdSequence;
    private Set<String> mMsgPkgsAllowedAsConvos = new HashSet();
    private final InjectableSystemClock mSystemClock;

    static class Archive {
        final SparseArray<Boolean> mEnabled;
@@ -748,7 +753,7 @@ public class NotificationManagerService extends SystemService {
                        parser, mAllowedManagedServicePackages, forRestore, userId);
                migratedManagedServices = true;
            } else if (mSnoozeHelper.XML_TAG_NAME.equals(parser.getName())) {
                mSnoozeHelper.readXml(parser, System.currentTimeMillis());
                mSnoozeHelper.readXml(parser, mSystemClock.currentTimeMillis());
            }
            if (LOCKSCREEN_ALLOW_SECURE_NOTIFICATIONS_TAG.equals(parser.getName())) {
                if (forRestore && userId != UserHandle.USER_SYSTEM) {
@@ -901,7 +906,7 @@ public class NotificationManagerService extends SystemService {
                    Slog.w(TAG, "No notification with key: " + key);
                    return;
                }
                final long now = System.currentTimeMillis();
                final long now = mSystemClock.currentTimeMillis();
                MetricsLogger.action(r.getItemLogMaker()
                        .setType(MetricsEvent.TYPE_ACTION)
                        .addTaggedData(MetricsEvent.NOTIFICATION_SHADE_INDEX, nv.rank)
@@ -933,7 +938,7 @@ public class NotificationManagerService extends SystemService {
                    Slog.w(TAG, "No notification with key: " + key);
                    return;
                }
                final long now = System.currentTimeMillis();
                final long now = mSystemClock.currentTimeMillis();
                MetricsLogger.action(r.getLogMaker(now)
                        .setCategory(MetricsEvent.NOTIFICATION_ITEM_ACTION)
                        .setType(MetricsEvent.TYPE_ACTION)
@@ -1697,15 +1702,18 @@ public class NotificationManagerService extends SystemService {
    public NotificationManagerService(Context context) {
        this(context,
                new NotificationRecordLoggerImpl(),
                new InjectableSystemClockImpl(),
                new InstanceIdSequence(NOTIFICATION_INSTANCE_ID_MAX));
    }

    @VisibleForTesting
    public NotificationManagerService(Context context,
            NotificationRecordLogger notificationRecordLogger,
            InjectableSystemClock systemClock,
            InstanceIdSequence notificationInstanceIdSequence) {
        super(context);
        mNotificationRecordLogger = notificationRecordLogger;
        mSystemClock = systemClock;
        mNotificationInstanceIdSequence = notificationInstanceIdSequence;
        Notification.processWhitelistToken = WHITELIST_TOKEN;
    }
@@ -2064,6 +2072,11 @@ public class NotificationManagerService extends SystemService {
        return getContext().getResources().getStringArray(key);
    }

    @VisibleForTesting
    protected Handler getWorkHandler() {
        return mHandler;
    }

    @Override
    public void onStart() {
        SnoozeHelper snoozeHelper = new SnoozeHelper(getContext(), (userId, r, muteOnReturn) -> {
@@ -2337,7 +2350,8 @@ public class NotificationManagerService extends SystemService {
            mHistoryManager.onBootPhaseAppsCanStart();
            registerDeviceConfigChange();
        } else if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
            mSnoozeHelper.scheduleRepostsForPersistedNotifications(System.currentTimeMillis());
            mSnoozeHelper.scheduleRepostsForPersistedNotifications(
                    mSystemClock.currentTimeMillis());
        }
    }

@@ -2697,7 +2711,7 @@ public class NotificationManagerService extends SystemService {
                        .setUserId(r.getSbn().getNormalizedUserId())
                        .setChannelId(r.getChannel().getId())
                        .setChannelName(r.getChannel().getName().toString())
                        .setPostedTimeMs(System.currentTimeMillis())
                        .setPostedTimeMs(mSystemClock.currentTimeMillis())
                        .setTitle(getHistoryTitle(r.getNotification()))
                        .setText(getHistoryText(
                                r.getSbn().getPackageContext(getContext()), r.getNotification()))
@@ -5225,7 +5239,7 @@ public class NotificationManagerService extends SystemService {
                                GroupHelper.AUTOGROUP_KEY, adjustedSbn.getUid(),
                                adjustedSbn.getInitialPid(), summaryNotification,
                                adjustedSbn.getUser(), GroupHelper.AUTOGROUP_KEY,
                                System.currentTimeMillis());
                                mSystemClock.currentTimeMillis());
                summaryRecord = new NotificationRecord(getContext(), summarySbn,
                        notificationRecord.getChannel());
                summaryRecord.setIsAppImportanceLocked(
@@ -5460,6 +5474,22 @@ public class NotificationManagerService extends SystemService {

                    mSnoozeHelper.dump(pw, filter);
                }

                // Log delayed notification cancels
                pw.println();
                pw.println("  Delayed notification cancels:");
                if (mDelayedCancelations.isEmpty()) {
                    pw.println("    None");
                } else {
                    Set<NotificationRecord> delayedKeys = mDelayedCancelations.keySet();
                    for (NotificationRecord record : delayedKeys) {
                        ArrayList<CancelNotificationRunnable> queuedCancels =
                                mDelayedCancelations.get(record);
                        pw.println("    (" + queuedCancels.size() + ") cancels enqueued for"
                                + record.getKey());
                    }
                }
                pw.println();
            }

            if (!zenOnly) {
@@ -5678,7 +5708,7 @@ public class NotificationManagerService extends SystemService {

        final StatusBarNotification n = new StatusBarNotification(
                pkg, opPkg, id, tag, notificationUid, callingPid, notification,
                user, null, System.currentTimeMillis());
                user, null, mSystemClock.currentTimeMillis());

        // setup local book-keeping
        String channelId = notification.getChannelId();
@@ -6011,7 +6041,7 @@ public class NotificationManagerService extends SystemService {
                    final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);
                    if (appEnqueueRate > mMaxPackageEnqueueRate) {
                        mUsageStats.registerOverRateQuota(pkg);
                        final long now = SystemClock.elapsedRealtime();
                        final long now = mSystemClock.elapsedRealtime();
                        if ((now - mLastOverRateLogTime) > MIN_PACKAGE_OVERRATE_LOG_INTERVAL) {
                            Slog.e(TAG, "Package enqueue rate is " + appEnqueueRate
                                    + ". Shedding " + r.getSbn().getKey() + ". package=" + pkg);
@@ -6223,43 +6253,13 @@ public class NotificationManagerService extends SystemService {
            this.mRank = rank;
            this.mCount = count;
            this.mListener = listener;
            this.mWhen = System.currentTimeMillis();
        }

        @Override
        public void run() {
            String listenerName = mListener == null ? null : mListener.component.toShortString();
            if (DBG) {
                EventLogTags.writeNotificationCancel(mCallingUid, mCallingPid, mPkg, mId, mTag,
                        mUserId, mMustHaveFlags, mMustNotHaveFlags, mReason, listenerName);
            }

            synchronized (mNotificationLock) {
                // Check to see if there is a notification in the enqueued list that hasn't had a
                // chance to post yet.
                List<NotificationRecord> enqueued = findEnqueuedNotificationsForCriteria(
                        mPkg, mTag, mId, mUserId);
                boolean repost = false;
                if (enqueued.size() > 0) {
                    // Found something, let's see what it was
                    repost = true;
                    // If all enqueues happened before this cancel then wait for them to happen,
                    // otherwise we should let this cancel through so the next enqueue happens
                    for (NotificationRecord r : enqueued) {
                        if (r.mUpdateTimeMs > mWhen) {
                            // At least one enqueue was posted after the cancel, so we're invalid
                            Slog.i(TAG, "notification cancel ignored due to newer enqueued entry"
                                    + "key=" + r.getSbn().getKey());
                            return;
                        }
                    }
                }
                if (repost) {
                    mHandler.post(this);
                    return;
            this.mWhen = mSystemClock.currentTimeMillis();
        }

        // Move the work to this function so it can be called from PostNotificationRunnable
        private void doNotificationCancelLocked() {
            // Look for the notification in the posted list, since we already checked enqueued.
            String listenerName = mListener == null ? null : mListener.component.toShortString();
            NotificationRecord r =
                    findNotificationByListLocked(mNotificationList, mPkg, mTag, mId, mUserId);
            if (r != null) {
@@ -6284,10 +6284,6 @@ public class NotificationManagerService extends SystemService {
                if ((r.getNotification().flags & mMustNotHaveFlags) != 0) {
                    return;
                }
                    if (r.getUpdateTimeMs() > mWhen) {
                        // In this case, a post must have slipped by when this runnable reposted
                        return;
                    }

                // Bubbled children get to stick around if the summary was manually cancelled
                // (user removed) from systemui.
@@ -6304,7 +6300,7 @@ public class NotificationManagerService extends SystemService {
                }

                // Cancel the notification.
                    boolean wasPosted = removeFromNotificationListsLocked(r);
                boolean wasPosted = removePreviousFromNotificationListsLocked(r, mWhen);
                cancelNotificationLocked(
                        r, mSendDelete, mReason, mRank, mCount, wasPosted, listenerName);
                cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName,
@@ -6325,6 +6321,39 @@ public class NotificationManagerService extends SystemService {
                }
            }
        }

        @Override
        public void run() {
            String listenerName = mListener == null ? null : mListener.component.toShortString();
            if (DBG) {
                EventLogTags.writeNotificationCancel(mCallingUid, mCallingPid, mPkg, mId, mTag,
                        mUserId, mMustHaveFlags, mMustNotHaveFlags, mReason, listenerName);
            }

            synchronized (mNotificationLock) {
                // Check to see if there is a notification in the enqueued list that hasn't had a
                // chance to post yet.
                List<NotificationRecord> enqueued = findEnqueuedNotificationsForCriteria(
                        mPkg, mTag, mId, mUserId);
                if (enqueued.size() > 0) {
                    // We have found notifications that were enqueued before this cancel, but not
                    // yet posted. Attach this cancel to the last enqueue (the most recent), and
                    // we will be executed in that notification's PostNotificationRunnable
                    NotificationRecord enqueuedToAttach = enqueued.get(enqueued.size() - 1);

                    ArrayList<CancelNotificationRunnable> delayed =
                            mDelayedCancelations.get(enqueuedToAttach);
                    if (delayed == null) {
                        delayed = new ArrayList<>();
                    }

                    delayed.add(this);
                    mDelayedCancelations.put(enqueuedToAttach, delayed);
                    return;
                }

                doNotificationCancelLocked();
            }
        }
    }

@@ -6346,7 +6375,7 @@ public class NotificationManagerService extends SystemService {
                        mSnoozeHelper.getSnoozeTimeForUnpostedNotification(
                                r.getUser().getIdentifier(),
                                r.getSbn().getPackageName(), r.getSbn().getKey());
                final long currentTime = System.currentTimeMillis();
                final long currentTime = mSystemClock.currentTimeMillis();
                if (snoozeAt.longValue() > currentTime) {
                    (new SnoozeNotificationRunnable(r.getSbn().getKey(),
                            snoozeAt.longValue() - currentTime, null)).snoozeLocked(r);
@@ -6406,15 +6435,26 @@ public class NotificationManagerService extends SystemService {
                            enqueueStatus);
                }

                postPostNotificationRunnableMaybeDelayedLocked(
                        r, new PostNotificationRunnable(r.getKey()));
            }
        }
    }

    /**
     * Mainly needed as a hook for tests which require setting up enqueued-but-not-posted
     * notification records
     */
    @GuardedBy("mNotificationLock")
    protected void postPostNotificationRunnableMaybeDelayedLocked(
            NotificationRecord r,
            PostNotificationRunnable runnable) {
        // tell the assistant service about the notification
        if (mAssistants.isEnabled()) {
            mAssistants.onNotificationEnqueuedLocked(r);
                    mHandler.postDelayed(new PostNotificationRunnable(r.getKey()),
                            DELAY_FOR_ASSISTANT_TIME);
            mHandler.postDelayed(runnable, DELAY_FOR_ASSISTANT_TIME);
        } else {
                    mHandler.post(new PostNotificationRunnable(r.getKey()));
                }
            }
            mHandler.post(runnable);
        }
    }

@@ -6566,13 +6606,23 @@ public class NotificationManagerService extends SystemService {
                            buzzBeepBlinkLoggingCode, getGroupInstanceId(n.getGroupKey()));
                } finally {
                    int N = mEnqueuedNotifications.size();
                    NotificationRecord enqueued = null;
                    for (int i = 0; i < N; i++) {
                        final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                        enqueued = mEnqueuedNotifications.get(i);
                        if (Objects.equals(key, enqueued.getKey())) {
                            mEnqueuedNotifications.remove(i);
                            break;
                        }
                    }

                    // If the enqueued notification record had a cancel attached after it, execute
                    // it right now
                    if (enqueued != null && mDelayedCancelations.get(enqueued) != null) {
                        for (CancelNotificationRunnable r : mDelayedCancelations.get(enqueued)) {
                            r.doNotificationCancelLocked();
                        }
                        mDelayedCancelations.remove(enqueued);
                    }
                }
            }
        }
@@ -6801,7 +6851,8 @@ public class NotificationManagerService extends SystemService {
                            .putExtra(EXTRA_KEY, record.getKey()),
                    PendingIntent.FLAG_UPDATE_CURRENT);
            mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                    SystemClock.elapsedRealtime() + record.getNotification().getTimeoutAfter(), pi);
                    mSystemClock.elapsedRealtime() + record.getNotification().getTimeoutAfter(),
                    pi);
        }
    }

@@ -7329,7 +7380,7 @@ public class NotificationManagerService extends SystemService {
                    || visibilityChanged
                    || interruptiveChanged;
            if (interceptBefore && !record.isIntercepted()
                    && record.isNewEnoughForAlerting(System.currentTimeMillis())) {
                    && record.isNewEnoughForAlerting(mSystemClock.currentTimeMillis())) {
                buzzBeepBlinkLocked(record);
            }
        }
@@ -7616,6 +7667,34 @@ public class NotificationManagerService extends SystemService {
        return wasPosted;
    }

    /**
     * Similar to the above method, removes all NotificationRecords with the same key as the given
     * NotificationRecord, but skips any records which are newer than the given one.
     */
    private boolean removePreviousFromNotificationListsLocked(NotificationRecord r,
            long removeBefore) {
        // Remove notification records that occurred before the given record from both lists,
        // specifically allowing newer ones to respect ordering
        boolean wasPosted = false;
        List<NotificationRecord> matching =
                findNotificationsByListLocked(mNotificationList, r.getKey());
        for (NotificationRecord record : matching) {
            // We don't need to check against update time for posted notifs
            mNotificationList.remove(record);
            mNotificationsByKey.remove(record.getSbn().getKey());
            wasPosted = true;
        }

        matching = findNotificationsByListLocked(mEnqueuedNotifications, r.getKey());
        for (NotificationRecord record : matching) {
            if (record.getUpdateTimeMs() <= removeBefore) {
                mNotificationList.remove(record);
            }
        }

        return wasPosted;
    }

    @GuardedBy("mNotificationLock")
    private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete,
            @NotificationListenerService.NotificationCancelReason int reason,
@@ -7731,7 +7810,7 @@ public class NotificationManagerService extends SystemService {
        // Save it for users of getHistoricalNotifications()
        mArchive.record(r.getSbn(), reason);

        final long now = System.currentTimeMillis();
        final long now = mSystemClock.currentTimeMillis();
        final LogMaker logMaker = r.getItemLogMaker()
                .setType(MetricsEvent.TYPE_DISMISS)
                .setSubtype(reason);
@@ -8287,6 +8366,21 @@ public class NotificationManagerService extends SystemService {
        return null;
    }

    @GuardedBy("mNotificationLock")
    private List<NotificationRecord> findNotificationsByListLocked(
            ArrayList<NotificationRecord> list,
            String key) {
        List<NotificationRecord> matching = new ArrayList<>();
        final int n = list.size();
        for (int i = 0; i < n; i++) {
            NotificationRecord r = list.get(i);
            if (key.equals(r.getKey())) {
                matching.add(r);
            }
        }
        return matching;
    }

    /**
     * There may be multiple records that match your criteria. For instance if there have been
     * multiple notifications posted which are enqueued for the same pkg, tag, id, userId. This
+2 −1
Original line number Diff line number Diff line
@@ -103,6 +103,7 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase {
    NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
    private InstanceIdSequence mNotificationInstanceIdSequence = new InstanceIdSequenceFake(
            1 << 30);
    private InjectableSystemClock mSystemClock = new FakeSystemClock();

    private NotificationManagerService mService;
    private String mPkg = "com.android.server.notification";
@@ -154,7 +155,7 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase {
        assertTrue(accessibilityManager.isEnabled());

        mService = spy(new NotificationManagerService(getContext(), mNotificationRecordLogger,
                mNotificationInstanceIdSequence));
                mSystemClock, mNotificationInstanceIdSequence));
        mService.setAudioManager(mAudioManager);
        mService.setVibrator(mVibrator);
        mService.setSystemReady(true);
Loading