1 /*
2  * Copyright 2018 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 androidx.core.view;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
20 
21 import android.accessibilityservice.AccessibilityService;
22 import android.os.Build;
23 import android.os.Bundle;
24 import android.text.style.ClickableSpan;
25 import android.util.SparseArray;
26 import android.view.View;
27 import android.view.View.AccessibilityDelegate;
28 import android.view.ViewGroup;
29 import android.view.accessibility.AccessibilityEvent;
30 import android.view.accessibility.AccessibilityNodeInfo;
31 import android.view.accessibility.AccessibilityNodeProvider;
32 
33 import androidx.annotation.RestrictTo;
34 import androidx.core.R;
35 import androidx.core.view.accessibility.AccessibilityClickableSpanCompat;
36 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
37 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
38 import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
39 
40 import org.jspecify.annotations.NonNull;
41 import org.jspecify.annotations.Nullable;
42 
43 import java.lang.ref.WeakReference;
44 import java.util.Collections;
45 import java.util.List;
46 
47 /**
48  * Helper for accessing {@link AccessibilityDelegate}.
49  * <p>
50  * <strong>Note:</strong> On platform versions prior to
51  * {@link Build.VERSION_CODES#M API 23}, delegate methods on
52  * views in the {@code android.widget.*} package are called <i>before</i>
53  * host methods. This prevents certain properties such as class name from
54  * being modified by overriding
55  * {@link AccessibilityDelegateCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat)},
56  * as any changes will be overwritten by the host class.
57  * <p>
58  * Starting in {@link Build.VERSION_CODES#M API 23}, delegate
59  * methods are called <i>after</i> host methods, which all properties to be
60  * modified without being overwritten by the host class.
61  */
62 public class AccessibilityDelegateCompat {
63 
64     static final class AccessibilityDelegateAdapter extends AccessibilityDelegate {
65         final AccessibilityDelegateCompat mCompat;
66 
AccessibilityDelegateAdapter(AccessibilityDelegateCompat compat)67         AccessibilityDelegateAdapter(AccessibilityDelegateCompat compat) {
68             mCompat = compat;
69         }
70 
71         @Override
dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event)72         public boolean dispatchPopulateAccessibilityEvent(View host,
73                 AccessibilityEvent event) {
74             return mCompat.dispatchPopulateAccessibilityEvent(host, event);
75         }
76 
77         @Override
onInitializeAccessibilityEvent(View host, AccessibilityEvent event)78         public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
79             mCompat.onInitializeAccessibilityEvent(host, event);
80         }
81 
82         @Override
onInitializeAccessibilityNodeInfo( View host, AccessibilityNodeInfo info)83         public void onInitializeAccessibilityNodeInfo(
84                 View host, AccessibilityNodeInfo info) {
85             AccessibilityNodeInfoCompat nodeInfoCompat = AccessibilityNodeInfoCompat.wrap(info);
86             nodeInfoCompat.setScreenReaderFocusable(ViewCompat.isScreenReaderFocusable(host));
87             nodeInfoCompat.setHeading(ViewCompat.isAccessibilityHeading(host));
88             nodeInfoCompat.setPaneTitle(ViewCompat.getAccessibilityPaneTitle(host));
89             nodeInfoCompat.setStateDescription(ViewCompat.getStateDescription(host));
90             mCompat.onInitializeAccessibilityNodeInfo(host, nodeInfoCompat);
91             nodeInfoCompat.addSpansToExtras(info.getText(), host);
92             List<AccessibilityActionCompat> actions = getActionList(host);
93             for (int i = 0; i < actions.size(); i++) {
94                 nodeInfoCompat.addAction(actions.get(i));
95             }
96         }
97 
98         @Override
onPopulateAccessibilityEvent(View host, AccessibilityEvent event)99         public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
100             mCompat.onPopulateAccessibilityEvent(host, event);
101         }
102 
103         @Override
onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event)104         public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
105                 AccessibilityEvent event) {
106             return mCompat.onRequestSendAccessibilityEvent(host, child, event);
107         }
108 
109         @Override
sendAccessibilityEvent(View host, int eventType)110         public void sendAccessibilityEvent(View host, int eventType) {
111             mCompat.sendAccessibilityEvent(host, eventType);
112         }
113 
114         @Override
sendAccessibilityEventUnchecked(View host, AccessibilityEvent event)115         public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) {
116             mCompat.sendAccessibilityEventUnchecked(host, event);
117         }
118 
119         @Override
getAccessibilityNodeProvider(View host)120         public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) {
121             AccessibilityNodeProviderCompat provider =
122                     mCompat.getAccessibilityNodeProvider(host);
123             return (provider != null)
124                     ? (AccessibilityNodeProvider) provider.getProvider() : null;
125         }
126 
127         @Override
performAccessibilityAction(View host, int action, Bundle args)128         public boolean performAccessibilityAction(View host, int action, Bundle args) {
129             return mCompat.performAccessibilityAction(host, action, args);
130         }
131     }
132 
133     private static final AccessibilityDelegate DEFAULT_DELEGATE = new AccessibilityDelegate();
134     private final AccessibilityDelegate mOriginalDelegate;
135 
136     private final AccessibilityDelegate mBridge;
137 
138     /**
139      * Creates a new instance.
140      */
AccessibilityDelegateCompat()141     public AccessibilityDelegateCompat() {
142         this(DEFAULT_DELEGATE);
143     }
144 
145     /**
146      */
147     @RestrictTo(LIBRARY_GROUP_PREFIX)
AccessibilityDelegateCompat(@onNull AccessibilityDelegate originalDelegate)148     public AccessibilityDelegateCompat(@NonNull AccessibilityDelegate originalDelegate) {
149         mOriginalDelegate = originalDelegate;
150         mBridge = new AccessibilityDelegateAdapter(this);
151     }
152 
153     /**
154      * @return The wrapped bridge implementation.
155      */
getBridge()156     AccessibilityDelegate getBridge() {
157         return mBridge;
158     }
159 
160     /**
161      * Sends an accessibility event of the given type. If accessibility is not
162      * enabled this method has no effect.
163      * <p>
164      * The default implementation behaves as {@link View#sendAccessibilityEvent(int)
165      * View#sendAccessibilityEvent(int)} for the case of no accessibility delegate
166      * been set.
167      * </p>
168      *
169      * @param host The View hosting the delegate.
170      * @param eventType The type of the event to send.
171      *
172      * @see View#sendAccessibilityEvent(int) View#sendAccessibilityEvent(int)
173      */
sendAccessibilityEvent(@onNull View host, int eventType)174     public void sendAccessibilityEvent(@NonNull View host, int eventType) {
175         mOriginalDelegate.sendAccessibilityEvent(host, eventType);
176     }
177 
178     /**
179      * Sends an accessibility event. This method behaves exactly as
180      * {@link #sendAccessibilityEvent(View, int)} but takes as an argument an
181      * empty {@link AccessibilityEvent} and does not perform a check whether
182      * accessibility is enabled.
183      * <p>
184      * The default implementation behaves as
185      * {@link View#sendAccessibilityEventUnchecked(AccessibilityEvent)
186      * View#sendAccessibilityEventUnchecked(AccessibilityEvent)} for
187      * the case of no accessibility delegate been set.
188      * </p>
189      *
190      * @param host The View hosting the delegate.
191      * @param event The event to send.
192      *
193      * @see View#sendAccessibilityEventUnchecked(AccessibilityEvent)
194      *      View#sendAccessibilityEventUnchecked(AccessibilityEvent)
195      */
sendAccessibilityEventUnchecked(@onNull View host, @NonNull AccessibilityEvent event)196     public void sendAccessibilityEventUnchecked(@NonNull View host,
197             @NonNull AccessibilityEvent event) {
198         mOriginalDelegate.sendAccessibilityEventUnchecked(host, event);
199     }
200 
201     /**
202      * Dispatches an {@link AccessibilityEvent} to the host {@link View} first and then
203      * to its children for adding their text content to the event.
204      * <p>
205      * The default implementation behaves as
206      * {@link View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)
207      * View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)} for
208      * the case of no accessibility delegate been set.
209      * </p>
210      *
211      * @param host The View hosting the delegate.
212      * @param event The event.
213      * @return True if the event population was completed.
214      *
215      * @see View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)
216      *      View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)
217      */
dispatchPopulateAccessibilityEvent(@onNull View host, @NonNull AccessibilityEvent event)218     public boolean dispatchPopulateAccessibilityEvent(@NonNull View host,
219             @NonNull AccessibilityEvent event) {
220         return mOriginalDelegate.dispatchPopulateAccessibilityEvent(host, event);
221     }
222 
223     /**
224      * Gives a chance to the host View to populate the accessibility event with its
225      * text content.
226      * <p>
227      * The default implementation behaves as
228      * {@link ViewCompat#onPopulateAccessibilityEvent(View, AccessibilityEvent)
229      * ViewCompat#onPopulateAccessibilityEvent(AccessibilityEvent)} for
230      * the case of no accessibility delegate been set.
231      * </p>
232      *
233      * @param host The View hosting the delegate.
234      * @param event The accessibility event which to populate.
235      *
236      * @see ViewCompat#onPopulateAccessibilityEvent(View ,AccessibilityEvent)
237      *      ViewCompat#onPopulateAccessibilityEvent(View, AccessibilityEvent)
238      */
onPopulateAccessibilityEvent(@onNull View host, @NonNull AccessibilityEvent event)239     public void onPopulateAccessibilityEvent(@NonNull View host,
240             @NonNull AccessibilityEvent event) {
241         mOriginalDelegate.onPopulateAccessibilityEvent(host, event);
242     }
243 
244     /**
245      * Initializes an {@link AccessibilityEvent} with information about the
246      * the host View which is the event source.
247      * <p>
248      * The default implementation behaves as
249      * {@link ViewCompat#onInitializeAccessibilityEvent(View v, AccessibilityEvent event)
250      * ViewCompat#onInitalizeAccessibilityEvent(View v, AccessibilityEvent event)} for
251      * the case of no accessibility delegate been set.
252      * </p>
253      *
254      * @param host The View hosting the delegate.
255      * @param event The event to initialize.
256      *
257      * @see ViewCompat#onInitializeAccessibilityEvent(View, AccessibilityEvent)
258      *      ViewCompat#onInitializeAccessibilityEvent(View, AccessibilityEvent)
259      */
onInitializeAccessibilityEvent(@onNull View host, @NonNull AccessibilityEvent event)260     public void onInitializeAccessibilityEvent(@NonNull View host,
261             @NonNull AccessibilityEvent event) {
262         mOriginalDelegate.onInitializeAccessibilityEvent(host, event);
263     }
264 
265     /**
266      * Initializes an {@link AccessibilityNodeInfoCompat} with information about the host view.
267      * <p>
268      * The default implementation behaves as
269      * {@link ViewCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat)
270      * ViewCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat)} for
271      * the case of no accessibility delegate been set.
272      * </p>
273      *
274      * @param host The View hosting the delegate.
275      * @param info The instance to initialize.
276      *
277      * @see ViewCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat)
278      *      ViewCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat)
279      */
onInitializeAccessibilityNodeInfo(@onNull View host, @NonNull AccessibilityNodeInfoCompat info)280     public void onInitializeAccessibilityNodeInfo(@NonNull View host,
281             @NonNull AccessibilityNodeInfoCompat info) {
282         mOriginalDelegate.onInitializeAccessibilityNodeInfo(
283                 host, info.unwrap());
284     }
285 
286     /**
287      * Called when a child of the host View has requested sending an
288      * {@link AccessibilityEvent} and gives an opportunity to the parent (the host)
289      * to augment the event.
290      * <p>
291      * The default implementation behaves as
292      * {@link ViewGroupCompat#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent)
293      * ViewGroupCompat#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent)} for
294      * the case of no accessibility delegate been set.
295      * </p>
296      *
297      * @param host The View hosting the delegate.
298      * @param child The child which requests sending the event.
299      * @param event The event to be sent.
300      * @return True if the event should be sent
301      *
302      * @see ViewGroupCompat#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent)
303      *      ViewGroupCompat#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent)
304      */
onRequestSendAccessibilityEvent(@onNull ViewGroup host, @NonNull View child, @NonNull AccessibilityEvent event)305     public boolean onRequestSendAccessibilityEvent(@NonNull ViewGroup host, @NonNull View child,
306             @NonNull AccessibilityEvent event) {
307         return mOriginalDelegate.onRequestSendAccessibilityEvent(host, child, event);
308     }
309 
310     /**
311      * Gets the provider for managing a virtual view hierarchy rooted at this View
312      * and reported to {@link AccessibilityService}s
313      * that explore the window content.
314      * <p>
315      * The default implementation behaves as
316      * {@link ViewCompat#getAccessibilityNodeProvider(View) ViewCompat#getAccessibilityNodeProvider(View)}
317      * for the case of no accessibility delegate been set.
318      * </p>
319      *
320      * @return The provider.
321      *
322      * @see AccessibilityNodeProviderCompat
323      */
getAccessibilityNodeProvider( @onNull View host)324     public @Nullable AccessibilityNodeProviderCompat getAccessibilityNodeProvider(
325             @NonNull View host) {
326         Object provider = mOriginalDelegate.getAccessibilityNodeProvider(host);
327         if (provider != null) {
328             return new AccessibilityNodeProviderCompat(provider);
329         }
330         return null;
331     }
332 
333     /**
334      * Performs the specified accessibility action on the view. For
335      * possible accessibility actions look at {@link AccessibilityNodeInfoCompat}.
336      * <p>
337      * The default implementation behaves as
338      * {@link View#performAccessibilityAction(int, Bundle)
339      *  View#performAccessibilityAction(int, Bundle)} for the case of
340      *  no accessibility delegate been set.
341      * </p>
342      *
343      *
344      * @param host View on which to perform the action.
345      * @param action The action to perform.
346      * @param args Optional action arguments.
347      * @return Whether the action was performed.
348      *
349      * @see View#performAccessibilityAction(int, Bundle)
350      *      View#performAccessibilityAction(int, Bundle)
351      */
performAccessibilityAction(@onNull View host, int action, @Nullable Bundle args)352     public boolean performAccessibilityAction(@NonNull View host, int action,
353             @Nullable Bundle args) {
354         boolean success = false;
355         List<AccessibilityActionCompat> actions = getActionList(host);
356         for (int i = 0; i < actions.size(); i++) {
357             AccessibilityActionCompat actionCompat = actions.get(i);
358             if (actionCompat.getId() == action) {
359                 success = actionCompat.perform(host, args);
360                 break;
361             }
362         }
363         if (!success) {
364             success = mOriginalDelegate.performAccessibilityAction(host, action, args);
365         }
366         if (!success && action == R.id.accessibility_action_clickable_span && args != null) {
367             success = performClickableSpanAction(
368                     args.getInt(AccessibilityClickableSpanCompat.SPAN_ID, -1), host);
369         }
370         return success;
371     }
372 
373     @SuppressWarnings("unchecked")
performClickableSpanAction(int clickableSpanId, View host)374     private boolean performClickableSpanAction(int clickableSpanId, View host) {
375         SparseArray<WeakReference<ClickableSpan>> spans =
376                 (SparseArray<WeakReference<ClickableSpan>>)
377                         host.getTag(R.id.tag_accessibility_clickable_spans);
378         if (spans != null) {
379             WeakReference<ClickableSpan> reference = spans.get(clickableSpanId);
380             if (reference != null) {
381                 ClickableSpan span = reference.get();
382                 if (isSpanStillValid(span, host)) {
383                     span.onClick(host);
384                     return true;
385                 }
386             }
387         }
388         return false;
389     }
390 
isSpanStillValid(ClickableSpan span, View view)391     private boolean isSpanStillValid(ClickableSpan span, View view) {
392         if (span != null) {
393             AccessibilityNodeInfo info = view.createAccessibilityNodeInfo();
394             ClickableSpan[] spans = AccessibilityNodeInfoCompat.getClickableSpans(info.getText());
395             for (int i = 0; spans != null && i < spans.length; i++) {
396                 if (span.equals(spans[i])) {
397                     return true;
398                 }
399             }
400         }
401         return false;
402     }
403 
404     @SuppressWarnings("unchecked")
getActionList(View view)405     static List<AccessibilityActionCompat> getActionList(View view) {
406         List<AccessibilityActionCompat> actions = (List<AccessibilityActionCompat>)
407                 view.getTag(R.id.tag_accessibility_actions);
408         return actions == null ? Collections.emptyList() : actions;
409     }
410 }
411