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

Commit d50a3ea1 authored by Yuting Fang's avatar Yuting Fang Committed by Android (Google) Code Review
Browse files

Merge "Cache AppOp mode to reduce binder calls to the system server" into main

parents 53b2c23a 85fa087d
Loading
Loading
Loading
Loading
+146 −10
Original line number Diff line number Diff line
@@ -63,6 +63,7 @@ import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.IpcDataCache;
import android.os.Looper;
import android.os.PackageTagsList;
import android.os.Parcel;
@@ -78,12 +79,14 @@ import android.permission.PermissionGroupUsage;
import android.permission.PermissionUsageHelper;
import android.permission.flags.Flags;
import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.LongSparseArray;
import android.util.LongSparseLongArray;
import android.util.Pools;
import android.util.SparseArray;
import android.util.SparseBooleanArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.Immutable;
@@ -7797,6 +7800,116 @@ public class AppOpsManager {
        }
    }

    private static final String APP_OP_MODE_CACHING_API = "getAppOpMode";
    private static final String APP_OP_MODE_CACHING_NAME = "appOpModeCache";
    private static final int APP_OP_MODE_CACHING_SIZE = 2048;

    private static final IpcDataCache.QueryHandler<AppOpModeQuery, Integer> sGetAppOpModeQuery =
            new IpcDataCache.QueryHandler<>() {
                @Override
                public Integer apply(AppOpModeQuery query) {
                    IAppOpsService service = getService();
                    try {
                        return service.checkOperationRawForDevice(query.op, query.uid,
                                query.packageName, query.attributionTag, query.virtualDeviceId);
                    } catch (RemoteException e) {
                        throw e.rethrowFromSystemServer();
                    }
                }

                @Override
                public boolean shouldBypassCache(@NonNull AppOpModeQuery query) {
                    // If the flag to enable the new caching behavior is off, bypass the cache.
                    return !Flags.appopModeCachingEnabled();
                }
            };

    // A LRU cache on binder clients that caches AppOp mode by uid, packageName, virtualDeviceId
    // and attributionTag.
    private static final IpcDataCache<AppOpModeQuery, Integer> sAppOpModeCache =
            new IpcDataCache<>(APP_OP_MODE_CACHING_SIZE, IpcDataCache.MODULE_SYSTEM,
                    APP_OP_MODE_CACHING_API, APP_OP_MODE_CACHING_NAME, sGetAppOpModeQuery);

    // Ops that we don't want to cache due to:
    // 1) Discrepancy of attributionTag support in checkOp and noteOp that determines if a package
    //    can bypass user restriction of an op: b/240617242. COARSE_LOCATION and FINE_LOCATION are
    //    the only two ops that are impacted.
    private static final SparseBooleanArray OPS_WITHOUT_CACHING = new SparseBooleanArray();
    static {
        OPS_WITHOUT_CACHING.put(OP_COARSE_LOCATION, true);
        OPS_WITHOUT_CACHING.put(OP_FINE_LOCATION, true);
    }

    private static boolean isAppOpModeCachingEnabled(int opCode) {
        if (!Flags.appopModeCachingEnabled()) {
            return false;
        }
        return !OPS_WITHOUT_CACHING.get(opCode, false);
    }

    /**
     * @hide
     */
    public static void invalidateAppOpModeCache() {
        if (Flags.appopModeCachingEnabled()) {
            IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM, APP_OP_MODE_CACHING_API);
        }
    }

    /**
     * Bypass AppOpModeCache in the local process
     *
     * @hide
     */
    public static void disableAppOpModeCache() {
        if (Flags.appopModeCachingEnabled()) {
            sAppOpModeCache.disableLocal();
        }
    }

    private static final class AppOpModeQuery {
        final int op;
        final int uid;
        final String packageName;
        final int virtualDeviceId;
        final String attributionTag;
        final String methodName;

        AppOpModeQuery(int op, int uid, @Nullable String packageName, int virtualDeviceId,
                @Nullable String attributionTag, @Nullable String methodName) {
            this.op = op;
            this.uid = uid;
            this.packageName = packageName;
            this.virtualDeviceId = virtualDeviceId;
            this.attributionTag = attributionTag;
            this.methodName = methodName;
        }

        @Override
        public String toString() {
            return TextUtils.formatSimple("AppOpModeQuery(op=%d, uid=%d, packageName=%s, "
                            + "virtualDeviceId=%d, attributionTag=%s, methodName=%s", op, uid,
                    packageName, virtualDeviceId, attributionTag, methodName);
        }

        @Override
        public int hashCode() {
            return Objects.hash(op, uid, packageName, virtualDeviceId, attributionTag);
        }

        @Override
        public boolean equals(@Nullable Object o) {
            if (this == o) return true;
            if (o == null) return false;
            if (this.getClass() != o.getClass()) return false;

            AppOpModeQuery other = (AppOpModeQuery) o;
            return op == other.op && uid == other.uid && Objects.equals(packageName,
                    other.packageName) && virtualDeviceId == other.virtualDeviceId
                    && Objects.equals(attributionTag, other.attributionTag);
        }
    }

    AppOpsManager(Context context, IAppOpsService service) {
        mContext = context;
        mService = service;
@@ -8851,12 +8964,16 @@ public class AppOpsManager {
    private int unsafeCheckOpRawNoThrow(int op, int uid, @NonNull String packageName,
            int virtualDeviceId) {
        try {
            if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
                return mService.checkOperationRaw(op, uid, packageName, null);
            int mode;
            if (isAppOpModeCachingEnabled(op)) {
                mode = sAppOpModeCache.query(
                        new AppOpModeQuery(op, uid, packageName, virtualDeviceId, null,
                                "unsafeCheckOpRawNoThrow"));
            } else {
                return mService.checkOperationRawForDevice(
                mode = mService.checkOperationRawForDevice(
                        op, uid, packageName, null, virtualDeviceId);
            }
            return mode;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
@@ -9284,8 +9401,21 @@ public class AppOpsManager {
    @UnsupportedAppUsage
    public int checkOp(int op, int uid, String packageName) {
        try {
            int mode = mService.checkOperationForDevice(op, uid, packageName,
            int mode;
            if (isAppOpModeCachingEnabled(op)) {
                mode = sAppOpModeCache.query(
                        new AppOpModeQuery(op, uid, packageName, Context.DEVICE_ID_DEFAULT, null,
                                "checkOp"));
                if (mode == MODE_FOREGROUND) {
                    // We only cache raw mode. If the mode is FOREGROUND, we need another binder
                    // call to fetch translated value based on the process state.
                    mode = mService.checkOperationForDevice(op, uid, packageName,
                            Context.DEVICE_ID_DEFAULT);
                }
            } else {
                mode = mService.checkOperationForDevice(op, uid, packageName,
                        Context.DEVICE_ID_DEFAULT);
            }
            if (mode == MODE_ERRORED) {
                throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName));
            }
@@ -9324,13 +9454,19 @@ public class AppOpsManager {
    private int checkOpNoThrow(int op, int uid, String packageName, int virtualDeviceId) {
        try {
            int mode;
            if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) {
                mode = mService.checkOperation(op, uid, packageName);
            if (isAppOpModeCachingEnabled(op)) {
                mode = sAppOpModeCache.query(
                        new AppOpModeQuery(op, uid, packageName, virtualDeviceId, null,
                                "checkOpNoThrow"));
                if (mode == MODE_FOREGROUND) {
                    // We only cache raw mode. If the mode is FOREGROUND, we need another binder
                    // call to fetch translated value based on the process state.
                    mode = mService.checkOperationForDevice(op, uid, packageName, virtualDeviceId);
                }
            } else {
                mode = mService.checkOperationForDevice(op, uid, packageName, virtualDeviceId);
            }

            return mode == AppOpsManager.MODE_FOREGROUND ? AppOpsManager.MODE_ALLOWED : mode;
            return mode;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
+22 −5
Original line number Diff line number Diff line
@@ -65,27 +65,31 @@ public class AppOpsRestrictionsImpl implements AppOpsRestrictions {

    @Override
    public boolean setGlobalRestriction(Object clientToken, int code, boolean restricted) {
        boolean changed;
        if (restricted) {
            if (!mGlobalRestrictions.containsKey(clientToken)) {
                mGlobalRestrictions.put(clientToken, new SparseBooleanArray());
            }
            SparseBooleanArray restrictedCodes = mGlobalRestrictions.get(clientToken);
            Objects.requireNonNull(restrictedCodes);
            boolean changed = !restrictedCodes.get(code);
            changed = !restrictedCodes.get(code);
            restrictedCodes.put(code, true);
            return changed;
        } else {
            SparseBooleanArray restrictedCodes = mGlobalRestrictions.get(clientToken);
            if (restrictedCodes == null) {
                return false;
            }
            boolean changed = restrictedCodes.get(code);
            changed = restrictedCodes.get(code);
            restrictedCodes.delete(code);
            if (restrictedCodes.size() == 0) {
                mGlobalRestrictions.remove(clientToken);
            }
            return changed;
        }

        if (changed) {
            AppOpsManager.invalidateAppOpModeCache();
        }
        return changed;
    }

    @Override
@@ -104,7 +108,11 @@ public class AppOpsRestrictionsImpl implements AppOpsRestrictions {

    @Override
    public boolean clearGlobalRestrictions(Object clientToken) {
        return mGlobalRestrictions.remove(clientToken) != null;
        boolean changed = mGlobalRestrictions.remove(clientToken) != null;
        if (changed) {
            AppOpsManager.invalidateAppOpModeCache();
        }
        return changed;
    }

    @RequiresPermission(anyOf = {
@@ -122,6 +130,9 @@ public class AppOpsRestrictionsImpl implements AppOpsRestrictions {
            changed |= putUserRestrictionExclusions(clientToken, userIds[i],
                    excludedPackageTags);
        }
        if (changed) {
            AppOpsManager.invalidateAppOpModeCache();
        }
        return changed;
    }

@@ -191,6 +202,9 @@ public class AppOpsRestrictionsImpl implements AppOpsRestrictions {
        changed |= mUserRestrictions.remove(clientToken) != null;
        changed |= mUserRestrictionExcludedPackageTags.remove(clientToken) != null;
        notifyAllUserRestrictions(allUserRestrictedCodes);
        if (changed) {
            AppOpsManager.invalidateAppOpModeCache();
        }
        return changed;
    }

@@ -244,6 +258,9 @@ public class AppOpsRestrictionsImpl implements AppOpsRestrictions {
            }
        }

        if (changed) {
            AppOpsManager.invalidateAppOpModeCache();
        }
        return changed;
    }

+77 −3
Original line number Diff line number Diff line
@@ -998,6 +998,7 @@ public class AppOpsService extends IAppOpsService.Stub {
                    @Override
                    public void onUidModeChanged(int uid, int code, int mode,
                            String persistentDeviceId) {
                        AppOpsManager.invalidateAppOpModeCache();
                        mHandler.sendMessage(PooledLambda.obtainMessage(
                                AppOpsService::notifyOpChangedForAllPkgsInUid, AppOpsService.this,
                                code, uid, false, persistentDeviceId));
@@ -1006,6 +1007,7 @@ public class AppOpsService extends IAppOpsService.Stub {
                    @Override
                    public void onPackageModeChanged(String packageName, int userId, int code,
                            int mode) {
                        AppOpsManager.invalidateAppOpModeCache();
                        mHandler.sendMessage(PooledLambda.obtainMessage(
                                AppOpsService::notifyOpChangedForPkg, AppOpsService.this,
                                packageName, code, mode, userId));
@@ -1032,6 +1034,11 @@ public class AppOpsService extends IAppOpsService.Stub {
        // To migrate storageFile to recentAccessesFile, these reads must be called in this order.
        readRecentAccesses();
        mAppOpsCheckingService.readState();
        // The system property used by the cache is created the first time it is written, that only
        // happens inside invalidateCache().  Until the service calls invalidateCache() the property
        // will not exist and the nonce will be UNSET.
        AppOpsManager.invalidateAppOpModeCache();
        AppOpsManager.disableAppOpModeCache();
    }

    public void publish() {
@@ -2830,6 +2837,13 @@ public class AppOpsService extends IAppOpsService.Stub {
    @Override
    public int checkOperationRaw(int code, int uid, String packageName,
            @Nullable String attributionTag) {
        if (Binder.getCallingPid() != Process.myPid()
                && Flags.appopAccessTrackingLoggingEnabled()) {
            FrameworkStatsLog.write(
                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, uid, code,
                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION,
                    false);
        }
        return mCheckOpsDelegateDispatcher.checkOperation(code, uid, packageName, attributionTag,
                Context.DEVICE_ID_DEFAULT, true /*raw*/);
    }
@@ -2837,6 +2851,13 @@ public class AppOpsService extends IAppOpsService.Stub {
    @Override
    public int checkOperationRawForDevice(int code, int uid, @Nullable String packageName,
            @Nullable String attributionTag, int virtualDeviceId) {
        if (Binder.getCallingPid() != Process.myPid()
                && Flags.appopAccessTrackingLoggingEnabled()) {
            FrameworkStatsLog.write(
                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, uid, code,
                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION,
                    false);
        }
        return mCheckOpsDelegateDispatcher.checkOperation(code, uid, packageName, attributionTag,
                virtualDeviceId, true /*raw*/);
    }
@@ -2894,9 +2915,15 @@ public class AppOpsService extends IAppOpsService.Stub {
                return AppOpsManager.MODE_IGNORED;
            }
        }

        if (Flags.appopModeCachingEnabled()) {
            return getAppOpMode(code, uid, resolvedPackageName, attributionTag, virtualDeviceId,
                    raw, true);
        } else {
            return checkOperationUnchecked(code, uid, resolvedPackageName, attributionTag,
                    virtualDeviceId, raw);
        }
    }

    /**
     * Get the mode of an app-op.
@@ -2961,6 +2988,54 @@ public class AppOpsService extends IAppOpsService.Stub {
        }
    }

    /**
     * This method unifies mode checking logic between checkOperationUnchecked and
     * noteOperationUnchecked. It can replace those two methods once the flag is fully rolled out.
     *
     * @param isCheckOp This param is only used in user's op restriction. When checking if a package
     *                  can bypass user's restriction we should account for attributionTag as well.
     *                  But existing checkOp APIs don't accept attributionTag so we added a hack to
     *                  skip attributionTag check for checkOp. After we add an overload of checkOp
     *                  that accepts attributionTag we should remove this param.
     */
    private @Mode int getAppOpMode(int code, int uid, @NonNull String packageName,
            @Nullable String attributionTag, int virtualDeviceId, boolean raw, boolean isCheckOp) {
        PackageVerificationResult pvr;
        try {
            pvr = verifyAndGetBypass(uid, packageName, attributionTag);
        } catch (SecurityException e) {
            logVerifyAndGetBypassFailure(uid, e, "getAppOpMode");
            return MODE_IGNORED;
        }

        if (isOpRestrictedDueToSuspend(code, packageName, uid)) {
            return MODE_IGNORED;
        }

        synchronized (this) {
            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, virtualDeviceId,
                    pvr.bypass, isCheckOp)) {
                return MODE_IGNORED;
            }
            if (isOpAllowedForUid(uid)) {
                return MODE_ALLOWED;
            }

            int switchCode = AppOpsManager.opToSwitch(code);
            int rawUidMode = mAppOpsCheckingService.getUidMode(uid,
                    getPersistentId(virtualDeviceId), switchCode);

            if (rawUidMode != AppOpsManager.opToDefaultMode(switchCode)) {
                return raw ? rawUidMode : evaluateForegroundMode(uid, switchCode, rawUidMode);
            }

            int rawPackageMode = mAppOpsCheckingService.getPackageMode(packageName, switchCode,
                    UserHandle.getUserId(uid));
            return raw ? rawPackageMode : evaluateForegroundMode(uid, switchCode, rawPackageMode);
        }
    }


    @Override
    public int checkAudioOperation(int code, int usage, int uid, String packageName) {
        return mCheckOpsDelegateDispatcher.checkAudioOperation(code, usage, uid, packageName);
@@ -3213,7 +3288,6 @@ public class AppOpsService extends IAppOpsService.Stub {
        PackageVerificationResult pvr;
        try {
            pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName);
            boolean wasNull = attributionTag == null;
            if (!pvr.isAttributionTagValid) {
                attributionTag = null;
            }
+2 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.app.AppOpsManager.OP_FINE_LOCATION;

import static org.junit.Assert.assertEquals;

import android.app.PropertyInvalidatedCache;
import android.content.Context;
import android.os.Handler;

@@ -63,6 +64,7 @@ public class AppOpsLegacyRestrictionsTest {

    @Before
    public void setUp() {
        PropertyInvalidatedCache.disableForTestMode();
        mSession = ExtendedMockito.mockitoSession()
                .initMocks(this)
                .strictness(Strictness.LENIENT)
+2 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import android.app.AppOpsManager;
import android.app.PropertyInvalidatedCache;
import android.companion.virtual.VirtualDeviceManager;
import android.content.Context;
import android.os.FileUtils;
@@ -69,6 +70,7 @@ public class AppOpsRecentAccessPersistenceTest {

    @Before
    public void setUp() {
        PropertyInvalidatedCache.disableForTestMode();
        when(mAppOpCheckingService.addAppOpsModeChangedListener(any())).thenReturn(true);
        LocalServices.addService(AppOpsCheckingServiceInterface.class, mAppOpCheckingService);