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.RestrictTo;
import androidx.core.graphics.drawable.IconCompat;
import androidx.core.util.Consumer;
import androidx.slice.builders.impl.TemplateBuilderImpl;


/**
 * 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 the first row of your slice was created with {@link GridRowBuilder} then there are a couple
 * of extra considerations you should take for when the slice is displayed in different modes,
 * particularly:
 * <ul>
 *     <li>{@link androidx.slice.widget.SliceView#MODE_SHORTCUT} - ensure you use
 *     {@link #setPrimaryAction(SliceAction)} on grid row builder to specify the shortcut action,
 *     icon, and label. This action will also be activated when the whole row is clicked, unless
 *     the individual cells in the row have actions set on them.</li>
 *     <li>{@link androidx.slice.widget.SliceView#MODE_SMALL} - in small format there might not
 *     be enough space to display a cell containing two lines of text and an image, in this case
 *     only one line of text will be shown with the image, favoring text added via
 *     {@link CellBuilder#addTitleText(CharSequence)} or the first text added if no title was
 *     added.</li>
 * </ul>
 * <p>
 * If more cells are added to the grid row than can be displayed, the cells will be cut off. Using
 * {@link #addSeeMoreAction(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)
 */
public class GridRowBuilder extends TemplateSliceBuilder {

    private androidx.slice.builders.impl.GridRowBuilder mImpl;
    private boolean mHasSeeMore;

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

    @Override
    void setImpl(TemplateBuilderImpl impl) {
        mImpl = (androidx.slice.builders.impl.GridRowBuilder) impl;
    }

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

    /**
     * Add a cell to the grid builder.
     */
    @NonNull
    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 #addSeeMoreAction(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");
        }
        mImpl.setSeeMoreCell((TemplateBuilderImpl) builder.mImpl);
        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 #addSeeMoreAction(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 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");
        }
        mImpl.setSeeMoreAction(intent);
        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 #addSeeMoreAction(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>
     *
     * @deprecated TO BE REMOVED
     */
    @NonNull
    @Deprecated
    public GridRowBuilder addSeeMoreCell(@NonNull CellBuilder builder) {
        if (mHasSeeMore) {
            throw new IllegalStateException("Trying to add see more cell when one has "
                    + "already been added");
        }
        mImpl.setSeeMoreCell((TemplateBuilderImpl) builder.mImpl);
        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 #addSeeMoreAction(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>
     *
     * @deprecated TO BE REMOVED
     */
    @NonNull
    @Deprecated
    public GridRowBuilder addSeeMoreCell(@NonNull Consumer<CellBuilder> c) {
        CellBuilder b = new CellBuilder(this);
        c.accept(b);
        return addSeeMoreCell(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>
     * @deprecated TO BE REMOVED
     */
    @Deprecated
    @NonNull
    public GridRowBuilder addSeeMoreAction(@NonNull PendingIntent intent) {
        if (mHasSeeMore) {
            throw new IllegalStateException("Trying to add see more action when one has "
                    + "already been added");
        }
        mImpl.setSeeMoreAction(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) {
        mImpl.setPrimaryAction(action);
        return this;
    }

    /**
     * Sets the content description for the entire grid row.
     */
    @NonNull
    public GridRowBuilder setContentDescription(@NonNull CharSequence description) {
        mImpl.setContentDescription(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) {
        mImpl.setLayoutDirection(layoutDirection);
        return this;
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public androidx.slice.builders.impl.GridRowBuilder getImpl() {
        return mImpl;
    }

    /**
     * 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 extends TemplateSliceBuilder {
        private androidx.slice.builders.impl.GridRowBuilder.CellBuilder mImpl;

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

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

        @Override
        void setImpl(TemplateBuilderImpl impl) {
            mImpl = (androidx.slice.builders.impl.GridRowBuilder.CellBuilder) impl;
        }

        /**
         * 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) {
            mImpl.addText(text, 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) {
            mImpl.addTitleText(text, 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) {
            mImpl.addImage(image, imageMode, 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) {
            mImpl.setContentIntent(intent);
            return this;
        }

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