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.permission.util.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 private static final long WRITE_DELAY_MILLIS = 200; 57 58 private final RolesPersistence mPersistence = RolesPersistence.createInstance(); 59 60 @UserIdInt 61 private final int mUserId; 62 63 @NonNull 64 private final RoleServicePlatformHelper mPlatformHelper; 65 66 @NonNull 67 private final Callback mCallback; 68 69 @NonNull 70 private final Object mLock = new Object(); 71 72 @GuardedBy("mLock") 73 private int mVersion = VERSION_UNDEFINED; 74 75 @GuardedBy("mLock") 76 @Nullable 77 private String mPackagesHash; 78 79 /** 80 * Maps role names to its holders' package names. The values should never be null. 81 */ 82 @GuardedBy("mLock") 83 @NonNull 84 private ArrayMap<String, ArraySet<String>> mRoles = new ArrayMap<>(); 85 86 @GuardedBy("mLock") 87 private boolean mWriteScheduled; 88 89 @GuardedBy("mLock") 90 private boolean mDestroyed; 91 92 @NonNull 93 private final Handler mWriteHandler = new Handler(BackgroundThread.get().getLooper()); 94 95 /** 96 * Create a new user state, and read its state from disk if previously persisted. 97 * 98 * @param userId the user id for this user state 99 * @param platformHelper the platform helper 100 * @param callback the callback for this user state 101 */ RoleUserState(@serIdInt int userId, @NonNull RoleServicePlatformHelper platformHelper, @NonNull Callback callback)102 public RoleUserState(@UserIdInt int userId, @NonNull RoleServicePlatformHelper platformHelper, 103 @NonNull Callback callback) { 104 mUserId = userId; 105 mPlatformHelper = platformHelper; 106 mCallback = callback; 107 108 readFile(); 109 } 110 111 /** 112 * Get the version of this user state. 113 */ getVersion()114 public int getVersion() { 115 synchronized (mLock) { 116 return mVersion; 117 } 118 } 119 120 /** 121 * Set the version of this user state. 122 * 123 * @param version the version to set 124 */ setVersion(int version)125 public void setVersion(int version) { 126 synchronized (mLock) { 127 if (mVersion == version) { 128 return; 129 } 130 mVersion = version; 131 scheduleWriteFileLocked(); 132 } 133 } 134 135 /** 136 * Get the hash representing the state of packages during the last time initial grants was run. 137 * 138 * @return the hash representing the state of packages 139 */ 140 @Nullable getPackagesHash()141 public String getPackagesHash() { 142 synchronized (mLock) { 143 return mPackagesHash; 144 } 145 } 146 147 /** 148 * Set the hash representing the state of packages during the last time initial grants was run. 149 * 150 * @param packagesHash the hash representing the state of packages 151 */ setPackagesHash(@ullable String packagesHash)152 public void setPackagesHash(@Nullable String packagesHash) { 153 synchronized (mLock) { 154 if (Objects.equals(mPackagesHash, packagesHash)) { 155 return; 156 } 157 mPackagesHash = packagesHash; 158 scheduleWriteFileLocked(); 159 } 160 } 161 162 /** 163 * Get whether the role is available. 164 * 165 * @param roleName the name of the role to get the holders for 166 * 167 * @return whether the role is available 168 */ isRoleAvailable(@onNull String roleName)169 public boolean isRoleAvailable(@NonNull String roleName) { 170 synchronized (mLock) { 171 return mRoles.containsKey(roleName); 172 } 173 } 174 175 /** 176 * Get the holders of a role. 177 * 178 * @param roleName the name of the role to query for 179 * 180 * @return the set of role holders, or {@code null} if and only if the role is not found 181 */ 182 @Nullable getRoleHolders(@onNull String roleName)183 public ArraySet<String> getRoleHolders(@NonNull String roleName) { 184 synchronized (mLock) { 185 ArraySet<String> packageNames = mRoles.get(roleName); 186 if (packageNames == null) { 187 return null; 188 } 189 return new ArraySet<>(packageNames); 190 } 191 } 192 193 /** 194 * Adds the given role, effectively marking it as {@link #isRoleAvailable available} 195 * 196 * @param roleName the name of the role 197 * 198 * @return whether any changes were made 199 */ addRoleName(@onNull String roleName)200 public boolean addRoleName(@NonNull String roleName) { 201 synchronized (mLock) { 202 if (!mRoles.containsKey(roleName)) { 203 mRoles.put(roleName, new ArraySet<>()); 204 Log.i(LOG_TAG, "Added new role: " + roleName); 205 scheduleWriteFileLocked(); 206 return true; 207 } else { 208 return false; 209 } 210 } 211 } 212 213 /** 214 * Set the names of all available roles. 215 * 216 * @param roleNames the names of all the available roles 217 */ setRoleNames(@onNull List<String> roleNames)218 public void setRoleNames(@NonNull List<String> roleNames) { 219 synchronized (mLock) { 220 boolean changed = false; 221 222 for (int i = mRoles.size() - 1; i >= 0; i--) { 223 String roleName = mRoles.keyAt(i); 224 225 if (!roleNames.contains(roleName)) { 226 ArraySet<String> packageNames = mRoles.valueAt(i); 227 if (!packageNames.isEmpty()) { 228 Log.e(LOG_TAG, "Holders of a removed role should have been cleaned up," 229 + " role: " + roleName + ", holders: " + packageNames); 230 } 231 mRoles.removeAt(i); 232 changed = true; 233 } 234 } 235 236 int roleNamesSize = roleNames.size(); 237 for (int i = 0; i < roleNamesSize; i++) { 238 changed |= addRoleName(roleNames.get(i)); 239 } 240 241 if (changed) { 242 scheduleWriteFileLocked(); 243 } 244 } 245 } 246 247 /** 248 * Add a holder to a role. 249 * 250 * @param roleName the name of the role to add the holder to 251 * @param packageName the package name of the new holder 252 * 253 * @return {@code false} if and only if the role is not found 254 */ 255 @CheckResult addRoleHolder(@onNull String roleName, @NonNull String packageName)256 public boolean addRoleHolder(@NonNull String roleName, @NonNull String packageName) { 257 boolean changed; 258 259 synchronized (mLock) { 260 ArraySet<String> roleHolders = mRoles.get(roleName); 261 if (roleHolders == null) { 262 Log.e(LOG_TAG, "Cannot add role holder for unknown role, role: " + roleName 263 + ", package: " + packageName); 264 return false; 265 } 266 changed = roleHolders.add(packageName); 267 if (changed) { 268 scheduleWriteFileLocked(); 269 } 270 } 271 272 if (changed) { 273 mCallback.onRoleHoldersChanged(roleName, mUserId); 274 } 275 return true; 276 } 277 278 /** 279 * Remove a holder from a role. 280 * 281 * @param roleName the name of the role to remove the holder from 282 * @param packageName the package name of the holder to remove 283 * 284 * @return {@code false} if and only if the role is not found 285 */ 286 @CheckResult removeRoleHolder(@onNull String roleName, @NonNull String packageName)287 public boolean removeRoleHolder(@NonNull String roleName, @NonNull String packageName) { 288 boolean changed; 289 290 synchronized (mLock) { 291 ArraySet<String> roleHolders = mRoles.get(roleName); 292 if (roleHolders == null) { 293 Log.e(LOG_TAG, "Cannot remove role holder for unknown role, role: " + roleName 294 + ", package: " + packageName); 295 return false; 296 } 297 298 changed = roleHolders.remove(packageName); 299 if (changed) { 300 scheduleWriteFileLocked(); 301 } 302 } 303 304 if (changed) { 305 mCallback.onRoleHoldersChanged(roleName, mUserId); 306 } 307 return true; 308 } 309 310 /** 311 * @see android.app.role.RoleManager#getHeldRolesFromController 312 */ 313 @NonNull getHeldRoles(@onNull String packageName)314 public List<String> getHeldRoles(@NonNull String packageName) { 315 synchronized (mLock) { 316 List<String> roleNames = new ArrayList<>(); 317 int size = mRoles.size(); 318 for (int i = 0; i < size; i++) { 319 if (mRoles.valueAt(i).contains(packageName)) { 320 roleNames.add(mRoles.keyAt(i)); 321 } 322 } 323 return roleNames; 324 } 325 } 326 327 /** 328 * Schedule writing the state to file. 329 */ 330 @GuardedBy("mLock") scheduleWriteFileLocked()331 private void scheduleWriteFileLocked() { 332 if (mDestroyed) { 333 return; 334 } 335 336 if (!mWriteScheduled) { 337 mWriteHandler.postDelayed(this::writeFile, WRITE_DELAY_MILLIS); 338 mWriteScheduled = true; 339 } 340 } 341 342 @WorkerThread writeFile()343 private void writeFile() { 344 RolesState roles; 345 synchronized (mLock) { 346 if (mDestroyed) { 347 return; 348 } 349 350 mWriteScheduled = false; 351 352 roles = new RolesState(mVersion, mPackagesHash, 353 (Map<String, Set<String>>) (Map<String, ?>) snapshotRolesLocked()); 354 } 355 356 mPersistence.writeForUser(roles, UserHandle.of(mUserId)); 357 } 358 readFile()359 private void readFile() { 360 synchronized (mLock) { 361 RolesState roleState = mPersistence.readForUser(UserHandle.of(mUserId)); 362 363 Map<String, Set<String>> roles; 364 if (roleState != null) { 365 mVersion = roleState.getVersion(); 366 mPackagesHash = roleState.getPackagesHash(); 367 roles = roleState.getRoles(); 368 } else { 369 roles = mPlatformHelper.getLegacyRoleState(mUserId); 370 } 371 mRoles.clear(); 372 for (Map.Entry<String, Set<String>> entry : roles.entrySet()) { 373 String roleName = entry.getKey(); 374 ArraySet<String> roleHolders = new ArraySet<>(entry.getValue()); 375 mRoles.put(roleName, roleHolders); 376 } 377 378 if (roleState == null) { 379 scheduleWriteFileLocked(); 380 } 381 } 382 } 383 384 /** 385 * Dump this user state. 386 * 387 * @param dumpOutputStream the output stream to dump to 388 */ dump(@onNull DualDumpOutputStream dumpOutputStream, @NonNull String fieldName, long fieldId)389 public void dump(@NonNull DualDumpOutputStream dumpOutputStream, @NonNull String fieldName, 390 long fieldId) { 391 int version; 392 String packagesHash; 393 ArrayMap<String, ArraySet<String>> roles; 394 synchronized (mLock) { 395 version = mVersion; 396 packagesHash = mPackagesHash; 397 roles = snapshotRolesLocked(); 398 } 399 400 long fieldToken = dumpOutputStream.start(fieldName, fieldId); 401 dumpOutputStream.write("user_id", RoleUserStateProto.USER_ID, mUserId); 402 dumpOutputStream.write("version", RoleUserStateProto.VERSION, version); 403 dumpOutputStream.write("packages_hash", RoleUserStateProto.PACKAGES_HASH, packagesHash); 404 405 int rolesSize = roles.size(); 406 for (int rolesIndex = 0; rolesIndex < rolesSize; rolesIndex++) { 407 String roleName = roles.keyAt(rolesIndex); 408 ArraySet<String> roleHolders = roles.valueAt(rolesIndex); 409 410 long rolesToken = dumpOutputStream.start("roles", RoleUserStateProto.ROLES); 411 dumpOutputStream.write("name", RoleProto.NAME, roleName); 412 413 int roleHoldersSize = roleHolders.size(); 414 for (int roleHoldersIndex = 0; roleHoldersIndex < roleHoldersSize; roleHoldersIndex++) { 415 String roleHolder = roleHolders.valueAt(roleHoldersIndex); 416 417 dumpOutputStream.write("holders", RoleProto.HOLDERS, roleHolder); 418 } 419 420 dumpOutputStream.end(rolesToken); 421 } 422 423 dumpOutputStream.end(fieldToken); 424 } 425 426 /** 427 * Get the roles and their holders. 428 * 429 * @return A copy of the roles and their holders 430 */ 431 @NonNull getRolesAndHolders()432 public ArrayMap<String, ArraySet<String>> getRolesAndHolders() { 433 synchronized (mLock) { 434 return snapshotRolesLocked(); 435 } 436 } 437 438 @GuardedBy("mLock") 439 @NonNull snapshotRolesLocked()440 private ArrayMap<String, ArraySet<String>> snapshotRolesLocked() { 441 ArrayMap<String, ArraySet<String>> roles = new ArrayMap<>(); 442 for (int i = 0, size = CollectionUtils.size(mRoles); i < size; ++i) { 443 String roleName = mRoles.keyAt(i); 444 ArraySet<String> roleHolders = mRoles.valueAt(i); 445 446 roleHolders = new ArraySet<>(roleHolders); 447 roles.put(roleName, roleHolders); 448 } 449 return roles; 450 } 451 452 /** 453 * Destroy this user state and delete the corresponding file. Any pending writes to the file 454 * will be cancelled, and any future interaction with this state will throw an exception. 455 */ destroy()456 public void destroy() { 457 synchronized (mLock) { 458 if (mDestroyed) { 459 throw new IllegalStateException("This RoleUserState has already been destroyed"); 460 } 461 mWriteHandler.removeCallbacksAndMessages(null); 462 mPersistence.deleteForUser(UserHandle.of(mUserId)); 463 mDestroyed = true; 464 } 465 } 466 467 /** 468 * Callback for a user state. 469 */ 470 public interface Callback { 471 472 /** 473 * Called when the holders of roles are changed. 474 * 475 * @param roleName the name of the role whose holders are changed 476 * @param userId the user id for this role holder change 477 */ onRoleHoldersChanged(@onNull String roleName, @UserIdInt int userId)478 void onRoleHoldersChanged(@NonNull String roleName, @UserIdInt int userId); 479 } 480 } 481