Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 22354f6d authored by Abodunrinwa Toki's avatar Abodunrinwa Toki Committed by Automerger Merge Worker
Browse files

Merge "Introduce an IconsContentProvider." into rvc-dev am: 33e891d1 am: 3b73108a

Change-Id: I130b1403b37018d99597c6e1639d359918f71459
parents efe1bbb5 3b73108a
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -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>
+124 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+144 −0
Original line number Diff line number Diff line
/*
 * 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;
        }
    }
}
+70 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+134 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}