Apply content descriptions to templates

* Adds content description to GridContent / RowContent
* Updates views to apply a content description
   - GridRowView: on the whole row, or per cell
   - RowView: on the whole row
   - Also applies description to any action buttons
* Updates samples to add some content descriptions, also
  fixes a couple of things in samples

Test: manual — test slices with talkback on in sample app
Bug: 74212452
Change-Id: Ifb5d8ddbabdeba1e384b7b717781d2e04431fd5b
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 1937530..a1bd996 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
@@ -31,13 +31,13 @@
 import android.net.wifi.WifiManager;
 import android.os.Handler;
 import android.provider.Settings;
-import androidx.annotation.NonNull;
 import android.text.SpannableString;
 import android.text.format.DateUtils;
 import android.text.style.ForegroundColorSpan;
 
 import java.util.Calendar;
 
+import androidx.annotation.NonNull;
 import androidx.slice.Slice;
 import androidx.slice.SliceProvider;
 import androidx.slice.builders.GridBuilder;
@@ -203,7 +203,8 @@
                     .addCell(cb -> cb
                         .addImage(Icon.createWithResource(getContext(), R.drawable.slices_4),
                                 LARGE_IMAGE))
-                    .addSeeMoreAction(getBroadcastIntent(ACTION_TOAST, "see your gallery")))
+                    .addSeeMoreAction(getBroadcastIntent(ACTION_TOAST, "see your gallery"))
+                    .setContentDescription("Images from your trip to Hawaii"))
                 .build();
     }
 
@@ -255,7 +256,8 @@
                 .addRow(rb
                         .setTitle("Mady Pitza")
                         .setSubtitle("Frequently contacted contact")
-                        .addEndItem(Icon.createWithResource(getContext(), R.drawable.mady)))
+                        .addEndItem(Icon.createWithResource(getContext(), R.drawable.mady),
+                                SMALL_IMAGE))
                 .addGrid(gb
                         .addCell(new GridBuilder.CellBuilder(gb)
                                 .addImage(Icon.createWithResource(getContext(), R.drawable.ic_call),
@@ -295,12 +297,14 @@
                         .setSummarySubtitle("Called " + lastCalledString)
                         .setPrimaryAction(primaryAction))
                 .addRow(b -> b
-                        .setTitleItem(Icon.createWithResource(getContext(), R.drawable.ic_call))
+                        .setTitleItem(Icon.createWithResource(getContext(), R.drawable.ic_call),
+                                ICON_IMAGE)
                         .setTitle("314-259-2653")
                         .setSubtitle("Call lasted 1 hr 17 min")
                         .addEndItem(lastCalled))
                 .addRow(b -> b
-                        .setTitleItem(Icon.createWithResource(getContext(), R.drawable.ic_text))
+                        .setTitleItem(Icon.createWithResource(getContext(), R.drawable.ic_text),
+                                ICON_IMAGE)
                         .setTitle("You: Coooooool see you then")
                         .addEndItem(System.currentTimeMillis() - 40 * DateUtils.MINUTE_IN_MILLIS))
                 .addAction(new SliceAction(getBroadcastIntent(ACTION_TOAST, "call"),
@@ -367,7 +371,8 @@
                 .addGrid(b -> b
                     .addCell(cb -> cb
                         .addImage(Icon.createWithResource(getContext(), R.drawable.reservation),
-                            LARGE_IMAGE)))
+                            LARGE_IMAGE)
+                        .setContentDescription("Image of your reservation in Seattle")))
                 .addGrid(b -> b
                     .addCell(cb -> cb
                         .addTitleText("Check In")
@@ -467,14 +472,18 @@
         boolean finalWifiEnabled = wifiEnabled;
         SliceAction primaryAction = new SliceAction(getIntent(Settings.ACTION_WIFI_SETTINGS),
                 Icon.createWithResource(getContext(), R.drawable.ic_wifi), "Wi-fi Settings");
+        String toggleCDString = wifiEnabled ? "Turn wifi off" : "Turn wifi on";
+        String sliceCDString = wifiEnabled ? "Wifi connected to " + state
+                : "Wifi disconnected, 10 networks available";
         ListBuilder lb = new ListBuilder(getContext(), sliceUri)
                 .setColor(0xff4285f4)
-                .addRow(b -> b
+                .setHeader(b -> b
                     .setTitle("Wi-fi")
                     .setSubtitle(state)
-                    .addEndItem(new SliceAction(getBroadcastIntent(ACTION_WIFI_CHANGED, null),
-                            "Toggle wifi", finalWifiEnabled))
-                    .setPrimaryAction(primaryAction));
+                    .setContentDescription(sliceCDString)
+                    .setPrimaryAction(primaryAction))
+                .addAction((new SliceAction(getBroadcastIntent(ACTION_WIFI_CHANGED, null),
+                        toggleCDString, finalWifiEnabled)));
 
         // Add fake wifi networks
         int[] wifiIcons = new int[] {R.drawable.ic_wifi_full, R.drawable.ic_wifi_low,
@@ -484,10 +493,14 @@
             Icon icon = Icon.createWithResource(getContext(), iconId);
             final String networkName = "Network" + i;
             ListBuilder.RowBuilder rb = new ListBuilder.RowBuilder(lb);
-            rb.setTitleItem(icon, ICON_IMAGE).setTitle("Network" + networkName);
+            rb.setTitleItem(icon, ICON_IMAGE).setTitle(networkName);
             boolean locked = i % 3 == 0;
             if (locked) {
-                rb.addEndItem(Icon.createWithResource(getContext(), R.drawable.ic_lock));
+                rb.addEndItem(Icon.createWithResource(getContext(), R.drawable.ic_lock),
+                        ICON_IMAGE);
+                rb.setContentDescription("Connect to " + networkName + ", password needed");
+            } else {
+                rb.setContentDescription("Connect to " + networkName);
             }
             String message = locked ? "Open wifi password dialog" : "Connect to " + networkName;
             rb.setPrimaryAction(new SliceAction(getBroadcastIntent(ACTION_TOAST, message), icon,
@@ -499,7 +512,8 @@
         if (TEST_CUSTOM_SEE_MORE) {
             lb.addSeeMoreRow(rb -> rb
                     .setTitle("See all available networks")
-                    .addEndItem(Icon.createWithResource(getContext(), R.drawable.ic_right_caret))
+                    .addEndItem(Icon.createWithResource(getContext(), R.drawable.ic_right_caret),
+                            SMALL_IMAGE)
                     .setPrimaryAction(primaryAction));
         } else {
             lb.addSeeMoreAction(primaryAction.getAction());
@@ -515,7 +529,8 @@
                         .setThumb(Icon.createWithResource(getContext(), R.drawable.ic_star_on))
                         .setAction(getBroadcastIntent(ACTION_TOAST_RANGE_VALUE, null))
                         .setMax(5)
-                        .setValue(3))
+                        .setValue(3)
+                        .setContentDescription("Slider for star ratings"))
                 .build();
     }
 
diff --git a/slices/view/src/main/java/androidx/slice/widget/ActionContent.java b/slices/view/src/main/java/androidx/slice/widget/ActionContent.java
index 2f8a068..aedd068 100644
--- a/slices/view/src/main/java/androidx/slice/widget/ActionContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/ActionContent.java
@@ -31,7 +31,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-
 import androidx.slice.SliceItem;
 import androidx.slice.core.SliceQuery;
 
@@ -115,11 +114,13 @@
     }
 
     /**
-     * @return the subtitle associated with this action.
+     * @return the content description associated with this action.
      */
     @Nullable
-    public SliceItem getContentDescriptionItem() {
-        return mContentDescItem;
+    public CharSequence getContentDescription() {
+        return mContentDescItem != null
+                ? mContentDescItem.getText()
+                : mTitleItem != null ? mTitleItem.getText() : null;
     }
 
     /**
diff --git a/slices/view/src/main/java/androidx/slice/widget/GridContent.java b/slices/view/src/main/java/androidx/slice/widget/GridContent.java
index 3c48bad..e6c1dfe 100644
--- a/slices/view/src/main/java/androidx/slice/widget/GridContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/GridContent.java
@@ -22,6 +22,7 @@
 import static android.app.slice.Slice.HINT_SHORTCUT;
 import static android.app.slice.Slice.HINT_TITLE;
 import static android.app.slice.Slice.SUBTYPE_COLOR;
+import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
@@ -36,13 +37,13 @@
 import android.app.slice.Slice;
 import android.content.Context;
 import android.content.res.Resources;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
 
 import java.util.ArrayList;
 import java.util.List;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 import androidx.slice.SliceItem;
 import androidx.slice.builders.ListBuilder;
 import androidx.slice.core.SliceQuery;
@@ -63,6 +64,7 @@
     private int mMaxCellLineCount;
     private boolean mHasImage;
     private @ListBuilder.ImageMode int mLargestImageMode;
+    private SliceItem mContentDescr;
 
     private int mBigPicMinHeight;
     private int mBigPicMaxHeight;
@@ -83,19 +85,10 @@
         mMaxHeight = res.getDimensionPixelSize(R.dimen.abc_slice_grid_max_height);
     }
 
-    private void reset() {
-        mColorItem = null;
-        mMaxCellLineCount = 0;
-        mHasImage = false;
-        mGridContent.clear();
-        mLargestImageMode = 0;
-    }
-
     /**
      * @return whether this grid has content that is valid to display.
      */
     private boolean populate(SliceItem gridItem) {
-        reset();
         mColorItem = SliceQuery.findSubtype(gridItem, FORMAT_INT, SUBTYPE_COLOR);
         mSeeMoreItem = SliceQuery.find(gridItem, null, HINT_SEE_MORE, null);
         if (mSeeMoreItem != null && FORMAT_SLICE.equals(mSeeMoreItem.getFormat())) {
@@ -107,10 +100,11 @@
         mAllImages = true;
         if (FORMAT_SLICE.equals(gridItem.getFormat())) {
             List<SliceItem> items = gridItem.getSlice().getItems().get(0).getSlice().getItems();
-            items = filterInvalidItems(items);
+            items = filterAndProcessItems(items);
             // Check if it it's only one item that is a slice
             if (items.size() == 1 && items.get(0).getFormat().equals(FORMAT_SLICE)) {
                 items = items.get(0).getSlice().getItems();
+                items = filterAndProcessItems(items);
             }
             for (int i = 0; i < items.size(); i++) {
                 SliceItem item = items.get(i);
@@ -169,6 +163,14 @@
     }
 
     /**
+     * @return content description for this row.
+     */
+    @Nullable
+    public CharSequence getContentDescription() {
+        return mContentDescr != null ? mContentDescr.getText() : null;
+    }
+
+    /**
      * @return whether this grid has content that is valid to display.
      */
     public boolean isValid() {
@@ -182,11 +184,16 @@
         return mAllImages;
     }
 
-    private List<SliceItem> filterInvalidItems(List<SliceItem> items) {
+    /**
+     * Filters non-cell items out of the list of items and finds content description.
+     */
+    private List<SliceItem> filterAndProcessItems(List<SliceItem> items) {
         List<SliceItem> filteredItems = new ArrayList<>();
         for (int i = 0; i < items.size(); i++) {
             SliceItem item = items.get(i);
-            if (item.hasHint(HINT_LIST_ITEM) && !item.hasHint(HINT_SHORTCUT)
+            if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
+                mContentDescr = item;
+            } else if (item.hasHint(HINT_LIST_ITEM) && !item.hasHint(HINT_SHORTCUT)
                     && !item.hasHint(HINT_SEE_MORE)) {
                 filteredItems.add(item);
             }
@@ -247,6 +254,7 @@
     public static class CellContent {
         private SliceItem mContentIntent;
         private ArrayList<SliceItem> mCellItems = new ArrayList<>();
+        private SliceItem mContentDescr;
         private int mTextCount;
         private boolean mHasImage;
         private int mImageMode = -1;
@@ -277,7 +285,9 @@
                 for (int i = 0; i < items.size(); i++) {
                     final SliceItem item = items.get(i);
                     final String itemFormat = item.getFormat();
-                    if (mTextCount < 2 && (FORMAT_TEXT.equals(itemFormat)
+                    if (SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType())) {
+                        mContentDescr = item;
+                    } else if (mTextCount < 2 && (FORMAT_TEXT.equals(itemFormat)
                             || FORMAT_TIMESTAMP.equals(itemFormat))) {
                         mTextCount++;
                         mCellItems.add(item);
@@ -319,9 +329,10 @@
          */
         private boolean isValidCellContent(SliceItem cellItem) {
             final String format = cellItem.getFormat();
-            return FORMAT_TEXT.equals(format)
+            return !SUBTYPE_CONTENT_DESCRIPTION.equals(cellItem.getSubType())
+                    && (FORMAT_TEXT.equals(format)
                     || FORMAT_TIMESTAMP.equals(format)
-                    || FORMAT_IMAGE.equals(format);
+                    || FORMAT_IMAGE.equals(format));
         }
 
         /**
@@ -358,5 +369,10 @@
         public int getImageMode() {
             return mImageMode;
         }
+
+        @Nullable
+        public CharSequence getContentDescription() {
+            return mContentDescr != null ? mContentDescr.getText() : null;
+        }
     }
 }
diff --git a/slices/view/src/main/java/androidx/slice/widget/GridRowView.java b/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
index 2237569..a0f7898 100644
--- a/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/GridRowView.java
@@ -32,8 +32,6 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.res.Resources;
-import androidx.annotation.ColorInt;
-import androidx.annotation.RestrictTo;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Pair;
@@ -52,6 +50,8 @@
 import java.util.Iterator;
 import java.util.List;
 
+import androidx.annotation.ColorInt;
+import androidx.annotation.RestrictTo;
 import androidx.slice.Slice;
 import androidx.slice.SliceItem;
 import androidx.slice.core.SliceQuery;
@@ -164,6 +164,10 @@
             mViewContainer.setTag(tagItem);
             makeClickable(mViewContainer);
         }
+        CharSequence contentDescr = gc.getContentDescription();
+        if (contentDescr != null) {
+            mViewContainer.setContentDescription(contentDescr);
+        }
         ArrayList<GridContent.CellContent> cells = gc.getGridContent();
         boolean hasSeeMore = gc.getSeeMoreItem() != null;
         for (int i = 0; i < cells.size(); i++) {
@@ -276,6 +280,10 @@
             }
         }
         if (added) {
+            CharSequence contentDescr = cell.getContentDescription();
+            if (contentDescr != null) {
+                cellContainer.setContentDescription(contentDescr);
+            }
             mViewContainer.addView(cellContainer,
                     new LinearLayout.LayoutParams(0, WRAP_CONTENT, 1));
             if (index != total - 1) {
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 13dc8fb..d7f7ab6 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowContent.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowContent.java
@@ -21,6 +21,7 @@
 import static android.app.slice.Slice.HINT_SHORTCUT;
 import static android.app.slice.Slice.HINT_SUMMARY;
 import static android.app.slice.Slice.HINT_TITLE;
+import static android.app.slice.Slice.SUBTYPE_CONTENT_DESCRIPTION;
 import static android.app.slice.SliceItem.FORMAT_ACTION;
 import static android.app.slice.SliceItem.FORMAT_IMAGE;
 import static android.app.slice.SliceItem.FORMAT_INT;
@@ -32,15 +33,15 @@
 import static androidx.slice.core.SliceHints.SUBTYPE_RANGE;
 
 import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
 import android.text.TextUtils;
 import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.List;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 import androidx.slice.SliceItem;
 import androidx.slice.core.SliceQuery;
 import androidx.slice.view.R;
@@ -60,8 +61,9 @@
     private SliceItem mSubtitleItem;
     private SliceItem mSummaryItem;
     private ArrayList<SliceItem> mEndItems = new ArrayList<>();
-    private boolean mEndItemsContainAction;
     private SliceItem mRange;
+    private SliceItem mContentDescr;
+    private boolean mEndItemsContainAction;
     private boolean mIsHeader;
     private int mLineCount = 0;
     private int mMaxHeight;
@@ -74,24 +76,9 @@
     }
 
     /**
-     * Resets the content.
-     */
-    public void reset() {
-        mPrimaryAction = null;
-        mRowSlice = null;
-        mStartItem = null;
-        mTitleItem = null;
-        mSubtitleItem = null;
-        mEndItems.clear();
-        mIsHeader = false;
-        mLineCount = 0;
-    }
-
-    /**
      * @return whether this row has content that is valid to display.
      */
     private boolean populate(SliceItem rowSlice, boolean isHeader) {
-        reset();
         mIsHeader = isHeader;
         mRowSlice = rowSlice;
         if (!isValidRow(rowSlice)) {
@@ -103,6 +90,8 @@
         mPrimaryAction = SliceQuery.find(rowSlice, FORMAT_SLICE, hints,
                 new String[] { HINT_ACTIONS } /* nonHints */);
 
+        mContentDescr = SliceQuery.findSubtype(rowSlice, FORMAT_TEXT, SUBTYPE_CONTENT_DESCRIPTION);
+
         // Filter anything not viable for displaying in a row
         ArrayList<SliceItem> rowItems = filterInvalidItems(rowSlice);
         // If we've only got one item that's a slice / action use those items instead
@@ -238,6 +227,14 @@
     }
 
     /**
+     * @return the content description to use for this row.
+     */
+    @Nullable
+    public CharSequence getContentDescription() {
+        return mContentDescr != null ? mContentDescr.getText() : null;
+    }
+
+    /**
      * @return whether {@link #getEndItems()} contains a SliceItem with FORMAT_SLICE, HINT_SHORTCUT
      */
     public boolean endItemsContainAction() {
@@ -339,7 +336,8 @@
             item = item.getSlice().getItems().get(0);
         }
         final String itemFormat = item.getFormat();
-        return FORMAT_TEXT.equals(itemFormat)
+        return (FORMAT_TEXT.equals(itemFormat)
+                && !SUBTYPE_CONTENT_DESCRIPTION.equals(item.getSubType()))
                 || FORMAT_IMAGE.equals(itemFormat)
                 || FORMAT_TIMESTAMP.equals(itemFormat)
                 || FORMAT_REMOTE_INPUT.equals(itemFormat)
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 9478092..13e6128 100644
--- a/slices/view/src/main/java/androidx/slice/widget/RowView.java
+++ b/slices/view/src/main/java/androidx/slice/widget/RowView.java
@@ -36,8 +36,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.drawable.Icon;
-import androidx.annotation.ColorInt;
-import androidx.annotation.RestrictTo;
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.LayoutInflater;
@@ -56,6 +54,8 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import androidx.annotation.ColorInt;
+import androidx.annotation.RestrictTo;
 import androidx.slice.Slice;
 import androidx.slice.SliceItem;
 import androidx.slice.core.SliceQuery;
@@ -183,7 +183,10 @@
             showSeeMore();
             return;
         }
-
+        CharSequence contentDescr = mRowContent.getContentDescription();
+        if (contentDescr != null) {
+            mContent.setContentDescription(contentDescr);
+        }
         boolean showStart = false;
         final SliceItem startItem = mRowContent.getStartItem();
         if (startItem != null) {
@@ -368,6 +371,10 @@
             toggle = new Switch(getContext());
             container.addView(toggle);
         }
+        CharSequence contentDesc = actionContent.getContentDescription();
+        if (contentDesc != null) {
+            toggle.setContentDescription(contentDesc);
+        }
         toggle.setChecked(actionContent.isChecked());
         toggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
             @Override
@@ -434,6 +441,9 @@
                 }
                 size = mIconSize;
             }
+            if (actionContent != null && actionContent.getContentDescription() != null) {
+                iv.setContentDescription(actionContent.getContentDescription());
+            }
             container.addView(iv);
             LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) iv.getLayoutParams();
             lp.width = size;