Fix crash when row has only has image at start or end
* Title items weren’t being filtered correctly
* Images incorrectly treated like wrapped actions when they’re not
* Fixing the crash revealed other issues around start items / toggles
and this CL fixes up some things around that + touch targets
Test: ./gradlew slice-view:connectedCheck
manual: tap on row slices with actions
- toggle, toggle2, wifi, ride, contact
Fixes: 75993180
Change-Id: Ice0988737e55369f75f27f83af4e53f478a4730e
diff --git a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
index 07d6ccc..965ea77 100644
--- a/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
+++ b/samples/SupportSliceDemos/src/main/java/com/example/androidx/slice/demos/SampleSliceProvider.java
@@ -66,8 +66,8 @@
"com.example.androidx.slice.action.TOAST_RANGE_VALUE";
public static final String[] URI_PATHS = {"message", "wifi", "note", "ride", "toggle",
- "toggle2", "contact", "gallery", "weather", "reservation", "loadlist", "loadgrid",
- "inputrange", "range", "contact2", "subscription"};
+ "toggle2", "toggletester", "contact", "gallery", "weather", "reservation", "loadlist",
+ "loadgrid", "inputrange", "range", "contact2", "subscription", "singleitems"};
/**
* @return Uri with the provided path
@@ -116,6 +116,8 @@
return createCustomToggleSlice(sliceUri);
case "/toggle2":
return createTwoCustomToggleSlices(sliceUri);
+ case "/toggletester":
+ return createdToggleTesterSlice(sliceUri);
case "/contact":
return createContact(sliceUri);
case "/contact2":
@@ -136,6 +138,8 @@
return createDownloadProgressRange(sliceUri);
case "/subscription":
return createCatSlice(sliceUri, false /* customSeeMore */);
+ case "/singleitems":
+ return createSingleSlice(sliceUri);
}
throw new IllegalArgumentException("Unknown uri " + sliceUri);
}
@@ -600,6 +604,116 @@
.build();
}
+ private Slice createdToggleTesterSlice(Uri uri) {
+ IconCompat star = IconCompat.createWithResource(getContext(), R.drawable.toggle_star);
+ IconCompat icon = IconCompat.createWithResource(getContext(), R.drawable.ic_star_on);
+
+ SliceAction primaryAction = new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "primary action"), icon, "Primary action");
+ SliceAction toggleAction = new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "star note"), star, "Star note", false);
+ SliceAction toggleAction2 = new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "star note 2"), star, "Star note 2", true);
+ SliceAction toggleAction3 = new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "star note 3"), star, "Star note 3", false);
+
+ ListBuilder lb = new ListBuilder(getContext(), uri, INFINITY);
+
+ // Primary action toggle
+ ListBuilder.RowBuilder primaryToggle = new ListBuilder.RowBuilder(lb);
+ primaryToggle.setTitle("Primary action is a toggle")
+ .setPrimaryAction(toggleAction);
+
+ // End toggle + normal primary action
+ ListBuilder.RowBuilder endToggle = new ListBuilder.RowBuilder(lb);
+ endToggle.setTitle("Only end toggles")
+ .setSubtitle("Normal primary action")
+ .setPrimaryAction(primaryAction)
+ .addEndItem(toggleAction)
+ .addEndItem(toggleAction2);
+
+ // Start toggle + normal primary
+ ListBuilder.RowBuilder startToggle = new ListBuilder.RowBuilder(lb);
+ startToggle.setTitle("One start toggle")
+ .setTitleItem(toggleAction)
+ .setSubtitle("Normal primary action")
+ .setPrimaryAction(primaryAction);
+
+ // Start + end toggles + normal primary action
+ ListBuilder.RowBuilder someToggles = new ListBuilder.RowBuilder(lb);
+ someToggles.setTitleItem(toggleAction)
+ .setPrimaryAction(primaryAction)
+ .setTitle("Start & end toggles")
+ .setSubtitle("Normal primary action")
+ .addEndItem(toggleAction2)
+ .addEndItem(toggleAction3);
+
+ // Start toggle ONLY
+ ListBuilder.RowBuilder startToggleOnly = new ListBuilder.RowBuilder(lb);
+ startToggleOnly.setTitle("Start action is a toggle")
+ .setSubtitle("No other actions")
+ .setTitleItem(toggleAction);
+
+ // End toggle ONLY
+ ListBuilder.RowBuilder endToggleOnly = new ListBuilder.RowBuilder(lb);
+ endToggleOnly.setTitle("End action is a toggle")
+ .setSubtitle("No other actions")
+ .addEndItem(toggleAction);
+
+ // All toggles: end item should be ignored / replaced with primary action
+ ListBuilder.RowBuilder muchToggles = new ListBuilder.RowBuilder(lb);
+ muchToggles.setTitleItem(toggleAction)
+ .setTitle("All toggles")
+ .setSubtitle("Even the primary action")
+ .setPrimaryAction(toggleAction2)
+ .addEndItem(toggleAction3);
+
+ lb.addRow(primaryToggle);
+ lb.addRow(endToggleOnly);
+ lb.addRow(endToggle);
+ lb.addRow(startToggleOnly);
+ lb.addRow(startToggle);
+ lb.addRow(someToggles);
+ lb.addRow(muchToggles);
+ return lb.build();
+ }
+
+ private Slice createSingleSlice(Uri uri) {
+ IconCompat ic2 = IconCompat.createWithResource(getContext(), R.drawable.ic_create);
+ IconCompat image = IconCompat.createWithResource(getContext(), R.drawable.cat_3);
+ IconCompat toggle = IconCompat.createWithResource(getContext(), R.drawable.toggle_star);
+ SliceAction toggleAction = new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "toggle action"), toggle, "toggle", false);
+ SliceAction simpleAction = new SliceAction(
+ getBroadcastIntent(ACTION_TOAST, "icon action"), ic2, "icon");
+ ListBuilder lb = new ListBuilder(getContext(), uri, INFINITY);
+ return lb.addRow(new ListBuilder.RowBuilder(lb)
+ .setTitle("Single title"))
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .setSubtitle("Single subtitle"))
+ //Time stamps
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .setTitleItem(System.currentTimeMillis()))
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .addEndItem(System.currentTimeMillis()))
+ // Toggle actions
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .setTitleItem(toggleAction))
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .addEndItem(toggleAction))
+ // Icon actions
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .setTitleItem(simpleAction))
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .addEndItem(simpleAction))
+ // Images
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .setTitleItem(image, SMALL_IMAGE))
+ .addRow(new ListBuilder.RowBuilder(lb)
+ .addEndItem(image, SMALL_IMAGE))
+ .build();
+ }
+
private Handler mHandler = new Handler();
private SparseArray<String> mListSummaries = new SparseArray<>();
private long mListLastUpdate;
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowContent.java b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
index 0c8d4a6..e71afbd 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
@@ -16,7 +16,6 @@
package androidx.slice.widget;
-import static android.app.slice.Slice.HINT_ACTIONS;
import static android.app.slice.Slice.HINT_PARTIAL;
import static android.app.slice.Slice.HINT_SEE_MORE;
import static android.app.slice.Slice.HINT_SHORTCUT;
@@ -95,15 +94,7 @@
Log.w(TAG, "Provided SliceItem is invalid for RowContent");
return false;
}
- // Find primary action first (otherwise filtered out of valid row items)
- String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE};
- mPrimaryAction = SliceQuery.find(rowSlice, FORMAT_SLICE, hints,
- new String[] { HINT_ACTIONS, HINT_KEYWORDS} /* nonHints */);
-
- if (mPrimaryAction == null && FORMAT_ACTION.equals(rowSlice.getFormat())
- && rowSlice.getSlice().getItems().size() == 1) {
- mPrimaryAction = rowSlice;
- }
+ determineStartAndPrimaryAction(rowSlice);
mContentDescr = SliceQuery.findSubtype(rowSlice, FORMAT_TEXT, SUBTYPE_CONTENT_DESCRIPTION);
@@ -112,7 +103,7 @@
// If we've only got one item that's a slice / action use those items instead
if (rowItems.size() == 1 && (FORMAT_ACTION.equals(rowItems.get(0).getFormat())
|| FORMAT_SLICE.equals(rowItems.get(0).getFormat()))
- && !rowItems.get(0).hasHint(HINT_SHORTCUT)) {
+ && !rowItems.get(0).hasAnyHints(HINT_SHORTCUT, HINT_TITLE)) {
if (isValidRow(rowItems.get(0))) {
rowSlice = rowItems.get(0);
rowItems = filterInvalidItems(rowSlice);
@@ -122,14 +113,12 @@
mRange = rowSlice;
}
if (rowItems.size() > 0) {
- // Start item
- SliceItem firstItem = rowItems.get(0);
- if (FORMAT_SLICE.equals(firstItem.getFormat())) {
- SliceItem unwrappedItem = firstItem.getSlice().getItems().get(0);
- if (isStartType(unwrappedItem)) {
- mStartItem = unwrappedItem;
- rowItems.remove(0);
- }
+ // Remove the things we already know about
+ if (mStartItem != null) {
+ rowItems.remove(mStartItem);
+ }
+ if (mPrimaryAction != null) {
+ rowItems.remove(mPrimaryAction);
}
// Text + end items
@@ -186,6 +175,37 @@
}
/**
+ * Sets the {@link #getPrimaryAction()} and {@link #getStartItem()} for this row.
+ */
+ private void determineStartAndPrimaryAction(@NonNull SliceItem rowSlice) {
+ List<SliceItem> possibleStartItems = SliceQuery.findAll(rowSlice, null, HINT_TITLE, null);
+ if (possibleStartItems.size() > 0) {
+ // The start item will be at position 0 if it exists
+ String format = possibleStartItems.get(0).getFormat();
+ if ((FORMAT_ACTION.equals(format)
+ && SliceQuery.find(possibleStartItems.get(0), FORMAT_IMAGE) != null)
+ || FORMAT_SLICE.equals(format)
+ || FORMAT_LONG.equals(format)
+ || FORMAT_IMAGE.equals(format)) {
+ mStartItem = possibleStartItems.get(0);
+ }
+ }
+
+ String[] hints = new String[] {HINT_SHORTCUT, HINT_TITLE};
+ List<SliceItem> possiblePrimaries = SliceQuery.findAll(rowSlice, FORMAT_SLICE, hints, null);
+ if (possiblePrimaries.isEmpty() && FORMAT_ACTION.equals(rowSlice.getFormat())
+ && rowSlice.getSlice().getItems().size() == 1) {
+ mPrimaryAction = rowSlice;
+ } else if (mStartItem != null && possiblePrimaries.size() > 1
+ && possiblePrimaries.get(0) == mStartItem) {
+ // Next item is the primary action
+ mPrimaryAction = possiblePrimaries.get(1);
+ } else if (possiblePrimaries.size() > 0) {
+ mPrimaryAction = possiblePrimaries.get(0);
+ }
+ }
+
+ /**
* @return the {@link SliceItem} used to populate this row.
*/
@NonNull
@@ -321,6 +341,7 @@
*/
public boolean isValid() {
return mStartItem != null
+ || mPrimaryAction != null
|| mTitleItem != null
|| mSubtitleItem != null
|| mEndItems.size() > 0
@@ -338,17 +359,17 @@
// Must be slice or action
if (FORMAT_SLICE.equals(rowSlice.getFormat())
|| FORMAT_ACTION.equals(rowSlice.getFormat())) {
- // Must have at least one legitimate child
List<SliceItem> rowItems = rowSlice.getSlice().getItems();
+ // Special case: default see more just has an action but no other items
+ if (rowSlice.hasHint(HINT_SEE_MORE) && rowItems.isEmpty()) {
+ return true;
+ }
+ // Must have at least one legitimate child
for (int i = 0; i < rowItems.size(); i++) {
if (isValidRowContent(rowSlice, rowItems.get(i))) {
return true;
}
}
- // Special case: default see more just has an action but no other items
- if (rowSlice.hasHint(HINT_SEE_MORE) && rowItems.isEmpty()) {
- return true;
- }
}
return false;
}
@@ -368,39 +389,20 @@
}
/**
- * @return whether this item is valid content to display in a row.
+ * @return whether this item is valid content to visibly appear in a row.
*/
private static boolean isValidRowContent(SliceItem slice, SliceItem item) {
- if (item.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED)) {
+ if (item.hasAnyHints(HINT_KEYWORDS, HINT_TTL, HINT_LAST_UPDATED)
+ || SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
return false;
}
- if (FORMAT_SLICE.equals(item.getFormat()) && !item.hasHint(HINT_SHORTCUT)) {
- // Unpack contents of slice
- item = item.getSlice().getItems().get(0);
- }
final String itemFormat = item.getFormat();
- return (FORMAT_TEXT.equals(itemFormat)
- && !SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType()))
- || FORMAT_IMAGE.equals(itemFormat)
+ return FORMAT_IMAGE.equals(itemFormat)
+ || FORMAT_TEXT.equals(itemFormat)
|| FORMAT_LONG.equals(itemFormat)
- || FORMAT_REMOTE_INPUT.equals(itemFormat)
- || (FORMAT_SLICE.equals(itemFormat) && item.hasHint(HINT_TITLE)
- && !item.hasHint(HINT_SHORTCUT))
- || (FORMAT_SLICE.equals(itemFormat) && item.hasHint(HINT_SHORTCUT)
- && !item.hasHint(HINT_TITLE))
|| FORMAT_ACTION.equals(itemFormat)
+ || FORMAT_REMOTE_INPUT.equals(itemFormat)
+ || FORMAT_SLICE.equals(itemFormat)
|| (FORMAT_INT.equals(itemFormat) && SUBTYPE_RANGE.equals(slice.getSubType()));
}
-
- /**
- * @return Whether this item is appropriate to be considered a "start" item, i.e. go in the
- * front slot of a row.
- */
- private static boolean isStartType(SliceItem item) {
- final String type = item.getFormat();
- return (FORMAT_ACTION.equals(type) && (SliceQuery.find(item, FORMAT_IMAGE) != null))
- || FORMAT_IMAGE.equals(type)
- || (FORMAT_LONG.equals(type)
- && !item.hasAnyHints(HINT_TTL, HINT_LAST_UPDATED));
- }
}
diff --git a/slices/view/src/main/java/androidx/slice/widget/RowView.java b/slices/view/src/main/java/androidx/slice/widget/RowView.java
index 672f7a3..015457f 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowView.java
@@ -21,7 +21,6 @@
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_TOGGLE;
import static android.app.slice.Slice.SUBTYPE_VALUE;
import static android.app.slice.SliceItem.FORMAT_ACTION;
import static android.app.slice.SliceItem.FORMAT_IMAGE;
@@ -46,6 +45,7 @@
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.StyleSpan;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
@@ -68,7 +68,6 @@
import androidx.slice.core.SliceQuery;
import androidx.slice.view.R;
-import java.util.ArrayList;
import java.util.List;
/**
@@ -92,7 +91,7 @@
private TextView mSecondaryText;
private TextView mLastUpdatedText;
private View mDivider;
- private ArrayList<SliceActionView> mToggles = new ArrayList<>();
+ private ArrayMap<SliceActionImpl, SliceActionView> mToggles = new ArrayMap<>();
private LinearLayout mEndContainer;
private ProgressBar mRangeBar;
private View mSeeMoreView;
@@ -269,11 +268,12 @@
addSubtitle(subtitleItem);
SliceItem primaryAction = mRowContent.getPrimaryAction();
- if (primaryAction != null) {
+ if (primaryAction != null && primaryAction != startItem) {
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;
}
@@ -294,12 +294,6 @@
// Use these if we have them instead
endItems = mHeaderActions;
}
- boolean hasRowAction = mRowAction != null;
- if (endItems.isEmpty()) {
- if (hasRowAction) setViewClickable(mRootView, true);
- return;
- }
-
// If we're here we might be able to show end items
int itemCount = 0;
boolean firstItemIsADefaultToggle = false;
@@ -308,7 +302,7 @@
final SliceItem endItem = endItems.get(i);
if (itemCount < MAX_END_ITEMS) {
if (addItem(endItem, mTintColor, false /* isStart */)) {
- if (FORMAT_ACTION.equals(endItem.getFormat())) {
+ if (SliceQuery.find(endItem, FORMAT_ACTION) != null) {
hasEndItemAction = true;
}
itemCount++;
@@ -321,19 +315,21 @@
}
// If there is a row action and the first end item is a default toggle, show the divider.
- mDivider.setVisibility(hasRowAction && firstItemIsADefaultToggle
+ mDivider.setVisibility(mRowAction != null && firstItemIsADefaultToggle
? View.VISIBLE : View.GONE);
- if (hasRowAction) {
- if (itemCount > 0 && hasEndItemAction) {
- setViewClickable(mContent, true);
+ boolean hasStartAction = startItem != null
+ && SliceQuery.find(startItem, FORMAT_ACTION) != null;
+
+ if (mRowAction != null) {
+ // If there are outside actions make only the content bit clickable
+ // TODO: if start item is an image touch feedback should include it
+ setViewClickable((hasEndItemAction || hasStartAction) ? mContent : mRootView, true);
+ } else if (hasEndItemAction != hasStartAction && (itemCount == 1 || hasStartAction)) {
+ // Single action; make whole row clickable for it
+ if (!mToggles.isEmpty()) {
+ mRowAction = mToggles.keySet().iterator().next();
} else {
- setViewClickable(mRootView, true);
- }
- } else if (mRowContent.endItemsContainAction() && itemCount == 1) {
- // If the only end item is an action, make the whole row clickable.
- SliceItem unwrappedActionItem = endItems.get(0).getSlice().getItems().get(0);
- if (!SUBTYPE_TOGGLE.equals(unwrappedActionItem.getSubType())) {
- mRowAction = new SliceActionImpl(endItems.get(0));
+ mRowAction = new SliceActionImpl(hasEndItemAction ? endItems.get(0) : startItem);
}
setViewClickable(mRootView, true);
}
@@ -445,9 +441,8 @@
info.setPosition(EventInfo.POSITION_START, 0, 1);
}
sav.setAction(actionContent, info, mObserver, color);
-
if (isToggle) {
- mToggles.add(sav);
+ mToggles.put(actionContent, sav);
}
}
@@ -460,7 +455,8 @@
int imageMode = 0;
SliceItem timeStamp = null;
ViewGroup container = isStart ? mStartContainer : mEndContainer;
- if (FORMAT_SLICE.equals(sliceItem.getFormat())) {
+ 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;
@@ -529,21 +525,25 @@
@Override
public void onClick(View view) {
- if (mRowAction != null && mRowAction.getActionItem() != null && !mRowAction.isToggle()) {
- // Check for a row action
- try {
- mRowAction.getActionItem().fireAction(null, null);
- if (mObserver != null) {
- EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_CONTENT,
- EventInfo.ROW_TYPE_LIST, mRowIndex);
- mObserver.onSliceAction(info, mRowAction.getSliceItem());
+ if (mRowAction != null && mRowAction.getActionItem() != null) {
+ // Check if it's a row click for a toggle, in this case need to update the UI
+ if (mRowAction.isToggle() && !(view instanceof SliceActionView)) {
+ SliceActionView sav = mToggles.get(mRowAction);
+ if (sav != null) {
+ sav.toggle();
}
- } catch (CanceledException e) {
- Log.e(TAG, "PendingIntent for slice cannot be sent", e);
+ } else {
+ try {
+ mRowAction.getActionItem().fireAction(null, null);
+ if (mObserver != null) {
+ EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_CONTENT,
+ EventInfo.ROW_TYPE_LIST, mRowIndex);
+ mObserver.onSliceAction(info, mRowAction.getSliceItem());
+ }
+ } catch (CanceledException e) {
+ Log.e(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();
}
}