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 androidx.annotation.RestrictTo.Scope.LIBRARY;

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.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.SliceHints;
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;

    ListContent mListContent;
    SliceChildView mCurrentView;
    View.OnLongClickListener mLongClickListener;
    Handler mHandler;

    private int mMode = MODE_LARGE;
    private Slice mCurrentSlice;
    private SliceMetrics mCurrentSliceMetrics;
    private List<SliceAction> mActions;
    private ActionRow mActionRow;
    private SliceMetadata mSliceMetadata;

    private boolean mShowActions = false;
    private boolean mIsScrollable = true;
    private boolean mShowLastUpdated = true;
    private boolean mCurrentSliceLoggedVisible = false;

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

    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);

        mCurrentView = new LargeTemplateView(getContext());
        mCurrentView.setMode(getMode());
        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();

        mCurrentView.setInsets(getPaddingStart(), getPaddingTop(), getPaddingEnd(),
                getPaddingBottom());
        setClipToPadding(false);

        super.setOnClickListener(this);
    }

    /**
     * Indicates whether this view reacts to click events or not.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public boolean isSliceViewClickable() {
        return mOnClickListener != null
                || (mListContent != null && mListContent.getPrimaryAction() != 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.getPrimaryAction() != null) {
            try {
                SliceActionImpl sa = new SliceActionImpl(mListContent.getPrimaryAction());
                boolean loading = sa.getActionItem().fireActionInternal(getContext(), null);
                if (loading) {
                    mCurrentView.setActionLoading(sa.getSliceItem());
                }
                if (mSliceObserver != null && mClickInfo != null && mClickInfo.length > 1) {
                    EventInfo eventInfo = new EventInfo(getMode(),
                            EventInfo.ACTION_TYPE_CONTENT, mClickInfo[0], mClickInfo[1]);
                    SliceItem sliceItem = mListContent.getPrimaryAction();
                    mSliceObserver.onSliceAction(eventInfo, sliceItem);
                    logSliceMetricsOnTouch(sliceItem, 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) {
        boolean ret = super.onInterceptTouchEvent(ev);
        if (mLongClickListener != null) {
            return handleTouchForLongpress(ev);
        }
        return ret;
    }

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

    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());
                break;

            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);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mPressing = false;
                mInLongpress = false;
                mHandler.removeCallbacks(mLongpressCheck);
                break;
        }
        return mInLongpress;
    }

    private int getHeightForMode(int maxHeight) {
        if (mListContent == null || !mListContent.isValid()) {
            return 0;
        }
        int mode = getMode();
        if (mode == MODE_SHORTCUT) {
            return mShortcutSize;
        }
        if (maxHeight > 0 && maxHeight <= mMinTemplateHeight) {
            maxHeight = mMinTemplateHeight;
            mListContent.setMaxSmallHeight(mMinTemplateHeight);
            mCurrentView.setMaxSmallHeight(mMinTemplateHeight);
        } else {
            mListContent.setMaxSmallHeight(0);
            mCurrentView.setMaxSmallHeight(0);
        }
        return mode == MODE_LARGE
                ? mListContent.getLargeHeight(maxHeight, mIsScrollable)
                : mListContent.getSmallHeight();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int childWidth = MeasureSpec.getSize(widthMeasureSpec);
        if (MODE_SHORTCUT == mMode) {
            // TODO: consider scaling the shortcut to fit if too small
            childWidth = mShortcutSize;
            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 == MeasureSpec.UNSPECIFIED
                ? -1 // no max, be default sizes
                : heightAvailable;
        final int sliceHeight = getHeightForMode(maxHeight);
        // Remove the padding from our available height
        int height = heightAvailable - getPaddingTop() - getPaddingBottom();
        if (heightAvailable >= sliceHeight + actionHeight
                || heightMode == MeasureSpec.UNSPECIFIED) {
            // Available space is larger than the slice or we be what we want
            if (heightMode == MeasureSpec.EXACTLY) {
                height = Math.min(sliceHeight, height);
            } else {
                height = sliceHeight;
            }
        } else {
            // Not enough space available for slice in current mode
            if (getMode() == MODE_LARGE
                    && heightAvailable >= mLargeHeight + actionHeight) {
                height = sliceHeight;
            } else if (getMode() == MODE_SHORTCUT) {
                // TODO: consider scaling the shortcut to fit if too small
                height = mShortcutSize;
            } else if (height <= mMinTemplateHeight) {
                height = sliceHeight;
            }
        }

        int childHeight = height + getPaddingTop() + getPaddingBottom();
        childWidth = childWidth + getPaddingLeft() + getPaddingRight();
        int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
        int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
        measureChild(mCurrentView, childWidthMeasureSpec, childHeightMeasureSpec);

        int actionPaddedHeight = actionHeight + getPaddingTop() + getPaddingBottom();
        int actionHeightSpec = MeasureSpec.makeMeasureSpec(actionPaddedHeight, MeasureSpec.EXACTLY);
        measureChild(mActionRow, childWidthMeasureSpec, actionHeightSpec);

        // Total height should include action row and our padding
        height += actionHeight + getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(width, height);
    }

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

    @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());
        if (isUpdate) {
            // If its an update check the loading state
            SliceMetadata oldSliceData = SliceMetadata.from(getContext(), mCurrentSlice);
            SliceMetadata newSliceData = SliceMetadata.from(getContext(), slice);
            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();
        }
        mCurrentSlice = slice;
        mListContent =
                new ListContent(getContext(), mCurrentSlice, mSliceStyle);
        if (!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
        mSliceMetadata = SliceMetadata.from(getContext(), mCurrentSlice);
        mActions = mSliceMetadata.getSliceActions();
        long lastUpdated = mSliceMetadata.getLastUpdatedTime();
        mCurrentView.setLastUpdated(lastUpdated);
        mCurrentView.setShowLastUpdated(mShowLastUpdated && isExpired());
        mCurrentView.setAllowTwoLines(mSliceMetadata.isPermissionSlice());

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

        if (mListContent.getLayoutDirItem() != null) {
            mCurrentView.setLayoutDirection(mListContent.getLayoutDirItem().getInt());
        } 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 */);
    }

    private boolean isNeverExpired() {
        if (mSliceMetadata == null) {
            return true;
        }
        long expiry = mSliceMetadata.getExpiry();
        return expiry == SliceHints.INFINITY;
    }

    boolean isExpired() {
        if (mSliceMetadata == null) {
            return false;
        }
        long expiry = mSliceMetadata.getExpiry();
        long now = System.currentTimeMillis();
        return expiry != 0 && expiry != SliceHints.INFINITY && now > expiry;
    }

    private long getTimeToExpiry() {
        if (mSliceMetadata == null) {
            return 0;
        }
        long expiry = mSliceMetadata.getExpiry();
        long now = System.currentTimeMillis();
        return (expiry == 0 || expiry == SliceHints.INFINITY || now > expiry) ? 0 : expiry - now;
    }

    /**
     * @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 != mIsScrollable) {
            mIsScrollable = isScrollable;
            if (mCurrentView instanceof LargeTemplateView) {
                ((LargeTemplateView) mCurrentView).setScrollable(mIsScrollable);
            }
        }
    }

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

    /**
     * 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);
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public void setTint(int tintColor) {
        setAccentColor(tintColor);
    }

    /**
     * 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 (mMode == 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;
        }
        mMode = mode;
        updateViewConfig();
    }

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

    /**
     * @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 LargeTemplateView(getContext());
            addView(mCurrentView, getChildLp(mCurrentView));
            newView = true;
        }

        // Set the mode
        mCurrentView.setMode(mode);

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

    private void applyConfigurations() {
        mCurrentView.setSliceActionListener(mSliceObserver);
        if (mCurrentView instanceof LargeTemplateView) {
            ((LargeTemplateView) mCurrentView).setScrollable(mIsScrollable);
        }
        mCurrentView.setStyle(mSliceStyle);
        mCurrentView.setTint(getTintColor());

        if (mListContent != null && mListContent.getLayoutDirItem() != null) {
            mCurrentView.setLayoutDirection(mListContent.getLayoutDirItem().getInt());
        } 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);
            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 && mMode != 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);
        } else {
            // Otherwise set them on the template
            mCurrentView.setSliceActions(sortedActions);
            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,
                        mListContent.getPrimaryAction().getSlice().getUri());
            }
        }
    }

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

    Runnable mRefreshLastUpdated = new Runnable() {
        @Override
        public void run() {
            if (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;
                    }
                }
            };
}