AccessibilityBridge.java 29.3 KB
Newer Older
H
Hixie 已提交
1 2 3 4
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
package io.flutter.view;
H
Hixie 已提交
6 7 8

import android.graphics.Rect;
import android.opengl.Matrix;
H
Hixie 已提交
9
import android.os.Bundle;
10
import android.util.Log;
H
Hixie 已提交
11
import android.view.View;
12
import android.view.accessibility.AccessibilityEvent;
H
Hixie 已提交
13 14
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
15
import io.flutter.plugin.common.BasicMessageChannel;
16
import io.flutter.plugin.common.StandardMessageCodec;
H
Hixie 已提交
17

18
import java.nio.ByteBuffer;
H
Hixie 已提交
19
import java.util.ArrayList;
20
import java.util.Arrays;
21 22
import java.util.Collections;
import java.util.Comparator;
H
Hixie 已提交
23
import java.util.HashMap;
24
import java.util.HashSet;
25
import java.util.Iterator;
H
Hixie 已提交
26 27
import java.util.List;
import java.util.Map;
28
import java.util.Set;
H
Hixie 已提交
29

30
class AccessibilityBridge extends AccessibilityNodeProvider implements BasicMessageChannel.MessageHandler<Object> {
31 32 33
    private static final String TAG = "FlutterView";

    private Map<Integer, SemanticsObject> mObjects;
34
    private final FlutterView mOwner;
35
    private boolean mAccessibilityEnabled = false;
36 37
    private SemanticsObject mA11yFocusedObject;
    private SemanticsObject mInputFocusedObject;
38 39
    private SemanticsObject mHoveredObject;

40 41
    private final BasicMessageChannel<Object> mFlutterAccessibilityChannel;

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
    enum Action {
        TAP(1 << 0),
        LONG_PRESS(1 << 1),
        SCROLL_LEFT(1 << 2),
        SCROLL_RIGHT(1 << 3),
        SCROLL_UP(1 << 4),
        SCROLL_DOWN(1 << 5),
        INCREASE(1 << 6),
        DECREASE(1 << 7),
        SHOW_ON_SCREEN(1 << 8),
        MOVE_CURSOR_FORWARD_BY_CHARACTER(1 << 9),
        MOVE_CURSOR_BACKWARD_BY_CHARACTER(1 << 10);

        Action(int value) {
            this.value = value;
        }

        final int value;
    }

    enum Flag {
        HAS_CHECKED_STATE(1 << 0),
        IS_CHECKED(1 << 1),
        IS_SELECTED(1 << 2),
        IS_BUTTON(1 << 3),
        IS_TEXT_FIELD(1 << 4),
        IS_FOCUSED(1 << 5);

        Flag(int value) {
            this.value = value;
        }

        final int value;
    }
76 77

    AccessibilityBridge(FlutterView owner) {
H
Hixie 已提交
78 79
        assert owner != null;
        mOwner = owner;
80
        mObjects = new HashMap<Integer, SemanticsObject>();
81
        mFlutterAccessibilityChannel = new BasicMessageChannel<>(owner, "flutter/accessibility",
82
            StandardMessageCodec.INSTANCE);
H
Hixie 已提交
83 84
    }

85
    void setAccessibilityEnabled(boolean accessibilityEnabled) {
86
        mAccessibilityEnabled = accessibilityEnabled;
87 88 89 90 91
        if (accessibilityEnabled) {
            mFlutterAccessibilityChannel.setMessageHandler(this);
        } else {
            mFlutterAccessibilityChannel.setMessageHandler(null);
        }
H
Hixie 已提交
92 93
    }

H
Hixie 已提交
94
    @Override
95
    @SuppressWarnings("deprecation")
H
Hixie 已提交
96 97 98 99
    public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
        if (virtualViewId == View.NO_ID) {
            AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mOwner);
            mOwner.onInitializeAccessibilityNodeInfo(result);
100
            if (mObjects.containsKey(0))
H
Hixie 已提交
101 102 103 104
                result.addChild(mOwner, 0);
            return result;
        }

105
        SemanticsObject object = mObjects.get(virtualViewId);
106
        if (object == null)
H
Hixie 已提交
107 108 109 110
            return null;

        AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(mOwner, virtualViewId);
        result.setPackageName(mOwner.getContext().getPackageName());
111
        result.setClassName("Flutter"); // TODO(goderbauer): Set proper class names
H
Hixie 已提交
112
        result.setSource(mOwner, virtualViewId);
113
        result.setFocusable(object.isFocusable());
114 115
        if (mInputFocusedObject != null)
            result.setFocused(mInputFocusedObject.id == virtualViewId);
116

117 118
        if (mA11yFocusedObject != null)
            result.setAccessibilityFocused(mA11yFocusedObject.id == virtualViewId);
H
Hixie 已提交
119

120
        if (object.hasFlag(Flag.IS_TEXT_FIELD)) {
121
            result.setClassName("android.widget.EditText");
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
            result.setEditable(true);

            // Cursor movements
            int granularities = 0;
            if (object.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) {
                result.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
                granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER;
            }
            if (object.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) {
                result.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
                granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER;
            }
            result.setMovementGranularities(granularities);
        }

        if (object.hasFlag(Flag.IS_BUTTON)) {
          result.setClassName("android.widget.Button");
        }
140

141 142 143
        if (object.parent != null) {
            assert object.id > 0;
            result.setParent(mOwner, object.parent.id);
H
Hixie 已提交
144
        } else {
145
            assert object.id == 0;
H
Hixie 已提交
146 147 148
            result.setParent(mOwner);
        }

149 150 151
        Rect bounds = object.getGlobalRect();
        if (object.parent != null) {
            Rect parentBounds = object.parent.getGlobalRect();
H
Hixie 已提交
152 153 154 155 156 157 158 159
            Rect boundsInParent = new Rect(bounds);
            boundsInParent.offset(-parentBounds.left, -parentBounds.top);
            result.setBoundsInParent(boundsInParent);
        } else {
            result.setBoundsInParent(bounds);
        }
        result.setBoundsInScreen(bounds);
        result.setVisibleToUser(true);
H
Hixie 已提交
160
        result.setEnabled(true); // TODO(ianh): Expose disabled subtrees
H
Hixie 已提交
161

162
        if (object.hasAction(Action.TAP)) {
163
            result.addAction(AccessibilityNodeInfo.ACTION_CLICK);
H
Hixie 已提交
164 165
            result.setClickable(true);
        }
166
        if (object.hasAction(Action.LONG_PRESS)) {
167
            result.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
H
Hixie 已提交
168 169
            result.setLongClickable(true);
        }
170 171
        if (object.hasAction(Action.SCROLL_LEFT) || object.hasAction(Action.SCROLL_UP)
                || object.hasAction(Action.SCROLL_RIGHT) || object.hasAction(Action.SCROLL_DOWN)) {
H
Hixie 已提交
172
            result.setScrollable(true);
173 174 175
            // This tells Android's a11y to send scroll events when reaching the end of
            // the visible viewport of a scrollable.
            result.setClassName("android.widget.ScrollView");
176 177 178
            // TODO(ianh): Once we're on SDK v23+, call addAction to
            // expose AccessibilityAction.ACTION_SCROLL_LEFT, _RIGHT,
            // _UP, and _DOWN when appropriate.
179
            if (object.hasAction(Action.SCROLL_LEFT) || object.hasAction(Action.SCROLL_UP)) {
180 181
                result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
            }
182
            if (object.hasAction(Action.SCROLL_RIGHT) || object.hasAction(Action.SCROLL_DOWN)) {
183 184
                result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
            }
H
Hixie 已提交
185
        }
186
        if (object.hasAction(Action.INCREASE) || object.hasAction(Action.DECREASE)) {
187
            result.setClassName("android.widget.SeekBar");
188
            if (object.hasAction(Action.INCREASE)) {
189 190
                result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
            }
191
            if (object.hasAction(Action.DECREASE)) {
192 193 194
                result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
            }
        }
H
Hixie 已提交
195

196 197 198
        result.setCheckable(object.hasFlag(Flag.HAS_CHECKED_STATE));
        result.setChecked(object.hasFlag(Flag.IS_CHECKED));
        result.setSelected(object.hasFlag(Flag.IS_SELECTED));
199
        result.setText(object.getValueLabelHint());
H
Hixie 已提交
200

H
Hixie 已提交
201
        // Accessibility Focus
202
        if (mA11yFocusedObject != null && mA11yFocusedObject.id == virtualViewId) {
203
            result.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
H
Hixie 已提交
204
        } else {
205
            result.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
H
Hixie 已提交
206 207
        }

208
        if (object.children != null) {
209 210 211 212 213 214 215 216 217 218
            List<SemanticsObject> childrenInTraversalOrder =
                new ArrayList<SemanticsObject>(object.children);
            Collections.sort(childrenInTraversalOrder, new Comparator<SemanticsObject>() {
                public int compare(SemanticsObject a, SemanticsObject b) {
                    final int top = Integer.compare(a.globalRect.top, b.globalRect.top);
                    // TODO(goderbauer): sort right-to-left in rtl environments.
                    return top == 0 ? Integer.compare(a.globalRect.left, b.globalRect.left) : top;
                }
            });
            for (SemanticsObject child : childrenInTraversalOrder) {
219 220
                result.addChild(mOwner, child.id);
            }
H
Hixie 已提交
221 222 223 224 225
        }

        return result;
    }

H
Hixie 已提交
226 227
    @Override
    public boolean performAction(int virtualViewId, int action, Bundle arguments) {
228
        SemanticsObject object = mObjects.get(virtualViewId);
229
        if (object == null) {
H
Hixie 已提交
230
            return false;
231
        }
H
Hixie 已提交
232 233
        switch (action) {
            case AccessibilityNodeInfo.ACTION_CLICK: {
234
                mOwner.dispatchSemanticsAction(virtualViewId, Action.TAP);
H
Hixie 已提交
235 236 237
                return true;
            }
            case AccessibilityNodeInfo.ACTION_LONG_CLICK: {
238
                mOwner.dispatchSemanticsAction(virtualViewId, Action.LONG_PRESS);
H
Hixie 已提交
239 240
                return true;
            }
241
            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
242 243 244
                if (object.hasAction(Action.SCROLL_UP)) {
                    mOwner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_UP);
                } else if (object.hasAction(Action.SCROLL_LEFT)) {
245
                    // TODO(ianh): bidi support using textDirection
246 247
                    mOwner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_LEFT);
                } else if (object.hasAction(Action.INCREASE)) {
248 249 250
                    object.value = object.increasedValue;
                    // Event causes Android to read out the updated value.
                    sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
251
                    mOwner.dispatchSemanticsAction(virtualViewId, Action.INCREASE);
H
Hixie 已提交
252 253 254 255 256
                } else {
                    return false;
                }
                return true;
            }
257
            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
258 259 260
                if (object.hasAction(Action.SCROLL_DOWN)) {
                    mOwner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_DOWN);
                } else if (object.hasAction(Action.SCROLL_RIGHT)) {
261
                    // TODO(ianh): bidi support using textDirection
262 263
                    mOwner.dispatchSemanticsAction(virtualViewId, Action.SCROLL_RIGHT);
                } else if (object.hasAction(Action.DECREASE)) {
264 265 266
                    object.value = object.decreasedValue;
                    // Event causes Android to read out the updated value.
                    sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
267
                    mOwner.dispatchSemanticsAction(virtualViewId, Action.DECREASE);
H
Hixie 已提交
268 269 270 271 272
                } else {
                    return false;
                }
                return true;
            }
273 274 275 276 277 278
            case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: {
                return performCursorMoveAction(object, virtualViewId, arguments, false);
            }
            case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: {
                return performCursorMoveAction(object, virtualViewId, arguments, true);
            }
H
Hixie 已提交
279 280
            case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
                sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
281
                mA11yFocusedObject = null;
H
Hixie 已提交
282 283 284 285
                return true;
            }
            case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
                sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
286

287
                if (mA11yFocusedObject == null) {
H
Hixie 已提交
288 289 290 291 292
                    // When Android focuses a node, it doesn't invalidate the view.
                    // (It does when it sends ACTION_CLEAR_ACCESSIBILITY_FOCUS, so
                    // we only have to worry about this when the focused node is null.)
                    mOwner.invalidate();
                }
293
                mA11yFocusedObject = object;
294

295
                if (object.hasAction(Action.INCREASE) || object.hasAction(Action.DECREASE)) {
296 297 298 299
                    // SeekBars only announce themselves after this event.
                    sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
                }

H
Hixie 已提交
300 301
                return true;
            }
302 303 304
            // TODO(goderbauer): Use ACTION_SHOW_ON_SCREEN from Android Support Library after
            //     https://github.com/flutter/flutter/issues/11099 is resolved.
            case 16908342: { // ACTION_SHOW_ON_SCREEN, added in API level 23
305
                mOwner.dispatchSemanticsAction(virtualViewId, Action.SHOW_ON_SCREEN);
306 307
                return true;
            }
H
Hixie 已提交
308 309 310 311
        }
        return false;
    }

312 313 314
    boolean performCursorMoveAction(SemanticsObject object, int virtualViewId, Bundle arguments, boolean forward) {
        final int granularity = arguments.getInt(
            AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
315 316 317
        // TODO(goderbauer): support extending selections.
        // final boolean extendSelection = arguments.getBoolean(
        //     AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
318 319 320
        switch (granularity) {
            case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER: {
                if (forward && object.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) {
321
                    mOwner.dispatchSemanticsAction(virtualViewId, Action.MOVE_CURSOR_FORWARD_BY_CHARACTER);
322 323 324
                    return true;
                }
                if (!forward && object.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) {
325
                    mOwner.dispatchSemanticsAction(virtualViewId, Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER);
326 327 328 329 330 331 332 333
                    return true;
                }
            }
            // TODO(goderbauer): support other granularities.
        }
        return false;
    }

334
    // TODO(ianh): implement findAccessibilityNodeInfosByText()
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350

    @Override
    public AccessibilityNodeInfo findFocus(int focus) {
        switch (focus) {
            case AccessibilityNodeInfo.FOCUS_INPUT: {
                if (mInputFocusedObject != null)
                    return createAccessibilityNodeInfo(mInputFocusedObject.id);
            }
            case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: {
                if (mA11yFocusedObject != null)
                    return createAccessibilityNodeInfo(mA11yFocusedObject.id);
            }
        }
        return null;
    }

351

352 353
    private SemanticsObject getRootObject() {
      assert mObjects.containsKey(0);
354 355 356
      return mObjects.get(0);
    }

357 358 359 360 361 362 363 364 365 366
    private SemanticsObject getOrCreateObject(int id) {
      SemanticsObject object = mObjects.get(id);
      if (object == null) {
          object = new SemanticsObject();
          object.id = id;
          mObjects.put(id, object);
      }
      return object;
    }

367
    void handleTouchExplorationExit() {
368 369 370
        if (mHoveredObject != null) {
            sendAccessibilityEvent(mHoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
            mHoveredObject = null;
371 372 373
        }
    }

374
    void handleTouchExploration(float x, float y) {
375
        if (mObjects.isEmpty()) {
376
            return;
377
        }
378
        SemanticsObject newObject = getRootObject().hitTest(new float[]{ x, y, 0, 1 });
379
        if (newObject != mHoveredObject) {
H
Hixie 已提交
380
            // sending ENTER before EXIT is how Android wants it
381 382
            if (newObject != null) {
                sendAccessibilityEvent(newObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
383
            }
384 385
            if (mHoveredObject != null) {
                sendAccessibilityEvent(mHoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
386
            }
387
            mHoveredObject = newObject;
388 389 390
        }
    }

391 392 393 394
    void updateSemantics(ByteBuffer buffer, String[] strings) {
        ArrayList<Integer> updated = new ArrayList<Integer>();
        while (buffer.hasRemaining()) {
            int id = buffer.getInt();
395 396 397 398 399
            SemanticsObject object = getOrCreateObject(id);
            object.updateWith(buffer, strings);
            if (object.hasFlag(Flag.IS_FOCUSED)) {
                mInputFocusedObject = object;
            }
400
            updated.add(id);
401
        }
402

403 404 405 406 407 408
        Set<SemanticsObject> visitedObjects = new HashSet<SemanticsObject>();
        SemanticsObject rootObject = getRootObject();
        if (rootObject != null) {
          final float[] identity = new float[16];
          Matrix.setIdentityM(identity, 0);
          rootObject.updateRecursively(identity, visitedObjects, false);
409
        }
410 411 412 413

        Iterator<Map.Entry<Integer, SemanticsObject>> it = mObjects.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<Integer, SemanticsObject> entry = it.next();
A
Adam Barth 已提交
414 415 416
            SemanticsObject object = entry.getValue();
            if (!visitedObjects.contains(object)) {
                willRemoveSemanticsObject(object);
417
                it.remove();
418 419
            }
        }
420 421 422

        for (Integer id : updated) {
            sendAccessibilityEvent(id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
423
        }
424 425
    }

426 427 428 429 430 431 432 433
    private AccessibilityEvent obtainAccessibilityEvent(int virtualViewId, int eventType) {
        assert virtualViewId != 0;
        AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
        event.setPackageName(mOwner.getContext().getPackageName());
        event.setSource(mOwner, virtualViewId);
        return event;
    }

434
    private void sendAccessibilityEvent(int virtualViewId, int eventType) {
435
        if (!mAccessibilityEnabled) {
H
Hixie 已提交
436 437
            return;
        }
438 439 440
        if (virtualViewId == 0) {
            mOwner.sendAccessibilityEvent(eventType);
        } else {
441 442 443 444 445 446 447
            sendAccessibilityEvent(obtainAccessibilityEvent(virtualViewId, eventType));
        }
    }

    private void sendAccessibilityEvent(AccessibilityEvent event) {
        if (!mAccessibilityEnabled) {
            return;
H
Hixie 已提交
448
        }
449
        mOwner.getParent().requestSendAccessibilityEvent(mOwner, event);
H
Hixie 已提交
450 451
    }

452 453 454
    // Message Handler for [mFlutterAccessibilityChannel].
    public void onMessage(Object message, BasicMessageChannel.Reply<Object> reply) {
        @SuppressWarnings("unchecked")
455 456
        final HashMap<String, Object> annotatedEvent = (HashMap<String, Object>)message;
        final String type = (String)annotatedEvent.get("type");
457 458
        @SuppressWarnings("unchecked")
        final HashMap<String, Object> data = (HashMap<String, Object>)annotatedEvent.get("data");
459 460 461

        switch (type) {
            case "scroll":
462
                final int nodeId = (int)annotatedEvent.get("nodeId");
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
                AccessibilityEvent event =
                    obtainAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_SCROLLED);
                char axis = ((String)data.get("axis")).charAt(0);
                double minPosition = (double)data.get("minScrollExtent");
                double maxPosition = (double)data.get("maxScrollExtent") - minPosition;
                double position = (double)data.get("pixels") - minPosition;
                if (axis == 'v') {
                    event.setScrollY((int)position);
                    event.setMaxScrollY((int)maxPosition);
                } else {
                    assert axis == 'h';
                    event.setScrollX((int)position);
                    event.setMaxScrollX((int)maxPosition);
                }
                sendAccessibilityEvent(event);
478
                break;
479 480 481
            case "announce":
                mOwner.announceForAccessibility((String) data.get("message"));
                break;
482 483 484
            default:
                assert false;
        }
485 486
    }

487
    private void willRemoveSemanticsObject(SemanticsObject object) {
488 489 490
        assert mObjects.containsKey(object.id);
        assert mObjects.get(object.id) == object;
        object.parent = null;
491 492 493 494 495 496
        if (mA11yFocusedObject == object) {
            sendAccessibilityEvent(mA11yFocusedObject.id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
            mA11yFocusedObject = null;
        }
        if (mInputFocusedObject == object) {
            mInputFocusedObject = null;
H
Hixie 已提交
497
        }
498 499
        if (mHoveredObject == object) {
            mHoveredObject = null;
500
        }
H
Hixie 已提交
501 502
    }

503
    void reset() {
504
        mObjects.clear();
505 506 507
        if (mA11yFocusedObject != null)
            sendAccessibilityEvent(mA11yFocusedObject.id, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
        mA11yFocusedObject = null;
508
        mHoveredObject = null;
509
        sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
H
Hixie 已提交
510 511
    }

512 513 514 515 516 517 518 519 520 521 522 523 524 525
    private enum TextDirection {
        UNKNOWN, LTR, RTL;

        public static TextDirection fromInt(int value) {
            switch (value) {
                case 1:
                    return RTL;
                case 2:
                    return LTR;
            }
            return UNKNOWN;
        }
    }

526 527
    private class SemanticsObject {
        SemanticsObject() { }
528

529
        int id = -1;
530 531

        int flags;
A
Adam Barth 已提交
532
        int actions;
533
        String label;
534
        String value;
535 536
        String increasedValue;
        String decreasedValue;
537
        String hint;
538
        TextDirection textDirection;
H
Hixie 已提交
539 540 541

        private float left;
        private float top;
542 543 544
        private float right;
        private float bottom;
        private float[] transform;
H
Hixie 已提交
545

546
        SemanticsObject parent;
547
        List<SemanticsObject> children;  // In inverse hit test order (i.e. paint order).
H
Hixie 已提交
548

549 550
        private boolean inverseTransformDirty = true;
        private float[] inverseTransform;
H
Hixie 已提交
551

552 553 554
        private boolean globalGeometryDirty = true;
        private float[] globalTransform;
        private Rect globalRect;
H
Hixie 已提交
555

556 557 558 559 560 561 562 563
        boolean hasAction(Action action) {
            return (actions & action.value) != 0;
        }

        boolean hasFlag(Flag flag) {
            return (flags & flag.value) != 0;
        }

A
Adam Barth 已提交
564 565 566 567 568 569 570 571 572 573 574 575
        void log(String indent) {
          Log.i(TAG, indent + "SemanticsObject id=" + id + " label=" + label + " actions=" +  actions + " flags=" + flags + "\n" +
                     indent + "  +-- rect.ltrb=(" + left + ", " + top + ", " + right + ", " + bottom + ")\n" +
                     indent + "  +-- transform=" + Arrays.toString(transform) + "\n");
          if (children != null) {
              String childIndent = indent + "  ";
              for (SemanticsObject child : children) {
                  child.log(childIndent);
              }
          }
        }

576 577
        void updateWith(ByteBuffer buffer, String[] strings) {
            flags = buffer.getInt();
A
Adam Barth 已提交
578
            actions = buffer.getInt();
H
Hixie 已提交
579

580 581 582 583 584 585
            int stringIndex = buffer.getInt();
            label = stringIndex == -1 ? null : strings[stringIndex];

            stringIndex = buffer.getInt();
            value = stringIndex == -1 ? null : strings[stringIndex];

586 587 588 589 590 591
            stringIndex = buffer.getInt();
            increasedValue = stringIndex == -1 ? null : strings[stringIndex];

            stringIndex = buffer.getInt();
            decreasedValue = stringIndex == -1 ? null : strings[stringIndex];

592 593
            stringIndex = buffer.getInt();
            hint = stringIndex == -1 ? null : strings[stringIndex];
594

595 596
            textDirection = TextDirection.fromInt(buffer.getInt());

597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620
            left = buffer.getFloat();
            top = buffer.getFloat();
            right = buffer.getFloat();
            bottom = buffer.getFloat();

            if (transform == null)
                transform = new float[16];
            for (int i = 0; i < 16; ++i)
                transform[i] = buffer.getFloat();
            inverseTransformDirty = true;
            globalGeometryDirty = true;

            final int childCount = buffer.getInt();
            if (childCount == 0) {
                children = null;
            } else {
                if (children == null)
                    children = new ArrayList<SemanticsObject>(childCount);
                else
                    children.clear();

                for (int i = 0; i < childCount; ++i) {
                    SemanticsObject child = getOrCreateObject(buffer.getInt());
                    child.parent = this;
A
Adam Barth 已提交
621
                    children.add(child);
622 623
                }
            }
H
Hixie 已提交
624 625
        }

626 627 628 629 630 631 632 633
        private void ensureInverseTransform() {
            if (!inverseTransformDirty)
                return;
            inverseTransformDirty = false;
            if (inverseTransform == null)
                inverseTransform = new float[16];
            if (!Matrix.invertM(inverseTransform, 0, transform, 0))
                Arrays.fill(inverseTransform, 0);
H
Hixie 已提交
634 635
        }

636
        Rect getGlobalRect() {
637
            assert !globalGeometryDirty;
H
Hixie 已提交
638 639
            return globalRect;
        }
640

641 642 643 644 645
        SemanticsObject hitTest(float[] point) {
            final float w = point[3];
            final float x = point[0] / w;
            final float y = point[1] / w;
            if (x < left || x >= right || y < top || y >= bottom)
646
                return null;
647
            if (children != null) {
648 649 650 651 652 653
                final float[] transformedPoint = new float[4];
                for (int i = children.size() - 1; i >= 0; i -= 1) {
                    final SemanticsObject child = children.get(i);
                    child.ensureInverseTransform();
                    Matrix.multiplyMV(transformedPoint, 0, child.inverseTransform, 0, point, 0);
                    final SemanticsObject result = child.hitTest(transformedPoint);
654 655 656
                    if (result != null) {
                        return result;
                    }
657 658 659 660
                }
            }
            return this;
        }
H
Hixie 已提交
661

662 663
        // TODO(goderbauer): This should be decided by the framework once we have more information
        //     about focusability there.
664
        boolean isFocusable() {
665 666 667
            int scrollableActions = Action.SCROLL_RIGHT.value | Action.SCROLL_LEFT.value
                    | Action.SCROLL_UP.value | Action.SCROLL_DOWN.value;
            return flags != 0 || (label != null && !label.isEmpty()) || (actions & ~scrollableActions) != 0;
668 669
        }

670 671
        void updateRecursively(float[] ancestorTransform, Set<SemanticsObject> visitedObjects, boolean forceUpdate) {
            visitedObjects.add(this);
H
Hixie 已提交
672

673 674 675 676 677 678
            if (globalGeometryDirty)
                forceUpdate = true;

            if (forceUpdate) {
                if (globalTransform == null)
                    globalTransform = new float[16];
I
Ian Hickson 已提交
679
                Matrix.multiplyMM(globalTransform, 0, ancestorTransform, 0, transform, 0);
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700

                final float[] sample = new float[4];
                sample[2] = 0;
                sample[3] = 1;

                final float[] point1 = new float[4];
                final float[] point2 = new float[4];
                final float[] point3 = new float[4];
                final float[] point4 = new float[4];

                sample[0] = left;
                sample[1] = top;
                transformPoint(point1, globalTransform, sample);

                sample[0] = right;
                sample[1] = top;
                transformPoint(point2, globalTransform, sample);

                sample[0] = right;
                sample[1] = bottom;
                transformPoint(point3, globalTransform, sample);
H
Hixie 已提交
701

702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
                sample[0] = left;
                sample[1] = bottom;
                transformPoint(point4, globalTransform, sample);

                if (globalRect == null)
                    globalRect = new Rect();

                globalRect.set(
                    Math.round(min(point1[0], point2[0], point3[0], point4[0])),
                    Math.round(min(point1[1], point2[1], point3[1], point4[1])),
                    Math.round(max(point1[0], point2[0], point3[0], point4[0])),
                    Math.round(max(point1[1], point2[1], point3[1], point4[1]))
                );

                globalGeometryDirty = false;
            }

            assert globalTransform != null;
            assert globalRect != null;

            if (children != null) {
                for (int i = 0; i < children.size(); ++i) {
                    children.get(i).updateRecursively(globalTransform, visitedObjects, forceUpdate);
                }
            }
        }

        private void transformPoint(float[] result, float[] transform, float[] point) {
            Matrix.multiplyMV(result, 0, transform, 0, point, 0);
            final float w = result[3];
            result[0] /= w;
            result[1] /= w;
            result[2] /= w;
            result[3] = 0;
        }

        private float min(float a, float b, float c, float d) {
            return Math.min(a, Math.min(b, Math.min(c, d)));
        }

        private float max(float a, float b, float c, float d) {
            return Math.max(a, Math.max(b, Math.max(c, d)));
        }
745 746 747 748 749 750 751 752 753 754 755 756 757

        private String getValueLabelHint() {
            StringBuilder sb = new StringBuilder();
            String[] array = { value, label, hint };
            for (String word: array) {
                if (word != null && (word = word.trim()).length() > 0) {
                    if (sb.length() > 0)
                        sb.append(", ");
                    sb.append(word);
                }
            }
            return sb.length() > 0 ? sb.toString() : null;
        }
758
    }
H
Hixie 已提交
759
}