SliceView.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.widget;

import static android.app.slice.Slice.SUBTYPE_COLOR;
import static android.app.slice.SliceItem.FORMAT_INT;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.UNSPECIFIED;
import static android.view.View.MeasureSpec.makeMeasureSpec;

import android.app.PendingIntent;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;

import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Observer;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.SliceMetadata;
import androidx.slice.core.SliceAction;
import androidx.slice.core.SliceActionImpl;
import androidx.slice.core.SliceQuery;
import androidx.slice.view.R;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;

/**
 * A view for displaying {@link Slice}s.
 * <p>
 * A slice is a piece of app content and actions that can be surfaced outside of the app it
 * originates from. SliceView is able to interpret the structure and contents of a slice and display
 * it. This structure is defined by the app providing the slice when the slice is constructed with a
 * {@link androidx.slice.builders.TemplateSliceBuilder}.
 * </p>
 * <p>
 * SliceView is able to display slices in a couple of different modes via {@see #setMode}.
 * <ul>
 * <li><b>Small</b>: The small format has a restricted height and display top-level information
 * and actions from the slice.</li>
 * <li><b>Large</b>: The large format displays as much of the slice as it can based on the space
 * provided for SliceView, if the slice overflows the space SliceView will scroll the content if
 * scrolling has been enabled on SliceView, {@see #setScrollable}.</li>
 * <li><b>Shortcut</b>: A shortcut shows minimal information and is presented as a tappable icon
 * representing the main content or action associated with the slice.</li>
 * </ul>
 * </p>
 * <p>
 * Slices can contain dynamic content that may update due to user interaction or a change in the
 * data being displayed in the slice. SliceView can be configured to listen for these updates easily
 * using {@link SliceLiveData}. Example usage:
 * <pre class="prettyprint">
 * SliceView v = new SliceView(getContext());
 * v.setMode(desiredMode);
 * LiveData<Slice> liveData = SliceLiveData.fromUri(sliceUri);
 * liveData.observe(lifecycleOwner, v);
 * </pre>
 * </p>
 * <p>
 * SliceView supports various style options, see {@link R.styleable#SliceView SliceView Attributes}.
 *
 * @see Slice
 * @see SliceLiveData
 */
@RequiresApi(19)
public class SliceView extends ViewGroup implements Observer<Slice>, View.OnClickListener {

    private static final String TAG = "SliceView";

    /**
     * Implement this interface to be notified of interactions with the slice displayed
     * in this view.
     * @see EventInfo
     */
    public interface OnSliceActionListener {
        /**
         * Called when an interaction has occurred with an element in this view.
         * @param info the type of event that occurred.
         * @param item the specific item within the {@link Slice} that was interacted with.
         */
        void onSliceAction(@NonNull EventInfo info, @NonNull SliceItem item);
    }

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    @IntDef({
            MODE_SMALL, MODE_LARGE, MODE_SHORTCUT
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface SliceMode {}

    /**
     * Mode indicating this slice should be presented in small format, only top-level information
     * and actions from the slice are shown.
     */
    public static final int MODE_SMALL       = 1;
    /**
     * Mode indicating this slice should be presented in large format, as much or all of the slice
     * contents are shown.
     */
    public static final int MODE_LARGE       = 2;
    /**
     * Mode indicating this slice should be presented as a tappable icon.
     */
    public static final int MODE_SHORTCUT    = 3;

    /**
     * Refresh last updated label every 60 seconds when the slice is visible.
     */
    private static final int REFRESH_LAST_UPDATED_IN_MILLIS = 60000;

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    ListContent mListContent;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    SliceChildView mCurrentView;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    View.OnLongClickListener mLongClickListener;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    Handler mHandler;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    SliceMetadata mSliceMetadata;

    private Slice mCurrentSlice;
    private SliceMetrics mCurrentSliceMetrics;
    private List<SliceAction> mActions;
    private ActionRow mActionRow;

    private boolean mShowActions = false;
    private boolean mShowLastUpdated = true;
    private boolean mCurrentSliceLoggedVisible = false;
    private boolean mShowTitleItems = false;
    private boolean mShowHeaderDivider = false;
    private boolean mShowActionDividers = false;

    private int mShortcutSize;
    private int mMinTemplateHeight;
    private int mLargeHeight;
    private int mActionRowHeight;

    private SliceViewPolicy mViewPolicy;
    private SliceStyle mSliceStyle;
    private int mThemeTintColor = -1;

    private OnSliceActionListener mSliceObserver;
    private int mTouchSlopSquared;
    private View.OnClickListener mOnClickListener;
    private int mDownX;
    private int mDownY;
    boolean mPressing;
    boolean mInLongpress;
    int[] mClickInfo;

    public SliceView(Context context) {
        this(context, null);
    }

    public SliceView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, R.attr.sliceViewStyle);
    }

    public SliceView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr, R.style.Widget_SliceView);
    }

    @RequiresApi(21)
    public SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs, defStyleAttr, defStyleRes);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        mSliceStyle = new SliceStyle(context, attrs, defStyleAttr, defStyleRes);
        mThemeTintColor = mSliceStyle.getTintColor();
        mShortcutSize = getContext().getResources()
                .getDimensionPixelSize(R.dimen.abc_slice_shortcut_size);
        mMinTemplateHeight = getContext().getResources()
                .getDimensionPixelSize(R.dimen.abc_slice_row_min_height);
        mLargeHeight = getResources().getDimensionPixelSize(R.dimen.abc_slice_large_height);
        mActionRowHeight = getResources().getDimensionPixelSize(
                R.dimen.abc_slice_action_row_height);
        mViewPolicy = new SliceViewPolicy();
        mCurrentView = new TemplateView(getContext());
        mCurrentView.setPolicy(mViewPolicy);
        addView(mCurrentView, getChildLp(mCurrentView));
        applyConfigurations();

        // TODO: action row background should support light / dark / maybe presenter customization
        mActionRow = new ActionRow(getContext(), true);
        mActionRow.setBackground(new ColorDrawable(0xffeeeeee));
        addView(mActionRow, getChildLp(mActionRow));
        updateActions();

        final int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        mTouchSlopSquared = slop * slop;
        mHandler = new Handler();

        setClipToPadding(false);
        super.setOnClickListener(this);
    }

    @VisibleForTesting
    void setSliceViewPolicy(SliceViewPolicy policy) {
        mViewPolicy = policy;
    }

    /**
     * Indicates whether this view reacts to click events or not.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public boolean isSliceViewClickable() {
        return mOnClickListener != null
                || (mListContent != null && mListContent.getShortcut(getContext()) != null);
    }

    /**
     * Sets the event info for logging a click.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public void setClickInfo(int[] info) {
        mClickInfo = info;
    }

    @Override
    public void onClick(View v) {
        if (mListContent != null && mListContent.getShortcut(getContext()) != null) {
            try {
                SliceActionImpl sa = (SliceActionImpl) mListContent.getShortcut(getContext());
                SliceItem actionItem = sa.getActionItem();
                boolean loading = actionItem != null
                        && actionItem.fireActionInternal(getContext(), null);
                if (loading) {
                    mCurrentView.setActionLoading(sa.getSliceItem());
                }
                if (actionItem != null && mSliceObserver != null && mClickInfo != null
                        && mClickInfo.length > 1) {
                    EventInfo eventInfo = new EventInfo(getMode(),
                            EventInfo.ACTION_TYPE_CONTENT, mClickInfo[0], mClickInfo[1]);
                    mSliceObserver.onSliceAction(eventInfo, sa.getSliceItem());
                    logSliceMetricsOnTouch(sa.getSliceItem(), eventInfo);
                }
            } catch (PendingIntent.CanceledException e) {
                Log.e(TAG, "PendingIntent for slice cannot be sent", e);
            }
        } else if (mOnClickListener != null) {
            mOnClickListener.onClick(this);
        }
    }

    @Override
    public void setOnClickListener(View.OnClickListener listener) {
        mOnClickListener = listener;
    }

    @Override
    public void setOnLongClickListener(View.OnLongClickListener listener) {
        super.setOnLongClickListener(listener);
        mLongClickListener = listener;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return (mLongClickListener != null && handleTouchForLongpress(ev))
                || super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return (mLongClickListener != null && handleTouchForLongpress(ev))
                || super.onTouchEvent(ev);
    }

    private boolean handleTouchForLongpress(MotionEvent ev) {
        int action = ev.getActionMasked();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mHandler.removeCallbacks(mLongpressCheck);
                mDownX = (int) ev.getRawX();
                mDownY = (int) ev.getRawY();
                mPressing = true;
                mInLongpress = false;
                mHandler.postDelayed(mLongpressCheck, ViewConfiguration.getLongPressTimeout());
                return false;

            case MotionEvent.ACTION_MOVE:
                final int deltaX = (int) ev.getRawX() - mDownX;
                final int deltaY = (int) ev.getRawY() - mDownY;
                int distance = (deltaX * deltaX) + (deltaY * deltaY);
                if (distance > mTouchSlopSquared) {
                    mPressing = false;
                    mHandler.removeCallbacks(mLongpressCheck);
                }
                // If a long press has already happened, consume further movement.
                return mInLongpress;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                boolean wasInLongpress = mInLongpress;
                mPressing = false;
                mInLongpress = false;
                mHandler.removeCallbacks(mLongpressCheck);
                // If a long press just happened, consume up event to avoid a duplicate short click.
                return wasInLongpress;

            default:
                return false;
        }
    }

    private void configureViewPolicy(int maxHeight) {
        if (mListContent != null && mListContent.isValid() && getMode() != MODE_SHORTCUT) {
            if (maxHeight > 0 && maxHeight < mSliceStyle.getRowMaxHeight()) {
                if (maxHeight <= mMinTemplateHeight) {
                    maxHeight = mMinTemplateHeight;
                }
                mViewPolicy.setMaxSmallHeight(maxHeight);
            } else {
                mViewPolicy.setMaxSmallHeight(0);
            }
            mViewPolicy.setMaxHeight(maxHeight);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        if (MODE_SHORTCUT == getMode()) {
            // TODO: consider scaling the shortcut to fit if too small
            width = mShortcutSize + getPaddingLeft() + getPaddingRight();
        }
        final int actionHeight = mActionRow.getVisibility() != View.GONE
                ? mActionRowHeight
                : 0;
        final int heightAvailable = MeasureSpec.getSize(heightMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        LayoutParams lp = getLayoutParams();
        final int maxHeight = (lp != null && lp.height == LayoutParams.WRAP_CONTENT)
                || heightMode == UNSPECIFIED
                ? -1 // no max, be default sizes
                : heightAvailable;
        configureViewPolicy(maxHeight);
        // Remove the padding from our available height
        int childrenHeight = heightAvailable - getPaddingTop() - getPaddingBottom();

        // never change the height if set to exactly
        if (heightMode != EXACTLY) {
            if (mListContent == null || !mListContent.isValid()) {
                childrenHeight = actionHeight;
            } else if (getMode() == MODE_SHORTCUT) {
                // No compromise in case of shortcut
                childrenHeight = mShortcutSize + actionHeight;
            } else {
                int requiredHeight =
                        mListContent.getHeight(mSliceStyle, mViewPolicy) + actionHeight;
                if (childrenHeight > requiredHeight || heightMode == UNSPECIFIED) {
                    // Available space is larger than what the slice wants
                    childrenHeight = requiredHeight;
                } else {
                    // Not enough space available for slice in current mode
                    if (getMode() == MODE_LARGE
                            && childrenHeight >= mLargeHeight + actionHeight) {
                        childrenHeight = mLargeHeight + actionHeight;
                    } else if (childrenHeight <= mMinTemplateHeight) {
                        childrenHeight = mMinTemplateHeight;
                    }
                }
            }
        }

        // Measure directly instead of calling measureChild as the later substracts padding
        // from the provided size
        int childWidthSpec = makeMeasureSpec(width, EXACTLY);
        int actionRowHeight = actionHeight > 0 ? (actionHeight + getPaddingBottom()) : 0;
        mActionRow.measure(childWidthSpec, makeMeasureSpec(actionRowHeight, EXACTLY));

        // Include the bottom padding for currentView only if action row is invisible
        int currentViewHeight = childrenHeight + getPaddingTop()
                + (actionHeight > 0 ? 0 : getPaddingBottom());
        mCurrentView.measure(childWidthSpec, makeMeasureSpec(currentViewHeight, EXACTLY));
        setMeasuredDimension(width,
                mCurrentView.getMeasuredHeight() + mActionRow.getMeasuredHeight());
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        View v = mCurrentView;
        v.layout(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());
        if (mActionRow.getVisibility() != View.GONE) {
            int top = v.getMeasuredHeight();
            mActionRow.layout(0, top, mActionRow.getMeasuredWidth(),
                    mActionRow.getMeasuredHeight() + top);
        }
    }

    @Override
    public void onChanged(@Nullable Slice slice) {
        setSlice(slice);
    }

    /**
     * Populates this view to the provided {@link Slice}.
     *
     * This will not update automatically if the slice content changes, for live
     * content see {@link SliceLiveData}.
     */
    public void setSlice(@Nullable Slice slice) {
        initSliceMetrics(slice);
        boolean isUpdate = slice != null && mCurrentSlice != null
                && slice.getUri().equals(mCurrentSlice.getUri());
        SliceMetadata oldSliceData = mSliceMetadata;
        mCurrentSlice = slice;
        mSliceMetadata = mCurrentSlice != null ? SliceMetadata.from(getContext(), mCurrentSlice)
                : null;
        if (isUpdate) {
            // If its an update check the loading state
            SliceMetadata newSliceData = mSliceMetadata;
            if (oldSliceData.getLoadingState() == SliceMetadata.LOADED_ALL
                    && newSliceData.getLoadingState() == SliceMetadata.LOADED_NONE) {
                // If it's the same slice going from "loaded all" to "loaded none"... let's
                // ignore the update.
                return;
            }
        } else {
            mCurrentView.resetView();
        }
        mListContent = mSliceMetadata != null ? mSliceMetadata.getListContent() : null;
        if (mShowTitleItems) {
            showTitleItems(true);
        }
        if (mShowHeaderDivider) {
            showHeaderDivider(true);
        }
        if (mShowActionDividers) {
            showActionDividers(true);
        }
        if (mListContent == null || !mListContent.isValid()) {
            mActions = null;
            mCurrentView.resetView();
            updateActions();
            return;
        }
        // New slice means we shouldn't have any actions loading
        mCurrentView.setLoadingActions(null);

        // Check if the slice content is expired and show when it was last updated
        mActions = mSliceMetadata.getSliceActions();
        mCurrentView.setLastUpdated(mSliceMetadata.getLastUpdatedTime());
        mCurrentView.setShowLastUpdated(mShowLastUpdated && mSliceMetadata.isExpired());
        mCurrentView.setAllowTwoLines(mSliceMetadata.isPermissionSlice());

        // Tint color can come with the slice, so may need to update it
        mCurrentView.setTint(getTintColor());

        if (mListContent.getLayoutDir() != -1) {
            mCurrentView.setLayoutDirection(mListContent.getLayoutDir());
        } else {
            mCurrentView.setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT);
        }

        // Set the slice
        mCurrentView.setSliceContent(mListContent);
        updateActions();

        // Log slice metrics visible.
        logSliceMetricsVisibilityChange(true /* visible */);

        // Automatically refresh the last updated label when the slice TTL isn't infinity.
        refreshLastUpdatedLabel(true /* visible */);
    }

    /**
     * @return the slice being used to populate this view.
     */
    @Nullable
    public Slice getSlice() {
        return mCurrentSlice;
    }

    /**
     * Returns the slice actions presented in this view.
     * <p>
     * Note that these may be different from {@link SliceMetadata#getSliceActions()} if the actions
     * set on the view have been adjusted using {@link #setSliceActions(List)}.
     */
    @Nullable
    public List<SliceAction> getSliceActions() {
        if (mActions != null && mActions.isEmpty()) {
            // They're empty because presenter set null slice actions, return null to be consistent.
            return null;
        }
        return mActions;
    }

    /**
     * Sets the slice actions to display for the slice contained in this view. Normally SliceView
     * will automatically show actions, however, it is possible to reorder or omit actions on the
     * view using this method. This is generally discouraged.
     * <p>
     * It is required that the slice be set on this view before actions can be set, otherwise
     * this will throw {@link IllegalStateException}. If any of the actions supplied are not
     * available for the slice set on this view (i.e. the action is not returned by
     * {@link SliceMetadata#getSliceActions()} this will throw {@link IllegalArgumentException}.
     */
    public void setSliceActions(@Nullable List<SliceAction> newActions) {
        // Check that these actions are part of available set
        if (mCurrentSlice == null || mSliceMetadata == null) {
            throw new IllegalStateException("Trying to set actions on a view without a slice");
        }
        List<SliceAction> availableActions = mSliceMetadata.getSliceActions();
        if (availableActions != null && newActions != null) {
            for (int i = 0; i < newActions.size(); i++) {
                if (!availableActions.contains(newActions.get(i))) {
                    throw new IllegalArgumentException(
                            "Trying to set an action that isn't available: " + newActions.get(i));
                }
            }
        }
        mActions = newActions == null ? new ArrayList<SliceAction>() : newActions;
        updateActions();
    }

    /**
     * Set the mode this view should present in.
     */
    public void setMode(@SliceMode int mode) {
        setMode(mode, false /* animate */);
    }

    /**
     * Set whether this view should allow scrollable content when presenting in {@link #MODE_LARGE}.
     */
    public void setScrollable(boolean isScrollable) {
        if (isScrollable != mViewPolicy.isScrollable()) {
            mViewPolicy.setScrollable(isScrollable);
        }
    }

    /**
     * Whether this view allow scrollable content when presenting in {@link #MODE_LARGE}.
     */
    public boolean isScrollable() {
        return mViewPolicy.isScrollable();
    }

    /**
     * Sets the listener to notify when an interaction event occurs on the view.
     * @see EventInfo
     */
    public void setOnSliceActionListener(@Nullable OnSliceActionListener observer) {
        mSliceObserver = observer;
        mCurrentView.setSliceActionListener(mSliceObserver);
    }

    /**
     * Contents of a slice such as icons, text, and controls (e.g. toggle) can be tinted. Normally
     * a color for tinting will be provided by the slice. Using this method will override
     * the slice-provided color information and instead tint elements with the color set here.
     *
     * @param accentColor the color to use for tinting contents of this view.
     */
    public void setAccentColor(@ColorInt int accentColor) {
        mThemeTintColor = accentColor;
        mSliceStyle.setTintColor(mThemeTintColor);
        mCurrentView.setTint(getTintColor());
    }

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public void setMode(@SliceMode int mode, boolean animate) {
        if (animate) {
            Log.e(TAG, "Animation not supported yet");
        }
        if (mViewPolicy.getMode() == mode) {
            return;
        }
        if (mode != MODE_SMALL && mode != MODE_LARGE && mode != MODE_SHORTCUT) {
            Log.w(TAG, "Unknown mode: " + mode
                    + " please use one of MODE_SHORTCUT, MODE_SMALL, MODE_LARGE");
            mode = MODE_LARGE;
        }
        mViewPolicy.setMode(mode);
        updateViewConfig();
    }

    /**
     * @return the mode this view is presenting in.
     */
    public @SliceMode int getMode() {
        return mViewPolicy.getMode();
    }

    /**
     * Whether this view should show title items on the first row of the slice.
     * Title items appear at the start of the row.
     */
    public void setShowTitleItems(boolean enabled) {
        mShowTitleItems = enabled;
        if (mListContent != null) {
            mListContent.showTitleItems(enabled);
        }
    }

    /**
     * @deprecated TO BE REMOVED
     * @removed
     */
    @Deprecated
    public void showTitleItems(boolean enabled) {
        setShowTitleItems(enabled);
    }

    /**
     * Whether this view should show the header divider.
     */
    public void setShowHeaderDivider(boolean enabled) {
        mShowHeaderDivider = enabled;
        if (mListContent != null) {
            mListContent.showHeaderDivider(enabled);
        }
    }

    /**
     * @deprecated TO BE REMOVED
     * @removed
     */
    @Deprecated
    public void showHeaderDivider(boolean enabled) {
        setShowHeaderDivider(enabled);
    }

    /**
     * Whether this view should show action dividers for rows.
     */
    public void setShowActionDividers(boolean enabled) {
        mShowActionDividers = enabled;
        if (mListContent != null) {
            mListContent.showActionDividers(enabled);
        }
    }

    /**
     * @deprecated TO BE REMOVED
     * @removed
     */
    @Deprecated
    public void showActionDividers(boolean enabled) {
        setShowActionDividers(enabled);
    }

    /**
     * @hide
     *
     * Whether this view should show a row of actions with it.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public void setShowActionRow(boolean show) {
        mShowActions = show;
        updateActions();
    }

    /**
     * @return whether this view is showing a row of actions.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public boolean isShowingActionRow() {
        return mShowActions;
    }

    /**
     * Updates the current view to represent the correct type of view for the current mode.
     * If the view is changed for the mode any configurations are also applied to it.
     */
    private void updateViewConfig() {
        boolean newView = false;

        // Check if our view is right for the current mode
        int mode = getMode();
        boolean isCurrentViewShortcut = mCurrentView instanceof ShortcutView;
        Set<SliceItem> loadingActions = mCurrentView.getLoadingActions();
        if (mode == MODE_SHORTCUT && !isCurrentViewShortcut) {
            removeView(mCurrentView);
            mCurrentView = new ShortcutView(getContext());
            addView(mCurrentView, getChildLp(mCurrentView));
            newView = true;
        } else if (mode != MODE_SHORTCUT && isCurrentViewShortcut) {
            removeView(mCurrentView);
            mCurrentView = new TemplateView(getContext());
            addView(mCurrentView, getChildLp(mCurrentView));
            newView = true;
        }

        // If the view changes we should apply any configurations to it
        if (newView) {
            mCurrentView.setPolicy(mViewPolicy);
            applyConfigurations();
            if (mListContent != null && mListContent.isValid()) {
                mCurrentView.setSliceContent(mListContent);
            }
            mCurrentView.setLoadingActions(loadingActions);
        }
        updateActions();
    }

    private void applyConfigurations() {
        mCurrentView.setSliceActionListener(mSliceObserver);
        mCurrentView.setStyle(mSliceStyle);
        mCurrentView.setTint(getTintColor());

        if (mListContent != null && mListContent.getLayoutDir() != -1) {
            mCurrentView.setLayoutDirection(mListContent.getLayoutDir());
        } else {
            mCurrentView.setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT);
        }
    }

    private void updateActions() {
        if (mActions == null) {
            // No actions, hide the row, clear out the view
            mActionRow.setVisibility(View.GONE);
            mCurrentView.setSliceActions(null);
            mCurrentView.setInsets(getPaddingStart(), getPaddingTop(), getPaddingEnd(),
                    getPaddingBottom());
            return;
        }
        // Sort actions based on priority and set them in action rows.
        List<SliceAction> sortedActions = new ArrayList<>(mActions);
        Collections.sort(sortedActions, SLICE_ACTION_PRIORITY_COMPARATOR);
        if (mShowActions && getMode() != MODE_SHORTCUT && mActions.size() >= 2) {
            // Show in action row if available
            mActionRow.setActions(sortedActions, getTintColor());
            mActionRow.setVisibility(View.VISIBLE);

            // Hide them on the template
            mCurrentView.setSliceActions(null);

            mCurrentView.setInsets(getPaddingStart(), getPaddingTop(), getPaddingEnd(), 0);
            mActionRow.setPaddingRelative(getPaddingStart(), 0, getPaddingEnd(),
                    getPaddingBottom());

        } else {
            // Otherwise set them on the template
            mCurrentView.setSliceActions(sortedActions);
            mCurrentView.setInsets(getPaddingStart(), getPaddingTop(), getPaddingEnd(),
                    getPaddingBottom());

            mActionRow.setVisibility(View.GONE);
        }
    }

    private int getTintColor() {
        if (mThemeTintColor != -1) {
            // Theme has specified a color, use that
            return mThemeTintColor;
        } else {
            final SliceItem colorItem = SliceQuery.findSubtype(
                    mCurrentSlice, FORMAT_INT, SUBTYPE_COLOR);
            return colorItem != null
                    ? colorItem.getInt()
                    : SliceViewUtil.getColorAccent(getContext());
        }
    }

    private LayoutParams getChildLp(View child) {
        if (child instanceof ShortcutView) {
            return new LayoutParams(mShortcutSize, mShortcutSize);
        } else {
            return new LayoutParams(LayoutParams.MATCH_PARENT,
                    LayoutParams.MATCH_PARENT);
        }
    }

    /**
     * @return String representation of the provided mode.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static String modeToString(@SliceMode int mode) {
        switch(mode) {
            case MODE_SHORTCUT:
                return "MODE SHORTCUT";
            case MODE_SMALL:
                return "MODE SMALL";
            case MODE_LARGE:
                return "MODE LARGE";
            default:
                return "unknown mode: " + mode;
        }
    }

    Runnable mLongpressCheck = new Runnable() {
        @Override
        public void run() {
            if (mPressing && mLongClickListener != null) {
                mInLongpress = true;
                mLongClickListener.onLongClick(SliceView.this);
                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
            }
        }
    };

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (isShown()) {
            logSliceMetricsVisibilityChange(true /* visible */);
            refreshLastUpdatedLabel(true /* visible */);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        logSliceMetricsVisibilityChange(false /* not visible */);
        refreshLastUpdatedLabel(false /* not visible */);
    }

    @Override
    protected void onVisibilityChanged(View changedView, int visibility) {
        super.onVisibilityChanged(changedView, visibility);
        if (isAttachedToWindow()) {
            logSliceMetricsVisibilityChange(visibility == VISIBLE);
            refreshLastUpdatedLabel(visibility == VISIBLE);
        }
    }

    @Override
    protected void onWindowVisibilityChanged(int visibility) {
        super.onWindowVisibilityChanged(visibility);
        logSliceMetricsVisibilityChange(visibility == VISIBLE);
        refreshLastUpdatedLabel(visibility == VISIBLE);
    }

    private void initSliceMetrics(@Nullable Slice slice) {
        if (slice == null || slice.getUri() == null) {
            logSliceMetricsVisibilityChange(false /* not visible */);
            mCurrentSliceMetrics = null;
        } else if (mCurrentSlice == null || !mCurrentSlice.getUri().equals(slice.getUri())) {
            logSliceMetricsVisibilityChange(false /* not visible */);
            mCurrentSliceMetrics =
                    SliceMetrics.getInstance(getContext(), slice.getUri());
        }
    }

    private void logSliceMetricsVisibilityChange(boolean visibility) {
        if (mCurrentSliceMetrics != null) {
            if (visibility && !mCurrentSliceLoggedVisible) {
                mCurrentSliceMetrics.logVisible();
                mCurrentSliceLoggedVisible = true;
            }
            if (!visibility && mCurrentSliceLoggedVisible) {
                mCurrentSliceMetrics.logHidden();
                mCurrentSliceLoggedVisible = false;
            }
        }
    }

    private void logSliceMetricsOnTouch(SliceItem item, EventInfo info) {
        if (mCurrentSliceMetrics != null) {
            if (item.getSlice() != null && item.getSlice().getUri() != null) {
                mCurrentSliceMetrics.logTouch(
                        info.actionType,
                        item.getSlice().getUri());
            }
        }
    }

    private void refreshLastUpdatedLabel(boolean visibility) {
        if (mShowLastUpdated && mSliceMetadata != null && !mSliceMetadata.neverExpires()) {
            if (visibility) {
                mHandler.postDelayed(mRefreshLastUpdated, mSliceMetadata.isExpired()
                        ? REFRESH_LAST_UPDATED_IN_MILLIS
                        : mSliceMetadata.getTimeToExpiry() + REFRESH_LAST_UPDATED_IN_MILLIS);
            } else {
                mHandler.removeCallbacks(mRefreshLastUpdated);
            }
        }
    }

    Runnable mRefreshLastUpdated = new Runnable() {
        @Override
        public void run() {
            if (mSliceMetadata != null && mSliceMetadata.isExpired()) {
                mCurrentView.setShowLastUpdated(true);
                mCurrentView.setSliceContent(mListContent);
            }
            mHandler.postDelayed(this, REFRESH_LAST_UPDATED_IN_MILLIS);
        }
    };

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public static final Comparator<SliceAction> SLICE_ACTION_PRIORITY_COMPARATOR =
            new Comparator<SliceAction>() {
                @Override
                public int compare(SliceAction action1, SliceAction action2) {
                    // Priority 0 is the highest and -1 meaning no priority.
                    int priority1 = action1.getPriority();
                    int priority2 = action2.getPriority();
                    if (priority1 < 0 && priority2 < 0) {
                        return 0;
                    } else if (priority1 < 0) {
                        return 1;
                    } else if (priority2 < 0) {
                        return -1;
                    } else if (priority2 < priority1) {
                        return 1;
                    } else if (priority2 > priority1) {
                        return -1;
                    } else {
                        return 0;
                    }
                }
            };
}