[go: nahoru, domu]

blob: 10234ed7c64516bdb3554f7a480eb7c41319baf1 [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.slice;
import static android.app.slice.Slice.EXTRA_RANGE_VALUE;
import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;
import static android.app.slice.Slice.HINT_ACTIONS;
import static android.app.slice.Slice.HINT_ERROR;
import static android.app.slice.Slice.HINT_KEYWORDS;
import static android.app.slice.Slice.HINT_LAST_UPDATED;
import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.Slice.HINT_PARTIAL;
import static android.app.slice.Slice.HINT_PERMISSION_REQUEST;
import static android.app.slice.Slice.HINT_SHORTCUT;
import static android.app.slice.Slice.HINT_TTL;
import static android.app.slice.Slice.SUBTYPE_MAX;
import static android.app.slice.Slice.SUBTYPE_VALUE;
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import static androidx.slice.core.SliceHints.HINT_CACHED;
import static androidx.slice.core.SliceHints.SUBTYPE_MIN;
import static androidx.slice.widget.EventInfo.ROW_TYPE_PROGRESS;
import static androidx.slice.widget.EventInfo.ROW_TYPE_SLIDER;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.math.MathUtils;
import androidx.core.util.Pair;
import androidx.slice.core.SliceAction;
import androidx.slice.core.SliceActionImpl;
import androidx.slice.core.SliceHints;
import androidx.slice.core.SliceQuery;
import androidx.slice.widget.EventInfo;
import androidx.slice.widget.ListContent;
import androidx.slice.widget.RowContent;
import androidx.slice.widget.SliceView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/**
* Utility class to parse a {@link Slice} and provide access to information around its contents.
*/
@RequiresApi(19)
public class SliceMetadata {
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@IntDef({
LOADED_NONE, LOADED_PARTIAL, LOADED_ALL
})
@Retention(RetentionPolicy.SOURCE)
public @interface SliceLoadingState{}
/**
* Indicates this slice is empty and waiting for content to be loaded.
*/
public static final int LOADED_NONE = 0;
/**
* Indicates this slice has some content but is waiting for other content to be loaded.
*/
public static final int LOADED_PARTIAL = 1;
/**
* Indicates this slice has fully loaded and is not waiting for other content.
*/
public static final int LOADED_ALL = 2;
private Slice mSlice;
private Context mContext;
private long mExpiry;
private long mLastUpdated;
private ListContent mListContent;
private RowContent mHeaderContent;
private SliceAction mPrimaryAction;
private List<SliceAction> mSliceActions;
private @EventInfo.SliceRowType int mTemplateType;
/**
* Create a SliceMetadata object to provide access to some information around the slice and
* its contents.
*
* @param context the context to use for the slice.
* @param slice the slice to extract metadata from.
*
* @return the metadata associated with the provided slice.
*/
public static SliceMetadata from(@NonNull Context context, @NonNull Slice slice) {
return new SliceMetadata(context, slice);
}
/**
* Create a SliceMetadata object to provide access to some information around the slice and
* its contents.
*
* @param context the context to use for the slice.
* @param slice the slice to extract metadata from.
*/
private SliceMetadata(@NonNull Context context, @NonNull Slice slice) {
mSlice = slice;
mContext = context;
SliceItem ttlItem = SliceQuery.find(slice, FORMAT_LONG, HINT_TTL, null);
if (ttlItem != null) {
mExpiry = ttlItem.getLong();
}
SliceItem updatedItem = SliceQuery.find(slice, FORMAT_LONG, HINT_LAST_UPDATED, null);
if (updatedItem != null) {
mLastUpdated = updatedItem.getLong();
}
mListContent = new ListContent(slice);
mHeaderContent = mListContent.getHeader();
mTemplateType = mListContent.getHeaderTemplateType();
mPrimaryAction = mListContent.getShortcut(mContext);
mSliceActions = mListContent.getSliceActions();
if (mSliceActions == null && mHeaderContent != null
&& SliceQuery.hasHints(mHeaderContent.getSliceItem(), HINT_LIST_ITEM)) {
// It's not a real header, check it for end items.
List<SliceItem> items = mHeaderContent.getEndItems();
List<SliceAction> actions = new ArrayList<>();
for (int i = 0; i < items.size(); i++) {
if (SliceQuery.find(items.get(i), FORMAT_ACTION) != null) {
actions.add(new SliceActionImpl(items.get(i)));
}
}
if (actions.size() > 0) {
mSliceActions = actions;
}
}
}
/**
* @return the title associated with this slice, if it exists.
*/
@Nullable
public CharSequence getTitle() {
CharSequence title = null;
if (mHeaderContent != null && mHeaderContent.getTitleItem() != null) {
title = mHeaderContent.getTitleItem().getText();
}
if (TextUtils.isEmpty(title) && mPrimaryAction != null) {
return mPrimaryAction.getTitle();
}
return title;
}
/**
* @return the subtitle associated with this slice, if it exists.
*/
@Nullable
public CharSequence getSubtitle() {
if (mHeaderContent != null && mHeaderContent.getSubtitleItem() != null) {
return mHeaderContent.getSubtitleItem().getText();
}
return null;
}
/**
* @return the summary associated with this slice, if it exists.
*/
@Nullable
public CharSequence getSummary() {
if (mHeaderContent != null && mHeaderContent.getSummaryItem() != null) {
return mHeaderContent.getSummaryItem().getText();
}
return null;
}
/**
* @return the group of actions associated with this slice, if they exist.
*/
@Nullable
public List<SliceAction> getSliceActions() {
return mSliceActions;
}
/**
* @return the primary action for this slice, null if none specified.
*/
@Nullable
public SliceAction getPrimaryAction() {
return mPrimaryAction;
}
/**
* @return the type of row that is used for the header of this slice, -1 if unknown.
*/
public @EventInfo.SliceRowType int getHeaderType() {
return mTemplateType;
}
/**
* @return whether this slice has content to show when presented
* in {@link SliceView#MODE_LARGE}.
*/
public boolean hasLargeMode() {
return mListContent.getRowItems().size() > 1;
}
/**
* @return the toggles associated with the header of this slice.
*/
public List<SliceAction> getToggles() {
List<SliceAction> toggles = new ArrayList<>();
// Is it the primary action?
if (mPrimaryAction != null && mPrimaryAction.isToggle()) {
toggles.add(mPrimaryAction);
} else if (mSliceActions != null && mSliceActions.size() > 0) {
for (int i = 0; i < mSliceActions.size(); i++) {
SliceAction action = mSliceActions.get(i);
if (action.isToggle()) {
toggles.add(action);
}
}
} else if (mHeaderContent != null) {
toggles.addAll(mHeaderContent.getToggleItems());
}
return toggles;
}
/**
* Sends the intent to adjust the state of the provided toggle action.
*
* @param toggleAction the toggle action.
* @param toggleValue the new value to set the toggle to.
* @return whether there was an action to send.
*/
public boolean sendToggleAction(SliceAction toggleAction, boolean toggleValue)
throws PendingIntent.CanceledException {
if (toggleAction != null) {
Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
.putExtra(EXTRA_TOGGLE_STATE, toggleValue);
toggleAction.getAction().send(mContext, 0, intent, null, null);
return true;
}
return false;
}
/**
* Gets the input range action associated with the header of this slice, if it exists.
*
* @return the {@link android.app.PendingIntent} for the input range.
*/
@Nullable
public PendingIntent getInputRangeAction() {
if (mTemplateType == ROW_TYPE_SLIDER) {
SliceItem range = mHeaderContent.getRange();
if (range != null) {
return range.getAction();
}
}
return null;
}
/**
* Sends the intent to adjust the input range value for the header of this slice, if it exists.
*
* @param newValue the value to set the input range to.
* @return whether there was an action to send.
*/
public boolean sendInputRangeAction(int newValue) throws PendingIntent.CanceledException {
if (mTemplateType == ROW_TYPE_SLIDER) {
SliceItem range = mHeaderContent.getRange();
if (range != null) {
// Ensure new value is valid
Pair<Integer, Integer> validRange = getRange();
int adjustedValue = MathUtils.clamp(newValue, validRange.first, validRange.second);
Intent intent = new Intent()
.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
.putExtra(EXTRA_RANGE_VALUE, adjustedValue);
range.fireAction(mContext, intent);
return true;
}
}
return false;
}
/**
* Gets the range information associated with a progress bar or input range associated with this
* slice, if it exists.
*
* @return a pair where the first item is the minimum value of the range and the second item is
* the maximum value of the range.
*/
@Nullable
public Pair<Integer, Integer> getRange() {
if (mTemplateType == ROW_TYPE_SLIDER
|| mTemplateType == ROW_TYPE_PROGRESS) {
SliceItem range = mHeaderContent.getRange();
SliceItem maxItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
SliceItem minItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MIN);
int max = maxItem != null ? maxItem.getInt() : 100; // default max of range
int min = minItem != null ? minItem.getInt() : 0; // default min of range
return new Pair<>(min, max);
}
return null;
}
/**
* Gets the current value for a progress bar or input range associated with this slice, if it
* exists, -1 if unknown.
*
* @return the current value of a progress bar or input range associated with this slice.
*/
@NonNull
public int getRangeValue() {
if (mTemplateType == ROW_TYPE_SLIDER
|| mTemplateType == ROW_TYPE_PROGRESS) {
SliceItem range = mHeaderContent.getRange();
SliceItem currentItem = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_VALUE);
return currentItem != null ? currentItem.getInt() : -1;
}
return -1;
}
/**
* @return whether this slice is a selection (a drop-down list) slice.
*/
public boolean isSelection() {
return (mTemplateType == EventInfo.ROW_TYPE_SELECTION);
}
/**
* @return the list of keywords associated with the provided slice, null if no keywords were
* specified or an empty list if the slice was specified to have no keywords.
*/
@Nullable
public List<String> getSliceKeywords() {
SliceItem keywordGroup = SliceQuery.find(mSlice, FORMAT_SLICE, HINT_KEYWORDS, null);
if (keywordGroup != null) {
List<SliceItem> itemList = SliceQuery.findAll(keywordGroup, FORMAT_TEXT);
if (itemList != null) {
ArrayList<String> stringList = new ArrayList<>();
for (int i = 0; i < itemList.size(); i++) {
String keyword = (String) itemList.get(i).getText();
if (!TextUtils.isEmpty(keyword)) {
stringList.add(keyword);
}
}
return stringList;
}
}
return null;
}
/**
* @return the current loading state for this slice.
*
* @see #LOADED_NONE
* @see #LOADED_PARTIAL
* @see #LOADED_ALL
*/
public int getLoadingState() {
// Check loading state
boolean hasHintPartial = SliceQuery.find(mSlice, null, HINT_PARTIAL, null) != null;
if (!mListContent.isValid()) {
// Empty slice
return LOADED_NONE;
} else if (hasHintPartial) {
// Slice with specific content to load
return LOADED_PARTIAL;
} else {
// Full slice
return LOADED_ALL;
}
}
/**
* A slice contains an expiry to indicate when the content in the slice might no longer be
* valid.
*
* @return the time, measured in milliseconds, between the expiry time of this slice and
* midnight, January 1, 1970 UTC, or {@link androidx.slice.builders.ListBuilder#INFINITY} if
* the slice is not time-sensitive.
*/
public long getExpiry() {
return mExpiry;
}
/**
* @return the time, measured in milliseconds, between when the slice was created or last
* updated, and midnight, January 1, 1970 UTC.
*/
public long getLastUpdatedTime() {
return mLastUpdated;
}
/**
* To present a slice from another app, the app must grant uri permissions for the slice. If
* these permissions have not been granted and the app slice is requested then
* a permission request slice will be returned instead, allowing the user to grant permission.
* This method can be used to identify if a slice is a permission request.
*
* @return whether this slice represents a permission request.
*/
public boolean isPermissionSlice() {
return mSlice.hasHint(HINT_PERMISSION_REQUEST);
}
/**
* Indicates whether this slice indicates an error, i.e. the normal contents of this slice are
* unavailable and instead the slice contains a message indicating an error.
*
* @return whether this slice represents an error.
*/
public boolean isErrorSlice() {
return mSlice.hasHint(HINT_ERROR);
}
/**
* Indicates whether this slice was created using {@link SliceUtils#parseSlice} or through
* normal binding.
*/
public boolean isCachedSlice() {
return mSlice.hasHint(HINT_CACHED);
}
/**
* @return the group of slice actions associated with the provided slice, if they exist.
* @hide
*/
@Nullable
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static List<SliceAction> getSliceActions(@NonNull Slice slice) {
SliceItem actionGroup = SliceQuery.find(slice, FORMAT_SLICE, HINT_ACTIONS, null);
String[] hints = new String[] {HINT_ACTIONS, HINT_SHORTCUT};
List<SliceItem> items = (actionGroup != null)
? SliceQuery.findAll(actionGroup, FORMAT_SLICE, hints, null)
: null;
if (items != null) {
List<SliceAction> actions = new ArrayList<>(items.size());
for (int i = 0; i < items.size(); i++) {
SliceItem item = items.get(i);
actions.add(new SliceActionImpl(item));
}
return actions;
}
return null;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public boolean isExpired() {
long now = System.currentTimeMillis();
return mExpiry != 0 && mExpiry != SliceHints.INFINITY && now > mExpiry;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public boolean neverExpires() {
return mExpiry == SliceHints.INFINITY;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public long getTimeToExpiry() {
long now = System.currentTimeMillis();
return (mExpiry == 0 || mExpiry == SliceHints.INFINITY || now > mExpiry)
? 0 : mExpiry - now;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public ListContent getListContent() {
return mListContent;
}
}