1/* 2 * Copyright 2017 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 17package androidx.slice.widget; 18 19import static android.app.slice.Slice.HINT_HORIZONTAL; 20import static android.app.slice.Slice.HINT_SUMMARY; 21import static android.app.slice.Slice.SUBTYPE_MESSAGE; 22import static android.app.slice.Slice.SUBTYPE_SOURCE; 23import static android.app.slice.SliceItem.FORMAT_INT; 24import static android.app.slice.SliceItem.FORMAT_TEXT; 25 26import static androidx.slice.widget.SliceView.MODE_LARGE; 27 28import android.app.slice.Slice; 29import android.content.Context; 30import android.util.AttributeSet; 31import android.view.LayoutInflater; 32import android.view.MotionEvent; 33import android.view.View; 34import android.view.ViewGroup; 35import android.view.ViewGroup.LayoutParams; 36 37import androidx.annotation.RestrictTo; 38import androidx.collection.ArrayMap; 39import androidx.recyclerview.widget.RecyclerView; 40import androidx.slice.SliceItem; 41import androidx.slice.core.SliceQuery; 42import androidx.slice.view.R; 43 44import java.util.ArrayList; 45import java.util.Iterator; 46import java.util.List; 47 48/** 49 * @hide 50 */ 51@RestrictTo(RestrictTo.Scope.LIBRARY) 52public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.SliceViewHolder> { 53 54 static final int TYPE_DEFAULT = 1; 55 static final int TYPE_HEADER = 2; // TODO: headers shouldn't scroll off 56 static final int TYPE_GRID = 3; 57 static final int TYPE_MESSAGE = 4; 58 static final int TYPE_MESSAGE_LOCAL = 5; 59 60 static final int HEADER_INDEX = 0; 61 62 private final IdGenerator mIdGen = new IdGenerator(); 63 private final Context mContext; 64 private List<SliceWrapper> mSlices = new ArrayList<>(); 65 private SliceView.OnSliceActionListener mSliceObserver; 66 private int mColor; 67 private AttributeSet mAttrs; 68 private int mDefStyleAttr; 69 private int mDefStyleRes; 70 private List<SliceItem> mSliceActions; 71 private boolean mShowLastUpdated; 72 private long mLastUpdated; 73 private SliceView mParent; 74 private LargeTemplateView mTemplateView; 75 76 public LargeSliceAdapter(Context context) { 77 mContext = context; 78 setHasStableIds(true); 79 } 80 81 /** 82 * Sets the SliceView parent and the template parent. 83 */ 84 public void setParents(SliceView parent, LargeTemplateView templateView) { 85 mParent = parent; 86 mTemplateView = templateView; 87 } 88 89 /** 90 * Sets the observer to pass down to child views. 91 */ 92 public void setSliceObserver(SliceView.OnSliceActionListener observer) { 93 mSliceObserver = observer; 94 } 95 96 /** 97 * Sets the actions to display for this slice, this adjusts what's displayed in the header item. 98 */ 99 public void setSliceActions(List<SliceItem> actions) { 100 mSliceActions = actions; 101 notifyHeaderChanged(); 102 } 103 104 /** 105 * Set the {@link SliceItem}'s to be displayed in the adapter and the accent color. 106 */ 107 public void setSliceItems(List<SliceItem> slices, int color, int mode) { 108 if (slices == null) { 109 mSlices.clear(); 110 } else { 111 mIdGen.resetUsage(); 112 mSlices = new ArrayList<>(slices.size()); 113 for (SliceItem s : slices) { 114 mSlices.add(new SliceWrapper(s, mIdGen, mode)); 115 } 116 } 117 mColor = color; 118 notifyDataSetChanged(); 119 } 120 121 /** 122 * Sets the attribute set to use for views in the list. 123 */ 124 public void setStyle(AttributeSet attrs, int defStyleAttr, int defStyleRes) { 125 mAttrs = attrs; 126 mDefStyleAttr = defStyleAttr; 127 mDefStyleRes = defStyleRes; 128 notifyDataSetChanged(); 129 } 130 131 /** 132 * Sets whether the last updated time should be shown on the slice. 133 */ 134 public void setShowLastUpdated(boolean showLastUpdated) { 135 mShowLastUpdated = showLastUpdated; 136 notifyHeaderChanged(); 137 } 138 139 /** 140 * Sets when the slice was last updated. 141 */ 142 public void setLastUpdated(long lastUpdated) { 143 mLastUpdated = lastUpdated; 144 notifyHeaderChanged(); 145 } 146 147 @Override 148 public SliceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 149 View v = inflateForType(viewType); 150 v.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); 151 return new SliceViewHolder(v); 152 } 153 154 @Override 155 public int getItemViewType(int position) { 156 return mSlices.get(position).mType; 157 } 158 159 @Override 160 public long getItemId(int position) { 161 return mSlices.get(position).mId; 162 } 163 164 @Override 165 public int getItemCount() { 166 return mSlices.size(); 167 } 168 169 @Override 170 public void onBindViewHolder(SliceViewHolder holder, int position) { 171 SliceWrapper slice = mSlices.get(position); 172 holder.bind(slice.mItem, position); 173 } 174 175 private void notifyHeaderChanged() { 176 if (getItemCount() > 0) { 177 notifyItemChanged(HEADER_INDEX); 178 } 179 } 180 181 private View inflateForType(int viewType) { 182 View v = new RowView(mContext); 183 switch (viewType) { 184 case TYPE_GRID: 185 v = LayoutInflater.from(mContext).inflate(R.layout.abc_slice_grid, null); 186 break; 187 case TYPE_MESSAGE: 188 v = LayoutInflater.from(mContext).inflate(R.layout.abc_slice_message, null); 189 break; 190 case TYPE_MESSAGE_LOCAL: 191 v = LayoutInflater.from(mContext).inflate(R.layout.abc_slice_message_local, 192 null); 193 break; 194 } 195 return v; 196 } 197 198 protected static class SliceWrapper { 199 private final SliceItem mItem; 200 private final int mType; 201 private final long mId; 202 203 public SliceWrapper(SliceItem item, IdGenerator idGen, int mode) { 204 mItem = item; 205 mType = getFormat(item); 206 mId = idGen.getId(item, mode); 207 } 208 209 public static int getFormat(SliceItem item) { 210 if (SUBTYPE_MESSAGE.equals(item.getSubType())) { 211 // TODO: Better way to determine me or not? Something more like Messaging style. 212 if (SliceQuery.findSubtype(item, null, SUBTYPE_SOURCE) != null) { 213 return TYPE_MESSAGE; 214 } else { 215 return TYPE_MESSAGE_LOCAL; 216 } 217 } 218 if (item.hasHint(HINT_HORIZONTAL)) { 219 return TYPE_GRID; 220 } 221 if (!item.hasHint(Slice.HINT_LIST_ITEM)) { 222 return TYPE_HEADER; 223 } 224 return TYPE_DEFAULT; 225 } 226 } 227 228 /** 229 * A {@link RecyclerView.ViewHolder} for presenting slices in {@link LargeSliceAdapter}. 230 */ 231 public class SliceViewHolder extends RecyclerView.ViewHolder implements View.OnTouchListener, 232 View.OnClickListener { 233 public final SliceChildView mSliceChildView; 234 235 public SliceViewHolder(View itemView) { 236 super(itemView); 237 mSliceChildView = itemView instanceof SliceChildView ? (SliceChildView) itemView : null; 238 } 239 240 void bind(SliceItem item, int position) { 241 if (mSliceChildView == null || item == null) { 242 return; 243 } 244 // Click listener used to pipe click events to parent 245 mSliceChildView.setOnClickListener(this); 246 // Touch listener used to pipe events to touch feedback drawable 247 mSliceChildView.setOnTouchListener(this); 248 249 final boolean isHeader = position == HEADER_INDEX; 250 int mode = mParent != null ? mParent.getMode() : MODE_LARGE; 251 mSliceChildView.setMode(mode); 252 mSliceChildView.setTint(mColor); 253 mSliceChildView.setStyle(mAttrs, mDefStyleAttr, mDefStyleRes); 254 mSliceChildView.setSliceItem(item, isHeader, position, getItemCount(), mSliceObserver); 255 mSliceChildView.setSliceActions(isHeader ? mSliceActions : null); 256 mSliceChildView.setLastUpdated(isHeader ? mLastUpdated : -1); 257 mSliceChildView.setShowLastUpdated(isHeader && mShowLastUpdated); 258 if (mSliceChildView instanceof RowView) { 259 ((RowView) mSliceChildView).setSingleItem(getItemCount() == 1); 260 } 261 int[] info = new int[2]; 262 info[0] = ListContent.getRowType(mContext, item, isHeader, mSliceActions); 263 info[1] = position; 264 mSliceChildView.setTag(info); 265 } 266 267 @Override 268 public void onClick(View v) { 269 if (mParent != null) { 270 mParent.setClickInfo((int[]) v.getTag()); 271 mParent.performClick(); 272 } 273 } 274 275 @Override 276 public boolean onTouch(View v, MotionEvent event) { 277 if (mTemplateView != null) { 278 mTemplateView.onForegroundActivated(event); 279 } 280 return false; 281 } 282 } 283 284 private static class IdGenerator { 285 private long mNextLong = 0; 286 private final ArrayMap<String, Long> mCurrentIds = new ArrayMap<>(); 287 private final ArrayMap<String, Integer> mUsedIds = new ArrayMap<>(); 288 289 public long getId(SliceItem item, int mode) { 290 String str = genString(item); 291 SliceItem summary = SliceQuery.find(item, null, HINT_SUMMARY, null); 292 if (summary != null) { 293 str += mode; // mode matters 294 } 295 if (!mCurrentIds.containsKey(str)) { 296 mCurrentIds.put(str, mNextLong++); 297 } 298 long id = mCurrentIds.get(str); 299 Integer usedIdIndex = mUsedIds.get(str); 300 int index = usedIdIndex != null ? usedIdIndex : 0; 301 mUsedIds.put(str, index + 1); 302 return id + index * 10000; 303 } 304 305 private String genString(SliceItem item) { 306 final StringBuilder builder = new StringBuilder(); 307 Iterator<SliceItem> items = SliceQuery.stream(item); 308 while (items.hasNext()) { 309 SliceItem i = items.next(); 310 builder.append(i.getFormat()); 311 builder.append(i.getHints()); 312 switch (i.getFormat()) { 313 case FORMAT_TEXT: 314 builder.append(i.getText()); 315 break; 316 case FORMAT_INT: 317 builder.append(i.getInt()); 318 break; 319 } 320 } 321 return builder.toString(); 322 } 323 324 public void resetUsage() { 325 mUsedIds.clear(); 326 } 327 } 328} 329