1/*
2 * Copyright (C) 2014 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
17package android.view.accessibility;
18
19import android.annotation.Nullable;
20import android.annotation.TestApi;
21import android.graphics.Rect;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.util.LongArray;
25import android.util.Pools.SynchronizedPool;
26
27import java.util.concurrent.atomic.AtomicInteger;
28
29/**
30 * This class represents a state snapshot of a window for accessibility
31 * purposes. The screen content contains one or more windows where some
32 * windows can be descendants of other windows, which is the windows are
33 * hierarchically ordered. Note that there is no root window. Hence, the
34 * screen content can be seen as a collection of window trees.
35 */
36public final class AccessibilityWindowInfo implements Parcelable {
37
38    private static final boolean DEBUG = false;
39
40    /**
41     * Window type: This is an application window. Such a window shows UI for
42     * interacting with an application.
43     */
44    public static final int TYPE_APPLICATION = 1;
45
46    /**
47     * Window type: This is an input method window. Such a window shows UI for
48     * inputting text such as keyboard, suggestions, etc.
49     */
50    public static final int TYPE_INPUT_METHOD = 2;
51
52    /**
53     * Window type: This is an system window. Such a window shows UI for
54     * interacting with the system.
55     */
56    public static final int TYPE_SYSTEM = 3;
57
58    /**
59     * Window type: Windows that are overlaid <em>only</em> by an {@link
60     * android.accessibilityservice.AccessibilityService} for interception of
61     * user interactions without changing the windows an accessibility service
62     * can introspect. In particular, an accessibility service can introspect
63     * only windows that a sighted user can interact with which they can touch
64     * these windows or can type into these windows. For example, if there
65     * is a full screen accessibility overlay that is touchable, the windows
66     * below it will be introspectable by an accessibility service regardless
67     * they are covered by a touchable window.
68     */
69    public static final int TYPE_ACCESSIBILITY_OVERLAY = 4;
70
71    /**
72     * Window type: A system window used to divide the screen in split-screen mode.
73     * This type of window is present only in split-screen mode.
74     */
75    public static final int TYPE_SPLIT_SCREEN_DIVIDER = 5;
76
77    /* Special values for window IDs */
78    /** @hide */
79    public static final int ACTIVE_WINDOW_ID = Integer.MAX_VALUE;
80    /** @hide */
81    public static final int UNDEFINED_WINDOW_ID = -1;
82    /** @hide */
83    public static final int ANY_WINDOW_ID = -2;
84    /** @hide */
85    public static final int PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID = -3;
86
87    private static final int BOOLEAN_PROPERTY_ACTIVE = 1 << 0;
88    private static final int BOOLEAN_PROPERTY_FOCUSED = 1 << 1;
89    private static final int BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED = 1 << 2;
90
91    // Housekeeping.
92    private static final int MAX_POOL_SIZE = 10;
93    private static final SynchronizedPool<AccessibilityWindowInfo> sPool =
94            new SynchronizedPool<AccessibilityWindowInfo>(MAX_POOL_SIZE);
95    private static AtomicInteger sNumInstancesInUse;
96
97    // Data.
98    private int mType = UNDEFINED_WINDOW_ID;
99    private int mLayer = UNDEFINED_WINDOW_ID;
100    private int mBooleanProperties;
101    private int mId = UNDEFINED_WINDOW_ID;
102    private int mParentId = UNDEFINED_WINDOW_ID;
103    private final Rect mBoundsInScreen = new Rect();
104    private LongArray mChildIds;
105    private CharSequence mTitle;
106    private int mAnchorId = UNDEFINED_WINDOW_ID;
107    private boolean mInPictureInPicture;
108
109    private int mConnectionId = UNDEFINED_WINDOW_ID;
110
111    private AccessibilityWindowInfo() {
112        /* do nothing - hide constructor */
113    }
114
115    /**
116     * Gets the title of the window.
117     *
118     * @return The title of the window, or {@code null} if none is available.
119     */
120    @Nullable
121    public CharSequence getTitle() {
122        return mTitle;
123    }
124
125    /**
126     * Sets the title of the window.
127     *
128     * @param title The title.
129     *
130     * @hide
131     */
132    public void setTitle(CharSequence title) {
133        mTitle = title;
134    }
135
136    /**
137     * Gets the type of the window.
138     *
139     * @return The type.
140     *
141     * @see #TYPE_APPLICATION
142     * @see #TYPE_INPUT_METHOD
143     * @see #TYPE_SYSTEM
144     * @see #TYPE_ACCESSIBILITY_OVERLAY
145     */
146    public int getType() {
147        return mType;
148    }
149
150    /**
151     * Sets the type of the window.
152     *
153     * @param type The type
154     *
155     * @hide
156     */
157    public void setType(int type) {
158        mType = type;
159    }
160
161    /**
162     * Gets the layer which determines the Z-order of the window. Windows
163     * with greater layer appear on top of windows with lesser layer.
164     *
165     * @return The window layer.
166     */
167    public int getLayer() {
168        return mLayer;
169    }
170
171    /**
172     * Sets the layer which determines the Z-order of the window. Windows
173     * with greater layer appear on top of windows with lesser layer.
174     *
175     * @param layer The window layer.
176     *
177     * @hide
178     */
179    public void setLayer(int layer) {
180        mLayer = layer;
181    }
182
183    /**
184     * Gets the root node in the window's hierarchy.
185     *
186     * @return The root node.
187     */
188    public AccessibilityNodeInfo getRoot() {
189        if (mConnectionId == UNDEFINED_WINDOW_ID) {
190            return null;
191        }
192        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
193        return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
194                mId, AccessibilityNodeInfo.ROOT_NODE_ID,
195                true, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS, null);
196    }
197
198    /**
199     * Sets the anchor node's ID.
200     *
201     * @param anchorId The anchor's accessibility id in its window.
202     *
203     * @hide
204     */
205    public void setAnchorId(int anchorId) {
206        mAnchorId = anchorId;
207    }
208
209    /**
210     * Gets the node that anchors this window to another.
211     *
212     * @return The anchor node, or {@code null} if none exists.
213     */
214    public AccessibilityNodeInfo getAnchor() {
215        if ((mConnectionId == UNDEFINED_WINDOW_ID) || (mAnchorId == UNDEFINED_WINDOW_ID)
216                || (mParentId == UNDEFINED_WINDOW_ID)) {
217            return null;
218        }
219
220        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
221        return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
222                mParentId, mAnchorId, true, 0, null);
223    }
224
225    /** @hide */
226    public void setPictureInPicture(boolean pictureInPicture) {
227        mInPictureInPicture = pictureInPicture;
228    }
229
230    /**
231     * Check if the window is in picture-in-picture mode.
232     *
233     * @return {@code true} if the window is in picture-in-picture mode, {@code false} otherwise.
234     * @removed
235     */
236    public boolean inPictureInPicture() {
237        return isInPictureInPictureMode();
238    }
239
240    /**
241     * Check if the window is in picture-in-picture mode.
242     *
243     * @return {@code true} if the window is in picture-in-picture mode, {@code false} otherwise.
244     */
245    public boolean isInPictureInPictureMode() {
246        return mInPictureInPicture;
247    }
248
249    /**
250     * Gets the parent window.
251     *
252     * @return The parent window, or {@code null} if none exists.
253     */
254    public AccessibilityWindowInfo getParent() {
255        if (mConnectionId == UNDEFINED_WINDOW_ID || mParentId == UNDEFINED_WINDOW_ID) {
256            return null;
257        }
258        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
259        return client.getWindow(mConnectionId, mParentId);
260    }
261
262    /**
263     * Sets the parent window id.
264     *
265     * @param parentId The parent id.
266     *
267     * @hide
268     */
269    public void setParentId(int parentId) {
270        mParentId = parentId;
271    }
272
273    /**
274     * Gets the unique window id.
275     *
276     * @return windowId The window id.
277     */
278    public int getId() {
279        return mId;
280    }
281
282    /**
283     * Sets the unique window id.
284     *
285     * @param id The window id.
286     *
287     * @hide
288     */
289    public void setId(int id) {
290        mId = id;
291    }
292
293    /**
294     * Sets the unique id of the IAccessibilityServiceConnection over which
295     * this instance can send requests to the system.
296     *
297     * @param connectionId The connection id.
298     *
299     * @hide
300     */
301    public void setConnectionId(int connectionId) {
302        mConnectionId = connectionId;
303    }
304
305    /**
306     * Gets the bounds of this window in the screen.
307     *
308     * @param outBounds The out window bounds.
309     */
310    public void getBoundsInScreen(Rect outBounds) {
311        outBounds.set(mBoundsInScreen);
312    }
313
314    /**
315     * Sets the bounds of this window in the screen.
316     *
317     * @param bounds The out window bounds.
318     *
319     * @hide
320     */
321    public void setBoundsInScreen(Rect bounds) {
322        mBoundsInScreen.set(bounds);
323    }
324
325    /**
326     * Gets if this window is active. An active window is the one
327     * the user is currently touching or the window has input focus
328     * and the user is not touching any window.
329     *
330     * @return Whether this is the active window.
331     */
332    public boolean isActive() {
333        return getBooleanProperty(BOOLEAN_PROPERTY_ACTIVE);
334    }
335
336    /**
337     * Sets if this window is active, which is this is the window
338     * the user is currently touching or the window has input focus
339     * and the user is not touching any window.
340     *
341     * @param active Whether this is the active window.
342     *
343     * @hide
344     */
345    public void setActive(boolean active) {
346        setBooleanProperty(BOOLEAN_PROPERTY_ACTIVE, active);
347    }
348
349    /**
350     * Gets if this window has input focus.
351     *
352     * @return Whether has input focus.
353     */
354    public boolean isFocused() {
355        return getBooleanProperty(BOOLEAN_PROPERTY_FOCUSED);
356    }
357
358    /**
359     * Sets if this window has input focus.
360     *
361     * @param focused Whether has input focus.
362     *
363     * @hide
364     */
365    public void setFocused(boolean focused) {
366        setBooleanProperty(BOOLEAN_PROPERTY_FOCUSED, focused);
367    }
368
369    /**
370     * Gets if this window has accessibility focus.
371     *
372     * @return Whether has accessibility focus.
373     */
374    public boolean isAccessibilityFocused() {
375        return getBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED);
376    }
377
378    /**
379     * Sets if this window has accessibility focus.
380     *
381     * @param focused Whether has accessibility focus.
382     *
383     * @hide
384     */
385    public void setAccessibilityFocused(boolean focused) {
386        setBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED, focused);
387    }
388
389    /**
390     * Gets the number of child windows.
391     *
392     * @return The child count.
393     */
394    public int getChildCount() {
395        return (mChildIds != null) ? mChildIds.size() : 0;
396    }
397
398    /**
399     * Gets the child window at a given index.
400     *
401     * @param index The index.
402     * @return The child.
403     */
404    public AccessibilityWindowInfo getChild(int index) {
405        if (mChildIds == null) {
406            throw new IndexOutOfBoundsException();
407        }
408        if (mConnectionId == UNDEFINED_WINDOW_ID) {
409            return null;
410        }
411        final int childId = (int) mChildIds.get(index);
412        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
413        return client.getWindow(mConnectionId, childId);
414    }
415
416    /**
417     * Adds a child window.
418     *
419     * @param childId The child window id.
420     *
421     * @hide
422     */
423    public void addChild(int childId) {
424        if (mChildIds == null) {
425            mChildIds = new LongArray();
426        }
427        mChildIds.add(childId);
428    }
429
430    /**
431     * Returns a cached instance if such is available or a new one is
432     * created.
433     *
434     * @return An instance.
435     */
436    public static AccessibilityWindowInfo obtain() {
437        AccessibilityWindowInfo info = sPool.acquire();
438        if (info == null) {
439            info = new AccessibilityWindowInfo();
440        }
441        if (sNumInstancesInUse != null) {
442            sNumInstancesInUse.incrementAndGet();
443        }
444        return info;
445    }
446
447    /**
448     * Returns a cached instance if such is available or a new one is
449     * created. The returned instance is initialized from the given
450     * <code>info</code>.
451     *
452     * @param info The other info.
453     * @return An instance.
454     */
455    public static AccessibilityWindowInfo obtain(AccessibilityWindowInfo info) {
456        AccessibilityWindowInfo infoClone = obtain();
457
458        infoClone.mType = info.mType;
459        infoClone.mLayer = info.mLayer;
460        infoClone.mBooleanProperties = info.mBooleanProperties;
461        infoClone.mId = info.mId;
462        infoClone.mParentId = info.mParentId;
463        infoClone.mBoundsInScreen.set(info.mBoundsInScreen);
464        infoClone.mTitle = info.mTitle;
465        infoClone.mAnchorId = info.mAnchorId;
466        infoClone.mInPictureInPicture = info.mInPictureInPicture;
467
468        if (info.mChildIds != null && info.mChildIds.size() > 0) {
469            if (infoClone.mChildIds == null) {
470                infoClone.mChildIds = info.mChildIds.clone();
471            } else {
472                infoClone.mChildIds.addAll(info.mChildIds);
473            }
474        }
475
476        infoClone.mConnectionId = info.mConnectionId;
477
478        return infoClone;
479    }
480
481    /**
482     * Specify a counter that will be incremented on obtain() and decremented on recycle()
483     *
484     * @hide
485     */
486    @TestApi
487    public static void setNumInstancesInUseCounter(AtomicInteger counter) {
488        if (sNumInstancesInUse != null) {
489            sNumInstancesInUse = counter;
490        }
491    }
492
493    /**
494     * Return an instance back to be reused.
495     * <p>
496     * <strong>Note:</strong> You must not touch the object after calling this function.
497     * </p>
498     *
499     * @throws IllegalStateException If the info is already recycled.
500     */
501    public void recycle() {
502        clear();
503        sPool.release(this);
504        if (sNumInstancesInUse != null) {
505            sNumInstancesInUse.decrementAndGet();
506        }
507    }
508
509    @Override
510    public int describeContents() {
511        return 0;
512    }
513
514    @Override
515    public void writeToParcel(Parcel parcel, int flags) {
516        parcel.writeInt(mType);
517        parcel.writeInt(mLayer);
518        parcel.writeInt(mBooleanProperties);
519        parcel.writeInt(mId);
520        parcel.writeInt(mParentId);
521        mBoundsInScreen.writeToParcel(parcel, flags);
522        parcel.writeCharSequence(mTitle);
523        parcel.writeInt(mAnchorId);
524        parcel.writeInt(mInPictureInPicture ? 1 : 0);
525
526        final LongArray childIds = mChildIds;
527        if (childIds == null) {
528            parcel.writeInt(0);
529        } else {
530            final int childCount = childIds.size();
531            parcel.writeInt(childCount);
532            for (int i = 0; i < childCount; i++) {
533                parcel.writeInt((int) childIds.get(i));
534            }
535        }
536
537        parcel.writeInt(mConnectionId);
538    }
539
540    private void initFromParcel(Parcel parcel) {
541        mType = parcel.readInt();
542        mLayer = parcel.readInt();
543        mBooleanProperties = parcel.readInt();
544        mId = parcel.readInt();
545        mParentId = parcel.readInt();
546        mBoundsInScreen.readFromParcel(parcel);
547        mTitle = parcel.readCharSequence();
548        mAnchorId = parcel.readInt();
549        mInPictureInPicture = parcel.readInt() == 1;
550
551        final int childCount = parcel.readInt();
552        if (childCount > 0) {
553            if (mChildIds == null) {
554                mChildIds = new LongArray(childCount);
555            }
556            for (int i = 0; i < childCount; i++) {
557                final int childId = parcel.readInt();
558                mChildIds.add(childId);
559            }
560        }
561
562        mConnectionId = parcel.readInt();
563    }
564
565    @Override
566    public int hashCode() {
567        return mId;
568    }
569
570    @Override
571    public boolean equals(Object obj) {
572        if (this == obj) {
573            return true;
574        }
575        if (obj == null) {
576            return false;
577        }
578        if (getClass() != obj.getClass()) {
579            return false;
580        }
581        AccessibilityWindowInfo other = (AccessibilityWindowInfo) obj;
582        return (mId == other.mId);
583    }
584
585    @Override
586    public String toString() {
587        StringBuilder builder = new StringBuilder();
588        builder.append("AccessibilityWindowInfo[");
589        builder.append("title=").append(mTitle);
590        builder.append("id=").append(mId);
591        builder.append(", type=").append(typeToString(mType));
592        builder.append(", layer=").append(mLayer);
593        builder.append(", bounds=").append(mBoundsInScreen);
594        builder.append(", focused=").append(isFocused());
595        builder.append(", active=").append(isActive());
596        builder.append(", pictureInPicture=").append(inPictureInPicture());
597        if (DEBUG) {
598            builder.append(", parent=").append(mParentId);
599            builder.append(", children=[");
600            if (mChildIds != null) {
601                final int childCount = mChildIds.size();
602                for (int i = 0; i < childCount; i++) {
603                    builder.append(mChildIds.get(i));
604                    if (i < childCount - 1) {
605                        builder.append(',');
606                    }
607                }
608            } else {
609                builder.append("null");
610            }
611            builder.append(']');
612        } else {
613            builder.append(", hasParent=").append(mParentId != UNDEFINED_WINDOW_ID);
614            builder.append(", isAnchored=").append(mAnchorId != UNDEFINED_WINDOW_ID);
615            builder.append(", hasChildren=").append(mChildIds != null
616                    && mChildIds.size() > 0);
617        }
618        builder.append(']');
619        return builder.toString();
620    }
621
622    /**
623     * Clears the internal state.
624     */
625    private void clear() {
626        mType = UNDEFINED_WINDOW_ID;
627        mLayer = UNDEFINED_WINDOW_ID;
628        mBooleanProperties = 0;
629        mId = UNDEFINED_WINDOW_ID;
630        mParentId = UNDEFINED_WINDOW_ID;
631        mBoundsInScreen.setEmpty();
632        if (mChildIds != null) {
633            mChildIds.clear();
634        }
635        mConnectionId = UNDEFINED_WINDOW_ID;
636        mAnchorId = UNDEFINED_WINDOW_ID;
637        mInPictureInPicture = false;
638        mTitle = null;
639    }
640
641    /**
642     * Gets the value of a boolean property.
643     *
644     * @param property The property.
645     * @return The value.
646     */
647    private boolean getBooleanProperty(int property) {
648        return (mBooleanProperties & property) != 0;
649    }
650
651    /**
652     * Sets a boolean property.
653     *
654     * @param property The property.
655     * @param value The value.
656     *
657     * @throws IllegalStateException If called from an AccessibilityService.
658     */
659    private void setBooleanProperty(int property, boolean value) {
660        if (value) {
661            mBooleanProperties |= property;
662        } else {
663            mBooleanProperties &= ~property;
664        }
665    }
666
667    private static String typeToString(int type) {
668        switch (type) {
669            case TYPE_APPLICATION: {
670                return "TYPE_APPLICATION";
671            }
672            case TYPE_INPUT_METHOD: {
673                return "TYPE_INPUT_METHOD";
674            }
675            case TYPE_SYSTEM: {
676                return "TYPE_SYSTEM";
677            }
678            case TYPE_ACCESSIBILITY_OVERLAY: {
679                return "TYPE_ACCESSIBILITY_OVERLAY";
680            }
681            case TYPE_SPLIT_SCREEN_DIVIDER: {
682                return "TYPE_SPLIT_SCREEN_DIVIDER";
683            }
684            default:
685                return "<UNKNOWN>";
686        }
687    }
688
689    /**
690     * Checks whether this window changed. The argument should be
691     * another state of the same window, which is have the same id
692     * and type as they never change.
693     *
694     * @param other The new state.
695     * @return Whether something changed.
696     *
697     * @hide
698     */
699    public boolean changed(AccessibilityWindowInfo other) {
700        if (other.mId != mId) {
701            throw new IllegalArgumentException("Not same window.");
702        }
703        if (other.mType != mType) {
704            throw new IllegalArgumentException("Not same type.");
705        }
706        if (!mBoundsInScreen.equals(other.mBoundsInScreen)) {
707            return true;
708        }
709        if (mLayer != other.mLayer) {
710            return true;
711        }
712        if (mBooleanProperties != other.mBooleanProperties) {
713            return true;
714        }
715        if (mParentId != other.mParentId) {
716            return true;
717        }
718        if (mChildIds == null) {
719            if (other.mChildIds != null) {
720                return true;
721            }
722        } else if (!mChildIds.equals(other.mChildIds)) {
723            return true;
724        }
725        return false;
726    }
727
728    public static final Parcelable.Creator<AccessibilityWindowInfo> CREATOR =
729            new Creator<AccessibilityWindowInfo>() {
730        @Override
731        public AccessibilityWindowInfo createFromParcel(Parcel parcel) {
732            AccessibilityWindowInfo info = obtain();
733            info.initFromParcel(parcel);
734            return info;
735        }
736
737        @Override
738        public AccessibilityWindowInfo[] newArray(int size) {
739            return new AccessibilityWindowInfo[size];
740        }
741    };
742}
743