1 /**************************************************************************** 2 ** 3 ** Copyright (C) 2016 The Qt Company Ltd. 4 ** Contact: https://www.qt.io/licensing/ 5 ** 6 ** This file is part of the Android port of the Qt Toolkit. 7 ** 8 ** $QT_BEGIN_LICENSE:LGPL$ 9 ** Commercial License Usage 10 ** Licensees holding valid commercial Qt licenses may use this file in 11 ** accordance with the commercial license agreement provided with the 12 ** Software or, alternatively, in accordance with the terms contained in 13 ** a written agreement between you and The Qt Company. For licensing terms 14 ** and conditions see https://www.qt.io/terms-conditions. For further 15 ** information use the contact form at https://www.qt.io/contact-us. 16 ** 17 ** GNU Lesser General Public License Usage 18 ** Alternatively, this file may be used under the terms of the GNU Lesser 19 ** General Public License version 3 as published by the Free Software 20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the 21 ** packaging of this file. Please review the following information to 22 ** ensure the GNU Lesser General Public License version 3 requirements 23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 24 ** 25 ** GNU General Public License Usage 26 ** Alternatively, this file may be used under the terms of the GNU 27 ** General Public License version 2.0 or (at your option) the GNU General 28 ** Public license version 3 or any later version approved by the KDE Free 29 ** Qt Foundation. The licenses are as published by the Free Software 30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 31 ** included in the packaging of this file. Please review the following 32 ** information to ensure the GNU General Public License requirements will 33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and 34 ** https://www.gnu.org/licenses/gpl-3.0.html. 35 ** 36 ** $QT_END_LICENSE$ 37 ** 38 ****************************************************************************/ 39 40 41 package org.qtproject.qt5.android.accessibility; 42 43 import android.accessibilityservice.AccessibilityService; 44 import android.app.Activity; 45 import android.graphics.Rect; 46 import android.os.Bundle; 47 import android.util.Log; 48 import android.view.View; 49 import android.view.ViewGroup; 50 import android.view.ViewParent; 51 import android.text.TextUtils; 52 53 import android.view.accessibility.*; 54 import android.view.MotionEvent; 55 import android.view.View.OnHoverListener; 56 57 import android.content.Context; 58 59 import java.util.LinkedList; 60 import java.util.List; 61 62 import org.qtproject.qt5.android.QtActivityDelegate; 63 64 public class QtAccessibilityDelegate extends View.AccessibilityDelegate 65 { 66 private static final String TAG = "Qt A11Y"; 67 68 // Qt uses the upper half of the unsiged integers 69 // all low positive ints should be fine. 70 public static final int INVALID_ID = 333; // half evil 71 72 // The platform might ask for the class implementing the "view". 73 // Pretend to be an inner class of the QtSurface. 74 private static final String DEFAULT_CLASS_NAME = "$VirtualChild"; 75 76 private View m_view = null; 77 private AccessibilityManager m_manager; 78 private QtActivityDelegate m_activityDelegate; 79 private Activity m_activity; 80 private ViewGroup m_layout; 81 82 // The accessible object that currently has the "accessibility focus" 83 // usually indicated by a yellow rectangle on screen. 84 private int m_focusedVirtualViewId = INVALID_ID; 85 // When exploring the screen by touch, the item "hovered" by the finger. 86 private int m_hoveredVirtualViewId = INVALID_ID; 87 88 // Cache coordinates of the view to know the global offset 89 // this is because the Android platform window does not take 90 // the offset of the view on screen into account (eg status bar on top) 91 private final int[] m_globalOffset = new int[2]; 92 93 private class HoverEventListener implements View.OnHoverListener 94 { 95 @Override onHover(View v, MotionEvent event)96 public boolean onHover(View v, MotionEvent event) 97 { 98 return dispatchHoverEvent(event); 99 } 100 } 101 QtAccessibilityDelegate(Activity activity, ViewGroup layout, QtActivityDelegate activityDelegate)102 public QtAccessibilityDelegate(Activity activity, ViewGroup layout, QtActivityDelegate activityDelegate) 103 { 104 m_activity = activity; 105 m_layout = layout; 106 m_activityDelegate = activityDelegate; 107 108 m_manager = (AccessibilityManager) m_activity.getSystemService(Context.ACCESSIBILITY_SERVICE); 109 if (m_manager != null) { 110 AccessibilityManagerListener accServiceListener = new AccessibilityManagerListener(); 111 if (!m_manager.addAccessibilityStateChangeListener(accServiceListener)) 112 Log.w("Qt A11y", "Could not register a11y state change listener"); 113 if (m_manager.isEnabled()) 114 accServiceListener.onAccessibilityStateChanged(true); 115 } 116 } 117 118 private class AccessibilityManagerListener implements AccessibilityManager.AccessibilityStateChangeListener 119 { 120 @Override onAccessibilityStateChanged(boolean enabled)121 public void onAccessibilityStateChanged(boolean enabled) 122 { 123 if (enabled) { 124 try { 125 View view = m_view; 126 if (view == null) { 127 view = new View(m_activity); 128 view.setId(View.NO_ID); 129 } 130 131 // ### Keep this for debugging for a while. It allows us to visually see that our View 132 // ### is on top of the surface(s) 133 // ColorDrawable color = new ColorDrawable(0x80ff8080); //0xAARRGGBB 134 // view.setBackground(color); 135 view.setAccessibilityDelegate(QtAccessibilityDelegate.this); 136 137 // if all is fine, add it to the layout 138 if (m_view == null) { 139 //m_layout.addAccessibilityView(view); 140 m_layout.addView(view, m_activityDelegate.getSurfaceCount(), 141 new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 142 } 143 m_view = view; 144 145 m_view.setOnHoverListener(new HoverEventListener()); 146 } catch (Exception e) { 147 // Unknown exception means something went wrong. 148 Log.w("Qt A11y", "Unknown exception: " + e.toString()); 149 } 150 } else { 151 if (m_view != null) { 152 m_layout.removeView(m_view); 153 m_view = null; 154 } 155 } 156 157 QtNativeAccessibility.setActive(enabled); 158 } 159 } 160 161 162 @Override getAccessibilityNodeProvider(View host)163 public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) 164 { 165 return m_nodeProvider; 166 } 167 168 // For "explore by touch" we need all movement events here first 169 // (user moves finger over screen to discover items on screen). dispatchHoverEvent(MotionEvent event)170 private boolean dispatchHoverEvent(MotionEvent event) 171 { 172 if (!m_manager.isTouchExplorationEnabled()) { 173 return false; 174 } 175 176 int virtualViewId = QtNativeAccessibility.hitTest(event.getX(), event.getY()); 177 if (virtualViewId == INVALID_ID) { 178 virtualViewId = View.NO_ID; 179 } 180 181 switch (event.getAction()) { 182 case MotionEvent.ACTION_HOVER_ENTER: 183 case MotionEvent.ACTION_HOVER_MOVE: 184 setHoveredVirtualViewId(virtualViewId); 185 break; 186 case MotionEvent.ACTION_HOVER_EXIT: 187 setHoveredVirtualViewId(virtualViewId); 188 break; 189 } 190 191 return true; 192 } 193 notifyLocationChange()194 public void notifyLocationChange() 195 { 196 invalidateVirtualViewId(m_focusedVirtualViewId); 197 } 198 notifyObjectHide(int viewId)199 public void notifyObjectHide(int viewId) 200 { 201 invalidateVirtualViewId(viewId); 202 } 203 notifyObjectFocus(int viewId)204 public void notifyObjectFocus(int viewId) 205 { 206 m_view.invalidate(); 207 sendEventForVirtualViewId(viewId, 208 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 209 } 210 sendEventForVirtualViewId(int virtualViewId, int eventType)211 public boolean sendEventForVirtualViewId(int virtualViewId, int eventType) 212 { 213 if ((virtualViewId == INVALID_ID) || !m_manager.isEnabled()) { 214 Log.w(TAG, "sendEventForVirtualViewId for invalid view"); 215 return false; 216 } 217 218 final ViewGroup group = (ViewGroup) m_view.getParent(); 219 if (group == null) { 220 Log.w(TAG, "Could not send AccessibilityEvent because group was null. This should really not happen."); 221 return false; 222 } 223 224 final AccessibilityEvent event; 225 event = getEventForVirtualViewId(virtualViewId, eventType); 226 return group.requestSendAccessibilityEvent(m_view, event); 227 } 228 invalidateVirtualViewId(int virtualViewId)229 public void invalidateVirtualViewId(int virtualViewId) 230 { 231 if (virtualViewId != INVALID_ID) 232 sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 233 } 234 setHoveredVirtualViewId(int virtualViewId)235 private void setHoveredVirtualViewId(int virtualViewId) 236 { 237 if (m_hoveredVirtualViewId == virtualViewId) { 238 return; 239 } 240 241 final int previousVirtualViewId = m_hoveredVirtualViewId; 242 m_hoveredVirtualViewId = virtualViewId; 243 sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 244 sendEventForVirtualViewId(previousVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 245 } 246 getEventForVirtualViewId(int virtualViewId, int eventType)247 private AccessibilityEvent getEventForVirtualViewId(int virtualViewId, int eventType) 248 { 249 final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 250 251 event.setEnabled(true); 252 event.setClassName(m_view.getClass().getName() + DEFAULT_CLASS_NAME); 253 254 event.setContentDescription(QtNativeAccessibility.descriptionForAccessibleObject(virtualViewId)); 255 if (event.getText().isEmpty() && TextUtils.isEmpty(event.getContentDescription())) 256 Log.w(TAG, "AccessibilityEvent with empty description"); 257 258 event.setPackageName(m_view.getContext().getPackageName()); 259 event.setSource(m_view, virtualViewId); 260 return event; 261 } 262 dumpNodes(int parentId)263 private void dumpNodes(int parentId) 264 { 265 Log.i(TAG, "A11Y hierarchy: " + parentId + " parent: " + QtNativeAccessibility.parentId(parentId)); 266 Log.i(TAG, " desc: " + QtNativeAccessibility.descriptionForAccessibleObject(parentId) + " rect: " + QtNativeAccessibility.screenRect(parentId)); 267 Log.i(TAG, " NODE: " + getNodeForVirtualViewId(parentId)); 268 int[] ids = QtNativeAccessibility.childIdListForAccessibleObject(parentId); 269 for (int i = 0; i < ids.length; ++i) { 270 Log.i(TAG, parentId + " has child: " + ids[i]); 271 dumpNodes(ids[i]); 272 } 273 } 274 getNodeForView()275 private AccessibilityNodeInfo getNodeForView() 276 { 277 // Since we don't want the parent to be focusable, but we can't remove 278 // actions from a node, copy over the necessary fields. 279 final AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(m_view); 280 final AccessibilityNodeInfo source = AccessibilityNodeInfo.obtain(m_view); 281 m_view.onInitializeAccessibilityNodeInfo(source); 282 283 // Get the actual position on screen, taking the status bar into account. 284 m_view.getLocationOnScreen(m_globalOffset); 285 final int offsetX = m_globalOffset[0]; 286 final int offsetY = m_globalOffset[1]; 287 288 // Copy over parent and screen bounds. 289 final Rect m_tempParentRect = new Rect(); 290 source.getBoundsInParent(m_tempParentRect); 291 result.setBoundsInParent(m_tempParentRect); 292 293 final Rect m_tempScreenRect = new Rect(); 294 source.getBoundsInScreen(m_tempScreenRect); 295 m_tempScreenRect.offset(offsetX, offsetY); 296 result.setBoundsInScreen(m_tempScreenRect); 297 298 // Set up the parent view, if applicable. 299 final ViewParent parent = m_view.getParent(); 300 if (parent instanceof View) { 301 result.setParent((View) parent); 302 } 303 304 result.setVisibleToUser(source.isVisibleToUser()); 305 result.setPackageName(source.getPackageName()); 306 result.setClassName(source.getClassName()); 307 308 // Spit out the entire hierarchy for debugging purposes 309 // dumpNodes(-1); 310 311 int[] ids = QtNativeAccessibility.childIdListForAccessibleObject(-1); 312 for (int i = 0; i < ids.length; ++i) 313 result.addChild(m_view, ids[i]); 314 315 return result; 316 } 317 getNodeForVirtualViewId(int virtualViewId)318 private AccessibilityNodeInfo getNodeForVirtualViewId(int virtualViewId) 319 { 320 final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(); 321 322 node.setClassName(m_view.getClass().getName() + DEFAULT_CLASS_NAME); 323 node.setPackageName(m_view.getContext().getPackageName()); 324 325 if (!QtNativeAccessibility.populateNode(virtualViewId, node)) 326 return node; 327 328 // set only if valid, otherwise we return a node that is invalid and will crash when accessed 329 node.setSource(m_view, virtualViewId); 330 331 if (TextUtils.isEmpty(node.getText()) && TextUtils.isEmpty(node.getContentDescription())) 332 Log.w(TAG, "AccessibilityNodeInfo with empty contentDescription: " + virtualViewId); 333 334 int parentId = QtNativeAccessibility.parentId(virtualViewId); 335 node.setParent(m_view, parentId); 336 337 Rect screenRect = QtNativeAccessibility.screenRect(virtualViewId); 338 final int offsetX = m_globalOffset[0]; 339 final int offsetY = m_globalOffset[1]; 340 screenRect.offset(offsetX, offsetY); 341 node.setBoundsInScreen(screenRect); 342 343 Rect rectInParent = screenRect; 344 Rect parentScreenRect = QtNativeAccessibility.screenRect(parentId); 345 rectInParent.offset(-parentScreenRect.left, -parentScreenRect.top); 346 node.setBoundsInParent(rectInParent); 347 348 // Manage internal accessibility focus state. 349 if (m_focusedVirtualViewId == virtualViewId) { 350 node.setAccessibilityFocused(true); 351 node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 352 } else { 353 node.setAccessibilityFocused(false); 354 node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); 355 } 356 357 return node; 358 } 359 360 private AccessibilityNodeProvider m_nodeProvider = new AccessibilityNodeProvider() 361 { 362 @Override 363 public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) 364 { 365 if (virtualViewId == View.NO_ID) { 366 return getNodeForView(); 367 } 368 return getNodeForVirtualViewId(virtualViewId); 369 } 370 371 @Override 372 public boolean performAction(int virtualViewId, int action, Bundle arguments) 373 { 374 boolean handled = false; 375 //Log.i(TAG, "PERFORM ACTION: " + action + " on " + virtualViewId); 376 switch (action) { 377 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: 378 // Only handle the FOCUS action if it's placing focus on 379 // a different view that was previously focused. 380 if (m_focusedVirtualViewId != virtualViewId) { 381 m_focusedVirtualViewId = virtualViewId; 382 m_view.invalidate(); 383 sendEventForVirtualViewId(virtualViewId, 384 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); 385 handled = true; 386 } 387 break; 388 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: 389 if (m_focusedVirtualViewId == virtualViewId) { 390 m_focusedVirtualViewId = INVALID_ID; 391 } 392 // Since we're managing focus at the parent level, we are 393 // likely to receive a FOCUS action before a CLEAR_FOCUS 394 // action. We'll give the benefit of the doubt to the 395 // framework and always handle FOCUS_CLEARED. 396 m_view.invalidate(); 397 sendEventForVirtualViewId(virtualViewId, 398 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); 399 handled = true; 400 break; 401 default: 402 // Let the node provider handle focus for the view node. 403 if (virtualViewId == View.NO_ID) { 404 return m_view.performAccessibilityAction(action, arguments); 405 } 406 } 407 handled |= performActionForVirtualViewId(virtualViewId, action, arguments); 408 409 return handled; 410 } 411 }; 412 performActionForVirtualViewId(int virtualViewId, int action, Bundle arguments)413 protected boolean performActionForVirtualViewId(int virtualViewId, int action, Bundle arguments) 414 { 415 // Log.i(TAG, "ACTION " + action + " on " + virtualViewId); 416 // dumpNodes(virtualViewId); 417 boolean success = false; 418 switch (action) { 419 case AccessibilityNodeInfo.ACTION_CLICK: 420 success = QtNativeAccessibility.clickAction(virtualViewId); 421 if (success) 422 sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED); 423 break; 424 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: 425 success = QtNativeAccessibility.scrollForward(virtualViewId); 426 if (success) 427 sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_VIEW_SCROLLED); 428 break; 429 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: 430 success = QtNativeAccessibility.scrollBackward(virtualViewId); 431 if (success) 432 sendEventForVirtualViewId(virtualViewId, AccessibilityEvent.TYPE_VIEW_SCROLLED); 433 break; 434 } 435 return success; 436 } 437 } 438