• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2020 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 package com.android.server.blob;
17 
18 import static android.app.blob.XmlTags.ATTR_COMMIT_TIME_MS;
19 import static android.app.blob.XmlTags.ATTR_DESCRIPTION;
20 import static android.app.blob.XmlTags.ATTR_DESCRIPTION_RES_NAME;
21 import static android.app.blob.XmlTags.ATTR_EXPIRY_TIME;
22 import static android.app.blob.XmlTags.ATTR_ID;
23 import static android.app.blob.XmlTags.ATTR_PACKAGE;
24 import static android.app.blob.XmlTags.ATTR_UID;
25 import static android.app.blob.XmlTags.ATTR_USER_ID;
26 import static android.app.blob.XmlTags.TAG_ACCESS_MODE;
27 import static android.app.blob.XmlTags.TAG_BLOB_HANDLE;
28 import static android.app.blob.XmlTags.TAG_COMMITTER;
29 import static android.app.blob.XmlTags.TAG_LEASEE;
30 import static android.os.Process.INVALID_UID;
31 import static android.system.OsConstants.O_RDONLY;
32 import static android.text.format.Formatter.FLAG_IEC_UNITS;
33 import static android.text.format.Formatter.formatFileSize;
34 
35 import static com.android.server.blob.BlobStoreConfig.TAG;
36 import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_COMMIT_TIME;
37 import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_DESC_RES_NAME;
38 import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_STRING_DESC;
39 import static com.android.server.blob.BlobStoreConfig.hasLeaseWaitTimeElapsed;
40 import static com.android.server.blob.BlobStoreUtils.getDescriptionResourceId;
41 import static com.android.server.blob.BlobStoreUtils.getPackageResources;
42 
43 import android.annotation.NonNull;
44 import android.annotation.Nullable;
45 import android.app.blob.BlobHandle;
46 import android.app.blob.LeaseInfo;
47 import android.content.Context;
48 import android.content.res.ResourceId;
49 import android.content.res.Resources;
50 import android.os.ParcelFileDescriptor;
51 import android.os.RevocableFileDescriptor;
52 import android.os.UserHandle;
53 import android.system.ErrnoException;
54 import android.system.Os;
55 import android.util.ArrayMap;
56 import android.util.ArraySet;
57 import android.util.Slog;
58 import android.util.SparseArray;
59 import android.util.StatsEvent;
60 import android.util.proto.ProtoOutputStream;
61 
62 import com.android.internal.annotations.GuardedBy;
63 import com.android.internal.annotations.VisibleForTesting;
64 import com.android.internal.util.IndentingPrintWriter;
65 import com.android.internal.util.XmlUtils;
66 import com.android.server.blob.BlobStoreManagerService.DumpArgs;
67 
68 import libcore.io.IoUtils;
69 
70 import org.xmlpull.v1.XmlPullParser;
71 import org.xmlpull.v1.XmlPullParserException;
72 import org.xmlpull.v1.XmlSerializer;
73 
74 import java.io.File;
75 import java.io.FileDescriptor;
76 import java.io.IOException;
77 import java.util.Objects;
78 import java.util.function.Consumer;
79 
80 class BlobMetadata {
81     private final Object mMetadataLock = new Object();
82 
83     private final Context mContext;
84 
85     private final long mBlobId;
86     private final BlobHandle mBlobHandle;
87     private final int mUserId;
88 
89     @GuardedBy("mMetadataLock")
90     private final ArraySet<Committer> mCommitters = new ArraySet<>();
91 
92     @GuardedBy("mMetadataLock")
93     private final ArraySet<Leasee> mLeasees = new ArraySet<>();
94 
95     /**
96      * Contains packageName -> {RevocableFileDescriptors}.
97      *
98      * Keep track of RevocableFileDescriptors given to clients which are not yet revoked/closed so
99      * that when clients access is revoked or the blob gets deleted, we can be sure that clients
100      * do not have any reference to the blob and the space occupied by the blob can be freed.
101      */
102     @GuardedBy("mRevocableFds")
103     private final ArrayMap<String, ArraySet<RevocableFileDescriptor>> mRevocableFds =
104             new ArrayMap<>();
105 
106     // Do not access this directly, instead use #getBlobFile().
107     private File mBlobFile;
108 
BlobMetadata(Context context, long blobId, BlobHandle blobHandle, int userId)109     BlobMetadata(Context context, long blobId, BlobHandle blobHandle, int userId) {
110         mContext = context;
111         this.mBlobId = blobId;
112         this.mBlobHandle = blobHandle;
113         this.mUserId = userId;
114     }
115 
getBlobId()116     long getBlobId() {
117         return mBlobId;
118     }
119 
getBlobHandle()120     BlobHandle getBlobHandle() {
121         return mBlobHandle;
122     }
123 
getUserId()124     int getUserId() {
125         return mUserId;
126     }
127 
addOrReplaceCommitter(@onNull Committer committer)128     void addOrReplaceCommitter(@NonNull Committer committer) {
129         synchronized (mMetadataLock) {
130             // We need to override the committer data, so first remove any existing
131             // committer before adding the new one.
132             mCommitters.remove(committer);
133             mCommitters.add(committer);
134         }
135     }
136 
setCommitters(ArraySet<Committer> committers)137     void setCommitters(ArraySet<Committer> committers) {
138         synchronized (mMetadataLock) {
139             mCommitters.clear();
140             mCommitters.addAll(committers);
141         }
142     }
143 
removeCommitter(@onNull String packageName, int uid)144     void removeCommitter(@NonNull String packageName, int uid) {
145         synchronized (mMetadataLock) {
146             mCommitters.removeIf((committer) ->
147                     committer.uid == uid && committer.packageName.equals(packageName));
148         }
149     }
150 
removeCommitter(@onNull Committer committer)151     void removeCommitter(@NonNull Committer committer) {
152         synchronized (mMetadataLock) {
153             mCommitters.remove(committer);
154         }
155     }
156 
removeCommittersFromUnknownPkgs(SparseArray<String> knownPackages)157     void removeCommittersFromUnknownPkgs(SparseArray<String> knownPackages) {
158         synchronized (mMetadataLock) {
159             mCommitters.removeIf(committer ->
160                     !committer.packageName.equals(knownPackages.get(committer.uid)));
161         }
162     }
163 
164     @Nullable
getExistingCommitter(@onNull String packageName, int uid)165     Committer getExistingCommitter(@NonNull String packageName, int uid) {
166         synchronized (mCommitters) {
167             for (int i = 0, size = mCommitters.size(); i < size; ++i) {
168                 final Committer committer = mCommitters.valueAt(i);
169                 if (committer.uid == uid && committer.packageName.equals(packageName)) {
170                     return committer;
171                 }
172             }
173         }
174         return null;
175     }
176 
addOrReplaceLeasee(String callingPackage, int callingUid, int descriptionResId, CharSequence description, long leaseExpiryTimeMillis)177     void addOrReplaceLeasee(String callingPackage, int callingUid, int descriptionResId,
178             CharSequence description, long leaseExpiryTimeMillis) {
179         synchronized (mMetadataLock) {
180             // We need to override the leasee data, so first remove any existing
181             // leasee before adding the new one.
182             final Leasee leasee = new Leasee(mContext, callingPackage, callingUid,
183                     descriptionResId, description, leaseExpiryTimeMillis);
184             mLeasees.remove(leasee);
185             mLeasees.add(leasee);
186         }
187     }
188 
setLeasees(ArraySet<Leasee> leasees)189     void setLeasees(ArraySet<Leasee> leasees) {
190         synchronized (mMetadataLock) {
191             mLeasees.clear();
192             mLeasees.addAll(leasees);
193         }
194     }
195 
removeLeasee(String packageName, int uid)196     void removeLeasee(String packageName, int uid) {
197         synchronized (mMetadataLock) {
198             mLeasees.removeIf((leasee) ->
199                     leasee.uid == uid && leasee.packageName.equals(packageName));
200         }
201     }
202 
removeLeaseesFromUnknownPkgs(SparseArray<String> knownPackages)203     void removeLeaseesFromUnknownPkgs(SparseArray<String> knownPackages) {
204         synchronized (mMetadataLock) {
205             mLeasees.removeIf(leasee ->
206                     !leasee.packageName.equals(knownPackages.get(leasee.uid)));
207         }
208     }
209 
removeExpiredLeases()210     void removeExpiredLeases() {
211         synchronized (mMetadataLock) {
212             mLeasees.removeIf(leasee -> !leasee.isStillValid());
213         }
214     }
215 
hasValidLeases()216     boolean hasValidLeases() {
217         synchronized (mMetadataLock) {
218             for (int i = 0, size = mLeasees.size(); i < size; ++i) {
219                 if (mLeasees.valueAt(i).isStillValid()) {
220                     return true;
221                 }
222             }
223             return false;
224         }
225     }
226 
getSize()227     long getSize() {
228         return getBlobFile().length();
229     }
230 
isAccessAllowedForCaller(@onNull String callingPackage, int callingUid)231     boolean isAccessAllowedForCaller(@NonNull String callingPackage, int callingUid) {
232         // Don't allow the blob to be accessed after it's expiry time has passed.
233         if (getBlobHandle().isExpired()) {
234             return false;
235         }
236         synchronized (mMetadataLock) {
237             // Check if packageName already holds a lease on the blob.
238             for (int i = 0, size = mLeasees.size(); i < size; ++i) {
239                 final Leasee leasee = mLeasees.valueAt(i);
240                 if (leasee.isStillValid() && leasee.equals(callingPackage, callingUid)) {
241                     return true;
242                 }
243             }
244 
245             for (int i = 0, size = mCommitters.size(); i < size; ++i) {
246                 final Committer committer = mCommitters.valueAt(i);
247 
248                 // Check if the caller is the same package that committed the blob.
249                 if (committer.equals(callingPackage, callingUid)) {
250                     return true;
251                 }
252 
253                 // Check if the caller is allowed access as per the access mode specified
254                 // by the committer.
255                 if (committer.blobAccessMode.isAccessAllowedForCaller(mContext,
256                         callingPackage, committer.packageName)) {
257                     return true;
258                 }
259             }
260         }
261         return false;
262     }
263 
isACommitter(@onNull String packageName, int uid)264     boolean isACommitter(@NonNull String packageName, int uid) {
265         synchronized (mMetadataLock) {
266             return isAnAccessor(mCommitters, packageName, uid);
267         }
268     }
269 
isALeasee(@ullable String packageName, int uid)270     boolean isALeasee(@Nullable String packageName, int uid) {
271         synchronized (mMetadataLock) {
272             final Leasee leasee = getAccessor(mLeasees, packageName, uid);
273             return leasee != null && leasee.isStillValid();
274         }
275     }
276 
isAnAccessor(@onNull ArraySet<T> accessors, @Nullable String packageName, int uid)277     private static <T extends Accessor> boolean isAnAccessor(@NonNull ArraySet<T> accessors,
278             @Nullable String packageName, int uid) {
279         // Check if the package is an accessor of the data blob.
280         return getAccessor(accessors, packageName, uid) != null;
281     }
282 
getAccessor(@onNull ArraySet<T> accessors, @Nullable String packageName, int uid)283     private static <T extends Accessor> T getAccessor(@NonNull ArraySet<T> accessors,
284             @Nullable String packageName, int uid) {
285         // Check if the package is an accessor of the data blob.
286         for (int i = 0, size = accessors.size(); i < size; ++i) {
287             final Accessor accessor = accessors.valueAt(i);
288             if (packageName != null && uid != INVALID_UID
289                     && accessor.equals(packageName, uid)) {
290                 return (T) accessor;
291             } else if (packageName != null && accessor.packageName.equals(packageName)) {
292                 return (T) accessor;
293             } else if (uid != INVALID_UID && accessor.uid == uid) {
294                 return (T) accessor;
295             }
296         }
297         return null;
298     }
299 
isALeasee(@onNull String packageName)300     boolean isALeasee(@NonNull String packageName) {
301         return isALeasee(packageName, INVALID_UID);
302     }
303 
isALeasee(int uid)304     boolean isALeasee(int uid) {
305         return isALeasee(null, uid);
306     }
307 
hasOtherLeasees(@onNull String packageName)308     boolean hasOtherLeasees(@NonNull String packageName) {
309         return hasOtherLeasees(packageName, INVALID_UID);
310     }
311 
hasOtherLeasees(int uid)312     boolean hasOtherLeasees(int uid) {
313         return hasOtherLeasees(null, uid);
314     }
315 
hasOtherLeasees(@ullable String packageName, int uid)316     private boolean hasOtherLeasees(@Nullable String packageName, int uid) {
317         synchronized (mMetadataLock) {
318             for (int i = 0, size = mLeasees.size(); i < size; ++i) {
319                 final Leasee leasee = mLeasees.valueAt(i);
320                 if (!leasee.isStillValid()) {
321                     continue;
322                 }
323                 // TODO: Also exclude packages which are signed with same cert?
324                 if (packageName != null && uid != INVALID_UID
325                         && !leasee.equals(packageName, uid)) {
326                     return true;
327                 } else if (packageName != null && !leasee.packageName.equals(packageName)) {
328                     return true;
329                 } else if (uid != INVALID_UID && leasee.uid != uid) {
330                     return true;
331                 }
332             }
333         }
334         return false;
335     }
336 
337     @Nullable
getLeaseInfo(@onNull String packageName, int uid)338     LeaseInfo getLeaseInfo(@NonNull String packageName, int uid) {
339         synchronized (mMetadataLock) {
340             for (int i = 0, size = mLeasees.size(); i < size; ++i) {
341                 final Leasee leasee = mLeasees.valueAt(i);
342                 if (!leasee.isStillValid()) {
343                     continue;
344                 }
345                 if (leasee.uid == uid && leasee.packageName.equals(packageName)) {
346                     final int descriptionResId = leasee.descriptionResEntryName == null
347                             ? Resources.ID_NULL
348                             : BlobStoreUtils.getDescriptionResourceId(
349                                     mContext, leasee.descriptionResEntryName, leasee.packageName,
350                                     UserHandle.getUserId(leasee.uid));
351                     return new LeaseInfo(packageName, leasee.expiryTimeMillis,
352                             descriptionResId, leasee.description);
353                 }
354             }
355         }
356         return null;
357     }
358 
forEachLeasee(Consumer<Leasee> consumer)359     void forEachLeasee(Consumer<Leasee> consumer) {
360         synchronized (mMetadataLock) {
361             mLeasees.forEach(consumer);
362         }
363     }
364 
getBlobFile()365     File getBlobFile() {
366         if (mBlobFile == null) {
367             mBlobFile = BlobStoreConfig.getBlobFile(mBlobId);
368         }
369         return mBlobFile;
370     }
371 
openForRead(String callingPackage)372     ParcelFileDescriptor openForRead(String callingPackage) throws IOException {
373         // TODO: Add limit on opened fds
374         FileDescriptor fd;
375         try {
376             fd = Os.open(getBlobFile().getPath(), O_RDONLY, 0);
377         } catch (ErrnoException e) {
378             throw e.rethrowAsIOException();
379         }
380         try {
381             if (BlobStoreConfig.shouldUseRevocableFdForReads()) {
382                 return createRevocableFd(fd, callingPackage);
383             } else {
384                 return new ParcelFileDescriptor(fd);
385             }
386         } catch (IOException e) {
387             IoUtils.closeQuietly(fd);
388             throw e;
389         }
390     }
391 
392     @NonNull
createRevocableFd(FileDescriptor fd, String callingPackage)393     private ParcelFileDescriptor createRevocableFd(FileDescriptor fd,
394             String callingPackage) throws IOException {
395         final RevocableFileDescriptor revocableFd =
396                 new RevocableFileDescriptor(mContext, fd);
397         synchronized (mRevocableFds) {
398             ArraySet<RevocableFileDescriptor> revocableFdsForPkg =
399                     mRevocableFds.get(callingPackage);
400             if (revocableFdsForPkg == null) {
401                 revocableFdsForPkg = new ArraySet<>();
402                 mRevocableFds.put(callingPackage, revocableFdsForPkg);
403             }
404             revocableFdsForPkg.add(revocableFd);
405         }
406         revocableFd.addOnCloseListener((e) -> {
407             synchronized (mRevocableFds) {
408                 final ArraySet<RevocableFileDescriptor> revocableFdsForPkg =
409                         mRevocableFds.get(callingPackage);
410                 if (revocableFdsForPkg != null) {
411                     revocableFdsForPkg.remove(revocableFd);
412                     if (revocableFdsForPkg.isEmpty()) {
413                         mRevocableFds.remove(callingPackage);
414                     }
415                 }
416             }
417         });
418         return revocableFd.getRevocableFileDescriptor();
419     }
420 
destroy()421     void destroy() {
422         revokeAllFds();
423         getBlobFile().delete();
424     }
425 
revokeAllFds()426     private void revokeAllFds() {
427         synchronized (mRevocableFds) {
428             for (int i = 0, pkgCount = mRevocableFds.size(); i < pkgCount; ++i) {
429                 final ArraySet<RevocableFileDescriptor> packageFds =
430                         mRevocableFds.valueAt(i);
431                 if (packageFds == null) {
432                     continue;
433                 }
434                 for (int j = 0, fdCount = packageFds.size(); j < fdCount; ++j) {
435                     packageFds.valueAt(j).revoke();
436                 }
437             }
438         }
439     }
440 
shouldBeDeleted(boolean respectLeaseWaitTime)441     boolean shouldBeDeleted(boolean respectLeaseWaitTime) {
442         // Expired data blobs
443         if (getBlobHandle().isExpired()) {
444             return true;
445         }
446 
447         // Blobs with no active leases
448         if ((!respectLeaseWaitTime || hasLeaseWaitTimeElapsedForAll())
449                 && !hasValidLeases()) {
450             return true;
451         }
452 
453         return false;
454     }
455 
456     @VisibleForTesting
hasLeaseWaitTimeElapsedForAll()457     boolean hasLeaseWaitTimeElapsedForAll() {
458         for (int i = 0, size = mCommitters.size(); i < size; ++i) {
459             final Committer committer = mCommitters.valueAt(i);
460             if (!hasLeaseWaitTimeElapsed(committer.getCommitTimeMs())) {
461                 return false;
462             }
463         }
464         return true;
465     }
466 
dumpAsStatsEvent(int atomTag)467     StatsEvent dumpAsStatsEvent(int atomTag) {
468         synchronized (mMetadataLock) {
469             ProtoOutputStream proto = new ProtoOutputStream();
470             // Write Committer data to proto format
471             for (int i = 0, size = mCommitters.size(); i < size; ++i) {
472                 final Committer committer = mCommitters.valueAt(i);
473                 final long token = proto.start(
474                         BlobStatsEventProto.BlobCommitterListProto.COMMITTER);
475                 proto.write(BlobStatsEventProto.BlobCommitterProto.UID, committer.uid);
476                 proto.write(BlobStatsEventProto.BlobCommitterProto.COMMIT_TIMESTAMP_MILLIS,
477                         committer.commitTimeMs);
478                 proto.write(BlobStatsEventProto.BlobCommitterProto.ACCESS_MODE,
479                         committer.blobAccessMode.getAccessType());
480                 proto.write(BlobStatsEventProto.BlobCommitterProto.NUM_WHITELISTED_PACKAGE,
481                         committer.blobAccessMode.getNumWhitelistedPackages());
482                 proto.end(token);
483             }
484             final byte[] committersBytes = proto.getBytes();
485 
486             proto = new ProtoOutputStream();
487             // Write Leasee data to proto format
488             for (int i = 0, size = mLeasees.size(); i < size; ++i) {
489                 final Leasee leasee = mLeasees.valueAt(i);
490                 final long token = proto.start(BlobStatsEventProto.BlobLeaseeListProto.LEASEE);
491                 proto.write(BlobStatsEventProto.BlobLeaseeProto.UID, leasee.uid);
492                 proto.write(BlobStatsEventProto.BlobLeaseeProto.LEASE_EXPIRY_TIMESTAMP_MILLIS,
493                         leasee.expiryTimeMillis);
494                 proto.end(token);
495             }
496             final byte[] leaseesBytes = proto.getBytes();
497 
498             // Construct the StatsEvent to represent this Blob
499             return StatsEvent.newBuilder()
500                     .setAtomId(atomTag)
501                     .writeLong(mBlobId)
502                     .writeLong(getSize())
503                     .writeLong(mBlobHandle.getExpiryTimeMillis())
504                     .writeByteArray(committersBytes)
505                     .writeByteArray(leaseesBytes)
506                     .build();
507         }
508     }
509 
dump(IndentingPrintWriter fout, DumpArgs dumpArgs)510     void dump(IndentingPrintWriter fout, DumpArgs dumpArgs) {
511         synchronized (mMetadataLock) {
512             fout.println("blobHandle:");
513             fout.increaseIndent();
514             mBlobHandle.dump(fout, dumpArgs.shouldDumpFull());
515             fout.decreaseIndent();
516             fout.println("size: " + formatFileSize(mContext, getSize(), FLAG_IEC_UNITS));
517 
518             fout.println("Committers:");
519             fout.increaseIndent();
520             if (mCommitters.isEmpty()) {
521                 fout.println("<empty>");
522             } else {
523                 for (int i = 0, count = mCommitters.size(); i < count; ++i) {
524                     final Committer committer = mCommitters.valueAt(i);
525                     fout.println("committer " + committer.toString());
526                     fout.increaseIndent();
527                     committer.dump(fout);
528                     fout.decreaseIndent();
529                 }
530             }
531             fout.decreaseIndent();
532 
533             fout.println("Leasees:");
534             fout.increaseIndent();
535             if (mLeasees.isEmpty()) {
536                 fout.println("<empty>");
537             } else {
538                 for (int i = 0, count = mLeasees.size(); i < count; ++i) {
539                     final Leasee leasee = mLeasees.valueAt(i);
540                     fout.println("leasee " + leasee.toString());
541                     fout.increaseIndent();
542                     leasee.dump(mContext, fout);
543                     fout.decreaseIndent();
544                 }
545             }
546             fout.decreaseIndent();
547 
548             fout.println("Open fds:");
549             fout.increaseIndent();
550             if (mRevocableFds.isEmpty()) {
551                 fout.println("<empty>");
552             } else {
553                 for (int i = 0, count = mRevocableFds.size(); i < count; ++i) {
554                     final String packageName = mRevocableFds.keyAt(i);
555                     final ArraySet<RevocableFileDescriptor> packageFds =
556                             mRevocableFds.valueAt(i);
557                     fout.println(packageName + "#" + packageFds.size());
558                 }
559             }
560             fout.decreaseIndent();
561         }
562     }
563 
writeToXml(XmlSerializer out)564     void writeToXml(XmlSerializer out) throws IOException {
565         synchronized (mMetadataLock) {
566             XmlUtils.writeLongAttribute(out, ATTR_ID, mBlobId);
567             XmlUtils.writeIntAttribute(out, ATTR_USER_ID, mUserId);
568 
569             out.startTag(null, TAG_BLOB_HANDLE);
570             mBlobHandle.writeToXml(out);
571             out.endTag(null, TAG_BLOB_HANDLE);
572 
573             for (int i = 0, count = mCommitters.size(); i < count; ++i) {
574                 out.startTag(null, TAG_COMMITTER);
575                 mCommitters.valueAt(i).writeToXml(out);
576                 out.endTag(null, TAG_COMMITTER);
577             }
578 
579             for (int i = 0, count = mLeasees.size(); i < count; ++i) {
580                 out.startTag(null, TAG_LEASEE);
581                 mLeasees.valueAt(i).writeToXml(out);
582                 out.endTag(null, TAG_LEASEE);
583             }
584         }
585     }
586 
587     @Nullable
createFromXml(XmlPullParser in, int version, Context context)588     static BlobMetadata createFromXml(XmlPullParser in, int version, Context context)
589             throws XmlPullParserException, IOException {
590         final long blobId = XmlUtils.readLongAttribute(in, ATTR_ID);
591         final int userId = XmlUtils.readIntAttribute(in, ATTR_USER_ID);
592 
593         BlobHandle blobHandle = null;
594         final ArraySet<Committer> committers = new ArraySet<>();
595         final ArraySet<Leasee> leasees = new ArraySet<>();
596         final int depth = in.getDepth();
597         while (XmlUtils.nextElementWithin(in, depth)) {
598             if (TAG_BLOB_HANDLE.equals(in.getName())) {
599                 blobHandle = BlobHandle.createFromXml(in);
600             } else if (TAG_COMMITTER.equals(in.getName())) {
601                 final Committer committer = Committer.createFromXml(in, version);
602                 if (committer != null) {
603                     committers.add(committer);
604                 }
605             } else if (TAG_LEASEE.equals(in.getName())) {
606                 leasees.add(Leasee.createFromXml(in, version));
607             }
608         }
609 
610         if (blobHandle == null) {
611             Slog.wtf(TAG, "blobHandle should be available");
612             return null;
613         }
614 
615         final BlobMetadata blobMetadata = new BlobMetadata(context, blobId, blobHandle, userId);
616         blobMetadata.setCommitters(committers);
617         blobMetadata.setLeasees(leasees);
618         return blobMetadata;
619     }
620 
621     static final class Committer extends Accessor {
622         public final BlobAccessMode blobAccessMode;
623         public final long commitTimeMs;
624 
Committer(String packageName, int uid, BlobAccessMode blobAccessMode, long commitTimeMs)625         Committer(String packageName, int uid, BlobAccessMode blobAccessMode, long commitTimeMs) {
626             super(packageName, uid);
627             this.blobAccessMode = blobAccessMode;
628             this.commitTimeMs = commitTimeMs;
629         }
630 
getCommitTimeMs()631         long getCommitTimeMs() {
632             return commitTimeMs;
633         }
634 
dump(IndentingPrintWriter fout)635         void dump(IndentingPrintWriter fout) {
636             fout.println("commit time: "
637                     + (commitTimeMs == 0 ? "<null>" : BlobStoreUtils.formatTime(commitTimeMs)));
638             fout.println("accessMode:");
639             fout.increaseIndent();
640             blobAccessMode.dump(fout);
641             fout.decreaseIndent();
642         }
643 
writeToXml(@onNull XmlSerializer out)644         void writeToXml(@NonNull XmlSerializer out) throws IOException {
645             XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, packageName);
646             XmlUtils.writeIntAttribute(out, ATTR_UID, uid);
647             XmlUtils.writeLongAttribute(out, ATTR_COMMIT_TIME_MS, commitTimeMs);
648 
649             out.startTag(null, TAG_ACCESS_MODE);
650             blobAccessMode.writeToXml(out);
651             out.endTag(null, TAG_ACCESS_MODE);
652         }
653 
654         @Nullable
createFromXml(@onNull XmlPullParser in, int version)655         static Committer createFromXml(@NonNull XmlPullParser in, int version)
656                 throws XmlPullParserException, IOException {
657             final String packageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE);
658             final int uid = XmlUtils.readIntAttribute(in, ATTR_UID);
659             final long commitTimeMs = version >= XML_VERSION_ADD_COMMIT_TIME
660                     ? XmlUtils.readLongAttribute(in, ATTR_COMMIT_TIME_MS)
661                     : 0;
662 
663             final int depth = in.getDepth();
664             BlobAccessMode blobAccessMode = null;
665             while (XmlUtils.nextElementWithin(in, depth)) {
666                 if (TAG_ACCESS_MODE.equals(in.getName())) {
667                     blobAccessMode = BlobAccessMode.createFromXml(in);
668                 }
669             }
670             if (blobAccessMode == null) {
671                 Slog.wtf(TAG, "blobAccessMode should be available");
672                 return null;
673             }
674             return new Committer(packageName, uid, blobAccessMode, commitTimeMs);
675         }
676     }
677 
678     static final class Leasee extends Accessor {
679         public final String descriptionResEntryName;
680         public final CharSequence description;
681         public final long expiryTimeMillis;
682 
Leasee(@onNull Context context, @NonNull String packageName, int uid, int descriptionResId, @Nullable CharSequence description, long expiryTimeMillis)683         Leasee(@NonNull Context context, @NonNull String packageName,
684                 int uid, int descriptionResId,
685                 @Nullable CharSequence description, long expiryTimeMillis) {
686             super(packageName, uid);
687             final Resources packageResources = getPackageResources(context, packageName,
688                     UserHandle.getUserId(uid));
689             this.descriptionResEntryName = getResourceEntryName(packageResources, descriptionResId);
690             this.expiryTimeMillis = expiryTimeMillis;
691             this.description = description == null
692                     ? getDescription(packageResources, descriptionResId)
693                     : description;
694         }
695 
Leasee(String packageName, int uid, @Nullable String descriptionResEntryName, @Nullable CharSequence description, long expiryTimeMillis)696         Leasee(String packageName, int uid, @Nullable String descriptionResEntryName,
697                 @Nullable CharSequence description, long expiryTimeMillis) {
698             super(packageName, uid);
699             this.descriptionResEntryName = descriptionResEntryName;
700             this.expiryTimeMillis = expiryTimeMillis;
701             this.description = description;
702         }
703 
704         @Nullable
getResourceEntryName(@ullable Resources packageResources, int resId)705         private static String getResourceEntryName(@Nullable Resources packageResources,
706                 int resId) {
707             if (!ResourceId.isValid(resId) || packageResources == null) {
708                 return null;
709             }
710             return packageResources.getResourceEntryName(resId);
711         }
712 
713         @Nullable
getDescription(@onNull Context context, @NonNull String descriptionResEntryName, @NonNull String packageName, int userId)714         private static String getDescription(@NonNull Context context,
715                 @NonNull String descriptionResEntryName, @NonNull String packageName, int userId) {
716             if (descriptionResEntryName == null || descriptionResEntryName.isEmpty()) {
717                 return null;
718             }
719             final Resources resources = getPackageResources(context, packageName, userId);
720             if (resources == null) {
721                 return null;
722             }
723             final int resId = getDescriptionResourceId(resources, descriptionResEntryName,
724                     packageName);
725             return resId == Resources.ID_NULL ? null : resources.getString(resId);
726         }
727 
728         @Nullable
getDescription(@ullable Resources packageResources, int descriptionResId)729         private static String getDescription(@Nullable Resources packageResources,
730                 int descriptionResId) {
731             if (!ResourceId.isValid(descriptionResId) || packageResources == null) {
732                 return null;
733             }
734             return packageResources.getString(descriptionResId);
735         }
736 
isStillValid()737         boolean isStillValid() {
738             return expiryTimeMillis == 0 || expiryTimeMillis >= System.currentTimeMillis();
739         }
740 
dump(@onNull Context context, @NonNull IndentingPrintWriter fout)741         void dump(@NonNull Context context, @NonNull IndentingPrintWriter fout) {
742             fout.println("desc: " + getDescriptionToDump(context));
743             fout.println("expiryMs: " + expiryTimeMillis);
744         }
745 
746         @NonNull
getDescriptionToDump(@onNull Context context)747         private String getDescriptionToDump(@NonNull Context context) {
748             String desc = getDescription(context, descriptionResEntryName, packageName,
749                     UserHandle.getUserId(uid));
750             if (desc == null) {
751                 desc = description.toString();
752             }
753             return desc == null ? "<none>" : desc;
754         }
755 
writeToXml(@onNull XmlSerializer out)756         void writeToXml(@NonNull XmlSerializer out) throws IOException {
757             XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, packageName);
758             XmlUtils.writeIntAttribute(out, ATTR_UID, uid);
759             XmlUtils.writeStringAttribute(out, ATTR_DESCRIPTION_RES_NAME, descriptionResEntryName);
760             XmlUtils.writeLongAttribute(out, ATTR_EXPIRY_TIME, expiryTimeMillis);
761             XmlUtils.writeStringAttribute(out, ATTR_DESCRIPTION, description);
762         }
763 
764         @NonNull
createFromXml(@onNull XmlPullParser in, int version)765         static Leasee createFromXml(@NonNull XmlPullParser in, int version)
766                 throws IOException {
767             final String packageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE);
768             final int uid = XmlUtils.readIntAttribute(in, ATTR_UID);
769             final String descriptionResEntryName;
770             if (version >= XML_VERSION_ADD_DESC_RES_NAME) {
771                 descriptionResEntryName = XmlUtils.readStringAttribute(
772                         in, ATTR_DESCRIPTION_RES_NAME);
773             } else {
774                 descriptionResEntryName = null;
775             }
776             final long expiryTimeMillis = XmlUtils.readLongAttribute(in, ATTR_EXPIRY_TIME);
777             final CharSequence description;
778             if (version >= XML_VERSION_ADD_STRING_DESC) {
779                 description = XmlUtils.readStringAttribute(in, ATTR_DESCRIPTION);
780             } else {
781                 description = null;
782             }
783 
784             return new Leasee(packageName, uid, descriptionResEntryName,
785                     description, expiryTimeMillis);
786         }
787     }
788 
789     static class Accessor {
790         public final String packageName;
791         public final int uid;
792 
Accessor(String packageName, int uid)793         Accessor(String packageName, int uid) {
794             this.packageName = packageName;
795             this.uid = uid;
796         }
797 
equals(String packageName, int uid)798         public boolean equals(String packageName, int uid) {
799             return this.uid == uid && this.packageName.equals(packageName);
800         }
801 
802         @Override
equals(Object obj)803         public boolean equals(Object obj) {
804             if (this == obj) {
805                 return true;
806             }
807             if (obj == null || !(obj instanceof Accessor)) {
808                 return false;
809             }
810             final Accessor other = (Accessor) obj;
811             return this.uid == other.uid && this.packageName.equals(other.packageName);
812         }
813 
814         @Override
hashCode()815         public int hashCode() {
816             return Objects.hash(packageName, uid);
817         }
818 
819         @Override
toString()820         public String toString() {
821             return "[" + packageName + ", " + uid + "]";
822         }
823     }
824 }
825