• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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