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

Commit e59ff019 authored by Yuting's avatar Yuting
Browse files

[DeviceAwareAppOp] Add device attributed AppOp accesses to the recent access file

This CL contains following changes:
1) Create a new AppOpsRecentAccessPersistence class to manage read/write of AppOp recent access file.
2) Instead of calling getPackagesForOps() to get all op accesses from the memory, use AppOpsService.mUidStates. This is to make it easy to get AppOp accesses from all devices by reading the raw data from mUidStates.
3) Add "dv" as a new XML attribute on the attributed op tag to represent an access entry from an external device. Add "pdv" as a new XML attribute to indicate the proxy is on an external device.

Bug: 336802155
Test: Added a new unit test for persistence: atest AppOpsRecentAccessPersistenceTest
Change-Id: I9a781f7de19dc0fc30de1f1335e21cf724ed2c88
parent d10af358
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -190,3 +190,11 @@ flag {
    description: "Enable getDeviceId API in OpEventProxyInfo"
    bug: "337340961"
 }

flag {
    name: "device_aware_app_op_new_schema_enabled"
    is_fixed_read_only: true
    namespace: "permissions"
    description: "Persist device attributed AppOp accesses on the disk"
    bug: "308201969"
}
 No newline at end of file
+403 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.appop;

import static android.app.AppOpsManager.extractFlagsFromKey;
import static android.app.AppOpsManager.extractUidStateFromKey;
import static android.companion.virtual.VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.companion.virtual.VirtualDeviceManager;
import android.os.Process;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseArray;
import android.util.Xml;

import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Objects;

/**
 * This class manages the read/write of AppOp recent accesses between memory and disk.
 */
final class AppOpsRecentAccessPersistence {
    static final String TAG = "AppOpsRecentAccessPersistence";
    final AtomicFile mRecentAccessesFile;
    final AppOpsService mAppOpsService;

    private static final String TAG_APP_OPS = "app-ops";
    private static final String TAG_PACKAGE = "pkg";
    private static final String TAG_UID = "uid";
    private static final String TAG_OP = "op";
    private static final String TAG_ATTRIBUTION_OP = "st";

    private static final String ATTR_NAME = "n";
    private static final String ATTR_ID = "id";
    private static final String ATTR_DEVICE_ID = "dv";
    private static final String ATTR_ACCESS_TIME = "t";
    private static final String ATTR_REJECT_TIME = "r";
    private static final String ATTR_ACCESS_DURATION = "d";
    private static final String ATTR_PROXY_PACKAGE = "pp";
    private static final String ATTR_PROXY_UID = "pu";
    private static final String ATTR_PROXY_ATTRIBUTION_TAG = "pc";
    private static final String ATTR_PROXY_DEVICE_ID = "pdv";

    /**
     * Version of the mRecentAccessesFile.
     * Increment by one every time an upgrade step is added at boot, none currently exists.
     */
    private static final int CURRENT_VERSION = 1;

    AppOpsRecentAccessPersistence(
            @NonNull AtomicFile recentAccessesFile, @NonNull AppOpsService appOpsService) {
        mRecentAccessesFile = recentAccessesFile;
        mAppOpsService = appOpsService;
    }

    /**
     * Load AppOp recent access data from disk into uidStates. The target uidStates will first clear
     * itself before loading.
     *
     * @param uidStates The in-memory object where you want to populate data from disk
     */
    void readRecentAccesses(@NonNull SparseArray<AppOpsService.UidState> uidStates) {
        synchronized (mRecentAccessesFile) {
            FileInputStream stream;
            try {
                stream = mRecentAccessesFile.openRead();
            } catch (FileNotFoundException e) {
                Slog.i(
                        TAG,
                        "No existing app ops "
                                + mRecentAccessesFile.getBaseFile()
                                + "; starting empty");
                return;
            }
            boolean success = false;
            uidStates.clear();
            mAppOpsService.mAppOpsCheckingService.clearAllModes();
            try {
                TypedXmlPullParser parser = Xml.resolvePullParser(stream);
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG
                        && type != XmlPullParser.END_DOCUMENT) {
                    // Parse next until we reach the start or end
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new IllegalStateException("no start tag found");
                }

                int outerDepth = parser.getDepth();
                while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                        && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
                    if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                        continue;
                    }

                    String tagName = parser.getName();
                    if (tagName.equals(TAG_PACKAGE)) {
                        readPackage(parser, uidStates);
                    } else if (tagName.equals(TAG_UID)) {
                        // uid tag may be present during migration, don't print warning.
                        XmlUtils.skipCurrentTag(parser);
                    } else {
                        Slog.w(TAG, "Unknown element under <app-ops>: " + parser.getName());
                        XmlUtils.skipCurrentTag(parser);
                    }
                }

                success = true;
            } catch (IllegalStateException | NullPointerException | NumberFormatException
                     | XmlPullParserException | IOException | IndexOutOfBoundsException e) {
                Slog.w(TAG, "Failed parsing " + e);
            } finally {
                if (!success) {
                    uidStates.clear();
                    mAppOpsService.mAppOpsCheckingService.clearAllModes();
                }
                try {
                    stream.close();
                } catch (IOException ignored) {
                }
            }
        }
    }

    private void readPackage(
            TypedXmlPullParser parser, SparseArray<AppOpsService.UidState> uidStates)
            throws NumberFormatException, XmlPullParserException, IOException {
        String pkgName = parser.getAttributeValue(null, ATTR_NAME);
        int outerDepth = parser.getDepth();
        int type;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                continue;
            }

            String tagName = parser.getName();
            if (tagName.equals(TAG_UID)) {
                readUid(parser, pkgName, uidStates);
            } else {
                Slog.w(TAG, "Unknown element under <pkg>: "
                        + parser.getName());
                XmlUtils.skipCurrentTag(parser);
            }
        }
    }

    private void readUid(TypedXmlPullParser parser, @NonNull String pkgName,
            SparseArray<AppOpsService.UidState> uidStates)
            throws NumberFormatException, XmlPullParserException, IOException {
        int uid = parser.getAttributeInt(null, ATTR_NAME);
        final AppOpsService.UidState uidState = mAppOpsService.new UidState(uid);
        uidStates.put(uid, uidState);

        int outerDepth = parser.getDepth();
        int type;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                continue;
            }
            String tagName = parser.getName();
            if (tagName.equals(TAG_OP)) {
                readOp(parser, uidState, pkgName);
            } else {
                Slog.w(TAG, "Unknown element under <pkg>: "
                        + parser.getName());
                XmlUtils.skipCurrentTag(parser);
            }
        }
    }

    private void readOp(TypedXmlPullParser parser,
            @NonNull AppOpsService.UidState uidState, @NonNull String pkgName)
            throws NumberFormatException, XmlPullParserException, IOException {
        int opCode = parser.getAttributeInt(null, ATTR_NAME);
        AppOpsService.Op op = mAppOpsService.new Op(uidState, pkgName, opCode, uidState.uid);

        int outerDepth = parser.getDepth();
        int type;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                continue;
            }
            String tagName = parser.getName();
            if (tagName.equals(TAG_ATTRIBUTION_OP)) {
                readAttributionOp(parser, op, XmlUtils.readStringAttribute(parser, ATTR_ID));
            } else {
                Slog.w(TAG, "Unknown element under <op>: "
                        + parser.getName());
                XmlUtils.skipCurrentTag(parser);
            }
        }

        AppOpsService.Ops ops = uidState.pkgOps.get(pkgName);
        if (ops == null) {
            ops = new AppOpsService.Ops(pkgName, uidState);
            uidState.pkgOps.put(pkgName, ops);
        }
        ops.put(op.op, op);
    }

    private void readAttributionOp(TypedXmlPullParser parser, @NonNull AppOpsService.Op parent,
            @Nullable String attribution)
            throws NumberFormatException, IOException, XmlPullParserException {
        final long key = parser.getAttributeLong(null, ATTR_NAME);
        final int uidState = extractUidStateFromKey(key);
        final int opFlags = extractFlagsFromKey(key);

        String deviceId = parser.getAttributeValue(null, ATTR_DEVICE_ID);
        final long accessTime = parser.getAttributeLong(null, ATTR_ACCESS_TIME, 0);
        final long rejectTime = parser.getAttributeLong(null, ATTR_REJECT_TIME, 0);
        final long accessDuration = parser.getAttributeLong(null, ATTR_ACCESS_DURATION, -1);
        final String proxyPkg = XmlUtils.readStringAttribute(parser, ATTR_PROXY_PACKAGE);
        final int proxyUid = parser.getAttributeInt(null, ATTR_PROXY_UID, Process.INVALID_UID);
        final String proxyAttributionTag =
                XmlUtils.readStringAttribute(parser, ATTR_PROXY_ATTRIBUTION_TAG);
        final String proxyDeviceId = parser.getAttributeValue(null, ATTR_PROXY_DEVICE_ID);

        if (deviceId == null || Objects.equals(deviceId, "")) {
            deviceId = PERSISTENT_DEVICE_ID_DEFAULT;
        }

        AttributedOp attributedOp = parent.getOrCreateAttribution(parent, attribution, deviceId);

        if (accessTime > 0) {
            attributedOp.accessed(accessTime, accessDuration, proxyUid, proxyPkg,
                    proxyAttributionTag, proxyDeviceId, uidState, opFlags);
        }
        if (rejectTime > 0) {
            attributedOp.rejected(rejectTime, uidState, opFlags);
        }
    }

    /**
     * Write uidStates into an XML file on the disk. It's a complete dump from memory, the XML file
     * will be re-written.
     *
     * @param uidStates The in-memory object that holds all AppOp recent access data.
     */
    void writeRecentAccesses(SparseArray<AppOpsService.UidState> uidStates) {
        synchronized (mRecentAccessesFile) {
            FileOutputStream stream;
            try {
                stream = mRecentAccessesFile.startWrite();
            } catch (IOException e) {
                Slog.w(TAG, "Failed to write state: " + e);
                return;
            }

            try {
                TypedXmlSerializer out = Xml.resolveSerializer(stream);
                out.startDocument(null, true);
                out.startTag(null, TAG_APP_OPS);
                out.attributeInt(null, "v", CURRENT_VERSION);

                for (int uidIndex = 0; uidIndex < uidStates.size(); uidIndex++) {
                    AppOpsService.UidState uidState = uidStates.valueAt(uidIndex);
                    int uid = uidState.uid;

                    for (int pkgIndex = 0; pkgIndex < uidState.pkgOps.size(); pkgIndex++) {
                        String packageName = uidState.pkgOps.keyAt(pkgIndex);
                        AppOpsService.Ops ops = uidState.pkgOps.valueAt(pkgIndex);

                        out.startTag(null, TAG_PACKAGE);
                        out.attribute(null, ATTR_NAME, packageName);
                        out.startTag(null, TAG_UID);
                        out.attributeInt(null, ATTR_NAME, uid);

                        for (int opIndex = 0; opIndex < ops.size(); opIndex++) {
                            AppOpsService.Op op = ops.valueAt(opIndex);

                            out.startTag(null, TAG_OP);
                            out.attributeInt(null, ATTR_NAME, op.op);

                            writeDeviceAttributedOps(out, op);

                            out.endTag(null, TAG_OP);
                        }

                        out.endTag(null, TAG_UID);
                        out.endTag(null, TAG_PACKAGE);
                    }
                }

                out.endTag(null, TAG_APP_OPS);
                out.endDocument();
                mRecentAccessesFile.finishWrite(stream);
            } catch (IOException e) {
                Slog.w(TAG, "Failed to write state, restoring backup.", e);
                mRecentAccessesFile.failWrite(stream);
            }
        }
    }

    private void writeDeviceAttributedOps(TypedXmlSerializer out, AppOpsService.Op op)
            throws IOException {
        for (String deviceId : op.mDeviceAttributedOps.keySet()) {
            ArrayMap<String, AttributedOp> attributedOps =
                    op.mDeviceAttributedOps.get(deviceId);

            for (int attrIndex = 0; attrIndex < attributedOps.size(); attrIndex++) {
                String attributionTag = attributedOps.keyAt(attrIndex);
                AppOpsManager.AttributedOpEntry attributedOpEntry =
                        attributedOps.valueAt(attrIndex).createAttributedOpEntryLocked();

                final ArraySet<Long> keys = attributedOpEntry.collectKeys();
                for (int k = 0; k < keys.size(); k++) {
                    final long key = keys.valueAt(k);

                    final int uidState = AppOpsManager.extractUidStateFromKey(key);
                    final int flags = AppOpsManager.extractFlagsFromKey(key);

                    final long accessTime =
                            attributedOpEntry.getLastAccessTime(uidState, uidState, flags);
                    final long rejectTime =
                            attributedOpEntry.getLastRejectTime(uidState, uidState, flags);
                    final long accessDuration =
                            attributedOpEntry.getLastDuration(uidState, uidState, flags);

                    // Proxy information for rejections is not backed up
                    final AppOpsManager.OpEventProxyInfo proxy =
                            attributedOpEntry.getLastProxyInfo(uidState, uidState, flags);

                    if (accessTime <= 0 && rejectTime <= 0 && accessDuration <= 0
                            && proxy == null) {
                        continue;
                    }

                    out.startTag(null, TAG_ATTRIBUTION_OP);
                    if (attributionTag != null) {
                        out.attribute(null, ATTR_ID, attributionTag);
                    }
                    out.attributeLong(null, ATTR_NAME, key);

                    if (!Objects.equals(
                            deviceId, VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) {
                        out.attribute(null, ATTR_DEVICE_ID, deviceId);
                    }
                    if (accessTime > 0) {
                        out.attributeLong(null, ATTR_ACCESS_TIME, accessTime);
                    }
                    if (rejectTime > 0) {
                        out.attributeLong(null, ATTR_REJECT_TIME, rejectTime);
                    }
                    if (accessDuration > 0) {
                        out.attributeLong(null, ATTR_ACCESS_DURATION, accessDuration);
                    }
                    if (proxy != null) {
                        out.attributeInt(null, ATTR_PROXY_UID, proxy.getUid());

                        if (proxy.getPackageName() != null) {
                            out.attribute(null, ATTR_PROXY_PACKAGE, proxy.getPackageName());
                        }
                        if (proxy.getAttributionTag() != null) {
                            out.attribute(
                                    null, ATTR_PROXY_ATTRIBUTION_TAG, proxy.getAttributionTag());
                        }
                        if (proxy.getDeviceId() != null
                                && !Objects.equals(
                                        proxy.getDeviceId(),
                                        VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) {
                            out.attribute(null, ATTR_PROXY_DEVICE_ID, proxy.getDeviceId());
                        }
                    }

                    out.endTag(null, TAG_ATTRIBUTION_OP);
                }
            }
        }
    }
}
+21 −4
Original line number Diff line number Diff line
@@ -71,6 +71,7 @@ import static android.content.Intent.ACTION_PACKAGE_REMOVED;
import static android.content.Intent.EXTRA_REPLACING;
import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP;
import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled;

import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;

@@ -261,6 +262,7 @@ public class AppOpsService extends IAppOpsService.Stub {
    private final @Nullable File mNoteOpCallerStacktracesFile;
    final Handler mHandler;

    private final AppOpsRecentAccessPersistence mRecentAccessPersistence;
    /**
     * Pool for {@link AttributedOp.OpEventProxyInfoPool} to avoid to constantly reallocate new
     * objects
@@ -408,7 +410,7 @@ public class AppOpsService extends IAppOpsService.Stub {
    private @Nullable UserManagerInternal mUserManagerInternal;

    /** Interface for app-op modes.*/
    @VisibleForTesting
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    AppOpsCheckingServiceInterface mAppOpsCheckingService;

    /** Interface for app-op restrictions.*/
@@ -528,7 +530,7 @@ public class AppOpsService extends IAppOpsService.Stub {
    @VisibleForTesting
    final Constants mConstants;

    @VisibleForTesting
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
    final class UidState {
        public final int uid;

@@ -642,7 +644,7 @@ public class AppOpsService extends IAppOpsService.Stub {
            }
        }

        private @NonNull AttributedOp getOrCreateAttribution(@NonNull Op parent,
        @NonNull AttributedOp getOrCreateAttribution(@NonNull Op parent,
                @Nullable String attributionTag, String persistentDeviceId) {
            ArrayMap<String, AttributedOp> attributedOps = mDeviceAttributedOps.get(
                    persistentDeviceId);
@@ -1003,6 +1005,7 @@ public class AppOpsService extends IAppOpsService.Stub {
        LockGuard.installLock(this, LockGuard.INDEX_APP_OPS);
        mStorageFile = new AtomicFile(storageFile, "appops_legacy");
        mRecentAccessesFile = new AtomicFile(recentAccessesFile, "appops_accesses");
        mRecentAccessPersistence = new AppOpsRecentAccessPersistence(mRecentAccessesFile, this);

        if (AppOpsManager.NOTE_OP_COLLECTION_ENABLED) {
            mNoteOpCallerStacktracesFile = new File(SystemServiceManager.ensureSystemDir(),
@@ -4908,10 +4911,16 @@ public class AppOpsService extends IAppOpsService.Stub {
    private void readRecentAccesses() {
        if (!mRecentAccessesFile.exists()) {
            readRecentAccesses(mStorageFile);
        } else {
            if (deviceAwareAppOpNewSchemaEnabled()) {
                synchronized (this) {
                    mRecentAccessPersistence.readRecentAccesses(mUidStates);
                }
            } else {
                readRecentAccesses(mRecentAccessesFile);
            }
        }
    }

    private void readRecentAccesses(AtomicFile file) {
        synchronized (file) {
@@ -5090,6 +5099,14 @@ public class AppOpsService extends IAppOpsService.Stub {

    @VisibleForTesting
    void writeRecentAccesses() {
        if (deviceAwareAppOpNewSchemaEnabled()) {
            synchronized (this) {
                mRecentAccessPersistence.writeRecentAccesses(mUidStates);
            }
            mHistoricalRegistry.writeAndClearDiscreteHistory();
            return;
        }

        synchronized (mRecentAccessesFile) {
            FileOutputStream stream;
            try {
+11 −0
Original line number Diff line number Diff line
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<app-ops v="1">
    <pkg n="com.android.servicestests.apps.testapp">
        <uid n="10001">
            <op n="26">
                <st id="attribution.tag.test.1" n="429496729601" t="1710799464518" d="2963" />
                <st n="1073741824008" dv="companion:1" t="1712610342977" d="7596" pp="com.android.servicestests.apps.proxy" pc="com.android.servicestests.apps.proxy.attrtag" pu="10002" pdv="companion:2" />
            </op>
        </uid>
    </pkg>
</app-ops>
 No newline at end of file
+188 −0

File added.

Preview size limit exceeded, changes collapsed.