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