GridContent.java

/*
 * Copyright 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.slice.widget;

import static android.app.slice.Slice.HINT_ACTIONS;
import static android.app.slice.Slice.HINT_KEYWORDS;
import static android.app.slice.Slice.HINT_LAST_UPDATED;
import static android.app.slice.Slice.HINT_SEE_MORE;
import static android.app.slice.Slice.HINT_SHORTCUT;
import static android.app.slice.Slice.HINT_TITLE;
import static android.app.slice.Slice.HINT_TTL;
import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static android.app.slice.SliceItem.FORMAT_TEXT;

import static androidx.slice.core.SliceHints.HINT_OVERLAY;
import static androidx.slice.core.SliceHints.SUBTYPE_DATE_PICKER;
import static androidx.slice.core.SliceHints.SUBTYPE_TIME_PICKER;
import static androidx.slice.core.SliceHints.UNKNOWN_IMAGE;

import android.content.Context;
import android.graphics.Point;
import android.graphics.drawable.Drawable;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.graphics.drawable.IconCompat;
import androidx.slice.SliceItem;
import androidx.slice.SliceUtils;
import androidx.slice.core.SliceAction;
import androidx.slice.core.SliceActionImpl;
import androidx.slice.core.SliceQuery;

import java.util.ArrayList;
import java.util.List;

/**
 * Extracts information required to present content in a grid format from a slice.
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@RequiresApi(19)
public class GridContent extends SliceContent {

    private boolean mAllImages;
    private SliceItem mPrimaryAction;
    private final ArrayList<CellContent> mGridContent = new ArrayList<>();
    private SliceItem mSeeMoreItem;
    private int mMaxCellLineCount;
    private int mLargestImageMode = UNKNOWN_IMAGE;
    private boolean mIsLastIndex;
    private IconCompat mFirstImage = null;
    private Point mFirstImageSize = null;

    private SliceItem mTitleItem;

    public GridContent(SliceItem gridItem, int position) {
        super(gridItem, position);
        populate(gridItem);
    }

    /**
     * @return whether this grid has content that is valid to display.
     */
    private boolean populate(SliceItem gridItem) {
        mSeeMoreItem = SliceQuery.find(gridItem, null, HINT_SEE_MORE, null);
        if (mSeeMoreItem != null && FORMAT_SLICE.equals(mSeeMoreItem.getFormat())) {
            List<SliceItem> seeMoreItems = mSeeMoreItem.getSlice().getItems();
            if (seeMoreItems != null && seeMoreItems.size() > 0) {
                mSeeMoreItem = seeMoreItems.get(0);
            }
        }
        String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE};
        mPrimaryAction = SliceQuery.find(gridItem, FORMAT_SLICE, hints,
                new String[] {HINT_ACTIONS} /* nonHints */);
        mAllImages = true;
        if (FORMAT_SLICE.equals(gridItem.getFormat())) {
            List<SliceItem> items = gridItem.getSlice().getItems();
            items = filterAndProcessItems(items);
            for (int i = 0; i < items.size(); i++) {
                SliceItem item = items.get(i);
                if (!SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
                    CellContent cc = new CellContent(item);
                    processContent(cc);
                }
            }
        } else {
            CellContent cc = new CellContent(gridItem);
            processContent(cc);
        }
        return isValid();
    }

    private void processContent(CellContent cc) {
        if (cc.isValid()) {
            if ((mTitleItem == null && cc.getTitleItem() != null)) {
                mTitleItem = cc.getTitleItem();
            }
            mGridContent.add(cc);
            if (!cc.isImageOnly()) {
                mAllImages = false;
            }
            mMaxCellLineCount = Math.max(mMaxCellLineCount, cc.getTextCount());
            if (mFirstImage == null && cc.hasImage()) {
                mFirstImage = cc.getImageIcon();
            }
            mLargestImageMode = mLargestImageMode == UNKNOWN_IMAGE
                    ? cc.getImageMode()
                    : Math.max(mLargestImageMode, cc.getImageMode());
        }
    }

    /**
     * @return the title of this grid row, if it exists.
     */
    @Nullable
    public CharSequence getTitle() {
        if (mTitleItem != null) {
            return mTitleItem.getSanitizedText();
        } else if (mPrimaryAction != null) {
            return new SliceActionImpl(mPrimaryAction).getTitle();
        }
        return null;
    }

    /**
     * @return the list of cell content for this grid.
     */
    @NonNull
    public ArrayList<CellContent> getGridContent() {
        return mGridContent;
    }

    /**
     * @return the content intent item for this grid.
     */
    @Nullable
    public SliceItem getContentIntent() {
        return mPrimaryAction;
    }

    /**
     * @return the see more item to use when not all items in the grid can be displayed.
     */
    @Nullable
    public SliceItem getSeeMoreItem() {
        return mSeeMoreItem;
    }

    /**
     * @return whether this grid has content that is valid to display.
     */
    @Override
    public boolean isValid() {
        return super.isValid() && mGridContent.size() > 0;
    }

    /**
     * @return whether the contents of this grid is just images.
     */
    public boolean isAllImages() {
        return mAllImages;
    }

    /**
     * @return the largest image size in this row, if there are images.
     */
    public int getLargestImageMode() {
        return mLargestImageMode;
    }

    /**
     * @return the first image dimensions in this row, if there are images.
     */
    @NonNull
    public Point getFirstImageSize(@NonNull Context context) {
        if (mFirstImage == null) {
            return new Point(-1, -1);
        }
        if (mFirstImageSize == null) {
            Drawable d = mFirstImage.loadDrawable(context);
            mFirstImageSize = new Point(d.getIntrinsicWidth(), d.getIntrinsicHeight());
        }
        return mFirstImageSize;
    }

    /**
     * Filters non-cell items out of the list of items and finds content description.
     */
    private List<SliceItem> filterAndProcessItems(List<SliceItem> items) {

        List<SliceItem> filteredItems = new ArrayList<>();
        for (int i = 0; i < items.size(); i++) {
            SliceItem item = items.get(i);
            // TODO: This see more can be removed at release
            boolean containsSeeMore = SliceQuery.find(item, null, HINT_SEE_MORE, null) != null;
            boolean isNonCellContent = containsSeeMore
                    || item.hasAnyHints(HINT_SHORTCUT, HINT_SEE_MORE, HINT_KEYWORDS, HINT_TTL,
                    HINT_LAST_UPDATED, HINT_OVERLAY);
            if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
                mContentDescr = item;
            } else if (!isNonCellContent) {
                filteredItems.add(item);
            }
        }
        return filteredItems;
    }

    /**
     * @return the max number of lines of text in the cells of this grid row.
     */
    public int getMaxCellLineCount() {
        return mMaxCellLineCount;
    }

    /**
     * @return whether this row contains an image.
     */
    public boolean hasImage() {
        return mFirstImage != null;
    }

    /**
     * @return whether this content is being displayed last in a list.
     */
    public boolean getIsLastIndex() { return mIsLastIndex; }

    /**
     * Sets whether this content is being displayed last in a list.
     */
    public void setIsLastIndex(boolean isLast) {
        mIsLastIndex = isLast;
    }

    @Override
    public int getHeight(SliceStyle style, SliceViewPolicy policy) {
        return style.getGridHeight(this, policy);
    }

    /**
     * Extracts information required to present content in a cell.
     *
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
    public static class CellContent {
        private SliceItem mContentIntent;
        private SliceItem mPicker;
        private final ArrayList<SliceItem> mCellItems = new ArrayList<>();
        private SliceItem mContentDescr;
        private int mTextCount;
        private int mImageCount;
        private IconCompat mImage;
        private SliceItem mOverlayItem;
        private int mImageMode = -1;
        private SliceItem mTitleItem;
        private SliceItem mToggleItem;

        public CellContent(SliceItem cellItem) {
            populate(cellItem);
        }

        /**
         * @return whether this row has content that is valid to display.
         */
        public boolean populate(SliceItem cellItem) {
            final String format = cellItem.getFormat();
            if (!cellItem.hasHint(HINT_SHORTCUT)
                    && (FORMAT_SLICE.equals(format) || FORMAT_ACTION.equals(format))) {
                List<SliceItem> items = cellItem.getSlice().getItems();
                List<SliceItem> sliceActionItems = null;

                // Fill the sliceActionItems with the first showing SliceAction in items.
                for (SliceItem item : items) {
                    if ((FORMAT_ACTION.equals(item.getFormat())
                            || FORMAT_SLICE.equals(item.getFormat()))
                            && !(SUBTYPE_DATE_PICKER.equals(item.getSubType())
                            || SUBTYPE_TIME_PICKER.equals(item.getSubType()))) {
                        sliceActionItems = item.getSlice().getItems();
                        SliceAction ac = new SliceActionImpl(item);
                        if (ac.isToggle()) {
                            mToggleItem = item;
                        } else {
                            mContentIntent = items.get(0);
                        }
                        break;
                    }
                }
                if (FORMAT_ACTION.equals(format)) {
                    mContentIntent = cellItem;
                }
                mTextCount = 0;
                mImageCount = 0;
                fillCellItems(items);

                if (mTextCount == 0 && mImageCount == 0 && sliceActionItems != null) {
                    fillCellItems(sliceActionItems);
                }
            } else if (isValidCellContent(cellItem)) {
                mCellItems.add(cellItem);
            }
            return isValid();
        }

        private void fillCellItems(List<SliceItem> items) {
            for (int i = 0; i < items.size(); i++) {
                final SliceItem item = items.get(i);
                final String itemFormat = item.getFormat();
                if (mPicker == null && (SUBTYPE_DATE_PICKER.equals(item.getSubType())
                        || SUBTYPE_TIME_PICKER.equals(item.getSubType()))) {
                    mPicker = item;
                } else if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
                    mContentDescr = item;
                } else if (mTextCount < 2 && (FORMAT_TEXT.equals(itemFormat)
                        || FORMAT_LONG.equals(itemFormat))) {
                    if (mTitleItem == null
                            || (!mTitleItem.hasHint(HINT_TITLE) && item.hasHint(HINT_TITLE))) {
                        mTitleItem = item;
                    }
                    if (item.hasHint(HINT_OVERLAY)) {
                        if (mOverlayItem == null) {
                            mOverlayItem = item;
                        }
                    } else {
                        mTextCount++;
                        mCellItems.add(item);
                    }
                } else if (mImageCount < 1 && FORMAT_IMAGE.equals(item.getFormat())) {
                    mImageMode = SliceUtils.parseImageMode(item);
                    mImageCount++;
                    mImage = item.getIcon();
                    mCellItems.add(item);
                }
            }
        }


        /**
         * @return toggle slice item if this cell has one.
         */
        @Nullable
        public SliceItem getToggleItem() {
            return mToggleItem;
        }

        /**
         * @return title text slice item if this cell has one.
         */
        @Nullable
        public SliceItem getTitleItem() {
            return mTitleItem;
        }

        /**
         * @return image overlay text slice item if this cell has one.
         */
        @Nullable
        public SliceItem getOverlayItem() {
            return mOverlayItem;
        }

        /**
         * @return the action to activate when this cell is tapped.
         */
        @Nullable
        public SliceItem getContentIntent() {
            return mContentIntent;
        }

        /**
         * @return the Picker to use when this cell is tapped.
         */
        @Nullable
        public SliceItem getPicker() {
            return mPicker;
        }

        /**
         * @return the slice items to display in this cell.
         */
        @NonNull
        public ArrayList<SliceItem> getCellItems() {
            return mCellItems;
        }

        /**
         * @return whether this is content that is valid to show in a grid cell.
         */
        private boolean isValidCellContent(SliceItem cellItem) {
            final String format = cellItem.getFormat();
            boolean isNonCellContent = SUBTYPE_CONTENT_DESCRIPTION.equals(cellItem.getSubType())
                    || cellItem.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED);
            return !isNonCellContent
                    && (FORMAT_TEXT.equals(format)
                    || FORMAT_LONG.equals(format)
                    || FORMAT_IMAGE.equals(format));
        }

        /**
         * @return whether this grid has content that is valid to display.
         */
        public boolean isValid() {
            return mPicker != null || (mCellItems.size() > 0 && mCellItems.size() <= 3);
        }

        /**
         * @return whether this cell contains just an image.
         */
        public boolean isImageOnly() {
            return mCellItems.size() == 1 && FORMAT_IMAGE.equals(mCellItems.get(0).getFormat());
        }

        /**
         * @return number of text items in this cell.
         */
        public int getTextCount() {
            return mTextCount;
        }

        /**
         * @return whether this cell contains an image.
         */
        public boolean hasImage() {
            return mImage != null;
        }

        /**
         * @return the mode of the image.
         */
        public int getImageMode() {
            return mImageMode;
        }

        /**
         * @return the IconCompat of the image.
         */
        @Nullable
        public IconCompat getImageIcon() {
            return mImage;
        }

        @Nullable
        public CharSequence getContentDescription() {
            return mContentDescr != null ? mContentDescr.getText() : null;
        }
    }
}