blob: c51c90121b2647a4720a1214f09c165f2c957237 [file] [log] [blame]
/*
* 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.EXTRA_RANGE_VALUE;
import static android.app.slice.Slice.HINT_NO_TINT;
import static android.app.slice.Slice.HINT_PARTIAL;
import static android.app.slice.Slice.HINT_SHORTCUT;
import static android.app.slice.Slice.SUBTYPE_MAX;
import static android.app.slice.Slice.SUBTYPE_VALUE;
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_LONG;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import static androidx.slice.core.SliceHints.ICON_IMAGE;
import static androidx.slice.core.SliceHints.SMALL_IMAGE;
import static androidx.slice.core.SliceHints.SUBTYPE_MIN;
import static androidx.slice.widget.EventInfo.ACTION_TYPE_BUTTON;
import static androidx.slice.widget.EventInfo.ACTION_TYPE_SLIDER;
import static androidx.slice.widget.EventInfo.ACTION_TYPE_TOGGLE;
import static androidx.slice.widget.EventInfo.ROW_TYPE_LIST;
import static androidx.slice.widget.EventInfo.ROW_TYPE_SLIDER;
import static androidx.slice.widget.EventInfo.ROW_TYPE_TOGGLE;
import static androidx.slice.widget.SliceView.MODE_SMALL;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
import android.content.Intent;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.StyleSpan;
import android.util.ArrayMap;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.slice.SliceItem;
import androidx.slice.SliceStructure;
import androidx.slice.core.SliceAction;
import androidx.slice.core.SliceActionImpl;
import androidx.slice.core.SliceQuery;
import androidx.slice.view.R;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Row item is in small template format and can be used to construct list items for use
* with {@link TemplateView}.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@RequiresApi(19)
public class RowView extends SliceChildView implements View.OnClickListener {
private static final String TAG = "RowView";
// The number of items that fit on the right hand side of a small slice
// TODO: this should be based on available width
private static final int MAX_END_ITEMS = 3;
// How frequently (ms) intent can be sent in response to slider moving.
private static final int SLIDER_INTERVAL = 200;
// On versions before M, SeekBar won't render properly if stretched taller than the default
// size.
private static final boolean sCanSpecifyLargerRangeBarHeight =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
private LinearLayout mRootView;
private LinearLayout mStartContainer;
private LinearLayout mContent;
private TextView mPrimaryText;
private TextView mSecondaryText;
private TextView mLastUpdatedText;
private View mBottomDivider;
private View mActionDivider;
private ArrayMap<SliceActionImpl, SliceActionView> mToggles = new ArrayMap<>();
private ArrayMap<SliceActionImpl, SliceActionView> mActions = new ArrayMap<>();
private LinearLayout mEndContainer;
private View mSeeMoreView;
private ProgressBar mRangeBar;
private ProgressBar mActionSpinner;
protected Set<SliceItem> mLoadingActions = new HashSet<>();
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mShowActionSpinner;
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mRowIndex;
@SuppressWarnings("WeakerAccess") /* synthetic access */
RowContent mRowContent;
private SliceActionImpl mRowAction;
private SliceItem mStartItem;
private boolean mIsHeader;
private List<SliceAction> mHeaderActions;
// Indicates whether header rows can have 2 lines of subtitle text
private boolean mAllowTwoLines;
// Indicates if there's a slider in this row that is currently being interacted with.
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mIsRangeSliding;
// Indicates that there was an update to the row but we skipped it while the slice was
// being interacted with.
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mRangeHasPendingUpdate;
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mRangeUpdaterRunning;
@SuppressWarnings("WeakerAccess") /* synthetic access */
Handler mHandler;
@SuppressWarnings("WeakerAccess") /* synthetic access */
long mLastSentRangeUpdate;
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mRangeValue;
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mRangeMinValue;
private SliceItem mRangeItem;
private int mImageSize;
private int mIconSize;
// How big the RowView wants mRangeBar to be.
private int mIdealRangeHeight;
// How big mRangeBar wants to be.
private int mMeasuredRangeHeight;
public RowView(Context context) {
super(context);
mIconSize = getContext().getResources().getDimensionPixelSize(R.dimen.abc_slice_icon_size);
mImageSize = getContext().getResources().getDimensionPixelSize(
R.dimen.abc_slice_small_image_size);
mRootView = (LinearLayout) LayoutInflater.from(context).inflate(
R.layout.abc_slice_small_template, this, false);
addView(mRootView);
mStartContainer = (LinearLayout) findViewById(R.id.icon_frame);
mContent = (LinearLayout) findViewById(android.R.id.content);
mPrimaryText = (TextView) findViewById(android.R.id.title);
mSecondaryText = (TextView) findViewById(android.R.id.summary);
mLastUpdatedText = (TextView) findViewById(R.id.last_updated);
mBottomDivider = findViewById(R.id.bottom_divider);
mActionDivider = findViewById(R.id.action_divider);
mActionSpinner = findViewById(R.id.action_sent_indicator);
SliceViewUtil.tintIndeterminateProgressBar(getContext(), mActionSpinner);
mEndContainer = (LinearLayout) findViewById(android.R.id.widget_frame);
mIdealRangeHeight = context.getResources().getDimensionPixelSize(
R.dimen.abc_slice_row_range_height);
}
@Override
public void setStyle(SliceStyle styles) {
super.setStyle(styles);
applyRowStyle();
}
private void applyRowStyle() {
if (mSliceStyle == null || mSliceStyle.getRowStyle() == null) {
return;
}
final RowStyle rowStyle = mSliceStyle.getRowStyle();
setViewPaddingEnd(mStartContainer, rowStyle.getTitleItemEndPadding());
setViewSidePaddings(mContent,
rowStyle.getContentStartPadding(), rowStyle.getContentEndPadding());
setViewSidePaddings(mEndContainer,
rowStyle.getEndItemStartPadding(), rowStyle.getEndItemEndPadding());
setViewSideMargins(mBottomDivider,
rowStyle.getBottomDividerStartPadding(), rowStyle.getBottomDividerEndPadding());
setViewHeight(mActionDivider, rowStyle.getActionDividerHeight());
}
private void setViewPaddingEnd(View v, int end) {
if (v != null && end >= 0) {
v.setPaddingRelative(v.getPaddingStart(), v.getPaddingTop(), end, v.getPaddingBottom());
}
}
private void setViewSidePaddings(View v, int start, int end) {
if (v != null && start >= 0 && end >= 0) {
v.setPaddingRelative(start, v.getPaddingTop(), end, v.getPaddingBottom());
}
}
private void setViewSideMargins(View v, int start, int end) {
if (v != null && start >= 0 && end >= 0) {
final MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams();
params.setMarginStart(start);
params.setMarginEnd(end);
mBottomDivider.setLayoutParams(params);
}
}
private void setViewHeight(View v, int height) {
if (v != null && height >= 0) {
final ViewGroup.LayoutParams params = v.getLayoutParams();
params.height = height;
v.setLayoutParams(params);
}
}
@Override
public void setInsets(int l, int t, int r, int b) {
super.setInsets(l, t, r, b);
setPadding(l, t, r, b);
}
/**
* @return height row content (i.e. title, subtitle) without the height of the range element.
*/
private int getRowContentHeight() {
int rowHeight = mRowContent.getHeight(mSliceStyle, mViewPolicy);
if (mRangeBar != null) {
rowHeight -= mIdealRangeHeight;
}
return rowHeight;
}
@Override
public void setTint(@ColorInt int tintColor) {
super.setTint(tintColor);
if (mRowContent != null) {
// TODO -- can be smarter about this
populateViews(true);
}
}
/**
* @param actions if the actions are null then there are no header actions for this row.
* If the actions are an empty list, then something has explicitly set that no header
* actions should appear.
*/
@Override
public void setSliceActions(List<SliceAction> actions) {
mHeaderActions = actions;
if (mRowContent != null) {
updateEndItems();
}
}
@Override
public void setShowLastUpdated(boolean showLastUpdated) {
super.setShowLastUpdated(showLastUpdated);
if (mRowContent != null) {
populateViews(true);
}
}
@Override
public void setAllowTwoLines(boolean allowTwoLines) {
mAllowTwoLines = allowTwoLines;
if (mRowContent != null) {
populateViews(true);
}
}
private void measureChildWithExactHeight(View child, int widthMeasureSpec, int childHeight) {
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight + mInsetTop + mInsetBottom,
MeasureSpec.EXACTLY);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childWidth = 0;
int rowHeight = getRowContentHeight();
if (rowHeight != 0) {
// Might be gone if we have range / progress but nothing else
mRootView.setVisibility(View.VISIBLE);
measureChildWithExactHeight(mRootView, widthMeasureSpec, rowHeight);
childWidth = mRootView.getMeasuredWidth();
} else {
mRootView.setVisibility(View.GONE);
}
if (mRangeBar != null) {
// If we're on a platform where SeekBar can't be stretched vertically, find out the
// exact size it would like to be so we can honor that in onLayout.
if (sCanSpecifyLargerRangeBarHeight) {
measureChildWithExactHeight(mRangeBar, widthMeasureSpec, mIdealRangeHeight);
} else {
measureChild(mRangeBar, widthMeasureSpec,
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
}
// Remember the measured height later for onLayout, since super.onMeasure will overwrite
// it.
mMeasuredRangeHeight = mRangeBar.getMeasuredHeight();
childWidth = Math.max(childWidth, mRangeBar.getMeasuredWidth());
}
childWidth = Math.max(childWidth + mInsetStart + mInsetEnd, getSuggestedMinimumWidth());
int totalHeight = mRowContent != null ? mRowContent.getHeight(mSliceStyle, mViewPolicy) : 0;
setMeasuredDimension(resolveSizeAndState(childWidth, widthMeasureSpec, 0),
totalHeight + mInsetTop + mInsetBottom);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftPadding = getPaddingLeft();
mRootView.layout(leftPadding, mInsetTop, mRootView.getMeasuredWidth() + leftPadding,
getRowContentHeight() + mInsetTop);
if (mRangeBar != null) {
// If we're on aa platform where SeekBar can't be stretched vertically, then
// mMeasuredRangeHeight can (and probably will) be smaller than mIdealRangeHeight, so we
// need to add some padding to make mRangeBar look like it's the larger size.
int verticalPadding = (mIdealRangeHeight - mMeasuredRangeHeight) / 2;
int top = getRowContentHeight() + verticalPadding + mInsetTop;
int bottom = top + mMeasuredRangeHeight;
mRangeBar.layout(leftPadding, top, mRangeBar.getMeasuredWidth() + leftPadding, bottom);
}
}
/**
* This is called when RowView is being used as a component in a large template.
*/
@Override
public void setSliceItem(SliceContent content, boolean isHeader, int index,
int rowCount, SliceView.OnSliceActionListener observer) {
setSliceActionListener(observer);
boolean isUpdate = false;
if (content != null && mRowContent != null && mRowContent.isValid()) {
// Might be same slice
SliceStructure prevSs = mRowContent != null
? new SliceStructure(mRowContent.getSliceItem()) : null;
SliceStructure newSs = new SliceStructure(content.getSliceItem().getSlice());
boolean sameStructure = prevSs != null && prevSs.equals(newSs);
boolean sameSliceId = prevSs != null
&& prevSs.getUri() != null && prevSs.getUri().equals(newSs.getUri());
isUpdate = sameSliceId && sameStructure;
}
mShowActionSpinner = false;
mIsHeader = isHeader;
mRowContent = (RowContent) content;
mRowIndex = index;
populateViews(isUpdate);
}
private void populateViews(boolean isUpdate) {
boolean skipSliderUpdate = isUpdate && mIsRangeSliding;
if (!skipSliderUpdate) {
resetViewState();
}
if (mRowContent.getLayoutDir() != -1) {
setLayoutDirection(mRowContent.getLayoutDir());
}
if (mRowContent.isDefaultSeeMore()) {
showSeeMore();
return;
}
CharSequence contentDescr = mRowContent.getContentDescription();
if (contentDescr != null) {
mContent.setContentDescription(contentDescr);
}
mStartItem = mRowContent.getStartItem();
boolean showStart = mStartItem != null && (mRowIndex > 0 || mRowContent.hasTitleItems());
if (showStart) {
showStart = addItem(mStartItem, mTintColor, true /* isStart */);
}
mStartContainer.setVisibility(showStart ? View.VISIBLE : View.GONE);
final SliceItem titleItem = mRowContent.getTitleItem();
if (titleItem != null) {
mPrimaryText.setText(titleItem.getSanitizedText());
}
if (mSliceStyle != null) {
mPrimaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mIsHeader
? mSliceStyle.getHeaderTitleSize()
: mSliceStyle.getTitleSize());
mPrimaryText.setTextColor(mSliceStyle.getTitleColor());
}
mPrimaryText.setVisibility(titleItem != null ? View.VISIBLE : View.GONE);
addSubtitle(titleItem != null /* hasTitle */);
mBottomDivider.setVisibility(mRowContent.hasBottomDivider() ? View.VISIBLE : View.GONE);
SliceItem primaryAction = mRowContent.getPrimaryAction();
if (primaryAction != null && primaryAction != mStartItem) {
mRowAction = new SliceActionImpl(primaryAction);
if (mRowAction.isToggle()) {
// If primary action is a toggle, add it and we're done
addAction(mRowAction, mTintColor, mEndContainer, false /* isStart */);
// TODO: if start item is tappable, touch feedback should exclude it
setViewClickable(mRootView, true);
return;
}
}
final SliceItem range = mRowContent.getRange();
if (range != null) {
if (mRowAction != null) {
setViewClickable(mRootView, true);
}
if (!skipSliderUpdate) {
determineRangeValues(range);
addRange(range);
} else {
// Even if we're skipping the update, we should still update the range item
mRangeItem = range;
}
return;
}
updateEndItems();
updateActionSpinner();
}
private void updateEndItems() {
if (mRowContent == null) {
return;
}
mEndContainer.removeAllViews();
// If we're here we can can show end items; check for top level actions first
List endItems = mRowContent.getEndItems();
if (mHeaderActions != null) {
// Use these if we have them instead
endItems = mHeaderActions;
}
// Add start item to end of row for the top row if end items are empty and presenter
// doesn't show title items.
if (mRowIndex == 0 && mStartItem != null && endItems.isEmpty()
&& !mRowContent.hasTitleItems()) {
endItems.add(mStartItem);
}
// If we're here we might be able to show end items
int endItemCount = 0;
boolean firstItemIsADefaultToggle = false;
boolean singleActionAtTheEnd = false;
SliceItem endAction = null;
for (int i = 0; i < endItems.size(); i++) {
final SliceItem endItem = (endItems.get(i) instanceof SliceItem)
? (SliceItem) endItems.get(i)
: ((SliceActionImpl) endItems.get(i)).getSliceItem();
if (endItemCount < MAX_END_ITEMS) {
if (addItem(endItem, mTintColor, false /* isStart */)) {
if (endAction == null && SliceQuery.find(endItem, FORMAT_ACTION) != null) {
endAction = endItem;
}
endItemCount++;
if (endItemCount == 1) {
firstItemIsADefaultToggle = !mToggles.isEmpty()
&& SliceQuery.find(endItem.getSlice(), FORMAT_IMAGE) == null;
singleActionAtTheEnd = endItems.size() == 1
&& SliceQuery.find(endItem, FORMAT_ACTION) != null;
}
}
}
}
mEndContainer.setVisibility(endItemCount > 0 ? VISIBLE : GONE);
// If there is a row action and the first end item is a default toggle, or action divider
// is set by presenter and a single action is at the end of the row, show the divider.
mActionDivider.setVisibility(mRowAction != null && (firstItemIsADefaultToggle
|| (mRowContent.hasActionDivider() && singleActionAtTheEnd))
? View.VISIBLE : View.GONE);
boolean hasStartAction = mStartItem != null
&& SliceQuery.find(mStartItem, FORMAT_ACTION) != null;
boolean hasEndItemAction = endAction != null;
boolean endAndRowActionTheSame = false;
if (mRowAction != null) {
setViewClickable(mRootView, true);
} else if (hasEndItemAction != hasStartAction && (endItemCount == 1 || hasStartAction)) {
// This row only has 1 action in start or end position; make whole row clickable for it
endAndRowActionTheSame = true;
if (!mToggles.isEmpty()) {
mRowAction = mToggles.keySet().iterator().next();
} else if (!mActions.isEmpty() && mActions.size() == 1) {
mRowAction = mActions.valueAt(0).getAction();
}
setViewClickable(mRootView, true);
}
if (mRowAction != null && !endAndRowActionTheSame
&& mLoadingActions.contains(mRowAction.getSliceItem())) {
mShowActionSpinner = true;
}
}
@Override
public void setLastUpdated(long lastUpdated) {
super.setLastUpdated(lastUpdated);
if (mRowContent != null) {
addSubtitle(mRowContent.getTitleItem() != null
&& TextUtils.isEmpty(mRowContent.getTitleItem().getSanitizedText()));
}
}
private void addSubtitle(boolean hasTitle) {
if (mRowContent == null) {
return;
}
final SliceItem subtitleItem = getMode() == MODE_SMALL
? mRowContent.getSummaryItem()
: mRowContent.getSubtitleItem();
CharSequence subtitleTimeString = null;
if (mShowLastUpdated && mLastUpdated != -1) {
CharSequence relativeTime = getRelativeTimeString(mLastUpdated);
if (relativeTime != null) {
subtitleTimeString =
getResources().getString(R.string.abc_slice_updated, relativeTime);
}
}
CharSequence subtitle = subtitleItem != null ? subtitleItem.getSanitizedText() : null;
boolean subtitleExists = !TextUtils.isEmpty(subtitle)
|| (subtitleItem != null && subtitleItem.hasHint(HINT_PARTIAL));
if (subtitleExists) {
mSecondaryText.setText(subtitle);
if (mSliceStyle != null) {
mSecondaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mIsHeader
? mSliceStyle.getHeaderSubtitleSize()
: mSliceStyle.getSubtitleSize());
mSecondaryText.setTextColor(mSliceStyle.getSubtitleColor());
int verticalPadding = mIsHeader
? mSliceStyle.getVerticalHeaderTextPadding()
: mSliceStyle.getVerticalTextPadding();
mSecondaryText.setPadding(0, verticalPadding, 0, 0);
}
}
if (subtitleTimeString != null) {
if (!TextUtils.isEmpty(subtitle)) {
subtitleTimeString = " \u00B7 " + subtitleTimeString;
}
SpannableString sp = new SpannableString(subtitleTimeString);
sp.setSpan(new StyleSpan(Typeface.ITALIC), 0, subtitleTimeString.length(), 0);
mLastUpdatedText.setText(sp);
if (mSliceStyle != null) {
mLastUpdatedText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mIsHeader
? mSliceStyle.getHeaderSubtitleSize() : mSliceStyle.getSubtitleSize());
mLastUpdatedText.setTextColor(mSliceStyle.getSubtitleColor());
}
}
mLastUpdatedText.setVisibility(TextUtils.isEmpty(subtitleTimeString) ? GONE : VISIBLE);
mSecondaryText.setVisibility(subtitleExists ? VISIBLE : GONE);
// If this is non-header or something that can have 2 lines in the header (e.g. permission
// slice) then allow 2 lines if there's only a subtitle and now timestring.
boolean canHaveMultiLines = mRowIndex > 0 || mAllowTwoLines;
int maxLines = canHaveMultiLines && !hasTitle && subtitleExists
&& TextUtils.isEmpty(subtitleTimeString)
? 2 : 1;
mSecondaryText.setSingleLine(maxLines == 1);
mSecondaryText.setMaxLines(maxLines);
// TODO: Consider refactoring layout structure to avoid this
// Need to request a layout to update the weights for these views when RV recycles them
mSecondaryText.requestLayout();
mLastUpdatedText.requestLayout();
}
private CharSequence getRelativeTimeString(long time) {
long difference = System.currentTimeMillis() - time;
if (difference > DateUtils.YEAR_IN_MILLIS) {
int years = (int) (difference / DateUtils.YEAR_IN_MILLIS);
return getResources().getQuantityString(
R.plurals.abc_slice_duration_years, years, years);
} else if (difference > DateUtils.DAY_IN_MILLIS) {
int days = (int) (difference / DateUtils.DAY_IN_MILLIS);
return getResources().getQuantityString(R.plurals.abc_slice_duration_days, days, days);
} else if (difference > DateUtils.MINUTE_IN_MILLIS) {
int mins = (int) (difference / DateUtils.MINUTE_IN_MILLIS);
return getResources().getQuantityString(R.plurals.abc_slice_duration_min, mins, mins);
} else {
return null;
}
}
private void determineRangeValues(SliceItem rangeItem) {
if (rangeItem == null) {
mRangeMinValue = 0;
mRangeValue = 0;
return;
}
mRangeItem = rangeItem;
SliceItem min = SliceQuery.findSubtype(mRangeItem, FORMAT_INT, SUBTYPE_MIN);
int minValue = 0;
if (min != null) {
minValue = min.getInt();
}
mRangeMinValue = minValue;
SliceItem progress = SliceQuery.findSubtype(mRangeItem, FORMAT_INT, SUBTYPE_VALUE);
if (progress != null) {
mRangeValue = progress.getInt() - minValue;
}
}
private void addRange(final SliceItem range) {
if (mHandler == null) {
mHandler = new Handler();
}
final boolean isSeekBar = FORMAT_ACTION.equals(range.getFormat());
final ProgressBar progressBar = isSeekBar
? new SeekBar(getContext())
: new ProgressBar(getContext(), null, android.R.attr.progressBarStyleHorizontal);
Drawable progressDrawable = DrawableCompat.wrap(progressBar.getProgressDrawable());
if (mTintColor != -1 && progressDrawable != null) {
DrawableCompat.setTint(progressDrawable, mTintColor);
progressBar.setProgressDrawable(progressDrawable);
}
SliceItem max = SliceQuery.findSubtype(range, FORMAT_INT, SUBTYPE_MAX);
if (max != null) {
progressBar.setMax(max.getInt() - mRangeMinValue);
}
progressBar.setProgress(mRangeValue);
progressBar.setVisibility(View.VISIBLE);
addView(progressBar);
mRangeBar = progressBar;
if (isSeekBar) {
SliceItem thumb = mRowContent.getInputRangeThumb();
SeekBar seekBar = (SeekBar) mRangeBar;
if (thumb != null && thumb.getIcon() != null) {
Drawable d = thumb.getIcon().loadDrawable(getContext());
if (d != null) {
seekBar.setThumb(d);
}
}
Drawable thumbDrawable = DrawableCompat.wrap(seekBar.getThumb());
if (mTintColor != -1 && thumbDrawable != null) {
DrawableCompat.setTint(thumbDrawable, mTintColor);
seekBar.setThumb(thumbDrawable);
}
seekBar.setOnSeekBarChangeListener(mSeekBarChangeListener);
}
}
void sendSliderValue() {
if (mRangeItem != null) {
try {
mLastSentRangeUpdate = System.currentTimeMillis();
mRangeItem.fireAction(getContext(),
new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
.putExtra(EXTRA_RANGE_VALUE, mRangeValue));
if (mObserver != null) {
EventInfo info = new EventInfo(getMode(), ACTION_TYPE_SLIDER, ROW_TYPE_SLIDER,
mRowIndex);
info.state = mRangeValue;
mObserver.onSliceAction(info, mRangeItem);
}
} catch (CanceledException e) {
Log.e(TAG, "PendingIntent for slice cannot be sent", e);
}
}
}
/**
* Add an action view to the container.
*/
private void addAction(final SliceActionImpl actionContent, int color, ViewGroup container,
boolean isStart) {
SliceActionView sav = new SliceActionView(getContext());
container.addView(sav);
if (container.getVisibility() == GONE) {
container.setVisibility(VISIBLE);
}
boolean isToggle = actionContent.isToggle();
int actionType = isToggle ? ACTION_TYPE_TOGGLE : ACTION_TYPE_BUTTON;
int rowType = isToggle ? ROW_TYPE_TOGGLE : ROW_TYPE_LIST;
EventInfo info = new EventInfo(getMode(), actionType, rowType, mRowIndex);
if (isStart) {
info.setPosition(EventInfo.POSITION_START, 0, 1);
}
sav.setAction(actionContent, info, mObserver, color, mLoadingListener);
if (mLoadingActions.contains(actionContent.getSliceItem())) {
sav.setLoading(true);
}
if (isToggle) {
mToggles.put(actionContent, sav);
} else {
mActions.put(actionContent, sav);
}
}
/**
* Adds simple items to a container. Simple items include actions with icons, images, or
* timestamps.
*/
private boolean addItem(SliceItem sliceItem, int color, boolean isStart) {
IconCompat icon = null;
int imageMode = 0;
SliceItem timeStamp = null;
ViewGroup container = isStart ? mStartContainer : mEndContainer;
if (FORMAT_SLICE.equals(sliceItem.getFormat())
|| FORMAT_ACTION.equals(sliceItem.getFormat())) {
if (sliceItem.hasHint(HINT_SHORTCUT)) {
addAction(new SliceActionImpl(sliceItem), color, container, isStart);
return true;
} else {
if (sliceItem.getSlice().getItems().size() == 0) {
return false;
}
sliceItem = sliceItem.getSlice().getItems().get(0);
}
}
if (FORMAT_IMAGE.equals(sliceItem.getFormat())) {
icon = sliceItem.getIcon();
imageMode = sliceItem.hasHint(HINT_NO_TINT) ? SMALL_IMAGE : ICON_IMAGE;
} else if (FORMAT_LONG.equals(sliceItem.getFormat())) {
timeStamp = sliceItem;
}
View addedView = null;
if (icon != null) {
boolean isIcon = imageMode == ICON_IMAGE;
ImageView iv = new ImageView(getContext());
iv.setImageDrawable(icon.loadDrawable(getContext()));
if (isIcon && color != -1) {
iv.setColorFilter(color);
}
container.addView(iv);
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) iv.getLayoutParams();
lp.width = mImageSize;
lp.height = mImageSize;
iv.setLayoutParams(lp);
int p = isIcon ? mIconSize / 2 : 0;
iv.setPadding(p, p, p, p);
addedView = iv;
} else if (timeStamp != null) {
TextView tv = new TextView(getContext());
tv.setText(SliceViewUtil.getTimestampString(getContext(), sliceItem.getLong()));
if (mSliceStyle != null) {
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, mSliceStyle.getSubtitleSize());
tv.setTextColor(mSliceStyle.getSubtitleColor());
}
container.addView(tv);
addedView = tv;
}
return addedView != null;
}
private void showSeeMore() {
final Button b = (Button) LayoutInflater.from(getContext()).inflate(
R.layout.abc_slice_row_show_more, this, false);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
if (mObserver != null) {
EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_SEE_MORE,
EventInfo.ROW_TYPE_LIST, mRowIndex);
mObserver.onSliceAction(info, mRowContent.getSliceItem());
}
mShowActionSpinner =
mRowContent.getSliceItem().fireActionInternal(getContext(), null);
if (mShowActionSpinner) {
if (mLoadingListener != null) {
mLoadingListener.onSliceActionLoading(mRowContent.getSliceItem(),
mRowIndex);
}
mLoadingActions.add(mRowContent.getSliceItem());
b.setVisibility(GONE);
}
updateActionSpinner();
} catch (CanceledException e) {
Log.e(TAG, "PendingIntent for slice cannot be sent", e);
}
}
});
if (mTintColor != -1) {
b.setTextColor(mTintColor);
}
mSeeMoreView = b;
mRootView.addView(mSeeMoreView);
if (mLoadingActions.contains(mRowContent.getSliceItem())) {
mShowActionSpinner = true;
b.setVisibility(GONE);
updateActionSpinner();
}
}
void updateActionSpinner() {
mActionSpinner.setVisibility(mShowActionSpinner ? VISIBLE : GONE);
}
@Override
public void setLoadingActions(Set<SliceItem> actions) {
if (actions == null) {
mLoadingActions.clear();
mShowActionSpinner = false;
} else {
mLoadingActions = actions;
}
updateEndItems();
updateActionSpinner();
}
@Override
public void onClick(View view) {
if (mRowAction == null || mRowAction.getActionItem() == null) {
return;
}
SliceActionView sav = mRowAction.isToggle()
? mToggles.get(mRowAction)
: mActions.get(mRowAction);
if (sav != null && !(view instanceof SliceActionView)) {
// Row might have a single action item set on it, in that case we activate that item
// and it will handle displaying any loading states / updating state for toggles
sav.sendAction();
} else {
if (mRowIndex == 0) {
// Header clicks are a little weird and SliceView needs to know about them to
// maintain loading state; this is hooked up in SliceAdapter -- it will call
// through to SliceView parent which has the info to perform the click.
performClick();
} else {
try {
mShowActionSpinner =
mRowAction.getActionItem().fireActionInternal(getContext(), null);
if (mShowActionSpinner && mLoadingListener != null) {
mLoadingListener.onSliceActionLoading(mRowAction.getSliceItem(), mRowIndex);
mLoadingActions.add(mRowAction.getSliceItem());
}
updateActionSpinner();
} catch (CanceledException e) {
Log.e(TAG, "PendingIntent for slice cannot be sent", e);
}
}
}
}
private void setViewClickable(View layout, boolean isClickable) {
layout.setOnClickListener(isClickable ? this : null);
layout.setBackground(isClickable
? SliceViewUtil.getDrawable(getContext(), android.R.attr.selectableItemBackground)
: null);
layout.setClickable(isClickable);
}
@Override
public void resetView() {
mRowContent = null;
mLoadingActions.clear();
resetViewState();
}
private void resetViewState() {
mRootView.setVisibility(VISIBLE);
setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT);
setViewClickable(mRootView, false);
setViewClickable(mContent, false);
mStartContainer.removeAllViews();
mEndContainer.removeAllViews();
mEndContainer.setVisibility(GONE);
mPrimaryText.setText(null);
mSecondaryText.setText(null);
mLastUpdatedText.setText(null);
mLastUpdatedText.setVisibility(GONE);
mToggles.clear();
mActions.clear();
mRowAction = null;
mStartItem = null;
mBottomDivider.setVisibility(GONE);
mActionDivider.setVisibility(GONE);
if (mSeeMoreView != null) {
mRootView.removeView(mSeeMoreView);
mSeeMoreView = null;
}
mIsRangeSliding = false;
mRangeHasPendingUpdate = false;
mRangeItem = null;
mRangeMinValue = 0;
mRangeValue = 0;
mLastSentRangeUpdate = 0;
mHandler = null;
if (mRangeBar != null) {
removeView(mRangeBar);
mRangeBar = null;
}
mActionSpinner.setVisibility(GONE);
}
Runnable mRangeUpdater = new Runnable() {
@Override
public void run() {
sendSliderValue();
mRangeUpdaterRunning = false;
}
};
private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener =
new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
mRangeValue = progress + mRangeMinValue;
final long now = System.currentTimeMillis();
if (mLastSentRangeUpdate != 0 && now - mLastSentRangeUpdate > SLIDER_INTERVAL) {
mRangeUpdaterRunning = false;
mHandler.removeCallbacks(mRangeUpdater);
sendSliderValue();
} else if (!mRangeUpdaterRunning) {
mRangeUpdaterRunning = true;
mHandler.postDelayed(mRangeUpdater, SLIDER_INTERVAL);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
mIsRangeSliding = true;
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
mIsRangeSliding = false;
if (mRangeUpdaterRunning || mRangeHasPendingUpdate) {
mRangeUpdaterRunning = false;
mRangeHasPendingUpdate = false;
mHandler.removeCallbacks(mRangeUpdater);
mRangeValue = seekBar.getProgress() + mRangeMinValue;
sendSliderValue();
}
}
};
}