Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.setupwizardlib.util;
     18 
     19 import android.graphics.Rect;
     20 import android.os.Build;
     21 import android.os.Bundle;
     22 import android.support.v4.view.AccessibilityDelegateCompat;
     23 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     24 import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
     25 import android.support.v4.widget.ExploreByTouchHelper;
     26 import android.text.Layout;
     27 import android.text.Spanned;
     28 import android.text.style.ClickableSpan;
     29 import android.util.Log;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 import android.view.ViewGroup;
     33 import android.view.accessibility.AccessibilityEvent;
     34 import android.widget.TextView;
     35 
     36 import java.util.List;
     37 
     38 /**
     39  * An accessibility delegate that allows {@link android.text.style.ClickableSpan} to be focused and
     40  * clicked by accessibility services.
     41  * <p>
     42  * <strong>Note: </strong> From Android O on, there is native support for ClickableSpan
     43  * accessibility, so this class is not needed (and indeed has no effect.)
     44  * </p>
     45  *
     46  * <p />Sample usage:
     47  * <pre>
     48  * LinkAccessibilityHelper mAccessibilityHelper;
     49  *
     50  * private void init() {
     51  *     mAccessibilityHelper = new LinkAccessibilityHelper(myTextView);
     52  *     ViewCompat.setAccessibilityDelegate(myTextView, mLinkHelper);
     53  * }
     54  *
     55  * {@literal @}Override
     56  * protected boolean dispatchHoverEvent({@literal @}NonNull MotionEvent event) {
     57  *     if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) {
     58  *         return true;
     59  *     }
     60  *     return super.dispatchHoverEvent(event);
     61  * }
     62  * </pre>
     63  *
     64  * @see com.android.setupwizardlib.view.RichTextView
     65  * @see android.support.v4.widget.ExploreByTouchHelper
     66  */
     67 public class LinkAccessibilityHelper extends AccessibilityDelegateCompat {
     68 
     69     private static final String TAG = "LinkAccessibilityHelper";
     70 
     71     private final TextView mView;
     72     private final Rect mTempRect = new Rect();
     73     private final ExploreByTouchHelper mExploreByTouchHelper;
     74 
     75     public LinkAccessibilityHelper(TextView view) {
     76         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
     77             // Pre-O, we essentially extend ExploreByTouchHelper to expose a virtual view hierarchy
     78             mExploreByTouchHelper = new ExploreByTouchHelper(view) {
     79                 @Override
     80                 protected int getVirtualViewAt(float x, float y) {
     81                     return LinkAccessibilityHelper.this.getVirtualViewAt(x, y);
     82                 }
     83 
     84                 @Override
     85                 protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
     86                     LinkAccessibilityHelper.this.getVisibleVirtualViews(virtualViewIds);
     87                 }
     88 
     89                 @Override
     90                 protected void onPopulateEventForVirtualView(int virtualViewId,
     91                         AccessibilityEvent event) {
     92                     LinkAccessibilityHelper
     93                             .this.onPopulateEventForVirtualView(virtualViewId, event);
     94                 }
     95 
     96                 @Override
     97                 protected void onPopulateNodeForVirtualView(int virtualViewId,
     98                         AccessibilityNodeInfoCompat infoCompat) {
     99                     LinkAccessibilityHelper
    100                             .this.onPopulateNodeForVirtualView(virtualViewId, infoCompat);
    101 
    102                 }
    103 
    104                 @Override
    105                 protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
    106                         Bundle arguments) {
    107                     return LinkAccessibilityHelper.this
    108                             .onPerformActionForVirtualView(virtualViewId, action, arguments);
    109                 }
    110             };
    111         } else {
    112             mExploreByTouchHelper = null;
    113         }
    114         mView = view;
    115     }
    116 
    117     @Override
    118     public void sendAccessibilityEvent(View host, int eventType) {
    119         if (mExploreByTouchHelper != null) {
    120             mExploreByTouchHelper.sendAccessibilityEvent(host, eventType);
    121         } else {
    122             super.sendAccessibilityEvent(host, eventType);
    123         }
    124     }
    125 
    126     @Override
    127     public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) {
    128         if (mExploreByTouchHelper != null) {
    129             mExploreByTouchHelper.sendAccessibilityEventUnchecked(host, event);
    130         } else {
    131             super.sendAccessibilityEventUnchecked(host, event);
    132         }
    133     }
    134 
    135     @Override
    136     public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
    137         return (mExploreByTouchHelper != null)
    138                 ? mExploreByTouchHelper.dispatchPopulateAccessibilityEvent(host, event)
    139                 : super.dispatchPopulateAccessibilityEvent(host, event);
    140     }
    141 
    142     @Override
    143     public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
    144         if (mExploreByTouchHelper != null) {
    145             mExploreByTouchHelper.onPopulateAccessibilityEvent(host, event);
    146         } else {
    147             super.onPopulateAccessibilityEvent(host, event);
    148         }
    149     }
    150 
    151     @Override
    152     public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
    153         if (mExploreByTouchHelper != null) {
    154             mExploreByTouchHelper.onInitializeAccessibilityEvent(host, event);
    155         } else {
    156             super.onInitializeAccessibilityEvent(host, event);
    157         }
    158     }
    159 
    160     @Override
    161     public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
    162         if (mExploreByTouchHelper != null) {
    163             mExploreByTouchHelper.onInitializeAccessibilityNodeInfo(host, info);
    164         } else {
    165             super.onInitializeAccessibilityNodeInfo(host, info);
    166         }
    167     }
    168 
    169     @Override
    170     public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
    171             AccessibilityEvent event) {
    172         return (mExploreByTouchHelper != null)
    173                 ? mExploreByTouchHelper.onRequestSendAccessibilityEvent(host, child, event)
    174                 : super.onRequestSendAccessibilityEvent(host, child, event);
    175     }
    176 
    177     @Override
    178     public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
    179         return (mExploreByTouchHelper != null)
    180                 ? mExploreByTouchHelper.getAccessibilityNodeProvider(host)
    181                 : super.getAccessibilityNodeProvider(host);
    182     }
    183 
    184     @Override
    185     public boolean performAccessibilityAction(View host, int action, Bundle args) {
    186         return (mExploreByTouchHelper != null)
    187                 ? mExploreByTouchHelper.performAccessibilityAction(host, action, args)
    188                 : super.performAccessibilityAction(host, action, args);
    189     }
    190 
    191     /**
    192      * Delegated to {@link ExploreByTouchHelper}
    193      */
    194     public final boolean dispatchHoverEvent(MotionEvent event) {
    195         return (mExploreByTouchHelper != null) ? mExploreByTouchHelper.dispatchHoverEvent(event)
    196                 : false;
    197     }
    198 
    199     protected int getVirtualViewAt(float x, float y) {
    200         final CharSequence text = mView.getText();
    201         if (text instanceof Spanned) {
    202             final Spanned spannedText = (Spanned) text;
    203             final int offset = getOffsetForPosition(mView, x, y);
    204             ClickableSpan[] linkSpans = spannedText.getSpans(offset, offset, ClickableSpan.class);
    205             if (linkSpans.length == 1) {
    206                 ClickableSpan linkSpan = linkSpans[0];
    207                 return spannedText.getSpanStart(linkSpan);
    208             }
    209         }
    210         return ExploreByTouchHelper.INVALID_ID;
    211     }
    212 
    213     protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
    214         final CharSequence text = mView.getText();
    215         if (text instanceof Spanned) {
    216             final Spanned spannedText = (Spanned) text;
    217             ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(),
    218                     ClickableSpan.class);
    219             for (ClickableSpan span : linkSpans) {
    220                 virtualViewIds.add(spannedText.getSpanStart(span));
    221             }
    222         }
    223     }
    224 
    225     protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
    226         final ClickableSpan span = getSpanForOffset(virtualViewId);
    227         if (span != null) {
    228             event.setContentDescription(getTextForSpan(span));
    229         } else {
    230             Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
    231             event.setContentDescription(mView.getText());
    232         }
    233     }
    234 
    235     protected void onPopulateNodeForVirtualView(int virtualViewId,
    236             AccessibilityNodeInfoCompat info) {
    237         final ClickableSpan span = getSpanForOffset(virtualViewId);
    238         if (span != null) {
    239             info.setContentDescription(getTextForSpan(span));
    240         } else {
    241             Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
    242             info.setContentDescription(mView.getText());
    243         }
    244         info.setFocusable(true);
    245         info.setClickable(true);
    246         getBoundsForSpan(span, mTempRect);
    247         if (mTempRect.isEmpty()) {
    248             Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId);
    249             mTempRect.set(0, 0, 1, 1);
    250         }
    251         info.setBoundsInParent(mTempRect);
    252         info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
    253     }
    254 
    255     protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
    256             Bundle arguments) {
    257         if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
    258             ClickableSpan span = getSpanForOffset(virtualViewId);
    259             if (span != null) {
    260                 span.onClick(mView);
    261                 return true;
    262             } else {
    263                 Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
    264             }
    265         }
    266         return false;
    267     }
    268 
    269     private ClickableSpan getSpanForOffset(int offset) {
    270         CharSequence text = mView.getText();
    271         if (text instanceof Spanned) {
    272             Spanned spannedText = (Spanned) text;
    273             ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class);
    274             if (spans.length == 1) {
    275                 return spans[0];
    276             }
    277         }
    278         return null;
    279     }
    280 
    281     private CharSequence getTextForSpan(ClickableSpan span) {
    282         CharSequence text = mView.getText();
    283         if (text instanceof Spanned) {
    284             Spanned spannedText = (Spanned) text;
    285             return spannedText.subSequence(spannedText.getSpanStart(span),
    286                     spannedText.getSpanEnd(span));
    287         }
    288         return text;
    289     }
    290 
    291     // Find the bounds of a span. If it spans multiple lines, it will only return the bounds for the
    292     // section on the first line.
    293     private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) {
    294         CharSequence text = mView.getText();
    295         outRect.setEmpty();
    296         if (text instanceof Spanned) {
    297             final Layout layout = mView.getLayout();
    298             if (layout != null) {
    299                 Spanned spannedText = (Spanned) text;
    300                 final int spanStart = spannedText.getSpanStart(span);
    301                 final int spanEnd = spannedText.getSpanEnd(span);
    302                 final float xStart = layout.getPrimaryHorizontal(spanStart);
    303                 final float xEnd = layout.getPrimaryHorizontal(spanEnd);
    304                 final int lineStart = layout.getLineForOffset(spanStart);
    305                 final int lineEnd = layout.getLineForOffset(spanEnd);
    306                 layout.getLineBounds(lineStart, outRect);
    307                 if (lineEnd == lineStart) {
    308                     // If the span is on a single line, adjust both the left and right bounds
    309                     // so outrect is exactly bounding the span.
    310                     outRect.left = (int) Math.min(xStart, xEnd);
    311                     outRect.right = (int) Math.max(xStart, xEnd);
    312                 } else {
    313                     // If the span wraps across multiple lines, only use the first line (as returned
    314                     // by layout.getLineBounds above), and adjust the "start" of outrect to where
    315                     // the span starts, leaving the "end" of outrect at the end of the line.
    316                     // ("start" being left for LTR, and right for RTL)
    317                     if (layout.getParagraphDirection(lineStart) == Layout.DIR_RIGHT_TO_LEFT) {
    318                         outRect.right = (int) xStart;
    319                     } else {
    320                         outRect.left = (int) xStart;
    321                     }
    322                 }
    323 
    324                 // Offset for padding
    325                 outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop());
    326             }
    327         }
    328         return outRect;
    329     }
    330 
    331     // Compat implementation of TextView#getOffsetForPosition().
    332 
    333     private static int getOffsetForPosition(TextView view, float x, float y) {
    334         if (view.getLayout() == null) return -1;
    335         final int line = getLineAtCoordinate(view, y);
    336         return getOffsetAtCoordinate(view, line, x);
    337     }
    338 
    339     private static float convertToLocalHorizontalCoordinate(TextView view, float x) {
    340         x -= view.getTotalPaddingLeft();
    341         // Clamp the position to inside of the view.
    342         x = Math.max(0.0f, x);
    343         x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x);
    344         x += view.getScrollX();
    345         return x;
    346     }
    347 
    348     private static int getLineAtCoordinate(TextView view, float y) {
    349         y -= view.getTotalPaddingTop();
    350         // Clamp the position to inside of the view.
    351         y = Math.max(0.0f, y);
    352         y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y);
    353         y += view.getScrollY();
    354         return view.getLayout().getLineForVertical((int) y);
    355     }
    356 
    357     private static int getOffsetAtCoordinate(TextView view, int line, float x) {
    358         x = convertToLocalHorizontalCoordinate(view, x);
    359         return view.getLayout().getOffsetForHorizontal(line, x);
    360     }
    361 }
    362