blob: 1cbf357f778a37d747e7705e35a47e506c0b54c2 [file] [log] [blame]
/*
* Copyright 2017 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.widget;
import static android.app.slice.Slice.HINT_ACTIONS;
import static android.app.slice.Slice.HINT_HORIZONTAL;
import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.Slice.HINT_SEE_MORE;
import static android.app.slice.Slice.HINT_SHORTCUT;
import static android.app.slice.Slice.SUBTYPE_COLOR;
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;
import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
import static androidx.slice.core.SliceHints.HINT_TTL;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.SliceMetadata;
import androidx.slice.core.SliceAction;
import androidx.slice.core.SliceActionImpl;
import androidx.slice.core.SliceQuery;
import java.util.ArrayList;
import java.util.List;
/**
* Extracts information required to present content in a list format from a slice.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class ListContent {
private SliceItem mHeaderItem;
private SliceItem mColorItem;
private SliceItem mSeeMoreItem;
private ArrayList<SliceItem> mRowItems = new ArrayList<>();
private List<SliceItem> mSliceActions;
private Context mContext;
public ListContent(Context context, Slice slice) {
mContext = context;
populate(slice);
}
/**
* @return whether this row has content that is valid to display.
*/
private boolean populate(Slice slice) {
mColorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
// Find slice actions
mSliceActions = SliceMetadata.getSliceActions(slice);
// Find header
mHeaderItem = findHeaderItem(slice);
if (mHeaderItem != null) {
mRowItems.add(mHeaderItem);
}
mSeeMoreItem = getSeeMoreItem(slice);
// Filter + create row items
List<SliceItem> children = slice.getItems();
for (int i = 0; i < children.size(); i++) {
final SliceItem child = children.get(i);
final String format = child.getFormat();
boolean isNonRowContent = child.hasAnyHints(HINT_ACTIONS, HINT_SEE_MORE, HINT_KEYWORDS,
HINT_TTL, HINT_LAST_UPDATED);
if (!isNonRowContent && (FORMAT_ACTION.equals(format) || FORMAT_SLICE.equals(format))) {
if (mHeaderItem == null && !child.hasHint(HINT_LIST_ITEM)) {
mHeaderItem = child;
mRowItems.add(0, child);
} else if (child.hasHint(HINT_LIST_ITEM)) {
mRowItems.add(child);
}
}
}
// Ensure we have something for the header -- use first row
if (mHeaderItem == null && mRowItems.size() >= 1) {
mHeaderItem = mRowItems.get(0);
}
return isValid();
}
/**
* Expects the provided list of items to be filtered (i.e. only things that can be turned into
* GridContent or RowContent) and in order (i.e. first item could be a header).
*
* @return the total height of all the rows contained in the provided list.
*/
public static int getListHeight(Context context, List<SliceItem> listItems) {
if (listItems == null) {
return 0;
}
int height = 0;
boolean hasRealHeader = false;
if (!listItems.isEmpty()) {
SliceItem maybeHeader = listItems.get(0);
hasRealHeader = !maybeHeader.hasAnyHints(HINT_LIST_ITEM, HINT_HORIZONTAL);
}
for (int i = 0; i < listItems.size(); i++) {
height += getHeight(context, listItems.get(i), i == 0 && hasRealHeader /* isHeader */);
}
return height;
}
/**
* Returns a list of items that can be displayed in the provided height. If this list
* has a {@link #getSeeMoreItem()} this will be returned in the list if appropriate.
*
* @param height the height to restrict the items, -1 to use default sizings for non-scrolling
* templates.
* @return the list of items that can be displayed in the provided height.
*/
@NonNull
public List<SliceItem> getItemsForNonScrollingList(int height) {
ArrayList<SliceItem> visibleItems = new ArrayList<>();
if (mRowItems == null || mRowItems.size() == 0) {
return visibleItems;
}
final int idealItemCount = hasHeader() ? 4 : 3;
final int minItemCount = hasHeader() ? 2 : 1;
int visibleHeight = 0;
// Need to show see more
if (mSeeMoreItem != null) {
RowContent rc = new RowContent(mContext, mSeeMoreItem, false /* isHeader */);
visibleHeight += rc.getActualHeight();
}
for (int i = 0; i < mRowItems.size(); i++) {
int itemHeight = getHeight(mContext, mRowItems.get(i), i == 0 /* isHeader */);
if ((height == -1 && i > idealItemCount)
|| (height > 0 && visibleHeight + itemHeight > height)) {
break;
} else {
visibleHeight += itemHeight;
visibleItems.add(mRowItems.get(i));
}
}
if (mSeeMoreItem != null && visibleItems.size() >= minItemCount) {
// Only add see more if we're at least showing one item and it's not the header
visibleItems.add(mSeeMoreItem);
}
if (visibleItems.size() == 0) {
// Didn't have enough space to show anything; should still show something
visibleItems.add(mRowItems.get(0));
}
return visibleItems;
}
private static int getHeight(Context context, SliceItem item, boolean isHeader) {
if (item.hasHint(HINT_HORIZONTAL)) {
GridContent gc = new GridContent(context, item);
return gc.getActualHeight();
} else {
RowContent rc = new RowContent(context, item, isHeader);
return rc.getActualHeight();
}
}
/**
* @return whether this list has content that is valid to display.
*/
public boolean isValid() {
return mRowItems.size() > 0;
}
@Nullable
public SliceItem getColorItem() {
return mColorItem;
}
@Nullable
public SliceItem getHeaderItem() {
return mHeaderItem;
}
@Nullable
public List<SliceItem> getSliceActions() {
return mSliceActions;
}
@Nullable
public SliceItem getSeeMoreItem() {
return mSeeMoreItem;
}
@NonNull
public ArrayList<SliceItem> getRowItems() {
return mRowItems;
}
/**
* @return whether this list has an explicit header (i.e. row item without HINT_LIST_ITEM)
*/
public boolean hasHeader() {
return mHeaderItem != null && isValidHeader(mHeaderItem);
}
/**
* @return the type of template that the header represents.
*/
public int getHeaderTemplateType() {
return getRowType(mContext, mHeaderItem, true, mSliceActions);
}
/**
* The type of template that the provided row item represents.
*
* @param context context used for this slice.
* @param rowItem the row item to determine the template type of.
* @param isHeader whether this row item is used as a header.
* @param actions the actions associated with this slice, only matter if this row is the header.
* @return the type of template the provided row item represents.
*/
public static int getRowType(Context context, SliceItem rowItem, boolean isHeader,
List<SliceItem> actions) {
if (rowItem != null) {
if (rowItem.hasHint(HINT_HORIZONTAL)) {
return EventInfo.ROW_TYPE_GRID;
} else {
RowContent rc = new RowContent(context, rowItem, isHeader);
SliceItem actionItem = rc.getPrimaryAction();
SliceAction primaryAction = null;
if (actionItem != null) {
primaryAction = new SliceActionImpl(actionItem);
}
if (rc.getRange() != null) {
return FORMAT_ACTION.equals(rc.getRange().getFormat())
? EventInfo.ROW_TYPE_SLIDER
: EventInfo.ROW_TYPE_PROGRESS;
} else if (primaryAction != null && primaryAction.isToggle()) {
return EventInfo.ROW_TYPE_TOGGLE;
} else if (isHeader && actions != null) {
for (int i = 0; i < actions.size(); i++) {
if (new SliceActionImpl(actions.get(i)).isToggle()) {
return EventInfo.ROW_TYPE_TOGGLE;
}
}
return EventInfo.ROW_TYPE_LIST;
} else {
return rc.getToggleItems().size() > 0
? EventInfo.ROW_TYPE_TOGGLE
: EventInfo.ROW_TYPE_LIST;
}
}
}
return EventInfo.ROW_TYPE_LIST;
}
/**
* @return the primary action for this list; i.e. action on the header or first row.
*/
@Nullable
public SliceItem getPrimaryAction() {
if (mHeaderItem != null) {
if (mHeaderItem.hasHint(HINT_HORIZONTAL)) {
GridContent gc = new GridContent(mContext, mHeaderItem);
return gc.getContentIntent();
} else {
RowContent rc = new RowContent(mContext, mHeaderItem, false);
return rc.getPrimaryAction();
}
}
return null;
}
@Nullable
private static SliceItem findHeaderItem(@NonNull Slice slice) {
// See if header is specified
String[] nonHints = new String[] {HINT_LIST_ITEM, HINT_SHORTCUT, HINT_ACTIONS,
HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED};
SliceItem header = SliceQuery.find(slice, FORMAT_SLICE, null, nonHints);
if (header != null && isValidHeader(header)) {
return header;
}
return null;
}
@Nullable
private static SliceItem getSeeMoreItem(@NonNull Slice slice) {
SliceItem item = SliceQuery.find(slice, null, HINT_SEE_MORE, null);
if (item != null && item.hasHint(HINT_SEE_MORE)) {
if (FORMAT_SLICE.equals(item.getFormat())) {
List<SliceItem> items = item.getSlice().getItems();
if (items.size() == 1 && FORMAT_ACTION.equals(items.get(0).getFormat())) {
return items.get(0);
}
return item;
}
}
return null;
}
/**
* @return whether the provided slice item is a valid header.
*/
public static boolean isValidHeader(SliceItem sliceItem) {
if (FORMAT_SLICE.equals(sliceItem.getFormat()) && !sliceItem.hasAnyHints(HINT_LIST_ITEM,
HINT_ACTIONS, HINT_KEYWORDS, HINT_SEE_MORE)) {
// Minimum valid header is a slice with text
SliceItem item = SliceQuery.find(sliceItem, FORMAT_TEXT, (String) null, null);
return item != null;
}
return false;
}
}