Home | History | Annotate | Download | only in widget
      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          */
    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 
    160     public SliceView(Context context) {
    161         this(context, null);
    162     }
    163 
    164     public SliceView(Context context, @Nullable AttributeSet attrs) {
    165         this(context, attrs, R.attr.sliceViewStyle);
    166     }
    167 
    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)
    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 
    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)
    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)
    229     public void setClickInfo(int[] info) {
    230         mClickInfo = info;
    231     }
    232 
    233     @Override
    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
    253     public void setOnClickListener(View.OnClickListener listener) {
    254         mOnClickListener = listener;
    255     }
    256 
    257     @Override
    258     public void setOnLongClickListener(View.OnLongClickListener listener) {
    259         super.setOnLongClickListener(listener);
    260         mLongClickListener = listener;
    261     }
    262 
    263     @Override
    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
    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 
    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 
    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
    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
    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
    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      */
    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
    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
    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      */
    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      */
    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      */
    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      */
    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
    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      */
    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)
    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      */
    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)
    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)
    545     public boolean isShowingActionRow() {
    546         return mShowActions;
    547     }
    548 
    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 
    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 
    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 
    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)
    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