Home | History | Annotate | Download | only in slice
      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      */
    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      */
    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
    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
    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      */
    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      */
    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      */
    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
    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
    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
    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
    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      */
    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      */
    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      */
    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      */
    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)
    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