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 com.android.tv.twopanelsettings.slices.compat.widget; 18 19 import static android.app.slice.Slice.HINT_ACTIONS; 20 import static android.app.slice.Slice.HINT_KEYWORDS; 21 import static android.app.slice.Slice.HINT_LAST_UPDATED; 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.HINT_TITLE; 25 import static android.app.slice.Slice.HINT_TTL; 26 import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION; 27 import static android.app.slice.SliceItem.FORMAT_ACTION; 28 import static android.app.slice.SliceItem.FORMAT_IMAGE; 29 import static android.app.slice.SliceItem.FORMAT_LONG; 30 import static android.app.slice.SliceItem.FORMAT_SLICE; 31 import static android.app.slice.SliceItem.FORMAT_TEXT; 32 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.HINT_OVERLAY; 33 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.SUBTYPE_DATE_PICKER; 34 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.SUBTYPE_TIME_PICKER; 35 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.UNKNOWN_IMAGE; 36 37 import android.content.Context; 38 import android.graphics.Point; 39 import android.graphics.drawable.Drawable; 40 import androidx.annotation.NonNull; 41 import androidx.annotation.Nullable; 42 import androidx.core.graphics.drawable.IconCompat; 43 import com.android.tv.twopanelsettings.slices.compat.SliceItem; 44 import com.android.tv.twopanelsettings.slices.compat.SliceUtils; 45 import com.android.tv.twopanelsettings.slices.compat.core.SliceAction; 46 import com.android.tv.twopanelsettings.slices.compat.core.SliceActionImpl; 47 import com.android.tv.twopanelsettings.slices.compat.core.SliceQuery; 48 import java.util.ArrayList; 49 import java.util.List; 50 51 /** 52 * Extracts information required to present content in a grid format from a slice. 53 * 54 * <p>Slice framework has been deprecated, it will not receive any updates moving forward. If you 55 * are looking for a framework that handles communication across apps, consider using {@link 56 * android.app.appsearch.AppSearchManager}. 57 */ 58 // @Deprecated // Supported for TV 59 public class GridContent extends SliceContent { 60 61 private boolean mAllImages; 62 private SliceItem mPrimaryAction; 63 private final ArrayList<CellContent> mGridContent = new ArrayList<>(); 64 private SliceItem mSeeMoreItem; 65 private int mMaxCellLineCount; 66 private int mLargestImageMode = UNKNOWN_IMAGE; 67 private boolean mIsLastIndex; 68 private IconCompat mFirstImage = null; 69 private Point mFirstImageSize = null; 70 71 private SliceItem mTitleItem; 72 73 /** */ 74 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) GridContent(@onNull SliceItem gridItem, int position)75 public GridContent(@NonNull SliceItem gridItem, int position) { 76 super(gridItem, position); 77 populate(gridItem); 78 } 79 80 /** 81 * @return whether this grid has content that is valid to display. 82 */ populate(SliceItem gridItem)83 private boolean populate(SliceItem gridItem) { 84 mSeeMoreItem = SliceQuery.find(gridItem, null, HINT_SEE_MORE, null); 85 if (mSeeMoreItem != null && FORMAT_SLICE.equals(mSeeMoreItem.getFormat())) { 86 List<SliceItem> seeMoreItems = mSeeMoreItem.getSlice().getItems(); 87 if (seeMoreItems != null && !seeMoreItems.isEmpty()) { 88 mSeeMoreItem = seeMoreItems.get(0); 89 } 90 } 91 String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE}; 92 mPrimaryAction = 93 SliceQuery.find(gridItem, FORMAT_SLICE, hints, new String[] {HINT_ACTIONS} /* nonHints */); 94 mAllImages = true; 95 if (FORMAT_SLICE.equals(gridItem.getFormat())) { 96 List<SliceItem> items = gridItem.getSlice().getItems(); 97 items = filterAndProcessItems(items); 98 for (int i = 0; i < items.size(); i++) { 99 SliceItem item = items.get(i); 100 if (!SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) { 101 CellContent cc = new CellContent(item); 102 processContent(cc); 103 } 104 } 105 } else { 106 CellContent cc = new CellContent(gridItem); 107 processContent(cc); 108 } 109 return isValid(); 110 } 111 processContent(CellContent cc)112 private void processContent(CellContent cc) { 113 if (cc.isValid()) { 114 if ((mTitleItem == null && cc.getTitleItem() != null)) { 115 mTitleItem = cc.getTitleItem(); 116 } 117 mGridContent.add(cc); 118 if (!cc.isImageOnly()) { 119 mAllImages = false; 120 } 121 mMaxCellLineCount = Math.max(mMaxCellLineCount, cc.getTextCount()); 122 if (mFirstImage == null && cc.hasImage()) { 123 mFirstImage = cc.getImageIcon(); 124 } 125 mLargestImageMode = 126 mLargestImageMode == UNKNOWN_IMAGE 127 ? cc.getImageMode() 128 : Math.max(mLargestImageMode, cc.getImageMode()); 129 } 130 } 131 132 /** 133 * @return the title of this grid row, if it exists. 134 */ 135 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) 136 @Nullable getTitle()137 public CharSequence getTitle() { 138 if (mTitleItem != null) { 139 return mTitleItem.getSanitizedText(); 140 } else if (mPrimaryAction != null) { 141 return new SliceActionImpl(mPrimaryAction).getTitle(); 142 } 143 return null; 144 } 145 146 /** 147 * @return the list of cell content for this grid. 148 */ 149 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) 150 @NonNull getGridContent()151 public ArrayList<CellContent> getGridContent() { 152 return mGridContent; 153 } 154 155 /** 156 * @return the content intent item for this grid. 157 */ 158 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) 159 @Nullable getContentIntent()160 public SliceItem getContentIntent() { 161 return mPrimaryAction; 162 } 163 164 /** 165 * @return the see more item to use when not all items in the grid can be displayed. 166 */ 167 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) 168 @Nullable getSeeMoreItem()169 public SliceItem getSeeMoreItem() { 170 return mSeeMoreItem; 171 } 172 173 /** 174 * @return whether this grid has content that is valid to display. 175 */ 176 @Override isValid()177 public boolean isValid() { 178 return super.isValid() && !mGridContent.isEmpty(); 179 } 180 181 /** 182 * @return whether the contents of this grid is just images. 183 */ 184 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) isAllImages()185 public boolean isAllImages() { 186 return mAllImages; 187 } 188 189 /** 190 * @return the largest image size in this row, if there are images. 191 */ 192 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) getLargestImageMode()193 public int getLargestImageMode() { 194 return mLargestImageMode; 195 } 196 197 /** 198 * @return the first image dimensions in this row, if there are images. If there are no images, 199 * return {-1, -1}. 200 */ 201 @NonNull getFirstImageSize(@onNull Context context)202 public Point getFirstImageSize(@NonNull Context context) { 203 if (mFirstImage == null) { 204 return new Point(-1, -1); 205 } 206 if (mFirstImageSize == null) { 207 Drawable d = mFirstImage.loadDrawable(context); 208 mFirstImageSize = new Point(d.getIntrinsicWidth(), d.getIntrinsicHeight()); 209 } 210 return mFirstImageSize; 211 } 212 213 /** Filters non-cell items out of the list of items and finds content description. */ filterAndProcessItems(List<SliceItem> items)214 private List<SliceItem> filterAndProcessItems(List<SliceItem> items) { 215 216 List<SliceItem> filteredItems = new ArrayList<>(); 217 for (int i = 0; i < items.size(); i++) { 218 SliceItem item = items.get(i); 219 // TODO: This see more can be removed at release 220 boolean containsSeeMore = SliceQuery.find(item, null, HINT_SEE_MORE, null) != null; 221 boolean isNonCellContent = 222 containsSeeMore 223 || item.hasAnyHints( 224 HINT_SHORTCUT, 225 HINT_SEE_MORE, 226 HINT_KEYWORDS, 227 HINT_TTL, 228 HINT_LAST_UPDATED, 229 HINT_OVERLAY); 230 if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) { 231 mContentDescr = item; 232 } else if (!isNonCellContent) { 233 filteredItems.add(item); 234 } 235 } 236 return filteredItems; 237 } 238 239 /** 240 * @return the max number of lines of text in the cells of this grid row. 241 */ 242 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) getMaxCellLineCount()243 public int getMaxCellLineCount() { 244 return mMaxCellLineCount; 245 } 246 247 /** 248 * @return whether this row contains an image. 249 */ 250 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) hasImage()251 public boolean hasImage() { 252 return mFirstImage != null; 253 } 254 255 /** 256 * @return whether this content is being displayed last in a list. 257 */ 258 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) getIsLastIndex()259 public boolean getIsLastIndex() { 260 return mIsLastIndex; 261 } 262 263 /** Sets whether this content is being displayed last in a list. */ 264 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) setIsLastIndex(boolean isLast)265 public void setIsLastIndex(boolean isLast) { 266 mIsLastIndex = isLast; 267 } 268 269 /** */ 270 // @RestrictTo(RestrictTo.Scope.LIBRARY) 271 @Override getHeight(@onNull SliceStyle style, @NonNull SliceViewPolicy policy)272 public int getHeight(@NonNull SliceStyle style, @NonNull SliceViewPolicy policy) { 273 return style.getGridHeight(this, policy); 274 } 275 276 /** Extracts information required to present content in a cell. */ 277 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) 278 public static class CellContent { 279 private SliceItem mContentIntent; 280 private SliceItem mPicker; 281 private final ArrayList<SliceItem> mCellItems = new ArrayList<>(); 282 private SliceItem mContentDescr; 283 private int mTextCount; 284 private int mImageCount; 285 private IconCompat mImage; 286 private SliceItem mOverlayItem; 287 private int mImageMode = -1; 288 private SliceItem mTitleItem; 289 private SliceItem mToggleItem; 290 CellContent(@onNull SliceItem cellItem)291 public CellContent(@NonNull SliceItem cellItem) { 292 populate(cellItem); 293 } 294 295 /** 296 * @return whether this row has content that is valid to display. 297 */ populate(@onNull SliceItem cellItem)298 public boolean populate(@NonNull SliceItem cellItem) { 299 final String format = cellItem.getFormat(); 300 if (!cellItem.hasHint(HINT_SHORTCUT) 301 && (FORMAT_SLICE.equals(format) || FORMAT_ACTION.equals(format))) { 302 List<SliceItem> items = cellItem.getSlice().getItems(); 303 List<SliceItem> sliceActionItems = null; 304 305 // Fill the sliceActionItems with the first showing SliceAction in items. 306 for (SliceItem item : items) { 307 if ((FORMAT_ACTION.equals(item.getFormat()) || FORMAT_SLICE.equals(item.getFormat())) 308 && !(SUBTYPE_DATE_PICKER.equals(item.getSubType()) 309 || SUBTYPE_TIME_PICKER.equals(item.getSubType()))) { 310 sliceActionItems = item.getSlice().getItems(); 311 SliceAction ac = new SliceActionImpl(item); 312 if (ac.isToggle()) { 313 mToggleItem = item; 314 } else { 315 mContentIntent = items.get(0); 316 } 317 break; 318 } 319 } 320 if (FORMAT_ACTION.equals(format)) { 321 mContentIntent = cellItem; 322 } 323 mTextCount = 0; 324 mImageCount = 0; 325 fillCellItems(items); 326 327 if (mTextCount == 0 && mImageCount == 0 && sliceActionItems != null) { 328 fillCellItems(sliceActionItems); 329 } 330 } else if (isValidCellContent(cellItem)) { 331 mCellItems.add(cellItem); 332 } 333 return isValid(); 334 } 335 fillCellItems(List<SliceItem> items)336 private void fillCellItems(List<SliceItem> items) { 337 for (int i = 0; i < items.size(); i++) { 338 final SliceItem item = items.get(i); 339 final String itemFormat = item.getFormat(); 340 if (mPicker == null 341 && (SUBTYPE_DATE_PICKER.equals(item.getSubType()) 342 || SUBTYPE_TIME_PICKER.equals(item.getSubType()))) { 343 mPicker = item; 344 } else if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) { 345 mContentDescr = item; 346 } else if (mTextCount < 2 347 && (FORMAT_TEXT.equals(itemFormat) || FORMAT_LONG.equals(itemFormat))) { 348 if (mTitleItem == null || (!mTitleItem.hasHint(HINT_TITLE) && item.hasHint(HINT_TITLE))) { 349 mTitleItem = item; 350 } 351 if (item.hasHint(HINT_OVERLAY)) { 352 if (mOverlayItem == null) { 353 mOverlayItem = item; 354 } 355 } else { 356 mTextCount++; 357 mCellItems.add(item); 358 } 359 } else if (mImageCount < 1 && FORMAT_IMAGE.equals(item.getFormat())) { 360 mImageMode = SliceUtils.parseImageMode(item); 361 mImageCount++; 362 mImage = item.getIcon(); 363 mCellItems.add(item); 364 } 365 } 366 } 367 368 /** 369 * @return toggle slice item if this cell has one. 370 */ 371 @Nullable getToggleItem()372 public SliceItem getToggleItem() { 373 return mToggleItem; 374 } 375 376 /** 377 * @return title text slice item if this cell has one. 378 */ 379 @Nullable getTitleItem()380 public SliceItem getTitleItem() { 381 return mTitleItem; 382 } 383 384 /** 385 * @return image overlay text slice item if this cell has one. 386 */ 387 @Nullable getOverlayItem()388 public SliceItem getOverlayItem() { 389 return mOverlayItem; 390 } 391 392 /** 393 * @return the action to activate when this cell is tapped. 394 */ 395 @Nullable getContentIntent()396 public SliceItem getContentIntent() { 397 return mContentIntent; 398 } 399 400 /** 401 * @return the Picker to use when this cell is tapped. 402 */ 403 @Nullable getPicker()404 public SliceItem getPicker() { 405 return mPicker; 406 } 407 408 /** 409 * @return the slice items to display in this cell. 410 */ 411 @NonNull getCellItems()412 public ArrayList<SliceItem> getCellItems() { 413 return mCellItems; 414 } 415 416 /** 417 * @return whether this is content that is valid to show in a grid cell. 418 */ isValidCellContent(SliceItem cellItem)419 private boolean isValidCellContent(SliceItem cellItem) { 420 final String format = cellItem.getFormat(); 421 boolean isNonCellContent = 422 SUBTYPE_CONTENT_DESCRIPTION.equals(cellItem.getSubType()) 423 || cellItem.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED); 424 return !isNonCellContent 425 && (FORMAT_TEXT.equals(format) 426 || FORMAT_LONG.equals(format) 427 || FORMAT_IMAGE.equals(format)); 428 } 429 430 /** 431 * @return whether this grid has content that is valid to display. 432 */ isValid()433 public boolean isValid() { 434 return mPicker != null || (!mCellItems.isEmpty() && mCellItems.size() <= 3); 435 } 436 437 /** 438 * @return whether this cell contains just an image. 439 */ isImageOnly()440 public boolean isImageOnly() { 441 return mCellItems.size() == 1 && FORMAT_IMAGE.equals(mCellItems.get(0).getFormat()); 442 } 443 444 /** 445 * @return number of text items in this cell. 446 */ getTextCount()447 public int getTextCount() { 448 return mTextCount; 449 } 450 451 /** 452 * @return whether this cell contains an image. 453 */ hasImage()454 public boolean hasImage() { 455 return mImage != null; 456 } 457 458 /** 459 * @return the mode of the image. 460 */ getImageMode()461 public int getImageMode() { 462 return mImageMode; 463 } 464 465 /** 466 * @return the IconCompat of the image. 467 */ 468 @Nullable getImageIcon()469 public IconCompat getImageIcon() { 470 return mImage; 471 } 472 473 @Nullable getContentDescription()474 public CharSequence getContentDescription() { 475 return mContentDescr != null ? mContentDescr.getText() : null; 476 } 477 } 478 } 479