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 
17 package androidx.slice.widget;
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_LIST_ITEM;
22 import static android.app.slice.Slice.HINT_SEE_MORE;
23 import static android.app.slice.Slice.HINT_SHORTCUT;
24 import static android.app.slice.Slice.SUBTYPE_COLOR;
25 import static android.app.slice.SliceItem.FORMAT_ACTION;
26 import static android.app.slice.SliceItem.FORMAT_INT;
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_TTL;
33 import static androidx.slice.widget.SliceView.MODE_LARGE;
34 import static androidx.slice.widget.SliceView.MODE_SMALL;
35 
36 import android.content.Context;
37 import android.content.res.TypedArray;
38 import android.util.AttributeSet;
39 
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 import androidx.annotation.RestrictTo;
43 import androidx.slice.Slice;
44 import androidx.slice.SliceItem;
45 import androidx.slice.SliceMetadata;
46 import androidx.slice.core.SliceAction;
47 import androidx.slice.core.SliceActionImpl;
48 import androidx.slice.core.SliceQuery;
49 import androidx.slice.view.R;
50 
51 import java.util.ArrayList;
52 import 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)
59 public 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 
ListContent(Context context, Slice slice, AttributeSet attrs, int defStyleAttr, int defStyleRes)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      */
populate(Slice slice)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      */
getListHeight(Context context, List<SliceItem> listItems)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
getItemsForNonScrollingList(int height)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      */
getHeight(Context context, SliceItem item, boolean isHeader, int index, int count, int mode)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      */
isValid()249     public boolean isValid() {
250         return mRowItems.size() > 0;
251     }
252 
253     @Nullable
getSlice()254     public Slice getSlice() {
255         return mSlice;
256     }
257 
258     @Nullable
getColorItem()259     public SliceItem getColorItem() {
260         return mColorItem;
261     }
262 
263     @Nullable
getHeaderItem()264     public SliceItem getHeaderItem() {
265         return mHeaderItem;
266     }
267 
268     @Nullable
getSliceActions()269     public List<SliceItem> getSliceActions() {
270         return mSliceActions;
271     }
272 
273     @Nullable
getSeeMoreItem()274     public SliceItem getSeeMoreItem() {
275         return mSeeMoreItem;
276     }
277 
278     @NonNull
getRowItems()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      */
hasHeader()286     public boolean hasHeader() {
287         return mHeaderItem != null && isValidHeader(mHeaderItem);
288     }
289 
290     /**
291      * @return the type of template that the header represents.
292      */
getHeaderTemplateType()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      */
getRowType(Context context, SliceItem rowItem, boolean isHeader, List<SliceItem> actions)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
getPrimaryAction()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
findHeaderItem(@onNull Slice slice)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
getSeeMoreItem(@onNull Slice slice)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      */
isValidHeader(SliceItem sliceItem)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