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

Commit daa08602 authored by Willie Koomson's avatar Willie Koomson Committed by Android (Google) Code Review
Browse files

Merge "Write Icon to proto" into main

parents 9928e34f e0c3f6e6
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -8360,7 +8360,7 @@ public class RemoteViews implements Parcelable, Filter {
        }
    }

    private interface PendingResources<T> {
    interface PendingResources<T> {
        T create(Context context, Resources appResources, HierarchyRootData rootData, int depth)
                throws Exception;
    }
+177 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.widget;

import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BlendMode;
import android.graphics.drawable.Icon;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import android.util.proto.ProtoUtils;

import androidx.annotation.NonNull;

import java.io.ByteArrayOutputStream;
import java.util.function.Function;

/**
 * This class provides serialization for certain types used within RemoteViews.
 *
 * @hide
 */
public class RemoteViewsSerializers {
    private static final String TAG = "RemoteViews";

    /**
     * Write Icon to proto.
     */
    public static void writeIconToProto(@NonNull ProtoOutputStream out,
            @NonNull Resources appResources, @NonNull Icon icon) {
        if (icon.getTintList() != null) {
            final long token = out.start(RemoteViewsProto.Icon.TINT_LIST);
            icon.getTintList().writeToProto(out);
            out.end(token);
        }
        out.write(RemoteViewsProto.Icon.BLEND_MODE, BlendMode.toValue(icon.getTintBlendMode()));
        switch (icon.getType()) {
            case Icon.TYPE_BITMAP:
                final ByteArrayOutputStream bitmapBytes = new ByteArrayOutputStream();
                icon.getBitmap().compress(Bitmap.CompressFormat.WEBP_LOSSLESS, 100, bitmapBytes);
                out.write(RemoteViewsProto.Icon.BITMAP, bitmapBytes.toByteArray());
                break;
            case Icon.TYPE_ADAPTIVE_BITMAP:
                final ByteArrayOutputStream adaptiveBitmapBytes = new ByteArrayOutputStream();
                icon.getBitmap().compress(Bitmap.CompressFormat.WEBP_LOSSLESS, 100,
                        adaptiveBitmapBytes);
                out.write(RemoteViewsProto.Icon.ADAPTIVE_BITMAP, adaptiveBitmapBytes.toByteArray());
                break;
            case Icon.TYPE_RESOURCE:
                out.write(RemoteViewsProto.Icon.RESOURCE,
                        appResources.getResourceName(icon.getResId()));
                break;
            case Icon.TYPE_DATA:
                out.write(RemoteViewsProto.Icon.DATA, icon.getDataBytes());
                break;
            case Icon.TYPE_URI:
                out.write(RemoteViewsProto.Icon.URI, icon.getUriString());
                break;
            case Icon.TYPE_URI_ADAPTIVE_BITMAP:
                out.write(RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP, icon.getUriString());
                break;
            default:
                Log.e(TAG, "Tried to serialize unknown Icon type " + icon.getType());
        }
    }

    /**
     * Create Icon from proto.
     */
    @NonNull
    public static Function<Resources, Icon> createIconFromProto(@NonNull ProtoInputStream in)
            throws Exception {
        final LongSparseArray<Object> values = new LongSparseArray<>();
        while (in.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
            switch (in.getFieldNumber()) {
                case (int) RemoteViewsProto.Icon.BLEND_MODE:
                    values.put(RemoteViewsProto.Icon.BLEND_MODE,
                            in.readInt(RemoteViewsProto.Icon.BLEND_MODE));
                    break;
                case (int) RemoteViewsProto.Icon.TINT_LIST:
                    final long tintListToken = in.start(RemoteViewsProto.Icon.TINT_LIST);
                    values.put(RemoteViewsProto.Icon.TINT_LIST, ColorStateList.createFromProto(in));
                    in.end(tintListToken);
                    break;
                case (int) RemoteViewsProto.Icon.BITMAP:
                    byte[] bitmapData = in.readBytes(RemoteViewsProto.Icon.BITMAP);
                    values.put(RemoteViewsProto.Icon.BITMAP,
                            BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length));
                    break;
                case (int) RemoteViewsProto.Icon.ADAPTIVE_BITMAP:
                    final byte[] bitmapAdaptiveData = in.readBytes(
                            RemoteViewsProto.Icon.ADAPTIVE_BITMAP);
                    values.put(RemoteViewsProto.Icon.ADAPTIVE_BITMAP,
                            BitmapFactory.decodeByteArray(bitmapAdaptiveData, 0,
                                    bitmapAdaptiveData.length));
                    break;
                case (int) RemoteViewsProto.Icon.RESOURCE:
                    values.put(RemoteViewsProto.Icon.RESOURCE,
                            in.readString(RemoteViewsProto.Icon.RESOURCE));
                    break;
                case (int) RemoteViewsProto.Icon.DATA:
                    values.put(RemoteViewsProto.Icon.DATA,
                            in.readBytes(RemoteViewsProto.Icon.DATA));
                    break;
                case (int) RemoteViewsProto.Icon.URI:
                    values.put(RemoteViewsProto.Icon.URI, in.readString(RemoteViewsProto.Icon.URI));
                    break;
                case (int) RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP:
                    values.put(RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP,
                            in.readString(RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP));
                    break;
                default:
                    Log.w(TAG, "Unhandled field while reading Icon proto!\n"
                            + ProtoUtils.currentFieldToString(in));
            }
        }

        return (resources) -> {
            final int blendMode = (int) values.get(RemoteViewsProto.Icon.BLEND_MODE, -1);
            final ColorStateList tintList = (ColorStateList) values.get(
                    RemoteViewsProto.Icon.TINT_LIST);
            final Bitmap bitmap = (Bitmap) values.get(RemoteViewsProto.Icon.BITMAP);
            final Bitmap bitmapAdaptive = (Bitmap) values.get(
                    RemoteViewsProto.Icon.ADAPTIVE_BITMAP);
            final String resName = (String) values.get(RemoteViewsProto.Icon.RESOURCE);
            final int resource = resName != null ? resources.getIdentifier(resName, /* defType= */
                    null,
                    /* defPackage= */ null) : -1;
            final byte[] data = (byte[]) values.get(RemoteViewsProto.Icon.DATA);
            final String uri = (String) values.get(RemoteViewsProto.Icon.URI);
            final String uriAdaptive = (String) values.get(
                    RemoteViewsProto.Icon.URI_ADAPTIVE_BITMAP);
            Icon icon;
            if (bitmap != null) {
                icon = Icon.createWithBitmap(bitmap);
            } else if (bitmapAdaptive != null) {
                icon = Icon.createWithAdaptiveBitmap(bitmapAdaptive);
            } else if (resource != -1) {
                icon = Icon.createWithResource(resources, resource);
            } else if (data != null) {
                icon = Icon.createWithData(data, 0, data.length);
            } else if (uri != null) {
                icon = Icon.createWithContentUri(uri);
            } else if (uriAdaptive != null) {
                icon = Icon.createWithAdaptiveBitmapContentUri(uriAdaptive);
            } else {
                // Either this Icon has no data or is of an unknown type.
                return null;
            }

            if (tintList != null) {
                icon.setTintList(tintList);
            }
            if (blendMode != -1) {
                icon.setTintBlendMode(BlendMode.fromValue(blendMode));
            }
            return icon;
        };
    }
}
+18 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ option java_multiple_files = true;
package android.widget;

import "frameworks/base/core/proto/android/privacy.proto";
import "frameworks/base/core/proto/android/content/res/color_state_list.proto";

/**
 * An android.widget.RemoteViews object. This is used by RemoteViews.createPreviewFromProto
@@ -71,6 +72,23 @@ message RemoteViewsProto {
        optional int32 view_type_count = 4;
        optional bool attached = 5;
    }

    /**
     * An android.graphics.drawable Icon.
     */
    message Icon {
        option (android.msg_privacy).dest = DEST_AUTOMATIC;
        optional int32 blend_mode = 1;
        optional android.content.res.ColorStateListProto tint_list = 2;
        oneof icon {
            bytes bitmap = 3;
            string resource = 4;
            bytes data = 5;
            string uri = 6;
            string uri_adaptive_bitmap = 7;
            bytes adaptive_bitmap = 8;
        };
    }
}


+94 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.widget

import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.BlendMode
import android.graphics.Color
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Icon
import android.util.proto.ProtoInputStream
import android.util.proto.ProtoOutputStream
import android.widget.RemoteViewsSerializers.createIconFromProto
import android.widget.RemoteViewsSerializers.writeIconToProto
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.frameworks.coretests.R
import com.google.common.truth.Truth.assertThat
import java.io.ByteArrayOutputStream
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class RemoteViewsSerializersTest {
    private val context = ApplicationProvider.getApplicationContext<Context>()

    /**
     * Based on android.graphics.drawable.IconTest#testParcel
     */
    @Test
    fun testWriteIconToProto() {
        val bitmap = (context.getDrawable(R.drawable.landscape) as BitmapDrawable).bitmap
        val bitmapData = ByteArrayOutputStream().let {
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
            it.toByteArray()
        }

        for (icon in listOf(
            Icon.createWithBitmap(bitmap),
            Icon.createWithAdaptiveBitmap(bitmap),
            Icon.createWithData(bitmapData, 0, bitmapData.size),
            Icon.createWithResource(context, R.drawable.landscape),
            Icon.createWithContentUri("content://com.example.myapp/my_icon"),
            Icon.createWithAdaptiveBitmapContentUri("content://com.example.myapp/my_icon"),
        )) {
            icon.tintList = ColorStateList.valueOf(Color.RED)
            icon.tintBlendMode = BlendMode.SRC_OVER
            val bytes = ProtoOutputStream().let {
                writeIconToProto(it, context.resources, icon)
                it.bytes
            }

            val copy = ProtoInputStream(bytes).let {
                createIconFromProto(it).apply(context.resources)
            }
            assertThat(copy.type).isEqualTo(icon.type)
            assertThat(copy.tintBlendMode).isEqualTo(icon.tintBlendMode)
            assertThat(equalColorStateLists(copy.tintList, icon.tintList)).isTrue()

            when (icon.type) {
                Icon.TYPE_DATA, Icon.TYPE_URI, Icon.TYPE_URI_ADAPTIVE_BITMAP,
                Icon.TYPE_RESOURCE -> {
                    assertThat(copy.sameAs(icon)).isTrue()
                }

                Icon.TYPE_BITMAP, Icon.TYPE_ADAPTIVE_BITMAP -> {
                    assertThat(copy.bitmap.sameAs(icon.bitmap)).isTrue()
                }
            }
        }
    }
}

fun equalColorStateLists(a: ColorStateList?, b: ColorStateList?): Boolean {
    if (a == null && b == null) return true
    return a != null && b != null &&
            a.colors.contentEquals(b.colors) &&
            a.states.foldIndexed(true) { i, acc, it -> acc && it.contentEquals(b.states[i])}
}