Add API for setting a fixed list of RemoteViews on an AdapterView

Bug: 182467111
Test: Sample app trying out different combinations of views and event
listeners
Test: atest RemoteViewsFixedCollectionAdapterTest

Change-Id: Ia3fc0af0499cbfaa85e9eb1bf2a24f1e404f17a3
This commit is contained in:
Stevie Kideckel
2021-03-10 08:02:36 +00:00
parent 7af62a210b
commit 65a7c2c56b
3 changed files with 525 additions and 0 deletions

View File

@@ -55028,6 +55028,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);
@@ -55067,6 +55068,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);

View File

@@ -0,0 +1,217 @@
/*
* 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;
}
}

View File

@@ -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.
*