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

Commit fa97060c authored by Hai Zhang's avatar Hai Zhang
Browse files

Throttle calls to grant default roles.

Package changes can be very frequent when for instance a new user is
created, and we used to re-run granting default roles every time a
package change is detected. In such cases the grant is inherently
async, so we can throttle it with a certain interval for better
performance.

This CL adds a throttled runnable and makes the async call in
RoleManagerService package change receiver throttled with an interval
of 1 second. The package hash computation is also moved to FgThread in
this case. The original blocking call in onStartUser() is not
affected and still runs on main thread.

Test: manual
Change-Id: I211ee3d4acbdf0662c6dfe4d67b35d253e12a472
parent 95eab8ae
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -115,6 +115,20 @@ public class AndroidFuture<T> extends CompletableFuture<T> implements Parcelable
        }
    }

    /**
     * Create a completed future with the given value.
     *
     * @param value the value for the completed future
     * @param <U> the type of the value
     * @return the completed future
     */
    @NonNull
    public static <U> AndroidFuture<U> completedFuture(U value) {
        AndroidFuture<U> future = new AndroidFuture<>();
        future.complete(value);
        return future;
    }

    @Override
    public boolean complete(@Nullable T value) {
        boolean changed = super.complete(value);
+75 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.internal.infra;

import android.annotation.NonNull;
import android.os.Handler;
import android.os.SystemClock;

import com.android.internal.annotations.GuardedBy;

/**
 * A throttled runnable that can wrap around a runnable and throttle calls to its run().
 *
 * The throttling logic makes sure that the original runnable will be called only after the
 * specified interval passes since the last actual call. The first call in a while (after the
 * specified interval passes since the last actual call) will always result in the original runnable
 * being called immediately, and then subsequent calls will start to be throttled. It is guaranteed
 * that any call to this throttled runnable will always result in the original runnable being called
 * afterwards, within the specified interval.
 */
public class ThrottledRunnable implements Runnable {

    @NonNull
    private final Handler mHandler;
    private final long mIntervalMillis;
    @NonNull
    private final Runnable mRunnable;

    @NonNull
    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private long mScheduledUptimeMillis;

    public ThrottledRunnable(@NonNull Handler handler, long intervalMillis,
            @NonNull Runnable runnable) {
        mHandler = handler;
        mIntervalMillis = intervalMillis;
        mRunnable = runnable;
    }

    @Override
    public void run() {
        synchronized (mLock) {
            if (mHandler.hasCallbacks(mRunnable)) {
                // We have a scheduled runnable.
                return;
            }
            long currentUptimeMillis = SystemClock.uptimeMillis();
            if (mScheduledUptimeMillis == 0
                    || currentUptimeMillis > mScheduledUptimeMillis + mIntervalMillis) {
                // First time in a while, schedule immediately.
                mScheduledUptimeMillis = currentUptimeMillis;
            } else {
                // We were scheduled not long ago, so schedule with delay for throttling.
                mScheduledUptimeMillis = mScheduledUptimeMillis + mIntervalMillis;
            }
            mHandler.postAtTime(mRunnable, mScheduledUptimeMillis);
        }
    }
}
+73 −49
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.server.role;

import android.Manifest;
import android.annotation.AnyThread;
import android.annotation.CheckResult;
import android.annotation.MainThread;
import android.annotation.NonNull;
@@ -49,7 +50,6 @@ import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.UserHandle;
import android.os.UserManagerInternal;
import android.provider.Telephony;
import android.service.sms.FinancialSmsService;
import android.telephony.IFinancialSmsCallback;
import android.text.TextUtils;
@@ -61,6 +61,8 @@ import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.infra.ThrottledRunnable;
import com.android.internal.telephony.SmsApplication;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.BitUtils;
@@ -82,7 +84,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -99,6 +100,8 @@ public class RoleManagerService extends SystemService implements RoleUserState.C

    private static final boolean DEBUG = false;

    private static final long GRANT_DEFAULT_ROLES_INTERVAL_MILLIS = 1000;

    @NonNull
    private final UserManagerInternal mUserManagerInternal;
    @NonNull
@@ -142,6 +145,14 @@ public class RoleManagerService extends SystemService implements RoleUserState.C
    @NonNull
    private final Handler mListenerHandler = FgThread.getHandler();

    /**
     * Maps user id to its throttled runnable for granting default roles.
     */
    @GuardedBy("mLock")
    @NonNull
    private final SparseArray<ThrottledRunnable> mGrantDefaultRolesThrottledRunnables =
            new SparseArray<>();

    public RoleManagerService(@NonNull Context context,
            @NonNull RoleHoldersResolver legacyRoleResolver) {
        super(context);
@@ -182,8 +193,6 @@ public class RoleManagerService extends SystemService implements RoleUserState.C
    public void onStart() {
        publishBinderService(Context.ROLE_SERVICE, new Stub());

        //TODO add watch for new user creation and run default grants for them

        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
        intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
@@ -203,64 +212,78 @@ public class RoleManagerService extends SystemService implements RoleUserState.C
                    // Package is being upgraded - we're about to get ACTION_PACKAGE_ADDED
                    return;
                }
                performInitialGrantsIfNecessaryAsync(userId);
                maybeGrantDefaultRolesAsync(userId);
            }
        }, UserHandle.ALL, intentFilter, null, null);
    }

    @Override
    public void onStartUser(@UserIdInt int userId) {
        performInitialGrantsIfNecessary(userId);
        maybeGrantDefaultRolesSync(userId);
    }

    private CompletableFuture<Void> performInitialGrantsIfNecessaryAsync(@UserIdInt int userId) {
        RoleUserState userState;
        userState = getOrCreateUserState(userId);
    @MainThread
    private void maybeGrantDefaultRolesSync(@UserIdInt int userId) {
        AndroidFuture<Void> future = maybeGrantDefaultRolesInternal(userId);
        try {
            future.get(30, TimeUnit.SECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            Slog.e(LOG_TAG, "Failed to grant default roles for user " + userId, e);
        }
    }

        String packagesHash = computeComponentStateHash(userId);
    private void maybeGrantDefaultRolesAsync(@UserIdInt int userId) {
        ThrottledRunnable runnable;
        synchronized (mLock) {
            runnable = mGrantDefaultRolesThrottledRunnables.get(userId);
            if (runnable == null) {
                runnable = new ThrottledRunnable(FgThread.getHandler(),
                        GRANT_DEFAULT_ROLES_INTERVAL_MILLIS,
                        () -> maybeGrantDefaultRolesInternal(userId));
                mGrantDefaultRolesThrottledRunnables.put(userId, runnable);
            }
        }
        runnable.run();
    }

    @AnyThread
    @NonNull
    private AndroidFuture<Void> maybeGrantDefaultRolesInternal(@UserIdInt int userId) {
        RoleUserState userState = getOrCreateUserState(userId);
        String oldPackagesHash = userState.getPackagesHash();
        boolean needGrant = !Objects.equals(packagesHash, oldPackagesHash);
        if (needGrant) {
        String newPackagesHash = computeComponentStateHash(userId);
        if (Objects.equals(oldPackagesHash, newPackagesHash)) {
            if (DEBUG) {
                Slog.i(LOG_TAG, "Already granted default roles for packages hash "
                        + newPackagesHash);
            }
            return AndroidFuture.completedFuture(null);
        }

        //TODO gradually add more role migrations statements here for remaining roles
        // Make sure to implement LegacyRoleResolutionPolicy#getRoleHolders
        // for a given role before adding a migration statement for it here
            migrateRoleIfNecessary(RoleManager.ROLE_SMS, userId);
            migrateRoleIfNecessary(RoleManager.ROLE_ASSISTANT, userId);
            migrateRoleIfNecessary(RoleManager.ROLE_DIALER, userId);
            migrateRoleIfNecessary(RoleManager.ROLE_EMERGENCY, userId);

            // Some vital packages state has changed since last role grant
            // Run grants again
            Slog.i(LOG_TAG, "Granting default permissions...");
            CompletableFuture<Void> result = new CompletableFuture<>();
        maybeMigrateRole(RoleManager.ROLE_SMS, userId);
        maybeMigrateRole(RoleManager.ROLE_ASSISTANT, userId);
        maybeMigrateRole(RoleManager.ROLE_DIALER, userId);
        maybeMigrateRole(RoleManager.ROLE_EMERGENCY, userId);

        // Some package state has changed, so grant default roles again.
        Slog.i(LOG_TAG, "Granting default roles...");
        AndroidFuture<Void> future = new AndroidFuture<>();
        getOrCreateController(userId).grantDefaultRoles(FgThread.getExecutor(),
                successful -> {
                    if (successful) {
                            userState.setPackagesHash(packagesHash);
                            result.complete(null);
                        userState.setPackagesHash(newPackagesHash);
                        future.complete(null);
                    } else {
                            result.completeExceptionally(new RuntimeException());
                        future.completeExceptionally(new RuntimeException());
                    }
                });
            return result;
        } else if (DEBUG) {
            Slog.i(LOG_TAG, "Already ran grants for package state " + packagesHash);
        }
        return CompletableFuture.completedFuture(null);
    }

    @MainThread
    private void performInitialGrantsIfNecessary(@UserIdInt int userId) {
        CompletableFuture<Void> result = performInitialGrantsIfNecessaryAsync(userId);
        try {
            result.get(30, TimeUnit.SECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            Slog.e(LOG_TAG, "Failed to grant defaults for user " + userId, e);
        }
        return future;
    }

    private void migrateRoleIfNecessary(String role, @UserIdInt int userId) {
    private void maybeMigrateRole(String role, @UserIdInt int userId) {
        // Any role for which we have a record are already migrated
        RoleUserState userState = getOrCreateUserState(userId);
        if (!userState.isRoleAvailable(role)) {
@@ -366,6 +389,7 @@ public class RoleManagerService extends SystemService implements RoleUserState.C
        RemoteCallbackList<IOnRoleHoldersChangedListener> listeners;
        RoleUserState userState;
        synchronized (mLock) {
            mGrantDefaultRolesThrottledRunnables.remove(userId);
            listeners = mListeners.removeReturnOld(userId);
            mControllers.remove(userId);
            userState = mUserStates.removeReturnOld(userId);
@@ -742,7 +766,7 @@ public class RoleManagerService extends SystemService implements RoleUserState.C

        @Override
        public boolean setDefaultBrowser(@Nullable String packageName, @UserIdInt int userId) {
            CompletableFuture<Void> future = new CompletableFuture<>();
            AndroidFuture<Void> future = new AndroidFuture<>();
            RemoteCallback callback = new RemoteCallback(result -> {
                boolean successful = result != null;
                if (successful) {