Bug 1315201: Override AS context menu a11y title. r=sebastian draft
authorMichael Comella <michael.l.comella@gmail.com>
Mon, 21 Aug 2017 18:13:34 -0700
changeset 651664 8d3b24fa3654102ad421e8fcc73fcede0dc85eca
parent 651663 88236273f6e1d26c6e6ef770b281f814f9c0b66e
child 651665 c20167ccb9cdba1350b81094cb1a4ffea7b842a9
push id75787
push usermichael.l.comella@gmail.com
push dateWed, 23 Aug 2017 22:00:44 +0000
reviewerssebastian
bugs1315201
milestone57.0a1
Bug 1315201: Override AS context menu a11y title. r=sebastian MozReview-Commit-ID: HeX7hTpVtEP
mobile/android/app/src/main/res/layout/activity_stream_contextmenu_bottomsheet.xml
mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/menu/BottomSheetContextMenu.java
--- a/mobile/android/app/src/main/res/layout/activity_stream_contextmenu_bottomsheet.xml
+++ b/mobile/android/app/src/main/res/layout/activity_stream_contextmenu_bottomsheet.xml
@@ -6,17 +6,20 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:orientation="vertical">
 
     <!-- a11y: When the dialog first appears, the title is announced so there is no
-         need to allow refocusing the title. -->
+         need to allow refocusing the title.
+
+         Also, we override the a11y title of the dialog in code. If you change the text in id/url or id/title,
+         update the code! -->
     <RelativeLayout
         android:id="@+id/info_wrapper"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:padding="10dp"
         android:importantForAccessibility="noHideDescendants">
 
         <org.mozilla.gecko.activitystream.homepanel.stream.StreamOverridablePageIconLayout
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/menu/BottomSheetContextMenu.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/homepanel/menu/BottomSheetContextMenu.java
@@ -1,26 +1,28 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 package org.mozilla.gecko.activitystream.homepanel.menu;
 
 import android.app.Activity;
 import android.content.Context;
+import android.net.Uri;
 import android.support.design.widget.BottomSheetBehavior;
 import android.support.design.widget.BottomSheetDialog;
 import android.support.design.widget.NavigationView;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.Window;
+import android.view.accessibility.AccessibilityEvent;
 import android.widget.TextView;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
 import org.mozilla.gecko.activitystream.homepanel.model.Item;
 import org.mozilla.gecko.activitystream.homepanel.stream.StreamOverridablePageIconLayout;
 import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.URIUtils;
@@ -36,16 +38,19 @@ import java.net.URISyntaxException;
 
     private final BottomSheetDialog bottomSheetDialog;
 
     private final NavigationView navigationView;
 
     private final View content;
     private final View activityView;
 
+    /** A reference, that represents the page domain, that allows a return value from an async task. */
+    private String[] pageDomainTextReference = new String[] { "" };
+
     public BottomSheetContextMenu(final Context context,
                                   final ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder,
                                   final MenuMode mode,
                                   final Item item,
                                   final boolean shouldOverrideIconWithImageProvider,
                                   HomePager.OnUrlOpenListener onUrlOpenListener,
                                   HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
                                   final int tilesWidth, final int tilesHeight) {
@@ -63,31 +68,34 @@ import java.net.URISyntaxException;
         bottomSheetDialog = new BottomSheetDialog(context);
         final LayoutInflater inflater = LayoutInflater.from(context);
         this.content = inflater.inflate(R.layout.activity_stream_contextmenu_bottomsheet, (ViewGroup) activityView, false);
 
         bottomSheetDialog.setContentView(content);
 
         final String pageTitle = item.getTitle();
         final String sheetPageTitle = !TextUtils.isEmpty(pageTitle) ? pageTitle : item.getUrl();
-        ((TextView) content.findViewById(R.id.title)).setText(sheetPageTitle);
+        final TextView titleView = (TextView) content.findViewById(R.id.title);
+        titleView.setText(sheetPageTitle);
 
         final TextView pageDomainView = (TextView) content.findViewById(R.id.url);
         final URI itemURI;
         try {
             itemURI = new URI(item.getUrl());
             final UpdatePageDomainAsyncTask updateDomainAsyncTask = new UpdatePageDomainAsyncTask(context, pageDomainView,
-                    itemURI);
+                    itemURI, pageDomainTextReference);
             updateDomainAsyncTask.execute();
         } catch (final URISyntaxException e) {
             // Invalid URI: not much processing we can do. Like the async task, the page title view sets itself to the
             // url on error so we leave this field blank.
             pageDomainView.setText("");
         }
 
+        overrideInitialAccessibilityAnnouncement(pageDomainView, titleView, sheetPageTitle, item.getUrl());
+
         // Copy layouted parameters from the Highlights / TopSites items to ensure consistency
         final StreamOverridablePageIconLayout pageIconLayout =
                 (StreamOverridablePageIconLayout) content.findViewById(R.id.page_icon_layout);
         final ViewGroup.LayoutParams layoutParams = pageIconLayout.getLayoutParams();
         layoutParams.width = tilesWidth;
         layoutParams.height = tilesHeight;
         pageIconLayout.setLayoutParams(layoutParams);
 
@@ -96,16 +104,66 @@ import java.net.URISyntaxException;
         pageIconLayout.updateIcon(item.getUrl(), overrideIconURL);
 
         navigationView = (NavigationView) content.findViewById(R.id.menu);
         navigationView.setNavigationItemSelectedListener(this);
 
         super.postInit();
     }
 
+    /**
+     * Override the announcement made when the dialog first appears with Talkback enabled.
+     *
+     * By default, the dialog will find the first TextView (the page domain) and speak its contents as the initial
+     * announcement. However, in order to uniquely identify a site, we need both the page domain and the page title: we
+     * override the announcement with that content.
+     *
+     * Caveat: the page domain is retrieved asynchronously so this becomes tricky. In theory, we could block the UI
+     * thread for the page domain and with the current implementation, it'd be fine:
+     * - It's async only because the first time this method is called, it reads a file from disk.
+     * Otherwise, it's unnecessary.
+     * - This method is guaranteed to have already been called because about:home shows before this context menu can show
+     *
+     * But the implementation could change and it seemed incorrect to block showing this context menu for
+     * *all* users solely for a11y text that can be estimated.
+     */
+    private void overrideInitialAccessibilityAnnouncement(final View pageDomainView, final View pageTitleView,
+            final String pageTitle, final String urlStr) {
+        final View.AccessibilityDelegate initialAnnouncementDelegate = new View.AccessibilityDelegate() {
+            @Override
+            public void onPopulateAccessibilityEvent(final View hostView, final AccessibilityEvent event) {
+                // The page domain is retrieved with an async operation and the return value is stored here.
+                final String shortenedHost = pageDomainTextReference[0];
+
+                final String finalHost;
+                if (!TextUtils.isEmpty(shortenedHost)) {
+                    finalHost = shortenedHost;
+                } else if (TextUtils.isEmpty(urlStr)) {
+                    // There's no url so we can't do any better.
+                    finalHost = "";
+                } else {
+                    // The async page domain isn't completed yet so we'll do a best approximation.
+                    final Uri uri = Uri.parse(urlStr);
+                    final String host = uri.getHost();
+                    finalHost = !TextUtils.isEmpty(host) ? host : urlStr;
+                }
+
+                final String announcementText = finalHost + ", " + pageTitle;
+                event.getText().add(announcementText);
+                super.onPopulateAccessibilityEvent(hostView, event);
+            }
+        };
+
+        // The dialog finds the first available TextView and announces its text as the accessibility title. The
+        // pageDomainView is first but since the title is completed asynchronously, it may not have text yet, and
+        // may be an invalid first TextView, we have to set the listener on both the first and second TextViews.
+        pageDomainView.setAccessibilityDelegate(initialAnnouncementDelegate);
+        pageTitleView.setAccessibilityDelegate(initialAnnouncementDelegate);
+    }
+
     @Override
     public MenuItem getItemByID(int id) {
         return navigationView.getMenu().findItem(id);
     }
 
     @Override
     public void show() {
         // Try to use a 16:9 "keyline", i.e. we leave a 16:9 window of activity content visible
@@ -164,20 +222,23 @@ import java.net.URISyntaxException;
 
     public void dismiss() {
         bottomSheetDialog.dismiss();
     }
 
     /** Updates the given TextView's text to the page domain. */
     private static class UpdatePageDomainAsyncTask extends URIUtils.GetFormattedDomainAsyncTask {
         private final WeakReference<TextView> pageDomainViewWeakReference;
+        private final String[] pageDomainTextReference;
 
-        private UpdatePageDomainAsyncTask(final Context context, final TextView pageDomainView, final URI uri) {
+        private UpdatePageDomainAsyncTask(final Context context, final TextView pageDomainView, final URI uri,
+                final String[] pageDomainTextReference) {
             super(context, uri, true, 0); // baseDomain.
             this.pageDomainViewWeakReference = new WeakReference<>(pageDomainView);
+            this.pageDomainTextReference = pageDomainTextReference;
         }
 
         @Override
         protected void onPostExecute(final String baseDomain) {
             super.onPostExecute(baseDomain);
 
             final TextView pageDomainView = pageDomainViewWeakReference.get();
             if (pageDomainView == null) {
@@ -190,12 +251,13 @@ import java.net.URISyntaxException;
 
             // In the unlikely error case, we leave the field blank rather than setting it to the url because
             // the page title view sets itself to the url on error.
             } else {
                 final String normalizedHost = StringUtils.stripCommonSubdomains(uri.getHost());
                 updateText = !TextUtils.isEmpty(normalizedHost) ? normalizedHost : "";
             }
 
+            pageDomainTextReference[0] = updateText;
             pageDomainView.setText(updateText);
         }
     }
 }