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