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 androidx.slice.widget; 18 19 import static android.app.slice.Slice.HINT_HORIZONTAL; 20 import static android.app.slice.Slice.HINT_PARTIAL; 21 import static android.app.slice.Slice.HINT_SEE_MORE; 22 import static android.app.slice.Slice.HINT_SHORTCUT; 23 import static android.app.slice.Slice.HINT_SUMMARY; 24 import static android.app.slice.Slice.HINT_TITLE; 25 import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION; 26 import static android.app.slice.Slice.SUBTYPE_RANGE; 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_INT; 30 import static android.app.slice.SliceItem.FORMAT_LONG; 31 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT; 32 import static android.app.slice.SliceItem.FORMAT_SLICE; 33 import static android.app.slice.SliceItem.FORMAT_TEXT; 34 35 import static androidx.slice.core.SliceHints.HINT_KEYWORDS; 36 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED; 37 import static androidx.slice.core.SliceHints.HINT_TTL; 38 39 import android.content.Context; 40 import android.text.TextUtils; 41 import android.util.Log; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.Nullable; 45 import androidx.annotation.RestrictTo; 46 import androidx.slice.SliceItem; 47 import androidx.slice.core.SliceAction; 48 import androidx.slice.core.SliceActionImpl; 49 import androidx.slice.core.SliceQuery; 50 import androidx.slice.view.R; 51 52 import java.util.ArrayList; 53 import java.util.List; 54 55 /** 56 * Extracts information required to present content in a row format from a slice. 57 * @hide 58 */ 59 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 60 public class RowContent { 61 private static final String TAG = "RowContent"; 62 63 private SliceItem mPrimaryAction; 64 private SliceItem mRowSlice; 65 private SliceItem mStartItem; 66 private SliceItem mTitleItem; 67 private SliceItem mSubtitleItem; 68 private SliceItem mSummaryItem; 69 private ArrayList<SliceItem> mEndItems = new ArrayList<>(); 70 private ArrayList<SliceAction> mToggleItems = new ArrayList<>(); 71 private SliceItem mRange; 72 private SliceItem mContentDescr; 73 private boolean mEndItemsContainAction; 74 private boolean mIsHeader; 75 private int mLineCount = 0; 76 private int mMaxHeight; 77 private int mMinHeight; 78 private int mRangeHeight; 79 RowContent(Context context, SliceItem rowSlice, boolean isHeader)80 public RowContent(Context context, SliceItem rowSlice, boolean isHeader) { 81 populate(rowSlice, isHeader); 82 mMaxHeight = context.getResources().getDimensionPixelSize(R.dimen.abc_slice_row_max_height); 83 mMinHeight = context.getResources().getDimensionPixelSize(R.dimen.abc_slice_row_min_height); 84 mRangeHeight = context.getResources().getDimensionPixelSize( 85 R.dimen.abc_slice_row_range_height); 86 } 87 88 /** 89 * @return whether this row has content that is valid to display. 90 */ populate(SliceItem rowSlice, boolean isHeader)91 private boolean populate(SliceItem rowSlice, boolean isHeader) { 92 mIsHeader = isHeader; 93 mRowSlice = rowSlice; 94 if (!isValidRow(rowSlice)) { 95 Log.w(TAG, "Provided SliceItem is invalid for RowContent"); 96 return false; 97 } 98 determineStartAndPrimaryAction(rowSlice); 99 100 mContentDescr = SliceQuery.findSubtype(rowSlice, FORMAT_TEXT, SUBTYPE_CONTENT_DESCRIPTION); 101 102 // Filter anything not viable for displaying in a row 103 ArrayList<SliceItem> rowItems = filterInvalidItems(rowSlice); 104 // If we've only got one item that's a slice / action use those items instead 105 if (rowItems.size() == 1 && (FORMAT_ACTION.equals(rowItems.get(0).getFormat()) 106 || FORMAT_SLICE.equals(rowItems.get(0).getFormat())) 107 && !rowItems.get(0).hasAnyHints(HINT_SHORTCUT, HINT_TITLE)) { 108 if (isValidRow(rowItems.get(0))) { 109 rowSlice = rowItems.get(0); 110 rowItems = filterInvalidItems(rowSlice); 111 } 112 } 113 if (SUBTYPE_RANGE.equals(rowSlice.getSubType())) { 114 mRange = rowSlice; 115 } 116 if (rowItems.size() > 0) { 117 // Remove the things we already know about 118 if (mStartItem != null) { 119 rowItems.remove(mStartItem); 120 } 121 if (mPrimaryAction != null) { 122 rowItems.remove(mPrimaryAction); 123 } 124 125 // Text + end items 126 ArrayList<SliceItem> endItems = new ArrayList<>(); 127 for (int i = 0; i < rowItems.size(); i++) { 128 final SliceItem item = rowItems.get(i); 129 if (FORMAT_TEXT.equals(item.getFormat())) { 130 if ((mTitleItem == null || !mTitleItem.hasHint(HINT_TITLE)) 131 && item.hasHint(HINT_TITLE) && !item.hasHint(HINT_SUMMARY)) { 132 mTitleItem = item; 133 } else if (mSubtitleItem == null && !item.hasHint(HINT_SUMMARY)) { 134 mSubtitleItem = item; 135 } else if (mSummaryItem == null && item.hasHint(HINT_SUMMARY)) { 136 mSummaryItem = item; 137 } 138 } else { 139 endItems.add(item); 140 } 141 } 142 if (hasText(mTitleItem)) { 143 mLineCount++; 144 } 145 if (hasText(mSubtitleItem)) { 146 mLineCount++; 147 } 148 // Special rules for end items: only one timestamp 149 boolean hasTimestamp = mStartItem != null 150 && FORMAT_LONG.equals(mStartItem.getFormat()); 151 for (int i = 0; i < endItems.size(); i++) { 152 final SliceItem item = endItems.get(i); 153 boolean isAction = SliceQuery.find(item, FORMAT_ACTION) != null; 154 if (FORMAT_LONG.equals(item.getFormat())) { 155 if (!hasTimestamp) { 156 hasTimestamp = true; 157 mEndItems.add(item); 158 } 159 } else { 160 processContent(item, isAction); 161 } 162 } 163 } 164 return isValid(); 165 } 166 processContent(@onNull SliceItem item, boolean isAction)167 private void processContent(@NonNull SliceItem item, boolean isAction) { 168 if (isAction) { 169 SliceAction ac = new SliceActionImpl(item); 170 if (ac.isToggle()) { 171 mToggleItems.add(ac); 172 } 173 } 174 mEndItems.add(item); 175 mEndItemsContainAction |= isAction; 176 } 177 178 /** 179 * Sets the {@link #getPrimaryAction()} and {@link #getStartItem()} for this row. 180 */ determineStartAndPrimaryAction(@onNull SliceItem rowSlice)181 private void determineStartAndPrimaryAction(@NonNull SliceItem rowSlice) { 182 List<SliceItem> possibleStartItems = SliceQuery.findAll(rowSlice, null, HINT_TITLE, null); 183 if (possibleStartItems.size() > 0) { 184 // The start item will be at position 0 if it exists 185 String format = possibleStartItems.get(0).getFormat(); 186 if ((FORMAT_ACTION.equals(format) 187 && SliceQuery.find(possibleStartItems.get(0), FORMAT_IMAGE) != null) 188 || FORMAT_SLICE.equals(format) 189 || FORMAT_LONG.equals(format) 190 || FORMAT_IMAGE.equals(format)) { 191 mStartItem = possibleStartItems.get(0); 192 } 193 } 194 195 String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE}; 196 List<SliceItem> possiblePrimaries = SliceQuery.findAll(rowSlice, FORMAT_SLICE, hints, null); 197 if (possiblePrimaries.isEmpty() && FORMAT_ACTION.equals(rowSlice.getFormat()) 198 && rowSlice.getSlice().getItems().size() == 1) { 199 mPrimaryAction = rowSlice; 200 } else if (mStartItem != null && possiblePrimaries.size() > 1 201 && possiblePrimaries.get(0) == mStartItem) { 202 // Next item is the primary action 203 mPrimaryAction = possiblePrimaries.get(1); 204 } else if (possiblePrimaries.size() > 0) { 205 mPrimaryAction = possiblePrimaries.get(0); 206 } 207 } 208 209 /** 210 * @return the {@link SliceItem} used to populate this row. 211 */ 212 @NonNull getSlice()213 public SliceItem getSlice() { 214 return mRowSlice; 215 } 216 217 /** 218 * @return the {@link SliceItem} representing the range in the row; can be null. 219 */ 220 @Nullable getRange()221 public SliceItem getRange() { 222 return mRange; 223 } 224 225 /** 226 * @return the {@link SliceItem} for the icon to use for the input range thumb drawable. 227 */ 228 @Nullable getInputRangeThumb()229 public SliceItem getInputRangeThumb() { 230 if (mRange != null) { 231 List<SliceItem> items = mRange.getSlice().getItems(); 232 for (int i = 0; i < items.size(); i++) { 233 if (FORMAT_IMAGE.equals(items.get(i).getFormat())) { 234 return items.get(i); 235 } 236 } 237 } 238 return null; 239 } 240 241 /** 242 * @return the {@link SliceItem} used for the main intent for this row; can be null. 243 */ 244 @Nullable getPrimaryAction()245 public SliceItem getPrimaryAction() { 246 return mPrimaryAction; 247 } 248 249 /** 250 * @return the {@link SliceItem} to display at the start of this row; can be null. 251 */ 252 @Nullable getStartItem()253 public SliceItem getStartItem() { 254 return mIsHeader ? null : mStartItem; 255 } 256 257 /** 258 * @return the {@link SliceItem} representing the title text for this row; can be null. 259 */ 260 @Nullable getTitleItem()261 public SliceItem getTitleItem() { 262 return mTitleItem; 263 } 264 265 /** 266 * @return the {@link SliceItem} representing the subtitle text for this row; can be null. 267 */ 268 @Nullable getSubtitleItem()269 public SliceItem getSubtitleItem() { 270 return mSubtitleItem; 271 } 272 273 @Nullable getSummaryItem()274 public SliceItem getSummaryItem() { 275 return mSummaryItem == null ? mSubtitleItem : mSummaryItem; 276 } 277 278 /** 279 * @return the list of {@link SliceItem} that can be shown as items at the end of the row. 280 */ getEndItems()281 public ArrayList<SliceItem> getEndItems() { 282 return mEndItems; 283 } 284 285 /** 286 * @return a list of toggles associated with this row. 287 */ getToggleItems()288 public ArrayList<SliceAction> getToggleItems() { 289 return mToggleItems; 290 } 291 292 /** 293 * @return the content description to use for this row. 294 */ 295 @Nullable getContentDescription()296 public CharSequence getContentDescription() { 297 return mContentDescr != null ? mContentDescr.getText() : null; 298 } 299 300 /** 301 * @return whether {@link #getEndItems()} contains a SliceItem with FORMAT_SLICE, HINT_SHORTCUT 302 */ endItemsContainAction()303 public boolean endItemsContainAction() { 304 return mEndItemsContainAction; 305 } 306 307 /** 308 * @return the number of lines of text contained in this row. 309 */ getLineCount()310 public int getLineCount() { 311 return mLineCount; 312 } 313 314 /** 315 * @return the height to display a row at when it is used as a small template. 316 */ getSmallHeight()317 public int getSmallHeight() { 318 return getRange() != null 319 ? getActualHeight() 320 : mMaxHeight; 321 } 322 323 /** 324 * @return the height the content in this template requires to be displayed. 325 */ getActualHeight()326 public int getActualHeight() { 327 if (!isValid()) { 328 return 0; 329 } 330 int rowHeight = (getLineCount() > 1 || mIsHeader) ? mMaxHeight : mMinHeight; 331 if (getRange() != null) { 332 if (getLineCount() > 0) { 333 rowHeight += mRangeHeight; 334 } else { 335 rowHeight = mIsHeader ? mMaxHeight : mRangeHeight; 336 } 337 } 338 return rowHeight; 339 } 340 hasText(SliceItem textSlice)341 private static boolean hasText(SliceItem textSlice) { 342 return textSlice != null 343 && (textSlice.hasHint(HINT_PARTIAL) 344 || !TextUtils.isEmpty(textSlice.getText())); 345 } 346 347 /** 348 * @return whether this row content represents a default see more item. 349 */ isDefaultSeeMore()350 public boolean isDefaultSeeMore() { 351 return FORMAT_ACTION.equals(mRowSlice.getFormat()) 352 && mRowSlice.getSlice().hasHint(HINT_SEE_MORE) 353 && mRowSlice.getSlice().getItems().isEmpty(); 354 } 355 356 /** 357 * @return whether this row has content that is valid to display. 358 */ isValid()359 public boolean isValid() { 360 return mStartItem != null 361 || mPrimaryAction != null 362 || mTitleItem != null 363 || mSubtitleItem != null 364 || mEndItems.size() > 0 365 || mRange != null 366 || isDefaultSeeMore(); 367 } 368 369 /** 370 * @return whether this is a valid item to use to populate a row of content. 371 */ isValidRow(SliceItem rowSlice)372 private static boolean isValidRow(SliceItem rowSlice) { 373 if (rowSlice == null) { 374 return false; 375 } 376 // Must be slice or action 377 if (FORMAT_SLICE.equals(rowSlice.getFormat()) 378 || FORMAT_ACTION.equals(rowSlice.getFormat())) { 379 List<SliceItem> rowItems = rowSlice.getSlice().getItems(); 380 // Special case: default see more just has an action but no other items 381 if (rowSlice.hasHint(HINT_SEE_MORE) && rowItems.isEmpty()) { 382 return true; 383 } 384 // Must have at least one legitimate child 385 for (int i = 0; i < rowItems.size(); i++) { 386 if (isValidRowContent(rowSlice, rowItems.get(i))) { 387 return true; 388 } 389 } 390 } 391 return false; 392 } 393 394 /** 395 * @return list of {@link SliceItem}s that are valid to display in a row according 396 * to {@link #isValidRowContent(SliceItem, SliceItem)}. 397 */ filterInvalidItems(SliceItem rowSlice)398 private static ArrayList<SliceItem> filterInvalidItems(SliceItem rowSlice) { 399 ArrayList<SliceItem> filteredList = new ArrayList<>(); 400 for (SliceItem i : rowSlice.getSlice().getItems()) { 401 if (isValidRowContent(rowSlice, i)) { 402 filteredList.add(i); 403 } 404 } 405 return filteredList; 406 } 407 408 /** 409 * @return whether this item is valid content to visibly appear in a row. 410 */ isValidRowContent(SliceItem slice, SliceItem item)411 private static boolean isValidRowContent(SliceItem slice, SliceItem item) { 412 if (item.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED, HINT_HORIZONTAL) 413 || SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) { 414 return false; 415 } 416 final String itemFormat = item.getFormat(); 417 return FORMAT_IMAGE.equals(itemFormat) 418 || FORMAT_TEXT.equals(itemFormat) 419 || FORMAT_LONG.equals(itemFormat) 420 || FORMAT_ACTION.equals(itemFormat) 421 || FORMAT_REMOTE_INPUT.equals(itemFormat) 422 || FORMAT_SLICE.equals(itemFormat) 423 || (FORMAT_INT.equals(itemFormat) && SUBTYPE_RANGE.equals(slice.getSubType())); 424 } 425 } 426