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 17 package androidx.slice.widget; 18 19 import static android.app.slice.Slice.HINT_ACTIONS; 20 import static android.app.slice.Slice.HINT_HORIZONTAL; 21 import static android.app.slice.Slice.HINT_LIST_ITEM; 22 import static android.app.slice.Slice.HINT_SEE_MORE; 23 import static android.app.slice.Slice.HINT_SHORTCUT; 24 import static android.app.slice.Slice.SUBTYPE_COLOR; 25 import static android.app.slice.SliceItem.FORMAT_ACTION; 26 import static android.app.slice.SliceItem.FORMAT_INT; 27 import static android.app.slice.SliceItem.FORMAT_SLICE; 28 import static android.app.slice.SliceItem.FORMAT_TEXT; 29 30 import static androidx.slice.core.SliceHints.HINT_KEYWORDS; 31 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED; 32 import static androidx.slice.core.SliceHints.HINT_TTL; 33 import static androidx.slice.widget.SliceView.MODE_LARGE; 34 import static androidx.slice.widget.SliceView.MODE_SMALL; 35 36 import android.content.Context; 37 import android.content.res.TypedArray; 38 import android.util.AttributeSet; 39 40 import androidx.annotation.NonNull; 41 import androidx.annotation.Nullable; 42 import androidx.annotation.RestrictTo; 43 import androidx.slice.Slice; 44 import androidx.slice.SliceItem; 45 import androidx.slice.SliceMetadata; 46 import androidx.slice.core.SliceAction; 47 import androidx.slice.core.SliceActionImpl; 48 import androidx.slice.core.SliceQuery; 49 import androidx.slice.view.R; 50 51 import java.util.ArrayList; 52 import java.util.List; 53 54 /** 55 * Extracts information required to present content in a list format from a slice. 56 * @hide 57 */ 58 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 59 public class ListContent { 60 61 private Slice mSlice; 62 private SliceItem mHeaderItem; 63 private SliceItem mColorItem; 64 private SliceItem mSeeMoreItem; 65 private ArrayList<SliceItem> mRowItems = new ArrayList<>(); 66 private List<SliceItem> mSliceActions; 67 private Context mContext; 68 69 private int mHeaderTitleSize; 70 private int mHeaderSubtitleSize; 71 private int mVerticalHeaderTextPadding; 72 private int mTitleSize; 73 private int mSubtitleSize; 74 private int mVerticalTextPadding; 75 private int mGridTitleSize; 76 private int mGridSubtitleSize; 77 private int mVerticalGridTextPadding; 78 private int mGridTopPadding; 79 private int mGridBottomPadding; 80 ListContent(Context context, Slice slice, AttributeSet attrs, int defStyleAttr, int defStyleRes)81 public ListContent(Context context, Slice slice, AttributeSet attrs, int defStyleAttr, 82 int defStyleRes) { 83 mSlice = slice; 84 mContext = context; 85 86 // TODO: duplicated code from SliceChildView; could do something better 87 // Some of this information will impact the size calculations for slice content. 88 TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView, 89 defStyleAttr, defStyleRes); 90 try { 91 mHeaderTitleSize = (int) a.getDimension( 92 R.styleable.SliceView_headerTitleSize, 0); 93 mHeaderSubtitleSize = (int) a.getDimension( 94 R.styleable.SliceView_headerSubtitleSize, 0); 95 mVerticalHeaderTextPadding = (int) a.getDimension( 96 R.styleable.SliceView_headerTextVerticalPadding, 0); 97 98 mTitleSize = (int) a.getDimension(R.styleable.SliceView_titleSize, 0); 99 mSubtitleSize = (int) a.getDimension( 100 R.styleable.SliceView_subtitleSize, 0); 101 mVerticalTextPadding = (int) a.getDimension( 102 R.styleable.SliceView_textVerticalPadding, 0); 103 104 mGridTitleSize = (int) a.getDimension(R.styleable.SliceView_gridTitleSize, 0); 105 mGridSubtitleSize = (int) a.getDimension( 106 R.styleable.SliceView_gridSubtitleSize, 0); 107 int defaultVerticalGridPadding = context.getResources().getDimensionPixelSize( 108 R.dimen.abc_slice_grid_text_inner_padding); 109 mVerticalGridTextPadding = (int) a.getDimension( 110 R.styleable.SliceView_gridTextVerticalPadding, defaultVerticalGridPadding); 111 mGridTopPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0); 112 mGridBottomPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0); 113 } finally { 114 a.recycle(); 115 } 116 117 populate(slice); 118 } 119 120 /** 121 * @return whether this row has content that is valid to display. 122 */ populate(Slice slice)123 private boolean populate(Slice slice) { 124 mColorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR); 125 // Find slice actions 126 mSliceActions = SliceMetadata.getSliceActions(slice); 127 // Find header 128 mHeaderItem = findHeaderItem(slice); 129 if (mHeaderItem != null) { 130 mRowItems.add(mHeaderItem); 131 } 132 mSeeMoreItem = getSeeMoreItem(slice); 133 // Filter + create row items 134 List<SliceItem> children = slice.getItems(); 135 for (int i = 0; i < children.size(); i++) { 136 final SliceItem child = children.get(i); 137 final String format = child.getFormat(); 138 boolean isNonRowContent = child.hasAnyHints(HINT_ACTIONS, HINT_SEE_MORE, HINT_KEYWORDS, 139 HINT_TTL, HINT_LAST_UPDATED); 140 if (!isNonRowContent && (FORMAT_ACTION.equals(format) || FORMAT_SLICE.equals(format))) { 141 if (mHeaderItem == null && !child.hasHint(HINT_LIST_ITEM)) { 142 mHeaderItem = child; 143 mRowItems.add(0, child); 144 } else if (child.hasHint(HINT_LIST_ITEM)) { 145 mRowItems.add(child); 146 } 147 } 148 } 149 // Ensure we have something for the header -- use first row 150 if (mHeaderItem == null && mRowItems.size() >= 1) { 151 mHeaderItem = mRowItems.get(0); 152 } 153 return isValid(); 154 } 155 156 /** 157 * Expects the provided list of items to be filtered (i.e. only things that can be turned into 158 * GridContent or RowContent) and in order (i.e. first item could be a header). 159 * 160 * @return the total height of all the rows contained in the provided list. 161 */ getListHeight(Context context, List<SliceItem> listItems)162 public int getListHeight(Context context, List<SliceItem> listItems) { 163 if (listItems == null) { 164 return 0; 165 } 166 int height = 0; 167 boolean hasRealHeader = false; 168 SliceItem maybeHeader = null; 169 if (!listItems.isEmpty()) { 170 maybeHeader = listItems.get(0); 171 hasRealHeader = !maybeHeader.hasAnyHints(HINT_LIST_ITEM, HINT_HORIZONTAL); 172 } 173 if (listItems.size() == 1 && !maybeHeader.hasHint(HINT_HORIZONTAL)) { 174 return getHeight(context, maybeHeader, true /* isHeader */, 0, 1, MODE_LARGE); 175 } 176 int rowCount = listItems.size(); 177 for (int i = 0; i < listItems.size(); i++) { 178 height += getHeight(context, listItems.get(i), i == 0 && hasRealHeader /* isHeader */, 179 i, rowCount, MODE_LARGE); 180 } 181 return height; 182 } 183 184 /** 185 * Returns a list of items that can be displayed in the provided height. If this list 186 * has a {@link #getSeeMoreItem()} this will be returned in the list if appropriate. 187 * 188 * @param height the height to restrict the items, -1 to use default sizings for non-scrolling 189 * templates. 190 * @return the list of items that can be displayed in the provided height. 191 */ 192 @NonNull getItemsForNonScrollingList(int height)193 public List<SliceItem> getItemsForNonScrollingList(int height) { 194 ArrayList<SliceItem> visibleItems = new ArrayList<>(); 195 if (mRowItems == null || mRowItems.size() == 0) { 196 return visibleItems; 197 } 198 final int idealItemCount = hasHeader() ? 4 : 3; 199 final int minItemCount = hasHeader() ? 2 : 1; 200 int visibleHeight = 0; 201 // Need to show see more 202 if (mSeeMoreItem != null) { 203 RowContent rc = new RowContent(mContext, mSeeMoreItem, false /* isHeader */); 204 visibleHeight += rc.getActualHeight(); 205 } 206 int rowCount = mRowItems.size(); 207 for (int i = 0; i < rowCount; i++) { 208 int itemHeight = getHeight(mContext, mRowItems.get(i), i == 0 /* isHeader */, 209 i, rowCount, MODE_LARGE); 210 if ((height == -1 && i > idealItemCount) 211 || (height > 0 && visibleHeight + itemHeight > height)) { 212 break; 213 } else { 214 visibleHeight += itemHeight; 215 visibleItems.add(mRowItems.get(i)); 216 } 217 } 218 if (mSeeMoreItem != null && visibleItems.size() >= minItemCount) { 219 // Only add see more if we're at least showing one item and it's not the header 220 visibleItems.add(mSeeMoreItem); 221 } 222 if (visibleItems.size() == 0) { 223 // Didn't have enough space to show anything; should still show something 224 visibleItems.add(mRowItems.get(0)); 225 } 226 return visibleItems; 227 } 228 229 /** 230 * Determines the height of the provided {@link SliceItem}. 231 */ getHeight(Context context, SliceItem item, boolean isHeader, int index, int count, int mode)232 public int getHeight(Context context, SliceItem item, boolean isHeader, int index, 233 int count, int mode) { 234 if (item.hasHint(HINT_HORIZONTAL)) { 235 GridContent gc = new GridContent(context, item); 236 int topPadding = gc.isAllImages() && index == 0 ? mGridTopPadding : 0; 237 int bottomPadding = gc.isAllImages() && index == count - 1 ? mGridBottomPadding : 0; 238 int height = mode == MODE_SMALL ? gc.getSmallHeight() : gc.getActualHeight(); 239 return height + topPadding + bottomPadding; 240 } else { 241 RowContent rc = new RowContent(context, item, isHeader); 242 return mode == MODE_SMALL ? rc.getSmallHeight() : rc.getActualHeight(); 243 } 244 } 245 246 /** 247 * @return whether this list has content that is valid to display. 248 */ isValid()249 public boolean isValid() { 250 return mRowItems.size() > 0; 251 } 252 253 @Nullable getSlice()254 public Slice getSlice() { 255 return mSlice; 256 } 257 258 @Nullable getColorItem()259 public SliceItem getColorItem() { 260 return mColorItem; 261 } 262 263 @Nullable getHeaderItem()264 public SliceItem getHeaderItem() { 265 return mHeaderItem; 266 } 267 268 @Nullable getSliceActions()269 public List<SliceItem> getSliceActions() { 270 return mSliceActions; 271 } 272 273 @Nullable getSeeMoreItem()274 public SliceItem getSeeMoreItem() { 275 return mSeeMoreItem; 276 } 277 278 @NonNull getRowItems()279 public ArrayList<SliceItem> getRowItems() { 280 return mRowItems; 281 } 282 283 /** 284 * @return whether this list has an explicit header (i.e. row item without HINT_LIST_ITEM) 285 */ hasHeader()286 public boolean hasHeader() { 287 return mHeaderItem != null && isValidHeader(mHeaderItem); 288 } 289 290 /** 291 * @return the type of template that the header represents. 292 */ getHeaderTemplateType()293 public int getHeaderTemplateType() { 294 return getRowType(mContext, mHeaderItem, true, mSliceActions); 295 } 296 297 /** 298 * The type of template that the provided row item represents. 299 * 300 * @param context context used for this slice. 301 * @param rowItem the row item to determine the template type of. 302 * @param isHeader whether this row item is used as a header. 303 * @param actions the actions associated with this slice, only matter if this row is the header. 304 * @return the type of template the provided row item represents. 305 */ getRowType(Context context, SliceItem rowItem, boolean isHeader, List<SliceItem> actions)306 public static int getRowType(Context context, SliceItem rowItem, boolean isHeader, 307 List<SliceItem> actions) { 308 if (rowItem != null) { 309 if (rowItem.hasHint(HINT_HORIZONTAL)) { 310 return EventInfo.ROW_TYPE_GRID; 311 } else { 312 RowContent rc = new RowContent(context, rowItem, isHeader); 313 SliceItem actionItem = rc.getPrimaryAction(); 314 SliceAction primaryAction = null; 315 if (actionItem != null) { 316 primaryAction = new SliceActionImpl(actionItem); 317 } 318 if (rc.getRange() != null) { 319 return FORMAT_ACTION.equals(rc.getRange().getFormat()) 320 ? EventInfo.ROW_TYPE_SLIDER 321 : EventInfo.ROW_TYPE_PROGRESS; 322 } else if (primaryAction != null && primaryAction.isToggle()) { 323 return EventInfo.ROW_TYPE_TOGGLE; 324 } else if (isHeader && actions != null) { 325 for (int i = 0; i < actions.size(); i++) { 326 if (new SliceActionImpl(actions.get(i)).isToggle()) { 327 return EventInfo.ROW_TYPE_TOGGLE; 328 } 329 } 330 return EventInfo.ROW_TYPE_LIST; 331 } else { 332 return rc.getToggleItems().size() > 0 333 ? EventInfo.ROW_TYPE_TOGGLE 334 : EventInfo.ROW_TYPE_LIST; 335 } 336 } 337 } 338 return EventInfo.ROW_TYPE_LIST; 339 } 340 341 /** 342 * @return the primary action for this list; i.e. action on the header or first row. 343 */ 344 @Nullable getPrimaryAction()345 public SliceItem getPrimaryAction() { 346 if (mHeaderItem != null) { 347 if (mHeaderItem.hasHint(HINT_HORIZONTAL)) { 348 GridContent gc = new GridContent(mContext, mHeaderItem); 349 return gc.getContentIntent(); 350 } else { 351 RowContent rc = new RowContent(mContext, mHeaderItem, false); 352 return rc.getPrimaryAction(); 353 } 354 } 355 return null; 356 } 357 358 @Nullable findHeaderItem(@onNull Slice slice)359 private static SliceItem findHeaderItem(@NonNull Slice slice) { 360 // See if header is specified 361 String[] nonHints = new String[] {HINT_LIST_ITEM, HINT_SHORTCUT, HINT_ACTIONS, 362 HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED, HINT_HORIZONTAL}; 363 SliceItem header = SliceQuery.find(slice, FORMAT_SLICE, null, nonHints); 364 if (header != null && isValidHeader(header)) { 365 return header; 366 } 367 return null; 368 } 369 370 @Nullable getSeeMoreItem(@onNull Slice slice)371 private static SliceItem getSeeMoreItem(@NonNull Slice slice) { 372 SliceItem item = SliceQuery.find(slice, null, HINT_SEE_MORE, null); 373 if (item != null) { 374 if (FORMAT_SLICE.equals(item.getFormat())) { 375 List<SliceItem> items = item.getSlice().getItems(); 376 if (items.size() == 1 && FORMAT_ACTION.equals(items.get(0).getFormat())) { 377 return items.get(0); 378 } 379 return item; 380 } 381 } 382 return null; 383 } 384 385 /** 386 * @return whether the provided slice item is a valid header. 387 */ isValidHeader(SliceItem sliceItem)388 public static boolean isValidHeader(SliceItem sliceItem) { 389 if (FORMAT_SLICE.equals(sliceItem.getFormat()) && !sliceItem.hasAnyHints(HINT_LIST_ITEM, 390 HINT_ACTIONS, HINT_KEYWORDS, HINT_SEE_MORE)) { 391 // Minimum valid header is a slice with text 392 SliceItem item = SliceQuery.find(sliceItem, FORMAT_TEXT, (String) null, null); 393 return item != null; 394 } 395 return false; 396 } 397 } 398