GridRowBuilder.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.builders;

import static androidx.annotation.RestrictTo.Scope.LIBRARY;

import android.app.PendingIntent;
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.graphics.drawable.IconCompat;
import androidx.core.util.Consumer;
import androidx.core.util.Pair;

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


/**
 * Builder to construct a grid row which may be added as a row to {@link ListBuilder}.
 * <p>
 * A grid row supports cells of vertically laid out content in a single row. Each cell can
 * contain a combination of text and images and is constructed using a {@link CellBuilder}.
 * <p>
 * A grid supports a couple of image types:
 * <ul>
 *     <li>{@link ListBuilder#ICON_IMAGE} - icon images are expected to be tintable and are
 *     shown at a standard icon size.</li>
 *     <li>{@link ListBuilder#SMALL_IMAGE} - small images are not tinted and are shown at
 *     a small size.</li>
 *     <li>{@link ListBuilder#LARGE_IMAGE} - large images are not tinted and are shown as
 *     large as they can be, in a {@link android.widget.ImageView.ScaleType#CENTER_CROP}</li>
 * </ul>
 * <p>
 * If more cells are added to the grid row than can be displayed, the cells will be cut off. Using
 * {@link #setSeeMoreAction(PendingIntent)} you can specify an action to take the user to see the
 * rest of the content, this will take up space as a cell item in a row if added.
 *
 * @see ListBuilder#addGridRow(GridRowBuilder)
 */
@RequiresApi(19)
public class GridRowBuilder {

    private final List<CellBuilder> mCells = new ArrayList<>();
    private boolean mHasSeeMore;
    private CellBuilder mSeeMoreCell;
    private PendingIntent mSeeMoreIntent;
    private SliceAction mPrimaryAction;
    private CharSequence mDescription;
    private int mLayoutDirection = -1;

    /**
     * Create a builder which will construct a slice displayed in a grid format.
     */
    public GridRowBuilder() {
    }

    /**
     * Create a builder which will construct a slice displayed in a grid format.
     * @param parent The builder constructing the parent slice.
     * @hide
     */
    @RestrictTo(LIBRARY)
    public GridRowBuilder(@NonNull ListBuilder parent) {
    }

    /**
     * Add a cell to the grid builder.
     */
    @NonNull
    public GridRowBuilder addCell(@NonNull CellBuilder builder) {
        mCells.add(builder);
        return this;
    }

    /**
     * Add a cell to the grid builder.
     * @hide
     */
    @NonNull
    @RestrictTo(LIBRARY)
    public GridRowBuilder addCell(@NonNull Consumer<CellBuilder> c) {
        CellBuilder b = new CellBuilder(this);
        c.accept(b);
        return addCell(b);
    }

    /**
     * If all content in a slice cannot be shown, the cell added here may be displayed where the
     * content is cut off.
     * <p>
     * This method should only be used if you want to display a custom cell to indicate more
     * content, consider using {@link #setSeeMoreAction(PendingIntent)} otherwise. If you do
     * choose to specify a custom cell, the cell should have
     * {@link CellBuilder#setContentIntent(PendingIntent)} specified to take the user to an
     * activity to see all of the content.
     * </p>
     * <p>
     * Only one see more affordance can be added, this throws {@link IllegalStateException} if
     * a row or action has been previously added.
     * </p>
     */
    @NonNull
    public GridRowBuilder setSeeMoreCell(@NonNull CellBuilder builder) {
        if (mHasSeeMore) {
            throw new IllegalStateException("Trying to add see more cell when one has "
                    + "already been added");
        }
        mSeeMoreCell = builder;
        mHasSeeMore = true;
        return this;
    }

    /**
     * If all content in a slice cannot be shown, the cell added here may be displayed where the
     * content is cut off.
     * <p>
     * This method should only be used if you want to display a custom cell to indicate more
     * content, consider using {@link #setSeeMoreAction(PendingIntent)} otherwise. If you do
     * choose to specify a custom cell, the cell should have
     * {@link CellBuilder#setContentIntent(PendingIntent)} specified to take the user to an
     * activity to see all of the content.
     * </p>
     * <p>
     * Only one see more affordance can be added, this throws {@link IllegalStateException} if
     * a row or action has been previously added.
     * </p>
     * @hide
     */
    @NonNull
    @RestrictTo(LIBRARY)
    public GridRowBuilder setSeeMoreCell(@NonNull Consumer<CellBuilder> c) {
        CellBuilder b = new CellBuilder(this);
        c.accept(b);
        return setSeeMoreCell(b);
    }

    /**
     * If all content in a slice cannot be shown, a "see more" affordance may be displayed where
     * the content is cut off. The action added here should take the user to an activity to see
     * all of the content, and will be invoked when the "see more" affordance is tapped.
     * <p>
     * Only one see more affordance can be added, this throws {@link IllegalStateException} if
     * a row or action has been previously added.
     * </p>
     */
    @NonNull
    public GridRowBuilder setSeeMoreAction(@NonNull PendingIntent intent) {
        if (mHasSeeMore) {
            throw new IllegalStateException("Trying to add see more action when one has "
                    + "already been added");
        }
        mSeeMoreIntent = intent;
        mHasSeeMore = true;
        return this;
    }

    /**
     * Sets the intent to send when the whole grid row is clicked.
     * <p>
     * If all the cells in the grid have specified a
     * {@link CellBuilder#setPrimaryAction(SliceAction)} then the action set here on the
     * {@link GridRowBuilder} may not ever be invoked.
     * <p>
     * If this grid row is the first row in {@link ListBuilder}, the action
     * set here will be used to represent the slice when presented in
     * {@link androidx.slice.widget.SliceView#MODE_SHORTCUT}.
     */
    @NonNull
    public GridRowBuilder setPrimaryAction(@NonNull SliceAction action) {
        mPrimaryAction = action;
        return this;
    }

    /**
     * Sets the content description for the entire grid row.
     */
    @NonNull
    public GridRowBuilder setContentDescription(@NonNull CharSequence description) {
        mDescription = description;
        return this;
    }

    /**
     * Sets the desired layout direction for the content in this row.
     *
     * @param layoutDirection the layout direction to set.
     */
    @NonNull
    public GridRowBuilder setLayoutDirection(@ListBuilder.LayoutDirection int layoutDirection) {
        mLayoutDirection = layoutDirection;
        return this;
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public SliceAction getPrimaryAction() {
        return mPrimaryAction;
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public List<CellBuilder> getCells() {
        return mCells;
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public CellBuilder getSeeMoreCell() {
        return mSeeMoreCell;
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public PendingIntent getSeeMoreIntent() {
        return mSeeMoreIntent;
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public CharSequence getDescription() {
        return mDescription;
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public int getLayoutDirection() {
        return mLayoutDirection;
    }

    /**
     * Builder to construct a cell. A cell can be added as an item to GridRowBuilder via
     * {@link GridRowBuilder#addCell(CellBuilder)}.
     * <p>
     * A cell supports up to two lines of text and one image. Content added to a cell will be
     * displayed in the order that the content is added to it. For example, the below code
     * would construct a cell with "First text", and image below it, and then "Second text" below
     * the image.
     *
     * <pre class="prettyprint">
     * CellBuilder cb = new CellBuilder(parent, sliceUri);
     * cb.addText("First text")
     *   .addImage(middleIcon)
     *   .addText("Second text");
     * </pre>
     * <p>
     * A cell supports a couple of image types:
     * <ul>
     *     <li>{@link ListBuilder#ICON_IMAGE} - icon images are expected to be tintable and are
     *     shown at a standard icon size.</li>
     *     <li>{@link ListBuilder#SMALL_IMAGE} - small images are not tinted and are shown at
     *     a small size.</li>
     *     <li>{@link ListBuilder#LARGE_IMAGE} - large images are not tinted and are shown as
     *     large as they can be, in a {@link android.widget.ImageView.ScaleType#CENTER_CROP}</li>
     * </ul>
     *
     * @see GridRowBuilder#addCell(CellBuilder)
     * @see ListBuilder#addGridRow(GridRowBuilder)
     * @see ListBuilder#ICON_IMAGE
     * @see ListBuilder#SMALL_IMAGE
     * @see ListBuilder#ICON_IMAGE
     */
    public static class CellBuilder {
        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        public static final int TYPE_TEXT = 0;
        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        public static final int TYPE_TITLE = 1;
        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        public static final int TYPE_IMAGE = 2;

        private List<Object> mObjects = new ArrayList<>();
        private List<Integer> mTypes = new ArrayList<>();
        private List<Boolean> mLoadings = new ArrayList<>();
        private CharSequence mCellDescription;
        private PendingIntent mContentIntent;

        /**
         * Create a builder which will construct a slice displayed as a cell in a grid.
         */
        public CellBuilder() {
        }

        /**
         * Create a builder which will construct a slice displayed as a cell in a grid.
         * @param parent The builder constructing the parent slice.
         * @hide
         */
        @RestrictTo(LIBRARY)
        public CellBuilder(@NonNull GridRowBuilder parent) {
        }

        /**
         * Create a builder which will construct a slice displayed as a cell in a grid.
         * @param uri Uri to tag for this slice.
         * @hide
         */
        @RestrictTo(LIBRARY)
        public CellBuilder(@NonNull GridRowBuilder parent, @NonNull Uri uri) {
        }

        /**
         * Adds text to the cell. There can be at most two text items, the first two added
         * will be used, others will be ignored.
         */
        @NonNull
        public CellBuilder addText(@NonNull CharSequence text) {
            return addText(text, false /* isLoading */);
        }

        /**
         * Adds text to the cell. There can be at most two text items, the first two added
         * will be used, others will be ignored.
         * <p>
         * Use this method to specify content that will appear in the template once it's been
         * loaded.
         * </p>
         * @param isLoading indicates whether the app is doing work to load the added content in the
         *                  background or not.
         */
        @NonNull
        public CellBuilder addText(@Nullable CharSequence text, boolean isLoading) {
            mObjects.add(text);
            mTypes.add(TYPE_TEXT);
            mLoadings.add(isLoading);
            return this;
        }

        /**
         * Adds text to the cell. Text added with this method will be styled as a title.
         * There can be at most two text items, the first two added will be used, others
         * will be ignored.
         */
        @NonNull
        public CellBuilder addTitleText(@NonNull CharSequence text) {
            return addTitleText(text, false /* isLoading */);
        }

        /**
         * Adds text to the cell. Text added with this method will be styled as a title.
         * There can be at most two text items, the first two added will be used, others
         * will be ignored.
         * <p>
         * Use this method to specify content that will appear in the template once it's been
         * loaded.
         * </p>
         * @param isLoading indicates whether the app is doing work to load the added content in the
         *                  background or not.
         */
        @NonNull
        public CellBuilder addTitleText(@Nullable CharSequence text, boolean isLoading) {
            mObjects.add(text);
            mTypes.add(TYPE_TITLE);
            mLoadings.add(isLoading);
            return this;
        }

        /**
         * Adds an image to the cell. There can be at most one image, the first one added will be
         * used, others will be ignored.
         *
         * @param image the image to display in the cell.
         * @param imageMode the mode that image should be displayed in.
         *
         * @see ListBuilder#ICON_IMAGE
         * @see ListBuilder#SMALL_IMAGE
         * @see ListBuilder#LARGE_IMAGE
         */
        @NonNull
        public CellBuilder addImage(@NonNull IconCompat image,
                @ListBuilder.ImageMode int imageMode) {
            return addImage(image, imageMode, false /* isLoading */);
        }

        /**
         * Adds an image to the cell. There can be at most one image, the first one added will be
         * used, others will be ignored.
         * <p>
         * Use this method to specify content that will appear in the template once it's been
         * loaded.
         * </p>
         * @param image the image to display in the cell.
         * @param imageMode the mode that image should be displayed in.
         * @param isLoading indicates whether the app is doing work to load the added content in the
         *                  background or not.
         *
         * @see ListBuilder#ICON_IMAGE
         * @see ListBuilder#SMALL_IMAGE
         * @see ListBuilder#LARGE_IMAGE
         */
        @NonNull
        public CellBuilder addImage(@Nullable IconCompat image,
                @ListBuilder.ImageMode int imageMode, boolean isLoading) {
            mObjects.add(new Pair<>(image, imageMode));
            mTypes.add(TYPE_IMAGE);
            mLoadings.add(isLoading);
            return this;
        }

        /**
         * Sets the action to be invoked if the user taps on this cell in the row.
         */
        @NonNull
        public CellBuilder setContentIntent(@NonNull PendingIntent intent) {
            mContentIntent = intent;
            return this;
        }

        /**
         * Sets the content description for this cell.
         */
        @NonNull
        public CellBuilder setContentDescription(@NonNull CharSequence description) {
            mCellDescription = description;
            return this;
        }

        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        public List<Object> getObjects() {
            return mObjects;
        }

        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        public List<Integer> getTypes() {
            return mTypes;
        }

        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        public List<Boolean> getLoadings() {
            return mLoadings;
        }

        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        public CharSequence getCellDescription() {
            return mCellDescription;
        }

        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        public PendingIntent getContentIntent() {
            return mContentIntent;
        }

        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        @Nullable
        public CharSequence getTitle() {
            for (int i = 0; i < mObjects.size(); i++) {
                if (mTypes.get(i) == TYPE_TITLE) {
                    return (CharSequence) mObjects.get(i);
                }
            }
            return null;
        }

        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        @Nullable
        public CharSequence getSubtitle() {
            for (int i = 0; i < mObjects.size(); i++) {
                if (mTypes.get(i) == TYPE_TEXT) {
                    return (CharSequence) mObjects.get(i);
                }
            }
            return null;
        }
    }
}