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_HORIZONTAL;
     20 import static android.app.slice.Slice.HINT_PARTIAL;
     21 import static android.app.slice.Slice.HINT_SEE_MORE;
     22 import static android.app.slice.Slice.HINT_SHORTCUT;
     23 import static android.app.slice.Slice.HINT_SUMMARY;
     24 import static android.app.slice.Slice.HINT_TITLE;
     25 import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
     26 import static android.app.slice.Slice.SUBTYPE_RANGE;
     27 import static android.app.slice.SliceItem.FORMAT_ACTION;
     28 import static android.app.slice.SliceItem.FORMAT_IMAGE;
     29 import static android.app.slice.SliceItem.FORMAT_INT;
     30 import static android.app.slice.SliceItem.FORMAT_LONG;
     31 import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
     32 import static android.app.slice.SliceItem.FORMAT_SLICE;
     33 import static android.app.slice.SliceItem.FORMAT_TEXT;
     34 
     35 import static androidx.slice.core.SliceHints.HINT_KEYWORDS;
     36 import static androidx.slice.core.SliceHints.HINT_LAST_UPDATED;
     37 import static androidx.slice.core.SliceHints.HINT_TTL;
     38 
     39 import android.content.Context;
     40 import android.text.TextUtils;
     41 import android.util.Log;
     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.SliceAction;
     48 import androidx.slice.core.SliceActionImpl;
     49 import androidx.slice.core.SliceQuery;
     50 import androidx.slice.view.R;
     51 
     52 import java.util.ArrayList;
     53 import java.util.List;
     54 
     55 /**
     56  * Extracts information required to present content in a row format from a slice.
     57  * @hide
     58  */
     59 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     60 public class RowContent {
     61     private static final String TAG = "RowContent";
     62 
     63     private SliceItem mPrimaryAction;
     64     private SliceItem mRowSlice;
     65     private SliceItem mStartItem;
     66     private SliceItem mTitleItem;
     67     private SliceItem mSubtitleItem;
     68     private SliceItem mSummaryItem;
     69     private ArrayList<SliceItem> mEndItems = new ArrayList<>();
     70     private ArrayList<SliceAction> mToggleItems = new ArrayList<>();
     71     private SliceItem mRange;
     72     private SliceItem mContentDescr;
     73     private boolean mEndItemsContainAction;
     74     private boolean mIsHeader;
     75     private int mLineCount = 0;
     76     private int mMaxHeight;
     77     private int mMinHeight;
     78     private int mRangeHeight;
     79 
     80     public RowContent(Context context, SliceItem rowSlice, boolean isHeader) {
     81         populate(rowSlice, isHeader);
     82         mMaxHeight = context.getResources().getDimensionPixelSize(R.dimen.abc_slice_row_max_height);
     83         mMinHeight = context.getResources().getDimensionPixelSize(R.dimen.abc_slice_row_min_height);
     84         mRangeHeight = context.getResources().getDimensionPixelSize(
     85                 R.dimen.abc_slice_row_range_height);
     86     }
     87 
     88     /**
     89      * @return whether this row has content that is valid to display.
     90      */
     91     private boolean populate(SliceItem rowSlice, boolean isHeader) {
     92         mIsHeader = isHeader;
     93         mRowSlice = rowSlice;
     94         if (!isValidRow(rowSlice)) {
     95             Log.w(TAG, "Provided SliceItem is invalid for RowContent");
     96             return false;
     97         }
     98         determineStartAndPrimaryAction(rowSlice);
     99 
    100         mContentDescr = SliceQuery.findSubtype(rowSlice, FORMAT_TEXT, SUBTYPE_CONTENT_DESCRIPTION);
    101 
    102         // Filter anything not viable for displaying in a row
    103         ArrayList<SliceItem> rowItems = filterInvalidItems(rowSlice);
    104         // If we've only got one item that's a slice / action use those items instead
    105         if (rowItems.size() == 1 && (FORMAT_ACTION.equals(rowItems.get(0).getFormat())
    106                 || FORMAT_SLICE.equals(rowItems.get(0).getFormat()))
    107                 && !rowItems.get(0).hasAnyHints(HINT_SHORTCUT, HINT_TITLE)) {
    108             if (isValidRow(rowItems.get(0))) {
    109                 rowSlice = rowItems.get(0);
    110                 rowItems = filterInvalidItems(rowSlice);
    111             }
    112         }
    113         if (SUBTYPE_RANGE.equals(rowSlice.getSubType())) {
    114             mRange = rowSlice;
    115         }
    116         if (rowItems.size() > 0) {
    117             // Remove the things we already know about
    118             if (mStartItem != null) {
    119                 rowItems.remove(mStartItem);
    120             }
    121             if (mPrimaryAction != null) {
    122                 rowItems.remove(mPrimaryAction);
    123             }
    124 
    125             // Text + end items
    126             ArrayList<SliceItem> endItems = new ArrayList<>();
    127             for (int i = 0; i < rowItems.size(); i++) {
    128                 final SliceItem item = rowItems.get(i);
    129                 if (FORMAT_TEXT.equals(item.getFormat())) {
    130                     if ((mTitleItem == null || !mTitleItem.hasHint(HINT_TITLE))
    131                             && item.hasHint(HINT_TITLE) && !item.hasHint(HINT_SUMMARY)) {
    132                         mTitleItem = item;
    133                     } else if (mSubtitleItem == null && !item.hasHint(HINT_SUMMARY)) {
    134                         mSubtitleItem = item;
    135                     } else if (mSummaryItem == null && item.hasHint(HINT_SUMMARY)) {
    136                         mSummaryItem = item;
    137                     }
    138                 } else {
    139                     endItems.add(item);
    140                 }
    141             }
    142             if (hasText(mTitleItem)) {
    143                 mLineCount++;
    144             }
    145             if (hasText(mSubtitleItem)) {
    146                 mLineCount++;
    147             }
    148             // Special rules for end items: only one timestamp
    149             boolean hasTimestamp = mStartItem != null
    150                     && FORMAT_LONG.equals(mStartItem.getFormat());
    151             for (int i = 0; i < endItems.size(); i++) {
    152                 final SliceItem item = endItems.get(i);
    153                 boolean isAction = SliceQuery.find(item, FORMAT_ACTION) != null;
    154                 if (FORMAT_LONG.equals(item.getFormat())) {
    155                     if (!hasTimestamp) {
    156                         hasTimestamp = true;
    157                         mEndItems.add(item);
    158                     }
    159                 } else {
    160                     processContent(item, isAction);
    161                 }
    162             }
    163         }
    164         return isValid();
    165     }
    166 
    167     private void processContent(@NonNull SliceItem item, boolean isAction) {
    168         if (isAction) {
    169             SliceAction ac = new SliceActionImpl(item);
    170             if (ac.isToggle()) {
    171                 mToggleItems.add(ac);
    172             }
    173         }
    174         mEndItems.add(item);
    175         mEndItemsContainAction |= isAction;
    176     }
    177 
    178     /**
    179      * Sets the {@link #getPrimaryAction()} and {@link #getStartItem()} for this row.
    180      */
    181     private void determineStartAndPrimaryAction(@NonNull SliceItem rowSlice) {
    182         List<SliceItem> possibleStartItems = SliceQuery.findAll(rowSlice, null, HINT_TITLE, null);
    183         if (possibleStartItems.size() > 0) {
    184             // The start item will be at position 0 if it exists
    185             String format = possibleStartItems.get(0).getFormat();
    186             if ((FORMAT_ACTION.equals(format)
    187                     && SliceQuery.find(possibleStartItems.get(0), FORMAT_IMAGE) != null)
    188                     || FORMAT_SLICE.equals(format)
    189                     || FORMAT_LONG.equals(format)
    190                     || FORMAT_IMAGE.equals(format)) {
    191                 mStartItem = possibleStartItems.get(0);
    192             }
    193         }
    194 
    195         String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE};
    196         List<SliceItem> possiblePrimaries = SliceQuery.findAll(rowSlice, FORMAT_SLICE, hints, null);
    197         if (possiblePrimaries.isEmpty() && FORMAT_ACTION.equals(rowSlice.getFormat())
    198                 && rowSlice.getSlice().getItems().size() == 1) {
    199             mPrimaryAction = rowSlice;
    200         } else if (mStartItem != null && possiblePrimaries.size() > 1
    201                 && possiblePrimaries.get(0) == mStartItem) {
    202             // Next item is the primary action
    203             mPrimaryAction = possiblePrimaries.get(1);
    204         } else if (possiblePrimaries.size() > 0) {
    205             mPrimaryAction = possiblePrimaries.get(0);
    206         }
    207     }
    208 
    209     /**
    210      * @return the {@link SliceItem} used to populate this row.
    211      */
    212     @NonNull
    213     public SliceItem getSlice() {
    214         return mRowSlice;
    215     }
    216 
    217     /**
    218      * @return the {@link SliceItem} representing the range in the row; can be null.
    219      */
    220     @Nullable
    221     public SliceItem getRange() {
    222         return mRange;
    223     }
    224 
    225     /**
    226      * @return the {@link SliceItem} for the icon to use for the input range thumb drawable.
    227      */
    228     @Nullable
    229     public SliceItem getInputRangeThumb() {
    230         if (mRange != null) {
    231             List<SliceItem> items = mRange.getSlice().getItems();
    232             for (int i = 0; i < items.size(); i++) {
    233                 if (FORMAT_IMAGE.equals(items.get(i).getFormat())) {
    234                     return items.get(i);
    235                 }
    236             }
    237         }
    238         return null;
    239     }
    240 
    241     /**
    242      * @return the {@link SliceItem} used for the main intent for this row; can be null.
    243      */
    244     @Nullable
    245     public SliceItem getPrimaryAction() {
    246         return mPrimaryAction;
    247     }
    248 
    249     /**
    250      * @return the {@link SliceItem} to display at the start of this row; can be null.
    251      */
    252     @Nullable
    253     public SliceItem getStartItem() {
    254         return mIsHeader ? null : mStartItem;
    255     }
    256 
    257     /**
    258      * @return the {@link SliceItem} representing the title text for this row; can be null.
    259      */
    260     @Nullable
    261     public SliceItem getTitleItem() {
    262         return mTitleItem;
    263     }
    264 
    265     /**
    266      * @return the {@link SliceItem} representing the subtitle text for this row; can be null.
    267      */
    268     @Nullable
    269     public SliceItem getSubtitleItem() {
    270         return mSubtitleItem;
    271     }
    272 
    273     @Nullable
    274     public SliceItem getSummaryItem() {
    275         return mSummaryItem == null ? mSubtitleItem : mSummaryItem;
    276     }
    277 
    278     /**
    279      * @return the list of {@link SliceItem} that can be shown as items at the end of the row.
    280      */
    281     public ArrayList<SliceItem> getEndItems() {
    282         return mEndItems;
    283     }
    284 
    285     /**
    286      * @return a list of toggles associated with this row.
    287      */
    288     public ArrayList<SliceAction> getToggleItems() {
    289         return mToggleItems;
    290     }
    291 
    292     /**
    293      * @return the content description to use for this row.
    294      */
    295     @Nullable
    296     public CharSequence getContentDescription() {
    297         return mContentDescr != null ? mContentDescr.getText() : null;
    298     }
    299 
    300     /**
    301      * @return whether {@link #getEndItems()} contains a SliceItem with FORMAT_SLICE, HINT_SHORTCUT
    302      */
    303     public boolean endItemsContainAction() {
    304         return mEndItemsContainAction;
    305     }
    306 
    307     /**
    308      * @return the number of lines of text contained in this row.
    309      */
    310     public int getLineCount() {
    311         return mLineCount;
    312     }
    313 
    314     /**
    315      * @return the height to display a row at when it is used as a small template.
    316      */
    317     public int getSmallHeight() {
    318         return getRange() != null
    319                 ? getActualHeight()
    320                 : mMaxHeight;
    321     }
    322 
    323     /**
    324      * @return the height the content in this template requires to be displayed.
    325      */
    326     public int getActualHeight() {
    327         if (!isValid()) {
    328             return 0;
    329         }
    330         int rowHeight = (getLineCount() > 1 || mIsHeader) ? mMaxHeight : mMinHeight;
    331         if (getRange() != null) {
    332             if (getLineCount() > 0) {
    333                 rowHeight += mRangeHeight;
    334             } else {
    335                 rowHeight = mIsHeader ? mMaxHeight : mRangeHeight;
    336             }
    337         }
    338         return rowHeight;
    339     }
    340 
    341     private static boolean hasText(SliceItem textSlice) {
    342         return textSlice != null
    343                 && (textSlice.hasHint(HINT_PARTIAL)
    344                     || !TextUtils.isEmpty(textSlice.getText()));
    345     }
    346 
    347     /**
    348      * @return whether this row content represents a default see more item.
    349      */
    350     public boolean isDefaultSeeMore() {
    351         return FORMAT_ACTION.equals(mRowSlice.getFormat())
    352                 && mRowSlice.getSlice().hasHint(HINT_SEE_MORE)
    353                 && mRowSlice.getSlice().getItems().isEmpty();
    354     }
    355 
    356     /**
    357      * @return whether this row has content that is valid to display.
    358      */
    359     public boolean isValid() {
    360         return mStartItem != null
    361                 || mPrimaryAction != null
    362                 || mTitleItem != null
    363                 || mSubtitleItem != null
    364                 || mEndItems.size() > 0
    365                 || mRange != null
    366                 || isDefaultSeeMore();
    367     }
    368 
    369     /**
    370      * @return whether this is a valid item to use to populate a row of content.
    371      */
    372     private static boolean isValidRow(SliceItem rowSlice) {
    373         if (rowSlice == null) {
    374             return false;
    375         }
    376         // Must be slice or action
    377         if (FORMAT_SLICE.equals(rowSlice.getFormat())
    378                 || FORMAT_ACTION.equals(rowSlice.getFormat())) {
    379             List<SliceItem> rowItems = rowSlice.getSlice().getItems();
    380             // Special case: default see more just has an action but no other items
    381             if (rowSlice.hasHint(HINT_SEE_MORE) && rowItems.isEmpty()) {
    382                 return true;
    383             }
    384             // Must have at least one legitimate child
    385             for (int i = 0; i < rowItems.size(); i++) {
    386                 if (isValidRowContent(rowSlice, rowItems.get(i))) {
    387                     return true;
    388                 }
    389             }
    390         }
    391         return false;
    392     }
    393 
    394     /**
    395      * @return list of {@link SliceItem}s that are valid to display in a row according
    396      * to {@link #isValidRowContent(SliceItem, SliceItem)}.
    397      */
    398     private static ArrayList<SliceItem> filterInvalidItems(SliceItem rowSlice) {
    399         ArrayList<SliceItem> filteredList = new ArrayList<>();
    400         for (SliceItem i : rowSlice.getSlice().getItems()) {
    401             if (isValidRowContent(rowSlice, i)) {
    402                 filteredList.add(i);
    403             }
    404         }
    405         return filteredList;
    406     }
    407 
    408     /**
    409      * @return whether this item is valid content to visibly appear in a row.
    410      */
    411     private static boolean isValidRowContent(SliceItem slice, SliceItem item) {
    412         if (item.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED, HINT_HORIZONTAL)
    413                 || SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
    414             return false;
    415         }
    416         final String itemFormat = item.getFormat();
    417         return FORMAT_IMAGE.equals(itemFormat)
    418                 || FORMAT_TEXT.equals(itemFormat)
    419                 || FORMAT_LONG.equals(itemFormat)
    420                 || FORMAT_ACTION.equals(itemFormat)
    421                 || FORMAT_REMOTE_INPUT.equals(itemFormat)
    422                 || FORMAT_SLICE.equals(itemFormat)
    423                 || (FORMAT_INT.equals(itemFormat) && SUBTYPE_RANGE.equals(slice.getSubType()));
    424     }
    425 }
    426