blob: 0b760a621d22808e15cb3a21d4e697cfb3419321 [file] [log] [blame]
/*
* 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.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.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.res.ResourceId;
import android.content.res.Resources;
import android.os.ParcelFileDescriptor;
import android.os.RevocableFileDescriptor;
import android.os.UserHandle;
import android.system.ErrnoException;
import android.system.Os;
import android.util.ArrayMap;
import android.util.ArraySet;
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.IndentingPrintWriter;
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;
private final int mUserId;
@GuardedBy("mMetadataLock")
private final ArraySet<Committer> mCommitters = new ArraySet<>();
@GuardedBy("mMetadataLock")
private final ArraySet<Leasee> mLeasees = new ArraySet<>();
/**
* Contains packageName -> {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<String, ArraySet<RevocableFileDescriptor>> mRevocableFds =
new ArrayMap<>();
// Do not access this directly, instead use #getBlobFile().
private File mBlobFile;
BlobMetadata(Context context, long blobId, BlobHandle blobHandle, int userId) {
mContext = context;
this.mBlobId = blobId;
this.mBlobHandle = blobHandle;
this.mUserId = userId;
}
long getBlobId() {
return mBlobId;
}
BlobHandle getBlobHandle() {
return mBlobHandle;
}
int getUserId() {
return mUserId;
}
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<String> knownPackages) {
synchronized (mMetadataLock) {
mCommitters.removeIf(committer ->
!committer.packageName.equals(knownPackages.get(committer.uid)));
}
}
@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<String> knownPackages) {
synchronized (mMetadataLock) {
mLeasees.removeIf(leasee ->
!leasee.packageName.equals(knownPackages.get(leasee.uid)));
}
}
void removeExpiredLeases() {
synchronized (mMetadataLock) {
mLeasees.removeIf(leasee -> !leasee.isStillValid());
}
}
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;
}
}
for (int i = 0, size = mCommitters.size(); i < size; ++i) {
final Committer committer = mCommitters.valueAt(i);
// 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;
}
}
}
return false;
}
boolean isACommitter(@NonNull String packageName, int uid) {
synchronized (mMetadataLock) {
return isAnAccessor(mCommitters, packageName, uid);
}
}
boolean isALeasee(@Nullable String packageName, int uid) {
synchronized (mMetadataLock) {
final Leasee leasee = getAccessor(mLeasees, packageName, uid);
return leasee != null && leasee.isStillValid();
}
}
private static <T extends Accessor> boolean isAnAccessor(@NonNull ArraySet<T> accessors,
@Nullable String packageName, int uid) {
// Check if the package is an accessor of the data blob.
return getAccessor(accessors, packageName, uid) != null;
}
private static <T extends Accessor> T getAccessor(@NonNull ArraySet<T> accessors,
@Nullable String packageName, int uid) {
// 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)) {
return (T) accessor;
} else if (uid != INVALID_UID && accessor.uid == uid) {
return (T) accessor;
}
}
return null;
}
boolean isALeasee(@NonNull String packageName) {
return isALeasee(packageName, INVALID_UID);
}
boolean isALeasee(int uid) {
return isALeasee(null, uid);
}
boolean hasOtherLeasees(@NonNull String packageName) {
return hasOtherLeasees(packageName, INVALID_UID);
}
boolean hasOtherLeasees(int uid) {
return hasOtherLeasees(null, uid);
}
private boolean hasOtherLeasees(@Nullable 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;
}
// 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)) {
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) 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);
} else {
return new ParcelFileDescriptor(fd);
}
} catch (IOException e) {
IoUtils.closeQuietly(fd);
throw e;
}
}
@NonNull
private ParcelFileDescriptor createRevocableFd(FileDescriptor fd,
String callingPackage) throws IOException {
final RevocableFileDescriptor revocableFd =
new RevocableFileDescriptor(mContext, fd);
synchronized (mRevocableFds) {
ArraySet<RevocableFileDescriptor> revocableFdsForPkg =
mRevocableFds.get(callingPackage);
if (revocableFdsForPkg == null) {
revocableFdsForPkg = new ArraySet<>();
mRevocableFds.put(callingPackage, revocableFdsForPkg);
}
revocableFdsForPkg.add(revocableFd);
}
revocableFd.addOnCloseListener((e) -> {
synchronized (mRevocableFds) {
final ArraySet<RevocableFileDescriptor> revocableFdsForPkg =
mRevocableFds.get(callingPackage);
if (revocableFdsForPkg != null) {
revocableFdsForPkg.remove(revocableFd);
if (revocableFdsForPkg.isEmpty()) {
mRevocableFds.remove(callingPackage);
}
}
}
});
return revocableFd.getRevocableFileDescriptor();
}
void destroy() {
revokeAllFds();
getBlobFile().delete();
}
private void revokeAllFds() {
synchronized (mRevocableFds) {
for (int i = 0, pkgCount = mRevocableFds.size(); i < pkgCount; ++i) {
final ArraySet<RevocableFileDescriptor> packageFds =
mRevocableFds.valueAt(i);
if (packageFds == null) {
continue;
}
for (int j = 0, fdCount = packageFds.size(); j < fdCount; ++j) {
packageFds.valueAt(j).revoke();
}
}
}
}
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.getNumWhitelistedPackages());
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 StatsEvent.newBuilder()
.setAtomId(atomTag)
.writeLong(mBlobId)
.writeLong(getSize())
.writeLong(mBlobHandle.getExpiryTimeMillis())
.writeByteArray(committersBytes)
.writeByteArray(leaseesBytes)
.build();
}
}
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 String packageName = mRevocableFds.keyAt(i);
final ArraySet<RevocableFileDescriptor> packageFds =
mRevocableFds.valueAt(i);
fout.println(packageName + "#" + packageFds.size());
}
}
fout.decreaseIndent();
}
}
void writeToXml(XmlSerializer out) throws IOException {
synchronized (mMetadataLock) {
XmlUtils.writeLongAttribute(out, ATTR_ID, mBlobId);
XmlUtils.writeIntAttribute(out, ATTR_USER_ID, mUserId);
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);
final int userId = 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, userId);
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 + "]";
}
}
}