1 /* 2 * Copyright 2018 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; 18 19 import static android.app.slice.Slice.EXTRA_RANGE_VALUE; 20 import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; 21 import static android.app.slice.Slice.HINT_ACTIONS; 22 import static android.app.slice.Slice.HINT_ERROR; 23 import static android.app.slice.Slice.HINT_KEYWORDS; 24 import static android.app.slice.Slice.HINT_LAST_UPDATED; 25 import static android.app.slice.Slice.HINT_LIST_ITEM; 26 import static android.app.slice.Slice.HINT_PARTIAL; 27 import static android.app.slice.Slice.HINT_PERMISSION_REQUEST; 28 import static android.app.slice.Slice.HINT_SHORTCUT; 29 import static android.app.slice.Slice.HINT_TTL; 30 import static android.app.slice.Slice.SUBTYPE_MAX; 31 import static android.app.slice.Slice.SUBTYPE_VALUE; 32 import static android.app.slice.SliceItem.FORMAT_ACTION; 33 import static android.app.slice.SliceItem.FORMAT_BUNDLE; 34 import static android.app.slice.SliceItem.FORMAT_INT; 35 import static android.app.slice.SliceItem.FORMAT_LONG; 36 import static android.app.slice.SliceItem.FORMAT_SLICE; 37 import static android.app.slice.SliceItem.FORMAT_TEXT; 38 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.HINT_CACHED; 39 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.SUBTYPE_HOST_EXTRAS; 40 import static com.android.tv.twopanelsettings.slices.compat.core.SliceHints.SUBTYPE_MIN; 41 import static com.android.tv.twopanelsettings.slices.compat.widget.EventInfo.ROW_TYPE_PROGRESS; 42 import static com.android.tv.twopanelsettings.slices.compat.widget.EventInfo.ROW_TYPE_SLIDER; 43 44 import android.app.PendingIntent; 45 import android.content.Context; 46 import android.content.Intent; 47 import android.os.Bundle; 48 import android.os.Parcelable; 49 import android.text.TextUtils; 50 import androidx.annotation.IntDef; 51 import androidx.annotation.NonNull; 52 import androidx.annotation.Nullable; 53 import androidx.core.math.MathUtils; 54 import androidx.core.util.Pair; 55 import com.android.tv.twopanelsettings.slices.compat.core.SliceAction; 56 import com.android.tv.twopanelsettings.slices.compat.core.SliceActionImpl; 57 import com.android.tv.twopanelsettings.slices.compat.core.SliceHints; 58 import com.android.tv.twopanelsettings.slices.compat.core.SliceQuery; 59 import com.android.tv.twopanelsettings.slices.compat.widget.EventInfo; 60 import com.android.tv.twopanelsettings.slices.compat.widget.ListContent; 61 import com.android.tv.twopanelsettings.slices.compat.widget.RowContent; 62 import java.lang.annotation.Retention; 63 import java.lang.annotation.RetentionPolicy; 64 import java.util.ArrayList; 65 import java.util.List; 66 67 /** 68 * Utility class to parse a {@link Slice} and provide access to information around its contents. 69 * 70 * <p>Slice framework has been deprecated, it will not receive any updates moving forward. If you 71 * are looking for a framework that handles communication across apps, consider using {@link 72 * android.app.appsearch.AppSearchManager}. 73 */ 74 // @Deprecated // Supported for TV 75 public class SliceMetadata { 76 77 /** */ 78 // @RestrictTo(RestrictTo.Scope.LIBRARY) 79 @IntDef({LOADED_NONE, LOADED_PARTIAL, LOADED_ALL}) 80 @Retention(RetentionPolicy.SOURCE) 81 public @interface SliceLoadingState {} 82 83 /** Indicates this slice is empty and waiting for content to be loaded. */ 84 public static final int LOADED_NONE = 0; 85 86 /** Indicates this slice has some content but is waiting for other content to be loaded. */ 87 public static final int LOADED_PARTIAL = 1; 88 89 /** Indicates this slice has fully loaded and is not waiting for other content. */ 90 public static final int LOADED_ALL = 2; 91 92 @NonNull private Slice mSlice; 93 @Nullable private Context mContext; 94 private long mExpiry; 95 private long mLastUpdated; 96 private ListContent mListContent; 97 private RowContent mHeaderContent; 98 private SliceAction mPrimaryAction; 99 private List<SliceAction> mSliceActions; 100 private @EventInfo.SliceRowType int mTemplateType; 101 private final Bundle mHostExtras; 102 103 /** 104 * Create a SliceMetadata object to provide access to some information around the slice and its 105 * contents. 106 * 107 * @param context the context to use for the slice. 108 * @param slice the slice to extract metadata from. 109 * @return the metadata associated with the provided slice. 110 */ 111 @NonNull from(@ullable Context context, @NonNull Slice slice)112 public static SliceMetadata from(@Nullable Context context, @NonNull Slice slice) { 113 return new SliceMetadata(context, slice); 114 } 115 116 /** 117 * Create a SliceMetadata object to provide access to some information around the slice and its 118 * contents. 119 * 120 * @param context the context to use for the slice. 121 * @param slice the slice to extract metadata from. 122 */ SliceMetadata(@ullable Context context, @NonNull Slice slice)123 private SliceMetadata(@Nullable Context context, @NonNull Slice slice) { 124 mSlice = slice; 125 mContext = context; 126 SliceItem ttlItem = SliceQuery.find(slice, FORMAT_LONG, HINT_TTL, null); 127 if (ttlItem != null) { 128 mExpiry = ttlItem.getLong(); 129 } 130 SliceItem updatedItem = SliceQuery.find(slice, FORMAT_LONG, HINT_LAST_UPDATED, null); 131 if (updatedItem != null) { 132 mLastUpdated = updatedItem.getLong(); 133 } 134 SliceItem hostExtrasItem = SliceQuery.findSubtype(slice, FORMAT_BUNDLE, SUBTYPE_HOST_EXTRAS); 135 if (hostExtrasItem != null && hostExtrasItem.mObj instanceof Bundle) { 136 mHostExtras = (Bundle) hostExtrasItem.mObj; 137 } else { 138 mHostExtras = Bundle.EMPTY; 139 } 140 mListContent = new ListContent(slice); 141 mHeaderContent = mListContent.getHeader(); 142 mTemplateType = mListContent.getHeaderTemplateType(); 143 mPrimaryAction = mListContent.getShortcut(mContext); 144 mSliceActions = mListContent.getSliceActions(); 145 if (mSliceActions == null 146 && mHeaderContent != null 147 && SliceQuery.hasHints(mHeaderContent.getSliceItem(), HINT_LIST_ITEM)) { 148 // It's not a real header, check it for end items. 149 List<SliceItem> items = mHeaderContent.getEndItems(); 150 List<SliceAction> actions = new ArrayList<>(); 151 for (int i = 0; i < items.size(); i++) { 152 if (SliceQuery.find(items.get(i), FORMAT_ACTION) != null) { 153 actions.add(new SliceActionImpl(items.get(i))); 154 } 155 } 156 if (!actions.isEmpty()) { 157 mSliceActions = actions; 158 } 159 } 160 } 161 162 /** 163 * @return the title associated with this slice, if it exists. 164 */ 165 @Nullable getTitle()166 public CharSequence getTitle() { 167 CharSequence title = null; 168 if (mHeaderContent != null && mHeaderContent.getTitleItem() != null) { 169 title = mHeaderContent.getTitleItem().getText(); 170 } 171 if (TextUtils.isEmpty(title) && mPrimaryAction != null) { 172 return mPrimaryAction.getTitle(); 173 } 174 return title; 175 } 176 177 /** 178 * @return the subtitle associated with this slice, if it exists. 179 */ 180 @Nullable getSubtitle()181 public CharSequence getSubtitle() { 182 if (mHeaderContent != null && mHeaderContent.getSubtitleItem() != null) { 183 return mHeaderContent.getSubtitleItem().getText(); 184 } 185 return null; 186 } 187 188 /** 189 * @return the summary associated with this slice, if it exists. 190 */ 191 @Nullable getSummary()192 public CharSequence getSummary() { 193 if (mHeaderContent != null && mHeaderContent.getSummaryItem() != null) { 194 return mHeaderContent.getSummaryItem().getText(); 195 } 196 return null; 197 } 198 199 /** 200 * @return the group of actions associated with this slice, if they exist. 201 */ 202 @Nullable getSliceActions()203 public List<SliceAction> getSliceActions() { 204 return mSliceActions; 205 } 206 207 /** 208 * @return the primary action for this slice, null if none specified. 209 */ 210 @Nullable getPrimaryAction()211 public SliceAction getPrimaryAction() { 212 return mPrimaryAction; 213 } 214 215 /** 216 * @return the type of row that is used for the header of this slice, -1 if unknown. 217 */ getHeaderType()218 public @EventInfo.SliceRowType int getHeaderType() { 219 return mTemplateType; 220 } 221 222 /** 223 * @return whether this slice has content to show when presented in {@link 224 * com.android.tv.twopanelsettings.slices.compat.widget.SliceViewPolicy#MODE_LARGE}. 225 */ hasLargeMode()226 public boolean hasLargeMode() { 227 return mListContent.getRowItems().size() > 1; 228 } 229 230 /** 231 * @return the toggles associated with the header of this slice. 232 */ getToggles()233 public List<SliceAction> getToggles() { 234 List<SliceAction> toggles = new ArrayList<>(); 235 // Is it the primary action? 236 if (mPrimaryAction != null && mPrimaryAction.isToggle()) { 237 toggles.add(mPrimaryAction); 238 } else if (mSliceActions != null && !mSliceActions.isEmpty()) { 239 for (int i = 0; i < mSliceActions.size(); i++) { 240 SliceAction action = mSliceActions.get(i); 241 if (action.isToggle()) { 242 toggles.add(action); 243 } 244 } 245 } else if (mHeaderContent != null) { 246 toggles.addAll(mHeaderContent.getToggleItems()); 247 } 248 return toggles; 249 } 250 251 @NonNull getHostExtras()252 public Bundle getHostExtras() { 253 return mHostExtras; 254 } 255 256 /** 257 * Sends the intent to adjust the state of the provided toggle action. 258 * 259 * @param toggleAction the toggle action. 260 * @param toggleValue the new value to set the toggle to. 261 * @return whether there was an action to send. 262 */ sendToggleAction(SliceAction toggleAction, boolean toggleValue)263 public boolean sendToggleAction(SliceAction toggleAction, boolean toggleValue) 264 throws PendingIntent.CanceledException { 265 if (toggleAction != null) { 266 Intent intent = 267 new Intent() 268 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) 269 .putExtra(EXTRA_TOGGLE_STATE, toggleValue); 270 if (mContext != null) { 271 SliceUtils.fireAction(mContext, toggleAction.getActionParcelable(), intent); 272 } 273 return true; 274 } 275 return false; 276 } 277 278 /** 279 * Gets the input range action associated with the header of this slice, if it exists. 280 * 281 * @return the {@link android.app.PendingIntent} for the input range. 282 */ 283 @Nullable getInputRangeAction()284 public Parcelable getInputRangeAction() { 285 if (mTemplateType == ROW_TYPE_SLIDER) { 286 SliceItem range = mHeaderContent.getRange(); 287 if (range != null) { 288 return range.getActionParcelable(); 289 } 290 } 291 return null; 292 } 293 294 /** 295 * Sends the intent to adjust the input range value for the header of this slice, if it exists. 296 * 297 * @param newValue the value to set the input range to. 298 * @return whether there was an action to send. 299 */ sendInputRangeAction(int newValue)300 public boolean sendInputRangeAction(int newValue) throws PendingIntent.CanceledException { 301 if (mTemplateType == ROW_TYPE_SLIDER) { 302 SliceItem range = mHeaderContent.getRange(); 303 if (range != null) { 304 // Ensure new value is valid 305 Pair<Integer, Integer> validRange = getRange(); 306 int adjustedValue = MathUtils.clamp(newValue, validRange.first, validRange.second); 307 Intent intent = 308 new Intent() 309 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) 310 .putExtra(EXTRA_RANGE_VALUE, adjustedValue); 311 range.fireAction(mContext, intent); 312 return true; 313 } 314 } 315 return false; 316 } 317 318 /** 319 * Gets the range information associated with a progress bar or input range associated with this 320 * slice, if it exists. 321 * 322 * @return a pair where the first item is the minimum value of the range and the second item is 323 * the maximum value of the range. 324 */ 325 @Nullable getRange()326 public Pair<Integer, Integer> getRange() { 327 if (mTemplateType == ROW_TYPE_SLIDER || mTemplateType == ROW_TYPE_PROGRESS) { 328 SliceItem range = mHeaderContent.getRange(); 329 SliceItem maxItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX); 330 SliceItem minItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MIN); 331 int max = maxItem != null ? maxItem.getInt() : 100; // default max of range 332 int min = minItem != null ? minItem.getInt() : 0; // default min of range 333 return new Pair<>(min, max); 334 } 335 return null; 336 } 337 338 /** 339 * Gets the current value for a progress bar or input range associated with this slice, if it 340 * exists, -1 if unknown. 341 * 342 * @return the current value of a progress bar or input range associated with this slice. 343 */ getRangeValue()344 public int getRangeValue() { 345 if (mTemplateType == ROW_TYPE_SLIDER || mTemplateType == ROW_TYPE_PROGRESS) { 346 SliceItem range = mHeaderContent.getRange(); 347 SliceItem currentItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE); 348 return currentItem != null ? currentItem.getInt() : -1; 349 } 350 return -1; 351 } 352 353 /** 354 * @return whether this slice is a selection (a drop-down list) slice. 355 */ isSelection()356 public boolean isSelection() { 357 return (mTemplateType == EventInfo.ROW_TYPE_SELECTION); 358 } 359 360 /** 361 * @return the list of keywords associated with the provided slice, null if no keywords were 362 * specified or an empty list if the slice was specified to have no keywords. 363 */ 364 @Nullable getSliceKeywords()365 public List<String> getSliceKeywords() { 366 SliceItem keywordGroup = SliceQuery.find(mSlice, FORMAT_SLICE, HINT_KEYWORDS, null); 367 if (keywordGroup != null) { 368 List<SliceItem> itemList = SliceQuery.findAll(keywordGroup, FORMAT_TEXT); 369 if (itemList != null) { 370 ArrayList<String> stringList = new ArrayList<>(); 371 for (int i = 0; i < itemList.size(); i++) { 372 String keyword = (String) itemList.get(i).getText(); 373 if (!TextUtils.isEmpty(keyword)) { 374 stringList.add(keyword); 375 } 376 } 377 return stringList; 378 } 379 } 380 return null; 381 } 382 383 /** 384 * @return the current loading state for this slice. 385 * @see #LOADED_NONE 386 * @see #LOADED_PARTIAL 387 * @see #LOADED_ALL 388 */ getLoadingState()389 public int getLoadingState() { 390 // Check loading state 391 boolean hasHintPartial = SliceQuery.find(mSlice, null, HINT_PARTIAL, null) != null; 392 if (!mListContent.isValid()) { 393 // Empty slice 394 return LOADED_NONE; 395 } else if (hasHintPartial) { 396 // Slice with specific content to load 397 return LOADED_PARTIAL; 398 } else { 399 // Full slice 400 return LOADED_ALL; 401 } 402 } 403 404 /** 405 * A slice contains an expiry to indicate when the content in the slice might no longer be valid. 406 * 407 * @return the time, measured in milliseconds, between the expiry time of this slice and midnight, 408 * January 1, 1970 UTC, or {@link 409 * com.android.tv.twopanelsettings.slices.compat.builders.ListBuilder#INFINITY} if the slice 410 * is not time-sensitive. 411 */ getExpiry()412 public long getExpiry() { 413 return mExpiry; 414 } 415 416 /** 417 * @return the time, measured in milliseconds, between when the slice was created or last updated, 418 * and midnight, January 1, 1970 UTC. 419 */ getLastUpdatedTime()420 public long getLastUpdatedTime() { 421 return mLastUpdated; 422 } 423 424 /** 425 * To present a slice from another app, the app must grant uri permissions for the slice. If these 426 * permissions have not been granted and the app slice is requested then a permission request 427 * slice will be returned instead, allowing the user to grant permission. This method can be used 428 * to identify if a slice is a permission request. 429 * 430 * @return whether this slice represents a permission request. 431 */ isPermissionSlice()432 public boolean isPermissionSlice() { 433 return mSlice.hasHint(HINT_PERMISSION_REQUEST); 434 } 435 436 /** 437 * Indicates whether this slice indicates an error, i.e. the normal contents of this slice are 438 * unavailable and instead the slice contains a message indicating an error. 439 * 440 * @return whether this slice represents an error. 441 */ isErrorSlice()442 public boolean isErrorSlice() { 443 return mSlice.hasHint(HINT_ERROR); 444 } 445 446 /** 447 * Indicates whether this slice was created using {@link SliceUtils#parseSlice} or through normal 448 * binding. 449 */ isCachedSlice()450 public boolean isCachedSlice() { 451 return mSlice.hasHint(HINT_CACHED); 452 } 453 454 /** 455 * @return the group of slice actions associated with the provided slice, if they exist. 456 */ 457 @Nullable 458 // @RestrictTo(RestrictTo.Scope.LIBRARY) getSliceActions(@onNull Slice slice)459 public static List<SliceAction> getSliceActions(@NonNull Slice slice) { 460 SliceItem actionGroup = SliceQuery.find(slice, FORMAT_SLICE, HINT_ACTIONS, null); 461 String[] hints = new String[] {HINT_ACTIONS, HINT_SHORTCUT}; 462 List<SliceItem> items = 463 (actionGroup != null) ? SliceQuery.findAll(actionGroup, FORMAT_SLICE, hints, null) : null; 464 if (items != null) { 465 List<SliceAction> actions = new ArrayList<>(items.size()); 466 for (int i = 0; i < items.size(); i++) { 467 SliceItem item = items.get(i); 468 actions.add(new SliceActionImpl(item)); 469 } 470 return actions; 471 } 472 return null; 473 } 474 475 /** */ 476 // @RestrictTo(RestrictTo.Scope.LIBRARY) isExpired()477 public boolean isExpired() { 478 long now = System.currentTimeMillis(); 479 return mExpiry != 0 && mExpiry != SliceHints.INFINITY && now > mExpiry; 480 } 481 482 /** */ 483 // @RestrictTo(RestrictTo.Scope.LIBRARY) neverExpires()484 public boolean neverExpires() { 485 return mExpiry == SliceHints.INFINITY; 486 } 487 488 /** */ 489 // @RestrictTo(RestrictTo.Scope.LIBRARY) getTimeToExpiry()490 public long getTimeToExpiry() { 491 long now = System.currentTimeMillis(); 492 return (mExpiry == 0 || mExpiry == SliceHints.INFINITY || now > mExpiry) ? 0 : mExpiry - now; 493 } 494 495 /** */ 496 // @RestrictTo(RestrictTo.Scope.LIBRARY) getListContent()497 public ListContent getListContent() { 498 return mListContent; 499 } 500 } 501