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;