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