blob: f218d3a5834bc48d35a2b06fc8208532dbf6a588 [file] [log] [blame]
Hai Zhangb7776682018-09-25 15:10:57 -07001/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.role;
18
19import android.annotation.CheckResult;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.annotation.UserIdInt;
23import android.annotation.WorkerThread;
24import android.os.Environment;
25import android.os.Handler;
26import android.util.ArrayMap;
27import android.util.ArraySet;
28import android.util.AtomicFile;
29import android.util.Slog;
30import android.util.Xml;
31
32import com.android.internal.annotations.GuardedBy;
33import com.android.internal.os.BackgroundThread;
Eugene Suslaabdefba2018-11-09 18:06:43 -080034import com.android.internal.util.CollectionUtils;
Hai Zhangb7776682018-09-25 15:10:57 -070035import com.android.internal.util.function.pooled.PooledLambda;
36
37import libcore.io.IoUtils;
38
39import org.xmlpull.v1.XmlPullParser;
40import org.xmlpull.v1.XmlPullParserException;
41import org.xmlpull.v1.XmlSerializer;
42
43import java.io.File;
44import java.io.FileInputStream;
45import java.io.FileNotFoundException;
46import java.io.FileOutputStream;
47import java.io.IOException;
48import java.nio.charset.StandardCharsets;
Hai Zhang8e60a8f2018-11-20 11:21:09 -080049import java.util.List;
Hai Zhangb7776682018-09-25 15:10:57 -070050
51/**
52 * Stores the state of roles for a user.
53 */
54public class RoleUserState {
55
56 private static final String LOG_TAG = RoleUserState.class.getSimpleName();
57
58 public static final int VERSION_UNDEFINED = -1;
59
60 private static final String ROLES_FILE_NAME = "roles.xml";
61
62 private static final String TAG_ROLES = "roles";
63 private static final String TAG_ROLE = "role";
64 private static final String TAG_HOLDER = "holder";
65 private static final String ATTRIBUTE_VERSION = "version";
66 private static final String ATTRIBUTE_NAME = "name";
Eugene Suslaabdefba2018-11-09 18:06:43 -080067 private static final String ATTRIBUTE_PACKAGES_HASH = "packagesHash";
Hai Zhangb7776682018-09-25 15:10:57 -070068
69 @UserIdInt
70 private final int mUserId;
71
72 @GuardedBy("RoleManagerService.mLock")
Hai Zhangb295ac42018-11-16 16:08:18 -080073 private int mVersion;
Hai Zhangb7776682018-09-25 15:10:57 -070074
Eugene Suslaabdefba2018-11-09 18:06:43 -080075 @GuardedBy("RoleManagerService.mLock")
76 private String mLastGrantPackagesHash = null;
77
Hai Zhangb7776682018-09-25 15:10:57 -070078 /**
79 * Maps role names to its holders' package names. The values should never be null.
80 */
81 @GuardedBy("RoleManagerService.mLock")
Eugene Suslaabdefba2018-11-09 18:06:43 -080082 @Nullable
83 private ArrayMap<String, ArraySet<String>> mRoles = null;
Hai Zhangb7776682018-09-25 15:10:57 -070084
85 @GuardedBy("RoleManagerService.mLock")
86 private boolean mDestroyed;
87
88 private final Handler mWriteHandler = new Handler(BackgroundThread.getHandler().getLooper());
89
90 public RoleUserState(@UserIdInt int userId) {
91 mUserId = userId;
92 }
93
94 /**
95 * Get the version of this user state.
96 */
97 @GuardedBy("RoleManagerService.mLock")
98 public int getVersionLocked() {
99 throwIfDestroyedLocked();
100 return mVersion;
101 }
102
103 /**
104 * Set the version of this user state.
105 *
106 * @param version the version to set
107 */
108 @GuardedBy("RoleManagerService.mLock")
109 public void setVersionLocked(int version) {
110 throwIfDestroyedLocked();
Hai Zhang8e60a8f2018-11-20 11:21:09 -0800111 if (mVersion == version) {
112 return;
113 }
Hai Zhangb7776682018-09-25 15:10:57 -0700114 mVersion = version;
Hai Zhang8e60a8f2018-11-20 11:21:09 -0800115 writeAsyncLocked();
Hai Zhangb7776682018-09-25 15:10:57 -0700116 }
117
118 /**
Eugene Suslaabdefba2018-11-09 18:06:43 -0800119 * Get the hash representing the state of packages during the last time initial grants was run
120 */
121 @GuardedBy("RoleManagerService.mLock")
122 public String getLastGrantPackagesHashLocked() {
123 return mLastGrantPackagesHash;
124 }
125
126 /**
127 * Set the hash representing the state of packages during the last time initial grants was run
128 */
129 @GuardedBy("RoleManagerService.mLock")
130 public void setLastGrantPackagesHashLocked(String lastGrantPackagesHash) {
131 mLastGrantPackagesHash = lastGrantPackagesHash;
132 writeAsyncLocked();
133 }
134
135 /**
Hai Zhangb7776682018-09-25 15:10:57 -0700136 * Get whether the role is available.
137 *
138 * @param roleName the name of the role to get the holders for
139 *
140 * @return whether the role is available
141 */
142 @GuardedBy("RoleManagerService.mLock")
143 public boolean isRoleAvailableLocked(@NonNull String roleName) {
144 throwIfDestroyedLocked();
145 return mRoles.containsKey(roleName);
146 }
147
148 /**
149 * Get the holders of a role.
150 *
151 * @param roleName the name of the role to query for
152 *
153 * @return the set of role holders. {@code null} should not be returned and indicates an issue.
154 */
155 @GuardedBy("RoleManagerService.mLock")
156 @Nullable
157 public ArraySet<String> getRoleHoldersLocked(@NonNull String roleName) {
158 throwIfDestroyedLocked();
159 return mRoles.get(roleName);
160 }
161
162 /**
Hai Zhang8e60a8f2018-11-20 11:21:09 -0800163 * Set the names of all available roles.
164 *
165 * @param roleNames the names of all the available roles
166 */
167 @GuardedBy("RoleManagerService.mLock")
168 public void setRoleNamesLocked(@NonNull List<String> roleNames) {
169 throwIfDestroyedLocked();
170 boolean changed = false;
171 for (int i = mRoles.size() - 1; i >= 0; i--) {
172 String roleName = mRoles.keyAt(i);
173 if (!roleNames.contains(roleName)) {
174 ArraySet<String> packageNames = mRoles.valueAt(i);
175 if (!packageNames.isEmpty()) {
176 Slog.e(LOG_TAG, "Holders of a removed role should have been cleaned up, role: "
177 + roleName + ", holders: " + packageNames);
178 }
179 mRoles.removeAt(i);
180 changed = true;
181 }
182 }
183 int roleNamesSize = roleNames.size();
184 for (int i = 0; i < roleNamesSize; i++) {
185 String roleName = roleNames.get(i);
186 if (!mRoles.containsKey(roleName)) {
187 mRoles.put(roleName, new ArraySet<>());
188 Slog.i(LOG_TAG, "Added new role: " + roleName);
189 changed = true;
190 }
191 }
192 if (changed) {
193 writeAsyncLocked();
194 }
195 }
196
197 /**
Hai Zhangb7776682018-09-25 15:10:57 -0700198 * Add a holder to a role.
199 *
200 * @param roleName the name of the role to add the holder to
201 * @param packageName the package name of the new holder
202 *
203 * @return {@code false} only if the set of role holders is null, which should not happen and
204 * indicates an issue.
205 */
206 @CheckResult
207 @GuardedBy("RoleManagerService.mLock")
208 public boolean addRoleHolderLocked(@NonNull String roleName, @NonNull String packageName) {
209 throwIfDestroyedLocked();
210 ArraySet<String> roleHolders = mRoles.get(roleName);
211 if (roleHolders == null) {
Hai Zhangb295ac42018-11-16 16:08:18 -0800212 Slog.e(LOG_TAG, "Cannot add role holder for unknown role, role: " + roleName
213 + ", package: " + packageName);
Hai Zhangb7776682018-09-25 15:10:57 -0700214 return false;
215 }
Hai Zhang8e60a8f2018-11-20 11:21:09 -0800216 boolean changed = roleHolders.add(packageName);
217 if (changed) {
218 writeAsyncLocked();
219 }
Hai Zhangb7776682018-09-25 15:10:57 -0700220 return true;
221 }
222
223 /**
224 * Remove a holder from a role.
225 *
226 * @param roleName the name of the role to remove the holder from
227 * @param packageName the package name of the holder to remove
228 *
229 * @return {@code false} only if the set of role holders is null, which should not happen and
230 * indicates an issue.
231 */
232 @CheckResult
233 @GuardedBy("RoleManagerService.mLock")
234 public boolean removeRoleHolderLocked(@NonNull String roleName, @NonNull String packageName) {
235 throwIfDestroyedLocked();
236 ArraySet<String> roleHolders = mRoles.get(roleName);
237 if (roleHolders == null) {
Hai Zhangb295ac42018-11-16 16:08:18 -0800238 Slog.e(LOG_TAG, "Cannot remove role holder for unknown role, role: " + roleName
239 + ", package: " + packageName);
Hai Zhangb7776682018-09-25 15:10:57 -0700240 return false;
241 }
Hai Zhang8e60a8f2018-11-20 11:21:09 -0800242 boolean changed = roleHolders.remove(packageName);
243 if (changed) {
244 writeAsyncLocked();
245 }
Hai Zhangb7776682018-09-25 15:10:57 -0700246 return true;
247 }
248
249 /**
Hai Zhangb7776682018-09-25 15:10:57 -0700250 * Schedule writing the state to file.
251 */
252 @GuardedBy("RoleManagerService.mLock")
Eugene Suslaabdefba2018-11-09 18:06:43 -0800253 void writeAsyncLocked() {
Hai Zhangb7776682018-09-25 15:10:57 -0700254 throwIfDestroyedLocked();
255 int version = mVersion;
256 ArrayMap<String, ArraySet<String>> roles = new ArrayMap<>();
Eugene Suslaabdefba2018-11-09 18:06:43 -0800257 for (int i = 0, size = CollectionUtils.size(mRoles); i < size; ++i) {
Hai Zhangb7776682018-09-25 15:10:57 -0700258 String roleName = mRoles.keyAt(i);
259 ArraySet<String> roleHolders = mRoles.valueAt(i);
260 roleHolders = new ArraySet<>(roleHolders);
261 roles.put(roleName, roleHolders);
262 }
263 mWriteHandler.removeCallbacksAndMessages(null);
Hai Zhang8e60a8f2018-11-20 11:21:09 -0800264 // TODO: Throttle writes.
Eugene Susla6d4922722018-11-08 16:25:28 -0800265 mWriteHandler.sendMessage(PooledLambda.obtainMessage(
Eugene Suslaabdefba2018-11-09 18:06:43 -0800266 RoleUserState::writeSync, this, version, roles, mLastGrantPackagesHash));
Hai Zhangb7776682018-09-25 15:10:57 -0700267 }
268
269 @WorkerThread
Eugene Suslaabdefba2018-11-09 18:06:43 -0800270 private void writeSync(int version, @NonNull ArrayMap<String, ArraySet<String>> roles,
271 String packagesHash) {
Hai Zhangb295ac42018-11-16 16:08:18 -0800272 AtomicFile atomicFile = new AtomicFile(getFile(mUserId), "roles-" + mUserId);
Hai Zhangb7776682018-09-25 15:10:57 -0700273 FileOutputStream out = null;
274 try {
Hai Zhangb295ac42018-11-16 16:08:18 -0800275 out = atomicFile.startWrite();
Hai Zhangb7776682018-09-25 15:10:57 -0700276
277 XmlSerializer serializer = Xml.newSerializer();
278 serializer.setOutput(out, StandardCharsets.UTF_8.name());
279 serializer.setFeature(
280 "http://xmlpull.org/v1/doc/features.html#indent-output", true);
281 serializer.startDocument(null, true);
282
Eugene Suslaabdefba2018-11-09 18:06:43 -0800283 serializeRoles(serializer, version, roles, packagesHash);
Hai Zhangb7776682018-09-25 15:10:57 -0700284
285 serializer.endDocument();
Hai Zhangb295ac42018-11-16 16:08:18 -0800286 atomicFile.finishWrite(out);
287 } catch (IllegalArgumentException | IllegalStateException | IOException e) {
288 Slog.wtf(LOG_TAG, "Failed to write roles.xml, restoring backup", e);
289 if (out != null) {
290 atomicFile.failWrite(out);
291 }
Hai Zhangb7776682018-09-25 15:10:57 -0700292 } finally {
293 IoUtils.closeQuietly(out);
294 }
295 }
296
297 @WorkerThread
298 private void serializeRoles(@NonNull XmlSerializer serializer, int version,
Eugene Suslaabdefba2018-11-09 18:06:43 -0800299 @NonNull ArrayMap<String, ArraySet<String>> roles, String packagesHash)
300 throws IOException {
Hai Zhangb7776682018-09-25 15:10:57 -0700301 serializer.startTag(null, TAG_ROLES);
302 serializer.attribute(null, ATTRIBUTE_VERSION, Integer.toString(version));
Eugene Suslaabdefba2018-11-09 18:06:43 -0800303 serializer.attribute(null, ATTRIBUTE_PACKAGES_HASH, packagesHash);
Hai Zhangb7776682018-09-25 15:10:57 -0700304 for (int i = 0, size = roles.size(); i < size; ++i) {
305 String roleName = roles.keyAt(i);
306 ArraySet<String> roleHolders = roles.valueAt(i);
307 serializer.startTag(null, TAG_ROLE);
308 serializer.attribute(null, ATTRIBUTE_NAME, roleName);
309 serializeRoleHolders(serializer, roleHolders);
310 serializer.endTag(null, TAG_ROLE);
311 }
312 serializer.endTag(null, TAG_ROLES);
313 }
314
315 @WorkerThread
316 private void serializeRoleHolders(@NonNull XmlSerializer serializer,
317 @NonNull ArraySet<String> roleHolders) throws IOException {
318 for (int i = 0, size = roleHolders.size(); i < size; ++i) {
319 String roleHolder = roleHolders.valueAt(i);
320 serializer.startTag(null, TAG_HOLDER);
321 serializer.attribute(null, ATTRIBUTE_NAME, roleHolder);
322 serializer.endTag(null, TAG_HOLDER);
323 }
324 }
325
326 /**
327 * Read the state from file.
328 */
329 @GuardedBy("RoleManagerService.mLock")
330 public void readSyncLocked() {
331 if (mRoles != null) {
Hai Zhangb295ac42018-11-16 16:08:18 -0800332 throw new IllegalStateException("This RoleUserState has already read the roles.xml");
Hai Zhangb7776682018-09-25 15:10:57 -0700333 }
334
Hai Zhangb295ac42018-11-16 16:08:18 -0800335 File file = getFile(mUserId);
336 try (FileInputStream in = new AtomicFile(file).openRead()) {
Hai Zhangb7776682018-09-25 15:10:57 -0700337 XmlPullParser parser = Xml.newPullParser();
338 parser.setInput(in, null);
339 parseXmlLocked(parser);
Hai Zhangb295ac42018-11-16 16:08:18 -0800340 } catch (FileNotFoundException e) {
341 Slog.i(LOG_TAG, "roles.xml not found");
342 mRoles = new ArrayMap<>();
343 mVersion = VERSION_UNDEFINED;
Hai Zhangb7776682018-09-25 15:10:57 -0700344 } catch (XmlPullParserException | IOException e) {
Hai Zhangb295ac42018-11-16 16:08:18 -0800345 throw new IllegalStateException("Failed to parse roles.xml: " + file, e);
Hai Zhangb7776682018-09-25 15:10:57 -0700346 }
347 }
348
349 private void parseXmlLocked(@NonNull XmlPullParser parser) throws IOException,
350 XmlPullParserException {
Hai Zhangb7776682018-09-25 15:10:57 -0700351 int type;
Hai Zhangb295ac42018-11-16 16:08:18 -0800352 int depth;
353 int innerDepth = parser.getDepth() + 1;
Hai Zhangb7776682018-09-25 15:10:57 -0700354 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
Hai Zhangb295ac42018-11-16 16:08:18 -0800355 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
356 if (depth > innerDepth || type != XmlPullParser.START_TAG) {
Hai Zhangb7776682018-09-25 15:10:57 -0700357 continue;
358 }
Hai Zhangb295ac42018-11-16 16:08:18 -0800359
Hai Zhangb7776682018-09-25 15:10:57 -0700360 if (parser.getName().equals(TAG_ROLES)) {
361 parseRolesLocked(parser);
362 return;
363 }
364 }
365 }
366
367 private void parseRolesLocked(@NonNull XmlPullParser parser) throws IOException,
368 XmlPullParserException {
369 mVersion = Integer.parseInt(parser.getAttributeValue(null, ATTRIBUTE_VERSION));
Eugene Suslaabdefba2018-11-09 18:06:43 -0800370 mLastGrantPackagesHash = parser.getAttributeValue(null, ATTRIBUTE_PACKAGES_HASH);
Hai Zhangb7776682018-09-25 15:10:57 -0700371 mRoles = new ArrayMap<>();
Hai Zhangb295ac42018-11-16 16:08:18 -0800372
Hai Zhangb7776682018-09-25 15:10:57 -0700373 int type;
Hai Zhangb295ac42018-11-16 16:08:18 -0800374 int depth;
375 int innerDepth = parser.getDepth() + 1;
Hai Zhangb7776682018-09-25 15:10:57 -0700376 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
Hai Zhangb295ac42018-11-16 16:08:18 -0800377 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
378 if (depth > innerDepth || type != XmlPullParser.START_TAG) {
Hai Zhangb7776682018-09-25 15:10:57 -0700379 continue;
380 }
Hai Zhangb295ac42018-11-16 16:08:18 -0800381
Hai Zhangb7776682018-09-25 15:10:57 -0700382 if (parser.getName().equals(TAG_ROLE)) {
383 String roleName = parser.getAttributeValue(null, ATTRIBUTE_NAME);
384 ArraySet<String> roleHolders = parseRoleHoldersLocked(parser);
385 mRoles.put(roleName, roleHolders);
386 }
387 }
388 }
389
390 @NonNull
391 private ArraySet<String> parseRoleHoldersLocked(@NonNull XmlPullParser parser)
392 throws IOException, XmlPullParserException {
393 ArraySet<String> roleHolders = new ArraySet<>();
Hai Zhangb295ac42018-11-16 16:08:18 -0800394
Hai Zhangb7776682018-09-25 15:10:57 -0700395 int type;
Hai Zhangb295ac42018-11-16 16:08:18 -0800396 int depth;
397 int innerDepth = parser.getDepth() + 1;
Hai Zhangb7776682018-09-25 15:10:57 -0700398 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
Hai Zhangb295ac42018-11-16 16:08:18 -0800399 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
400 if (depth > innerDepth || type != XmlPullParser.START_TAG) {
Hai Zhangb7776682018-09-25 15:10:57 -0700401 continue;
402 }
Hai Zhangb295ac42018-11-16 16:08:18 -0800403
Hai Zhangb7776682018-09-25 15:10:57 -0700404 if (parser.getName().equals(TAG_HOLDER)) {
405 String roleHolder = parser.getAttributeValue(null, ATTRIBUTE_NAME);
406 roleHolders.add(roleHolder);
407 }
408 }
Hai Zhangb295ac42018-11-16 16:08:18 -0800409
Hai Zhangb7776682018-09-25 15:10:57 -0700410 return roleHolders;
411 }
412
413 /**
414 * Destroy this state and delete the corresponding file. Any pending writes to the file will be
415 * cancelled and any future interaction with this state will throw an exception.
416 */
417 @GuardedBy("RoleManagerService.mLock")
418 public void destroySyncLocked() {
419 throwIfDestroyedLocked();
420 mWriteHandler.removeCallbacksAndMessages(null);
421 getFile(mUserId).delete();
422 mDestroyed = true;
423 }
424
425 @GuardedBy("RoleManagerService.mLock")
426 private void throwIfDestroyedLocked() {
427 if (mDestroyed) {
428 throw new IllegalStateException("This RoleUserState has already been destroyed");
429 }
430 }
431
432 private static @NonNull File getFile(@UserIdInt int userId) {
433 return new File(Environment.getUserSystemDirectory(userId), ROLES_FILE_NAME);
434 }
435}