blob: 417c0692f92938c1ddf41c9ea623f7ec62a37b8a [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.app.slice.widget;
import static android.app.slice.Slice.HINT_NO_TINT;
import static android.app.slice.Slice.HINT_SELECTED;
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_TIMESTAMP;
import static androidx.app.slice.core.SliceHints.EXTRA_SLIDER_VALUE;
import static androidx.app.slice.core.SliceHints.EXTRA_TOGGLE_STATE;
import static androidx.app.slice.core.SliceHints.SUBTYPE_MAX;
import static androidx.app.slice.core.SliceHints.SUBTYPE_PROGRESS;
import static androidx.app.slice.core.SliceHints.SUBTYPE_TOGGLE;
import static androidx.app.slice.widget.SliceView.MODE_LARGE;
import static androidx.app.slice.widget.SliceView.MODE_SMALL;
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.support.annotation.ColorInt;
import android.support.annotation.RestrictTo;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.ToggleButton;
import java.util.ArrayList;
import java.util.List;
import androidx.app.slice.Slice;
import androidx.app.slice.SliceItem;
import androidx.app.slice.core.SliceQuery;
import androidx.app.slice.view.R;
/**
* Row item is in small template format and can be used to construct list items for use
* with {@link LargeTemplateView}.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@TargetApi(23)
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
private static final int MAX_END_ITEMS = 3;
private LinearLayout mStartContainer;
private LinearLayout mContent;
private TextView mPrimaryText;
private TextView mSecondaryText;
private View mDivider;
private ArrayList<CompoundButton> mToggles = new ArrayList<>();
private LinearLayout mEndContainer;
private SeekBar mSeekBar;
private ProgressBar mProgressBar;
private boolean mInSmallMode;
private int mRowIndex;
private RowContent mRowContent;
private SliceItem mRowAction;
private boolean mIsHeader;
private int mIconSize;
private int mPadding;
public RowView(Context context) {
super(context);
mIconSize = getContext().getResources().getDimensionPixelSize(R.dimen.abc_slice_icon_size);
mPadding = getContext().getResources().getDimensionPixelSize(R.dimen.abc_slice_padding);
inflate(context, R.layout.abc_slice_small_template, this);
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);
mDivider = findViewById(R.id.divider);
mEndContainer = (LinearLayout) findViewById(android.R.id.widget_frame);
mSeekBar = (SeekBar) findViewById(R.id.seek_bar);
mProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
}
@Override
public @SliceView.SliceMode int getMode() {
return mInSmallMode ? MODE_SMALL : MODE_LARGE;
}
@Override
public void setTint(@ColorInt int tintColor) {
super.setTint(tintColor);
if (mRowContent != null) {
// TODO -- can be smarter about this
resetView();
populateViews();
}
}
/**
* This is called when RowView is being used as a component in a large template.
*/
@Override
public void setSliceItem(SliceItem slice, boolean isHeader, int index,
SliceView.SliceObserver observer) {
mInSmallMode = false;
setSliceObserver(observer);
mRowIndex = index;
mIsHeader = isHeader;
mRowContent = new RowContent(slice, !mIsHeader /* showStartItem */);
populateViews();
}
/**
* This is called when RowView is being used as a small template.
*/
@Override
public void setSlice(Slice slice) {
mInSmallMode = true;
mRowIndex = 0;
mIsHeader = true;
ListContent lc = new ListContent(slice);
mRowContent = new RowContent(lc.getSummaryItem(), false /* showStartItem */);
populateViews();
}
private void populateViews() {
resetView();
boolean showStart = false;
final SliceItem startItem = mRowContent.getStartItem();
if (startItem != null) {
final EventInfo info = new EventInfo(getMode(),
EventInfo.ACTION_TYPE_BUTTON,
EventInfo.ROW_TYPE_LIST, mRowIndex);
info.setPosition(EventInfo.POSITION_START, 0, 1);
showStart = addItem(startItem, mTintColor, true /* isStart */, 0 /* padding */, info);
}
mStartContainer.setVisibility(showStart ? View.VISIBLE : View.GONE);
final SliceItem titleItem = mRowContent.getTitleItem();
if (titleItem != null) {
mPrimaryText.setText(titleItem.getText());
}
mPrimaryText.setVisibility(titleItem != null ? View.VISIBLE : View.GONE);
final SliceItem subTitle = mRowContent.getSubtitleItem();
if (subTitle != null) {
mSecondaryText.setText(subTitle.getText());
}
mSecondaryText.setVisibility(subTitle != null ? View.VISIBLE : View.GONE);
final SliceItem slider = mRowContent.getSlider();
if (slider != null) {
addSlider(slider);
return;
}
mRowAction = mRowContent.getContentIntent();
ArrayList<SliceItem> endItems = mRowContent.getEndItems();
if (endItems.isEmpty()) {
return;
}
// If we're here we might be able to show end items
int itemCount = 0;
// Prefer to show actions as end items if possible; fall back to the first format type.
String desiredFormat = mRowContent.endItemsContainAction()
? FORMAT_ACTION : endItems.get(0).getFormat();
boolean firstItemIsADefaultToggle = false;
for (int i = 0; i < endItems.size(); i++) {
final SliceItem endItem = endItems.get(i);
final String endFormat = endItem.getFormat();
// Only show one type of format at the end of the slice, use whatever is first
if (itemCount <= MAX_END_ITEMS
&& (desiredFormat.equals(endFormat)
|| FORMAT_TIMESTAMP.equals(endFormat))) {
final EventInfo info = new EventInfo(getMode(),
EventInfo.ACTION_TYPE_BUTTON,
EventInfo.ROW_TYPE_LIST, mRowIndex);
info.setPosition(EventInfo.POSITION_END, i,
Math.min(endItems.size(), MAX_END_ITEMS));
if (addItem(endItem, mTintColor, false /* isStart */, mPadding, info)) {
itemCount++;
if (itemCount == 1) {
firstItemIsADefaultToggle = !mToggles.isEmpty()
&& SliceQuery.find(endItem.getSlice(), FORMAT_IMAGE) == null;
}
}
}
}
boolean hasRowAction = mRowAction != null;
boolean hasEndItemAction = FORMAT_ACTION.contentEquals(desiredFormat);
// If there is a row action and the first end item is a default toggle, show the divider.
mDivider.setVisibility(hasRowAction && firstItemIsADefaultToggle
? View.VISIBLE : View.GONE);
if (hasRowAction) {
if (itemCount > 0 && hasEndItemAction) {
setViewClickable(mContent, true);
} else {
setViewClickable(this, true);
}
} else {
// If the only end item is an action, make the whole row clickable.
if (mRowContent.endItemsContainAction() && itemCount == 1) {
setViewClickable(this, true);
}
}
}
private void addSlider(final SliceItem slider) {
final ProgressBar progressBar;
if (FORMAT_ACTION.equals(slider.getFormat())) {
// Seek bar
progressBar = mSeekBar;
mSeekBar.setVisibility(View.VISIBLE);
SliceItem thumb = SliceQuery.find(slider, FORMAT_IMAGE);
if (thumb != null) {
mSeekBar.setThumb(thumb.getIcon().loadDrawable(getContext()));
}
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
try {
PendingIntent pi = slider.getAction();
Intent i = new Intent().putExtra(EXTRA_SLIDER_VALUE, progress);
// TODO: sending this PendingIntent should be rate limited.
pi.send(getContext(), 0, i, null, null);
} catch (CanceledException e) { }
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
});
} else {
// Progress bar
progressBar = mProgressBar;
mProgressBar.setVisibility(View.VISIBLE);
}
SliceItem max = SliceQuery.findSubtype(slider, FORMAT_INT, SUBTYPE_MAX);
if (max != null) {
progressBar.setMax(max.getInt());
}
SliceItem progress = SliceQuery.findSubtype(slider, FORMAT_INT, SUBTYPE_PROGRESS);
if (progress != null) {
progressBar.setProgress(progress.getInt());
}
}
/**
* Add a toggle view to container.
*/
private void addToggle(final SliceItem toggleItem, int color, ViewGroup container) {
// Check if this is a custom toggle
Icon checkedIcon = null;
List<SliceItem> sliceItems = toggleItem.getSlice().getItems();
if (sliceItems.size() > 0) {
checkedIcon = FORMAT_IMAGE.equals(sliceItems.get(0).getFormat())
? sliceItems.get(0).getIcon()
: null;
}
final CompoundButton toggle;
if (checkedIcon != null) {
if (color != -1) {
// TODO - Should custom toggle buttons be tinted? What if the app wants diff
// colors per state?
checkedIcon.setTint(color);
}
toggle = new ToggleButton(getContext());
((ToggleButton) toggle).setTextOff("");
((ToggleButton) toggle).setTextOn("");
toggle.setBackground(checkedIcon.loadDrawable(getContext()));
container.addView(toggle);
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) toggle.getLayoutParams();
lp.width = mIconSize;
lp.height = mIconSize;
} else {
toggle = new Switch(getContext());
container.addView(toggle);
}
toggle.setChecked(SliceQuery.hasHints(toggleItem.getSlice(), HINT_SELECTED));
toggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
try {
PendingIntent pi = toggleItem.getAction();
Intent i = new Intent().putExtra(EXTRA_TOGGLE_STATE, isChecked);
pi.send(getContext(), 0, i, null, null);
if (mObserver != null) {
final EventInfo info = new EventInfo(getMode(),
EventInfo.ACTION_TYPE_TOGGLE,
EventInfo.ROW_TYPE_LIST, mRowIndex);
info.state = isChecked ? EventInfo.STATE_ON : EventInfo.STATE_OFF;
mObserver.onSliceAction(info, toggleItem);
}
} catch (CanceledException e) {
toggle.setSelected(!isChecked);
}
}
});
mToggles.add(toggle);
}
/**
* Adds simple items to a container. Simple items include actions with icons, images, or
* timestamps.
*/
private boolean addItem(SliceItem sliceItem, int color, boolean isStart, int padding,
final EventInfo info) {
SliceItem image = null;
SliceItem action = null;
SliceItem timeStamp = null;
ViewGroup container = isStart ? mStartContainer : mEndContainer;
if (FORMAT_ACTION.equals(sliceItem.getFormat())) {
if (SliceQuery.hasHints(sliceItem.getSlice(), SUBTYPE_TOGGLE)) {
addToggle(sliceItem, color, container);
return true;
}
image = SliceQuery.find(sliceItem.getSlice(), FORMAT_IMAGE);
timeStamp = SliceQuery.find(sliceItem.getSlice(), FORMAT_TIMESTAMP);
action = sliceItem;
} else if (FORMAT_IMAGE.equals(sliceItem.getFormat())) {
image = sliceItem;
} else if (FORMAT_TIMESTAMP.equals(sliceItem.getFormat())) {
timeStamp = sliceItem;
}
View addedView = null;
if (image != null) {
ImageView iv = new ImageView(getContext());
iv.setImageIcon(image.getIcon());
if (color != -1 && !sliceItem.hasHint(HINT_NO_TINT)) {
iv.setColorFilter(color);
}
container.addView(iv);
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) iv.getLayoutParams();
lp.width = mIconSize;
lp.height = mIconSize;
lp.setMarginStart(padding);
addedView = iv;
} else if (timeStamp != null) {
TextView tv = new TextView(getContext());
tv.setText(SliceViewUtil.getRelativeTimeString(sliceItem.getTimestamp()));
container.addView(tv);
addedView = tv;
}
if (action != null && addedView != null) {
final SliceItem sliceAction = action;
addedView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
sliceAction.getAction().send();
if (mObserver != null) {
mObserver.onSliceAction(info, sliceAction);
}
} catch (CanceledException e) {
e.printStackTrace();
}
}
});
addedView.setBackground(SliceViewUtil.getDrawable(getContext(),
android.R.attr.selectableItemBackground));
}
return addedView != null;
}
@Override
public void onClick(View view) {
if (mRowAction != null && FORMAT_ACTION.equals(mRowAction.getFormat())) {
// Check for a row action
try {
mRowAction.getAction().send();
if (mObserver != null) {
EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_CONTENT,
EventInfo.ROW_TYPE_LIST, mRowIndex);
mObserver.onSliceAction(info, mRowAction);
}
} catch (CanceledException e) {
Log.w(TAG, "PendingIntent for slice cannot be sent", e);
}
} else if (mToggles.size() == 1) {
// If there is only one toggle and no row action, just toggle it.
mToggles.get(0).toggle();
}
}
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() {
setViewClickable(this, false);
setViewClickable(mContent, false);
mStartContainer.removeAllViews();
mEndContainer.removeAllViews();
mPrimaryText.setText(null);
mSecondaryText.setText(null);
mToggles.clear();
mRowAction = null;
mDivider.setVisibility(View.GONE);
mSeekBar.setVisibility(View.GONE);
mProgressBar.setVisibility(View.GONE);
}
}