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