blob: 3e3e1566b2dd9d3789c2ea0b693a1ac077000d94 [file] [log] [blame]
/*
* 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.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.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 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;
@GuardedBy("RoleManagerService.mLock")
private int mVersion = VERSION_UNDEFINED;
@GuardedBy("RoleManagerService.mLock")
@Nullable
private String mLastGrantPackagesHash;
/**
* Maps role names to its holders' package names. The values should never be null.
*/
@GuardedBy("RoleManagerService.mLock")
@NonNull
private ArrayMap<String, ArraySet<String>> mRoles = new ArrayMap<>();
@GuardedBy("RoleManagerService.mLock")
private boolean mDestroyed;
@NonNull
private final Handler mWriteHandler = new Handler(BackgroundThread.getHandler().getLooper());
private RoleUserState(@UserIdInt int userId) {
mUserId = userId;
readSyncLocked();
}
/**
* Create a new instance of user state, and read its state from disk if previously persisted.
*
* @param userId the user id for the new user state
*
* @return the new user state
*/
@GuardedBy("RoleManagerService.mLock")
public static RoleUserState newInstanceLocked(@UserIdInt int userId) {
return new RoleUserState(userId);
}
/**
* Get the version of this user state.
*/
@GuardedBy("RoleManagerService.mLock")
public int getVersionLocked() {
throwIfDestroyedLocked();
return mVersion;
}
/**
* Set the version of this user state.
*
* @param version the version to set
*/
@GuardedBy("RoleManagerService.mLock")
public void setVersionLocked(int version) {
throwIfDestroyedLocked();
if (mVersion == version) {
return;
}
mVersion = version;
writeAsyncLocked();
}
/**
* Get the hash representing the state of packages during the last time initial grants was run.
*
* @return the hash representing the state of packages
*/
@GuardedBy("RoleManagerService.mLock")
public String getLastGrantPackagesHashLocked() {
return mLastGrantPackagesHash;
}
/**
* Set the hash representing the state of packages during the last time initial grants was run.
*
* @param lastGrantPackagesHash the hash representing the state of packages
*/
@GuardedBy("RoleManagerService.mLock")
public void setLastGrantPackagesHashLocked(@Nullable String lastGrantPackagesHash) {
throwIfDestroyedLocked();
if (Objects.equals(mLastGrantPackagesHash, lastGrantPackagesHash)) {
return;
}
mLastGrantPackagesHash = lastGrantPackagesHash;
writeAsyncLocked();
}
/**
* Get whether the role is available.
*
* @param roleName the name of the role to get the holders for
*
* @return whether the role is available
*/
@GuardedBy("RoleManagerService.mLock")
public boolean isRoleAvailableLocked(@NonNull String roleName) {
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. {@code null} should not be returned and indicates an issue.
*/
@GuardedBy("RoleManagerService.mLock")
@Nullable
public ArraySet<String> getRoleHoldersLocked(@NonNull String roleName) {
throwIfDestroyedLocked();
return mRoles.get(roleName);
}
/**
* Set the names of all available roles.
*
* @param roleNames the names of all the available roles
*/
@GuardedBy("RoleManagerService.mLock")
public void setRoleNamesLocked(@NonNull List<String> roleNames) {
throwIfDestroyedLocked();
boolean changed = false;
for (int i = mRoles.size() - 1; i >= 0; i--) {
String roleName = mRoles.keyAt(i);
if (!roleNames.contains(roleName)) {
ArraySet<String> 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++) {
String roleName = roleNames.get(i);
if (!mRoles.containsKey(roleName)) {
mRoles.put(roleName, new ArraySet<>());
Slog.i(LOG_TAG, "Added new role: " + roleName);
changed = true;
}
}
if (changed) {
writeAsyncLocked();
}
}
/**
* 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} only if the set of role holders is null, which should not happen and
* indicates an issue.
*/
@CheckResult
@GuardedBy("RoleManagerService.mLock")
public boolean addRoleHolderLocked(@NonNull String roleName, @NonNull String packageName) {
throwIfDestroyedLocked();
ArraySet<String> roleHolders = mRoles.get(roleName);
if (roleHolders == null) {
Slog.e(LOG_TAG, "Cannot add role holder for unknown role, role: " + roleName
+ ", package: " + packageName);
return false;
}
boolean changed = roleHolders.add(packageName);
if (changed) {
writeAsyncLocked();
}
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} only if the set of role holders is null, which should not happen and
* indicates an issue.
*/
@CheckResult
@GuardedBy("RoleManagerService.mLock")
public boolean removeRoleHolderLocked(@NonNull String roleName, @NonNull String packageName) {
throwIfDestroyedLocked();
ArraySet<String> roleHolders = mRoles.get(roleName);
if (roleHolders == null) {
Slog.e(LOG_TAG, "Cannot remove role holder for unknown role, role: " + roleName
+ ", package: " + packageName);
return false;
}
boolean changed = roleHolders.remove(packageName);
if (changed) {
writeAsyncLocked();
}
return true;
}
/**
* Schedule writing the state to file.
*/
@GuardedBy("RoleManagerService.mLock")
private void writeAsyncLocked() {
throwIfDestroyedLocked();
ArrayMap<String, ArraySet<String>> roles = new ArrayMap<>();
for (int i = 0, size = CollectionUtils.size(mRoles); i < size; ++i) {
String roleName = mRoles.keyAt(i);
ArraySet<String> roleHolders = mRoles.valueAt(i);
roleHolders = new ArraySet<>(roleHolders);
roles.put(roleName, roleHolders);
}
mWriteHandler.removeCallbacksAndMessages(null);
// TODO: Throttle writes.
mWriteHandler.sendMessage(PooledLambda.obtainMessage(RoleUserState::writeSync, this,
mVersion, mLastGrantPackagesHash, roles));
}
@WorkerThread
private void writeSync(int version, @Nullable String packagesHash,
@NonNull ArrayMap<String, ArraySet<String>> roles) {
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);
} 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<String, ArraySet<String>> 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<String> 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<String> 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.
*/
@GuardedBy("RoleManagerService.mLock")
private void readSyncLocked() {
File file = getFile(mUserId);
try (FileInputStream in = new AtomicFile(file).openRead()) {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(in, null);
parseXmlLocked(parser);
} 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));
mLastGrantPackagesHash = 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<String> roleHolders = parseRoleHoldersLocked(parser);
mRoles.put(roleName, roleHolders);
}
}
}
@NonNull
private ArraySet<String> parseRoleHoldersLocked(@NonNull XmlPullParser parser)
throws IOException, XmlPullParserException {
ArraySet<String> 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;
}
/**
* Destroy this 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.
*/
@GuardedBy("RoleManagerService.mLock")
public void destroySyncLocked() {
throwIfDestroyedLocked();
mWriteHandler.removeCallbacksAndMessages(null);
getFile(mUserId).delete();
mDestroyed = true;
}
@GuardedBy("RoleManagerService.mLock")
private void throwIfDestroyedLocked() {
if (mDestroyed) {
throw new IllegalStateException("This RoleUserState has already been destroyed");
}
}
private static @NonNull File getFile(@UserIdInt int userId) {
return new File(Environment.getUserSystemDirectory(userId), ROLES_FILE_NAME);
}
}