Android Accessibility at GDG Devfest Brussels 2016

Android Accessibility at GDG Devfest Brussels 2016

'Accessibility for everyone' session at the GDG Brussels 2016 Devfest.

103e1ebcacd620770cf32a36b9aba17e?s=128

AppFoundry

October 23, 2016
Tweet

Transcript

  1. Android accessibility

  2. Accessibility /ˈapps for everybody/ Android apps that are usable by

    everyone. Make no assumptions about the user.
  3. AppFoundry appfoundry.be

  4. None
  5. None
  6. None
  7. Why?

  8. Assumptions Sight See the screen Mobility Touch the screen Audio

    Hear Speech Interact
  9. Accessibility? 2 % visual impairment 8 % men color blind

    0,1 % blind 8 out of 10 with visual impairment or blindness are older than 65
  10. Accessibility? up to 20 % motor impairment Permanent Parkinson’s Tremor

    … Situational Injury … Hands busy
  11. Accessibility services Talkback Screen reader Brailleback Braille display Switch Access

    Control by switch Voice Access Interact
  12. Brailleback

  13. Switch Access

  14. Voice access Voice Assistant High-level voice commands Accessibility Low-level commands

    Device control Scrolling, clicking, text editing
  15. Android - iOS

  16. Improve user experience for accessibility

  17. Color blindness Normal vision Deuteranopia Protanopia Tritanopia

  18. Contrast

  19. Color correction Accessibility options Color simulation Developer options

  20. Color contrast WCAG : background - foreground : 4.5 contrast

    ratio
  21. Android Accessibility features Large Text Magnification Gestures High Contrast Text

    Color inversion Color correction Caption support App developer Text size = sp Layouts = responsive
  22. None
  23. Content description <TextView
 android:id="@+id/day"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="@string/mon"
 android:contentDescription="@string/monday" />

  24. Content description <TextView
 android:id="@+id/day"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="@string/mon"
 android:contentDescription="@string/monday" />

  25. Meaningful image <ImageView
 android:id="@+id/action_mode_close_button"
 android:contentDescription="Done"
 android:focusable="true"
 android:clickable="true"
 app:srcCompat="?attr/actionModeCloseDrawable"
 style="?attr/actionModeCloseButtonStyle"
 android:layout_width="wrap_content"


    android:layout_height="match_parent"/>
  26. Meaningful image <ImageView
 android:id="@+id/action_mode_close_button"
 android:contentDescription="Done"
 android:focusable="true"
 android:clickable="true"
 app:srcCompat="?attr/actionModeCloseDrawable"
 style="?attr/actionModeCloseButtonStyle"
 android:layout_width="wrap_content"


    android:layout_height="match_parent"/>
  27. Decorative image <ImageView
 android:contentDescription=“@null"
 app:srcCompat=“@drawable/decoration“
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"/>

  28. Decorative image <ImageView
 android:contentDescription=“@null"
 app:srcCompat=“@drawable/decoration“
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"/>

  29. Content description <RelativeLayout
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:contentDescription="@string/acc">

  30. Content description <RelativeLayout
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:contentDescription="@string/acc">

  31. States public void setIconAccessibilityText(boolean isFavorite, String teamName) {
 favoriteTeamMenuItem.setTitle(
 isFavorite

    ?
 ”defavoritise {teamName}” :
 ”mark {teamName} as favorite”;
 }
  32. AccessibilityLiveRegion <TextView
 android:id="@+id/evaluation_result"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:accessibilityLiveRegion="polite"/>

  33. AccessibilityLiveRegion <TextView
 android:id="@+id/evaluation_result"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:accessibilityLiveRegion="polite"/>

  34. AnnounceForAccessibility private void makeViewVisible() {
 view.setVisibility(View.VISIBLE);
 view.announceForAccessibility(getString(R.string.acc));
 }

  35. AnnounceForAccessibility private void makeViewVisible() {
 view.setVisibility(View.VISIBLE);
 view.announceForAccessibility(getString(R.string.acc));
 }

  36. AnnounceForAccessibility private void setFavoriteListener(final Team team, final boolean isFavorite) {


    view.setOnClickListener(new View.OnClickListener() {
 
 @Override
 public void onClick(View v) {
 teamListener.onTeamClicked(team);
 String announcement = isFavorite
 ? "Removed " + team.displayTitle() + " from favourites"
 : "Added " + team.displayTitle() + " to favourites";
 view.announceForAccessibility(announcement);
 }
 
 });
 }
  37. AnnounceForAccessibility private void setFavoriteListener(final Team team, final boolean isFavorite) {


    view.setOnClickListener(new View.OnClickListener() {
 
 @Override
 public void onClick(View v) {
 teamListener.onTeamClicked(team);
 String announcement = isFavorite
 ? "Removed " + team.displayTitle() + " from favourites"
 : "Added " + team.displayTitle() + " to favourites";
 view.announceForAccessibility(announcement);
 }
 
 });
 }
  38. Control focus order <LinearLayout
 android:orientation="horizontal">
 
 <EditText
 android:id="@+id/edit"
 android:nextFocusForward="@+id/text" />


    
 <TextView
 android:id="@+id/text"
 android:focusable="true"
 android:text="Hello, I am a focusable TextView" />
 </LinearLayout>
  39. Control focus order <LinearLayout
 android:orientation="horizontal">
 
 <EditText
 android:id="@+id/edit"
 android:nextFocusForward="@+id/text" />


    
 <TextView
 android:id="@+id/text"
 android:focusable="true"
 android:text="Hello, I am a focusable TextView" />
 </LinearLayout>
  40. Filter / change if necessary

  41. Ads ☹ No Ads Accessibility services detected

  42. Talkback detection public boolean isAccessibilityEnabled() { 
 final AccessibilityManager am

    = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 
 boolean isAccessibilityEnabled = am.isEnabled();
 boolean isExploreByTouchEnabled = am.isTouchExplorationEnabled(); 
 return isAccessibilityEnabled && isExploreByTouchEnabled;
 }
  43. Talkback detection public boolean isAccessibilityEnabled() {
 final AccessibilityManager am =

    (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 
 boolean isAccessibilityEnabled = am.isEnabled();
 boolean isExploreByTouchEnabled = am.isTouchExplorationEnabled(); 
 boolean isSpokenFeedbackEnabled = isSpokenFeedbackEnabled(am);
 return isAccessibilityEnabled && isExploreByTouchEnabled && isSpokenFeedbackEnabled;
 } 
 
 /**
 * Check whether 'spoken feedback' services are enabled.
 */
 private boolean isSpokenFeedbackEnabled(final AccessibilityManager am) {
 List<AccessibilityServiceInfo> enabledSpokenFeedbackServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN); 
 return !enabledSpokenFeedbackServices.isEmpty();
 }
  44. Content description good / meaningful texts

  45. Meaningful description private void setAccessibilityText(final TeamInfo teamInfo) {
 String accessibilityText;


    if (teamInfo.coaches().isEmpty()) {
 accessibilityText = getString(R.string.no_trainer_acc, teamInfo.teamName());
 } else {
 List<String> trainerNames = getTrainerNames(teamInfo.coaches());
 
 Joiner joiner = Joiner.on(andStr).skipNulls();
 String concatTrainers = joiner.join(trainerNames);
 accessibilityText = getQuantityString(R.plurals.trainer_acc, teamInfo.coaches().size(), teamInfo.teamName(), concatTrainers);
 }
 
 getItemView().setContentDescription(accessibilityText);
 }
  46. Help from your backend

  47. None
  48. None
  49. Custom views

  50. Custom actions

  51. MatchView public interface MatchListener {
 
 void onMatchClicked(Match match);
 


    void onMatchComment(Match match);
 
 void onMatchShare(Match match);
 
 void onMatchLiked(Match match);
 }
  52. None
  53. Don’t descend into view android:importantForAccessibility="noHideDescendants"

  54. DIY public void showMatch(final Match match, final MatchListener listener) {


    …
 
 setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View view) {
 if (isAccessibilityEnabled) {
 showCustomActions(match);
 } else {
 listener.onMatchClicked(match);
 }
 }
 });
  55. None
  56. None
  57. Accessibility delegate public static class MatchAccessibilityDelegate extends AccessibilityDelegateCompat {
 


    private final Match match;
 private final MatchListener listener;
 
 public MatchAccessibilityDelegate(final Match match, final MatchListener listener) {
 this.match = match;
 this.listener = listener;
 }
 
 @Override
 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
 super.onInitializeAccessibilityNodeInfo(host, info);
 info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.comment, "Comment"));
 info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.share, "Share"));
 …
 }
 
 @Override
 public boolean performAccessibilityAction(View host, int action, Bundle args) {
 switch (action) {
 case R.id.comment:
 listener.onMatchComment(match);
 return true;
 case R.id.share:
 listener.onMatchShare(match);
 return true;
 …
 return true;

  58. Accessibility delegate public static class MatchAccessibilityDelegate extends AccessibilityDelegateCompat {
 


    private final Match match;
 private final MatchListener listener;
 
 public MatchAccessibilityDelegate(final Match match, final MatchListener listener) {
 this.match = match;
 this.listener = listener;
 }
 
 @Override
 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
 super.onInitializeAccessibilityNodeInfo(host, info);
 info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.comment, "Comment"));
 info.addAction(new AccessibilityNodeInfoCompat.AccessibilityActionCompat(R.id.share, "Share"));
 …
 }
 
 @Override
 public boolean performAccessibilityAction(View host, int action, Bundle args) {
 switch (action) {
 case R.id.comment:
 listener.onMatchComment(match);
 return true;
 case R.id.share:
 listener.onMatchShare(match);
 return true;
 …

  59. Accessibility delegate public void showMatch(final Match match, final MatchListener listener)

    {
 …
 
 ViewCompat.setAccessibilityDelegate(this, new MatchAccessibilityDelegate(match, listener));
 
 …
  60. None
  61. Testing

  62. Manual testing

  63. None
  64. Talkback basics Touch to explore Advance through elements (swipe left

    - right) Double tap selection Gestures for Back, Home, Context menus, …
  65. Accessibility scanner

  66. None
  67. None
  68. None
  69. None
  70. None
  71. Code scanning (Lint)

  72. None
  73. None
  74. Lint rules <?xml version="1.0" encoding="UTF-8"?> <lint> <issue id="ContentDescription" severity="error" />

    </lint> lintOptions { abortOnError true }
  75. Instrumentation tests

  76. Espresso - Accessibility @Test
 public void testPlayStopButtonContentDescription() {
 final ViewInteraction

    playButtonInteraction = onView(withId(R.id.play_button));
 playButtonInteraction.check(matches(withContentDescription("Play")));
 
 playButtonInteraction.perform(click());
 playButtonInteraction.check(matches(withContentDescription("Stop")));
 }
  77. Espresso - Accessibility androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2.2' @Before
 public void setUp() {


    AccessibilityChecks.enable();
 }
  78. None
  79. Accessibility checks com..…AccessibilityViewCheckException: There were 2 accessibility errors: AppCompatImageButton{id=2131427416, res-name=imageButton1,

    visibility=VISIBLE, width=144, height=132, has- focus=true, has-focusable=true, has-window-focus=false, is-clickable=true, is-enabled=true, is- focused=true, is-focusable=true, is-layout-requested=false, is-selected=false, root-is-layout- requested=false, has-input-connection=false, x=48.0, y=48.0}: View falls below the minimum recommended size for touch targets. Minimum touch target size is 48x48dp. Actual size is 48.0x44.0dp (screen density is 3.0)., AppCompatImageButton{id=2131427416, res-name=imageButton1, visibility=VISIBLE, width=144, height=132, has- focus=true, has-focusable=true, has-window-focus=false, is-clickable=true, is-enabled=true, is- focused=true, is-focusable=true, is-layout-requested=false, is-selected=false, root-is-layout- requested=false, has-input-connection=false, x=48.0, y=48.0}: View is missing speakable text needed for a screen reader at … at android.support.test.espresso.contrib.AccessibilityChecks$2.check(AccessibilityChecks.java:58) at android.support.test.espresso.action.ViewActions$1.perform(ViewActions.java:131) … at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
  80. Android Testing Support Library https://developer.android.com/guide/topics/ui/accessibility

  81. Sample github.com/filipmaelbrancke/AndroidAccessibility Accessibility sample Sample used in this presentation: Custom

    Actions Espresso Test
  82. Resources Accessibility Testing Checklist https://developer.android.com/training/accessibility/testing.html Google’s Accessibility Test Framework github.com/google/Accessibility-Test-Framework-for-Android

    Espresso Accessibility Checking google.github.io/android-testing-support-library/docs/accesibility-checking/ Talkback Gestures support.google.com/accessibility/android/answer/6151827?hl=en
  83. + accessibility + accessibility

  84. None
  85. Questions? Filip Maelbrancke Consultant @ AppFoundry filip.maelbrancke@appfoundry.be @fmaelbrancke