1/*
2 * Copyright 2017 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
17package androidx.slice.widget;
18
19import static android.app.slice.Slice.HINT_ACTIONS;
20import static android.app.slice.Slice.HINT_HORIZONTAL;
21import static android.app.slice.Slice.HINT_LIST_ITEM;
22import static android.app.slice.Slice.HINT_SEE_MORE;
23import static android.app.slice.Slice.HINT_SHORTCUT;
24import static android.app.slice.Slice.SUBTYPE_COLOR;
25import static android.app.slice.SliceItem.FORMAT_ACTION;
26import static android.app.slice.SliceItem.FORMAT_INT;
27import static android.app.slice.SliceItem.FORMAT_SLICE;
28import static android.app.slice.SliceItem.FORMAT_TEXT;
29
30import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
31import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
32import static androidx.slice.core.SliceHints.HINT_TTL;
33import static androidx.slice.widget.SliceView.MODE_LARGE;
34import static androidx.slice.widget.SliceView.MODE_SMALL;
35
36import android.content.Context;
37import android.content.res.TypedArray;
38import android.util.AttributeSet;
39
40import androidx.annotation.NonNull;
41import androidx.annotation.Nullable;
42import androidx.annotation.RestrictTo;
43import androidx.slice.Slice;
44import androidx.slice.SliceItem;
45import androidx.slice.SliceMetadata;
46import androidx.slice.core.SliceAction;
47import androidx.slice.core.SliceActionImpl;
48import androidx.slice.core.SliceQuery;
49import androidx.slice.view.R;
50
51import java.util.ArrayList;
52import java.util.List;
53
54/**
55 * Extracts information required to present content in a list format from a slice.
56 * @hide
57 */
58@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
59public class ListContent {
60
61    private Slice mSlice;
62    private SliceItem mHeaderItem;
63    private SliceItem mColorItem;
64    private SliceItem mSeeMoreItem;
65    private ArrayList<SliceItem> mRowItems = new ArrayList<>();
66    private List<SliceItem> mSliceActions;
67    private Context mContext;
68
69    private int mHeaderTitleSize;
70    private int mHeaderSubtitleSize;
71    private int mVerticalHeaderTextPadding;
72    private int mTitleSize;
73    private int mSubtitleSize;
74    private int mVerticalTextPadding;
75    private int mGridTitleSize;
76    private int mGridSubtitleSize;
77    private int mVerticalGridTextPadding;
78    private int mGridTopPadding;
79    private int mGridBottomPadding;
80
81    public ListContent(Context context, Slice slice, AttributeSet attrs, int defStyleAttr,
82            int defStyleRes) {
83        mSlice = slice;
84        mContext = context;
85
86        // TODO: duplicated code from SliceChildView; could do something better
87        // Some of this information will impact the size calculations for slice content.
88        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView,
89                defStyleAttr, defStyleRes);
90        try {
91            mHeaderTitleSize = (int) a.getDimension(
92                    R.styleable.SliceView_headerTitleSize, 0);
93            mHeaderSubtitleSize = (int) a.getDimension(
94                    R.styleable.SliceView_headerSubtitleSize, 0);
95            mVerticalHeaderTextPadding = (int) a.getDimension(
96                    R.styleable.SliceView_headerTextVerticalPadding, 0);
97
98            mTitleSize = (int) a.getDimension(R.styleable.SliceView_titleSize, 0);
99            mSubtitleSize = (int) a.getDimension(
100                    R.styleable.SliceView_subtitleSize, 0);
101            mVerticalTextPadding = (int) a.getDimension(
102                    R.styleable.SliceView_textVerticalPadding, 0);
103
104            mGridTitleSize = (int) a.getDimension(R.styleable.SliceView_gridTitleSize, 0);
105            mGridSubtitleSize = (int) a.getDimension(
106                    R.styleable.SliceView_gridSubtitleSize, 0);
107            int defaultVerticalGridPadding = context.getResources().getDimensionPixelSize(
108                    R.dimen.abc_slice_grid_text_inner_padding);
109            mVerticalGridTextPadding = (int) a.getDimension(
110                    R.styleable.SliceView_gridTextVerticalPadding, defaultVerticalGridPadding);
111            mGridTopPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0);
112            mGridBottomPadding = (int) a.getDimension(R.styleable.SliceView_gridTopPadding, 0);
113        } finally {
114            a.recycle();
115        }
116
117        populate(slice);
118    }
119
120    /**
121     * @return whether this row has content that is valid to display.
122     */
123    private boolean populate(Slice slice) {
124        mColorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
125        // Find slice actions
126        mSliceActions = SliceMetadata.getSliceActions(slice);
127        // Find header
128        mHeaderItem = findHeaderItem(slice);
129        if (mHeaderItem != null) {
130            mRowItems.add(mHeaderItem);
131        }
132        mSeeMoreItem = getSeeMoreItem(slice);
133        // Filter + create row items
134        List<SliceItem> children = slice.getItems();
135        for (int i = 0; i < children.size(); i++) {
136            final SliceItem child = children.get(i);
137            final String format = child.getFormat();
138            boolean isNonRowContent = child.hasAnyHints(HINT_ACTIONS, HINT_SEE_MORE, HINT_KEYWORDS,
139                    HINT_TTL, HINT_LAST_UPDATED);
140            if (!isNonRowContent && (FORMAT_ACTION.equals(format) || FORMAT_SLICE.equals(format))) {
141                if (mHeaderItem == null && !child.hasHint(HINT_LIST_ITEM)) {
142                    mHeaderItem = child;
143                    mRowItems.add(0, child);
144                } else if (child.hasHint(HINT_LIST_ITEM)) {
145                    mRowItems.add(child);
146                }
147            }
148        }
149        // Ensure we have something for the header -- use first row
150        if (mHeaderItem == null && mRowItems.size() >= 1) {
151            mHeaderItem = mRowItems.get(0);
152        }
153        return isValid();
154    }
155
156    /**
157     * Expects the provided list of items to be filtered (i.e. only things that can be turned into
158     * GridContent or RowContent) and in order (i.e. first item could be a header).
159     *
160     * @return the total height of all the rows contained in the provided list.
161     */
162    public int getListHeight(Context context, List<SliceItem> listItems) {
163        if (listItems == null) {
164            return 0;
165        }
166        int height = 0;
167        boolean hasRealHeader = false;
168        SliceItem maybeHeader = null;
169        if (!listItems.isEmpty()) {
170            maybeHeader = listItems.get(0);
171            hasRealHeader = !maybeHeader.hasAnyHints(HINT_LIST_ITEM, HINT_HORIZONTAL);
172        }
173        if (listItems.size() == 1 && !maybeHeader.hasHint(HINT_HORIZONTAL)) {
174            return getHeight(context, maybeHeader, true /* isHeader */, 0, 1, MODE_LARGE);
175        }
176        int rowCount = listItems.size();
177        for (int i = 0; i < listItems.size(); i++) {
178            height += getHeight(context, listItems.get(i), i == 0 && hasRealHeader /* isHeader */,
179                    i, rowCount, MODE_LARGE);
180        }
181        return height;
182    }
183
184    /**
185     * Returns a list of items that can be displayed in the provided height. If this list
186     * has a {@link #getSeeMoreItem()} this will be returned in the list if appropriate.
187     *
188     * @param height the height to restrict the items, -1 to use default sizings for non-scrolling
189     *               templates.
190     * @return the list of items that can be displayed in the provided  height.
191     */
192    @NonNull
193    public List<SliceItem> getItemsForNonScrollingList(int height) {
194        ArrayList<SliceItem> visibleItems = new ArrayList<>();
195        if (mRowItems == null || mRowItems.size() == 0) {
196            return visibleItems;
197        }
198        final int idealItemCount = hasHeader() ? 4 : 3;
199        final int minItemCount = hasHeader() ? 2 : 1;
200        int visibleHeight = 0;
201        // Need to show see more
202        if (mSeeMoreItem != null) {
203            RowContent rc = new RowContent(mContext, mSeeMoreItem, false /* isHeader */);
204            visibleHeight += rc.getActualHeight();
205        }
206        int rowCount = mRowItems.size();
207        for (int i = 0; i < rowCount; i++) {
208            int itemHeight = getHeight(mContext, mRowItems.get(i), i == 0 /* isHeader */,
209                    i, rowCount, MODE_LARGE);
210            if ((height == -1 && i > idealItemCount)
211                    || (height > 0 && visibleHeight + itemHeight > height)) {
212                break;
213            } else {
214                visibleHeight += itemHeight;
215                visibleItems.add(mRowItems.get(i));
216            }
217        }
218        if (mSeeMoreItem != null && visibleItems.size() >= minItemCount) {
219            // Only add see more if we're at least showing one item and it's not the header
220            visibleItems.add(mSeeMoreItem);
221        }
222        if (visibleItems.size() == 0) {
223            // Didn't have enough space to show anything; should still show something
224            visibleItems.add(mRowItems.get(0));
225        }
226        return visibleItems;
227    }
228
229    /**
230     * Determines the height of the provided {@link SliceItem}.
231     */
232    public int getHeight(Context context, SliceItem item, boolean isHeader, int index,
233            int count, int mode) {
234        if (item.hasHint(HINT_HORIZONTAL)) {
235            GridContent gc = new GridContent(context, item);
236            int topPadding = gc.isAllImages() && index == 0 ? mGridTopPadding : 0;
237            int bottomPadding = gc.isAllImages() && index == count - 1 ? mGridBottomPadding : 0;
238            int height = mode == MODE_SMALL ? gc.getSmallHeight() : gc.getActualHeight();
239            return height + topPadding + bottomPadding;
240        } else {
241            RowContent rc = new RowContent(context, item, isHeader);
242            return mode == MODE_SMALL ? rc.getSmallHeight() : rc.getActualHeight();
243        }
244    }
245
246    /**
247     * @return whether this list has content that is valid to display.
248     */
249    public boolean isValid() {
250        return mRowItems.size() > 0;
251    }
252
253    @Nullable
254    public Slice getSlice() {
255        return mSlice;
256    }
257
258    @Nullable
259    public SliceItem getColorItem() {
260        return mColorItem;
261    }
262
263    @Nullable
264    public SliceItem getHeaderItem() {
265        return mHeaderItem;
266    }
267
268    @Nullable
269    public List<SliceItem> getSliceActions() {
270        return mSliceActions;
271    }
272
273    @Nullable
274    public SliceItem getSeeMoreItem() {
275        return mSeeMoreItem;
276    }
277
278    @NonNull
279    public ArrayList<SliceItem> getRowItems() {
280        return mRowItems;
281    }
282
283    /**
284     * @return whether this list has an explicit header (i.e. row item without HINT_LIST_ITEM)
285     */
286    public boolean hasHeader() {
287        return mHeaderItem != null && isValidHeader(mHeaderItem);
288    }
289
290    /**
291     * @return the type of template that the header represents.
292     */
293    public int getHeaderTemplateType() {
294        return getRowType(mContext, mHeaderItem, true, mSliceActions);
295    }
296
297    /**
298     * The type of template that the provided row item represents.
299     *
300     * @param context context used for this slice.
301     * @param rowItem the row item to determine the template type of.
302     * @param isHeader whether this row item is used as a header.
303     * @param actions the actions associated with this slice, only matter if this row is the header.
304     * @return the type of template the provided row item represents.
305     */
306    public static int getRowType(Context context, SliceItem rowItem, boolean isHeader,
307                                 List<SliceItem> actions) {
308        if (rowItem != null) {
309            if (rowItem.hasHint(HINT_HORIZONTAL)) {
310                return EventInfo.ROW_TYPE_GRID;
311            } else {
312                RowContent rc = new RowContent(context, rowItem, isHeader);
313                SliceItem actionItem = rc.getPrimaryAction();
314                SliceAction primaryAction = null;
315                if (actionItem != null) {
316                    primaryAction = new SliceActionImpl(actionItem);
317                }
318                if (rc.getRange() != null) {
319                    return FORMAT_ACTION.equals(rc.getRange().getFormat())
320                            ? EventInfo.ROW_TYPE_SLIDER
321                            : EventInfo.ROW_TYPE_PROGRESS;
322                } else if (primaryAction != null && primaryAction.isToggle()) {
323                    return EventInfo.ROW_TYPE_TOGGLE;
324                } else if (isHeader && actions != null) {
325                    for (int i = 0; i < actions.size(); i++) {
326                        if (new SliceActionImpl(actions.get(i)).isToggle()) {
327                            return EventInfo.ROW_TYPE_TOGGLE;
328                        }
329                    }
330                    return EventInfo.ROW_TYPE_LIST;
331                } else {
332                    return rc.getToggleItems().size() > 0
333                            ? EventInfo.ROW_TYPE_TOGGLE
334                            : EventInfo.ROW_TYPE_LIST;
335                }
336            }
337        }
338        return EventInfo.ROW_TYPE_LIST;
339    }
340
341    /**
342     * @return the primary action for this list; i.e. action on the header or first row.
343     */
344    @Nullable
345    public SliceItem getPrimaryAction() {
346        if (mHeaderItem != null) {
347            if (mHeaderItem.hasHint(HINT_HORIZONTAL)) {
348                GridContent gc = new GridContent(mContext, mHeaderItem);
349                return gc.getContentIntent();
350            } else {
351                RowContent rc = new RowContent(mContext, mHeaderItem, false);
352                return rc.getPrimaryAction();
353            }
354        }
355        return null;
356    }
357
358    @Nullable
359    private static SliceItem findHeaderItem(@NonNull Slice slice) {
360        // See if header is specified
361        String[] nonHints = new String[] {HINT_LIST_ITEM, HINT_SHORTCUT, HINT_ACTIONS,
362                HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED, HINT_HORIZONTAL};
363        SliceItem header = SliceQuery.find(slice, FORMAT_SLICE, null, nonHints);
364        if (header != null && isValidHeader(header)) {
365            return header;
366        }
367        return null;
368    }
369
370    @Nullable
371    private static SliceItem getSeeMoreItem(@NonNull Slice slice) {
372        SliceItem item = SliceQuery.find(slice, null, HINT_SEE_MORE, null);
373        if (item != null) {
374            if (FORMAT_SLICE.equals(item.getFormat())) {
375                List<SliceItem> items = item.getSlice().getItems();
376                if (items.size() == 1 && FORMAT_ACTION.equals(items.get(0).getFormat())) {
377                    return items.get(0);
378                }
379                return item;
380            }
381        }
382        return null;
383    }
384
385    /**
386     * @return whether the provided slice item is a valid header.
387     */
388    public static boolean isValidHeader(SliceItem sliceItem) {
389        if (FORMAT_SLICE.equals(sliceItem.getFormat()) && !sliceItem.hasAnyHints(HINT_LIST_ITEM,
390                HINT_ACTIONS, HINT_KEYWORDS, HINT_SEE_MORE)) {
391             // Minimum valid header is a slice with text
392            SliceItem item = SliceQuery.find(sliceItem, FORMAT_TEXT, (String) null, null);
393            return item != null;
394        }
395        return false;
396    }
397}
398