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