1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.widget; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.util.SparseIntArray; 22 import android.view.View; 23 import android.view.ViewGroup; 24 import android.widget.RemoteViews.ColorResources; 25 import android.widget.RemoteViews.InteractionHandler; 26 import android.widget.RemoteViews.RemoteCollectionItems; 27 28 import com.android.internal.R; 29 30 import java.util.stream.IntStream; 31 32 /** 33 * List {@link Adapter} backed by a {@link RemoteCollectionItems}. 34 * 35 * @hide 36 */ 37 class RemoteCollectionItemsAdapter extends BaseAdapter { 38 39 private final int mViewTypeCount; 40 41 private RemoteCollectionItems mItems; 42 private InteractionHandler mInteractionHandler; 43 private ColorResources mColorResources; 44 45 private SparseIntArray mLayoutIdToViewType; 46 RemoteCollectionItemsAdapter( @onNull RemoteCollectionItems items, @NonNull InteractionHandler interactionHandler, @NonNull ColorResources colorResources)47 RemoteCollectionItemsAdapter( 48 @NonNull RemoteCollectionItems items, 49 @NonNull InteractionHandler interactionHandler, 50 @NonNull ColorResources colorResources) { 51 // View type count can never increase after an adapter has been set on a ListView. 52 // Additionally, decreasing it could inhibit view recycling if the count were to back and 53 // forth between 3-2-3-2 for example. Therefore, the view type count, should be fixed for 54 // the lifetime of the adapter. 55 mViewTypeCount = items.getViewTypeCount(); 56 57 mItems = items; 58 mInteractionHandler = interactionHandler; 59 mColorResources = colorResources; 60 61 initLayoutIdToViewType(); 62 } 63 64 /** 65 * Updates the data for the adapter, allowing recycling of views. Note that if the view type 66 * count has increased, a new adapter should be created and set on the AdapterView instead of 67 * calling this method. 68 */ setData( @onNull RemoteCollectionItems items, @NonNull InteractionHandler interactionHandler, @NonNull ColorResources colorResources)69 void setData( 70 @NonNull RemoteCollectionItems items, 71 @NonNull InteractionHandler interactionHandler, 72 @NonNull ColorResources colorResources) { 73 if (mViewTypeCount < items.getViewTypeCount()) { 74 throw new IllegalArgumentException( 75 "RemoteCollectionItemsAdapter cannot increase view type count after creation"); 76 } 77 78 mItems = items; 79 mInteractionHandler = interactionHandler; 80 mColorResources = colorResources; 81 82 initLayoutIdToViewType(); 83 84 notifyDataSetChanged(); 85 } 86 initLayoutIdToViewType()87 private void initLayoutIdToViewType() { 88 SparseIntArray previousLayoutIdToViewType = mLayoutIdToViewType; 89 mLayoutIdToViewType = new SparseIntArray(mViewTypeCount); 90 91 int[] layoutIds = IntStream.range(0, mItems.getItemCount()) 92 .map(position -> mItems.getItemView(position).getLayoutId()) 93 .distinct() 94 .toArray(); 95 if (layoutIds.length > mViewTypeCount) { 96 throw new IllegalArgumentException( 97 "Collection items uses " + layoutIds.length + " distinct layouts, which is " 98 + "more than view type count of " + mViewTypeCount); 99 } 100 101 // Tracks whether a layout id (by index, not value) has been assigned a view type. 102 boolean[] processedLayoutIdIndices = new boolean[layoutIds.length]; 103 // Tracks whether a view type has been assigned to a layout id already. 104 boolean[] assignedViewTypes = new boolean[mViewTypeCount]; 105 106 if (previousLayoutIdToViewType != null) { 107 for (int i = 0; i < layoutIds.length; i++) { 108 int layoutId = layoutIds[i]; 109 // Copy over any previously used view types for layout ids in the collection to keep 110 // view types stable across data updates. 111 int previousViewType = previousLayoutIdToViewType.get(layoutId, -1); 112 // Skip this layout id if it wasn't assigned to a view type previously. 113 if (previousViewType < 0) continue; 114 115 mLayoutIdToViewType.put(layoutId, previousViewType); 116 processedLayoutIdIndices[i] = true; 117 assignedViewTypes[previousViewType] = true; 118 } 119 } 120 121 int lastViewType = -1; 122 for (int i = 0; i < layoutIds.length; i++) { 123 // If a view type has already been assigned to the layout id, skip it. 124 if (processedLayoutIdIndices[i]) continue; 125 126 int layoutId = layoutIds[i]; 127 // If no view type is assigned for the layout id, choose the next possible value that 128 // isn't already assigned to a layout id. There is guaranteed to be some value available 129 // due to the prior validation logic that count(distinct layout ids) <= viewTypeCount. 130 int viewType = IntStream.range(lastViewType + 1, layoutIds.length) 131 .filter(type -> !assignedViewTypes[type]) 132 .findFirst() 133 .orElseThrow( 134 () -> new IllegalStateException( 135 "RemoteCollectionItems has more distinct layout ids than its " 136 + "view type count")); 137 mLayoutIdToViewType.put(layoutId, viewType); 138 processedLayoutIdIndices[i] = true; 139 assignedViewTypes[viewType] = true; 140 lastViewType = viewType; 141 } 142 } 143 144 @Override getCount()145 public int getCount() { 146 return mItems.getItemCount(); 147 } 148 149 @Override getItem(int position)150 public RemoteViews getItem(int position) { 151 return mItems.getItemView(position); 152 } 153 154 @Override getItemId(int position)155 public long getItemId(int position) { 156 return mItems.getItemId(position); 157 } 158 159 @Override getItemViewType(int position)160 public int getItemViewType(int position) { 161 return mLayoutIdToViewType.get(mItems.getItemView(position).getLayoutId()); 162 } 163 164 @Override getViewTypeCount()165 public int getViewTypeCount() { 166 return mViewTypeCount; 167 } 168 169 @Override hasStableIds()170 public boolean hasStableIds() { 171 return mItems.hasStableIds(); 172 } 173 174 @Nullable 175 @Override getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)176 public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { 177 if (position >= getCount()) return null; 178 179 RemoteViews item = mItems.getItemView(position); 180 item.addFlags(RemoteViews.FLAG_WIDGET_IS_COLLECTION_CHILD); 181 View reapplyView = getViewToReapply(convertView, item); 182 183 // Reapply the RemoteViews if we can. 184 if (reapplyView != null) { 185 try { 186 item.reapply( 187 parent.getContext(), 188 reapplyView, 189 mInteractionHandler, 190 null /* size */, 191 mColorResources); 192 return reapplyView; 193 } catch (RuntimeException e) { 194 // We can't reapply for some reason, we'll fallback to an apply and inflate a 195 // new view. 196 } 197 } 198 199 return item.apply( 200 parent.getContext(), 201 parent, 202 mInteractionHandler, 203 null /* size */, 204 mColorResources); 205 } 206 207 /** Returns {@code convertView} if it can be used to reapply {@code item}, or null otherwise. */ 208 @Nullable getViewToReapply(@ullable View convertView, @NonNull RemoteViews item)209 private static View getViewToReapply(@Nullable View convertView, @NonNull RemoteViews item) { 210 if (convertView == null) return null; 211 212 Object layoutIdTag = convertView.getTag(R.id.widget_frame); 213 if (!(layoutIdTag instanceof Integer)) return null; 214 215 return item.getLayoutId() == (Integer) layoutIdTag ? convertView : null; 216 } 217 } 218