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

Commit 0aac72e8 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge changes from topic "origami-widget" into main

* changes:
  Wire on click handlers for RemoteCanvas
  Add API which uses RemoteViews as a vehicle for draw instructions
parents b79728f6 fa4a42a9
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -59297,6 +59297,7 @@ package android.widget {
    ctor public RemoteViews(@NonNull java.util.Map<android.util.SizeF,android.widget.RemoteViews>);
    ctor public RemoteViews(android.widget.RemoteViews);
    ctor public RemoteViews(android.os.Parcel);
    ctor @FlaggedApi("android.appwidget.flags.draw_data_parcel") public RemoteViews(@NonNull android.widget.RemoteViews.DrawInstructions);
    method public void addStableView(@IdRes int, @NonNull android.widget.RemoteViews, int);
    method public void addView(@IdRes int, android.widget.RemoteViews);
    method public android.view.View apply(android.content.Context, android.view.ViewGroup);
@@ -59405,6 +59406,15 @@ package android.widget {
    ctor public RemoteViews.ActionException(String);
  }
  @FlaggedApi("android.appwidget.flags.draw_data_parcel") public static final class RemoteViews.DrawInstructions {
    method @FlaggedApi("android.appwidget.flags.draw_data_parcel") public void appendInstructions(@NonNull byte[]);
  }
  @FlaggedApi("android.appwidget.flags.draw_data_parcel") public static final class RemoteViews.DrawInstructions.Builder {
    ctor @FlaggedApi("android.appwidget.flags.draw_data_parcel") public RemoteViews.DrawInstructions.Builder(@NonNull java.util.List<byte[]>);
    method @FlaggedApi("android.appwidget.flags.draw_data_parcel") @NonNull public android.widget.RemoteViews.DrawInstructions build();
  }
  public static final class RemoteViews.RemoteCollectionItems implements android.os.Parcelable {
    method public int describeContents();
    method public int getItemCount();
+117 −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 android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL;

import android.annotation.AttrRes;
import android.annotation.FlaggedApi;
import android.annotation.StyleRes;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import java.util.function.IntConsumer;

/**
 * {@link RemoteCanvas} is designed to support arbitrary protocols between two processes using
 * {@link RemoteViews.DrawInstructions}. Upon instantiation in the host process,
 * {@link RemoteCanvas#setDrawInstructions(RemoteViews.DrawInstructions)} is called so that the
 * host process can render the {@link RemoteViews.DrawInstructions} from the provider process
 * accordingly.
 *
 * @hide
 */
@FlaggedApi(FLAG_DRAW_DATA_PARCEL)
public class RemoteCanvas extends View {

    private static final String TAG = "RemoteCanvas";

    @Nullable
    private SparseArray<Runnable> mCallbacks;

    private final IntConsumer mOnClickHandler = (viewId) -> {
        if (mCallbacks == null) {
            Log.w(TAG, "Cannot find callback for " + viewId
                    + ", in fact there were no callbacks from this RemoteViews at all.");
            return;
        }
        final Runnable cb = getCallbacks().get(viewId);
        if (cb != null) {
            cb.run();
        } else {
            Log.w(TAG, "Cannot find callback for " + viewId);
        }
    };

    RemoteCanvas(@NonNull Context context) {
        super(context);
    }

    RemoteCanvas(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    RemoteCanvas(@NonNull Context context, @Nullable AttributeSet attrs,
                 @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    RemoteCanvas(@NonNull Context context, @Nullable AttributeSet attrs,
                 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    /**
     * Setter method for the {@link RemoteViews.DrawInstructions} from the provider process for
     * the host process to render accordingly.
     *
     * @param instructions {@link RemoteViews.DrawInstructions} from the provider process.
     */
    void setDrawInstructions(@NonNull final RemoteViews.DrawInstructions instructions) {
        setTag(instructions);
        // TODO: handle draw instructions
        // TODO: attach mOnClickHandler
    }

    /**
     * Adds a callback function to a clickable area in the RemoteCanvas.
     *
     * @param viewId the viewId of the clickable area
     * @param cb the callback function to be triggered when clicked
     */
    void addOnClickHandler(final int viewId, @NonNull final Runnable cb) {
        getCallbacks().set(viewId, cb);
    }

    /**
     * Returns all callbacks added to the RemoteCanvas through
     * {@link #addOnClickHandler(int, Runnable)}.
     */
    @VisibleForTesting
    public SparseArray<Runnable> getCallbacks() {
        if (mCallbacks == null) {
            mCallbacks = new SparseArray<>();
        }
        return mCallbacks;
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -18,3 +18,4 @@ package android.widget;

parcelable RemoteViews;
parcelable RemoteViews.RemoteCollectionItems;
parcelable RemoteViews.DrawInstructions;
+231 −10
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.widget;

import static android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL;
import static android.appwidget.flags.Flags.drawDataParcel;
import static android.appwidget.flags.Flags.remoteAdapterConversion;
import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR;

@@ -243,6 +245,7 @@ public class RemoteViews implements Parcelable, Filter {
    private static final int ATTRIBUTE_REFLECTION_ACTION_TAG = 32;
    private static final int SET_REMOTE_ADAPTER_TAG = 33;
    private static final int SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG = 34;
    private static final int SET_DRAW_INSTRUCTION_TAG = 35;

    /** @hide **/
    @IntDef(prefix = "MARGIN_", value = {
@@ -442,6 +445,19 @@ public class RemoteViews implements Parcelable, Filter {
    @Nullable
    private LayoutInflater.Factory2 mLayoutInflaterFactory2;

    /**
     * Indicates whether this {@link RemoteViews} was instantiated with a {@link DrawInstructions}
     * object. {@link DrawInstructions} serves as an alternative protocol for the host process
     * to render.
     */
    private boolean mHasDrawInstructions;

    @Nullable
    private SparseArray<PendingIntent> mPendingIntentTemplate;

    @Nullable
    private SparseArray<Intent> mFillInIntent;

    private static final InteractionHandler DEFAULT_INTERACTION_HANDLER =
            (view, pendingIntent, response) ->
                    startPendingIntent(view, pendingIntent, response.getLaunchOptions(view));
@@ -1463,6 +1479,11 @@ public class RemoteViews implements Parcelable, Filter {

        @Override
        public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
            if (hasDrawInstructions() && root instanceof RemoteCanvas target) {
                target.addOnClickHandler(mViewId, () ->
                        mResponse.handleViewInteraction(root, params.handler));
                return;
            }
            final View target = root.findViewById(mViewId);
            if (target == null) return;

@@ -3851,6 +3872,45 @@ public class RemoteViews implements Parcelable, Filter {
        }
    }

    private static class SetDrawInstructionAction extends Action {

        @Nullable
        private final DrawInstructions mInstructions;

        SetDrawInstructionAction(@NonNull final DrawInstructions instructions) {
            mInstructions = instructions;
        }

        SetDrawInstructionAction(@NonNull final Parcel in) {
            if (drawDataParcel()) {
                mInstructions = DrawInstructions.readFromParcel(in);
            } else {
                mInstructions = null;
            }
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            if (drawDataParcel()) {
                DrawInstructions.writeToParcel(mInstructions, dest, flags);
            }
        }

        @Override
        public void apply(View root, ViewGroup rootParent, ActionApplyParams params)
                throws ActionException {
            if (drawDataParcel() && mInstructions != null
                    && root instanceof RemoteCanvas remoteCanvas) {
                remoteCanvas.setDrawInstructions(mInstructions);
            }
        }

        @Override
        public int getActionTag() {
            return SET_DRAW_INSTRUCTION_TAG;
        }
    }

    /**
     * Create a new RemoteViews object that will display the views contained
     * in the specified layout file.
@@ -4080,6 +4140,7 @@ public class RemoteViews implements Parcelable, Filter {
        mClassCookies = src.mClassCookies;
        mIdealSize = src.mIdealSize;
        mProviderInstanceId = src.mProviderInstanceId;
        mHasDrawInstructions = src.mHasDrawInstructions;

        if (src.hasLandscapeAndPortraitLayouts()) {
            mLandscape = createInitializedFrom(src.mLandscape, hierarchyRoot);
@@ -4114,12 +4175,26 @@ public class RemoteViews implements Parcelable, Filter {
    /**
     * Reads a RemoteViews object from a parcel.
     *
     * @param parcel
     * @param parcel the parcel object
     */
    public RemoteViews(Parcel parcel) {
        this(parcel, /* rootData= */ null, /* info= */ null, /* depth= */ 0);
    }

    /**
     * Instantiates a RemoteViews object using {@link DrawInstructions}, which serves as an
     * alternative to XML layout. {@link DrawInstructions} objects contains the instructions which
     * can be interpreted and rendered accordingly in the host process.
     *
     * @param drawInstructions The {@link DrawInstructions} object
     */
    @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
    public RemoteViews(@NonNull final DrawInstructions drawInstructions) {
        Objects.requireNonNull(drawInstructions);
        mHasDrawInstructions = true;
        addAction(new SetDrawInstructionAction(drawInstructions));
    }

    private RemoteViews(@NonNull Parcel parcel, @Nullable HierarchyRootData rootData,
            @Nullable ApplicationInfo info, int depth) {
        if (depth > MAX_NESTED_VIEWS
@@ -4178,6 +4253,7 @@ public class RemoteViews implements Parcelable, Filter {
        }
        mApplyFlags = parcel.readInt();
        mProviderInstanceId = parcel.readLong();
        mHasDrawInstructions = parcel.readBoolean();

        // Ensure that all descendants have their caches set up recursively.
        if (mIsRoot) {
@@ -4254,6 +4330,8 @@ public class RemoteViews implements Parcelable, Filter {
                return new AttributeReflectionAction(parcel);
            case SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG:
                return new SetOnStylusHandwritingResponse(parcel);
            case SET_DRAW_INSTRUCTION_TAG:
                return new SetDrawInstructionAction(parcel);
            default:
                throw new ActionException("Tag " + tag + " not found");
        }
@@ -4747,8 +4825,13 @@ public class RemoteViews implements Parcelable, Filter {
     *          by a child of viewId and executed when that child is clicked
     */
    public void setPendingIntentTemplate(@IdRes int viewId, PendingIntent pendingIntentTemplate) {
        if (hasDrawInstructions()) {
            getPendingIntentTemplate().set(viewId, pendingIntentTemplate);
            tryAddRemoteResponse(viewId);
        } else {
            addAction(new SetPendingIntentTemplate(viewId, pendingIntentTemplate));
        }
    }

    /**
     * When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is very
@@ -4768,8 +4851,13 @@ public class RemoteViews implements Parcelable, Filter {
     *        in order to determine the on-click behavior of the view specified by viewId
     */
    public void setOnClickFillInIntent(@IdRes int viewId, Intent fillInIntent) {
        if (hasDrawInstructions()) {
            getFillInIntent().set(viewId, fillInIntent);
            tryAddRemoteResponse(viewId);
        } else {
            setOnClickResponse(viewId, RemoteResponse.fromFillInIntent(fillInIntent));
        }
    }

    /**
     * Equivalent to calling
@@ -5791,6 +5879,10 @@ public class RemoteViews implements Parcelable, Filter {
        }
    }

    private boolean hasDrawInstructions() {
        return mHasDrawInstructions;
    }

    private RemoteViews getRemoteViewsToApply(Context context) {
        if (hasLandscapeAndPortraitLayouts()) {
            int orientation = context.getResources().getConfiguration().orientation;
@@ -5973,6 +6065,10 @@ public class RemoteViews implements Parcelable, Filter {
        if (applyThemeResId != 0) {
            inflationContext = new ContextThemeWrapper(inflationContext, applyThemeResId);
        }
        // If the RemoteViews contains draw instructions, just use it instead.
        if (rv.hasDrawInstructions()) {
            return new RemoteCanvas(inflationContext);
        }
        LayoutInflater inflater = LayoutInflater.from(context);

        // Clone inflater so we load resources from correct context and
@@ -6236,7 +6332,7 @@ public class RemoteViews implements Parcelable, Filter {

    /** @hide */
    public boolean canRecycleView(@Nullable View v) {
        if (v == null) {
        if (v == null || hasDrawInstructions()) {
            return false;
        }
        Integer previousLayoutId = (Integer) v.getTag(R.id.widget_frame);
@@ -6388,6 +6484,32 @@ public class RemoteViews implements Parcelable, Filter {
        return context;
    }

    @NonNull
    private SparseArray<PendingIntent> getPendingIntentTemplate() {
        if (mPendingIntentTemplate == null) {
            mPendingIntentTemplate = new SparseArray<>();
        }
        return mPendingIntentTemplate;
    }

    @NonNull
    private SparseArray<Intent> getFillInIntent() {
        if (mFillInIntent == null) {
            mFillInIntent = new SparseArray<>();
        }
        return mFillInIntent;
    }

    private void tryAddRemoteResponse(final int viewId) {
        final PendingIntent pendingIntent = getPendingIntentTemplate().get(viewId);
        final Intent intent = getFillInIntent().get(viewId);
        if (pendingIntent != null && intent != null) {
            addAction(new SetOnClickResponse(viewId,
                    RemoteResponse.fromPendingIntentTemplateAndFillInIntent(
                            pendingIntent, intent)));
        }
    }

    /**
     * Utility class to hold all the options when applying the remote views
     * @hide
@@ -6624,6 +6746,7 @@ public class RemoteViews implements Parcelable, Filter {
        }
        dest.writeInt(mApplyFlags);
        dest.writeLong(mProviderInstanceId);
        dest.writeBoolean(mHasDrawInstructions);

        dest.restoreAllowSquashing(prevSquashingAllowed);
    }
@@ -6926,6 +7049,14 @@ public class RemoteViews implements Parcelable, Filter {
            return response;
        }

        private static RemoteResponse fromPendingIntentTemplateAndFillInIntent(
                @NonNull final PendingIntent pendingIntent, @NonNull final Intent intent) {
            RemoteResponse response = new RemoteResponse();
            response.mPendingIntent = pendingIntent;
            response.mFillIntent = intent;
            return response;
        }

        /**
         * Adds a shared element to be transferred as part of the transition between Activities
         * using cross-Activity scene animations. The position of the first element will be used as
@@ -6964,8 +7095,8 @@ public class RemoteViews implements Parcelable, Filter {

        private void writeToParcel(Parcel dest, int flags) {
            PendingIntent.writePendingIntentOrNullToParcel(mPendingIntent, dest);
            if (mPendingIntent == null) {
                // Only write the intent if pending intent is null
            dest.writeBoolean((mFillIntent != null));
            if (mFillIntent != null) {
                dest.writeTypedObject(mFillIntent, flags);
            }
            dest.writeInt(mInteractionType);
@@ -6975,9 +7106,7 @@ public class RemoteViews implements Parcelable, Filter {

        private void readFromParcel(Parcel parcel) {
            mPendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel);
            if (mPendingIntent == null) {
                mFillIntent = parcel.readTypedObject(Intent.CREATOR);
            }
            mFillIntent = parcel.readBoolean() ? parcel.readTypedObject(Intent.CREATOR) : null;
            mInteractionType = parcel.readInt();
            int[] viewIds = parcel.createIntArray();
            mViewIds = viewIds == null ? null : IntArray.wrap(viewIds);
@@ -7054,7 +7183,7 @@ public class RemoteViews implements Parcelable, Filter {

        /** @hide */
        public Pair<Intent, ActivityOptions> getLaunchOptions(View view) {
            Intent intent = mPendingIntent != null ? new Intent() : new Intent(mFillIntent);
            Intent intent = mFillIntent == null ? new Intent() : new Intent(mFillIntent);
            intent.setSourceBounds(getSourceBounds(view));

            if (view instanceof CompoundButton
@@ -7412,6 +7541,98 @@ public class RemoteViews implements Parcelable, Filter {
        }
    }

    /**
     * A data parcel that carries the instructions to draw the RemoteViews, as an alternative to
     * XML layout.
     */
    @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
    public static final class DrawInstructions {

        @NonNull
        private final List<byte[]> mInstructions;

        private DrawInstructions() {
            throw new UnsupportedOperationException(
                    "DrawInstructions cannot be instantiate without instructions");
        }

        private DrawInstructions(@NonNull List<byte[]> instructions) {
            // Create and retain an immutable copy of given instructions.
            mInstructions = new ArrayList<>(instructions.size());
            for (byte[] instruction : instructions) {
                final int len = instruction.length;
                final byte[] target = new byte[len];
                System.arraycopy(instruction, 0, target, 0, len);
                mInstructions.add(target);
            }
        }

        @Nullable
        private static DrawInstructions readFromParcel(@NonNull final Parcel in) {
            int size = in.readInt();
            if (size == -1) {
                return null;
            }
            byte[] instruction;
            final List<byte[]> instructions = new ArrayList<>(size);
            for (int i = 0; i < size; i++) {
                instruction = new byte[in.readInt()];
                in.readByteArray(instruction);
                instructions.add(instruction);
            }
            return new DrawInstructions(instructions);
        }
        private static void writeToParcel(@Nullable final DrawInstructions drawInstructions,
                @NonNull final Parcel dest, final int flags) {
            if (drawInstructions == null) {
                dest.writeInt(-1);
                return;
            }
            final List<byte[]> instructions = drawInstructions.mInstructions;
            dest.writeInt(instructions.size());
            for (byte[] instruction : instructions) {
                dest.writeInt(instruction.length);
                dest.writeByteArray(instruction);
            }
        }

        /**
         * Append additional instructions to this {@link DrawInstructions} object.
         */
        @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
        public void appendInstructions(@NonNull final byte[] instructions) {
            mInstructions.add(instructions);
        }

        /**
         * Builder class for {@link DrawInstructions} objects.
         */
        @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
        public static final class Builder {

            private final List<byte[]> mInstructions;

            /**
             * Constructor.
             *
             * @param instructions Information to draw the RemoteViews.
             */
            @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
            public Builder(@NonNull final List<byte[]> instructions) {
                mInstructions = new ArrayList<>(instructions);
            }

            /**
             * Creates a {@link DrawInstructions} instance.
             */
            @NonNull
            @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
            public DrawInstructions build() {
                return new DrawInstructions(mInstructions);
            }
        }
    }

    /**
     * Get the ID of the top-level view of the XML layout, if set using
     * {@link RemoteViews#RemoteViews(String, int, int)}.
+45 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.widget;

import static android.appwidget.flags.Flags.drawDataParcel;

import static com.android.internal.R.id.pending_intent_tag;

import static org.junit.Assert.assertArrayEquals;
@@ -63,6 +65,7 @@ import org.junit.runner.RunWith;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
@@ -414,6 +417,48 @@ public class RemoteViewsTest {
        assertNotNull(view.findViewById(R.id.light_background_text));
    }

    @Test
    public void remoteCanvasCanAccessDrawInstructions() {
        if (!drawDataParcel()) {
            return;
        }
        final RemoteViews.DrawInstructions drawInstructions = getDrawInstructions();
        final RemoteViews rv = new RemoteViews(drawInstructions);
        final View view = rv.apply(mContext, mContainer);
        assertTrue(view instanceof RemoteCanvas);
        assertEquals(drawInstructions, view.getTag());
    }

    @Test
    public void remoteCanvasWiresClickHandlers() {
        if (!drawDataParcel()) {
            return;
        }
        final RemoteViews.DrawInstructions drawInstructions = getDrawInstructions();
        final RemoteViews rv = new RemoteViews(drawInstructions);
        final PendingIntent pi = PendingIntent.getActivity(mContext, 0,
                new Intent(Intent.ACTION_VIEW), PendingIntent.FLAG_IMMUTABLE);
        final Intent i = new Intent().putExtra("TEST", "Success");
        final int viewId = 1;
        rv.setPendingIntentTemplate(viewId, pi);
        rv.setOnClickFillInIntent(viewId, i);
        final View view = rv.apply(mContext, mContainer);
        assertTrue(view instanceof RemoteCanvas);
        RemoteCanvas target = (RemoteCanvas) view;
        assertEquals(1, target.getCallbacks().size());
        assertNotNull(target.getCallbacks().get(viewId));
    }

    private RemoteViews.DrawInstructions getDrawInstructions() {
        final byte[] first = new byte[] {'f', 'i', 'r', 's', 't'};
        final byte[] second = new byte[] {'s', 'e', 'c', 'o', 'n', 'd'};
        final RemoteViews.DrawInstructions drawInstructions =
                new RemoteViews.DrawInstructions.Builder(
                        Collections.singletonList(first)).build();
        drawInstructions.appendInstructions(second);
        return drawInstructions;
    }

    private RemoteViews createViewChained(int depth, String... texts) {
        RemoteViews result = new RemoteViews(mPackage, R.layout.remote_view_host);