1 /* 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 17 package com.android.server.role; 18 19 import android.annotation.CheckResult; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.UserIdInt; 23 import android.annotation.WorkerThread; 24 import android.os.Environment; 25 import android.os.Handler; 26 import android.os.UserHandle; 27 import android.util.ArrayMap; 28 import android.util.ArraySet; 29 import android.util.AtomicFile; 30 import android.util.Slog; 31 import android.util.Xml; 32 33 import com.android.internal.annotations.GuardedBy; 34 import com.android.internal.os.BackgroundThread; 35 import com.android.internal.util.CollectionUtils; 36 import com.android.internal.util.dump.DualDumpOutputStream; 37 import com.android.internal.util.function.pooled.PooledLambda; 38 import com.android.role.persistence.RolesPersistence; 39 import com.android.role.persistence.RolesState; 40 41 import org.xmlpull.v1.XmlPullParser; 42 import org.xmlpull.v1.XmlPullParserException; 43 44 import java.io.File; 45 import java.io.FileInputStream; 46 import java.io.FileNotFoundException; 47 import java.io.IOException; 48 import java.util.ArrayList; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.Objects; 52 import java.util.Set; 53 54 /** 55 * Stores the state of roles for a user. 56 */ 57 public class RoleUserState { 58 59 private static final String LOG_TAG = RoleUserState.class.getSimpleName(); 60 61 public static final int VERSION_UNDEFINED = -1; 62 63 private static final String ROLES_FILE_NAME = "roles.xml"; 64 65 private static final long WRITE_DELAY_MILLIS = 200; 66 67 private static final String TAG_ROLES = "roles"; 68 private static final String TAG_ROLE = "role"; 69 private static final String TAG_HOLDER = "holder"; 70 private static final String ATTRIBUTE_VERSION = "version"; 71 private static final String ATTRIBUTE_NAME = "name"; 72 private static final String ATTRIBUTE_PACKAGES_HASH = "packagesHash"; 73 74 private final RolesPersistence mPersistence = RolesPersistence.createInstance(); 75 76 @UserIdInt 77 private final int mUserId; 78 79 @NonNull 80 private final Callback mCallback; 81 82 @NonNull 83 private final Object mLock = new Object(); 84 85 @GuardedBy("mLock") 86 private int mVersion = VERSION_UNDEFINED; 87 88 @GuardedBy("mLock") 89 @Nullable 90 private String mPackagesHash; 91 92 /** 93 * Maps role names to its holders' package names. The values should never be null. 94 */ 95 @GuardedBy("mLock") 96 @NonNull 97 private ArrayMap<String, ArraySet<String>> mRoles = new ArrayMap<>(); 98 99 @GuardedBy("mLock") 100 private boolean mWriteScheduled; 101 102 @GuardedBy("mLock") 103 private boolean mDestroyed; 104 105 @NonNull 106 private final Handler mWriteHandler = new Handler(BackgroundThread.getHandler().getLooper()); 107 108 /** 109 * Create a new user state, and read its state from disk if previously persisted. 110 * 111 * @param userId the user id for this user state 112 * @param callback the callback for this user state 113 */ RoleUserState(@serIdInt int userId, @NonNull Callback callback)114 public RoleUserState(@UserIdInt int userId, @NonNull Callback callback) { 115 mUserId = userId; 116 mCallback = callback; 117 118 readFile(); 119 } 120 121 /** 122 * Get the version of this user state. 123 */ getVersion()124 public int getVersion() { 125 synchronized (mLock) { 126 return mVersion; 127 } 128 } 129 130 /** 131 * Set the version of this user state. 132 * 133 * @param version the version to set 134 */ setVersion(int version)135 public void setVersion(int version) { 136 synchronized (mLock) { 137 if (mVersion == version) { 138 return; 139 } 140 mVersion = version; 141 scheduleWriteFileLocked(); 142 } 143 } 144 145 /** 146 * Get the hash representing the state of packages during the last time initial grants was run. 147 * 148 * @return the hash representing the state of packages 149 */ 150 @Nullable getPackagesHash()151 public String getPackagesHash() { 152 synchronized (mLock) { 153 return mPackagesHash; 154 } 155 } 156 157 /** 158 * Set the hash representing the state of packages during the last time initial grants was run. 159 * 160 * @param packagesHash the hash representing the state of packages 161 */ setPackagesHash(@ullable String packagesHash)162 public void setPackagesHash(@Nullable String packagesHash) { 163 synchronized (mLock) { 164 if (Objects.equals(mPackagesHash, packagesHash)) { 165 return; 166 } 167 mPackagesHash = packagesHash; 168 scheduleWriteFileLocked(); 169 } 170 } 171 172 /** 173 * Get whether the role is available. 174 * 175 * @param roleName the name of the role to get the holders for 176 * 177 * @return whether the role is available 178 */ isRoleAvailable(@onNull String roleName)179 public boolean isRoleAvailable(@NonNull String roleName) { 180 synchronized (mLock) { 181 return mRoles.containsKey(roleName); 182 } 183 } 184 185 /** 186 * Get the holders of a role. 187 * 188 * @param roleName the name of the role to query for 189 * 190 * @return the set of role holders, or {@code null} if and only if the role is not found 191 */ 192 @Nullable getRoleHolders(@onNull String roleName)193 public ArraySet<String> getRoleHolders(@NonNull String roleName) { 194 synchronized (mLock) { 195 ArraySet<String> packageNames = mRoles.get(roleName); 196 if (packageNames == null) { 197 return null; 198 } 199 return new ArraySet<>(packageNames); 200 } 201 } 202 203 /** 204 * Adds the given role, effectively marking it as {@link #isRoleAvailable available} 205 * 206 * @param roleName the name of the role 207 * 208 * @return whether any changes were made 209 */ addRoleName(@onNull String roleName)210 public boolean addRoleName(@NonNull String roleName) { 211 synchronized (mLock) { 212 if (!mRoles.containsKey(roleName)) { 213 mRoles.put(roleName, new ArraySet<>()); 214 Slog.i(LOG_TAG, "Added new role: " + roleName); 215 scheduleWriteFileLocked(); 216 return true; 217 } else { 218 return false; 219 } 220 } 221 } 222 223 /** 224 * Set the names of all available roles. 225 * 226 * @param roleNames the names of all the available roles 227 */ setRoleNames(@onNull List<String> roleNames)228 public void setRoleNames(@NonNull List<String> roleNames) { 229 synchronized (mLock) { 230 boolean changed = false; 231 232 for (int i = mRoles.size() - 1; i >= 0; i--) { 233 String roleName = mRoles.keyAt(i); 234 235 if (!roleNames.contains(roleName)) { 236 ArraySet<String> packageNames = mRoles.valueAt(i); 237 if (!packageNames.isEmpty()) { 238 Slog.e(LOG_TAG, "Holders of a removed role should have been cleaned up," 239 + " role: " + roleName + ", holders: " + packageNames); 240 } 241 mRoles.removeAt(i); 242 changed = true; 243 } 244 } 245 246 int roleNamesSize = roleNames.size(); 247 for (int i = 0; i < roleNamesSize; i++) { 248 changed |= addRoleName(roleNames.get(i)); 249 } 250 251 if (changed) { 252 scheduleWriteFileLocked(); 253 } 254 } 255 } 256 257 /** 258 * Add a holder to a role. 259 * 260 * @param roleName the name of the role to add the holder to 261 * @param packageName the package name of the new holder 262 * 263 * @return {@code false} if and only if the role is not found 264 */ 265 @CheckResult addRoleHolder(@onNull String roleName, @NonNull String packageName)266 public boolean addRoleHolder(@NonNull String roleName, @NonNull String packageName) { 267 boolean changed; 268 269 synchronized (mLock) { 270 ArraySet<String> roleHolders = mRoles.get(roleName); 271 if (roleHolders == null) { 272 Slog.e(LOG_TAG, "Cannot add role holder for unknown role, role: " + roleName 273 + ", package: " + packageName); 274 return false; 275 } 276 changed = roleHolders.add(packageName); 277 if (changed) { 278 scheduleWriteFileLocked(); 279 } 280 } 281 282 if (changed) { 283 mCallback.onRoleHoldersChanged(roleName, mUserId, null, packageName); 284 } 285 return true; 286 } 287 288 /** 289 * Remove a holder from a role. 290 * 291 * @param roleName the name of the role to remove the holder from 292 * @param packageName the package name of the holder to remove 293 * 294 * @return {@code false} if and only if the role is not found 295 */ 296 @CheckResult removeRoleHolder(@onNull String roleName, @NonNull String packageName)297 public boolean removeRoleHolder(@NonNull String roleName, @NonNull String packageName) { 298 boolean changed; 299 300 synchronized (mLock) { 301 ArraySet<String> roleHolders = mRoles.get(roleName); 302 if (roleHolders == null) { 303 Slog.e(LOG_TAG, "Cannot remove role holder for unknown role, role: " + roleName 304 + ", package: " + packageName); 305 return false; 306 } 307 308 changed = roleHolders.remove(packageName); 309 if (changed) { 310 scheduleWriteFileLocked(); 311 } 312 } 313 314 if (changed) { 315 mCallback.onRoleHoldersChanged(roleName, mUserId, packageName, null); 316 } 317 return true; 318 } 319 320 /** 321 * @see android.app.role.RoleManager#getHeldRolesFromController 322 */ 323 @NonNull getHeldRoles(@onNull String packageName)324 public List<String> getHeldRoles(@NonNull String packageName) { 325 synchronized (mLock) { 326 List<String> roleNames = new ArrayList<>(); 327 int size = mRoles.size(); 328 for (int i = 0; i < size; i++) { 329 if (mRoles.valueAt(i).contains(packageName)) { 330 roleNames.add(mRoles.keyAt(i)); 331 } 332 } 333 return roleNames; 334 } 335 } 336 337 /** 338 * Schedule writing the state to file. 339 */ 340 @GuardedBy("mLock") scheduleWriteFileLocked()341 private void scheduleWriteFileLocked() { 342 if (mDestroyed) { 343 return; 344 } 345 346 if (!mWriteScheduled) { 347 mWriteHandler.sendMessageDelayed(PooledLambda.obtainMessage(RoleUserState::writeFile, 348 this), WRITE_DELAY_MILLIS); 349 mWriteScheduled = true; 350 } 351 } 352 353 @WorkerThread writeFile()354 private void writeFile() { 355 RolesState roles; 356 synchronized (mLock) { 357 if (mDestroyed) { 358 return; 359 } 360 361 mWriteScheduled = false; 362 363 roles = new RolesState(mVersion, mPackagesHash, 364 (Map<String, Set<String>>) (Map<String, ?>) snapshotRolesLocked()); 365 } 366 367 mPersistence.writeForUser(roles, UserHandle.of(mUserId)); 368 } 369 readFile()370 private void readFile() { 371 synchronized (mLock) { 372 RolesState roles = mPersistence.readForUser(UserHandle.of(mUserId)); 373 if (roles == null) { 374 readLegacyFileLocked(); 375 scheduleWriteFileLocked(); 376 return; 377 } 378 379 mVersion = roles.getVersion(); 380 mPackagesHash = roles.getPackagesHash(); 381 382 mRoles.clear(); 383 for (Map.Entry<String, Set<String>> entry : roles.getRoles().entrySet()) { 384 String roleName = entry.getKey(); 385 ArraySet<String> roleHolders = new ArraySet<>(entry.getValue()); 386 mRoles.put(roleName, roleHolders); 387 } 388 } 389 } 390 readLegacyFileLocked()391 private void readLegacyFileLocked() { 392 File file = getFile(mUserId); 393 try (FileInputStream in = new AtomicFile(file).openRead()) { 394 XmlPullParser parser = Xml.newPullParser(); 395 parser.setInput(in, null); 396 parseXmlLocked(parser); 397 Slog.i(LOG_TAG, "Read roles.xml successfully"); 398 } catch (FileNotFoundException e) { 399 Slog.i(LOG_TAG, "roles.xml not found"); 400 } catch (XmlPullParserException | IOException e) { 401 throw new IllegalStateException("Failed to parse roles.xml: " + file, e); 402 } 403 } 404 parseXmlLocked(@onNull XmlPullParser parser)405 private void parseXmlLocked(@NonNull XmlPullParser parser) throws IOException, 406 XmlPullParserException { 407 int type; 408 int depth; 409 int innerDepth = parser.getDepth() + 1; 410 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 411 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { 412 if (depth > innerDepth || type != XmlPullParser.START_TAG) { 413 continue; 414 } 415 416 if (parser.getName().equals(TAG_ROLES)) { 417 parseRolesLocked(parser); 418 return; 419 } 420 } 421 Slog.w(LOG_TAG, "Missing <" + TAG_ROLES + "> in roles.xml"); 422 } 423 parseRolesLocked(@onNull XmlPullParser parser)424 private void parseRolesLocked(@NonNull XmlPullParser parser) throws IOException, 425 XmlPullParserException { 426 mVersion = Integer.parseInt(parser.getAttributeValue(null, ATTRIBUTE_VERSION)); 427 mPackagesHash = parser.getAttributeValue(null, ATTRIBUTE_PACKAGES_HASH); 428 mRoles.clear(); 429 430 int type; 431 int depth; 432 int innerDepth = parser.getDepth() + 1; 433 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 434 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { 435 if (depth > innerDepth || type != XmlPullParser.START_TAG) { 436 continue; 437 } 438 439 if (parser.getName().equals(TAG_ROLE)) { 440 String roleName = parser.getAttributeValue(null, ATTRIBUTE_NAME); 441 ArraySet<String> roleHolders = parseRoleHoldersLocked(parser); 442 mRoles.put(roleName, roleHolders); 443 } 444 } 445 } 446 447 @NonNull parseRoleHoldersLocked(@onNull XmlPullParser parser)448 private ArraySet<String> parseRoleHoldersLocked(@NonNull XmlPullParser parser) 449 throws IOException, XmlPullParserException { 450 ArraySet<String> roleHolders = new ArraySet<>(); 451 452 int type; 453 int depth; 454 int innerDepth = parser.getDepth() + 1; 455 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 456 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { 457 if (depth > innerDepth || type != XmlPullParser.START_TAG) { 458 continue; 459 } 460 461 if (parser.getName().equals(TAG_HOLDER)) { 462 String roleHolder = parser.getAttributeValue(null, ATTRIBUTE_NAME); 463 roleHolders.add(roleHolder); 464 } 465 } 466 467 return roleHolders; 468 } 469 470 /** 471 * Dump this user state. 472 * 473 * @param dumpOutputStream the output stream to dump to 474 */ dump(@onNull DualDumpOutputStream dumpOutputStream, @NonNull String fieldName, long fieldId)475 public void dump(@NonNull DualDumpOutputStream dumpOutputStream, @NonNull String fieldName, 476 long fieldId) { 477 int version; 478 String packagesHash; 479 ArrayMap<String, ArraySet<String>> roles; 480 synchronized (mLock) { 481 version = mVersion; 482 packagesHash = mPackagesHash; 483 roles = snapshotRolesLocked(); 484 } 485 486 long fieldToken = dumpOutputStream.start(fieldName, fieldId); 487 dumpOutputStream.write("user_id", RoleUserStateProto.USER_ID, mUserId); 488 dumpOutputStream.write("version", RoleUserStateProto.VERSION, version); 489 dumpOutputStream.write("packages_hash", RoleUserStateProto.PACKAGES_HASH, packagesHash); 490 491 int rolesSize = roles.size(); 492 for (int rolesIndex = 0; rolesIndex < rolesSize; rolesIndex++) { 493 String roleName = roles.keyAt(rolesIndex); 494 ArraySet<String> roleHolders = roles.valueAt(rolesIndex); 495 496 long rolesToken = dumpOutputStream.start("roles", RoleUserStateProto.ROLES); 497 dumpOutputStream.write("name", RoleProto.NAME, roleName); 498 499 int roleHoldersSize = roleHolders.size(); 500 for (int roleHoldersIndex = 0; roleHoldersIndex < roleHoldersSize; roleHoldersIndex++) { 501 String roleHolder = roleHolders.valueAt(roleHoldersIndex); 502 503 dumpOutputStream.write("holders", RoleProto.HOLDERS, roleHolder); 504 } 505 506 dumpOutputStream.end(rolesToken); 507 } 508 509 dumpOutputStream.end(fieldToken); 510 } 511 512 /** 513 * Get the roles and their holders. 514 * 515 * @return A copy of the roles and their holders 516 */ 517 @NonNull getRolesAndHolders()518 public ArrayMap<String, ArraySet<String>> getRolesAndHolders() { 519 synchronized (mLock) { 520 return snapshotRolesLocked(); 521 } 522 } 523 524 @GuardedBy("mLock") 525 @NonNull snapshotRolesLocked()526 private ArrayMap<String, ArraySet<String>> snapshotRolesLocked() { 527 ArrayMap<String, ArraySet<String>> roles = new ArrayMap<>(); 528 for (int i = 0, size = CollectionUtils.size(mRoles); i < size; ++i) { 529 String roleName = mRoles.keyAt(i); 530 ArraySet<String> roleHolders = mRoles.valueAt(i); 531 532 roleHolders = new ArraySet<>(roleHolders); 533 roles.put(roleName, roleHolders); 534 } 535 return roles; 536 } 537 538 /** 539 * Destroy this user state and delete the corresponding file. Any pending writes to the file 540 * will be cancelled, and any future interaction with this state will throw an exception. 541 */ destroy()542 public void destroy() { 543 synchronized (mLock) { 544 if (mDestroyed) { 545 throw new IllegalStateException("This RoleUserState has already been destroyed"); 546 } 547 mWriteHandler.removeCallbacksAndMessages(null); 548 mPersistence.deleteForUser(UserHandle.of(mUserId)); 549 mDestroyed = true; 550 } 551 } 552 553 @NonNull getFile(@serIdInt int userId)554 private static File getFile(@UserIdInt int userId) { 555 return new File(Environment.getUserSystemDirectory(userId), ROLES_FILE_NAME); 556 } 557 558 /** 559 * Callback for a user state. 560 */ 561 public interface Callback { 562 563 /** 564 * Called when the holders of roles are changed. 565 * 566 * @param roleName the name of the role whose holders are changed 567 * @param userId the user id for this role holder change 568 */ onRoleHoldersChanged(@onNull String roleName, @UserIdInt int userId, @Nullable String removedHolder, @Nullable String addedHolder)569 void onRoleHoldersChanged(@NonNull String roleName, @UserIdInt int userId, 570 @Nullable String removedHolder, @Nullable String addedHolder); 571 } 572 } 573