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

Commit 85fa087d authored by yutingfang's avatar yutingfang Committed by Yuting Fang
Browse files

Cache AppOp mode to reduce binder calls to the system server

This CL contains following changes to cache AppOp mode value in
AppOpsManager:
1) Create a new IpcDataCache in AppOpsManager to cache op mode by uid,
   packageName, opCode, virtualDeviceId and attributionTag.
2) Unify mode checking logic between checkOp and noteOp in the
   system service in getAppOpModeRaw. The changes include:
   * checkOperation used to return default mode if uidState is null or package doesn't belong to the uid. Now it returns MODE_IGNORED.
   * isOpRestrictedDueToSuspend used to only apply to checkOperation,
     now it applies to noteOperation as well.
   * When checking if a package can bypass user's restriction on an op, we didn't account for the
     attributionTag for checkOperation, now we will.
3) Skip two location app ops for caching due to they are affected by
   attributionTag change described in #2
4) Invalidate cache when op mode value changes, op restriction changes.
5) Use cache only in various checkOp APIs for now. noteOp API change
   will come later. Skip cache for in-process binder calls

Flag: android.permission.flags.appop_mode_caching_enabled
Bug: 366013082
Test: presubmit
Change-Id: Ifba35afa6ed7fe1f3d2fd583015179364e70b883
parent 703e9559
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;
@@ -7792,6 +7795,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;
@@ -8846,12 +8959,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();
        }
@@ -9279,8 +9396,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));
            }
@@ -9319,13 +9449,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);