diff options
| author | 2024-07-23 23:23:31 +0000 | |
|---|---|---|
| committer | 2024-07-23 23:23:31 +0000 | |
| commit | daa08602ea43202fb9e109eff4e593b31aa0b1d6 (patch) | |
| tree | 6b13b857d5ec2d45c1601e053763b1e437901fc0 | |
| parent | 9928e34fa49b3807833b77595efaa923a959eb27 (diff) | |
| parent | e0c3f6e6d22c04bf4dbfe0c08907956000de54bc (diff) | |
Merge "Write Icon to proto" into main
4 files changed, 290 insertions, 1 deletions
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 0dadbe374aa4..eb3581717637 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -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; } diff --git a/core/java/android/widget/RemoteViewsSerializers.java b/core/java/android/widget/RemoteViewsSerializers.java new file mode 100644 index 000000000000..600fea4a0bb8 --- /dev/null +++ b/core/java/android/widget/RemoteViewsSerializers.java @@ -0,0 +1,177 @@ +/* + * 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; + }; + } +} diff --git a/core/proto/android/widget/remoteviews.proto b/core/proto/android/widget/remoteviews.proto index f08ea1b6f092..37d1c5b03ee5 100644 --- a/core/proto/android/widget/remoteviews.proto +++ b/core/proto/android/widget/remoteviews.proto @@ -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; + }; + } } diff --git a/core/tests/coretests/src/android/widget/RemoteViewsSerializersTest.kt b/core/tests/coretests/src/android/widget/RemoteViewsSerializersTest.kt new file mode 100644 index 000000000000..44d10d32606c --- /dev/null +++ b/core/tests/coretests/src/android/widget/RemoteViewsSerializersTest.kt @@ -0,0 +1,94 @@ +/* + * 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])} +} |