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
17package androidx.slice.widget;
18
19import static android.app.slice.Slice.HINT_LARGE;
20import static android.app.slice.Slice.HINT_NO_TINT;
21import static android.app.slice.Slice.HINT_TITLE;
22import static android.app.slice.SliceItem.FORMAT_ACTION;
23import static android.app.slice.SliceItem.FORMAT_IMAGE;
24import static android.app.slice.SliceItem.FORMAT_LONG;
25import static android.app.slice.SliceItem.FORMAT_SLICE;
26import static android.app.slice.SliceItem.FORMAT_TEXT;
27import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
28import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
29
30import static androidx.slice.widget.SliceView.MODE_SMALL;
31
32import android.app.PendingIntent;
33import android.content.Context;
34import android.content.res.Resources;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.util.Pair;
38import android.util.TypedValue;
39import android.view.Gravity;
40import android.view.LayoutInflater;
41import android.view.View;
42import android.view.ViewGroup;
43import android.widget.FrameLayout;
44import android.widget.ImageView;
45import android.widget.ImageView.ScaleType;
46import android.widget.LinearLayout;
47import android.widget.TextView;
48
49import androidx.annotation.ColorInt;
50import androidx.annotation.RestrictTo;
51import androidx.slice.SliceItem;
52import androidx.slice.core.SliceQuery;
53import androidx.slice.view.R;
54
55import java.util.ArrayList;
56import java.util.Iterator;
57import java.util.List;
58
59/**
60 * @hide
61 */
62@RestrictTo(RestrictTo.Scope.LIBRARY)
63public class GridRowView extends SliceChildView implements View.OnClickListener {
64
65    private static final String TAG = "GridView";
66
67    private static final int TITLE_TEXT_LAYOUT = R.layout.abc_slice_title;
68    private static final int TEXT_LAYOUT = R.layout.abc_slice_secondary_text;
69
70    // Max number of normal cell items that can be shown in a row
71    private static final int MAX_CELLS = 5;
72
73    // Max number of text items that can show in a cell
74    private static final int MAX_CELL_TEXT = 2;
75    // Max number of text items that can show in a cell if the mode is small
76    private static final int MAX_CELL_TEXT_SMALL = 1;
77    // Max number of images that can show in a cell
78    private static final int MAX_CELL_IMAGES = 1;
79
80    private int mRowIndex;
81    private int mRowCount;
82
83    private int mSmallImageSize;
84    private int mIconSize;
85    private int mGutter;
86    private int mTextPadding;
87
88    private GridContent mGridContent;
89    private LinearLayout mViewContainer;
90
91    public GridRowView(Context context) {
92        this(context, null);
93    }
94
95    public GridRowView(Context context, AttributeSet attrs) {
96        super(context, attrs);
97        final Resources res = getContext().getResources();
98        mViewContainer = new LinearLayout(getContext());
99        mViewContainer.setOrientation(LinearLayout.HORIZONTAL);
100        addView(mViewContainer, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
101        mViewContainer.setGravity(Gravity.CENTER_VERTICAL);
102        mIconSize = res.getDimensionPixelSize(R.dimen.abc_slice_icon_size);
103        mSmallImageSize = res.getDimensionPixelSize(R.dimen.abc_slice_small_image_size);
104        mGutter = res.getDimensionPixelSize(R.dimen.abc_slice_grid_gutter);
105        mTextPadding = res.getDimensionPixelSize(R.dimen.abc_slice_grid_text_padding);
106    }
107
108    @Override
109    public int getSmallHeight() {
110        // GridRow is small if its the first element in a list without a header presented in small
111        if (mGridContent == null) {
112            return 0;
113        }
114        return mGridContent.getSmallHeight() + getExtraTopPadding() + getExtraBottomPadding();
115    }
116
117    @Override
118    public int getActualHeight() {
119        if (mGridContent == null) {
120            return 0;
121        }
122        return mGridContent.getActualHeight() + getExtraTopPadding() + getExtraBottomPadding();
123    }
124
125    private int getExtraTopPadding() {
126        if (mGridContent != null && mGridContent.isAllImages()) {
127            // Might need to add padding if in first or last position
128            if (mRowIndex == 0) {
129                return mGridTopPadding;
130            }
131        }
132        return 0;
133    }
134
135    private int getExtraBottomPadding() {
136        if (mGridContent != null && mGridContent.isAllImages()) {
137            if (mRowIndex == mRowCount - 1 || getMode() == MODE_SMALL) {
138                return mGridBottomPadding;
139            }
140        }
141        return 0;
142    }
143
144    @Override
145    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
146        int height = getMode() == MODE_SMALL ? getSmallHeight() : getActualHeight();
147        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
148        mViewContainer.getLayoutParams().height = height;
149        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
150    }
151
152    @Override
153    public void setTint(@ColorInt int tintColor) {
154        super.setTint(tintColor);
155        if (mGridContent != null) {
156            GridContent gc = mGridContent;
157            // TODO -- could be smarter about this
158            resetView();
159            populateViews(gc);
160        }
161    }
162
163    /**
164     * This is called when GridView is being used as a component in a larger template.
165     */
166    @Override
167    public void setSliceItem(SliceItem slice, boolean isHeader, int rowIndex,
168            int rowCount, SliceView.OnSliceActionListener observer) {
169        resetView();
170        setSliceActionListener(observer);
171        mRowIndex = rowIndex;
172        mRowCount = rowCount;
173        mGridContent = new GridContent(getContext(), slice);
174        populateViews(mGridContent);
175        mViewContainer.setPadding(0, getExtraTopPadding(), 0, getExtraBottomPadding());
176    }
177
178    private void populateViews(GridContent gc) {
179        if (gc.getContentIntent() != null) {
180            EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_CONTENT,
181                    EventInfo.ROW_TYPE_GRID, mRowIndex);
182            Pair<SliceItem, EventInfo> tagItem = new Pair<>(gc.getContentIntent(), info);
183            mViewContainer.setTag(tagItem);
184            makeClickable(mViewContainer, true);
185        }
186        CharSequence contentDescr = gc.getContentDescription();
187        if (contentDescr != null) {
188            mViewContainer.setContentDescription(contentDescr);
189        }
190        ArrayList<GridContent.CellContent> cells = gc.getGridContent();
191        boolean hasSeeMore = gc.getSeeMoreItem() != null;
192        for (int i = 0; i < cells.size(); i++) {
193            if (mViewContainer.getChildCount() >= MAX_CELLS) {
194                if (hasSeeMore) {
195                    addSeeMoreCount(cells.size() - MAX_CELLS);
196                }
197                break;
198            }
199            addCell(cells.get(i), i, Math.min(cells.size(), MAX_CELLS));
200        }
201    }
202
203    private void addSeeMoreCount(int numExtra) {
204        // Remove last element
205        View last = mViewContainer.getChildAt(mViewContainer.getChildCount() - 1);
206        mViewContainer.removeView(last);
207
208        SliceItem seeMoreItem = mGridContent.getSeeMoreItem();
209        int index = mViewContainer.getChildCount();
210        int total = MAX_CELLS;
211        if ((FORMAT_SLICE.equals(seeMoreItem.getFormat())
212                || FORMAT_ACTION.equals(seeMoreItem.getFormat()))
213                && seeMoreItem.getSlice().getItems().size() > 0) {
214            // It's a custom see more cell, add it
215            addCell(new GridContent.CellContent(seeMoreItem), index, total);
216            return;
217        }
218
219        // Default see more, create it
220        LayoutInflater inflater = LayoutInflater.from(getContext());
221        TextView extraText;
222        ViewGroup seeMoreView;
223        if (mGridContent.isAllImages()) {
224            seeMoreView = (FrameLayout) inflater.inflate(R.layout.abc_slice_grid_see_more_overlay,
225                    mViewContainer, false);
226            seeMoreView.addView(last, 0, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
227            extraText = seeMoreView.findViewById(R.id.text_see_more_count);
228        } else {
229            seeMoreView = (LinearLayout) inflater.inflate(
230                    R.layout.abc_slice_grid_see_more, mViewContainer, false);
231            extraText = seeMoreView.findViewById(R.id.text_see_more_count);
232
233            // Update text appearance
234            TextView moreText = seeMoreView.findViewById(R.id.text_see_more);
235            moreText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mGridTitleSize);
236            moreText.setTextColor(mTitleColor);
237        }
238        mViewContainer.addView(seeMoreView, new LinearLayout.LayoutParams(0, MATCH_PARENT, 1));
239        extraText.setText(getResources().getString(R.string.abc_slice_more_content, numExtra));
240
241        // Make it clickable
242        EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_SEE_MORE,
243                EventInfo.ROW_TYPE_GRID, mRowIndex);
244        info.setPosition(EventInfo.POSITION_CELL, index, total);
245        Pair<SliceItem, EventInfo> tagItem = new Pair<>(seeMoreItem, info);
246        seeMoreView.setTag(tagItem);
247        makeClickable(seeMoreView, true);
248    }
249
250    /**
251     * Adds a cell to the grid view based on the provided {@link SliceItem}.
252     */
253    private void addCell(GridContent.CellContent cell, int index, int total) {
254        final int maxCellText = getMode() == MODE_SMALL
255                ? MAX_CELL_TEXT_SMALL
256                : MAX_CELL_TEXT;
257        LinearLayout cellContainer = new LinearLayout(getContext());
258        cellContainer.setOrientation(LinearLayout.VERTICAL);
259        cellContainer.setGravity(Gravity.CENTER_HORIZONTAL);
260
261        ArrayList<SliceItem> cellItems = cell.getCellItems();
262        SliceItem contentIntentItem = cell.getContentIntent();
263
264        int textCount = 0;
265        int imageCount = 0;
266        boolean added = false;
267        boolean singleItem = cellItems.size() == 1;
268        List<SliceItem> textItems = null;
269        // In small format we display one text item and prefer titles
270        if (!singleItem && getMode() == MODE_SMALL) {
271            // Get all our text items
272            textItems = new ArrayList<>();
273            for (SliceItem cellItem : cellItems) {
274                if (FORMAT_TEXT.equals(cellItem.getFormat())) {
275                    textItems.add(cellItem);
276                }
277            }
278            // If we have more than 1 remove non-titles
279            Iterator<SliceItem> iterator = textItems.iterator();
280            while (textItems.size() > 1) {
281                SliceItem item = iterator.next();
282                if (!item.hasAnyHints(HINT_TITLE, HINT_LARGE)) {
283                    iterator.remove();
284                }
285            }
286        }
287        SliceItem prevItem = null;
288        for (int i = 0; i < cellItems.size(); i++) {
289            SliceItem item = cellItems.get(i);
290            final String itemFormat = item.getFormat();
291            int padding = determinePadding(prevItem);
292            if (textCount < maxCellText && (FORMAT_TEXT.equals(itemFormat)
293                    || FORMAT_LONG.equals(itemFormat))) {
294                if (textItems != null && !textItems.contains(item)) {
295                    continue;
296                }
297                if (addItem(item, mTintColor, cellContainer, padding)) {
298                    prevItem = item;
299                    textCount++;
300                    added = true;
301                }
302            } else if (imageCount < MAX_CELL_IMAGES && FORMAT_IMAGE.equals(item.getFormat())) {
303                if (addItem(item, mTintColor, cellContainer, 0)) {
304                    prevItem = item;
305                    imageCount++;
306                    added = true;
307                }
308            }
309        }
310        if (added) {
311            CharSequence contentDescr = cell.getContentDescription();
312            if (contentDescr != null) {
313                cellContainer.setContentDescription(contentDescr);
314            }
315            mViewContainer.addView(cellContainer,
316                    new LinearLayout.LayoutParams(0, WRAP_CONTENT, 1));
317            if (index != total - 1) {
318                // If we're not the last or only element add space between items
319                MarginLayoutParams lp =
320                        (LinearLayout.MarginLayoutParams) cellContainer.getLayoutParams();
321                lp.setMarginEnd(mGutter);
322                cellContainer.setLayoutParams(lp);
323            }
324            if (contentIntentItem != null) {
325                EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_BUTTON,
326                        EventInfo.ROW_TYPE_GRID, mRowIndex);
327                info.setPosition(EventInfo.POSITION_CELL, index, total);
328                Pair<SliceItem, EventInfo> tagItem = new Pair<>(contentIntentItem, info);
329                cellContainer.setTag(tagItem);
330                makeClickable(cellContainer, true);
331            }
332        }
333    }
334
335    /**
336     * Adds simple items to a container. Simple items include icons, text, and timestamps.
337     *
338     * @param item item to add to the container.
339     * @param container the container to add to.
340     * @param padding the padding to apply to the item.
341     *
342     * @return Whether an item was added.
343     */
344    private boolean addItem(SliceItem item, int color, ViewGroup container, int padding) {
345        final String format = item.getFormat();
346        View addedView = null;
347        if (FORMAT_TEXT.equals(format) || FORMAT_LONG.equals(format)) {
348            boolean title = SliceQuery.hasAnyHints(item, HINT_LARGE, HINT_TITLE);
349            TextView tv = (TextView) LayoutInflater.from(getContext()).inflate(title
350                    ? TITLE_TEXT_LAYOUT : TEXT_LAYOUT, null);
351            tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, title ? mGridTitleSize : mGridSubtitleSize);
352            tv.setTextColor(title ? mTitleColor : mSubtitleColor);
353            CharSequence text = FORMAT_LONG.equals(format)
354                    ? SliceViewUtil.getRelativeTimeString(item.getTimestamp())
355                    : item.getText();
356            tv.setText(text);
357            container.addView(tv);
358            tv.setPadding(0, padding, 0, 0);
359            addedView = tv;
360        } else if (FORMAT_IMAGE.equals(format)) {
361            ImageView iv = new ImageView(getContext());
362            iv.setImageDrawable(item.getIcon().loadDrawable(getContext()));
363            LinearLayout.LayoutParams lp;
364            if (item.hasHint(HINT_LARGE)) {
365                iv.setScaleType(ScaleType.CENTER_CROP);
366                lp = new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
367            } else {
368                boolean isIcon = !item.hasHint(HINT_NO_TINT);
369                int size = isIcon ? mIconSize : mSmallImageSize;
370                iv.setScaleType(isIcon ? ScaleType.CENTER_INSIDE : ScaleType.CENTER_CROP);
371                lp = new LinearLayout.LayoutParams(size, size);
372            }
373            if (color != -1 && !item.hasHint(HINT_NO_TINT)) {
374                iv.setColorFilter(color);
375            }
376            container.addView(iv, lp);
377            addedView = iv;
378        }
379        return addedView != null;
380    }
381
382    private int determinePadding(SliceItem prevItem) {
383        if (prevItem == null) {
384            // No need for top padding
385            return 0;
386        } else if (FORMAT_IMAGE.equals(prevItem.getFormat())) {
387            return mTextPadding;
388        } else if (FORMAT_TEXT.equals(prevItem.getFormat())
389                || FORMAT_LONG.equals(prevItem.getFormat())) {
390            return mVerticalGridTextPadding;
391        }
392        return 0;
393    }
394
395    private void makeClickable(View layout, boolean isClickable) {
396        layout.setOnClickListener(isClickable ? this : null);
397        layout.setBackground(isClickable
398                ? SliceViewUtil.getDrawable(getContext(), android.R.attr.selectableItemBackground)
399                : null);
400        layout.setClickable(isClickable);
401    }
402
403    @Override
404    public void onClick(View view) {
405        Pair<SliceItem, EventInfo> tagItem = (Pair<SliceItem, EventInfo>) view.getTag();
406        final SliceItem actionItem = tagItem.first;
407        final EventInfo info = tagItem.second;
408        if (actionItem != null && FORMAT_ACTION.equals(actionItem.getFormat())) {
409            try {
410                actionItem.fireAction(null, null);
411                if (mObserver != null) {
412                    mObserver.onSliceAction(info, actionItem);
413                }
414            } catch (PendingIntent.CanceledException e) {
415                Log.e(TAG, "PendingIntent for slice cannot be sent", e);
416            }
417        }
418    }
419
420    @Override
421    public void resetView() {
422        mViewContainer.removeAllViews();
423        makeClickable(mViewContainer, false);
424    }
425}
426