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_HORIZONTAL; 21 import static android.app.slice.Slice.HINT_KEYWORDS; 22 import static android.app.slice.Slice.HINT_LAST_UPDATED; 23 import static android.app.slice.Slice.HINT_LIST_ITEM; 24 import static android.app.slice.Slice.HINT_SEE_MORE; 25 import static android.app.slice.Slice.HINT_SHORTCUT; 26 import static android.app.slice.Slice.HINT_TITLE; 27 import static android.app.slice.Slice.HINT_TTL; 28 import static android.app.slice.SliceItem.FORMAT_ACTION; 29 import static android.app.slice.SliceItem.FORMAT_SLICE; 30 import static android.app.slice.SliceItem.FORMAT_TEXT; 31 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.HINT_SELECTION_OPTION; 32 import static com.android.tv.twopanelsettings.slices.compat.widget.SliceViewPolicy.MODE_SMALL; 33 34 import android.content.Context; 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 import com.android.tv.twopanelsettings.slices.compat.Slice; 38 import com.android.tv.twopanelsettings.slices.compat.SliceItem; 39 import com.android.tv.twopanelsettings.slices.compat.SliceMetadata; 40 import com.android.tv.twopanelsettings.slices.compat.core.SliceAction; 41 import com.android.tv.twopanelsettings.slices.compat.core.SliceActionImpl; 42 import com.android.tv.twopanelsettings.slices.compat.core.SliceQuery; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.List; 46 47 /** Extracts information required to present content in a list format from a slice. */ 48 // @RestrictTo(RestrictTo.Scope.LIBRARY) 49 // @Deprecated // Supported for TV 50 public class ListContent extends SliceContent { 51 52 private SliceAction mPrimaryAction; 53 private RowContent mHeaderContent; 54 private RowContent mSeeMoreContent; 55 private ArrayList<SliceContent> mRowItems = new ArrayList<>(); 56 private List<SliceAction> mSliceActions; 57 ListContent(@onNull Slice slice)58 public ListContent(@NonNull Slice slice) { 59 super(slice); 60 if (mSliceItem == null) { 61 return; 62 } 63 populate(slice); 64 } 65 66 // @Deprecated // Supported for TV ListContent(Context context, @NonNull Slice slice)67 public ListContent(Context context, @NonNull Slice slice) { 68 super(slice); 69 if (mSliceItem == null) { 70 return; 71 } 72 populate(slice); 73 } 74 populate(Slice slice)75 private void populate(Slice slice) { 76 if (slice == null) { 77 return; 78 } 79 mSliceActions = SliceMetadata.getSliceActions(slice); 80 final SliceItem headerItem = findHeaderItem(slice); 81 if (headerItem != null) { 82 mHeaderContent = new RowContent(headerItem, 0); 83 mRowItems.add(mHeaderContent); 84 } 85 final SliceItem seeMoreItem = getSeeMoreItem(slice); 86 if (seeMoreItem != null) { 87 mSeeMoreContent = new RowContent(seeMoreItem, -1); 88 } 89 90 // Filter + create row items 91 List<SliceItem> children = slice.getItems(); 92 for (int i = 0; i < children.size(); i++) { 93 final SliceItem child = children.get(i); 94 final String format = child.getFormat(); 95 boolean isNonRowContent = 96 child.hasAnyHints( 97 HINT_ACTIONS, HINT_SEE_MORE, HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED); 98 if (!isNonRowContent && (FORMAT_ACTION.equals(format) || FORMAT_SLICE.equals(format))) { 99 if (mHeaderContent == null && !child.hasHint(HINT_LIST_ITEM)) { 100 mHeaderContent = new RowContent(child, 0); 101 mRowItems.add(0, mHeaderContent); 102 } else if (child.hasHint(HINT_LIST_ITEM)) { 103 if (child.hasHint(HINT_HORIZONTAL)) { 104 mRowItems.add(new GridContent(child, i)); 105 } else { 106 mRowItems.add(new RowContent(child, i)); 107 } 108 } 109 } 110 } 111 // Ensure we have something for the header -- use first row 112 if (mHeaderContent == null && mRowItems.size() >= 1) { 113 // We enforce RowContent has first item on builder side; if that changes this 114 // could be an issue 115 mHeaderContent = (RowContent) mRowItems.get(0); 116 mHeaderContent.setIsHeader(true); 117 } 118 if (!mRowItems.isEmpty() && mRowItems.get(mRowItems.size() - 1) instanceof GridContent) { 119 // Grid item is the last item, note that. 120 ((GridContent) mRowItems.get(mRowItems.size() - 1)).setIsLastIndex(true); 121 } 122 mPrimaryAction = findPrimaryAction(); 123 } 124 125 @Override getHeight(SliceStyle style, SliceViewPolicy policy)126 public int getHeight(SliceStyle style, SliceViewPolicy policy) { 127 return style.getListHeight(this, policy); 128 } 129 130 /** 131 * Gets the row items to display in this list. 132 * 133 * @param availableHeight the available height for displaying the list. 134 * @param style the style info to use when determining row items to return. 135 * @param policy the policy info (scrolling, mode) to use when determining row items to return. 136 * @return the row items that should be shown based on the provided configuration. 137 */ getRowItems( int availableHeight, SliceStyle style, SliceViewPolicy policy)138 public DisplayedListItems getRowItems( 139 int availableHeight, SliceStyle style, SliceViewPolicy policy) { 140 if (policy.getMode() == MODE_SMALL) { 141 return new DisplayedListItems( 142 new ArrayList<>(Arrays.asList(getHeader())), /* hiddenItemCount= */ mRowItems.size() - 1); 143 } else if (!policy.isScrollable() && availableHeight > 0) { 144 return style.getListItemsForNonScrollingList(this, availableHeight, policy); 145 } 146 return new DisplayedListItems(style.getListItemsToDisplay(this), /* hiddenItemCount= */ 0); 147 } 148 149 /** 150 * @return whether this list has content that is valid to display. 151 */ 152 @Override isValid()153 public boolean isValid() { 154 return super.isValid() && !mRowItems.isEmpty(); 155 } 156 157 @Nullable getHeader()158 public RowContent getHeader() { 159 return mHeaderContent; 160 } 161 162 @Nullable getSliceActions()163 public List<SliceAction> getSliceActions() { 164 return mSliceActions; 165 } 166 167 @NonNull getRowItems()168 public ArrayList<SliceContent> getRowItems() { 169 return mRowItems; 170 } 171 getSeeMoreItem()172 public SliceContent getSeeMoreItem() { 173 return mSeeMoreContent; 174 } 175 176 /** 177 * @return the type of template that the header represents. 178 */ getHeaderTemplateType()179 public int getHeaderTemplateType() { 180 return getRowType(mHeaderContent, true, mSliceActions); 181 } 182 183 @Override 184 @Nullable getShortcut(@ullable Context context)185 public SliceAction getShortcut(@Nullable Context context) { 186 return mPrimaryAction != null ? mPrimaryAction : super.getShortcut(context); 187 } 188 189 /** Whether the first row should show title items on the start. */ showTitleItems(boolean enabled)190 public void showTitleItems(boolean enabled) { 191 if (mHeaderContent != null) { 192 mHeaderContent.showTitleItems(enabled); 193 } 194 } 195 196 /** Whether the header row should show the bottom divider. */ showHeaderDivider(boolean enabled)197 public void showHeaderDivider(boolean enabled) { 198 if (mHeaderContent != null && mRowItems.size() > 1) { 199 mHeaderContent.showBottomDivider(enabled); 200 } 201 } 202 203 /** Whether all the row contents should show action dividers. */ showActionDividers(boolean enabled)204 public void showActionDividers(boolean enabled) { 205 for (SliceContent item : mRowItems) { 206 if (item instanceof RowContent) { 207 ((RowContent) item).showActionDivider(enabled); 208 } 209 } 210 } 211 212 /** 213 * @return suitable action to use for a tap on the slice template or for the shortcut. 214 */ 215 @Nullable findPrimaryAction()216 private SliceAction findPrimaryAction() { 217 SliceItem action = null; 218 if (mHeaderContent != null) { 219 action = mHeaderContent.getPrimaryAction(); 220 } 221 if (action == null) { 222 String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE}; 223 action = SliceQuery.find(mSliceItem, FORMAT_ACTION, hints, null); 224 } 225 if (action == null) { 226 action = SliceQuery.find(mSliceItem, FORMAT_ACTION, (String) null, null); 227 } 228 return action != null ? new SliceActionImpl(action) : null; 229 } 230 231 /** 232 * The type of template that the provided row item represents. 233 * 234 * @param content the content to determine the template type of. 235 * @param isHeader whether this row item is used as a header. 236 * @param actions the actions associated with this slice, only matter if this row is the header. 237 * @return the type of template the provided row item represents. 238 */ getRowType(SliceContent content, boolean isHeader, List<SliceAction> actions)239 public static int getRowType(SliceContent content, boolean isHeader, List<SliceAction> actions) { 240 if (content != null) { 241 if (content instanceof GridContent) { 242 return EventInfo.ROW_TYPE_GRID; 243 } else { 244 RowContent rc = (RowContent) content; 245 SliceItem actionItem = rc.getPrimaryAction(); 246 SliceAction primaryAction = null; 247 if (actionItem != null) { 248 primaryAction = new SliceActionImpl(actionItem); 249 } 250 if (rc.getRange() != null) { 251 return FORMAT_ACTION.equals(rc.getRange().getFormat()) 252 ? EventInfo.ROW_TYPE_SLIDER 253 : EventInfo.ROW_TYPE_PROGRESS; 254 } else if (rc.getSelection() != null) { 255 return EventInfo.ROW_TYPE_SELECTION; 256 } else if (primaryAction != null && primaryAction.isToggle()) { 257 return EventInfo.ROW_TYPE_TOGGLE; 258 } else if (isHeader && actions != null) { 259 for (int i = 0; i < actions.size(); i++) { 260 if (actions.get(i).isToggle()) { 261 return EventInfo.ROW_TYPE_TOGGLE; 262 } 263 } 264 return EventInfo.ROW_TYPE_LIST; 265 } else { 266 return !rc.getToggleItems().isEmpty() 267 ? EventInfo.ROW_TYPE_TOGGLE 268 : EventInfo.ROW_TYPE_LIST; 269 } 270 } 271 } 272 return EventInfo.ROW_TYPE_LIST; 273 } 274 275 /** 276 * @return the total height of all the rows contained in the provided list. 277 */ getListHeight( List<SliceContent> listItems, SliceStyle style, SliceViewPolicy policy)278 public static int getListHeight( 279 List<SliceContent> listItems, SliceStyle style, SliceViewPolicy policy) { 280 return style.getListItemsHeight(listItems, policy); 281 } 282 283 @Nullable findHeaderItem(@onNull Slice slice)284 private static SliceItem findHeaderItem(@NonNull Slice slice) { 285 // See if header is specified 286 String[] nonHints = 287 new String[] { 288 HINT_LIST_ITEM, 289 HINT_SHORTCUT, 290 HINT_ACTIONS, 291 HINT_KEYWORDS, 292 HINT_TTL, 293 HINT_LAST_UPDATED, 294 HINT_HORIZONTAL, 295 HINT_SELECTION_OPTION 296 }; 297 SliceItem header = SliceQuery.find(slice, FORMAT_SLICE, null, nonHints); 298 if (header != null && isValidHeader(header)) { 299 return header; 300 } 301 return null; 302 } 303 304 @Nullable getSeeMoreItem(@onNull Slice slice)305 private static SliceItem getSeeMoreItem(@NonNull Slice slice) { 306 SliceItem item = 307 SliceQuery.findTopLevelItem(slice, null, null, new String[] {HINT_SEE_MORE}, null); 308 if (item != null) { 309 if (FORMAT_SLICE.equals(item.getFormat())) { 310 List<SliceItem> items = item.getSlice().getItems(); 311 if (items.size() == 1 && FORMAT_ACTION.equals(items.get(0).getFormat())) { 312 return items.get(0); 313 } 314 return item; 315 } 316 } 317 return null; 318 } 319 320 /** 321 * @return whether the provided slice item is a valid header. 322 */ isValidHeader(SliceItem sliceItem)323 private static boolean isValidHeader(SliceItem sliceItem) { 324 if (FORMAT_SLICE.equals(sliceItem.getFormat()) 325 && !sliceItem.hasAnyHints(HINT_ACTIONS, HINT_KEYWORDS, HINT_SEE_MORE)) { 326 // Minimum valid header is a slice with text 327 SliceItem item = SliceQuery.find(sliceItem, FORMAT_TEXT, (String) null, null); 328 return item != null; 329 } 330 return false; 331 } 332 } 333