Loading core/java/android/appwidget/flags.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -50,3 +50,10 @@ flag { purpose: PURPOSE_BUGFIX } } flag { name: "remote_views_proto" namespace: "app_widgets" description: "Enable support for persisting RemoteViews previews to Protobuf" bug: "306546610" } core/java/android/widget/RemoteViews.java +279 −0 Original line number Diff line number Diff line Loading @@ -17,8 +17,10 @@ package android.widget; import static android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL; import static android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO; import static android.appwidget.flags.Flags.drawDataParcel; import static android.appwidget.flags.Flags.remoteAdapterConversion; import static android.util.proto.ProtoInputStream.NO_MORE_FIELDS; import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR; import android.annotation.AttrRes; Loading Loading @@ -94,6 +96,9 @@ import android.util.SparseArray; import android.util.SparseIntArray; import android.util.TypedValue; import android.util.TypedValue.ComplexDimensionUnit; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import android.util.proto.ProtoUtils; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.LayoutInflater.Filter; Loading Loading @@ -7822,4 +7827,278 @@ public class RemoteViews implements Parcelable, Filter { mClassCookies = classCookies; } } /** * Write this RemoteViews to proto. * @hide */ @FlaggedApi(FLAG_REMOTE_VIEWS_PROTO) public void writePreviewToProto(@NonNull Context context, @NonNull ProtoOutputStream out) { if (mApplication != null) { // mApplication may be null if this was created with DrawInstructions constructor. out.write(RemoteViewsProto.PACKAGE_NAME, mApplication.packageName); } Resources appResources = getContextForResourcesEnsuringCorrectCachedApkPaths( context).getResources(); if (mLayoutId != 0) { out.write(RemoteViewsProto.LAYOUT_ID, appResources.getResourceName(mLayoutId)); } if (mLightBackgroundLayoutId != 0) { out.write(RemoteViewsProto.LIGHT_BACKGROUND_LAYOUT_ID, appResources.getResourceName(mLightBackgroundLayoutId)); } if (mViewId != 0 && mViewId != -1) { out.write(RemoteViewsProto.VIEW_ID, appResources.getResourceName(mViewId)); } out.write(RemoteViewsProto.IS_ROOT, mIsRoot); out.write(RemoteViewsProto.APPLY_FLAGS, mApplyFlags); out.write(RemoteViewsProto.HAS_DRAW_INSTRUCTIONS, mHasDrawInstructions); if (mProviderInstanceId != -1) { out.write(RemoteViewsProto.PROVIDER_INSTANCE_ID, mProviderInstanceId); } if (!hasMultipleLayouts()) { out.write(RemoteViewsProto.MODE, MODE_NORMAL); if (mIdealSize != null) { final long token = out.start(RemoteViewsProto.IDEAL_SIZE); out.write(SizeFProto.WIDTH, mIdealSize.getWidth()); out.write(SizeFProto.HEIGHT, mIdealSize.getHeight()); out.end(token); } } else if (hasSizedRemoteViews()) { out.write(RemoteViewsProto.MODE, MODE_HAS_SIZED_REMOTEVIEWS); for (RemoteViews view : mSizedRemoteViews) { final long sizedViewToken = out.start(RemoteViewsProto.SIZED_REMOTEVIEWS); view.writePreviewToProto(context, out); out.end(sizedViewToken); } } else { out.write(RemoteViewsProto.MODE, MODE_HAS_LANDSCAPE_AND_PORTRAIT); final long landscapeViewToken = out.start(RemoteViewsProto.LANDSCAPE_REMOTEVIEWS); mLandscape.writePreviewToProto(context, out); out.end(landscapeViewToken); final long portraitViewToken = out.start(RemoteViewsProto.PORTRAIT_REMOTEVIEWS); mPortrait.writePreviewToProto(context, out); out.end(portraitViewToken); } } /** * Create a RemoteViews from proto input. * @hide */ @FlaggedApi(FLAG_REMOTE_VIEWS_PROTO) public static RemoteViews createPreviewFromProto(Context context, ProtoInputStream in) throws Exception { return createFromProto(in).create(context, context.getResources(), /* rootData= */ null, /* depth= */ 0); } private static PendingResources<RemoteViews> createFromProto(ProtoInputStream in) throws Exception { // Grouping these variables into an anonymous object allows us to access them through `ref` // (which is final) later in the lambda. final var ref = new Object() { final RemoteViews mRv = new RemoteViews(); int mMode = 0; int mApplyFlags = 0; long mProviderInstanceId = -1; String mPackageName = null; SizeF mIdealSize = null; String mLayoutResName = null; String mLightBackgroundResName = null; String mViewResName = null; final List<PendingResources<RemoteViews>> mSizedRemoteViews = new ArrayList<>(); PendingResources<RemoteViews> mLandscapeViews = null; PendingResources<RemoteViews> mPortraitViews = null; boolean mIsRoot = false; boolean mHasDrawInstructions = false; }; try { while (in.nextField() != NO_MORE_FIELDS) { switch (in.getFieldNumber()) { case (int) RemoteViewsProto.MODE: ref.mMode = in.readInt(RemoteViewsProto.MODE); break; case (int) RemoteViewsProto.PACKAGE_NAME: ref.mPackageName = in.readString(RemoteViewsProto.PACKAGE_NAME); break; case (int) RemoteViewsProto.IDEAL_SIZE: final long idealSizeToken = in.start(RemoteViewsProto.IDEAL_SIZE); ref.mIdealSize = createSizeFFromProto(in); in.end(idealSizeToken); break; case (int) RemoteViewsProto.LAYOUT_ID: ref.mLayoutResName = in.readString(RemoteViewsProto.LAYOUT_ID); break; case (int) RemoteViewsProto.LIGHT_BACKGROUND_LAYOUT_ID: ref.mLightBackgroundResName = in.readString( RemoteViewsProto.LIGHT_BACKGROUND_LAYOUT_ID); break; case (int) RemoteViewsProto.VIEW_ID: ref.mViewResName = in.readString(RemoteViewsProto.VIEW_ID); break; case (int) RemoteViewsProto.APPLY_FLAGS: ref.mApplyFlags = in.readInt(RemoteViewsProto.APPLY_FLAGS); break; case (int) RemoteViewsProto.PROVIDER_INSTANCE_ID: ref.mProviderInstanceId = in.readInt(RemoteViewsProto.PROVIDER_INSTANCE_ID); break; case (int) RemoteViewsProto.SIZED_REMOTEVIEWS: final long sizedToken = in.start(RemoteViewsProto.SIZED_REMOTEVIEWS); ref.mSizedRemoteViews.add(createFromProto(in)); in.end(sizedToken); break; case (int) RemoteViewsProto.LANDSCAPE_REMOTEVIEWS: final long landscapeToken = in.start( RemoteViewsProto.LANDSCAPE_REMOTEVIEWS); ref.mLandscapeViews = createFromProto(in); in.end(landscapeToken); break; case (int) RemoteViewsProto.PORTRAIT_REMOTEVIEWS: final long portraitToken = in.start(RemoteViewsProto.PORTRAIT_REMOTEVIEWS); ref.mPortraitViews = createFromProto(in); in.end(portraitToken); break; case (int) RemoteViewsProto.IS_ROOT: ref.mIsRoot = in.readBoolean(RemoteViewsProto.IS_ROOT); break; case (int) RemoteViewsProto.HAS_DRAW_INSTRUCTIONS: ref.mHasDrawInstructions = in.readBoolean( RemoteViewsProto.HAS_DRAW_INSTRUCTIONS); break; default: Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n" + ProtoUtils.currentFieldToString(in)); } } } catch (IOException e) { throw new RuntimeException(e); } return (context, resources, rootData, depth) -> { if (depth > MAX_NESTED_VIEWS && (UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID)) { throw new IllegalArgumentException("Too many nested views."); } depth++; RemoteViews rv = ref.mRv; rv.mApplyFlags = ref.mApplyFlags; rv.mIsRoot = ref.mIsRoot; rv.mHasDrawInstructions = ref.mHasDrawInstructions; // The root view will read its HierarchyRootData (bitmap cache, collection cache) from // proto; all nested views will instead get it through the rootData parameter. if (rootData == null) { if (!rv.mIsRoot || depth != 1) { throw new IllegalStateException( "A nested view did not receive HierarchyRootData"); } rootData = rv.getHierarchyRootData(); } else { rv.configureAsChild(rootData); } Context appContext = null; Resources appResources = null; if (!ref.mHasDrawInstructions) { checkProtoResultNotNull(ref.mPackageName, "No application info"); rv.mApplication = context.getPackageManager().getApplicationInfo(ref.mPackageName, /* flags= */ 0); appContext = rv.getContextForResourcesEnsuringCorrectCachedApkPaths(context); appResources = appContext.getResources(); checkProtoResultNotNull(ref.mLayoutResName, "No layout id"); rv.mLayoutId = appResources.getIdentifier(ref.mLayoutResName, /* defType= */ null, /* defPackage= */ null); checkValidResource(rv.mLayoutId, "Invalid layout id", ref.mLayoutResName); if (ref.mViewResName != null) { rv.mViewId = appResources.getIdentifier(ref.mViewResName, /* defType= */ null, /* defPackage= */ null); checkValidResource(rv.mViewId, "Invalid view id", ref.mViewResName); } if (ref.mLightBackgroundResName != null) { int lightBackgroundLayoutId = appResources.getIdentifier( ref.mLightBackgroundResName, /* defType= */ null, /* defPackage= */ null); checkValidResource(lightBackgroundLayoutId, "Invalid light background layout id", ref.mLightBackgroundResName); rv.setLightBackgroundLayoutId(lightBackgroundLayoutId); } } if (ref.mProviderInstanceId != -1) { rv.mProviderInstanceId = ref.mProviderInstanceId; } if (ref.mMode == MODE_NORMAL) { rv.setIdealSize(ref.mIdealSize); return rv; } else if (ref.mMode == MODE_HAS_SIZED_REMOTEVIEWS) { List<RemoteViews> sizedViews = new ArrayList<>(); for (RemoteViews.PendingResources<RemoteViews> pendingViews : ref.mSizedRemoteViews) { RemoteViews views = pendingViews.create(context, resources, rootData, depth); sizedViews.add(views); } rv.initializeSizedRemoteViews(sizedViews.iterator()); return rv; } else if (ref.mMode == MODE_HAS_LANDSCAPE_AND_PORTRAIT) { checkProtoResultNotNull(ref.mLandscapeViews, "Missing landscape views"); checkProtoResultNotNull(ref.mPortraitViews, "Missing portrait views"); RemoteViews parentRv = new RemoteViews( ref.mLandscapeViews.create(context, resources, rootData, depth), ref.mPortraitViews.create(context, resources, rootData, depth)); parentRv.initializeFrom(/* src= */ rv, /* hierarchyRoot= */ rv); return parentRv; } else { throw new InvalidProtoException(ref.mMode + " is not a valid mode."); } }; } private static class InvalidProtoException extends Exception { InvalidProtoException(String message) { super(message); } } private interface PendingResources<T> { T create(Context context, Resources appResources, HierarchyRootData rootData, int depth) throws Exception; } private static void checkValidResource(int id, String message, String resName) throws Exception { if (id == 0) throw new Exception(message + ": " + resName); } private static void checkProtoResultNotNull(Object o, String message) throws InvalidProtoException { if (o == null) { throw new InvalidProtoException(message); } } private static SizeF createSizeFFromProto(ProtoInputStream in) throws Exception { float width = 0; float height = 0; while (in.nextField() != NO_MORE_FIELDS) { switch (in.getFieldNumber()) { case (int) SizeFProto.WIDTH: width = in.readFloat(SizeFProto.WIDTH); break; case (int) SizeFProto.HEIGHT: height = in.readFloat(SizeFProto.HEIGHT); break; default: Log.w(LOG_TAG, "Unhandled field while reading SizeF proto!\n" + ProtoUtils.currentFieldToString(in)); } } return new SizeF(width, height); } } core/proto/android/widget/remoteviews.proto 0 → 100644 +60 −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 optional 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. */ syntax = "proto2"; option java_multiple_files = true; package android.widget; import "frameworks/base/core/proto/android/privacy.proto"; /** * An android.widget.RemoteViews object. This is used by RemoteViews.createPreviewFromProto * and RemoteViews.writePreviewToProto. * * Any addition of fields here will require an update to the parsing code in RemoteViews.java. * Otherwise the field will be ignored when parsing (with a logged warning). * * Do not change the tag number or type of any fields in order to maintain compatibility with * previous versions. If a field is deleted, use `reserved` to mark its tag number. */ message RemoteViewsProto { option (android.msg_privacy).dest = DEST_AUTOMATIC; optional int32 mode = 1; optional string package_name = 2; optional string layout_id = 3; optional string light_background_layout_id = 4; optional string view_id = 5; optional SizeFProto ideal_size = 6; optional int32 apply_flags = 7; optional int64 provider_instance_id = 8; // RemoteViews for different sizes (created with RemoteViews(Map<SizeF, RemoteViews) // constructor). repeated RemoteViewsProto sized_remoteviews = 9; // RemoteViews for portrait/landscape (created with RemoteViews(RemoteViews, RemoteViews)i // constructor). optional RemoteViewsProto portrait_remoteviews = 10; optional RemoteViewsProto landscape_remoteviews = 11; optional bool is_root = 12; optional bool has_draw_instructions = 13; } message SizeFProto { optional float width = 1; optional float height = 2; } core/tests/coretests/src/android/widget/RemoteViewsProtoTest.java 0 → 100644 +155 −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 static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import android.content.Context; import android.util.SizeF; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import android.view.View; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.coretests.R; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import java.util.Map; /** * Tests for RemoteViews. */ @RunWith(AndroidJUnit4.class) @SmallTest public class RemoteViewsProtoTest { // This can point to any other package which exists on the device. private static final String OTHER_PACKAGE = "com.android.systemui"; @Rule public final ExpectedException exception = ExpectedException.none(); private Context mContext; private String mPackage; private LinearLayout mContainer; @Before public void setup() { mContext = InstrumentationRegistry.getContext(); mPackage = mContext.getPackageName(); mContainer = new LinearLayout(mContext); } @Test public void copy_canStillBeApplied() { RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test); RemoteViews clone = recreateFromProto(original); clone.apply(mContext, mContainer); } @SuppressWarnings("ReturnValueIgnored") @Test public void clone_repeatedly() { RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test); recreateFromProto(original); recreateFromProto(original); original.apply(mContext, mContainer); } @Test public void clone_chained() { RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test); RemoteViews clone = recreateFromProto(recreateFromProto(original)); clone.apply(mContext, mContainer); } @Test public void landscapePortraitViews_lightBackgroundLayoutFlag() { RemoteViews inner = new RemoteViews(mPackage, R.layout.remote_views_text); inner.setLightBackgroundLayoutId(R.layout.remote_views_light_background_text); RemoteViews parent = new RemoteViews(inner, inner); parent.addFlags(RemoteViews.FLAG_USE_LIGHT_BACKGROUND_LAYOUT); View view = recreateFromProto(parent).apply(mContext, mContainer); assertNull(view.findViewById(R.id.text)); assertNotNull(view.findViewById(R.id.light_background_text)); } @Test public void sizedViews_lightBackgroundLayoutFlag() { RemoteViews inner = new RemoteViews(mPackage, R.layout.remote_views_text); inner.setLightBackgroundLayoutId(R.layout.remote_views_light_background_text); RemoteViews parent = new RemoteViews( Map.of(new SizeF(0, 0), inner, new SizeF(100, 100), inner)); parent.addFlags(RemoteViews.FLAG_USE_LIGHT_BACKGROUND_LAYOUT); View view = recreateFromProto(parent).apply(mContext, mContainer); assertNull(view.findViewById(R.id.text)); assertNotNull(view.findViewById(R.id.light_background_text)); } @Test public void nestedLandscapeViews() throws Exception { RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test); for (int i = 0; i < 10; i++) { views = new RemoteViews(views, new RemoteViews(mPackage, R.layout.remote_views_test)); } // writeTo/createFromProto works recreateFromProto(views); views = new RemoteViews(mPackage, R.layout.remote_views_test); for (int i = 0; i < 11; i++) { views = new RemoteViews(views, new RemoteViews(mPackage, R.layout.remote_views_test)); } // writeTo/createFromProto fails exception.expect(IllegalArgumentException.class); recreateFromProtoNoRethrow(views); } private RemoteViews recreateFromProto(RemoteViews views) { try { return recreateFromProtoNoRethrow(views); } catch (Exception e) { throw new RuntimeException(e); } } private RemoteViews recreateFromProtoNoRethrow(RemoteViews views) throws Exception { ProtoOutputStream out = new ProtoOutputStream(); views.writePreviewToProto(mContext, out); ProtoInputStream in = new ProtoInputStream(out.getBytes()); return RemoteViews.createPreviewFromProto(mContext, in); } } services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java +2 −1 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import android.os.Parcel; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.util.Log; import android.util.proto.ProtoOutputStream; import android.view.LayoutInflater; import android.view.View; import android.widget.RemoteViews; Loading Loading @@ -105,7 +106,7 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { private static final ImmutableSet<Class<?>> UNUSABLE_TYPES = ImmutableSet.of(Consumer.class, IBinder.class, MediaSession.Token.class, Parcel.class, PrintWriter.class, Resources.Theme.class, View.class, LayoutInflater.Factory2.class); LayoutInflater.Factory2.class, ProtoOutputStream.class); // Maximum number of times we allow generating the same class recursively. // E.g. new RemoteViews.addView(new RemoteViews()) but stop there. Loading Loading
core/java/android/appwidget/flags.aconfig +7 −0 Original line number Diff line number Diff line Loading @@ -50,3 +50,10 @@ flag { purpose: PURPOSE_BUGFIX } } flag { name: "remote_views_proto" namespace: "app_widgets" description: "Enable support for persisting RemoteViews previews to Protobuf" bug: "306546610" }
core/java/android/widget/RemoteViews.java +279 −0 Original line number Diff line number Diff line Loading @@ -17,8 +17,10 @@ package android.widget; import static android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL; import static android.appwidget.flags.Flags.FLAG_REMOTE_VIEWS_PROTO; import static android.appwidget.flags.Flags.drawDataParcel; import static android.appwidget.flags.Flags.remoteAdapterConversion; import static android.util.proto.ProtoInputStream.NO_MORE_FIELDS; import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR; import android.annotation.AttrRes; Loading Loading @@ -94,6 +96,9 @@ import android.util.SparseArray; import android.util.SparseIntArray; import android.util.TypedValue; import android.util.TypedValue.ComplexDimensionUnit; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import android.util.proto.ProtoUtils; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.LayoutInflater.Filter; Loading Loading @@ -7822,4 +7827,278 @@ public class RemoteViews implements Parcelable, Filter { mClassCookies = classCookies; } } /** * Write this RemoteViews to proto. * @hide */ @FlaggedApi(FLAG_REMOTE_VIEWS_PROTO) public void writePreviewToProto(@NonNull Context context, @NonNull ProtoOutputStream out) { if (mApplication != null) { // mApplication may be null if this was created with DrawInstructions constructor. out.write(RemoteViewsProto.PACKAGE_NAME, mApplication.packageName); } Resources appResources = getContextForResourcesEnsuringCorrectCachedApkPaths( context).getResources(); if (mLayoutId != 0) { out.write(RemoteViewsProto.LAYOUT_ID, appResources.getResourceName(mLayoutId)); } if (mLightBackgroundLayoutId != 0) { out.write(RemoteViewsProto.LIGHT_BACKGROUND_LAYOUT_ID, appResources.getResourceName(mLightBackgroundLayoutId)); } if (mViewId != 0 && mViewId != -1) { out.write(RemoteViewsProto.VIEW_ID, appResources.getResourceName(mViewId)); } out.write(RemoteViewsProto.IS_ROOT, mIsRoot); out.write(RemoteViewsProto.APPLY_FLAGS, mApplyFlags); out.write(RemoteViewsProto.HAS_DRAW_INSTRUCTIONS, mHasDrawInstructions); if (mProviderInstanceId != -1) { out.write(RemoteViewsProto.PROVIDER_INSTANCE_ID, mProviderInstanceId); } if (!hasMultipleLayouts()) { out.write(RemoteViewsProto.MODE, MODE_NORMAL); if (mIdealSize != null) { final long token = out.start(RemoteViewsProto.IDEAL_SIZE); out.write(SizeFProto.WIDTH, mIdealSize.getWidth()); out.write(SizeFProto.HEIGHT, mIdealSize.getHeight()); out.end(token); } } else if (hasSizedRemoteViews()) { out.write(RemoteViewsProto.MODE, MODE_HAS_SIZED_REMOTEVIEWS); for (RemoteViews view : mSizedRemoteViews) { final long sizedViewToken = out.start(RemoteViewsProto.SIZED_REMOTEVIEWS); view.writePreviewToProto(context, out); out.end(sizedViewToken); } } else { out.write(RemoteViewsProto.MODE, MODE_HAS_LANDSCAPE_AND_PORTRAIT); final long landscapeViewToken = out.start(RemoteViewsProto.LANDSCAPE_REMOTEVIEWS); mLandscape.writePreviewToProto(context, out); out.end(landscapeViewToken); final long portraitViewToken = out.start(RemoteViewsProto.PORTRAIT_REMOTEVIEWS); mPortrait.writePreviewToProto(context, out); out.end(portraitViewToken); } } /** * Create a RemoteViews from proto input. * @hide */ @FlaggedApi(FLAG_REMOTE_VIEWS_PROTO) public static RemoteViews createPreviewFromProto(Context context, ProtoInputStream in) throws Exception { return createFromProto(in).create(context, context.getResources(), /* rootData= */ null, /* depth= */ 0); } private static PendingResources<RemoteViews> createFromProto(ProtoInputStream in) throws Exception { // Grouping these variables into an anonymous object allows us to access them through `ref` // (which is final) later in the lambda. final var ref = new Object() { final RemoteViews mRv = new RemoteViews(); int mMode = 0; int mApplyFlags = 0; long mProviderInstanceId = -1; String mPackageName = null; SizeF mIdealSize = null; String mLayoutResName = null; String mLightBackgroundResName = null; String mViewResName = null; final List<PendingResources<RemoteViews>> mSizedRemoteViews = new ArrayList<>(); PendingResources<RemoteViews> mLandscapeViews = null; PendingResources<RemoteViews> mPortraitViews = null; boolean mIsRoot = false; boolean mHasDrawInstructions = false; }; try { while (in.nextField() != NO_MORE_FIELDS) { switch (in.getFieldNumber()) { case (int) RemoteViewsProto.MODE: ref.mMode = in.readInt(RemoteViewsProto.MODE); break; case (int) RemoteViewsProto.PACKAGE_NAME: ref.mPackageName = in.readString(RemoteViewsProto.PACKAGE_NAME); break; case (int) RemoteViewsProto.IDEAL_SIZE: final long idealSizeToken = in.start(RemoteViewsProto.IDEAL_SIZE); ref.mIdealSize = createSizeFFromProto(in); in.end(idealSizeToken); break; case (int) RemoteViewsProto.LAYOUT_ID: ref.mLayoutResName = in.readString(RemoteViewsProto.LAYOUT_ID); break; case (int) RemoteViewsProto.LIGHT_BACKGROUND_LAYOUT_ID: ref.mLightBackgroundResName = in.readString( RemoteViewsProto.LIGHT_BACKGROUND_LAYOUT_ID); break; case (int) RemoteViewsProto.VIEW_ID: ref.mViewResName = in.readString(RemoteViewsProto.VIEW_ID); break; case (int) RemoteViewsProto.APPLY_FLAGS: ref.mApplyFlags = in.readInt(RemoteViewsProto.APPLY_FLAGS); break; case (int) RemoteViewsProto.PROVIDER_INSTANCE_ID: ref.mProviderInstanceId = in.readInt(RemoteViewsProto.PROVIDER_INSTANCE_ID); break; case (int) RemoteViewsProto.SIZED_REMOTEVIEWS: final long sizedToken = in.start(RemoteViewsProto.SIZED_REMOTEVIEWS); ref.mSizedRemoteViews.add(createFromProto(in)); in.end(sizedToken); break; case (int) RemoteViewsProto.LANDSCAPE_REMOTEVIEWS: final long landscapeToken = in.start( RemoteViewsProto.LANDSCAPE_REMOTEVIEWS); ref.mLandscapeViews = createFromProto(in); in.end(landscapeToken); break; case (int) RemoteViewsProto.PORTRAIT_REMOTEVIEWS: final long portraitToken = in.start(RemoteViewsProto.PORTRAIT_REMOTEVIEWS); ref.mPortraitViews = createFromProto(in); in.end(portraitToken); break; case (int) RemoteViewsProto.IS_ROOT: ref.mIsRoot = in.readBoolean(RemoteViewsProto.IS_ROOT); break; case (int) RemoteViewsProto.HAS_DRAW_INSTRUCTIONS: ref.mHasDrawInstructions = in.readBoolean( RemoteViewsProto.HAS_DRAW_INSTRUCTIONS); break; default: Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n" + ProtoUtils.currentFieldToString(in)); } } } catch (IOException e) { throw new RuntimeException(e); } return (context, resources, rootData, depth) -> { if (depth > MAX_NESTED_VIEWS && (UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID)) { throw new IllegalArgumentException("Too many nested views."); } depth++; RemoteViews rv = ref.mRv; rv.mApplyFlags = ref.mApplyFlags; rv.mIsRoot = ref.mIsRoot; rv.mHasDrawInstructions = ref.mHasDrawInstructions; // The root view will read its HierarchyRootData (bitmap cache, collection cache) from // proto; all nested views will instead get it through the rootData parameter. if (rootData == null) { if (!rv.mIsRoot || depth != 1) { throw new IllegalStateException( "A nested view did not receive HierarchyRootData"); } rootData = rv.getHierarchyRootData(); } else { rv.configureAsChild(rootData); } Context appContext = null; Resources appResources = null; if (!ref.mHasDrawInstructions) { checkProtoResultNotNull(ref.mPackageName, "No application info"); rv.mApplication = context.getPackageManager().getApplicationInfo(ref.mPackageName, /* flags= */ 0); appContext = rv.getContextForResourcesEnsuringCorrectCachedApkPaths(context); appResources = appContext.getResources(); checkProtoResultNotNull(ref.mLayoutResName, "No layout id"); rv.mLayoutId = appResources.getIdentifier(ref.mLayoutResName, /* defType= */ null, /* defPackage= */ null); checkValidResource(rv.mLayoutId, "Invalid layout id", ref.mLayoutResName); if (ref.mViewResName != null) { rv.mViewId = appResources.getIdentifier(ref.mViewResName, /* defType= */ null, /* defPackage= */ null); checkValidResource(rv.mViewId, "Invalid view id", ref.mViewResName); } if (ref.mLightBackgroundResName != null) { int lightBackgroundLayoutId = appResources.getIdentifier( ref.mLightBackgroundResName, /* defType= */ null, /* defPackage= */ null); checkValidResource(lightBackgroundLayoutId, "Invalid light background layout id", ref.mLightBackgroundResName); rv.setLightBackgroundLayoutId(lightBackgroundLayoutId); } } if (ref.mProviderInstanceId != -1) { rv.mProviderInstanceId = ref.mProviderInstanceId; } if (ref.mMode == MODE_NORMAL) { rv.setIdealSize(ref.mIdealSize); return rv; } else if (ref.mMode == MODE_HAS_SIZED_REMOTEVIEWS) { List<RemoteViews> sizedViews = new ArrayList<>(); for (RemoteViews.PendingResources<RemoteViews> pendingViews : ref.mSizedRemoteViews) { RemoteViews views = pendingViews.create(context, resources, rootData, depth); sizedViews.add(views); } rv.initializeSizedRemoteViews(sizedViews.iterator()); return rv; } else if (ref.mMode == MODE_HAS_LANDSCAPE_AND_PORTRAIT) { checkProtoResultNotNull(ref.mLandscapeViews, "Missing landscape views"); checkProtoResultNotNull(ref.mPortraitViews, "Missing portrait views"); RemoteViews parentRv = new RemoteViews( ref.mLandscapeViews.create(context, resources, rootData, depth), ref.mPortraitViews.create(context, resources, rootData, depth)); parentRv.initializeFrom(/* src= */ rv, /* hierarchyRoot= */ rv); return parentRv; } else { throw new InvalidProtoException(ref.mMode + " is not a valid mode."); } }; } private static class InvalidProtoException extends Exception { InvalidProtoException(String message) { super(message); } } private interface PendingResources<T> { T create(Context context, Resources appResources, HierarchyRootData rootData, int depth) throws Exception; } private static void checkValidResource(int id, String message, String resName) throws Exception { if (id == 0) throw new Exception(message + ": " + resName); } private static void checkProtoResultNotNull(Object o, String message) throws InvalidProtoException { if (o == null) { throw new InvalidProtoException(message); } } private static SizeF createSizeFFromProto(ProtoInputStream in) throws Exception { float width = 0; float height = 0; while (in.nextField() != NO_MORE_FIELDS) { switch (in.getFieldNumber()) { case (int) SizeFProto.WIDTH: width = in.readFloat(SizeFProto.WIDTH); break; case (int) SizeFProto.HEIGHT: height = in.readFloat(SizeFProto.HEIGHT); break; default: Log.w(LOG_TAG, "Unhandled field while reading SizeF proto!\n" + ProtoUtils.currentFieldToString(in)); } } return new SizeF(width, height); } }
core/proto/android/widget/remoteviews.proto 0 → 100644 +60 −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 optional 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. */ syntax = "proto2"; option java_multiple_files = true; package android.widget; import "frameworks/base/core/proto/android/privacy.proto"; /** * An android.widget.RemoteViews object. This is used by RemoteViews.createPreviewFromProto * and RemoteViews.writePreviewToProto. * * Any addition of fields here will require an update to the parsing code in RemoteViews.java. * Otherwise the field will be ignored when parsing (with a logged warning). * * Do not change the tag number or type of any fields in order to maintain compatibility with * previous versions. If a field is deleted, use `reserved` to mark its tag number. */ message RemoteViewsProto { option (android.msg_privacy).dest = DEST_AUTOMATIC; optional int32 mode = 1; optional string package_name = 2; optional string layout_id = 3; optional string light_background_layout_id = 4; optional string view_id = 5; optional SizeFProto ideal_size = 6; optional int32 apply_flags = 7; optional int64 provider_instance_id = 8; // RemoteViews for different sizes (created with RemoteViews(Map<SizeF, RemoteViews) // constructor). repeated RemoteViewsProto sized_remoteviews = 9; // RemoteViews for portrait/landscape (created with RemoteViews(RemoteViews, RemoteViews)i // constructor). optional RemoteViewsProto portrait_remoteviews = 10; optional RemoteViewsProto landscape_remoteviews = 11; optional bool is_root = 12; optional bool has_draw_instructions = 13; } message SizeFProto { optional float width = 1; optional float height = 2; }
core/tests/coretests/src/android/widget/RemoteViewsProtoTest.java 0 → 100644 +155 −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 static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import android.content.Context; import android.util.SizeF; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import android.view.View; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.frameworks.coretests.R; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import java.util.Map; /** * Tests for RemoteViews. */ @RunWith(AndroidJUnit4.class) @SmallTest public class RemoteViewsProtoTest { // This can point to any other package which exists on the device. private static final String OTHER_PACKAGE = "com.android.systemui"; @Rule public final ExpectedException exception = ExpectedException.none(); private Context mContext; private String mPackage; private LinearLayout mContainer; @Before public void setup() { mContext = InstrumentationRegistry.getContext(); mPackage = mContext.getPackageName(); mContainer = new LinearLayout(mContext); } @Test public void copy_canStillBeApplied() { RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test); RemoteViews clone = recreateFromProto(original); clone.apply(mContext, mContainer); } @SuppressWarnings("ReturnValueIgnored") @Test public void clone_repeatedly() { RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test); recreateFromProto(original); recreateFromProto(original); original.apply(mContext, mContainer); } @Test public void clone_chained() { RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test); RemoteViews clone = recreateFromProto(recreateFromProto(original)); clone.apply(mContext, mContainer); } @Test public void landscapePortraitViews_lightBackgroundLayoutFlag() { RemoteViews inner = new RemoteViews(mPackage, R.layout.remote_views_text); inner.setLightBackgroundLayoutId(R.layout.remote_views_light_background_text); RemoteViews parent = new RemoteViews(inner, inner); parent.addFlags(RemoteViews.FLAG_USE_LIGHT_BACKGROUND_LAYOUT); View view = recreateFromProto(parent).apply(mContext, mContainer); assertNull(view.findViewById(R.id.text)); assertNotNull(view.findViewById(R.id.light_background_text)); } @Test public void sizedViews_lightBackgroundLayoutFlag() { RemoteViews inner = new RemoteViews(mPackage, R.layout.remote_views_text); inner.setLightBackgroundLayoutId(R.layout.remote_views_light_background_text); RemoteViews parent = new RemoteViews( Map.of(new SizeF(0, 0), inner, new SizeF(100, 100), inner)); parent.addFlags(RemoteViews.FLAG_USE_LIGHT_BACKGROUND_LAYOUT); View view = recreateFromProto(parent).apply(mContext, mContainer); assertNull(view.findViewById(R.id.text)); assertNotNull(view.findViewById(R.id.light_background_text)); } @Test public void nestedLandscapeViews() throws Exception { RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test); for (int i = 0; i < 10; i++) { views = new RemoteViews(views, new RemoteViews(mPackage, R.layout.remote_views_test)); } // writeTo/createFromProto works recreateFromProto(views); views = new RemoteViews(mPackage, R.layout.remote_views_test); for (int i = 0; i < 11; i++) { views = new RemoteViews(views, new RemoteViews(mPackage, R.layout.remote_views_test)); } // writeTo/createFromProto fails exception.expect(IllegalArgumentException.class); recreateFromProtoNoRethrow(views); } private RemoteViews recreateFromProto(RemoteViews views) { try { return recreateFromProtoNoRethrow(views); } catch (Exception e) { throw new RuntimeException(e); } } private RemoteViews recreateFromProtoNoRethrow(RemoteViews views) throws Exception { ProtoOutputStream out = new ProtoOutputStream(); views.writePreviewToProto(mContext, out); ProtoInputStream in = new ProtoInputStream(out.getBytes()); return RemoteViews.createPreviewFromProto(mContext, in); } }
services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java +2 −1 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import android.os.Parcel; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.util.Log; import android.util.proto.ProtoOutputStream; import android.view.LayoutInflater; import android.view.View; import android.widget.RemoteViews; Loading Loading @@ -105,7 +106,7 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { private static final ImmutableSet<Class<?>> UNUSABLE_TYPES = ImmutableSet.of(Consumer.class, IBinder.class, MediaSession.Token.class, Parcel.class, PrintWriter.class, Resources.Theme.class, View.class, LayoutInflater.Factory2.class); LayoutInflater.Factory2.class, ProtoOutputStream.class); // Maximum number of times we allow generating the same class recursively. // E.g. new RemoteViews.addView(new RemoteViews()) but stop there. Loading