/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.server.blob;

import static android.Manifest.permission.ACCESS_BLOBS_ACROSS_USERS;
import static android.app.blob.XmlTags.ATTR_COMMIT_TIME_MS;
import static android.app.blob.XmlTags.ATTR_DESCRIPTION;
import static android.app.blob.XmlTags.ATTR_DESCRIPTION_RES_NAME;
import static android.app.blob.XmlTags.ATTR_EXPIRY_TIME;
import static android.app.blob.XmlTags.ATTR_ID;
import static android.app.blob.XmlTags.ATTR_PACKAGE;
import static android.app.blob.XmlTags.ATTR_UID;
import static android.app.blob.XmlTags.ATTR_USER_ID;
import static android.app.blob.XmlTags.TAG_ACCESS_MODE;
import static android.app.blob.XmlTags.TAG_BLOB_HANDLE;
import static android.app.blob.XmlTags.TAG_COMMITTER;
import static android.app.blob.XmlTags.TAG_LEASEE;
import static android.os.Process.INVALID_UID;
import static android.system.OsConstants.O_RDONLY;
import static android.text.format.Formatter.FLAG_IEC_UNITS;
import static android.text.format.Formatter.formatFileSize;

import static com.android.server.blob.BlobStoreConfig.TAG;
import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_COMMIT_TIME;
import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_DESC_RES_NAME;
import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_STRING_DESC;
import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ALLOW_ACCESS_ACROSS_USERS;
import static com.android.server.blob.BlobStoreConfig.hasLeaseWaitTimeElapsed;
import static com.android.server.blob.BlobStoreUtils.getDescriptionResourceId;
import static com.android.server.blob.BlobStoreUtils.getPackageResources;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.blob.BlobHandle;
import android.app.blob.LeaseInfo;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.ResourceId;
import android.content.res.Resources;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.os.RevocableFileDescriptor;
import android.os.UserHandle;
import android.permission.PermissionManager;
import android.system.ErrnoException;
import android.system.Os;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.SparseArray;
import android.util.StatsEvent;
import android.util.proto.ProtoOutputStream;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.XmlUtils;
import com.android.server.blob.BlobStoreManagerService.DumpArgs;

import libcore.io.IoUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.util.Objects;
import java.util.function.Consumer;

class BlobMetadata {
    private final Object mMetadataLock = new Object();

    private final Context mContext;

    private final long mBlobId;
    private final BlobHandle mBlobHandle;

    @GuardedBy("mMetadataLock")
    private final ArraySet<Committer> mCommitters = new ArraySet<>();

    @GuardedBy("mMetadataLock")
    private final ArraySet<Leasee> mLeasees = new ArraySet<>();

    /**
     * Contains Accessor -> {RevocableFileDescriptors}.
     *
     * Keep track of RevocableFileDescriptors given to clients which are not yet revoked/closed so
     * that when clients access is revoked or the blob gets deleted, we can be sure that clients
     * do not have any reference to the blob and the space occupied by the blob can be freed.
     */
    @GuardedBy("mRevocableFds")
    private final ArrayMap<Accessor, ArraySet<RevocableFileDescriptor>> mRevocableFds =
            new ArrayMap<>();

    // Do not access this directly, instead use #getBlobFile().
    private File mBlobFile;

    BlobMetadata(Context context, long blobId, BlobHandle blobHandle) {
        mContext = context;
        this.mBlobId = blobId;
        this.mBlobHandle = blobHandle;
    }

    long getBlobId() {
        return mBlobId;
    }

    BlobHandle getBlobHandle() {
        return mBlobHandle;
    }

    void addOrReplaceCommitter(@NonNull Committer committer) {
        synchronized (mMetadataLock) {
            // We need to override the committer data, so first remove any existing
            // committer before adding the new one.
            mCommitters.remove(committer);
            mCommitters.add(committer);
        }
    }

    void setCommitters(ArraySet<Committer> committers) {
        synchronized (mMetadataLock) {
            mCommitters.clear();
            mCommitters.addAll(committers);
        }
    }

    void removeCommitter(@NonNull String packageName, int uid) {
        synchronized (mMetadataLock) {
            mCommitters.removeIf((committer) ->
                    committer.uid == uid && committer.packageName.equals(packageName));
        }
    }

    void removeCommitter(@NonNull Committer committer) {
        synchronized (mMetadataLock) {
            mCommitters.remove(committer);
        }
    }

    void removeCommittersFromUnknownPkgs(SparseArray<SparseArray<String>> knownPackages) {
        synchronized (mMetadataLock) {
            mCommitters.removeIf(committer -> {
                final int userId = UserHandle.getUserId(committer.uid);
                final SparseArray<String> userPackages = knownPackages.get(userId);
                if (userPackages == null) {
                    return true;
                }
                return !committer.packageName.equals(userPackages.get(committer.uid));
            });
        }
    }

    void addCommittersAndLeasees(BlobMetadata blobMetadata) {
        mCommitters.addAll(blobMetadata.mCommitters);
        mLeasees.addAll(blobMetadata.mLeasees);
    }

    @Nullable
    Committer getExistingCommitter(@NonNull String packageName, int uid) {
        synchronized (mCommitters) {
            for (int i = 0, size = mCommitters.size(); i < size; ++i) {
                final Committer committer = mCommitters.valueAt(i);
                if (committer.uid == uid && committer.packageName.equals(packageName)) {
                    return committer;
                }
            }
        }
        return null;
    }

    void addOrReplaceLeasee(String callingPackage, int callingUid, int descriptionResId,
            CharSequence description, long leaseExpiryTimeMillis) {
        synchronized (mMetadataLock) {
            // We need to override the leasee data, so first remove any existing
            // leasee before adding the new one.
            final Leasee leasee = new Leasee(mContext, callingPackage, callingUid,
                    descriptionResId, description, leaseExpiryTimeMillis);
            mLeasees.remove(leasee);
            mLeasees.add(leasee);
        }
    }

    void setLeasees(ArraySet<Leasee> leasees) {
        synchronized (mMetadataLock) {
            mLeasees.clear();
            mLeasees.addAll(leasees);
        }
    }

    void removeLeasee(String packageName, int uid) {
        synchronized (mMetadataLock) {
            mLeasees.removeIf((leasee) ->
                    leasee.uid == uid && leasee.packageName.equals(packageName));
        }
    }

    void removeLeaseesFromUnknownPkgs(SparseArray<SparseArray<String>> knownPackages) {
        synchronized (mMetadataLock) {
            mLeasees.removeIf(leasee -> {
                final int userId = UserHandle.getUserId(leasee.uid);
                final SparseArray<String> userPackages = knownPackages.get(userId);
                if (userPackages == null) {
                    return true;
                }
                return !leasee.packageName.equals(userPackages.get(leasee.uid));
            });
        }
    }

    void removeExpiredLeases() {
        synchronized (mMetadataLock) {
            mLeasees.removeIf(leasee -> !leasee.isStillValid());
        }
    }

    void removeDataForUser(int userId) {
        synchronized (mMetadataLock) {
            mCommitters.removeIf(committer -> (userId == UserHandle.getUserId(committer.uid)));
            mLeasees.removeIf(leasee -> (userId == UserHandle.getUserId(leasee.uid)));
            mRevocableFds.entrySet().removeIf(entry -> {
                final Accessor accessor = entry.getKey();
                final ArraySet<RevocableFileDescriptor> rFds = entry.getValue();
                if (userId != UserHandle.getUserId(accessor.uid)) {
                    return false;
                }
                for (int i = 0, fdCount = rFds.size(); i < fdCount; ++i) {
                    rFds.valueAt(i).revoke();
                }
                rFds.clear();
                return true;
            });
        }
    }

    boolean hasValidLeases() {
        synchronized (mMetadataLock) {
            for (int i = 0, size = mLeasees.size(); i < size; ++i) {
                if (mLeasees.valueAt(i).isStillValid()) {
                    return true;
                }
            }
            return false;
        }
    }

    long getSize() {
        return getBlobFile().length();
    }

    boolean isAccessAllowedForCaller(@NonNull String callingPackage, int callingUid) {
        // Don't allow the blob to be accessed after it's expiry time has passed.
        if (getBlobHandle().isExpired()) {
            return false;
        }
        synchronized (mMetadataLock) {
            // Check if packageName already holds a lease on the blob.
            for (int i = 0, size = mLeasees.size(); i < size; ++i) {
                final Leasee leasee = mLeasees.valueAt(i);
                if (leasee.isStillValid() && leasee.equals(callingPackage, callingUid)) {
                    return true;
                }
            }

            final int callingUserId = UserHandle.getUserId(callingUid);
            for (int i = 0, size = mCommitters.size(); i < size; ++i) {
                final Committer committer = mCommitters.valueAt(i);
                if (callingUserId != UserHandle.getUserId(committer.uid)) {
                    continue;
                }

                // Check if the caller is the same package that committed the blob.
                if (committer.equals(callingPackage, callingUid)) {
                    return true;
                }

                // Check if the caller is allowed access as per the access mode specified
                // by the committer.
                if (committer.blobAccessMode.isAccessAllowedForCaller(mContext,
                        callingPackage, committer.packageName)) {
                    return true;
                }
            }

            final boolean canCallerAccessBlobsAcrossUsers = checkCallerCanAccessBlobsAcrossUsers(
                    callingPackage, callingUserId);
            if (!canCallerAccessBlobsAcrossUsers) {
                return false;
            }
            for (int i = 0, size = mCommitters.size(); i < size; ++i) {
                final Committer committer = mCommitters.valueAt(i);
                final int committerUserId = UserHandle.getUserId(committer.uid);
                if (callingUserId == committerUserId) {
                    continue;
                }
                if (!isPackageInstalledOnUser(callingPackage, committerUserId)) {
                    continue;
                }

                // Check if the caller is allowed access as per the access mode specified
                // by the committer.
                if (committer.blobAccessMode.isAccessAllowedForCaller(mContext,
                        callingPackage, committer.packageName)) {
                    return true;
                }
            }

        }
        return false;
    }

    private static boolean checkCallerCanAccessBlobsAcrossUsers(
            String callingPackage, int callingUserId) {
        final long token = Binder.clearCallingIdentity();
        try {
            return PermissionManager.checkPackageNamePermission(ACCESS_BLOBS_ACROSS_USERS,
                    callingPackage, callingUserId) == PackageManager.PERMISSION_GRANTED;
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    private boolean isPackageInstalledOnUser(String packageName, int userId) {
        final long token = Binder.clearCallingIdentity();
        try {
            mContext.getPackageManager().getPackageInfoAsUser(packageName, 0, userId);
            return true;
        } catch (PackageManager.NameNotFoundException e) {
            return false;
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    boolean hasACommitterOrLeaseeInUser(int userId) {
        return hasACommitterInUser(userId) || hasALeaseeInUser(userId);
    }

    boolean hasACommitterInUser(int userId) {
        synchronized (mMetadataLock) {
            for (int i = 0, size = mCommitters.size(); i < size; ++i) {
                final Committer committer = mCommitters.valueAt(i);
                if (userId == UserHandle.getUserId(committer.uid)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean hasALeaseeInUser(int userId) {
        synchronized (mMetadataLock) {
            for (int i = 0, size = mLeasees.size(); i < size; ++i) {
                final Leasee leasee = mLeasees.valueAt(i);
                if (userId == UserHandle.getUserId(leasee.uid)) {
                    return true;
                }
            }
        }
        return false;
    }

    boolean isACommitter(@NonNull String packageName, int uid) {
        synchronized (mMetadataLock) {
            return isAnAccessor(mCommitters, packageName, uid, UserHandle.getUserId(uid));
        }
    }

    boolean isALeasee(@Nullable String packageName, int uid) {
        synchronized (mMetadataLock) {
            final Leasee leasee = getAccessor(mLeasees, packageName, uid,
                    UserHandle.getUserId(uid));
            return leasee != null && leasee.isStillValid();
        }
    }

    private boolean isALeaseeInUser(@Nullable String packageName, int uid, int userId) {
        synchronized (mMetadataLock) {
            final Leasee leasee = getAccessor(mLeasees, packageName, uid, userId);
            return leasee != null && leasee.isStillValid();
        }
    }

    private static <T extends Accessor> boolean isAnAccessor(@NonNull ArraySet<T> accessors,
            @Nullable String packageName, int uid, int userId) {
        // Check if the package is an accessor of the data blob.
        return getAccessor(accessors, packageName, uid, userId) != null;
    }

    private static <T extends Accessor> T getAccessor(@NonNull ArraySet<T> accessors,
            @Nullable String packageName, int uid, int userId) {
        // Check if the package is an accessor of the data blob.
        for (int i = 0, size = accessors.size(); i < size; ++i) {
            final Accessor accessor = accessors.valueAt(i);
            if (packageName != null && uid != INVALID_UID
                    && accessor.equals(packageName, uid)) {
                return (T) accessor;
            } else if (packageName != null && accessor.packageName.equals(packageName)
                    && userId == UserHandle.getUserId(accessor.uid)) {
                return (T) accessor;
            } else if (uid != INVALID_UID && accessor.uid == uid) {
                return (T) accessor;
            }
        }
        return null;
    }

    boolean shouldAttributeToUser(int userId) {
        synchronized (mMetadataLock) {
            for (int i = 0, size = mLeasees.size(); i < size; ++i) {
                final Leasee leasee = mLeasees.valueAt(i);
                // Don't attribute the blob to userId if there is a lease on it from another user.
                if (userId != UserHandle.getUserId(leasee.uid)) {
                    return false;
                }
            }
        }
        return true;
    }

    boolean shouldAttributeToLeasee(@NonNull String packageName, int userId,
            boolean callerHasStatsPermission) {
        if (!isALeaseeInUser(packageName, INVALID_UID, userId)) {
            return false;
        }
        if (!callerHasStatsPermission || !hasOtherLeasees(packageName, INVALID_UID, userId)) {
            return true;
        }
        return false;
    }

    boolean shouldAttributeToLeasee(int uid, boolean callerHasStatsPermission) {
        final int userId = UserHandle.getUserId(uid);
        if (!isALeaseeInUser(null, uid, userId)) {
            return false;
        }
        if (!callerHasStatsPermission || !hasOtherLeasees(null, uid, userId)) {
            return true;
        }
        return false;
    }

    private boolean hasOtherLeasees(@Nullable String packageName, int uid, int userId) {
        synchronized (mMetadataLock) {
            for (int i = 0, size = mLeasees.size(); i < size; ++i) {
                final Leasee leasee = mLeasees.valueAt(i);
                if (!leasee.isStillValid()) {
                    continue;
                }
                // TODO: Also exclude packages which are signed with same cert?
                if (packageName != null && uid != INVALID_UID
                        && !leasee.equals(packageName, uid)) {
                    return true;
                } else if (packageName != null && (!leasee.packageName.equals(packageName)
                        || userId != UserHandle.getUserId(leasee.uid))) {
                    return true;
                } else if (uid != INVALID_UID && leasee.uid != uid) {
                    return true;
                }
            }
        }
        return false;
    }

    @Nullable
    LeaseInfo getLeaseInfo(@NonNull String packageName, int uid) {
        synchronized (mMetadataLock) {
            for (int i = 0, size = mLeasees.size(); i < size; ++i) {
                final Leasee leasee = mLeasees.valueAt(i);
                if (!leasee.isStillValid()) {
                    continue;
                }
                if (leasee.uid == uid && leasee.packageName.equals(packageName)) {
                    final int descriptionResId = leasee.descriptionResEntryName == null
                            ? Resources.ID_NULL
                            : BlobStoreUtils.getDescriptionResourceId(
                                    mContext, leasee.descriptionResEntryName, leasee.packageName,
                                    UserHandle.getUserId(leasee.uid));
                    return new LeaseInfo(packageName, leasee.expiryTimeMillis,
                            descriptionResId, leasee.description);
                }
            }
        }
        return null;
    }

    void forEachLeasee(Consumer<Leasee> consumer) {
        synchronized (mMetadataLock) {
            mLeasees.forEach(consumer);
        }
    }

    File getBlobFile() {
        if (mBlobFile == null) {
            mBlobFile = BlobStoreConfig.getBlobFile(mBlobId);
        }
        return mBlobFile;
    }

    ParcelFileDescriptor openForRead(String callingPackage, int callingUid) throws IOException {
        // TODO: Add limit on opened fds
        FileDescriptor fd;
        try {
            fd = Os.open(getBlobFile().getPath(), O_RDONLY, 0);
        } catch (ErrnoException e) {
            throw e.rethrowAsIOException();
        }
        try {
            if (BlobStoreConfig.shouldUseRevocableFdForReads()) {
                return createRevocableFd(fd, callingPackage, callingUid);
            } else {
                return new ParcelFileDescriptor(fd);
            }
        } catch (IOException e) {
            IoUtils.closeQuietly(fd);
            throw e;
        }
    }

    @NonNull
    private ParcelFileDescriptor createRevocableFd(FileDescriptor fd,
            String callingPackage, int callingUid) throws IOException {
        final RevocableFileDescriptor revocableFd =
                new RevocableFileDescriptor(mContext, fd);
        final Accessor accessor;
        synchronized (mRevocableFds) {
            accessor = new Accessor(callingPackage, callingUid);
            ArraySet<RevocableFileDescriptor> revocableFdsForAccessor =
                    mRevocableFds.get(accessor);
            if (revocableFdsForAccessor == null) {
                revocableFdsForAccessor = new ArraySet<>();
                mRevocableFds.put(accessor, revocableFdsForAccessor);
            }
            revocableFdsForAccessor.add(revocableFd);
        }
        revocableFd.addOnCloseListener((e) -> {
            synchronized (mRevocableFds) {
                final ArraySet<RevocableFileDescriptor> revocableFdsForAccessor =
                        mRevocableFds.get(accessor);
                if (revocableFdsForAccessor != null) {
                    revocableFdsForAccessor.remove(revocableFd);
                    if (revocableFdsForAccessor.isEmpty()) {
                        mRevocableFds.remove(accessor);
                    }
                }
            }
        });
        return revocableFd.getRevocableFileDescriptor();
    }

    void destroy() {
        revokeAndClearAllFds();
        getBlobFile().delete();
    }

    private void revokeAndClearAllFds() {
        synchronized (mRevocableFds) {
            for (int i = 0, accessorCount = mRevocableFds.size(); i < accessorCount; ++i) {
                final ArraySet<RevocableFileDescriptor> rFds =
                        mRevocableFds.valueAt(i);
                if (rFds == null) {
                    continue;
                }
                for (int j = 0, fdCount = rFds.size(); j < fdCount; ++j) {
                    rFds.valueAt(j).revoke();
                }
            }
            mRevocableFds.clear();
        }
    }

    boolean shouldBeDeleted(boolean respectLeaseWaitTime) {
        // Expired data blobs
        if (getBlobHandle().isExpired()) {
            return true;
        }

        // Blobs with no active leases
        if ((!respectLeaseWaitTime || hasLeaseWaitTimeElapsedForAll())
                && !hasValidLeases()) {
            return true;
        }

        return false;
    }

    @VisibleForTesting
    boolean hasLeaseWaitTimeElapsedForAll() {
        for (int i = 0, size = mCommitters.size(); i < size; ++i) {
            final Committer committer = mCommitters.valueAt(i);
            if (!hasLeaseWaitTimeElapsed(committer.getCommitTimeMs())) {
                return false;
            }
        }
        return true;
    }

    StatsEvent dumpAsStatsEvent(int atomTag) {
        synchronized (mMetadataLock) {
            ProtoOutputStream proto = new ProtoOutputStream();
            // Write Committer data to proto format
            for (int i = 0, size = mCommitters.size(); i < size; ++i) {
                final Committer committer = mCommitters.valueAt(i);
                final long token = proto.start(
                        BlobStatsEventProto.BlobCommitterListProto.COMMITTER);
                proto.write(BlobStatsEventProto.BlobCommitterProto.UID, committer.uid);
                proto.write(BlobStatsEventProto.BlobCommitterProto.COMMIT_TIMESTAMP_MILLIS,
                        committer.commitTimeMs);
                proto.write(BlobStatsEventProto.BlobCommitterProto.ACCESS_MODE,
                        committer.blobAccessMode.getAccessType());
                proto.write(BlobStatsEventProto.BlobCommitterProto.NUM_WHITELISTED_PACKAGE,
                        committer.blobAccessMode.getAllowedPackagesCount());
                proto.end(token);
            }
            final byte[] committersBytes = proto.getBytes();

            proto = new ProtoOutputStream();
            // Write Leasee data to proto format
            for (int i = 0, size = mLeasees.size(); i < size; ++i) {
                final Leasee leasee = mLeasees.valueAt(i);
                final long token = proto.start(BlobStatsEventProto.BlobLeaseeListProto.LEASEE);
                proto.write(BlobStatsEventProto.BlobLeaseeProto.UID, leasee.uid);
                proto.write(BlobStatsEventProto.BlobLeaseeProto.LEASE_EXPIRY_TIMESTAMP_MILLIS,
                        leasee.expiryTimeMillis);
                proto.end(token);
            }
            final byte[] leaseesBytes = proto.getBytes();

            // Construct the StatsEvent to represent this Blob
            return FrameworkStatsLog.buildStatsEvent(atomTag, mBlobId, getSize(),
                    mBlobHandle.getExpiryTimeMillis(), committersBytes, leaseesBytes);
        }
    }

    void dump(IndentingPrintWriter fout, DumpArgs dumpArgs) {
        synchronized (mMetadataLock) {
            fout.println("blobHandle:");
            fout.increaseIndent();
            mBlobHandle.dump(fout, dumpArgs.shouldDumpFull());
            fout.decreaseIndent();
            fout.println("size: " + formatFileSize(mContext, getSize(), FLAG_IEC_UNITS));

            fout.println("Committers:");
            fout.increaseIndent();
            if (mCommitters.isEmpty()) {
                fout.println("<empty>");
            } else {
                for (int i = 0, count = mCommitters.size(); i < count; ++i) {
                    final Committer committer = mCommitters.valueAt(i);
                    fout.println("committer " + committer.toString());
                    fout.increaseIndent();
                    committer.dump(fout);
                    fout.decreaseIndent();
                }
            }
            fout.decreaseIndent();

            fout.println("Leasees:");
            fout.increaseIndent();
            if (mLeasees.isEmpty()) {
                fout.println("<empty>");
            } else {
                for (int i = 0, count = mLeasees.size(); i < count; ++i) {
                    final Leasee leasee = mLeasees.valueAt(i);
                    fout.println("leasee " + leasee.toString());
                    fout.increaseIndent();
                    leasee.dump(mContext, fout);
                    fout.decreaseIndent();
                }
            }
            fout.decreaseIndent();

            fout.println("Open fds:");
            fout.increaseIndent();
            if (mRevocableFds.isEmpty()) {
                fout.println("<empty>");
            } else {
                for (int i = 0, count = mRevocableFds.size(); i < count; ++i) {
                    final Accessor accessor = mRevocableFds.keyAt(i);
                    final ArraySet<RevocableFileDescriptor> rFds =
                            mRevocableFds.valueAt(i);
                    fout.println(accessor + ": #" + rFds.size());
                }
            }
            fout.decreaseIndent();
        }
    }

    void writeToXml(XmlSerializer out) throws IOException {
        synchronized (mMetadataLock) {
            XmlUtils.writeLongAttribute(out, ATTR_ID, mBlobId);

            out.startTag(null, TAG_BLOB_HANDLE);
            mBlobHandle.writeToXml(out);
            out.endTag(null, TAG_BLOB_HANDLE);

            for (int i = 0, count = mCommitters.size(); i < count; ++i) {
                out.startTag(null, TAG_COMMITTER);
                mCommitters.valueAt(i).writeToXml(out);
                out.endTag(null, TAG_COMMITTER);
            }

            for (int i = 0, count = mLeasees.size(); i < count; ++i) {
                out.startTag(null, TAG_LEASEE);
                mLeasees.valueAt(i).writeToXml(out);
                out.endTag(null, TAG_LEASEE);
            }
        }
    }

    @Nullable
    static BlobMetadata createFromXml(XmlPullParser in, int version, Context context)
            throws XmlPullParserException, IOException {
        final long blobId = XmlUtils.readLongAttribute(in, ATTR_ID);
        if (version < XML_VERSION_ALLOW_ACCESS_ACROSS_USERS) {
            XmlUtils.readIntAttribute(in, ATTR_USER_ID);
        }

        BlobHandle blobHandle = null;
        final ArraySet<Committer> committers = new ArraySet<>();
        final ArraySet<Leasee> leasees = new ArraySet<>();
        final int depth = in.getDepth();
        while (XmlUtils.nextElementWithin(in, depth)) {
            if (TAG_BLOB_HANDLE.equals(in.getName())) {
                blobHandle = BlobHandle.createFromXml(in);
            } else if (TAG_COMMITTER.equals(in.getName())) {
                final Committer committer = Committer.createFromXml(in, version);
                if (committer != null) {
                    committers.add(committer);
                }
            } else if (TAG_LEASEE.equals(in.getName())) {
                leasees.add(Leasee.createFromXml(in, version));
            }
        }

        if (blobHandle == null) {
            Slog.wtf(TAG, "blobHandle should be available");
            return null;
        }

        final BlobMetadata blobMetadata = new BlobMetadata(context, blobId, blobHandle);
        blobMetadata.setCommitters(committers);
        blobMetadata.setLeasees(leasees);
        return blobMetadata;
    }

    static final class Committer extends Accessor {
        public final BlobAccessMode blobAccessMode;
        public final long commitTimeMs;

        Committer(String packageName, int uid, BlobAccessMode blobAccessMode, long commitTimeMs) {
            super(packageName, uid);
            this.blobAccessMode = blobAccessMode;
            this.commitTimeMs = commitTimeMs;
        }

        long getCommitTimeMs() {
            return commitTimeMs;
        }

        void dump(IndentingPrintWriter fout) {
            fout.println("commit time: "
                    + (commitTimeMs == 0 ? "<null>" : BlobStoreUtils.formatTime(commitTimeMs)));
            fout.println("accessMode:");
            fout.increaseIndent();
            blobAccessMode.dump(fout);
            fout.decreaseIndent();
        }

        void writeToXml(@NonNull XmlSerializer out) throws IOException {
            XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, packageName);
            XmlUtils.writeIntAttribute(out, ATTR_UID, uid);
            XmlUtils.writeLongAttribute(out, ATTR_COMMIT_TIME_MS, commitTimeMs);

            out.startTag(null, TAG_ACCESS_MODE);
            blobAccessMode.writeToXml(out);
            out.endTag(null, TAG_ACCESS_MODE);
        }

        @Nullable
        static Committer createFromXml(@NonNull XmlPullParser in, int version)
                throws XmlPullParserException, IOException {
            final String packageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE);
            final int uid = XmlUtils.readIntAttribute(in, ATTR_UID);
            final long commitTimeMs = version >= XML_VERSION_ADD_COMMIT_TIME
                    ? XmlUtils.readLongAttribute(in, ATTR_COMMIT_TIME_MS)
                    : 0;

            final int depth = in.getDepth();
            BlobAccessMode blobAccessMode = null;
            while (XmlUtils.nextElementWithin(in, depth)) {
                if (TAG_ACCESS_MODE.equals(in.getName())) {
                    blobAccessMode = BlobAccessMode.createFromXml(in);
                }
            }
            if (blobAccessMode == null) {
                Slog.wtf(TAG, "blobAccessMode should be available");
                return null;
            }
            return new Committer(packageName, uid, blobAccessMode, commitTimeMs);
        }
    }

    static final class Leasee extends Accessor {
        public final String descriptionResEntryName;
        public final CharSequence description;
        public final long expiryTimeMillis;

        Leasee(@NonNull Context context, @NonNull String packageName,
                int uid, int descriptionResId,
                @Nullable CharSequence description, long expiryTimeMillis) {
            super(packageName, uid);
            final Resources packageResources = getPackageResources(context, packageName,
                    UserHandle.getUserId(uid));
            this.descriptionResEntryName = getResourceEntryName(packageResources, descriptionResId);
            this.expiryTimeMillis = expiryTimeMillis;
            this.description = description == null
                    ? getDescription(packageResources, descriptionResId)
                    : description;
        }

        Leasee(String packageName, int uid, @Nullable String descriptionResEntryName,
                @Nullable CharSequence description, long expiryTimeMillis) {
            super(packageName, uid);
            this.descriptionResEntryName = descriptionResEntryName;
            this.expiryTimeMillis = expiryTimeMillis;
            this.description = description;
        }

        @Nullable
        private static String getResourceEntryName(@Nullable Resources packageResources,
                int resId) {
            if (!ResourceId.isValid(resId) || packageResources == null) {
                return null;
            }
            return packageResources.getResourceEntryName(resId);
        }

        @Nullable
        private static String getDescription(@NonNull Context context,
                @NonNull String descriptionResEntryName, @NonNull String packageName, int userId) {
            if (descriptionResEntryName == null || descriptionResEntryName.isEmpty()) {
                return null;
            }
            final Resources resources = getPackageResources(context, packageName, userId);
            if (resources == null) {
                return null;
            }
            final int resId = getDescriptionResourceId(resources, descriptionResEntryName,
                    packageName);
            return resId == Resources.ID_NULL ? null : resources.getString(resId);
        }

        @Nullable
        private static String getDescription(@Nullable Resources packageResources,
                int descriptionResId) {
            if (!ResourceId.isValid(descriptionResId) || packageResources == null) {
                return null;
            }
            return packageResources.getString(descriptionResId);
        }

        boolean isStillValid() {
            return expiryTimeMillis == 0 || expiryTimeMillis >= System.currentTimeMillis();
        }

        void dump(@NonNull Context context, @NonNull IndentingPrintWriter fout) {
            fout.println("desc: " + getDescriptionToDump(context));
            fout.println("expiryMs: " + expiryTimeMillis);
        }

        @NonNull
        private String getDescriptionToDump(@NonNull Context context) {
            String desc = getDescription(context, descriptionResEntryName, packageName,
                    UserHandle.getUserId(uid));
            if (desc == null) {
                desc = description.toString();
            }
            return desc == null ? "<none>" : desc;
        }

        void writeToXml(@NonNull XmlSerializer out) throws IOException {
            XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, packageName);
            XmlUtils.writeIntAttribute(out, ATTR_UID, uid);
            XmlUtils.writeStringAttribute(out, ATTR_DESCRIPTION_RES_NAME, descriptionResEntryName);
            XmlUtils.writeLongAttribute(out, ATTR_EXPIRY_TIME, expiryTimeMillis);
            XmlUtils.writeStringAttribute(out, ATTR_DESCRIPTION, description);
        }

        @NonNull
        static Leasee createFromXml(@NonNull XmlPullParser in, int version)
                throws IOException {
            final String packageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE);
            final int uid = XmlUtils.readIntAttribute(in, ATTR_UID);
            final String descriptionResEntryName;
            if (version >= XML_VERSION_ADD_DESC_RES_NAME) {
                descriptionResEntryName = XmlUtils.readStringAttribute(
                        in, ATTR_DESCRIPTION_RES_NAME);
            } else {
                descriptionResEntryName = null;
            }
            final long expiryTimeMillis = XmlUtils.readLongAttribute(in, ATTR_EXPIRY_TIME);
            final CharSequence description;
            if (version >= XML_VERSION_ADD_STRING_DESC) {
                description = XmlUtils.readStringAttribute(in, ATTR_DESCRIPTION);
            } else {
                description = null;
            }

            return new Leasee(packageName, uid, descriptionResEntryName,
                    description, expiryTimeMillis);
        }
    }

    static class Accessor {
        public final String packageName;
        public final int uid;

        Accessor(String packageName, int uid) {
            this.packageName = packageName;
            this.uid = uid;
        }

        public boolean equals(String packageName, int uid) {
            return this.uid == uid && this.packageName.equals(packageName);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null || !(obj instanceof Accessor)) {
                return false;
            }
            final Accessor other = (Accessor) obj;
            return this.uid == other.uid && this.packageName.equals(other.packageName);
        }

        @Override
        public int hashCode() {
            return Objects.hash(packageName, uid);
        }

        @Override
        public String toString() {
            return "[" + packageName + ", " + uid + "]";
        }
    }
}
