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