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