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

Commit 882d3e88 authored by Jan Tomljanovic's avatar Jan Tomljanovic Committed by Android (Google) Code Review
Browse files

Merge changes from topic "154198299"

* changes:
  Implement rate limiting toasts.
  Implement multi rate limiter.
parents 9e6d4107 09027467
Loading
Loading
Loading
Loading
+51 −3
Original line number Diff line number Diff line
@@ -277,6 +277,7 @@ import com.android.server.pm.PackageManagerService;
import com.android.server.policy.PhoneWindowManager;
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.uri.UriGrantsManagerInternal;
import com.android.server.utils.quota.MultiRateLimiter;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.BackgroundActivityStartCallback;
import com.android.server.wm.WindowManagerInternal;
@@ -298,6 +299,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@@ -371,6 +373,20 @@ public class NotificationManagerService extends SystemService {
            RoleManager.ROLE_EMERGENCY
    };

    // Used for rate limiting toasts by package.
    static final String TOAST_QUOTA_TAG = "toast_quota_tag";

    // This constant defines rate limits applied to showing toasts. The numbers are set in a way
    // such that an aggressive toast showing strategy would result in a roughly 1.5x longer wait
    // time (before the package is allowed to show toasts again) each time the toast rate limit is
    // reached. It's meant to protect the user against apps spamming them with toasts (either
    // accidentally or on purpose).
    private static final MultiRateLimiter.RateLimit[] TOAST_RATE_LIMITS = {
            MultiRateLimiter.RateLimit.create(3, Duration.ofSeconds(20)),
            MultiRateLimiter.RateLimit.create(5, Duration.ofSeconds(42)),
            MultiRateLimiter.RateLimit.create(6, Duration.ofSeconds(68)),
    };

    // When #matchesCallFilter is called from the ringer, wait at most
    // 3s to resolve the contacts. This timeout is required since
    // ContactsProvider might take a long time to start up.
@@ -422,6 +438,16 @@ public class NotificationManagerService extends SystemService {
    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R)
    private static final long NOTIFICATION_TRAMPOLINE_BLOCK = 167676448L;

    /**
     * Rate limit showing toasts, on a per package basis.
     *
     * It limits the effects of {@link android.widget.Toast#show()} calls to prevent overburdening
     * the user with too many toasts in a limited time. Any attempt to show more toasts than allowed
     * in a certain time frame will result in the toast being discarded.
     */
    @ChangeId
    private static final long RATE_LIMIT_TOASTS = 154198299L;

    private IActivityManager mAm;
    private ActivityTaskManagerInternal mAtm;
    private ActivityManager mActivityManager;
@@ -500,6 +526,9 @@ public class NotificationManagerService extends SystemService {
    @GuardedBy("mToastQueue")
    private boolean mIsCurrentToastShown = false;

    // Used for rate limiting toasts by package.
    private MultiRateLimiter mToastRateLimiter;

    // The last key in this list owns the hardware.
    ArrayList<String> mLights = new ArrayList<>();

@@ -1915,7 +1944,8 @@ public class NotificationManagerService extends SystemService {
            DevicePolicyManagerInternal dpm, IUriGrantsManager ugm,
            UriGrantsManagerInternal ugmInternal, AppOpsManager appOps, UserManager userManager,
            NotificationHistoryManager historyManager, StatsManager statsManager,
            TelephonyManager telephonyManager, ActivityManagerInternal ami) {
            TelephonyManager telephonyManager, ActivityManagerInternal ami,
            MultiRateLimiter toastRateLimiter) {
        mHandler = handler;
        Resources resources = getContext().getResources();
        mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(),
@@ -2107,6 +2137,8 @@ public class NotificationManagerService extends SystemService {
                com.android.internal.R.array.config_notificationMsgPkgsAllowedAsConvos));
        mStatsManager = statsManager;

        mToastRateLimiter = toastRateLimiter;

        // register for various Intents.
        // If this is called within a test, make sure to unregister the intent receivers by
        // calling onDestroy()
@@ -2217,7 +2249,8 @@ public class NotificationManagerService extends SystemService {
                mStatsManager = (StatsManager) getContext().getSystemService(
                        Context.STATS_MANAGER),
                getContext().getSystemService(TelephonyManager.class),
                LocalServices.getService(ActivityManagerInternal.class));
                LocalServices.getService(ActivityManagerInternal.class),
                createToastRateLimiter());

        publishBinderService(Context.NOTIFICATION_SERVICE, mService, /* allowIsolated= */ false,
                DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL);
@@ -2855,6 +2888,10 @@ public class NotificationManagerService extends SystemService {
        return mInternalService;
    }

    private MultiRateLimiter createToastRateLimiter() {
        return new MultiRateLimiter.Builder(getContext()).addRateLimits(TOAST_RATE_LIMITS).build();
    }

    @VisibleForTesting
    final IBinder mService = new INotificationManager.Stub() {
        // Toasts
@@ -7329,10 +7366,21 @@ public class NotificationManagerService extends SystemService {

        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            if (record.show()) {
            int userId = UserHandle.getUserId(record.uid);
            boolean rateLimitingEnabled =
                    CompatChanges.isChangeEnabled(RATE_LIMIT_TOASTS, record.uid);
            boolean isWithinQuota =
                    mToastRateLimiter.isWithinQuota(userId, record.pkg, TOAST_QUOTA_TAG);
            if ((!rateLimitingEnabled || isWithinQuota) && record.show()) {
                scheduleDurationReachedLocked(record);
                mIsCurrentToastShown = true;
                if (rateLimitingEnabled) {
                    mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG);
                }
                return;
            } else if (rateLimitingEnabled && !isWithinQuota) {
                Slog.w(TAG, "Package " + record.pkg + " is above allowed toast quota, the "
                        + "following toast was blocked and discarded: " + record);
            }
            int index = mToastQueue.indexOf(record);
            if (index >= 0) {
+183 −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.utils.quota;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

/**
 * Can be used to rate limit events per app based on multiple rates at the same time. For example,
 * it can limit an event to happen only:
 *
 * <li>5 times in 20 seconds</li>
 * and
 * <li>6 times in 40 seconds</li>
 * and
 * <li>10 times in 1 hour</li>
 *
 * <p><br>
 * All listed rates apply at the same time, and the UPTC will be out of quota if it doesn't satisfy
 * all the given rates. The underlying mechanism used is
 * {@link com.android.server.utils.quota.CountQuotaTracker}, so all its conditions apply, as well
 * as an additional constraint: all the user-package-tag combinations (UPTC) are considered to be in
 * the same {@link com.android.server.utils.quota.Category}.
 * </p>
 *
 * @hide
 */
public class MultiRateLimiter {

    private static final CountQuotaTracker[] EMPTY_TRACKER_ARRAY = {};

    private final Object mLock = new Object();
    @GuardedBy("mLock")
    private final CountQuotaTracker[] mQuotaTrackers;

    private MultiRateLimiter(List<CountQuotaTracker> quotaTrackers) {
        mQuotaTrackers = quotaTrackers.toArray(EMPTY_TRACKER_ARRAY);
    }

    /** Record that an event happened and count it towards the given quota. */
    public void noteEvent(int userId, @NonNull String packageName, @Nullable String tag) {
        synchronized (mLock) {
            noteEventLocked(userId, packageName, tag);
        }
    }

    /** Check whether the given UPTC is allowed to trigger an event. */
    public boolean isWithinQuota(int userId, @NonNull String packageName, @Nullable String tag) {
        synchronized (mLock) {
            return isWithinQuotaLocked(userId, packageName, tag);
        }
    }

    @GuardedBy("mLock")
    private void noteEventLocked(int userId, @NonNull String packageName, @Nullable String tag) {
        for (CountQuotaTracker quotaTracker : mQuotaTrackers) {
            quotaTracker.noteEvent(userId, packageName, tag);
        }
    }

    @GuardedBy("mLock")
    private boolean isWithinQuotaLocked(int userId, @NonNull String packageName,
            @Nullable String tag) {
        for (CountQuotaTracker quotaTracker : mQuotaTrackers) {
            if (!quotaTracker.isWithinQuota(userId, packageName, tag)) {
                return false;
            }
        }
        return true;
    }

    /** Can create a new {@link MultiRateLimiter}. */
    public static class Builder {

        private final List<CountQuotaTracker> mQuotaTrackers;
        private final Context mContext;
        private final Categorizer mCategorizer;
        private final Category mCategory;
        @Nullable private final QuotaTracker.Injector mInjector;

        /**
         * Creates a new builder and allows to inject an object that can be used
         * to manipulate elapsed time in tests.
         */
        @VisibleForTesting
        Builder(Context context, QuotaTracker.Injector injector) {
            this.mQuotaTrackers = new ArrayList<>();
            this.mContext = context;
            this.mInjector = injector;
            this.mCategorizer = Categorizer.SINGLE_CATEGORIZER;
            this.mCategory = Category.SINGLE_CATEGORY;
        }

        /** Creates a new builder for {@link MultiRateLimiter}. */
        public Builder(Context context) {
            this(context, null);
        }

        /**
         * Adds another rate limit to be used in {@link MultiRateLimiter}.
         *
         * @param limit The maximum event count an app can have in the rolling time window.
         * @param windowSize The rolling time window to use when checking quota usage.
         */
        public Builder addRateLimit(int limit, Duration windowSize) {
            CountQuotaTracker countQuotaTracker;
            if (mInjector != null) {
                countQuotaTracker = new CountQuotaTracker(mContext, mCategorizer, mInjector);
            } else {
                countQuotaTracker = new CountQuotaTracker(mContext, mCategorizer);
            }
            countQuotaTracker.setCountLimit(mCategory, limit, windowSize.toMillis());
            mQuotaTrackers.add(countQuotaTracker);
            return this;
        }

        /** Adds another rate limit to be used in {@link MultiRateLimiter}. */
        public Builder addRateLimit(@NonNull RateLimit rateLimit) {
            return addRateLimit(rateLimit.mLimit, rateLimit.mWindowSize);
        }

        /** Adds all given rate limits that will be used in {@link MultiRateLimiter}. */
        public Builder addRateLimits(@NonNull RateLimit[] rateLimits) {
            for (RateLimit rateLimit : rateLimits) {
                addRateLimit(rateLimit);
            }
            return this;
        }

        /**
         * Return a new {@link com.android.server.utils.quota.MultiRateLimiter} using set rate
         * limit.
         */
        public MultiRateLimiter build() {
            return new MultiRateLimiter(mQuotaTrackers);
        }
    }

    /** Helper class that describes a rate limit. */
    public static class RateLimit {
        public final int mLimit;
        public final Duration mWindowSize;

        /**
         * @param limit The maximum count of some occurrence in the rolling time window.
         * @param windowSize The rolling time window to use when checking quota usage.
         */
        private RateLimit(int limit, Duration windowSize) {
            this.mLimit = limit;
            this.mWindowSize = windowSize;
        }

        /**
         * @param limit The maximum count of some occurrence in the rolling time window.
         * @param windowSize The rolling time window to use when checking quota usage.
         */
        public static RateLimit create(int limit, Duration windowSize) {
            return new RateLimit(limit, windowSize);
        }
    }
}
+197 −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.utils.quota;

import static com.google.common.truth.Truth.assertThat;

import android.testing.TestableContext;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;

import org.junit.Rule;
import org.junit.Test;

import java.time.Duration;

@SmallTest
public class MultiRateLimiterTest {

    private static final int USER_ID = 1;
    private static final String PACKAGE_NAME_1 = "com.android.package.one";
    private static final String PACKAGE_NAME_2 = "com.android.package.two";
    private static final String TAG = "tag";

    @Rule
    public final TestableContext mContext =
            new TestableContext(InstrumentationRegistry.getContext(), null);

    private final InjectorForTest mInjector = new InjectorForTest();

    private static class InjectorForTest extends QuotaTracker.Injector {
        Duration mElapsedTime = Duration.ZERO;

        @Override
        public long getElapsedRealtime() {
            return mElapsedTime.toMillis();
        }

        @Override
        public boolean isAlarmManagerReady() {
            return true;
        }
    }

    @Test
    public void testSingleRateLimit_belowLimit_isWithinQuota() {
        MultiRateLimiter multiRateLimiter =  new MultiRateLimiter.Builder(mContext, mInjector)
                .addRateLimit(3, Duration.ofSeconds(20))
                .build();

        // Three quick events are within quota.
        mInjector.mElapsedTime = Duration.ZERO;
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(50);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(100);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
    }

    @Test
    public void testSingleRateLimit_aboveLimit_isNotWithinQuota() {
        MultiRateLimiter multiRateLimiter =  new MultiRateLimiter.Builder(mContext, mInjector)
                .addRateLimit(3, Duration.ofSeconds(20))
                .build();

        mInjector.mElapsedTime = Duration.ZERO;
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(50);
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(100);
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(150);
        // We hit the limit, 4th event in under 20 seconds is not within quota.
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isFalse();
    }

    @Test
    public void testSingleRateLimit_afterGoingAboveQuotaAndWaitingWindow_isBackWithinQuota() {
        MultiRateLimiter multiRateLimiter =  new MultiRateLimiter.Builder(mContext, mInjector)
                .addRateLimit(3, Duration.ofSeconds(20))
                .build();

        mInjector.mElapsedTime = Duration.ZERO;
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(50);
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(100);
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(150);
        // We hit the limit, 4th event in under 20 seconds is not within quota.
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isFalse();

        mInjector.mElapsedTime = Duration.ofSeconds(21);
        // 20 seconds have passed, we're again within quota.
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
    }

    @Test
    public void createMultipleRateLimits_testTheyLimitsAsExpected() {
        MultiRateLimiter multiRateLimiter = new MultiRateLimiter.Builder(mContext, mInjector)
                .addRateLimit(3, Duration.ofSeconds(20)) // 1st limit
                .addRateLimit(4, Duration.ofSeconds(40)) // 2nd limit
                .addRateLimit(5, Duration.ofSeconds(60)) // 3rd limit
                .build();

        // Testing the 1st limit
        mInjector.mElapsedTime = Duration.ZERO;
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(50);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(100);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(150);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isFalse();

        mInjector.mElapsedTime = Duration.ofSeconds(21);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        // Testing the 2nd limit
        mInjector.mElapsedTime = Duration.ofSeconds(35);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isFalse();

        mInjector.mElapsedTime = Duration.ofSeconds(42);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        // Testing the 3rd limit.
        mInjector.mElapsedTime = Duration.ofSeconds(43);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isFalse();

        mInjector.mElapsedTime = Duration.ofSeconds(62);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);
    }

    @Test
    public void createSingleRateLimit_testItLimitsOnlyGivenUptc() {
        MultiRateLimiter multiRateLimiter =  new MultiRateLimiter.Builder(mContext, mInjector)
                .addRateLimit(3, Duration.ofSeconds(20))
                .build();

        mInjector.mElapsedTime = Duration.ZERO;
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_2, TAG)).isTrue();
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(50);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_2, TAG)).isTrue();
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(100);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_2, TAG)).isTrue();
        multiRateLimiter.noteEvent(USER_ID, PACKAGE_NAME_1, TAG);

        mInjector.mElapsedTime = Duration.ofMillis(150);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isFalse();
        // Different userId - packageName - tag combination is still allowed.
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_2, TAG)).isTrue();

        mInjector.mElapsedTime = Duration.ofSeconds(21);
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_1, TAG)).isTrue();
        assertThat(multiRateLimiter.isWithinQuota(USER_ID, PACKAGE_NAME_2, TAG)).isTrue();
    }
}
+67 −2

File changed.

Preview size limit exceeded, changes collapsed.

+3 −1
Original line number Diff line number Diff line
@@ -67,6 +67,7 @@ import com.android.server.lights.LightsManager;
import com.android.server.notification.NotificationManagerService.NotificationAssistants;
import com.android.server.notification.NotificationManagerService.NotificationListeners;
import com.android.server.uri.UriGrantsManagerInternal;
import com.android.server.utils.quota.MultiRateLimiter;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.WindowManagerInternal;

@@ -156,7 +157,8 @@ public class RoleObserverTest extends UiServiceTestCase {
                    mock(UriGrantsManagerInternal.class),
                    mock(AppOpsManager.class), mUm, mock(NotificationHistoryManager.class),
                    mock(StatsManager.class), mock(TelephonyManager.class),
                    mock(ActivityManagerInternal.class));
                    mock(ActivityManagerInternal.class),
                    mock(MultiRateLimiter.class));
        } catch (SecurityException e) {
            if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
                throw e;