All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.server.role.RoleUserState Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * Copyright (C) 2018 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.role;

import android.annotation.CheckResult;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.annotation.WorkerThread;
import android.os.Environment;
import android.os.Handler;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.Xml;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.CollectionUtils;
import com.android.internal.util.dump.DualDumpOutputStream;
import com.android.internal.util.function.pooled.PooledLambda;

import libcore.io.IoUtils;

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

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Stores the state of roles for a user.
 */
public class RoleUserState {

    private static final String LOG_TAG = RoleUserState.class.getSimpleName();

    public static final int VERSION_UNDEFINED = -1;

    private static final String ROLES_FILE_NAME = "roles.xml";

    private static final long WRITE_DELAY_MILLIS = 200;

    private static final String TAG_ROLES = "roles";
    private static final String TAG_ROLE = "role";
    private static final String TAG_HOLDER = "holder";
    private static final String ATTRIBUTE_VERSION = "version";
    private static final String ATTRIBUTE_NAME = "name";
    private static final String ATTRIBUTE_PACKAGES_HASH = "packagesHash";

    @UserIdInt
    private final int mUserId;

    @NonNull
    private final Callback mCallback;

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

    @GuardedBy("mLock")
    private int mVersion = VERSION_UNDEFINED;

    @GuardedBy("mLock")
    @Nullable
    private String mPackagesHash;

    /**
     * Maps role names to its holders' package names. The values should never be null.
     */
    @GuardedBy("mLock")
    @NonNull
    private ArrayMap> mRoles = new ArrayMap<>();

    @GuardedBy("mLock")
    private boolean mWriteScheduled;

    @GuardedBy("mLock")
    private boolean mDestroyed;

    @NonNull
    private final Handler mWriteHandler = new Handler(BackgroundThread.getHandler().getLooper());

    /**
     * Create a new user state, and read its state from disk if previously persisted.
     *
     * @param userId the user id for this user state
     * @param callback the callback for this user state
     */
    public RoleUserState(@UserIdInt int userId, @NonNull Callback callback) {
        mUserId = userId;
        mCallback = callback;

        readFile();
    }

    /**
     * Get the version of this user state.
     */
    public int getVersion() {
        synchronized (mLock) {
            throwIfDestroyedLocked();

            return mVersion;
        }
    }

    /**
     * Set the version of this user state.
     *
     * @param version the version to set
     */
    public void setVersion(int version) {
        synchronized (mLock) {
            throwIfDestroyedLocked();

            if (mVersion == version) {
                return;
            }
            mVersion = version;
            scheduleWriteFileLocked();
        }
    }

    /**
     * Get the hash representing the state of packages during the last time initial grants was run.
     *
     * @return the hash representing the state of packages
     */
    @Nullable
    public String getPackagesHash() {
        synchronized (mLock) {
            return mPackagesHash;
        }
    }

    /**
     * Set the hash representing the state of packages during the last time initial grants was run.
     *
     * @param packagesHash the hash representing the state of packages
     */
    public void setPackagesHash(@Nullable String packagesHash) {
        synchronized (mLock) {
            throwIfDestroyedLocked();

            if (Objects.equals(mPackagesHash, packagesHash)) {
                return;
            }
            mPackagesHash = packagesHash;
            scheduleWriteFileLocked();
        }
    }

    /**
     * Get whether the role is available.
     *
     * @param roleName the name of the role to get the holders for
     *
     * @return whether the role is available
     */
    public boolean isRoleAvailable(@NonNull String roleName) {
        synchronized (mLock) {
            throwIfDestroyedLocked();

            return mRoles.containsKey(roleName);
        }
    }

    /**
     * Get the holders of a role.
     *
     * @param roleName the name of the role to query for
     *
     * @return the set of role holders, or {@code null} if and only if the role is not found
     */
    @Nullable
    public ArraySet getRoleHolders(@NonNull String roleName) {
        synchronized (mLock) {
            throwIfDestroyedLocked();

            ArraySet packageNames = mRoles.get(roleName);
            if (packageNames == null) {
                return null;
            }
            return new ArraySet<>(packageNames);
        }
    }

    /**
     * Adds the given role, effectively marking it as {@link #isRoleAvailable available}
     *
     * @param roleName the name of the role
     *
     * @return whether any changes were made
     */
    public boolean addRoleName(@NonNull String roleName) {
        synchronized (mLock) {
            throwIfDestroyedLocked();

            if (!mRoles.containsKey(roleName)) {
                mRoles.put(roleName, new ArraySet<>());
                Slog.i(LOG_TAG, "Added new role: " + roleName);
                scheduleWriteFileLocked();
                return true;
            } else {
                return false;
            }
        }
    }

    /**
     * Set the names of all available roles.
     *
     * @param roleNames the names of all the available roles
     */
    public void setRoleNames(@NonNull List roleNames) {
        synchronized (mLock) {
            throwIfDestroyedLocked();

            boolean changed = false;

            for (int i = mRoles.size() - 1; i >= 0; i--) {
                String roleName = mRoles.keyAt(i);

                if (!roleNames.contains(roleName)) {
                    ArraySet packageNames = mRoles.valueAt(i);
                    if (!packageNames.isEmpty()) {
                        Slog.e(LOG_TAG, "Holders of a removed role should have been cleaned up,"
                                + " role: " + roleName + ", holders: " + packageNames);
                    }
                    mRoles.removeAt(i);
                    changed = true;
                }
            }

            int roleNamesSize = roleNames.size();
            for (int i = 0; i < roleNamesSize; i++) {
                changed |= addRoleName(roleNames.get(i));
            }

            if (changed) {
                scheduleWriteFileLocked();
            }
        }
    }

    /**
     * Add a holder to a role.
     *
     * @param roleName the name of the role to add the holder to
     * @param packageName the package name of the new holder
     *
     * @return {@code false} if and only if the role is not found
     */
    @CheckResult
    public boolean addRoleHolder(@NonNull String roleName, @NonNull String packageName) {
        boolean changed;

        synchronized (mLock) {
            throwIfDestroyedLocked();

            ArraySet roleHolders = mRoles.get(roleName);
            if (roleHolders == null) {
                Slog.e(LOG_TAG, "Cannot add role holder for unknown role, role: " + roleName
                        + ", package: " + packageName);
                return false;
            }
            changed = roleHolders.add(packageName);
            if (changed) {
                scheduleWriteFileLocked();
            }
        }

        if (changed) {
            mCallback.onRoleHoldersChanged(roleName, mUserId, null, packageName);
        }
        return true;
    }

    /**
     * Remove a holder from a role.
     *
     * @param roleName the name of the role to remove the holder from
     * @param packageName the package name of the holder to remove
     *
     * @return {@code false} if and only if the role is not found
     */
    @CheckResult
    public boolean removeRoleHolder(@NonNull String roleName, @NonNull String packageName) {
        boolean changed;

        synchronized (mLock) {
            throwIfDestroyedLocked();

            ArraySet roleHolders = mRoles.get(roleName);
            if (roleHolders == null) {
                Slog.e(LOG_TAG, "Cannot remove role holder for unknown role, role: " + roleName
                        + ", package: " + packageName);
                return false;
            }

            changed = roleHolders.remove(packageName);
            if (changed) {
                scheduleWriteFileLocked();
            }
        }

        if (changed) {
            mCallback.onRoleHoldersChanged(roleName, mUserId, packageName, null);
        }
        return true;
    }

    /**
     * @see android.app.role.RoleManager#getHeldRolesFromController
     */
    @NonNull
    public List getHeldRoles(@NonNull String packageName) {
        ArrayList result = new ArrayList<>();
        int size = mRoles.size();
        for (int i = 0; i < size; i++) {
            if (mRoles.valueAt(i).contains(packageName)) {
                result.add(mRoles.keyAt(i));
            }
        }
        return result;
    }

    /**
     * Schedule writing the state to file.
     */
    @GuardedBy("mLock")
    private void scheduleWriteFileLocked() {
        throwIfDestroyedLocked();

        if (!mWriteScheduled) {
            mWriteHandler.sendMessageDelayed(PooledLambda.obtainMessage(RoleUserState::writeFile,
                    this), WRITE_DELAY_MILLIS);
            mWriteScheduled = true;
        }
    }

    @WorkerThread
    private void writeFile() {
        int version;
        String packagesHash;
        ArrayMap> roles;
        synchronized (mLock) {
            if (mDestroyed) {
                return;
            }

            mWriteScheduled = false;

            version = mVersion;
            packagesHash = mPackagesHash;
            roles = snapshotRolesLocked();
        }

        AtomicFile atomicFile = new AtomicFile(getFile(mUserId), "roles-" + mUserId);
        FileOutputStream out = null;
        try {
            out = atomicFile.startWrite();

            XmlSerializer serializer = Xml.newSerializer();
            serializer.setOutput(out, StandardCharsets.UTF_8.name());
            serializer.setFeature(
                    "http://xmlpull.org/v1/doc/features.html#indent-output", true);
            serializer.startDocument(null, true);

            serializeRoles(serializer, version, packagesHash, roles);

            serializer.endDocument();
            atomicFile.finishWrite(out);
            Slog.i(LOG_TAG, "Wrote roles.xml successfully");
        } catch (IllegalArgumentException | IllegalStateException | IOException e) {
            Slog.wtf(LOG_TAG, "Failed to write roles.xml, restoring backup", e);
            if (out != null) {
                atomicFile.failWrite(out);
            }
        } finally {
            IoUtils.closeQuietly(out);
        }
    }

    @WorkerThread
    private void serializeRoles(@NonNull XmlSerializer serializer, int version,
            @Nullable String packagesHash, @NonNull ArrayMap> roles)
            throws IOException {
        serializer.startTag(null, TAG_ROLES);

        serializer.attribute(null, ATTRIBUTE_VERSION, Integer.toString(version));

        if (packagesHash != null) {
            serializer.attribute(null, ATTRIBUTE_PACKAGES_HASH, packagesHash);
        }

        for (int i = 0, size = roles.size(); i < size; ++i) {
            String roleName = roles.keyAt(i);
            ArraySet roleHolders = roles.valueAt(i);

            serializer.startTag(null, TAG_ROLE);
            serializer.attribute(null, ATTRIBUTE_NAME, roleName);
            serializeRoleHolders(serializer, roleHolders);
            serializer.endTag(null, TAG_ROLE);
        }

        serializer.endTag(null, TAG_ROLES);
    }

    @WorkerThread
    private void serializeRoleHolders(@NonNull XmlSerializer serializer,
            @NonNull ArraySet roleHolders) throws IOException {
        for (int i = 0, size = roleHolders.size(); i < size; ++i) {
            String roleHolder = roleHolders.valueAt(i);

            serializer.startTag(null, TAG_HOLDER);
            serializer.attribute(null, ATTRIBUTE_NAME, roleHolder);
            serializer.endTag(null, TAG_HOLDER);
        }
    }

    /**
     * Read the state from file.
     */
    private void readFile() {
        synchronized (mLock) {
            File file = getFile(mUserId);
            try (FileInputStream in = new AtomicFile(file).openRead()) {
                XmlPullParser parser = Xml.newPullParser();
                parser.setInput(in, null);
                parseXmlLocked(parser);
                Slog.i(LOG_TAG, "Read roles.xml successfully");
            } catch (FileNotFoundException e) {
                Slog.i(LOG_TAG, "roles.xml not found");
            } catch (XmlPullParserException | IOException e) {
                throw new IllegalStateException("Failed to parse roles.xml: " + file, e);
            }
        }
    }

    private void parseXmlLocked(@NonNull XmlPullParser parser) throws IOException,
            XmlPullParserException {
        int type;
        int depth;
        int innerDepth = parser.getDepth() + 1;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
            if (depth > innerDepth || type != XmlPullParser.START_TAG) {
                continue;
            }

            if (parser.getName().equals(TAG_ROLES)) {
                parseRolesLocked(parser);
                return;
            }
        }
        Slog.w(LOG_TAG, "Missing <" + TAG_ROLES + "> in roles.xml");
    }

    private void parseRolesLocked(@NonNull XmlPullParser parser) throws IOException,
            XmlPullParserException {
        mVersion = Integer.parseInt(parser.getAttributeValue(null, ATTRIBUTE_VERSION));
        mPackagesHash = parser.getAttributeValue(null, ATTRIBUTE_PACKAGES_HASH);
        mRoles.clear();

        int type;
        int depth;
        int innerDepth = parser.getDepth() + 1;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
            if (depth > innerDepth || type != XmlPullParser.START_TAG) {
                continue;
            }

            if (parser.getName().equals(TAG_ROLE)) {
                String roleName = parser.getAttributeValue(null, ATTRIBUTE_NAME);
                ArraySet roleHolders = parseRoleHoldersLocked(parser);
                mRoles.put(roleName, roleHolders);
            }
        }
    }

    @NonNull
    private ArraySet parseRoleHoldersLocked(@NonNull XmlPullParser parser)
            throws IOException, XmlPullParserException {
        ArraySet roleHolders = new ArraySet<>();

        int type;
        int depth;
        int innerDepth = parser.getDepth() + 1;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
            if (depth > innerDepth || type != XmlPullParser.START_TAG) {
                continue;
            }

            if (parser.getName().equals(TAG_HOLDER)) {
                String roleHolder = parser.getAttributeValue(null, ATTRIBUTE_NAME);
                roleHolders.add(roleHolder);
            }
        }

        return roleHolders;
    }

    /**
     * Dump this user state.
     *
     * @param dumpOutputStream the output stream to dump to
     */
    public void dump(@NonNull DualDumpOutputStream dumpOutputStream, @NonNull String fieldName,
            long fieldId) {
        int version;
        String packagesHash;
        ArrayMap> roles;
        synchronized (mLock) {
            throwIfDestroyedLocked();

            version = mVersion;
            packagesHash = mPackagesHash;
            roles = snapshotRolesLocked();
        }

        long fieldToken = dumpOutputStream.start(fieldName, fieldId);
        dumpOutputStream.write("user_id", RoleUserStateProto.USER_ID, mUserId);
        dumpOutputStream.write("version", RoleUserStateProto.VERSION, version);
        dumpOutputStream.write("packages_hash", RoleUserStateProto.PACKAGES_HASH, packagesHash);

        int rolesSize = roles.size();
        for (int rolesIndex = 0; rolesIndex < rolesSize; rolesIndex++) {
            String roleName = roles.keyAt(rolesIndex);
            ArraySet roleHolders = roles.valueAt(rolesIndex);

            long rolesToken = dumpOutputStream.start("roles", RoleUserStateProto.ROLES);
            dumpOutputStream.write("name", RoleProto.NAME, roleName);

            int roleHoldersSize = roleHolders.size();
            for (int roleHoldersIndex = 0; roleHoldersIndex < roleHoldersSize; roleHoldersIndex++) {
                String roleHolder = roleHolders.valueAt(roleHoldersIndex);

                dumpOutputStream.write("holders", RoleProto.HOLDERS, roleHolder);
            }

            dumpOutputStream.end(rolesToken);
        }

        dumpOutputStream.end(fieldToken);
    }

    /**
     * Get the roles and their holders.
     *
     * @return A copy of the roles and their holders
     */
    @NonNull
    public ArrayMap> getRolesAndHolders() {
        synchronized (mLock) {
            return snapshotRolesLocked();
        }
    }

    @GuardedBy("mLock")
    @NonNull
    private ArrayMap> snapshotRolesLocked() {
        ArrayMap> roles = new ArrayMap<>();
        for (int i = 0, size = CollectionUtils.size(mRoles); i < size; ++i) {
            String roleName = mRoles.keyAt(i);
            ArraySet roleHolders = mRoles.valueAt(i);

            roleHolders = new ArraySet<>(roleHolders);
            roles.put(roleName, roleHolders);
        }
        return roles;
    }

    /**
     * Destroy this user state and delete the corresponding file. Any pending writes to the file
     * will be cancelled, and any future interaction with this state will throw an exception.
     */
    public void destroy() {
        synchronized (mLock) {
            throwIfDestroyedLocked();
            mWriteHandler.removeCallbacksAndMessages(null);
            getFile(mUserId).delete();
            mDestroyed = true;
        }
    }

    @GuardedBy("mLock")
    private void throwIfDestroyedLocked() {
        if (mDestroyed) {
            throw new IllegalStateException("This RoleUserState has already been destroyed");
        }
    }

    @NonNull
    private static File getFile(@UserIdInt int userId) {
        return new File(Environment.getUserSystemDirectory(userId), ROLES_FILE_NAME);
    }

    /**
     * Callback for a user state.
     */
    public interface Callback {

        /**
         * Called when the holders of roles are changed.
         *
         * @param roleName the name of the role whose holders are changed
         * @param userId the user id for this role holder change
         */
        void onRoleHoldersChanged(@NonNull String roleName, @UserIdInt int userId,
                @Nullable String removedHolder, @Nullable String addedHolder);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy