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_SEE_MORE; 21 import static android.app.slice.Slice.HINT_SHORTCUT; 22 import static android.app.slice.Slice.HINT_TITLE; 23 import static android.app.slice.Slice.SUBTYPE_COLOR; 24 import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION; 25 import static android.app.slice.SliceItem.FORMAT_ACTION; 26 import static android.app.slice.SliceItem.FORMAT_IMAGE; 27 import static android.app.slice.SliceItem.FORMAT_INT; 28 import static android.app.slice.SliceItem.FORMAT_LONG; 29 import static android.app.slice.SliceItem.FORMAT_SLICE; 30 import static android.app.slice.SliceItem.FORMAT_TEXT; 31 32 import static androidx.slice.core.SliceHints.HINT_KEYWORDS; 33 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED; 34 import static androidx.slice.core.SliceHints.HINT_TTL; 35 import static androidx.slice.core.SliceHints.ICON_IMAGE; 36 import static androidx.slice.core.SliceHints.LARGE_IMAGE; 37 import static androidx.slice.core.SliceHints.SMALL_IMAGE; 38 39 import android.app.slice.Slice; 40 import android.content.Context; 41 import android.content.res.Resources; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.Nullable; 45 import androidx.annotation.RestrictTo; 46 import androidx.slice.SliceItem; 47 import androidx.slice.core.SliceHints; 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 grid format from a slice. 56 * @hide 57 */ 58 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 59 public class GridContent { 60 61 private boolean mAllImages; 62 private SliceItem mColorItem; 63 private SliceItem mPrimaryAction; 64 private ArrayList<CellContent> mGridContent = new ArrayList<>(); 65 private SliceItem mSeeMoreItem; 66 private int mMaxCellLineCount; 67 private boolean mHasImage; 68 private @SliceHints.ImageMode int mLargestImageMode; 69 private SliceItem mContentDescr; 70 71 private int mBigPicMinHeight; 72 private int mBigPicMaxHeight; 73 private int mAllImagesHeight; 74 private int mImageTextHeight; 75 private int mMaxHeight; 76 private int mMinHeight; 77 GridContent(Context context, SliceItem gridItem)78 public GridContent(Context context, SliceItem gridItem) { 79 populate(gridItem); 80 81 Resources res = context.getResources(); 82 mBigPicMinHeight = res.getDimensionPixelSize(R.dimen.abc_slice_big_pic_min_height); 83 mBigPicMaxHeight = res.getDimensionPixelSize(R.dimen.abc_slice_big_pic_max_height); 84 mAllImagesHeight = res.getDimensionPixelSize(R.dimen.abc_slice_grid_image_only_height); 85 mImageTextHeight = res.getDimensionPixelSize(R.dimen.abc_slice_grid_image_text_height); 86 mMinHeight = res.getDimensionPixelSize(R.dimen.abc_slice_grid_min_height); 87 mMaxHeight = res.getDimensionPixelSize(R.dimen.abc_slice_grid_max_height); 88 } 89 90 /** 91 * @return whether this grid has content that is valid to display. 92 */ populate(SliceItem gridItem)93 private boolean populate(SliceItem gridItem) { 94 mColorItem = SliceQuery.findSubtype(gridItem, FORMAT_INT, SUBTYPE_COLOR); 95 mSeeMoreItem = SliceQuery.find(gridItem, null, HINT_SEE_MORE, null); 96 if (mSeeMoreItem != null && FORMAT_SLICE.equals(mSeeMoreItem.getFormat())) { 97 mSeeMoreItem = mSeeMoreItem.getSlice().getItems().get(0); 98 } 99 String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE}; 100 mPrimaryAction = SliceQuery.find(gridItem, FORMAT_SLICE, hints, 101 new String[] {HINT_ACTIONS} /* nonHints */); 102 mAllImages = true; 103 if (FORMAT_SLICE.equals(gridItem.getFormat())) { 104 List<SliceItem> items = gridItem.getSlice().getItems(); 105 if (items.size() == 1 && FORMAT_SLICE.equals(items.get(0).getFormat())) { 106 // TODO: this can be removed at release 107 items = items.get(0).getSlice().getItems(); 108 } 109 items = filterAndProcessItems(items); 110 // Check if it it's only one item that is a slice 111 if (items.size() == 1 && items.get(0).getFormat().equals(FORMAT_SLICE)) { 112 items = items.get(0).getSlice().getItems(); 113 } 114 for (int i = 0; i < items.size(); i++) { 115 SliceItem item = items.get(i); 116 if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) { 117 mContentDescr = item; 118 } else { 119 CellContent cc = new CellContent(item); 120 processContent(cc); 121 } 122 } 123 } else { 124 CellContent cc = new CellContent(gridItem); 125 processContent(cc); 126 } 127 return isValid(); 128 } 129 processContent(CellContent cc)130 private void processContent(CellContent cc) { 131 if (cc.isValid()) { 132 mGridContent.add(cc); 133 if (!cc.isImageOnly()) { 134 mAllImages = false; 135 } 136 mMaxCellLineCount = Math.max(mMaxCellLineCount, cc.getTextCount()); 137 mHasImage |= cc.hasImage(); 138 mLargestImageMode = Math.max(mLargestImageMode, cc.getImageMode()); 139 } 140 } 141 142 /** 143 * @return the list of cell content for this grid. 144 */ 145 @NonNull getGridContent()146 public ArrayList<CellContent> getGridContent() { 147 return mGridContent; 148 } 149 150 /** 151 * @return the color to tint content in this grid. 152 */ 153 @Nullable getColorItem()154 public SliceItem getColorItem() { 155 return mColorItem; 156 } 157 158 /** 159 * @return the content intent item for this grid. 160 */ 161 @Nullable getContentIntent()162 public SliceItem getContentIntent() { 163 return mPrimaryAction; 164 } 165 166 /** 167 * @return the see more item to use when not all items in the grid can be displayed. 168 */ 169 @Nullable getSeeMoreItem()170 public SliceItem getSeeMoreItem() { 171 return mSeeMoreItem; 172 } 173 174 /** 175 * @return content description for this row. 176 */ 177 @Nullable getContentDescription()178 public CharSequence getContentDescription() { 179 return mContentDescr != null ? mContentDescr.getText() : null; 180 } 181 182 /** 183 * @return whether this grid has content that is valid to display. 184 */ isValid()185 public boolean isValid() { 186 return mGridContent.size() > 0; 187 } 188 189 /** 190 * @return whether the contents of this grid is just images. 191 */ isAllImages()192 public boolean isAllImages() { 193 return mAllImages; 194 } 195 196 /** 197 * Filters non-cell items out of the list of items and finds content description. 198 */ filterAndProcessItems(List<SliceItem> items)199 private List<SliceItem> filterAndProcessItems(List<SliceItem> items) { 200 List<SliceItem> filteredItems = new ArrayList<>(); 201 for (int i = 0; i < items.size(); i++) { 202 SliceItem item = items.get(i); 203 // TODO: This see more can be removed at release 204 boolean containsSeeMore = SliceQuery.find(item, null, HINT_SEE_MORE, null) != null; 205 boolean isNonCellContent = containsSeeMore 206 || item.hasAnyHints(HINT_SHORTCUT, HINT_SEE_MORE, HINT_KEYWORDS, HINT_TTL, 207 HINT_LAST_UPDATED); 208 if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) { 209 mContentDescr = item; 210 } else if (!isNonCellContent) { 211 filteredItems.add(item); 212 } 213 } 214 return filteredItems; 215 } 216 217 /** 218 * @return the max number of lines of text in the cells of this grid row. 219 */ getMaxCellLineCount()220 public int getMaxCellLineCount() { 221 return mMaxCellLineCount; 222 } 223 224 /** 225 * @return whether this row contains an image. 226 */ hasImage()227 public boolean hasImage() { 228 return mHasImage; 229 } 230 231 /** 232 * @return the height to display a grid row at when it is used as a small template. 233 * Does not include padding that might be added by slice view attributes, 234 * see {@link ListContent#getListHeight(Context, List)}. 235 */ getSmallHeight()236 public int getSmallHeight() { 237 return getHeight(true /* isSmall */); 238 } 239 240 /** 241 * @return the height the content in this template requires to be displayed. 242 * Does not include padding that might be added by slice view attributes, 243 * see {@link ListContent#getListHeight(Context, List)}. 244 */ getActualHeight()245 public int getActualHeight() { 246 return getHeight(false /* isSmall */); 247 } 248 getHeight(boolean isSmall)249 private int getHeight(boolean isSmall) { 250 if (!isValid()) { 251 return 0; 252 } 253 if (mAllImages) { 254 return mGridContent.size() == 1 255 ? isSmall ? mBigPicMinHeight : mBigPicMaxHeight 256 : mLargestImageMode == ICON_IMAGE ? mMinHeight : mAllImagesHeight; 257 } else { 258 boolean twoLines = getMaxCellLineCount() > 1; 259 boolean hasImage = hasImage(); 260 return (twoLines && !isSmall) 261 ? hasImage ? mMaxHeight : mMinHeight 262 : mLargestImageMode == ICON_IMAGE ? mMinHeight : mImageTextHeight; 263 } 264 } 265 266 /** 267 * Extracts information required to present content in a cell. 268 * @hide 269 */ 270 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 271 public static class CellContent { 272 private SliceItem mContentIntent; 273 private ArrayList<SliceItem> mCellItems = new ArrayList<>(); 274 private SliceItem mContentDescr; 275 private int mTextCount; 276 private boolean mHasImage; 277 private int mImageMode = -1; 278 CellContent(SliceItem cellItem)279 public CellContent(SliceItem cellItem) { 280 populate(cellItem); 281 } 282 283 /** 284 * @return whether this row has content that is valid to display. 285 */ populate(SliceItem cellItem)286 public boolean populate(SliceItem cellItem) { 287 final String format = cellItem.getFormat(); 288 if (!cellItem.hasHint(HINT_SHORTCUT) 289 && (FORMAT_SLICE.equals(format) || FORMAT_ACTION.equals(format))) { 290 List<SliceItem> items = cellItem.getSlice().getItems(); 291 // If we've only got one item that's a slice / action use those items instead 292 if (items.size() == 1 && (FORMAT_ACTION.equals(items.get(0).getFormat()) 293 || FORMAT_SLICE.equals(items.get(0).getFormat()))) { 294 mContentIntent = items.get(0); 295 items = items.get(0).getSlice().getItems(); 296 } 297 if (FORMAT_ACTION.equals(format)) { 298 mContentIntent = cellItem; 299 } 300 mTextCount = 0; 301 int imageCount = 0; 302 for (int i = 0; i < items.size(); i++) { 303 final SliceItem item = items.get(i); 304 final String itemFormat = item.getFormat(); 305 if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) { 306 mContentDescr = item; 307 } else if (mTextCount < 2 && (FORMAT_TEXT.equals(itemFormat) 308 || FORMAT_LONG.equals(itemFormat))) { 309 mTextCount++; 310 mCellItems.add(item); 311 } else if (imageCount < 1 && FORMAT_IMAGE.equals(item.getFormat())) { 312 if (item.hasHint(Slice.HINT_NO_TINT)) { 313 mImageMode = item.hasHint(Slice.HINT_LARGE) 314 ? LARGE_IMAGE 315 : SMALL_IMAGE; 316 } else { 317 mImageMode = ICON_IMAGE; 318 } 319 imageCount++; 320 mHasImage = true; 321 mCellItems.add(item); 322 } 323 } 324 } else if (isValidCellContent(cellItem)) { 325 mCellItems.add(cellItem); 326 } 327 return isValid(); 328 } 329 330 /** 331 * @return the action to activate when this cell is tapped. 332 */ getContentIntent()333 public SliceItem getContentIntent() { 334 return mContentIntent; 335 } 336 337 /** 338 * @return the slice items to display in this cell. 339 */ getCellItems()340 public ArrayList<SliceItem> getCellItems() { 341 return mCellItems; 342 } 343 344 /** 345 * @return whether this is content that is valid to show in a grid cell. 346 */ isValidCellContent(SliceItem cellItem)347 private boolean isValidCellContent(SliceItem cellItem) { 348 final String format = cellItem.getFormat(); 349 boolean isNonCellContent = SUBTYPE_CONTENT_DESCRIPTION.equals(cellItem.getSubType()) 350 || cellItem.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED); 351 return !isNonCellContent 352 && (FORMAT_TEXT.equals(format) 353 || FORMAT_LONG.equals(format) 354 || FORMAT_IMAGE.equals(format)); 355 } 356 357 /** 358 * @return whether this grid has content that is valid to display. 359 */ isValid()360 public boolean isValid() { 361 return mCellItems.size() > 0 && mCellItems.size() <= 3; 362 } 363 364 /** 365 * @return whether this cell contains just an image. 366 */ isImageOnly()367 public boolean isImageOnly() { 368 return mCellItems.size() == 1 && FORMAT_IMAGE.equals(mCellItems.get(0).getFormat()); 369 } 370 371 /** 372 * @return number of text items in this cell. 373 */ getTextCount()374 public int getTextCount() { 375 return mTextCount; 376 } 377 378 /** 379 * @return whether this cell contains an image. 380 */ hasImage()381 public boolean hasImage() { 382 return mHasImage; 383 } 384 385 /** 386 * @return the mode of the image. 387 */ getImageMode()388 public int getImageMode() { 389 return mImageMode; 390 } 391 392 @Nullable getContentDescription()393 public CharSequence getContentDescription() { 394 return mContentDescr != null ? mContentDescr.getText() : null; 395 } 396 } 397 } 398