• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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