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