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_SEE_MORE;
21 import static android.app.slice.Slice.HINT_SHORTCUT;
22 import static android.app.slice.Slice.HINT_TITLE;
23 import static android.app.slice.Slice.SUBTYPE_COLOR;
24 import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
25 import static android.app.slice.SliceItem.FORMAT_ACTION;
26 import static android.app.slice.SliceItem.FORMAT_IMAGE;
27 import static android.app.slice.SliceItem.FORMAT_INT;
28 import static android.app.slice.SliceItem.FORMAT_LONG;
29 import static android.app.slice.SliceItem.FORMAT_SLICE;
30 import static android.app.slice.SliceItem.FORMAT_TEXT;
31 
32 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
33 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
34 import static androidx.slice.core.SliceHints.HINT_TTL;
35 import static androidx.slice.core.SliceHints.ICON_IMAGE;
36 import static androidx.slice.core.SliceHints.LARGE_IMAGE;
37 import static androidx.slice.core.SliceHints.SMALL_IMAGE;
38 
39 import android.app.slice.Slice;
40 import android.content.Context;
41 import android.content.res.Resources;
42 
43 import androidx.annotation.NonNull;
44 import androidx.annotation.Nullable;
45 import androidx.annotation.RestrictTo;
46 import androidx.slice.SliceItem;
47 import androidx.slice.core.SliceHints;
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 grid format from a slice.
56  * @hide
57  */
58 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
59 public class GridContent {
60 
61     private boolean mAllImages;
62     private SliceItem mColorItem;
63     private SliceItem mPrimaryAction;
64     private ArrayList<CellContent> mGridContent = new ArrayList<>();
65     private SliceItem mSeeMoreItem;
66     private int mMaxCellLineCount;
67     private boolean mHasImage;
68     private @SliceHints.ImageMode int mLargestImageMode;
69     private SliceItem mContentDescr;
70 
71     private int mBigPicMinHeight;
72     private int mBigPicMaxHeight;
73     private int mAllImagesHeight;
74     private int mImageTextHeight;
75     private int mMaxHeight;
76     private int mMinHeight;
77 
GridContent(Context context, SliceItem gridItem)78     public GridContent(Context context, SliceItem gridItem) {
79         populate(gridItem);
80 
81         Resources res = context.getResources();
82         mBigPicMinHeight = res.getDimensionPixelSize(R.dimen.abc_slice_big_pic_min_height);
83         mBigPicMaxHeight = res.getDimensionPixelSize(R.dimen.abc_slice_big_pic_max_height);
84         mAllImagesHeight = res.getDimensionPixelSize(R.dimen.abc_slice_grid_image_only_height);
85         mImageTextHeight = res.getDimensionPixelSize(R.dimen.abc_slice_grid_image_text_height);
86         mMinHeight = res.getDimensionPixelSize(R.dimen.abc_slice_grid_min_height);
87         mMaxHeight = res.getDimensionPixelSize(R.dimen.abc_slice_grid_max_height);
88     }
89 
90     /**
91      * @return whether this grid has content that is valid to display.
92      */
populate(SliceItem gridItem)93     private boolean populate(SliceItem gridItem) {
94         mColorItem = SliceQuery.findSubtype(gridItem, FORMAT_INT, SUBTYPE_COLOR);
95         mSeeMoreItem = SliceQuery.find(gridItem, null, HINT_SEE_MORE, null);
96         if (mSeeMoreItem != null && FORMAT_SLICE.equals(mSeeMoreItem.getFormat())) {
97             mSeeMoreItem = mSeeMoreItem.getSlice().getItems().get(0);
98         }
99         String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE};
100         mPrimaryAction = SliceQuery.find(gridItem, FORMAT_SLICE, hints,
101                 new String[] {HINT_ACTIONS} /* nonHints */);
102         mAllImages = true;
103         if (FORMAT_SLICE.equals(gridItem.getFormat())) {
104             List<SliceItem> items = gridItem.getSlice().getItems();
105             if (items.size() == 1 && FORMAT_SLICE.equals(items.get(0).getFormat())) {
106                 // TODO: this can be removed at release
107                 items = items.get(0).getSlice().getItems();
108             }
109             items = filterAndProcessItems(items);
110             // Check if it it's only one item that is a slice
111             if (items.size() == 1 && items.get(0).getFormat().equals(FORMAT_SLICE)) {
112                 items = items.get(0).getSlice().getItems();
113             }
114             for (int i = 0; i < items.size(); i++) {
115                 SliceItem item = items.get(i);
116                 if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
117                     mContentDescr = item;
118                 } else {
119                     CellContent cc = new CellContent(item);
120                     processContent(cc);
121                 }
122             }
123         } else {
124             CellContent cc = new CellContent(gridItem);
125             processContent(cc);
126         }
127         return isValid();
128     }
129 
processContent(CellContent cc)130     private void processContent(CellContent cc) {
131         if (cc.isValid()) {
132             mGridContent.add(cc);
133             if (!cc.isImageOnly()) {
134                 mAllImages = false;
135             }
136             mMaxCellLineCount = Math.max(mMaxCellLineCount, cc.getTextCount());
137             mHasImage |= cc.hasImage();
138             mLargestImageMode = Math.max(mLargestImageMode, cc.getImageMode());
139         }
140     }
141 
142     /**
143      * @return the list of cell content for this grid.
144      */
145     @NonNull
getGridContent()146     public ArrayList<CellContent> getGridContent() {
147         return mGridContent;
148     }
149 
150     /**
151      * @return the color to tint content in this grid.
152      */
153     @Nullable
getColorItem()154     public SliceItem getColorItem() {
155         return mColorItem;
156     }
157 
158     /**
159      * @return the content intent item for this grid.
160      */
161     @Nullable
getContentIntent()162     public SliceItem getContentIntent() {
163         return mPrimaryAction;
164     }
165 
166     /**
167      * @return the see more item to use when not all items in the grid can be displayed.
168      */
169     @Nullable
getSeeMoreItem()170     public SliceItem getSeeMoreItem() {
171         return mSeeMoreItem;
172     }
173 
174     /**
175      * @return content description for this row.
176      */
177     @Nullable
getContentDescription()178     public CharSequence getContentDescription() {
179         return mContentDescr != null ? mContentDescr.getText() : null;
180     }
181 
182     /**
183      * @return whether this grid has content that is valid to display.
184      */
isValid()185     public boolean isValid() {
186         return mGridContent.size() > 0;
187     }
188 
189     /**
190      * @return whether the contents of this grid is just images.
191      */
isAllImages()192     public boolean isAllImages() {
193         return mAllImages;
194     }
195 
196     /**
197      * Filters non-cell items out of the list of items and finds content description.
198      */
filterAndProcessItems(List<SliceItem> items)199     private List<SliceItem> filterAndProcessItems(List<SliceItem> items) {
200         List<SliceItem> filteredItems = new ArrayList<>();
201         for (int i = 0; i < items.size(); i++) {
202             SliceItem item = items.get(i);
203             // TODO: This see more can be removed at release
204             boolean containsSeeMore = SliceQuery.find(item, null, HINT_SEE_MORE, null) != null;
205             boolean isNonCellContent = containsSeeMore
206                     || item.hasAnyHints(HINT_SHORTCUT, HINT_SEE_MORE, HINT_KEYWORDS, HINT_TTL,
207                             HINT_LAST_UPDATED);
208             if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
209                 mContentDescr = item;
210             } else if (!isNonCellContent) {
211                 filteredItems.add(item);
212             }
213         }
214         return filteredItems;
215     }
216 
217     /**
218      * @return the max number of lines of text in the cells of this grid row.
219      */
getMaxCellLineCount()220     public int getMaxCellLineCount() {
221         return mMaxCellLineCount;
222     }
223 
224     /**
225      * @return whether this row contains an image.
226      */
hasImage()227     public boolean hasImage() {
228         return mHasImage;
229     }
230 
231     /**
232      * @return the height to display a grid row at when it is used as a small template.
233      * Does not include padding that might be added by slice view attributes,
234      * see {@link ListContent#getListHeight(Context, List)}.
235      */
getSmallHeight()236     public int getSmallHeight() {
237         return getHeight(true /* isSmall */);
238     }
239 
240     /**
241      * @return the height the content in this template requires to be displayed.
242      * Does not include padding that might be added by slice view attributes,
243      * see {@link ListContent#getListHeight(Context, List)}.
244      */
getActualHeight()245     public int getActualHeight() {
246         return getHeight(false /* isSmall */);
247     }
248 
getHeight(boolean isSmall)249     private int getHeight(boolean isSmall) {
250         if (!isValid()) {
251             return 0;
252         }
253         if (mAllImages) {
254             return mGridContent.size() == 1
255                     ? isSmall ? mBigPicMinHeight : mBigPicMaxHeight
256                     : mLargestImageMode == ICON_IMAGE ? mMinHeight : mAllImagesHeight;
257         } else {
258             boolean twoLines = getMaxCellLineCount() > 1;
259             boolean hasImage = hasImage();
260             return (twoLines && !isSmall)
261                     ? hasImage ? mMaxHeight : mMinHeight
262                     : mLargestImageMode == ICON_IMAGE ? mMinHeight : mImageTextHeight;
263         }
264     }
265 
266     /**
267      * Extracts information required to present content in a cell.
268      * @hide
269      */
270     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
271     public static class CellContent {
272         private SliceItem mContentIntent;
273         private ArrayList<SliceItem> mCellItems = new ArrayList<>();
274         private SliceItem mContentDescr;
275         private int mTextCount;
276         private boolean mHasImage;
277         private int mImageMode = -1;
278 
CellContent(SliceItem cellItem)279         public CellContent(SliceItem cellItem) {
280             populate(cellItem);
281         }
282 
283         /**
284          * @return whether this row has content that is valid to display.
285          */
populate(SliceItem cellItem)286         public boolean populate(SliceItem cellItem) {
287             final String format = cellItem.getFormat();
288             if (!cellItem.hasHint(HINT_SHORTCUT)
289                     && (FORMAT_SLICE.equals(format) || FORMAT_ACTION.equals(format))) {
290                 List<SliceItem> items = cellItem.getSlice().getItems();
291                 // If we've only got one item that's a slice / action use those items instead
292                 if (items.size() == 1 && (FORMAT_ACTION.equals(items.get(0).getFormat())
293                         || FORMAT_SLICE.equals(items.get(0).getFormat()))) {
294                     mContentIntent = items.get(0);
295                     items = items.get(0).getSlice().getItems();
296                 }
297                 if (FORMAT_ACTION.equals(format)) {
298                     mContentIntent = cellItem;
299                 }
300                 mTextCount = 0;
301                 int imageCount = 0;
302                 for (int i = 0; i < items.size(); i++) {
303                     final SliceItem item = items.get(i);
304                     final String itemFormat = item.getFormat();
305                     if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
306                         mContentDescr = item;
307                     } else if (mTextCount < 2 && (FORMAT_TEXT.equals(itemFormat)
308                             || FORMAT_LONG.equals(itemFormat))) {
309                         mTextCount++;
310                         mCellItems.add(item);
311                     } else if (imageCount < 1 && FORMAT_IMAGE.equals(item.getFormat())) {
312                         if (item.hasHint(Slice.HINT_NO_TINT)) {
313                             mImageMode = item.hasHint(Slice.HINT_LARGE)
314                                     ? LARGE_IMAGE
315                                     : SMALL_IMAGE;
316                         } else {
317                             mImageMode = ICON_IMAGE;
318                         }
319                         imageCount++;
320                         mHasImage = true;
321                         mCellItems.add(item);
322                     }
323                 }
324             } else if (isValidCellContent(cellItem)) {
325                 mCellItems.add(cellItem);
326             }
327             return isValid();
328         }
329 
330         /**
331          * @return the action to activate when this cell is tapped.
332          */
getContentIntent()333         public SliceItem getContentIntent() {
334             return mContentIntent;
335         }
336 
337         /**
338          * @return the slice items to display in this cell.
339          */
getCellItems()340         public ArrayList<SliceItem> getCellItems() {
341             return mCellItems;
342         }
343 
344         /**
345          * @return whether this is content that is valid to show in a grid cell.
346          */
isValidCellContent(SliceItem cellItem)347         private boolean isValidCellContent(SliceItem cellItem) {
348             final String format = cellItem.getFormat();
349             boolean isNonCellContent = SUBTYPE_CONTENT_DESCRIPTION.equals(cellItem.getSubType())
350                     || cellItem.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED);
351             return !isNonCellContent
352                     && (FORMAT_TEXT.equals(format)
353                     || FORMAT_LONG.equals(format)
354                     || FORMAT_IMAGE.equals(format));
355         }
356 
357         /**
358          * @return whether this grid has content that is valid to display.
359          */
isValid()360         public boolean isValid() {
361             return mCellItems.size() > 0 && mCellItems.size() <= 3;
362         }
363 
364         /**
365          * @return whether this cell contains just an image.
366          */
isImageOnly()367         public boolean isImageOnly() {
368             return mCellItems.size() == 1 && FORMAT_IMAGE.equals(mCellItems.get(0).getFormat());
369         }
370 
371         /**
372          * @return number of text items in this cell.
373          */
getTextCount()374         public int getTextCount() {
375             return mTextCount;
376         }
377 
378         /**
379          * @return whether this cell contains an image.
380          */
hasImage()381         public boolean hasImage() {
382             return mHasImage;
383         }
384 
385         /**
386          * @return the mode of the image.
387          */
getImageMode()388         public int getImageMode() {
389             return mImageMode;
390         }
391 
392         @Nullable
getContentDescription()393         public CharSequence getContentDescription() {
394             return mContentDescr != null ? mContentDescr.getText() : null;
395         }
396     }
397 }
398