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.SUBTYPE_COLOR;
20 import static android.app.slice.SliceItem.FORMAT_INT;
21 
22 import android.app.PendingIntent;
23 import android.content.Context;
24 import android.content.res.TypedArray;
25 import android.graphics.drawable.ColorDrawable;
26 import android.os.Handler;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.HapticFeedbackConstants;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.ViewConfiguration;
33 import android.view.ViewGroup;
34 
35 import androidx.annotation.ColorInt;
36 import androidx.annotation.IntDef;
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 import androidx.annotation.RequiresApi;
40 import androidx.annotation.RestrictTo;
41 import androidx.lifecycle.Observer;
42 import androidx.slice.Slice;
43 import androidx.slice.SliceItem;
44 import androidx.slice.SliceMetadata;
45 import androidx.slice.core.SliceActionImpl;
46 import androidx.slice.core.SliceHints;
47 import androidx.slice.core.SliceQuery;
48 import androidx.slice.view.R;
49 
50 import java.lang.annotation.Retention;
51 import java.lang.annotation.RetentionPolicy;
52 import java.util.List;
53 
54 /**
55  * A view for displaying a {@link Slice} which is a piece of app content and actions. SliceView is
56  * able to present slice content in a templated format outside of the associated app. The way this
57  * content is displayed depends on the structure of the slice, the hints associated with the
58  * content, and the mode that SliceView is configured for. The modes that SliceView supports are:
59  * <ul>
60  * <li><b>Shortcut</b>: A shortcut is presented as an icon and a text label representing the main
61  * content or action associated with the slice.</li>
62  * <li><b>Small</b>: The small format has a restricted height and can present a single
63  * {@link SliceItem} or a limited collection of items.</li>
64  * <li><b>Large</b>: The large format displays multiple small templates in a list, if scrolling is
65  * not enabled (see {@link #setScrollable(boolean)}) the view will show as many items as it can
66  * comfortably fit.</li>
67  * </ul>
68  * <p>
69  * When constructing a slice, the contents of it can be annotated with hints, these provide the OS
70  * with some information on how the content should be displayed. For example, text annotated with
71  * {@link android.app.slice.Slice#HINT_TITLE} would be placed in the title position of a template.
72  * A slice annotated with {@link android.app.slice.Slice#HINT_LIST} would present the child items
73  * of that slice in a list.
74  * <p>
75  * Example usage:
76  *
77  * <pre class="prettyprint">
78  * SliceView v = new SliceView(getContext());
79  * v.setMode(desiredMode);
80  * LiveData<Slice> liveData = SliceLiveData.fromUri(sliceUri);
81  * liveData.observe(lifecycleOwner, v);
82  * </pre>
83  * @see SliceLiveData
84  */
85 public class SliceView extends ViewGroup implements Observer<Slice>, View.OnClickListener {
86 
87     private static final String TAG = "SliceView";
88 
89     /**
90      * Implement this interface to be notified of interactions with the slice displayed
91      * in this view.
92      * @see EventInfo
93      */
94     public interface OnSliceActionListener {
95         /**
96          * Called when an interaction has occurred with an element in this view.
97          * @param info the type of event that occurred.
98          * @param item the specific item within the {@link Slice} that was interacted with.
99          */
onSliceAction(@onNull EventInfo info, @NonNull SliceItem item)100         void onSliceAction(@NonNull EventInfo info, @NonNull SliceItem item);
101     }
102 
103     /**
104      * @hide
105      */
106     @RestrictTo(RestrictTo.Scope.LIBRARY)
107     @IntDef({
108             MODE_SMALL, MODE_LARGE, MODE_SHORTCUT
109     })
110     @Retention(RetentionPolicy.SOURCE)
111     public @interface SliceMode {}
112 
113     /**
114      * Mode indicating this slice should be presented in small template format.
115      */
116     public static final int MODE_SMALL       = 1;
117     /**
118      * Mode indicating this slice should be presented in large template format.
119      */
120     public static final int MODE_LARGE       = 2;
121     /**
122      * Mode indicating this slice should be presented as an icon. A shortcut requires an intent,
123      * icon, and label. This can be indicated by using {@link android.app.slice.Slice#HINT_TITLE}
124      * on an action in a slice.
125      */
126     public static final int MODE_SHORTCUT    = 3;
127 
128     private int mMode = MODE_LARGE;
129     private Slice mCurrentSlice;
130     private ListContent mListContent;
131     private SliceChildView mCurrentView;
132     private List<SliceItem> mActions;
133     private ActionRow mActionRow;
134 
135     private boolean mShowActions = false;
136     private boolean mIsScrollable = true;
137     private boolean mShowLastUpdated = true;
138 
139     private int mShortcutSize;
140     private int mMinLargeHeight;
141     private int mMaxLargeHeight;
142     private int mActionRowHeight;
143 
144     private AttributeSet mAttrs;
145     private int mDefStyleAttr;
146     private int mDefStyleRes;
147     private int mThemeTintColor = -1;
148 
149     private OnSliceActionListener mSliceObserver;
150     private int mTouchSlopSquared;
151     private View.OnLongClickListener mLongClickListener;
152     private View.OnClickListener mOnClickListener;
153     private int mDownX;
154     private int mDownY;
155     private boolean mPressing;
156     private boolean mInLongpress;
157     private Handler mHandler;
158     int[] mClickInfo;
159 
SliceView(Context context)160     public SliceView(Context context) {
161         this(context, null);
162     }
163 
SliceView(Context context, @Nullable AttributeSet attrs)164     public SliceView(Context context, @Nullable AttributeSet attrs) {
165         this(context, attrs, R.attr.sliceViewStyle);
166     }
167 
SliceView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)168     public SliceView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
169         super(context, attrs, defStyleAttr);
170         init(context, attrs, defStyleAttr, R.style.Widget_SliceView);
171     }
172 
173     @RequiresApi(21)
SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)174     public SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
175         super(context, attrs, defStyleAttr, defStyleRes);
176         init(context, attrs, defStyleAttr, defStyleRes);
177     }
178 
init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)179     private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
180         mAttrs = attrs;
181         mDefStyleAttr = defStyleAttr;
182         mDefStyleRes = defStyleRes;
183         TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SliceView,
184                 defStyleAttr, defStyleRes);
185 
186         try {
187             mThemeTintColor = a.getColor(R.styleable.SliceView_tintColor, -1);
188         } finally {
189             a.recycle();
190         }
191         mShortcutSize = getContext().getResources()
192                 .getDimensionPixelSize(R.dimen.abc_slice_shortcut_size);
193         mMinLargeHeight = getResources().getDimensionPixelSize(R.dimen.abc_slice_large_height);
194         mMaxLargeHeight = getResources().getDimensionPixelSize(R.dimen.abc_slice_max_large_height);
195         mActionRowHeight = getResources().getDimensionPixelSize(
196                 R.dimen.abc_slice_action_row_height);
197 
198         mCurrentView = new LargeTemplateView(getContext());
199         mCurrentView.setMode(getMode());
200         addView(mCurrentView, getChildLp(mCurrentView));
201 
202         // TODO: action row background should support light / dark / maybe presenter customization
203         mActionRow = new ActionRow(getContext(), true);
204         mActionRow.setBackground(new ColorDrawable(0xffeeeeee));
205         addView(mActionRow, getChildLp(mActionRow));
206 
207         final int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
208         mTouchSlopSquared = slop * slop;
209         mHandler = new Handler();
210 
211         super.setOnClickListener(this);
212     }
213 
214     /**
215      * Indicates whether this view reacts to click events or not.
216      * @hide
217      */
218     @RestrictTo(RestrictTo.Scope.LIBRARY)
isSliceViewClickable()219     public boolean isSliceViewClickable() {
220         return mOnClickListener != null
221                 || (mListContent != null && mListContent.getPrimaryAction() != null);
222     }
223 
224     /**
225      * Sets the event info for logging a click.
226      * @hide
227      */
228     @RestrictTo(RestrictTo.Scope.LIBRARY)
setClickInfo(int[] info)229     public void setClickInfo(int[] info) {
230         mClickInfo = info;
231     }
232 
233     @Override
onClick(View v)234     public void onClick(View v) {
235         if (mListContent != null && mListContent.getPrimaryAction() != null) {
236             try {
237                 SliceActionImpl sa = new SliceActionImpl(mListContent.getPrimaryAction());
238                 sa.getAction().send();
239                 if (mSliceObserver != null && mClickInfo != null && mClickInfo.length > 1) {
240                     EventInfo eventInfo = new EventInfo(getMode(),
241                             EventInfo.ACTION_TYPE_CONTENT, mClickInfo[0], mClickInfo[1]);
242                     mSliceObserver.onSliceAction(eventInfo, mListContent.getPrimaryAction());
243                 }
244             } catch (PendingIntent.CanceledException e) {
245                 Log.e(TAG, "PendingIntent for slice cannot be sent", e);
246             }
247         } else if (mOnClickListener != null) {
248             mOnClickListener.onClick(this);
249         }
250     }
251 
252     @Override
setOnClickListener(View.OnClickListener listener)253     public void setOnClickListener(View.OnClickListener listener) {
254         mOnClickListener = listener;
255     }
256 
257     @Override
setOnLongClickListener(View.OnLongClickListener listener)258     public void setOnLongClickListener(View.OnLongClickListener listener) {
259         super.setOnLongClickListener(listener);
260         mLongClickListener = listener;
261     }
262 
263     @Override
onInterceptTouchEvent(MotionEvent ev)264     public boolean onInterceptTouchEvent(MotionEvent ev) {
265         boolean ret = super.onInterceptTouchEvent(ev);
266         if (mLongClickListener != null) {
267             return handleTouchForLongpress(ev);
268         }
269         return ret;
270     }
271 
272     @Override
onTouchEvent(MotionEvent ev)273     public boolean onTouchEvent(MotionEvent ev) {
274         boolean ret = super.onTouchEvent(ev);
275         if (mLongClickListener != null) {
276             return handleTouchForLongpress(ev);
277         }
278         return ret;
279     }
280 
handleTouchForLongpress(MotionEvent ev)281     private boolean handleTouchForLongpress(MotionEvent ev) {
282         int action = ev.getActionMasked();
283         switch (action) {
284             case MotionEvent.ACTION_DOWN:
285                 mHandler.removeCallbacks(mLongpressCheck);
286                 mDownX = (int) ev.getRawX();
287                 mDownY = (int) ev.getRawY();
288                 mPressing = true;
289                 mInLongpress = false;
290                 mHandler.postDelayed(mLongpressCheck, ViewConfiguration.getLongPressTimeout());
291                 break;
292 
293             case MotionEvent.ACTION_MOVE:
294                 final int deltaX = (int) ev.getRawX() - mDownX;
295                 final int deltaY = (int) ev.getRawY() - mDownY;
296                 int distance = (deltaX * deltaX) + (deltaY * deltaY);
297                 if (distance > mTouchSlopSquared) {
298                     mPressing = false;
299                     mHandler.removeCallbacks(mLongpressCheck);
300                 }
301                 break;
302 
303             case MotionEvent.ACTION_CANCEL:
304             case MotionEvent.ACTION_UP:
305                 mPressing = false;
306                 mInLongpress = false;
307                 mHandler.removeCallbacks(mLongpressCheck);
308                 break;
309         }
310         return mInLongpress;
311     }
312 
getHeightForMode()313     private int getHeightForMode() {
314         int mode = getMode();
315         if (mode == MODE_SHORTCUT) {
316             return mListContent != null && mListContent.isValid() ? mShortcutSize : 0;
317         }
318         return mode == MODE_LARGE
319                 ? mCurrentView.getActualHeight()
320                 : mCurrentView.getSmallHeight();
321     }
322 
323     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)324     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
325         int width = MeasureSpec.getSize(widthMeasureSpec);
326         int childWidth = MeasureSpec.getSize(widthMeasureSpec);
327         if (MODE_SHORTCUT == mMode) {
328             // TODO: consider scaling the shortcut to fit if too small
329             childWidth = mShortcutSize;
330             width = mShortcutSize + getPaddingLeft() + getPaddingRight();
331         }
332         final int actionHeight = mActionRow.getVisibility() != View.GONE
333                 ? mActionRowHeight
334                 : 0;
335         final int sliceHeight = getHeightForMode();
336         final int heightAvailable = MeasureSpec.getSize(heightMeasureSpec);
337         final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
338         // Remove the padding from our available height
339         int height = heightAvailable - getPaddingTop() - getPaddingBottom();
340         if (heightAvailable >= sliceHeight + actionHeight
341                 || heightMode == MeasureSpec.UNSPECIFIED) {
342             // Available space is larger than the slice or we be what we want
343             if (heightMode != MeasureSpec.EXACTLY) {
344                 if (!mIsScrollable) {
345                     height = Math.min(mMaxLargeHeight, sliceHeight);
346                 } else {
347                     // If we want to be bigger than max, then we can be a good scrollable at min
348                     // large height, if it's not larger lets just use its desired height
349                     height = sliceHeight > mMaxLargeHeight ? mMinLargeHeight : sliceHeight;
350                 }
351             }
352         } else {
353             // Not enough space available for slice in current mode
354             if (getMode() == MODE_LARGE && heightAvailable >= mMinLargeHeight + actionHeight) {
355                 // It's just a slice with scrolling content; cap it to height available.
356                 height = Math.min(mMinLargeHeight, heightAvailable);
357             } else if (getMode() == MODE_SHORTCUT) {
358                 // TODO: consider scaling the shortcut to fit if too small
359                 height = mShortcutSize;
360             }
361         }
362 
363         int childHeight = height + getPaddingTop() + getPaddingBottom();
364         int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
365         int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
366         measureChild(mCurrentView, childWidthMeasureSpec, childHeightMeasureSpec);
367 
368         int actionPaddedHeight = actionHeight + getPaddingTop() + getPaddingBottom();
369         int actionHeightSpec = MeasureSpec.makeMeasureSpec(actionPaddedHeight, MeasureSpec.EXACTLY);
370         measureChild(mActionRow, childWidthMeasureSpec, actionHeightSpec);
371 
372         // Total height should include action row and our padding
373         height += actionHeight + getPaddingTop() + getPaddingBottom();
374         setMeasuredDimension(width, height);
375     }
376 
377     @Override
onLayout(boolean changed, int l, int t, int r, int b)378     protected void onLayout(boolean changed, int l, int t, int r, int b) {
379         View v = mCurrentView;
380         final int left = getPaddingLeft();
381         final int top = getPaddingTop();
382         v.layout(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
383         if (mActionRow.getVisibility() != View.GONE) {
384             mActionRow.layout(left,
385                     top + v.getMeasuredHeight(),
386                     left + mActionRow.getMeasuredWidth(),
387                     top + v.getMeasuredHeight() + mActionRow.getMeasuredHeight());
388         }
389     }
390 
391     @Override
onChanged(@ullable Slice slice)392     public void onChanged(@Nullable Slice slice) {
393         setSlice(slice);
394     }
395 
396     /**
397      * Populates this view to the provided {@link Slice}.
398      *
399      * This will not update automatically if the slice content changes, for live
400      * content see {@link SliceLiveData}.
401      */
setSlice(@ullable Slice slice)402     public void setSlice(@Nullable Slice slice) {
403         if (slice != null) {
404             if (mCurrentSlice == null || !mCurrentSlice.getUri().equals(slice.getUri())) {
405                 mCurrentView.resetView();
406             }
407         } else {
408             // No slice, no actions
409             mActions = null;
410         }
411         mActions = SliceMetadata.getSliceActions(slice);
412         mCurrentSlice = slice;
413         reinflate();
414     }
415 
416     /**
417      * @return the slice being used to populate this view.
418      */
419     @Nullable
getSlice()420     public Slice getSlice() {
421         return mCurrentSlice;
422     }
423 
424     /**
425      * Returns the slice actions presented in this view.
426      * <p>
427      * Note that these may be different from {@link SliceMetadata#getSliceActions()} if the actions
428      * set on the view have been adjusted using {@link #setSliceActions(List)}.
429      */
430     @Nullable
getSliceActions()431     public List<SliceItem> getSliceActions() {
432         return mActions;
433     }
434 
435     /**
436      * Sets the slice actions to display for the slice contained in this view. Normally SliceView
437      * will automatically show actions, however, it is possible to reorder or omit actions on the
438      * view using this method. This is generally discouraged.
439      * <p>
440      * It is required that the slice be set on this view before actions can be set, otherwise
441      * this will throw {@link IllegalStateException}. If any of the actions supplied are not
442      * available for the slice set on this view (i.e. the action is not returned by
443      * {@link SliceMetadata#getSliceActions()} this will throw {@link IllegalArgumentException}.
444      */
setSliceActions(@ullable List<SliceItem> newActions)445     public void setSliceActions(@Nullable List<SliceItem> newActions) {
446         // Check that these actions are part of available set
447         if (mCurrentSlice == null) {
448             throw new IllegalStateException("Trying to set actions on a view without a slice");
449         }
450         List<SliceItem> availableActions = SliceMetadata.getSliceActions(mCurrentSlice);
451         if (availableActions != null && newActions != null) {
452             for (int i = 0; i < newActions.size(); i++) {
453                 if (!availableActions.contains(newActions.get(i))) {
454                     throw new IllegalArgumentException(
455                             "Trying to set an action that isn't available: " + newActions.get(i));
456                 }
457             }
458         }
459         mActions = newActions;
460         updateActions();
461     }
462 
463     /**
464      * Set the mode this view should present in.
465      */
setMode(@liceMode int mode)466     public void setMode(@SliceMode int mode) {
467         setMode(mode, false /* animate */);
468     }
469 
470     /**
471      * Set whether this view should allow scrollable content when presenting in {@link #MODE_LARGE}.
472      */
setScrollable(boolean isScrollable)473     public void setScrollable(boolean isScrollable) {
474         mIsScrollable = isScrollable;
475         reinflate();
476     }
477 
478     /**
479      * Sets the listener to notify when an interaction events occur on the view.
480      * @see EventInfo
481      */
setOnSliceActionListener(@ullable OnSliceActionListener observer)482     public void setOnSliceActionListener(@Nullable OnSliceActionListener observer) {
483         mSliceObserver = observer;
484         mCurrentView.setSliceActionListener(mSliceObserver);
485     }
486 
487     /**
488      * @deprecated TO BE REMOVED; use {@link #setAccentColor(int)} instead.
489      */
490     @Deprecated
setTint(int tintColor)491     public void setTint(int tintColor) {
492         setAccentColor(tintColor);
493     }
494 
495     /**
496      * Contents of a slice such as icons, text, and controls (e.g. toggle) can be tinted. Normally
497      * a color for tinting will be provided by the slice. Using this method will override
498      * the slice-provided color information and instead tint elements with the color set here.
499      *
500      * @param accentColor the color to use for tinting contents of this view.
501      */
setAccentColor(@olorInt int accentColor)502     public void setAccentColor(@ColorInt int accentColor) {
503         mThemeTintColor = accentColor;
504         mCurrentView.setTint(accentColor);
505     }
506 
507     /**
508      * @hide
509      */
510     @RestrictTo(RestrictTo.Scope.LIBRARY)
setMode(@liceMode int mode, boolean animate)511     public void setMode(@SliceMode int mode, boolean animate) {
512         if (animate) {
513             Log.e(TAG, "Animation not supported yet");
514         }
515         if (mMode == mode) {
516             return;
517         }
518         mMode = mode;
519         reinflate();
520     }
521 
522     /**
523      * @return the mode this view is presenting in.
524      */
getMode()525     public @SliceMode int getMode() {
526         return mMode;
527     }
528 
529     /**
530      * @hide
531      *
532      * Whether this view should show a row of actions with it.
533      */
534     @RestrictTo(RestrictTo.Scope.LIBRARY)
setShowActionRow(boolean show)535     public void setShowActionRow(boolean show) {
536         mShowActions = show;
537         updateActions();
538     }
539 
540     /**
541      * @return whether this view is showing a row of actions.
542      * @hide
543      */
544     @RestrictTo(RestrictTo.Scope.LIBRARY)
isShowingActionRow()545     public boolean isShowingActionRow() {
546         return mShowActions;
547     }
548 
reinflate()549     private void reinflate() {
550         if (mCurrentSlice == null) {
551             mCurrentView.resetView();
552             updateActions();
553             return;
554         }
555         mListContent = new ListContent(getContext(), mCurrentSlice, mAttrs, mDefStyleAttr,
556                 mDefStyleRes);
557         if (!mListContent.isValid()) {
558             mCurrentView.resetView();
559             updateActions();
560             return;
561         }
562 
563         // TODO: Smarter mapping here from one state to the next.
564         int mode = getMode();
565         boolean isCurrentViewShortcut = mCurrentView instanceof ShortcutView;
566         if (mode == MODE_SHORTCUT && !isCurrentViewShortcut) {
567             removeAllViews();
568             mCurrentView = new ShortcutView(getContext());
569             addView(mCurrentView, getChildLp(mCurrentView));
570         } else if (mode != MODE_SHORTCUT && isCurrentViewShortcut) {
571             removeAllViews();
572             mCurrentView = new LargeTemplateView(getContext());
573             addView(mCurrentView, getChildLp(mCurrentView));
574         }
575         mCurrentView.setMode(mode);
576 
577         mCurrentView.setSliceActionListener(mSliceObserver);
578         if (mCurrentView instanceof LargeTemplateView) {
579             ((LargeTemplateView) mCurrentView).setScrollable(mIsScrollable);
580         }
581         mCurrentView.setStyle(mAttrs, mDefStyleAttr, mDefStyleRes);
582         mCurrentView.setTint(getTintColor());
583 
584         // Check if the slice content is expired and show when it was last updated
585         SliceMetadata sliceMetadata = SliceMetadata.from(getContext(), mCurrentSlice);
586         long lastUpdated = sliceMetadata.getLastUpdatedTime();
587         long expiry = sliceMetadata.getExpiry();
588         long now = System.currentTimeMillis();
589         mCurrentView.setLastUpdated(lastUpdated);
590         boolean expired = expiry != 0 && expiry != SliceHints.INFINITY && now > expiry;
591         mCurrentView.setShowLastUpdated(mShowLastUpdated && expired);
592 
593         // Set the slice
594         mCurrentView.setSliceContent(mListContent);
595         updateActions();
596     }
597 
updateActions()598     private void updateActions() {
599         if (mActions == null || mActions.isEmpty()) {
600             // No actions, hide the row, clear out the view
601             mActionRow.setVisibility(View.GONE);
602             mCurrentView.setSliceActions(null);
603             return;
604         }
605 
606         // TODO: take priority attached to actions into account
607         if (mShowActions && mMode != MODE_SHORTCUT && mActions.size() >= 2) {
608             // Show in action row if available
609             mActionRow.setActions(mActions, getTintColor());
610             mActionRow.setVisibility(View.VISIBLE);
611             // Hide them on the template
612             mCurrentView.setSliceActions(null);
613         } else if (mActions.size() > 0) {
614             // Otherwise set them on the template
615             mCurrentView.setSliceActions(mActions);
616             mActionRow.setVisibility(View.GONE);
617         }
618     }
619 
getTintColor()620     private int getTintColor() {
621         if (mThemeTintColor != -1) {
622             // Theme has specified a color, use that
623             return mThemeTintColor;
624         } else {
625             final SliceItem colorItem = SliceQuery.findSubtype(
626                     mCurrentSlice, FORMAT_INT, SUBTYPE_COLOR);
627             return colorItem != null
628                     ? colorItem.getInt()
629                     : SliceViewUtil.getColorAccent(getContext());
630         }
631     }
632 
getChildLp(View child)633     private LayoutParams getChildLp(View child) {
634         if (child instanceof ShortcutView) {
635             return new LayoutParams(mShortcutSize, mShortcutSize);
636         } else {
637             return new LayoutParams(LayoutParams.MATCH_PARENT,
638                     LayoutParams.MATCH_PARENT);
639         }
640     }
641 
642     /**
643      * @return String representation of the provided mode.
644      * @hide
645      */
646     @RestrictTo(RestrictTo.Scope.LIBRARY)
modeToString(@liceMode int mode)647     public static String modeToString(@SliceMode int mode) {
648         switch(mode) {
649             case MODE_SHORTCUT:
650                 return "MODE SHORTCUT";
651             case MODE_SMALL:
652                 return "MODE SMALL";
653             case MODE_LARGE:
654                 return "MODE LARGE";
655             default:
656                 return "unknown mode: " + mode;
657         }
658     }
659 
660     Runnable mLongpressCheck = new Runnable() {
661         @Override
662         public void run() {
663             if (mPressing && mLongClickListener != null) {
664                 mInLongpress = true;
665                 mLongClickListener.onLongClick(SliceView.this);
666                 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
667             }
668         }
669     };
670 }
671