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

Commit f1d3b37b authored by Stevie Kideckel's avatar Stevie Kideckel Committed by Automerger Merge Worker
Browse files

Merge "Add API for setting a fixed list of RemoteViews on an AdapterView" into...

Merge "Add API for setting a fixed list of RemoteViews on an AdapterView" into sc-dev am: d73b4b67

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/13828553

MUST ONLY BE SUBMITTED BY AUTOMERGER

Change-Id: I13a7c86128da6dfe45b25a4678edaf440f646aec
parents dec80520 d73b4b67
Loading
Loading
Loading
Loading
+20 −0
Original line number Diff line number Diff line
@@ -55030,6 +55030,7 @@ package android.widget {
    method public void setRelativeScrollPosition(@IdRes int, int);
    method @Deprecated public void setRemoteAdapter(int, @IdRes int, android.content.Intent);
    method public void setRemoteAdapter(@IdRes int, android.content.Intent);
    method public void setRemoteAdapter(@IdRes int, @NonNull android.widget.RemoteViews.RemoteCollectionItems);
    method public void setScrollPosition(@IdRes int, int);
    method public void setShort(@IdRes int, String, short);
    method public void setString(@IdRes int, String, String);
@@ -55069,6 +55070,25 @@ package android.widget {
    ctor public RemoteViews.ActionException(String);
  }
  public static final class RemoteViews.RemoteCollectionItems implements android.os.Parcelable {
    method public int describeContents();
    method public int getItemCount();
    method public long getItemId(int);
    method @NonNull public android.widget.RemoteViews getItemView(int);
    method public int getViewTypeCount();
    method public boolean hasStableIds();
    method public void writeToParcel(@NonNull android.os.Parcel, int);
    field @NonNull public static final android.os.Parcelable.Creator<android.widget.RemoteViews.RemoteCollectionItems> CREATOR;
  }
  public static final class RemoteViews.RemoteCollectionItems.Builder {
    ctor public RemoteViews.RemoteCollectionItems.Builder();
    method @NonNull public android.widget.RemoteViews.RemoteCollectionItems.Builder addItem(long, @NonNull android.widget.RemoteViews);
    method @NonNull public android.widget.RemoteViews.RemoteCollectionItems build();
    method @NonNull public android.widget.RemoteViews.RemoteCollectionItems.Builder setHasStableIds(boolean);
    method @NonNull public android.widget.RemoteViews.RemoteCollectionItems.Builder setViewTypeCount(int);
  }
  public static class RemoteViews.RemoteResponse {
    ctor public RemoteViews.RemoteResponse();
    method @NonNull public android.widget.RemoteViews.RemoteResponse addSharedElement(@IdRes int, @NonNull String);
+217 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.annotation.NonNull;
import android.annotation.Nullable;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RemoteViews.ColorResources;
import android.widget.RemoteViews.InteractionHandler;
import android.widget.RemoteViews.RemoteCollectionItems;

import com.android.internal.R;

import java.util.stream.IntStream;

/**
 * List {@link Adapter} backed by a {@link RemoteCollectionItems}.
 *
 * @hide
 */
class RemoteCollectionItemsAdapter extends BaseAdapter {

    private final int mViewTypeCount;

    private RemoteCollectionItems mItems;
    private InteractionHandler mInteractionHandler;
    private ColorResources mColorResources;

    private SparseIntArray mLayoutIdToViewType;

    RemoteCollectionItemsAdapter(
            @NonNull RemoteCollectionItems items,
            @NonNull InteractionHandler interactionHandler,
            @NonNull ColorResources colorResources) {
        // View type count can never increase after an adapter has been set on a ListView.
        // Additionally, decreasing it could inhibit view recycling if the count were to back and
        // forth between 3-2-3-2 for example. Therefore, the view type count, should be fixed for
        // the lifetime of the adapter.
        mViewTypeCount = items.getViewTypeCount();

        mItems = items;
        mInteractionHandler = interactionHandler;
        mColorResources = colorResources;

        initLayoutIdToViewType();
    }

    /**
     * Updates the data for the adapter, allowing recycling of views. Note that if the view type
     * count has increased, a new adapter should be created and set on the AdapterView instead of
     * calling this method.
     */
    void setData(
            @NonNull RemoteCollectionItems items,
            @NonNull InteractionHandler interactionHandler,
            @NonNull ColorResources colorResources) {
        if (mViewTypeCount < items.getViewTypeCount()) {
            throw new IllegalArgumentException(
                    "RemoteCollectionItemsAdapter cannot increase view type count after creation");
        }

        mItems = items;
        mInteractionHandler = interactionHandler;
        mColorResources = colorResources;

        initLayoutIdToViewType();

        notifyDataSetChanged();
    }

    private void initLayoutIdToViewType() {
        SparseIntArray previousLayoutIdToViewType = mLayoutIdToViewType;
        mLayoutIdToViewType = new SparseIntArray(mViewTypeCount);

        int[] layoutIds = IntStream.range(0, mItems.getItemCount())
                .map(position -> mItems.getItemView(position).getLayoutId())
                .distinct()
                .toArray();
        if (layoutIds.length > mViewTypeCount) {
            throw new IllegalArgumentException(
                    "Collection items uses " + layoutIds.length + " distinct layouts, which is "
                            + "more than view type count of " + mViewTypeCount);
        }

        // Tracks whether a layout id (by index, not value) has been assigned a view type.
        boolean[] processedLayoutIdIndices = new boolean[layoutIds.length];
        // Tracks whether a view type has been assigned to a layout id already.
        boolean[] assignedViewTypes = new boolean[mViewTypeCount];

        if (previousLayoutIdToViewType != null) {
            for (int i = 0; i < layoutIds.length; i++) {
                int layoutId = layoutIds[i];
                // Copy over any previously used view types for layout ids in the collection to keep
                // view types stable across data updates.
                int previousViewType = previousLayoutIdToViewType.get(layoutId, -1);
                // Skip this layout id if it wasn't assigned to a view type previously.
                if (previousViewType < 0) continue;

                mLayoutIdToViewType.put(layoutId, previousViewType);
                processedLayoutIdIndices[i] = true;
                assignedViewTypes[previousViewType] = true;
            }
        }

        int lastViewType = -1;
        for (int i = 0; i < layoutIds.length; i++) {
            // If a view type has already been assigned to the layout id, skip it.
            if (processedLayoutIdIndices[i]) continue;

            int layoutId = layoutIds[i];
            // If no view type is assigned for the layout id, choose the next possible value that
            // isn't already assigned to a layout id. There is guaranteed to be some value available
            // due to the prior validation logic that count(distinct layout ids) <= viewTypeCount.
            int viewType = IntStream.range(lastViewType + 1, layoutIds.length)
                    .filter(type -> !assignedViewTypes[type])
                    .findFirst()
                    .orElseThrow(
                            () -> new IllegalStateException(
                                    "RemoteCollectionItems has more distinct layout ids than its "
                                            + "view type count"));
            mLayoutIdToViewType.put(layoutId, viewType);
            processedLayoutIdIndices[i] = true;
            assignedViewTypes[viewType] = true;
            lastViewType = viewType;
        }
    }

    @Override
    public int getCount() {
        return mItems.getItemCount();
    }

    @Override
    public RemoteViews getItem(int position) {
        return mItems.getItemView(position);
    }

    @Override
    public long getItemId(int position) {
        return mItems.getItemId(position);
    }

    @Override
    public int getItemViewType(int position) {
        return mLayoutIdToViewType.get(mItems.getItemView(position).getLayoutId());
    }

    @Override
    public int getViewTypeCount() {
        return mViewTypeCount;
    }

    @Override
    public boolean hasStableIds() {
        return mItems.hasStableIds();
    }

    @Nullable
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        if (position >= getCount()) return null;

        RemoteViews item = mItems.getItemView(position);
        item.addFlags(RemoteViews.FLAG_WIDGET_IS_COLLECTION_CHILD);
        View reapplyView = getViewToReapply(convertView, item);

        // Reapply the RemoteViews if we can.
        if (reapplyView != null) {
            try {
                item.reapply(
                        parent.getContext(),
                        reapplyView,
                        mInteractionHandler,
                        null /* size */,
                        mColorResources);
                return reapplyView;
            } catch (RuntimeException e) {
                // We can't reapply for some reason, we'll fallback to an apply and inflate a
                // new view.
            }
        }

        return item.apply(
                parent.getContext(),
                parent,
                mInteractionHandler,
                null /* size */,
                mColorResources);
    }

    /** Returns {@code convertView} if it can be used to reapply {@code item}, or null otherwise. */
    @Nullable
    private static View getViewToReapply(@Nullable View convertView, @NonNull RemoteViews item) {
        if (convertView == null) return null;

        Object layoutIdTag = convertView.getTag(R.id.widget_frame);
        if (!(layoutIdTag instanceof Integer)) return null;

        return item.getLayoutId() == (Integer) layoutIdTag ? convertView : null;
    }
}
+288 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import android.annotation.Nullable;
import android.annotation.Px;
import android.annotation.StringRes;
import android.annotation.StyleRes;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.ActivityThread;
@@ -74,6 +75,7 @@ import android.util.ArrayMap;
import android.util.DisplayMetrics;
import android.util.IntArray;
import android.util.Log;
import android.util.LongArray;
import android.util.Pair;
import android.util.SizeF;
import android.util.SparseIntArray;
@@ -112,6 +114,7 @@ import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -224,6 +227,7 @@ public class RemoteViews implements Parcelable, Filter {
    private static final int SET_VIEW_OUTLINE_RADIUS_TAG = 28;
    private static final int SET_ON_CHECKED_CHANGE_RESPONSE_TAG = 29;
    private static final int NIGHT_MODE_REFLECTION_ACTION_TAG = 30;
    private static final int SET_REMOTE_COLLECTION_ITEMS_ADAPTER_TAG = 31;

    /** @hide **/
    @IntDef(prefix = "MARGIN_", value = {
@@ -899,6 +903,72 @@ public class RemoteViews implements Parcelable, Filter {
        ArrayList<RemoteViews> list;
    }

    private static class SetRemoteCollectionItemListAdapterAction extends Action {
        private final RemoteCollectionItems mItems;

        SetRemoteCollectionItemListAdapterAction(@IdRes int id, RemoteCollectionItems items) {
            viewId = id;
            mItems = items;
        }

        SetRemoteCollectionItemListAdapterAction(Parcel parcel) {
            viewId = parcel.readInt();
            mItems = parcel.readTypedObject(RemoteCollectionItems.CREATOR);
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(viewId);
            dest.writeTypedObject(mItems, flags);
        }

        @Override
        public void apply(View root, ViewGroup rootParent, InteractionHandler handler,
                ColorResources colorResources) throws ActionException {
            View target = root.findViewById(viewId);
            if (target == null) return;

            if (!(target instanceof AdapterView)) {
                Log.e(LOG_TAG, "Cannot call setRemoteAdapter on a view which is not "
                        + "an AdapterView (id: " + viewId + ")");
                return;
            }

            AdapterView adapterView = (AdapterView) target;
            Adapter adapter = adapterView.getAdapter();
            // We can reuse the adapter if it's a RemoteCollectionItemsAdapter and the view type
            // count hasn't increased. Note that AbsListView allocates a fixed size array for view
            // recycling in setAdapter, so we must call setAdapter again if the number of view types
            // increases.
            if (adapter instanceof RemoteCollectionItemsAdapter
                    && adapter.getViewTypeCount() >= mItems.getViewTypeCount()) {
                try {
                    ((RemoteCollectionItemsAdapter) adapter).setData(
                            mItems, handler, colorResources);
                } catch (Throwable throwable) {
                    // setData should never failed with the validation in the items builder, but if
                    // it does, catch and rethrow.
                    throw new ActionException(throwable);
                }
                return;
            }

            try {
                adapterView.setAdapter(
                        new RemoteCollectionItemsAdapter(mItems, handler, colorResources));
            } catch (Throwable throwable) {
                // This could throw if the AdapterView somehow doesn't accept BaseAdapter due to
                // a type error.
                throw new ActionException(throwable);
            }
        }

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

    private class SetRemoteViewsAdapterIntent extends Action {
        public SetRemoteViewsAdapterIntent(@IdRes int id, Intent intent) {
            this.viewId = id;
@@ -3543,6 +3613,8 @@ public class RemoteViews implements Parcelable, Filter {
                return new SetOnCheckedChangeResponse(parcel);
            case NIGHT_MODE_REFLECTION_ACTION_TAG:
                return new NightModeReflectionAction(parcel);
            case SET_REMOTE_COLLECTION_ITEMS_ADAPTER_TAG:
                return new SetRemoteCollectionItemListAdapterAction(parcel);
            default:
                throw new ActionException("Tag " + tag + " not found");
        }
@@ -4214,6 +4286,25 @@ public class RemoteViews implements Parcelable, Filter {
        addAction(new SetRemoteViewsAdapterList(viewId, list, viewTypeCount));
    }

    /**
     * Creates a simple Adapter for the viewId specified. The viewId must point to an AdapterView,
     * ie. {@link ListView}, {@link GridView}, {@link StackView} or {@link AdapterViewAnimator}.
     * This is a simpler but less flexible approach to populating collection widgets. Its use is
     * encouraged for most scenarios, as long as the total memory within the list of RemoteViews
     * is relatively small (ie. doesn't contain large or numerous Bitmaps, see {@link
     * RemoteViews#setImageViewBitmap}). In the case of numerous images, the use of API is still
     * possible by setting image URIs instead of Bitmaps, see {@link RemoteViews#setImageViewUri}.
     *
     * This API is supported in the compatibility library for previous API levels, see
     * RemoteViewsCompat.
     *
     * @param viewId The id of the {@link AdapterView}.
     * @param items The items to display in the {@link AdapterView}.
     */
    public void setRemoteAdapter(@IdRes int viewId, @NonNull RemoteCollectionItems items) {
        addAction(new SetRemoteCollectionItemListAdapterAction(viewId, items));
    }

    /**
     * Equivalent to calling {@link ListView#smoothScrollToPosition(int)}.
     *
@@ -6026,6 +6117,203 @@ public class RemoteViews implements Parcelable, Filter {
        return true;
    }

    /** Representation of a fixed list of items to be displayed in a RemoteViews collection. */
    public static final class RemoteCollectionItems implements Parcelable {
        private final long[] mIds;
        private final RemoteViews[] mViews;
        private final boolean mHasStableIds;
        private final int mViewTypeCount;

        RemoteCollectionItems(
                long[] ids, RemoteViews[] views, boolean hasStableIds, int viewTypeCount) {
            mIds = ids;
            mViews = views;
            mHasStableIds = hasStableIds;
            mViewTypeCount = viewTypeCount;
            if (ids.length != views.length) {
                throw new IllegalArgumentException(
                        "RemoteCollectionItems has different number of ids and views");
            }
            if (viewTypeCount < 1) {
                throw new IllegalArgumentException("View type count must be >= 1");
            }
            int layoutIdCount = (int) Arrays.stream(views)
                    .mapToInt(RemoteViews::getLayoutId)
                    .distinct()
                    .count();
            if (layoutIdCount > viewTypeCount) {
                throw new IllegalArgumentException(
                        "View type count is set to " + viewTypeCount + ", but the collection "
                                + "contains " + layoutIdCount + " different layout ids");
            }
        }

        RemoteCollectionItems(Parcel in) {
            int length = in.readInt();
            mIds = new long[length];
            in.readLongArray(mIds);
            mViews = new RemoteViews[length];
            in.readTypedArray(mViews, RemoteViews.CREATOR);
            mHasStableIds = in.readBoolean();
            mViewTypeCount = in.readInt();
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            dest.writeInt(mIds.length);
            dest.writeLongArray(mIds);
            dest.writeTypedArray(mViews, flags);
            dest.writeBoolean(mHasStableIds);
            dest.writeInt(mViewTypeCount);
        }

        /**
         * Returns the id for {@code position}. See {@link #hasStableIds()} for whether this id
         * should be considered meaningful across collection updates.
         *
         * @return Id for the position.
         */
        public long getItemId(int position) {
            return mIds[position];
        }

        /**
         * Returns the {@link RemoteViews} to display at {@code position}.
         *
         * @return RemoteViews for the position.
         */
        @NonNull
        public RemoteViews getItemView(int position) {
            return mViews[position];
        }

        /**
         * Returns the number of elements in the collection.
         *
         * @return Count of items.
         */
        public int getItemCount() {
            return mIds.length;
        }

        /**
         * Returns the view type count for the collection when used in an adapter
         *
         * @return Count of view types for the collection when used in an adapter.
         * @see android.widget.Adapter#getViewTypeCount()
         */
        public int getViewTypeCount() {
            return mViewTypeCount;
        }

        /**
         * Indicates whether the item ids are stable across changes to the underlying data.
         *
         * @return True if the same id always refers to the same object.
         * @see android.widget.Adapter#hasStableIds()
         */
        public boolean hasStableIds() {
            return mHasStableIds;
        }

        @NonNull
        public static final Creator<RemoteCollectionItems> CREATOR =
                new Creator<RemoteCollectionItems>() {
            @NonNull
            @Override
            public RemoteCollectionItems createFromParcel(@NonNull Parcel source) {
                return new RemoteCollectionItems(source);
            }

            @NonNull
            @Override
            public RemoteCollectionItems[] newArray(int size) {
                return new RemoteCollectionItems[size];
            }
        };

        /** Builder class for {@link RemoteCollectionItems} objects.*/
        public static final class Builder {
            private final LongArray mIds = new LongArray();
            private final List<RemoteViews> mViews = new ArrayList<>();
            private boolean mHasStableIds;
            private int mViewTypeCount;

            /**
             * Adds a {@link RemoteViews} to the collection.
             *
             * @param id Id to associate with the row. Use {@link #setHasStableIds(boolean)} to
             *           indicate that ids are stable across changes to the collection.
             * @param view RemoteViews to display for the row.
             */
            @NonNull
            // Covered by getItemId, getItemView, getItemCount.
            @SuppressLint("MissingGetterMatchingBuilder")
            public Builder addItem(long id, @NonNull RemoteViews view) {
                if (view == null) throw new NullPointerException();
                if (view.hasMultipleLayouts()) {
                    throw new IllegalArgumentException(
                            "RemoteViews used in a RemoteCollectionItems cannot specify separate "
                                    + "layouts for orientations or sizes.");
                }
                mIds.add(id);
                mViews.add(view);
                return this;
            }

            /**
             * Sets whether the item ids are stable across changes to the underlying data.
             *
             * @see android.widget.Adapter#hasStableIds()
             */
            @NonNull
            public Builder setHasStableIds(boolean hasStableIds) {
                mHasStableIds = hasStableIds;
                return this;
            }

            /**
             * Sets the view type count for the collection when used in an adapter. This can be set
             * to the maximum number of different layout ids that will be used by RemoteViews in
             * this collection.
             *
             * If this value is not set, then a value will be inferred from the provided items. As
             * a result, the adapter may need to be recreated when the list is updated with
             * previously unseen RemoteViews layouts for new items.
             *
             * @see android.widget.Adapter#getViewTypeCount()
             */
            @NonNull
            public Builder setViewTypeCount(int viewTypeCount) {
                mViewTypeCount = viewTypeCount;
                return this;
            }

            /** Creates the {@link RemoteCollectionItems} defined by this builder. */
            @NonNull
            public RemoteCollectionItems build() {
                if (mViewTypeCount < 1) {
                    // If a view type count wasn't specified, set it to be the number of distinct
                    // layout ids used in the items.
                    mViewTypeCount = (int) mViews.stream()
                            .mapToInt(RemoteViews::getLayoutId)
                            .distinct()
                            .count();
                }
                return new RemoteCollectionItems(
                        mIds.toArray(),
                        mViews.toArray(new RemoteViews[0]),
                        mHasStableIds,
                        Math.max(mViewTypeCount, 1));
            }
        }
    }

    /**
     * Set the ID of the top-level view of the XML layout.
     *