Home | History | Annotate | Download | only in widget
      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 
     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