Merge "Add hidden APIs to query and delete shared blobs." into rvc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
7106cb4737
119
apex/blobstore/framework/java/android/app/blob/AccessorInfo.java
Normal file
119
apex/blobstore/framework/java/android/app/blob/AccessorInfo.java
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (C) 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 android.app.blob;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class to provide information about an accessor of a shared blob.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public final class AccessorInfo implements Parcelable {
|
||||
private final String mPackageName;
|
||||
private final long mExpiryTimeMs;
|
||||
private final int mDescriptionResId;
|
||||
private final CharSequence mDescription;
|
||||
|
||||
public AccessorInfo(String packageName, long expiryTimeMs,
|
||||
int descriptionResId, CharSequence description) {
|
||||
mPackageName = packageName;
|
||||
mExpiryTimeMs = expiryTimeMs;
|
||||
mDescriptionResId = descriptionResId;
|
||||
mDescription = description;
|
||||
}
|
||||
|
||||
private AccessorInfo(Parcel in) {
|
||||
mPackageName = in.readString();
|
||||
mExpiryTimeMs = in.readLong();
|
||||
mDescriptionResId = in.readInt();
|
||||
mDescription = in.readCharSequence();
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return mPackageName;
|
||||
}
|
||||
|
||||
public long getExpiryTimeMs() {
|
||||
return mExpiryTimeMs;
|
||||
}
|
||||
|
||||
public int getDescriptionResId() {
|
||||
return mDescriptionResId;
|
||||
}
|
||||
|
||||
public CharSequence getDescription() {
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
dest.writeString(mPackageName);
|
||||
dest.writeLong(mExpiryTimeMs);
|
||||
dest.writeInt(mDescriptionResId);
|
||||
dest.writeCharSequence(mDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AccessorInfo {"
|
||||
+ "package: " + mPackageName + ","
|
||||
+ "expiryMs: " + mExpiryTimeMs + ","
|
||||
+ "descriptionResId: " + mDescriptionResId + ","
|
||||
+ "description: " + mDescription + ","
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String toShortString() {
|
||||
return mPackageName;
|
||||
}
|
||||
|
||||
public static String toShortString(List<AccessorInfo> accessors) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("[");
|
||||
for (int i = 0, size = accessors.size(); i < size; ++i) {
|
||||
sb.append(accessors.get(i).toShortString());
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static final Creator<AccessorInfo> CREATOR = new Creator<AccessorInfo>() {
|
||||
@Override
|
||||
@NonNull
|
||||
public AccessorInfo createFromParcel(Parcel source) {
|
||||
return new AccessorInfo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public AccessorInfo[] newArray(int size) {
|
||||
return new AccessorInfo[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
19
apex/blobstore/framework/java/android/app/blob/BlobInfo.aidl
Normal file
19
apex/blobstore/framework/java/android/app/blob/BlobInfo.aidl
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 android.app.blob;
|
||||
|
||||
/** {@hide} */
|
||||
parcelable BlobInfo;
|
||||
109
apex/blobstore/framework/java/android/app/blob/BlobInfo.java
Normal file
109
apex/blobstore/framework/java/android/app/blob/BlobInfo.java
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 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 android.app.blob;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class to provide information about a shared blob.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public final class BlobInfo implements Parcelable {
|
||||
private final long mId;
|
||||
private final long mExpiryTimeMs;
|
||||
private final CharSequence mLabel;
|
||||
private final List<AccessorInfo> mAccessors;
|
||||
|
||||
public BlobInfo(long id, long expiryTimeMs, CharSequence label,
|
||||
List<AccessorInfo> accessors) {
|
||||
mId = id;
|
||||
mExpiryTimeMs = expiryTimeMs;
|
||||
mLabel = label;
|
||||
mAccessors = accessors;
|
||||
}
|
||||
|
||||
private BlobInfo(Parcel in) {
|
||||
mId = in.readLong();
|
||||
mExpiryTimeMs = in.readLong();
|
||||
mLabel = in.readCharSequence();
|
||||
mAccessors = in.readArrayList(null /* classloader */);
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
public long getExpiryTimeMs() {
|
||||
return mExpiryTimeMs;
|
||||
}
|
||||
|
||||
public CharSequence getLabel() {
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
public List<AccessorInfo> getAccessors() {
|
||||
return Collections.unmodifiableList(mAccessors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||
dest.writeLong(mId);
|
||||
dest.writeLong(mExpiryTimeMs);
|
||||
dest.writeCharSequence(mLabel);
|
||||
dest.writeList(mAccessors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toShortString();
|
||||
}
|
||||
|
||||
private String toShortString() {
|
||||
return "BlobInfo {"
|
||||
+ "id: " + mId + ","
|
||||
+ "expiryMs: " + mExpiryTimeMs + ","
|
||||
+ "label: " + mLabel + ","
|
||||
+ "accessors: " + AccessorInfo.toShortString(mAccessors) + ","
|
||||
+ "}";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static final Creator<BlobInfo> CREATOR = new Creator<BlobInfo>() {
|
||||
@Override
|
||||
@NonNull
|
||||
public BlobInfo createFromParcel(Parcel source) {
|
||||
return new BlobInfo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public BlobInfo[] newArray(int size) {
|
||||
return new BlobInfo[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -27,11 +27,13 @@ import android.os.ParcelFileDescriptor;
|
||||
import android.os.ParcelableException;
|
||||
import android.os.RemoteCallback;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
|
||||
import com.android.internal.util.function.pooled.PooledLambda;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -145,9 +147,6 @@ public class BlobStoreManager {
|
||||
/** @hide */
|
||||
public static final int INVALID_RES_ID = -1;
|
||||
|
||||
/** @hide */
|
||||
public static final String DESC_RES_TYPE_STRING = "string";
|
||||
|
||||
private final Context mContext;
|
||||
private final IBlobStoreManager mService;
|
||||
|
||||
@@ -495,6 +494,31 @@ public class BlobStoreManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@NonNull
|
||||
public List<BlobInfo> queryBlobsForUser(@NonNull UserHandle user) throws IOException {
|
||||
try {
|
||||
return mService.queryBlobsForUser(user.getIdentifier());
|
||||
} catch (ParcelableException e) {
|
||||
e.maybeRethrow(IOException.class);
|
||||
throw new RuntimeException(e);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public void deleteBlob(@NonNull BlobInfo blobInfo) throws IOException {
|
||||
try {
|
||||
mService.deleteBlob(blobInfo.getId());
|
||||
} catch (ParcelableException e) {
|
||||
e.maybeRethrow(IOException.class);
|
||||
throw new RuntimeException(e);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an ongoing session of a blob's contribution to the blob store managed by the
|
||||
* system.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package android.app.blob;
|
||||
|
||||
import android.app.blob.BlobHandle;
|
||||
import android.app.blob.BlobInfo;
|
||||
import android.app.blob.IBlobStoreSession;
|
||||
import android.os.RemoteCallback;
|
||||
|
||||
@@ -31,4 +32,7 @@ interface IBlobStoreManager {
|
||||
void releaseLease(in BlobHandle handle, in String packageName);
|
||||
|
||||
void waitForIdle(in RemoteCallback callback);
|
||||
|
||||
List<BlobInfo> queryBlobsForUser(int userId);
|
||||
void deleteBlob(long blobId);
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.android.server.blob;
|
||||
|
||||
import static android.app.blob.BlobStoreManager.DESC_RES_TYPE_STRING;
|
||||
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;
|
||||
@@ -30,16 +29,16 @@ import static android.app.blob.XmlTags.TAG_LEASEE;
|
||||
import static android.os.Process.INVALID_UID;
|
||||
import static android.system.OsConstants.O_RDONLY;
|
||||
|
||||
import static com.android.server.blob.BlobStoreConfig.LOGV;
|
||||
import static com.android.server.blob.BlobStoreConfig.TAG;
|
||||
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.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.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.ResourceId;
|
||||
import android.content.res.Resources;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
@@ -65,6 +64,7 @@ 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();
|
||||
@@ -281,6 +281,10 @@ class BlobMetadata {
|
||||
return false;
|
||||
}
|
||||
|
||||
void forEachLeasee(Consumer<Leasee> consumer) {
|
||||
mLeasees.forEach(consumer);
|
||||
}
|
||||
|
||||
File getBlobFile() {
|
||||
if (mBlobFile == null) {
|
||||
mBlobFile = BlobStoreConfig.getBlobFile(mBlobId);
|
||||
@@ -506,16 +510,9 @@ class BlobMetadata {
|
||||
if (resources == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final int resId = resources.getIdentifier(descriptionResEntryName,
|
||||
DESC_RES_TYPE_STRING, packageName);
|
||||
return resId <= 0 ? null : resources.getString(resId);
|
||||
} catch (Resources.NotFoundException e) {
|
||||
if (LOGV) {
|
||||
Slog.w(TAG, "Description resource not found", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
final int resId = getDescriptionResourceId(resources, descriptionResEntryName,
|
||||
packageName);
|
||||
return resId == Resources.ID_NULL ? null : resources.getString(resId);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -546,19 +543,6 @@ class BlobMetadata {
|
||||
return desc == null ? "<none>" : desc;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Resources getPackageResources(@NonNull Context context,
|
||||
@NonNull String packageName, int userId) {
|
||||
try {
|
||||
return context.getPackageManager()
|
||||
.getResourcesForApplicationAsUser(packageName, userId);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Slog.d(TAG, "Unknown package in user " + userId + ": "
|
||||
+ packageName, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void writeToXml(@NonNull XmlSerializer out) throws IOException {
|
||||
XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, packageName);
|
||||
XmlUtils.writeIntAttribute(out, ATTR_UID, uid);
|
||||
|
||||
@@ -36,6 +36,8 @@ import static com.android.server.blob.BlobStoreSession.STATE_COMMITTED;
|
||||
import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_INVALID;
|
||||
import static com.android.server.blob.BlobStoreSession.STATE_VERIFIED_VALID;
|
||||
import static com.android.server.blob.BlobStoreSession.stateToString;
|
||||
import static com.android.server.blob.BlobStoreUtils.getDescriptionResourceId;
|
||||
import static com.android.server.blob.BlobStoreUtils.getPackageResources;
|
||||
|
||||
import android.annotation.CurrentTimeSecondsLong;
|
||||
import android.annotation.IdRes;
|
||||
@@ -43,7 +45,9 @@ import android.annotation.IntRange;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.UserIdInt;
|
||||
import android.app.blob.AccessorInfo;
|
||||
import android.app.blob.BlobHandle;
|
||||
import android.app.blob.BlobInfo;
|
||||
import android.app.blob.IBlobStoreManager;
|
||||
import android.app.blob.IBlobStoreSession;
|
||||
import android.content.BroadcastReceiver;
|
||||
@@ -100,6 +104,7 @@ import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
@@ -110,6 +115,7 @@ import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Service responsible for maintaining and facilitating access to data blobs published by apps.
|
||||
@@ -434,6 +440,48 @@ public class BlobStoreManagerService extends SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
private List<BlobInfo> queryBlobsForUserInternal(int userId) {
|
||||
final ArrayList<BlobInfo> blobInfos = new ArrayList<>();
|
||||
synchronized (mBlobsLock) {
|
||||
final ArrayMap<String, WeakReference<Resources>> resources = new ArrayMap<>();
|
||||
final Function<String, Resources> resourcesGetter = (packageName) -> {
|
||||
final WeakReference<Resources> resourcesRef = resources.get(packageName);
|
||||
Resources packageResources = resourcesRef == null ? null : resourcesRef.get();
|
||||
if (packageResources == null) {
|
||||
packageResources = getPackageResources(mContext, packageName, userId);
|
||||
resources.put(packageName, new WeakReference<>(packageResources));
|
||||
}
|
||||
return packageResources;
|
||||
};
|
||||
getUserBlobsLocked(userId).forEach((blobHandle, blobMetadata) -> {
|
||||
final ArrayList<AccessorInfo> accessorInfos = new ArrayList<>();
|
||||
blobMetadata.forEachLeasee(leasee -> {
|
||||
final int descriptionResId = leasee.descriptionResEntryName == null
|
||||
? Resources.ID_NULL
|
||||
: getDescriptionResourceId(resourcesGetter.apply(leasee.packageName),
|
||||
leasee.descriptionResEntryName, leasee.packageName);
|
||||
accessorInfos.add(new AccessorInfo(leasee.packageName, leasee.expiryTimeMillis,
|
||||
descriptionResId, leasee.description));
|
||||
});
|
||||
blobInfos.add(new BlobInfo(blobMetadata.getBlobId(),
|
||||
blobHandle.getExpiryTimeMillis(), blobHandle.getLabel(), accessorInfos));
|
||||
});
|
||||
}
|
||||
return blobInfos;
|
||||
}
|
||||
|
||||
private void deleteBlobInternal(long blobId, int callingUid) {
|
||||
synchronized (mBlobsLock) {
|
||||
final ArrayMap<BlobHandle, BlobMetadata> userBlobs = getUserBlobsLocked(
|
||||
UserHandle.getUserId(callingUid));
|
||||
userBlobs.entrySet().removeIf(entry -> {
|
||||
final BlobMetadata blobMetadata = entry.getValue();
|
||||
return blobMetadata.getBlobId() == blobId;
|
||||
});
|
||||
writeBlobsInfoAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyCallingPackage(int callingUid, String callingPackage) {
|
||||
if (mPackageManagerInternal.getPackageUid(
|
||||
callingPackage, 0, UserHandle.getUserId(callingUid)) != callingUid) {
|
||||
@@ -1249,6 +1297,28 @@ public class BlobStoreManagerService extends SystemService {
|
||||
.recycleOnUse());
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<BlobInfo> queryBlobsForUser(@UserIdInt int userId) {
|
||||
if (Binder.getCallingUid() != Process.SYSTEM_UID) {
|
||||
throw new SecurityException("Only system uid is allowed to call "
|
||||
+ "queryBlobsForUser()");
|
||||
}
|
||||
|
||||
return queryBlobsForUserInternal(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBlob(long blobId) {
|
||||
final int callingUid = Binder.getCallingUid();
|
||||
if (callingUid != Process.SYSTEM_UID) {
|
||||
throw new SecurityException("Only system uid is allowed to call "
|
||||
+ "deleteBlob()");
|
||||
}
|
||||
|
||||
deleteBlobInternal(blobId, callingUid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
|
||||
@Nullable String[] args) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 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 com.android.server.blob.BlobStoreConfig.TAG;
|
||||
|
||||
import android.annotation.IdRes;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.util.Slog;
|
||||
|
||||
class BlobStoreUtils {
|
||||
private static final String DESC_RES_TYPE_STRING = "string";
|
||||
|
||||
@Nullable
|
||||
static Resources getPackageResources(@NonNull Context context,
|
||||
@NonNull String packageName, int userId) {
|
||||
try {
|
||||
return context.getPackageManager()
|
||||
.getResourcesForApplicationAsUser(packageName, userId);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Slog.d(TAG, "Unknown package in user " + userId + ": "
|
||||
+ packageName, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@IdRes
|
||||
static int getDescriptionResourceId(@NonNull Resources resources,
|
||||
@NonNull String resourceEntryName, @NonNull String packageName) {
|
||||
return resources.getIdentifier(resourceEntryName, DESC_RES_TYPE_STRING, packageName);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user