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

Commit 76c096d8 authored by Julia Reynolds's avatar Julia Reynolds
Browse files

Rate limit notification sounds/vibrations

1 alert per second.

Bug: 36662487
Test: runtest systemui-notification
Change-Id: I2046ae17b9e89ac0a83e182b91422fd242bc7557
parent 3219d5e2
Loading
Loading
Loading
Loading
+1 −1
Original line number Original line Diff line number Diff line
@@ -10681,7 +10681,7 @@ public final class Settings {
        /**
        /**
         * The maximum allowed notification enqueue rate in Hertz.
         * The maximum allowed notification enqueue rate in Hertz.
         *
         *
         * Should be a float, and includes both posts and updates.
         * Should be a float, and includes updates only.
         * @hide
         * @hide
         */
         */
        public static final String MAX_NOTIFICATION_ENQUEUE_RATE = "max_notification_enqueue_rate";
        public static final String MAX_NOTIFICATION_ENQUEUE_RATE = "max_notification_enqueue_rate";
+35 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2017 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;


/**
 * {@hide}
 */
public class AlertRateLimiter {
    static final long ALLOWED_ALERT_INTERVAL = 1000;
    private long mLastNotificationMillis = 0;

    boolean isRateLimited(long now) {
        final long millisSinceLast = now - mLastNotificationMillis;
        if (millisSinceLast < 0 || millisSinceLast < ALLOWED_ALERT_INTERVAL) {
            return true;
        }
        mLastNotificationMillis = now;
        return false;
    }
}
+35 −20
Original line number Original line Diff line number Diff line
@@ -1120,6 +1120,11 @@ public class NotificationManagerService extends SystemService {
        mIsTelevision = isTelevision;
        mIsTelevision = isTelevision;
    }
    }


    @VisibleForTesting
    void setUsageStats(NotificationUsageStats us) {
        mUsageStats = us;
    }

    // TODO: Tests should call onStart instead once the methods above are removed.
    // TODO: Tests should call onStart instead once the methods above are removed.
    @VisibleForTesting
    @VisibleForTesting
    void init(Looper looper, IPackageManager packageManager, PackageManager packageManagerClient,
    void init(Looper looper, IPackageManager packageManager, PackageManager packageManagerClient,
@@ -3838,18 +3843,6 @@ public class NotificationManagerService extends SystemService {
        // Should this notification make noise, vibe, or use the LED?
        // Should this notification make noise, vibe, or use the LED?
        final boolean aboveThreshold =
        final boolean aboveThreshold =
                record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT;
                record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT;
        final boolean canInterrupt = aboveThreshold && !record.isIntercepted();
        if (DBG)
            Slog.v(TAG,
                    "pkg=" + record.sbn.getPackageName() + " canInterrupt=" + canInterrupt +
                            " intercept=" + record.isIntercepted()
            );

        // If we're not supposed to beep, vibrate, etc. then don't.
        final String disableEffects = disableNotificationEffects(record);
        if (disableEffects != null) {
            ZenLog.traceDisableEffects(record, disableEffects);
        }


        // Remember if this notification already owns the notification channels.
        // Remember if this notification already owns the notification channels.
        boolean wasBeep = key != null && key.equals(mSoundNotificationKey);
        boolean wasBeep = key != null && key.equals(mSoundNotificationKey);
@@ -3858,20 +3851,16 @@ public class NotificationManagerService extends SystemService {
        boolean hasValidVibrate = false;
        boolean hasValidVibrate = false;
        boolean hasValidSound = false;
        boolean hasValidSound = false;


        if (isNotificationForCurrentUser(record)) {
        if (aboveThreshold && isNotificationForCurrentUser(record)) {
            // If the notification will appear in the status bar, it should send an accessibility
            // If the notification will appear in the status bar, it should send an accessibility
            // event
            // event
            if (!record.isUpdate && record.getImportance() > IMPORTANCE_MIN) {
            if (!record.isUpdate && record.getImportance() > IMPORTANCE_MIN) {
                sendAccessibilityEvent(notification, record.sbn.getPackageName());
                sendAccessibilityEvent(notification, record.sbn.getPackageName());
            }
            }

            if (mSystemReady && mAudioManager != null) {
            if (disableEffects == null
                    && canInterrupt
                    && mSystemReady
                    && mAudioManager != null) {
                if (DBG) Slog.v(TAG, "Interrupting!");
                Uri soundUri = record.getSound();
                Uri soundUri = record.getSound();
                hasValidSound = soundUri != null && !Uri.EMPTY.equals(soundUri);
                hasValidSound = soundUri != null && !Uri.EMPTY.equals(soundUri);

                long[] vibration = record.getVibration();
                long[] vibration = record.getVibration();
                // Demote sound to vibration if vibration missing & phone in vibration mode.
                // Demote sound to vibration if vibration missing & phone in vibration mode.
                if (vibration == null
                if (vibration == null
@@ -3882,7 +3871,10 @@ public class NotificationManagerService extends SystemService {
                }
                }
                hasValidVibrate = vibration != null;
                hasValidVibrate = vibration != null;


                if (!shouldMuteNotificationLocked(record)) {
                boolean hasAudibleAlert = hasValidSound || hasValidVibrate;

                if (hasAudibleAlert && !shouldMuteNotificationLocked(record)) {
                    if (DBG) Slog.v(TAG, "Interrupting!");
                    if (hasValidSound) {
                    if (hasValidSound) {
                        mSoundNotificationKey = key;
                        mSoundNotificationKey = key;
                        if (mInCall) {
                        if (mInCall) {
@@ -3939,14 +3931,37 @@ public class NotificationManagerService extends SystemService {


    @GuardedBy("mNotificationLock")
    @GuardedBy("mNotificationLock")
    boolean shouldMuteNotificationLocked(final NotificationRecord record) {
    boolean shouldMuteNotificationLocked(final NotificationRecord record) {
        // Suppressed because it's a silent update
        final Notification notification = record.getNotification();
        final Notification notification = record.getNotification();
        if(record.isUpdate
        if(record.isUpdate
                && (notification.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) {
                && (notification.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) {
            return true;
            return true;
        }
        }

        // Suppressed for being too recently noisy
        final String pkg = record.sbn.getPackageName();
        if (mUsageStats.isAlertRateLimited(pkg)) {
            Slog.e(TAG, "Muting recently noisy " + record.getKey());
            return true;
        }

        // muted by listener
        final String disableEffects = disableNotificationEffects(record);
        if (disableEffects != null) {
            ZenLog.traceDisableEffects(record, disableEffects);
            return true;
        }

        // suppressed due to DND
        if (record.isIntercepted()) {
            return true;
        }

        // Suppressed because another notification in its group handles alerting
        if (record.sbn.isGroup()) {
        if (record.sbn.isGroup()) {
            return notification.suppressAlertingDueToGrouping();
            return notification.suppressAlertingDueToGrouping();
        }
        }

        return false;
        return false;
    }
    }


+30 −0
Original line number Original line Diff line number Diff line
@@ -113,6 +113,18 @@ public class NotificationUsageStats {
        }
        }
    }
    }


    /**
     * Called when a notification wants to alert.
     */
    public synchronized boolean isAlertRateLimited(String packageName) {
        AggregatedStats stats = getOrCreateAggregatedStatsLocked(packageName);
        if (stats != null) {
            return stats.isAlertRateLimited();
        } else {
            return false;
        }
    }

    /**
    /**
     * Called when a notification is tentatively enqueued by an app, before rate checking.
     * Called when a notification is tentatively enqueued by an app, before rate checking.
     */
     */
@@ -386,7 +398,9 @@ public class NotificationUsageStats {
        public ImportanceHistogram quietImportance;
        public ImportanceHistogram quietImportance;
        public ImportanceHistogram finalImportance;
        public ImportanceHistogram finalImportance;
        public RateEstimator enqueueRate;
        public RateEstimator enqueueRate;
        public AlertRateLimiter alertRate;
        public int numRateViolations;
        public int numRateViolations;
        public int numAlertViolations;
        public int numQuotaViolations;
        public int numQuotaViolations;
        public long mLastAccessTime;
        public long mLastAccessTime;


@@ -398,6 +412,7 @@ public class NotificationUsageStats {
            quietImportance = new ImportanceHistogram(context, "note_imp_quiet_");
            quietImportance = new ImportanceHistogram(context, "note_imp_quiet_");
            finalImportance = new ImportanceHistogram(context, "note_importance_");
            finalImportance = new ImportanceHistogram(context, "note_importance_");
            enqueueRate = new RateEstimator();
            enqueueRate = new RateEstimator();
            alertRate = new AlertRateLimiter();
        }
        }


        public AggregatedStats getPrevious() {
        public AggregatedStats getPrevious() {
@@ -510,6 +525,7 @@ public class NotificationUsageStats {
            maybeCount("note_sub_text", (numWithSubText - previous.numWithSubText));
            maybeCount("note_sub_text", (numWithSubText - previous.numWithSubText));
            maybeCount("note_info_text", (numWithInfoText - previous.numWithInfoText));
            maybeCount("note_info_text", (numWithInfoText - previous.numWithInfoText));
            maybeCount("note_over_rate", (numRateViolations - previous.numRateViolations));
            maybeCount("note_over_rate", (numRateViolations - previous.numRateViolations));
            maybeCount("note_over_alert_rate", (numAlertViolations - previous.numAlertViolations));
            maybeCount("note_over_quota", (numQuotaViolations - previous.numQuotaViolations));
            maybeCount("note_over_quota", (numQuotaViolations - previous.numQuotaViolations));
            noisyImportance.maybeCount(previous.noisyImportance);
            noisyImportance.maybeCount(previous.noisyImportance);
            quietImportance.maybeCount(previous.quietImportance);
            quietImportance.maybeCount(previous.quietImportance);
@@ -542,6 +558,7 @@ public class NotificationUsageStats {
            previous.numWithSubText = numWithSubText;
            previous.numWithSubText = numWithSubText;
            previous.numWithInfoText = numWithInfoText;
            previous.numWithInfoText = numWithInfoText;
            previous.numRateViolations = numRateViolations;
            previous.numRateViolations = numRateViolations;
            previous.numAlertViolations = numAlertViolations;
            previous.numQuotaViolations = numQuotaViolations;
            previous.numQuotaViolations = numQuotaViolations;
            noisyImportance.update(previous.noisyImportance);
            noisyImportance.update(previous.noisyImportance);
            quietImportance.update(previous.quietImportance);
            quietImportance.update(previous.quietImportance);
@@ -576,6 +593,14 @@ public class NotificationUsageStats {
            enqueueRate.update(now);
            enqueueRate.update(now);
        }
        }


        public boolean isAlertRateLimited() {
            boolean limited = alertRate.isRateLimited(SystemClock.elapsedRealtime());
            if (limited) {
                numAlertViolations++;
            }
            return limited;
        }

        private String toStringWithIndent(String indent) {
        private String toStringWithIndent(String indent) {
            StringBuilder output = new StringBuilder();
            StringBuilder output = new StringBuilder();
            output.append(indent).append("AggregatedStats{\n");
            output.append(indent).append("AggregatedStats{\n");
@@ -634,7 +659,11 @@ public class NotificationUsageStats {
            output.append("numWithSubText=").append(numWithSubText).append("\n");
            output.append("numWithSubText=").append(numWithSubText).append("\n");
            output.append(indentPlusTwo);
            output.append(indentPlusTwo);
            output.append("numWithInfoText=").append(numWithInfoText).append("\n");
            output.append("numWithInfoText=").append(numWithInfoText).append("\n");
            output.append(indentPlusTwo);
            output.append("numRateViolations=").append(numRateViolations).append("\n");
            output.append("numRateViolations=").append(numRateViolations).append("\n");
            output.append(indentPlusTwo);
            output.append("numAlertViolations=").append(numAlertViolations).append("\n");
            output.append(indentPlusTwo);
            output.append("numQuotaViolations=").append(numQuotaViolations).append("\n");
            output.append("numQuotaViolations=").append(numQuotaViolations).append("\n");
            output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n");
            output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n");
            output.append(indentPlusTwo).append(quietImportance.toString()).append("\n");
            output.append(indentPlusTwo).append(quietImportance.toString()).append("\n");
@@ -677,6 +706,7 @@ public class NotificationUsageStats {
            maybePut(dump, "numRateViolations", numRateViolations);
            maybePut(dump, "numRateViolations", numRateViolations);
            maybePut(dump, "numQuotaLViolations", numQuotaViolations);
            maybePut(dump, "numQuotaLViolations", numQuotaViolations);
            maybePut(dump, "notificationEnqueueRate", getEnqueueRate());
            maybePut(dump, "notificationEnqueueRate", getEnqueueRate());
            maybePut(dump, "numAlertViolations", numAlertViolations);
            noisyImportance.maybePut(dump, previous.noisyImportance);
            noisyImportance.maybePut(dump, previous.noisyImportance);
            quietImportance.maybePut(dump, previous.quietImportance);
            quietImportance.maybePut(dump, previous.quietImportance);
            finalImportance.maybePut(dump, previous.finalImportance);
            finalImportance.maybePut(dump, previous.finalImportance);
+72 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2016 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;

import static com.android.server.notification.AlertRateLimiter.ALLOWED_ALERT_INTERVAL;

import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;

import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class AlertRateLimiterTest extends NotificationTestCase {

    private long mTestStartTime;
    private
    AlertRateLimiter mLimiter;

    @Before
    public void setUp() {
        mTestStartTime = 1225731600000L;
        mLimiter = new AlertRateLimiter();
    }

    @Test
    public void testFirstAlertAllowed() throws Exception {
        assertFalse(mLimiter.isRateLimited(mTestStartTime));
    }

    @Test
    public void testAllowedAfterSecond() throws Exception {
        assertFalse(mLimiter.isRateLimited(mTestStartTime));
        assertFalse(mLimiter.isRateLimited(mTestStartTime + ALLOWED_ALERT_INTERVAL));
    }

    @Test
    public void testAllowedAfterSecondEvenWithBlockedEntries() throws Exception {
        assertFalse(mLimiter.isRateLimited(mTestStartTime));
        assertTrue(mLimiter.isRateLimited(mTestStartTime + ALLOWED_ALERT_INTERVAL - 1));
        assertFalse(mLimiter.isRateLimited(mTestStartTime + ALLOWED_ALERT_INTERVAL));
    }

    @Test
    public void testAllowedDisallowedBeforeSecond() throws Exception {
        assertFalse(mLimiter.isRateLimited(mTestStartTime));
        assertTrue(mLimiter.isRateLimited(mTestStartTime + ALLOWED_ALERT_INTERVAL - 1));
    }

    @Test
    public void testDisallowedTimePast() throws Exception {
        assertFalse(mLimiter.isRateLimited(mTestStartTime));
        assertTrue(mLimiter.isRateLimited(mTestStartTime - ALLOWED_ALERT_INTERVAL));
    }
}
Loading