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