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 androidx.slice;
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_PARTIAL;
22 import static android.app.slice.Slice.HINT_SHORTCUT;
23 import static android.app.slice.Slice.SUBTYPE_MAX;
24 import static android.app.slice.Slice.SUBTYPE_VALUE;
25 import static android.app.slice.SliceItem.FORMAT_INT;
26 import static android.app.slice.SliceItem.FORMAT_LONG;
27 import static android.app.slice.SliceItem.FORMAT_SLICE;
28 import static android.app.slice.SliceItem.FORMAT_TEXT;
29 
30 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
31 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
32 import static androidx.slice.core.SliceHints.HINT_PERMISSION_REQUEST;
33 import static androidx.slice.core.SliceHints.HINT_TTL;
34 import static androidx.slice.core.SliceHints.SUBTYPE_MIN;
35 import static androidx.slice.widget.EventInfo.ROW_TYPE_PROGRESS;
36 import static androidx.slice.widget.EventInfo.ROW_TYPE_SLIDER;
37 
38 import android.app.PendingIntent;
39 import android.content.Context;
40 import android.text.TextUtils;
41 
42 import androidx.annotation.IntDef;
43 import androidx.annotation.NonNull;
44 import androidx.annotation.Nullable;
45 import androidx.annotation.RestrictTo;
46 import androidx.core.util.Pair;
47 import androidx.slice.core.SliceAction;
48 import androidx.slice.core.SliceActionImpl;
49 import androidx.slice.core.SliceQuery;
50 import androidx.slice.widget.EventInfo;
51 import androidx.slice.widget.GridContent;
52 import androidx.slice.widget.ListContent;
53 import androidx.slice.widget.RowContent;
54 import androidx.slice.widget.SliceView;
55 
56 import java.lang.annotation.Retention;
57 import java.lang.annotation.RetentionPolicy;
58 import java.util.ArrayList;
59 import java.util.List;
60 
61 /**
62  * Utility class to parse a Slice and provide access to some information around its contents.
63  */
64 public class SliceMetadata {
65 
66     /**
67      * @hide
68      */
69     @RestrictTo(RestrictTo.Scope.LIBRARY)
70     @IntDef({
71             LOADED_NONE, LOADED_PARTIAL, LOADED_ALL
72     })
73     @Retention(RetentionPolicy.SOURCE)
74     public @interface SliceLoadingState{}
75 
76     /**
77      * Indicates this slice is empty and waiting for content to be loaded.
78      */
79     public static final int LOADED_NONE = 0;
80     /**
81      * Indicates this slice has some content but is waiting for other content to be loaded.
82      */
83     public static final int LOADED_PARTIAL = 1;
84     /**
85      * Indicates this slice has fully loaded and is not waiting for other content.
86      */
87     public static final int LOADED_ALL = 2;
88 
89     private Slice mSlice;
90     private Context mContext;
91     private long mExpiry;
92     private long mLastUpdated;
93     private ListContent mListContent;
94     private SliceItem mHeaderItem;
95     private SliceActionImpl mPrimaryAction;
96     private List<SliceItem> mSliceActions;
97     private @EventInfo.SliceRowType int mTemplateType;
98 
99     /**
100      * Create a SliceMetadata object to provide access to some information around the slice and
101      * its contents.
102      *
103      * @param context the context to use for the slice.
104      * @param slice the slice to extract metadata from.
105      *
106      * @return the metadata associated with the provided slice.
107      */
from(@onNull Context context, @NonNull Slice slice)108     public static SliceMetadata from(@NonNull Context context, @NonNull Slice slice) {
109         return new SliceMetadata(context, slice);
110     }
111 
112     /**
113      * Create a SliceMetadata object to provide access to some information around the slice and
114      * its contents.
115      *
116      * @param context the context to use for the slice.
117      * @param slice the slice to extract metadata from.
118      */
SliceMetadata(@onNull Context context, @NonNull Slice slice)119     private SliceMetadata(@NonNull Context context, @NonNull Slice slice) {
120         mSlice = slice;
121         mContext = context;
122         SliceItem ttlItem = SliceQuery.find(slice, FORMAT_LONG, HINT_TTL, null);
123         if (ttlItem != null) {
124             mExpiry = ttlItem.getTimestamp();
125         }
126         SliceItem updatedItem = SliceQuery.find(slice, FORMAT_LONG, HINT_LAST_UPDATED, null);
127         if (updatedItem != null) {
128             mLastUpdated = updatedItem.getTimestamp();
129         }
130         mSliceActions = getSliceActions(mSlice);
131 
132         mListContent = new ListContent(context, slice, null, 0, 0);
133         mHeaderItem = mListContent.getHeaderItem();
134         mTemplateType = mListContent.getHeaderTemplateType();
135 
136         SliceItem action = mListContent.getPrimaryAction();
137         if (action != null) {
138             mPrimaryAction = new SliceActionImpl(action);
139         }
140     }
141 
142     /**
143      * @return the group of actions associated with this slice, if they exist.
144      */
145     @Nullable
getSliceActions()146     public List<SliceItem> getSliceActions() {
147         return mSliceActions;
148     }
149 
150     /**
151      * @return the primary action for this slice, null if none specified.
152      */
153     @Nullable
getPrimaryAction()154     public SliceAction getPrimaryAction() {
155         return mPrimaryAction;
156     }
157 
158     /**
159      * @return the type of row that is used for the header of this slice, -1 if unknown.
160      */
getHeaderType()161     public @EventInfo.SliceRowType int getHeaderType() {
162         return mTemplateType;
163     }
164 
165     /**
166      * @return whether this slice has content to show when presented
167      * in {@link SliceView#MODE_LARGE}.
168      */
hasLargeMode()169     public boolean hasLargeMode() {
170         boolean isHeaderFullGrid = false;
171         if (mHeaderItem != null && mHeaderItem.hasHint(HINT_HORIZONTAL)) {
172             GridContent gc = new GridContent(mContext, mHeaderItem);
173             isHeaderFullGrid = gc.hasImage() && gc.getMaxCellLineCount() > 1;
174         }
175         return mListContent.getRowItems().size() > 1 || isHeaderFullGrid;
176     }
177 
178     /**
179      * @return the toggles associated with the header of this slice.
180      */
getToggles()181     public List<SliceAction> getToggles() {
182         List<SliceAction> toggles = new ArrayList<>();
183         // Is it the primary action?
184         if (mPrimaryAction != null && mPrimaryAction.isToggle()) {
185             toggles.add(mPrimaryAction);
186         } else if (mSliceActions != null && mSliceActions.size() > 0) {
187             for (int i = 0; i < mSliceActions.size(); i++) {
188                 SliceAction action = new SliceActionImpl(mSliceActions.get(i));
189                 if (action.isToggle()) {
190                     toggles.add(action);
191                 }
192             }
193         } else {
194             RowContent rc = new RowContent(mContext, mHeaderItem, true /* isHeader */);
195             toggles = rc.getToggleItems();
196         }
197         return toggles;
198     }
199 
200     /**
201      * Gets the input range action associated for this slice, if it exists.
202      *
203      * @return the {@link android.app.PendingIntent} for the input range.
204      */
205     @Nullable
getInputRangeAction()206     public PendingIntent getInputRangeAction() {
207         if (mTemplateType == ROW_TYPE_SLIDER) {
208             RowContent rc = new RowContent(mContext, mHeaderItem, true /* isHeader */);
209             SliceItem range = rc.getRange();
210             if (range != null) {
211                 return range.getAction();
212             }
213         }
214         return null;
215     }
216 
217     /**
218      * Gets the range information associated with a progress bar or input range associated with this
219      * slice, if it exists.
220      *
221      * @return a pair where the first item is the minimum value of the range and the second item is
222      * the maximum value of the range.
223      */
224     @Nullable
getRange()225     public Pair<Integer, Integer> getRange() {
226         if (mTemplateType == ROW_TYPE_SLIDER
227                 || mTemplateType == ROW_TYPE_PROGRESS) {
228             RowContent rc = new RowContent(mContext, mHeaderItem, true /* isHeader */);
229             SliceItem range = rc.getRange();
230             SliceItem maxItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
231             SliceItem minItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MIN);
232             int max = maxItem != null ? maxItem.getInt() : 100; // default max of range
233             int min = minItem != null ? minItem.getInt() : 0; // default min of range
234             return new Pair<>(min, max);
235         }
236         return null;
237     }
238 
239     /**
240      * Gets the current value for a progress bar or input range associated with this slice, if it
241      * exists, -1 if unknown.
242      *
243      * @return the current value of a progress bar or input range associated with this slice.
244      */
245     @NonNull
getRangeValue()246     public int getRangeValue() {
247         if (mTemplateType == ROW_TYPE_SLIDER
248                 || mTemplateType == ROW_TYPE_PROGRESS) {
249             RowContent rc = new RowContent(mContext, mHeaderItem, true /* isHeader */);
250             SliceItem range = rc.getRange();
251             SliceItem currentItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE);
252             return currentItem != null ? currentItem.getInt() : -1;
253         }
254         return -1;
255 
256     }
257 
258     /**
259      * @return the list of keywords associated with the provided slice, null if no keywords were
260      * specified or an empty list if the slice was specified to have no keywords.
261      */
262     @Nullable
getSliceKeywords()263     public List<String> getSliceKeywords() {
264         SliceItem keywordGroup = SliceQuery.find(mSlice, FORMAT_SLICE, HINT_KEYWORDS, null);
265         if (keywordGroup != null) {
266             List<SliceItem> itemList = SliceQuery.findAll(keywordGroup, FORMAT_TEXT);
267             if (itemList != null) {
268                 ArrayList<String> stringList = new ArrayList<>();
269                 for (int i = 0; i < itemList.size(); i++) {
270                     String keyword = (String) itemList.get(i).getText();
271                     if (!TextUtils.isEmpty(keyword)) {
272                         stringList.add(keyword);
273                     }
274                 }
275                 return stringList;
276             }
277         }
278         return null;
279     }
280 
281     /**
282      * @return the current loading state for this slice.
283      *
284      * @see #LOADED_NONE
285      * @see #LOADED_PARTIAL
286      * @see #LOADED_ALL
287      */
getLoadingState()288     public int getLoadingState() {
289         // Check loading state
290         boolean hasHintPartial = SliceQuery.find(mSlice, null, HINT_PARTIAL, null) != null;
291         if (!mListContent.isValid()) {
292             // Empty slice
293             return LOADED_NONE;
294         } else if (hasHintPartial) {
295             // Slice with specific content to load
296             return LOADED_PARTIAL;
297         } else {
298             // Full slice
299             return LOADED_ALL;
300         }
301     }
302 
303     /**
304      * A slice contains an expiry to indicate when the content in the slice might no longer be
305      * valid.
306      *
307      * @return the time, measured in milliseconds, between the expiry time of this slice and
308      * midnight, January 1, 1970 UTC, or {@link androidx.slice.builders.ListBuilder#INFINITY} if
309      * the slice is not time-sensitive.
310      */
getExpiry()311     public long getExpiry() {
312         return mExpiry;
313     }
314 
315     /**
316      * @return the time, measured in milliseconds, between when the slice was created or last
317      * updated, and midnight, January 1, 1970 UTC.
318      */
getLastUpdatedTime()319     public long getLastUpdatedTime() {
320         return mLastUpdated;
321     }
322 
323     /**
324      * To present a slice from another app, the app must grant uri permissions for the slice. If
325      * these permissions have not been granted and the app slice is requested then
326      * a permission request slice will be returned instead, allowing the user to grant permission.
327      * This method can be used to identify if a slice is a permission request.
328      *
329      * @return whether this slice represents a permission request.
330      */
isPermissionSlice()331     public boolean isPermissionSlice() {
332         return mSlice.hasHint(HINT_PERMISSION_REQUEST);
333     }
334 
335     /**
336      * @return the group of actions associated with the provided slice, if they exist.
337      * @hide
338      */
339     @Nullable
340     @RestrictTo(RestrictTo.Scope.LIBRARY)
getSliceActions(@onNull Slice slice)341     public static List<SliceItem> getSliceActions(@NonNull Slice slice) {
342         SliceItem actionGroup = SliceQuery.find(slice, FORMAT_SLICE, HINT_ACTIONS, null);
343         String[] hints = new String[] {HINT_ACTIONS, HINT_SHORTCUT};
344         return (actionGroup != null)
345                 ? SliceQuery.findAll(actionGroup, FORMAT_SLICE, hints, null)
346                 : null;
347     }
348 }
349