Merge "Introduce an IconsContentProvider." into rvc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
33e891d1a2
@@ -5457,6 +5457,13 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="com.android.server.textclassifier.IconsContentProvider"
|
||||
android:authorities="com.android.textclassifier.icons"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.textclassifier;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.server.textclassifier.IconsUriHelper.ResourceInfo;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* A content provider that is used to access icons returned from the TextClassifier service.
|
||||
*
|
||||
* <p>Use {@link IconsUriHelper#getContentUri(String, int)} to access a uri for a specific resource.
|
||||
* The uri may be passed to other processes to access the specified resource.
|
||||
*
|
||||
* <p>NOTE: Care must be taken to avoid leaking resources to non-permitted apps via this provider.
|
||||
*/
|
||||
public final class IconsContentProvider extends ContentProvider {
|
||||
|
||||
private static final String TAG = "IconsContentProvider";
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode) {
|
||||
try {
|
||||
final ResourceInfo res = IconsUriHelper.getInstance().getResourceInfo(uri);
|
||||
final Drawable drawable = Icon.createWithResource(res.packageName, res.id)
|
||||
.loadDrawable(getContext());
|
||||
final byte[] data = getBitmapData(drawable);
|
||||
final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
|
||||
final ParcelFileDescriptor readSide = pipe[0];
|
||||
final ParcelFileDescriptor writeSide = pipe[1];
|
||||
try (OutputStream out = new AutoCloseOutputStream(writeSide)) {
|
||||
out.write(data);
|
||||
return readSide;
|
||||
}
|
||||
} catch (IOException | RuntimeException e) {
|
||||
Log.e(TAG, "Error retrieving icon for uri: " + uri, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bitmap data for the specified drawable.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static byte[] getBitmapData(Drawable drawable) {
|
||||
if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
|
||||
throw new IllegalStateException("The icon is zero-sized");
|
||||
}
|
||||
|
||||
final Bitmap bitmap = Bitmap.createBitmap(
|
||||
drawable.getIntrinsicWidth(),
|
||||
drawable.getIntrinsicHeight(),
|
||||
Bitmap.Config.ARGB_8888);
|
||||
|
||||
final Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
|
||||
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
final byte[] byteArray = stream.toByteArray();
|
||||
bitmap.recycle();
|
||||
return byteArray;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return "image/png";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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.textclassifier;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.net.Uri;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.annotations.VisibleForTesting.Visibility;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A helper for mapping an icon resource to a content uri.
|
||||
*
|
||||
* <p>NOTE: Care must be taken to avoid passing resource uris to non-permitted apps via this helper.
|
||||
*/
|
||||
@VisibleForTesting(visibility = Visibility.PACKAGE)
|
||||
public final class IconsUriHelper {
|
||||
|
||||
public static final String AUTHORITY = "com.android.textclassifier.icons";
|
||||
|
||||
private static final String TAG = "IconsUriHelper";
|
||||
private static final Supplier<String> DEFAULT_ID_SUPPLIER = () -> UUID.randomUUID().toString();
|
||||
|
||||
// TODO: Consider using an LRU cache to limit resource usage.
|
||||
// This may depend on the expected number of packages that a device typically has.
|
||||
@GuardedBy("mPackageIds")
|
||||
private final Map<String, String> mPackageIds = new ArrayMap<>();
|
||||
|
||||
private final Supplier<String> mIdSupplier;
|
||||
|
||||
private static final IconsUriHelper sSingleton = new IconsUriHelper(null);
|
||||
|
||||
private IconsUriHelper(@Nullable Supplier<String> idSupplier) {
|
||||
mIdSupplier = (idSupplier != null) ? idSupplier : DEFAULT_ID_SUPPLIER;
|
||||
|
||||
// Useful for testing:
|
||||
// Magic id for the android package so it is the same across classloaders.
|
||||
// This is okay as this package does not have access restrictions, and
|
||||
// the TextClassifierService hardly returns icons from this package.
|
||||
mPackageIds.put("android", "android");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of this object for testing purposes.
|
||||
*/
|
||||
public static IconsUriHelper newInstanceForTesting(@Nullable Supplier<String> idSupplier) {
|
||||
return new IconsUriHelper(idSupplier);
|
||||
}
|
||||
|
||||
static IconsUriHelper getInstance() {
|
||||
return sSingleton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Uri for the specified icon resource.
|
||||
*
|
||||
* @param packageName the resource's package name
|
||||
* @param resId the resource id
|
||||
* @see #getResourceInfo(Uri)
|
||||
*/
|
||||
public Uri getContentUri(String packageName, int resId) {
|
||||
Objects.requireNonNull(packageName);
|
||||
synchronized (mPackageIds) {
|
||||
if (!mPackageIds.containsKey(packageName)) {
|
||||
// TODO: Ignore packages that don't actually exist on the device.
|
||||
mPackageIds.put(packageName, mIdSupplier.get());
|
||||
}
|
||||
return new Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(AUTHORITY)
|
||||
.path(mPackageIds.get(packageName))
|
||||
.appendPath(Integer.toString(resId))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid {@link ResourceInfo} for the specified uri. Returns {@code null} if a valid
|
||||
* {@link ResourceInfo} cannot be returned for the specified uri.
|
||||
*
|
||||
* @see #getContentUri(String, int);
|
||||
*/
|
||||
@Nullable
|
||||
public ResourceInfo getResourceInfo(Uri uri) {
|
||||
if (!"content".equals(uri.getScheme())) {
|
||||
return null;
|
||||
}
|
||||
if (!AUTHORITY.equals(uri.getAuthority())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final List<String> pathItems = uri.getPathSegments();
|
||||
try {
|
||||
synchronized (mPackageIds) {
|
||||
final String packageId = pathItems.get(0);
|
||||
final int resId = Integer.parseInt(pathItems.get(1));
|
||||
for (String packageName : mPackageIds.keySet()) {
|
||||
if (packageId.equals(mPackageIds.get(packageName))) {
|
||||
return new ResourceInfo(packageName, resId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.v(TAG, "Could not get resource info. Reason: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A holder for a resource's package name and id.
|
||||
*/
|
||||
public static final class ResourceInfo {
|
||||
|
||||
public final String packageName;
|
||||
public final int id;
|
||||
|
||||
private ResourceInfo(String packageName, int id) {
|
||||
this.packageName = Objects.requireNonNull(packageName);
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.textclassifier;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Sanity test for {@link IconsContentProvider}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class IconsContentProviderTest {
|
||||
|
||||
@Test
|
||||
public void testLoadResource() {
|
||||
final Context context = ApplicationProvider.getApplicationContext();
|
||||
// Testing with the android package name because this is the only package name
|
||||
// that returns the same uri across multiple classloaders.
|
||||
final String packageName = "android";
|
||||
final int resId = android.R.drawable.btn_star;
|
||||
final Uri uri = IconsUriHelper.getInstance().getContentUri(packageName, resId);
|
||||
|
||||
final Drawable expected = Icon.createWithResource(packageName, resId).loadDrawable(context);
|
||||
// Ensure we are testing with a non-empty image.
|
||||
assertThat(expected.getIntrinsicWidth()).isGreaterThan(0);
|
||||
assertThat(expected.getIntrinsicHeight()).isGreaterThan(0);
|
||||
|
||||
final Drawable actual = Icon.createWithContentUri(uri).loadDrawable(context);
|
||||
assertThat(actual).isNotNull();
|
||||
assertThat(IconsContentProvider.getBitmapData(actual))
|
||||
.isEqualTo(IconsContentProvider.getBitmapData(expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadResource_badUri() {
|
||||
final Uri badUri = new Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(IconsUriHelper.AUTHORITY)
|
||||
.path("badPackageId")
|
||||
.appendPath("1234")
|
||||
.build();
|
||||
|
||||
final Context context = ApplicationProvider.getApplicationContext();
|
||||
assertThat(Icon.createWithContentUri(badUri).loadDrawable(context)).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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.textclassifier;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.server.textclassifier.IconsUriHelper.ResourceInfo;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Tests for {@link IconsUriHelper}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class IconsUriHelperTest {
|
||||
|
||||
private IconsUriHelper mIconsUriHelper;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mIconsUriHelper = IconsUriHelper.newInstanceForTesting(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetContentUri() {
|
||||
final IconsUriHelper iconsUriHelper = IconsUriHelper.newInstanceForTesting(() -> "pkgId");
|
||||
final Uri expected = new Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(IconsUriHelper.AUTHORITY)
|
||||
.path("pkgId")
|
||||
.appendPath("1234")
|
||||
.build();
|
||||
|
||||
final Uri actual = iconsUriHelper.getContentUri("com.package.name", 1234);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetContentUri_multiplePackages() {
|
||||
final Uri uri1 = mIconsUriHelper.getContentUri("com.package.name1", 1234);
|
||||
final Uri uri2 = mIconsUriHelper.getContentUri("com.package.name2", 5678);
|
||||
|
||||
assertThat(uri1.getScheme()).isEqualTo("content");
|
||||
assertThat(uri2.getScheme()).isEqualTo("content");
|
||||
|
||||
assertThat(uri1.getAuthority()).isEqualTo(IconsUriHelper.AUTHORITY);
|
||||
assertThat(uri2.getAuthority()).isEqualTo(IconsUriHelper.AUTHORITY);
|
||||
|
||||
assertThat(uri1.getPathSegments().get(1)).isEqualTo("1234");
|
||||
assertThat(uri2.getPathSegments().get(1)).isEqualTo("5678");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetContentUri_samePackageIdForSamePackageName() {
|
||||
final String packageName = "com.package.name";
|
||||
final Uri uri1 = mIconsUriHelper.getContentUri(packageName, 1234);
|
||||
final Uri uri2 = mIconsUriHelper.getContentUri(packageName, 5678);
|
||||
|
||||
final String id1 = uri1.getPathSegments().get(0);
|
||||
final String id2 = uri2.getPathSegments().get(0);
|
||||
|
||||
assertThat(id1).isEqualTo(id2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetResourceInfo() {
|
||||
mIconsUriHelper.getContentUri("com.package.name1", 123);
|
||||
final Uri uri = mIconsUriHelper.getContentUri("com.package.name2", 456);
|
||||
mIconsUriHelper.getContentUri("com.package.name3", 789);
|
||||
|
||||
final ResourceInfo res = mIconsUriHelper.getResourceInfo(uri);
|
||||
assertThat(res.packageName).isEqualTo("com.package.name2");
|
||||
assertThat(res.id).isEqualTo(456);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetResourceInfo_unrecognizedUri() {
|
||||
final Uri uri = new Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(IconsUriHelper.AUTHORITY)
|
||||
.path("unrecognized")
|
||||
.appendPath("1234")
|
||||
.build();
|
||||
assertThat(mIconsUriHelper.getResourceInfo(uri)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetResourceInfo_invalidScheme() {
|
||||
final IconsUriHelper iconsUriHelper = IconsUriHelper.newInstanceForTesting(() -> "pkgId");
|
||||
iconsUriHelper.getContentUri("com.package.name", 1234);
|
||||
|
||||
final Uri uri = new Uri.Builder()
|
||||
.scheme("file")
|
||||
.authority(IconsUriHelper.AUTHORITY)
|
||||
.path("pkgId")
|
||||
.appendPath("1234")
|
||||
.build();
|
||||
assertThat(iconsUriHelper.getResourceInfo(uri)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetResourceInfo_invalidAuthority() {
|
||||
final IconsUriHelper iconsUriHelper = IconsUriHelper.newInstanceForTesting(() -> "pkgId");
|
||||
iconsUriHelper.getContentUri("com.package.name", 1234);
|
||||
|
||||
final Uri uri = new Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority("invalid.authority")
|
||||
.path("pkgId")
|
||||
.appendPath("1234")
|
||||
.build();
|
||||
assertThat(iconsUriHelper.getResourceInfo(uri)).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user