• 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_HORIZONTAL;
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_PARTIAL;
23 import static android.app.slice.Slice.HINT_SEE_MORE;
24 import static android.app.slice.Slice.HINT_SHORTCUT;
25 import static android.app.slice.Slice.HINT_SUMMARY;
26 import static android.app.slice.Slice.HINT_TITLE;
27 import static android.app.slice.Slice.HINT_TTL;
28 import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
29 import static android.app.slice.Slice.SUBTYPE_RANGE;
30 import static android.app.slice.SliceItem.FORMAT_ACTION;
31 import static android.app.slice.SliceItem.FORMAT_IMAGE;
32 import static android.app.slice.SliceItem.FORMAT_INT;
33 import static android.app.slice.SliceItem.FORMAT_LONG;
34 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
35 import static android.app.slice.SliceItem.FORMAT_SLICE;
36 import static android.app.slice.SliceItem.FORMAT_TEXT;
37 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.HINT_END_OF_SECTION;
38 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.SUBTYPE_SELECTION;
39 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.SUBTYPE_SELECTION_OPTION_KEY;
40 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.SUBTYPE_SELECTION_OPTION_VALUE;
41 
42 import android.text.TextUtils;
43 import android.util.Log;
44 import androidx.annotation.NonNull;
45 import androidx.annotation.Nullable;
46 import com.android.tv.twopanelsettings.slices.compat.SliceItem;
47 import com.android.tv.twopanelsettings.slices.compat.core.SliceAction;
48 import com.android.tv.twopanelsettings.slices.compat.core.SliceActionImpl;
49 import com.android.tv.twopanelsettings.slices.compat.core.SliceQuery;
50 import java.util.ArrayList;
51 import java.util.List;
52 
53 /** Extracts information required to present content in a row format from a slice. */
54 // @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
55 // @Deprecated // Supported for TV
56 public class RowContent extends SliceContent {
57   private static final String TAG = "RowContent";
58 
59   private SliceItem mPrimaryAction;
60   private SliceItem mStartItem;
61   private SliceItem mTitleItem;
62   private SliceItem mSubtitleItem;
63   private SliceItem mSummaryItem;
64   private final ArrayList<SliceItem> mEndItems = new ArrayList<>();
65   private final ArrayList<SliceAction> mToggleItems = new ArrayList<>();
66   private SliceItem mRange;
67   private SliceItem mSelection;
68   private boolean mIsHeader;
69   private int mLineCount = 0;
70   private boolean mShowTitleItems;
71   private boolean mShowBottomDivider;
72   private boolean mShowActionDivider;
73 
RowContent(SliceItem rowSlice, int position)74   public RowContent(SliceItem rowSlice, int position) {
75     super(rowSlice, position);
76     populate(rowSlice, position == 0);
77   }
78 
79   /**
80    * @return whether this row has content that is valid to display.
81    */
populate(SliceItem rowSlice, boolean isHeader)82   private boolean populate(SliceItem rowSlice, boolean isHeader) {
83     if (rowSlice.hasHint(HINT_END_OF_SECTION)) {
84       showBottomDivider(true);
85     }
86 
87     mIsHeader = isHeader;
88     if (!isValidRow(rowSlice)) {
89       Log.w(TAG, "Provided SliceItem is invalid for RowContent");
90       return false;
91     }
92     determineStartAndPrimaryAction(rowSlice);
93 
94     // Filter anything not viable for displaying in a row
95     ArrayList<SliceItem> rowItems = filterInvalidItems(rowSlice);
96     // If we've only got one item that's a slice / action use those items instead
97     boolean isOneItem = false;
98     if (rowItems.size() == 1
99         && (FORMAT_ACTION.equals(rowItems.get(0).getFormat())
100             || FORMAT_SLICE.equals(rowItems.get(0).getFormat()))
101         && !rowItems.get(0).hasAnyHints(HINT_SHORTCUT, HINT_TITLE)) {
102       if (isValidRow(rowItems.get(0))) {
103         isOneItem = true;
104         rowSlice = rowItems.get(0);
105         rowItems = filterInvalidItems(rowSlice);
106       }
107     }
108     if (SUBTYPE_RANGE.equals(rowSlice.getSubType())) {
109       // It must be a Range, InputRange, or StarRating without StartItem/EndItem.
110       if (SliceQuery.findSubtype(rowSlice, FORMAT_ACTION, SUBTYPE_RANGE) == null || isOneItem) {
111         mRange = rowSlice;
112       } else {
113         // Remove the startItem we already know about
114         rowItems.remove(mStartItem);
115         // After removing startItem, if size == 1, then it must be action<range>
116         if (rowItems.size() == 1) {
117           if (isValidRow(rowItems.get(0))) {
118             rowSlice = rowItems.get(0);
119             rowItems = filterInvalidItems(rowSlice);
120             mRange = rowSlice;
121             // Remove thumb icon, don't let it be added into endItems
122             rowItems.remove(getInputRangeThumb());
123           }
124         } else { // It must have end items.
125           mRange = SliceQuery.findSubtype(rowSlice, FORMAT_ACTION, SUBTYPE_RANGE);
126           // Refactor the rowItems, then it can be parsed correctly
127           ArrayList<SliceItem> rangeItems = filterInvalidItems(mRange);
128           rangeItems.remove(getInputRangeThumb());
129           rowItems.remove(mRange);
130           rowItems.addAll(rangeItems);
131         }
132       }
133     }
134     if (SUBTYPE_SELECTION.equals(rowSlice.getSubType())) {
135       mSelection = rowSlice;
136     }
137     if (!rowItems.isEmpty()) {
138       // Remove the things we already know about
139       if (mStartItem != null) {
140         rowItems.remove(mStartItem);
141       }
142       if (mPrimaryAction != null) {
143         rowItems.remove(mPrimaryAction);
144       }
145 
146       // Text + end items
147       ArrayList<SliceItem> endItems = new ArrayList<>();
148       for (int i = 0; i < rowItems.size(); i++) {
149         final SliceItem item = rowItems.get(i);
150         if (FORMAT_TEXT.equals(item.getFormat())) {
151           if ((mTitleItem == null || !mTitleItem.hasHint(HINT_TITLE))
152               && item.hasHint(HINT_TITLE)
153               && !item.hasHint(HINT_SUMMARY)) {
154             mTitleItem = item;
155           } else if (mSubtitleItem == null && !item.hasHint(HINT_SUMMARY)) {
156             mSubtitleItem = item;
157           } else if (mSummaryItem == null && item.hasHint(HINT_SUMMARY)) {
158             mSummaryItem = item;
159           }
160         } else {
161           endItems.add(item);
162         }
163       }
164       if (hasText(mTitleItem)) {
165         mLineCount++;
166       }
167       if (hasText(mSubtitleItem)) {
168         mLineCount++;
169       }
170       // Special rules for end items: only one timestamp
171       boolean hasTimestamp = mStartItem != null && FORMAT_LONG.equals(mStartItem.getFormat());
172       for (int i = 0; i < endItems.size(); i++) {
173         final SliceItem item = endItems.get(i);
174         boolean isAction = SliceQuery.find(item, FORMAT_ACTION) != null;
175         if (FORMAT_LONG.equals(item.getFormat())) {
176           if (!hasTimestamp) {
177             hasTimestamp = true;
178             mEndItems.add(item);
179           }
180         } else {
181           processContent(item, isAction);
182         }
183       }
184     }
185     return isValid();
186   }
187 
processContent(@onNull SliceItem item, boolean isAction)188   private void processContent(@NonNull SliceItem item, boolean isAction) {
189     if (isAction) {
190       SliceAction ac = new SliceActionImpl(item);
191       if (ac.isToggle()) {
192         mToggleItems.add(ac);
193       }
194     }
195     mEndItems.add(item);
196   }
197 
198   /** Sets the {@link #getPrimaryAction()} and {@link #getStartItem()} for this row. */
determineStartAndPrimaryAction(@onNull SliceItem rowSlice)199   private void determineStartAndPrimaryAction(@NonNull SliceItem rowSlice) {
200     List<SliceItem> possibleStartItems = SliceQuery.findAll(rowSlice, null, HINT_TITLE, null);
201     if (!possibleStartItems.isEmpty()) {
202       // The start item will be at position 0 if it exists
203       String format = possibleStartItems.get(0).getFormat();
204       if ((FORMAT_ACTION.equals(format)
205               && SliceQuery.find(possibleStartItems.get(0), FORMAT_IMAGE) != null)
206           || FORMAT_SLICE.equals(format)
207           || FORMAT_LONG.equals(format)
208           || FORMAT_IMAGE.equals(format)) {
209         mStartItem = possibleStartItems.get(0);
210       }
211     }
212 
213     // Possible primary actions could be in the format of slice or action.
214     String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE};
215     List<SliceItem> possiblePrimaries = SliceQuery.findAll(rowSlice, FORMAT_SLICE, hints, null);
216     possiblePrimaries.addAll(SliceQuery.findAll(rowSlice, FORMAT_ACTION, hints, null));
217 
218     if (possiblePrimaries.isEmpty()
219         && FORMAT_ACTION.equals(rowSlice.getFormat())
220         && rowSlice.getSlice().getItems().size() == 1) {
221       mPrimaryAction = rowSlice;
222     } else if (mStartItem != null
223         && possiblePrimaries.size() > 1
224         && possiblePrimaries.get(0) == mStartItem) {
225       // Next item is the primary action
226       mPrimaryAction = possiblePrimaries.get(1);
227     } else if (!possiblePrimaries.isEmpty()) {
228       mPrimaryAction = possiblePrimaries.get(0);
229     }
230   }
231 
232   @Override
isValid()233   public boolean isValid() {
234     return super.isValid()
235         && (mStartItem != null
236             || mPrimaryAction != null
237             || mTitleItem != null
238             || mSubtitleItem != null
239             || !mEndItems.isEmpty()
240             || mRange != null
241             || mSelection != null
242             || isDefaultSeeMore());
243   }
244 
245   /**
246    * @return whether this row represents a header or not.
247    */
getIsHeader()248   public boolean getIsHeader() {
249     return mIsHeader;
250   }
251 
252   /** Sets whether this row represents a header or not. */
setIsHeader(boolean isHeader)253   public void setIsHeader(boolean isHeader) {
254     mIsHeader = isHeader;
255   }
256 
257   /**
258    * @return the {@link SliceItem} representing the range in the row; can be null.
259    */
260   @Nullable
getRange()261   public SliceItem getRange() {
262     return mRange;
263   }
264 
265   /**
266    * @return the {@link SliceItem} representing the selection in the row; can be null.
267    */
268   @Nullable
getSelection()269   public SliceItem getSelection() {
270     return mSelection;
271   }
272 
273   /**
274    * @return the {@link SliceItem} for the icon to use for the input range thumb drawable.
275    */
276   @Nullable
getInputRangeThumb()277   public SliceItem getInputRangeThumb() {
278     if (mRange != null) {
279       List<SliceItem> items = mRange.getSlice().getItems();
280       for (int i = 0; i < items.size(); i++) {
281         if (FORMAT_IMAGE.equals(items.get(i).getFormat())) {
282           return items.get(i);
283         }
284       }
285     }
286     return null;
287   }
288 
289   /**
290    * @return the {@link SliceItem} used for the main intent for this row; can be null.
291    */
292   @Nullable
getPrimaryAction()293   public SliceItem getPrimaryAction() {
294     return mPrimaryAction;
295   }
296 
297   /**
298    * @return the {@link SliceItem} to display at the start of this row; can be null.
299    */
300   @Nullable
getStartItem()301   public SliceItem getStartItem() {
302     return mIsHeader && !mShowTitleItems ? null : mStartItem;
303   }
304 
305   /**
306    * @return the {@link SliceItem} representing the title text for this row; can be null.
307    */
308   @Nullable
getTitleItem()309   public SliceItem getTitleItem() {
310     return mTitleItem;
311   }
312 
313   /**
314    * @return the {@link SliceItem} representing the subtitle text for this row; can be null.
315    */
316   @Nullable
getSubtitleItem()317   public SliceItem getSubtitleItem() {
318     return mSubtitleItem;
319   }
320 
321   @Nullable
getSummaryItem()322   public SliceItem getSummaryItem() {
323     return mSummaryItem == null ? mSubtitleItem : mSummaryItem;
324   }
325 
326   /**
327    * @return the list of {@link SliceItem} that can be shown as items at the end of the row.
328    */
getEndItems()329   public List<SliceItem> getEndItems() {
330     return mEndItems;
331   }
332 
333   /**
334    * @return a list of toggles associated with this row.
335    */
getToggleItems()336   public List<SliceAction> getToggleItems() {
337     return mToggleItems;
338   }
339 
340   /**
341    * @return the number of lines of text contained in this row.
342    */
getLineCount()343   public int getLineCount() {
344     return mLineCount;
345   }
346 
347   /** */
348   // @RestrictTo(RestrictTo.Scope.LIBRARY)
349   @Override
getHeight(SliceStyle style, SliceViewPolicy policy)350   public int getHeight(SliceStyle style, SliceViewPolicy policy) {
351     return style.getRowHeight(this, policy);
352   }
353 
354   /**
355    * @return whether this row content represents a default see more item.
356    */
isDefaultSeeMore()357   public boolean isDefaultSeeMore() {
358     return FORMAT_ACTION.equals(mSliceItem.getFormat())
359         && mSliceItem.getSlice().hasHint(HINT_SEE_MORE)
360         && mSliceItem.getSlice().getItems().isEmpty();
361   }
362 
363   /** Set whether this row content needs to show the title items on the start. */
showTitleItems(boolean enabled)364   public void showTitleItems(boolean enabled) {
365     mShowTitleItems = enabled;
366   }
367 
368   /**
369    * @return whether this row content needs to show the title items on the start.
370    */
hasTitleItems()371   public boolean hasTitleItems() {
372     return mShowTitleItems;
373   }
374 
375   /** Set whether this row content needs to show the bottom divider. */
showBottomDivider(boolean enabled)376   public void showBottomDivider(boolean enabled) {
377     mShowBottomDivider = enabled;
378   }
379 
380   /**
381    * @return whether this row content needs to show the bottom divider.
382    */
hasBottomDivider()383   public boolean hasBottomDivider() {
384     return mShowBottomDivider;
385   }
386 
387   /** Set whether this row content needs to show the action divider. */
showActionDivider(boolean enabled)388   public void showActionDivider(boolean enabled) {
389     mShowActionDivider = enabled;
390   }
391 
392   /**
393    * @return whether this row content needs to show the action divider.
394    */
hasActionDivider()395   public boolean hasActionDivider() {
396     return mShowActionDivider;
397   }
398 
399   /**
400    * @return whether this slice item has text that is present or will be present (i.e. loading).
401    */
hasText(SliceItem textSlice)402   private static boolean hasText(SliceItem textSlice) {
403     return textSlice != null
404         && (textSlice.hasHint(HINT_PARTIAL) || !TextUtils.isEmpty(textSlice.getText()));
405   }
406 
407   /**
408    * @return whether this is a valid item to use to populate a row of content.
409    */
isValidRow(SliceItem rowSlice)410   private static boolean isValidRow(SliceItem rowSlice) {
411     if (rowSlice == null) {
412       return false;
413     }
414     // Must be slice or action
415     if (FORMAT_SLICE.equals(rowSlice.getFormat()) || FORMAT_ACTION.equals(rowSlice.getFormat())) {
416       List<SliceItem> rowItems = rowSlice.getSlice().getItems();
417       // Special case: default see more just has an action but no other items
418       if (rowSlice.hasHint(HINT_SEE_MORE) && rowItems.isEmpty()) {
419         return true;
420       }
421       // Must have at least one legitimate child
422       for (int i = 0; i < rowItems.size(); i++) {
423         if (isValidRowContent(rowSlice, rowItems.get(i))) {
424           return true;
425         }
426       }
427     }
428     return false;
429   }
430 
431   /**
432    * @return list of {@link SliceItem}s that are valid to display in a row according to {@link
433    *     #isValidRowContent(SliceItem, SliceItem)}.
434    */
filterInvalidItems(SliceItem rowSlice)435   private static ArrayList<SliceItem> filterInvalidItems(SliceItem rowSlice) {
436     ArrayList<SliceItem> filteredList = new ArrayList<>();
437     for (SliceItem i : rowSlice.getSlice().getItems()) {
438       if (isValidRowContent(rowSlice, i)) {
439         filteredList.add(i);
440       }
441     }
442     return filteredList;
443   }
444 
445   /**
446    * @return whether this item is valid content to visibly appear in a row.
447    */
isValidRowContent(SliceItem slice, SliceItem item)448   private static boolean isValidRowContent(SliceItem slice, SliceItem item) {
449     // XXX: This is fragile -- new subtypes may be erroneously displayed by old clients, since
450     // this is effectively a blocklist, not an allowlist. I'm not sure if SELECTION_OPTION_KEY
451     // needs to be here, but better safe than sorry.
452     if (item.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED, HINT_HORIZONTAL)
453         || SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())
454         || SUBTYPE_SELECTION_OPTION_KEY.equals(item.getSubType())
455         || SUBTYPE_SELECTION_OPTION_VALUE.equals(item.getSubType())) {
456       return false;
457     }
458     final String itemFormat = item.getFormat();
459     // XXX: This is confusing -- the FORMAT_INTs in a SUBTYPE_RANGE Slice aren't visible
460     // themselves, they're just used to inform rendering.
461     return FORMAT_IMAGE.equals(itemFormat)
462         || FORMAT_TEXT.equals(itemFormat)
463         || FORMAT_LONG.equals(itemFormat)
464         || FORMAT_ACTION.equals(itemFormat)
465         || FORMAT_REMOTE_INPUT.equals(itemFormat)
466         || FORMAT_SLICE.equals(itemFormat)
467         || (FORMAT_INT.equals(itemFormat) && SUBTYPE_RANGE.equals(slice.getSubType()));
468   }
469 }
470