1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.core.view.accessibility;
18 
19 import static android.os.Build.VERSION.SDK_INT;
20 import static android.view.Display.DEFAULT_DISPLAY;
21 
22 import android.graphics.Rect;
23 import android.graphics.Region;
24 import android.os.Build;
25 import android.os.LocaleList;
26 import android.os.SystemClock;
27 import android.view.accessibility.AccessibilityNodeInfo;
28 import android.view.accessibility.AccessibilityWindowInfo;
29 
30 import androidx.annotation.RequiresApi;
31 import androidx.core.os.LocaleListCompat;
32 
33 import org.jspecify.annotations.NonNull;
34 import org.jspecify.annotations.Nullable;
35 
36 /**
37  * Helper for accessing {@link android.view.accessibility.AccessibilityWindowInfo}.
38  */
39 public class AccessibilityWindowInfoCompat {
40     private final Object mInfo;
41 
42     private static final int UNDEFINED = -1;
43 
44     /**
45      * Window type: This is an application window. Such a window shows UI for
46      * interacting with an application.
47      */
48     public static final int TYPE_APPLICATION = 1;
49 
50     /**
51      * Window type: This is an input method window. Such a window shows UI for
52      * inputting text such as keyboard, suggestions, etc.
53      */
54     public static final int TYPE_INPUT_METHOD = 2;
55 
56     /**
57      * Window type: This is an system window. Such a window shows UI for
58      * interacting with the system.
59      */
60     public static final int TYPE_SYSTEM = 3;
61 
62     /**
63      * Window type: Windows that are overlaid <em>only</em> by an {@link
64      * android.accessibilityservice.AccessibilityService} for interception of
65      * user interactions without changing the windows an accessibility service
66      * can introspect. In particular, an accessibility service can introspect
67      * only windows that a sighted user can interact with which they can touch
68      * these windows or can type into these windows. For example, if there
69      * is a full screen accessibility overlay that is touchable, the windows
70      * below it will be introspectable by an accessibility service regardless
71      * they are covered by a touchable window.
72      */
73     public static final int TYPE_ACCESSIBILITY_OVERLAY = 4;
74 
75     /**
76      * Window type: A system window used to divide the screen in split-screen mode.
77      * This type of window is present only in split-screen mode.
78      */
79     public static final int TYPE_SPLIT_SCREEN_DIVIDER = 5;
80 
81     /**
82      * Window type: A system window used to show the UI for the interaction with
83      * window-based magnification, which includes the magnified content and the option menu.
84      */
85     public static final int TYPE_MAGNIFICATION_OVERLAY = 6;
86 
87     /**
88      * Creates a wrapper for info implementation.
89      *
90      * @param object The info to wrap.
91      * @return A wrapper for if the object is not null, null otherwise.
92      */
wrapNonNullInstance(Object object)93     static AccessibilityWindowInfoCompat wrapNonNullInstance(Object object) {
94         if (object != null) {
95             return new AccessibilityWindowInfoCompat(object);
96         }
97         return null;
98     }
99 
100     /**
101      * Creates a new AccessibilityWindowInfoCompat.
102      * <p>
103      * Compatibility:
104      *  <ul>
105      *      <li>Api &lt; 30: Will not wrap an
106      *      {@link android.view.accessibility.AccessibilityWindowInfo} instance.</li>
107      *  </ul>
108      * </p>
109      *
110      */
AccessibilityWindowInfoCompat()111     public AccessibilityWindowInfoCompat() {
112         if (SDK_INT >= 30) {
113             mInfo = Api30Impl.instantiateAccessibilityWindowInfo();
114         } else {
115             mInfo = null;
116         }
117     }
118 
AccessibilityWindowInfoCompat(Object info)119     private AccessibilityWindowInfoCompat(Object info) {
120         mInfo = info;
121     }
122 
123     /**
124      * Gets the type of the window.
125      *
126      * @return The type.
127      * @see #TYPE_APPLICATION
128      * @see #TYPE_INPUT_METHOD
129      * @see #TYPE_SYSTEM
130      * @see #TYPE_ACCESSIBILITY_OVERLAY
131      */
getType()132     public int getType() {
133         if (SDK_INT >= 21) {
134             return Api21Impl.getType((AccessibilityWindowInfo) mInfo);
135         } else {
136             return UNDEFINED;
137         }
138     }
139 
140     /**
141      * Gets the layer which determines the Z-order of the window. Windows
142      * with greater layer appear on top of windows with lesser layer.
143      *
144      * @return The window layer.
145      */
getLayer()146     public int getLayer() {
147         if (SDK_INT >= 21) {
148             return Api21Impl.getLayer((AccessibilityWindowInfo) mInfo);
149         } else {
150             return UNDEFINED;
151         }
152     }
153 
154     /**
155      * Gets the root node in the window's hierarchy.
156      *
157      * @return The root node.
158      */
getRoot()159     public @Nullable AccessibilityNodeInfoCompat getRoot() {
160         if (SDK_INT >= 21) {
161             return AccessibilityNodeInfoCompat.wrapNonNullInstance(
162                     Api21Impl.getRoot((AccessibilityWindowInfo) mInfo));
163         } else {
164             return null;
165         }
166     }
167 
168     /**
169      * Gets the root node in the window's hierarchy.
170      *
171      * @param prefetchingStrategy the prefetching strategy.
172      * @return The root node.
173      *
174      * @see AccessibilityNodeInfoCompat#getParent(int) for a description of prefetching.
175      */
getRoot(int prefetchingStrategy)176     public @Nullable AccessibilityNodeInfoCompat getRoot(int prefetchingStrategy) {
177         if (Build.VERSION.SDK_INT >= 33) {
178             return Api33Impl.getRoot(mInfo, prefetchingStrategy);
179         }
180         return getRoot();
181     }
182 
183     /**
184      * Check if the window is in picture-in-picture mode.
185      * <p>
186      * Compatibility:
187      * <ul>
188      *     <li>API &lt; 26: Returns false.</li>
189      * </ul>
190      * @return {@code true} if the window is in picture-in-picture mode, {@code false} otherwise.
191      */
isInPictureInPictureMode()192     public boolean isInPictureInPictureMode() {
193         if (SDK_INT >= 26) {
194             return Api26Impl.isInPictureInPictureMode((AccessibilityWindowInfo) mInfo);
195         } else {
196             return false;
197         }
198     }
199 
200     /**
201      * Gets the parent window if such.
202      *
203      * @return The parent window.
204      */
getParent()205     public @Nullable AccessibilityWindowInfoCompat getParent() {
206         if (SDK_INT >= 21) {
207             return wrapNonNullInstance(Api21Impl.getParent((AccessibilityWindowInfo) mInfo));
208         } else {
209             return null;
210         }
211     }
212 
213     /**
214      * Gets the unique window id.
215      *
216      * @return windowId The window id.
217      */
getId()218     public int getId() {
219         if (SDK_INT >= 21) {
220             return Api21Impl.getId((AccessibilityWindowInfo) mInfo);
221         } else {
222             return UNDEFINED;
223         }
224     }
225 
226     /**
227      * Gets the touchable region of this window in the screen.
228      * <p>
229      * Compatibility:
230      * <ul>
231      *     <li>API &lt; 33: Gets the bounds of this window in the screen. </li>
232      *     <li>API &lt; 21: Does not operate. </li>
233      * </ul>
234      *
235      * @param outRegion The out window region.
236      */
getRegionInScreen(@onNull Region outRegion)237     public void getRegionInScreen(@NonNull Region outRegion) {
238         if (SDK_INT >= 33) {
239             Api33Impl.getRegionInScreen((AccessibilityWindowInfo) mInfo, outRegion);
240         } else if (SDK_INT >= 21) {
241             Rect outBounds = new Rect();
242             Api21Impl.getBoundsInScreen((AccessibilityWindowInfo) mInfo, outBounds);
243             outRegion.set(outBounds);
244         }
245     }
246 
247     /**
248      * Gets the bounds of this window in the screen.
249      * <p>
250      * Compatibility:
251      * <ul>
252      *   <li>API &lt; 21: Does not operate. </li>
253      * </ul>
254      *
255      * @param outBounds The out window bounds.
256      */
getBoundsInScreen(@onNull Rect outBounds)257     public void getBoundsInScreen(@NonNull Rect outBounds) {
258         if (SDK_INT >= 21) {
259             Api21Impl.getBoundsInScreen((AccessibilityWindowInfo) mInfo, outBounds);
260         }
261     }
262 
263     /**
264      * Gets if this window is active. An active window is the one
265      * the user is currently touching or the window has input focus
266      * and the user is not touching any window.
267      *
268      * @return Whether this is the active window.
269      */
isActive()270     public boolean isActive() {
271         if (SDK_INT >= 21) {
272             return Api21Impl.isActive((AccessibilityWindowInfo) mInfo);
273         } else {
274             return true;
275         }
276     }
277 
278     /**
279      * Gets if this window has input focus.
280      *
281      * @return Whether has input focus.
282      */
isFocused()283     public boolean isFocused() {
284         if (SDK_INT >= 21) {
285             return Api21Impl.isFocused((AccessibilityWindowInfo) mInfo);
286         } else {
287             return true;
288         }
289     }
290 
291     /**
292      * Gets if this window has accessibility focus.
293      *
294      * @return Whether has accessibility focus.
295      */
isAccessibilityFocused()296     public boolean isAccessibilityFocused() {
297         if (SDK_INT >= 21) {
298             return Api21Impl.isAccessibilityFocused((AccessibilityWindowInfo) mInfo);
299         } else {
300             return true;
301         }
302     }
303 
304     /**
305      * Gets the number of child windows.
306      *
307      * @return The child count.
308      */
getChildCount()309     public int getChildCount() {
310         if (SDK_INT >= 21) {
311             return Api21Impl.getChildCount((AccessibilityWindowInfo) mInfo);
312         } else {
313             return 0;
314         }
315     }
316 
317     /**
318      * Gets the child window at a given index.
319      *
320      * @param index The index.
321      * @return The child.
322      */
getChild(int index)323     public @Nullable AccessibilityWindowInfoCompat getChild(int index) {
324         if (SDK_INT >= 21) {
325             return wrapNonNullInstance(Api21Impl.getChild((AccessibilityWindowInfo) mInfo, index));
326         } else {
327             return null;
328         }
329     }
330 
331     /**
332      * Returns the ID of the display this window is on, for use with
333      * {@link android.hardware.display.DisplayManager#getDisplay(int)}.
334      * <p>
335      * Compatibility:
336      * <ul>
337      *   <li>Api &lt; 33: Will return {@link android.view.Display.DEFAULT_DISPLAY}.</li>
338      * </ul>
339      *
340      * @return the logical display id.
341      */
getDisplayId()342     public int getDisplayId() {
343         if (SDK_INT >= 33) {
344             return Api33Impl.getDisplayId((AccessibilityWindowInfo) mInfo);
345         } else {
346             return DEFAULT_DISPLAY;
347         }
348     }
349 
350     /**
351      * Returns the {@link SystemClock#uptimeMillis()} at which the last transition happens.
352      * A transition happens when {@link #getBoundsInScreen(Rect)} is changed.
353      * <p>
354      * Compatibility:
355      * <ul>
356      *   <li>Api &lt; 34: Will return 0.</li>
357      * </ul>
358      * @return The transition timestamp.
359      */
getTransitionTimeMillis()360     public long getTransitionTimeMillis() {
361         if (SDK_INT >= 34) {
362             return Api34Impl.getTransitionTimeMillis((AccessibilityWindowInfo) mInfo);
363         }
364         return 0;
365     }
366 
367     /**
368      * Returns the {@link android.os.LocaleList} of the window.
369      * <p>
370      * Compatibility:
371      * <ul>
372      *   <li>Api &lt; 34: Will return {@link LocaleListCompat#getEmptyLocaleList()}.</li>
373      * </ul>
374      * @return the locales of the window.
375      */
getLocales()376     public @NonNull LocaleListCompat getLocales() {
377         if (SDK_INT >= 34) {
378             return LocaleListCompat.wrap(Api34Impl.getLocales((AccessibilityWindowInfo) mInfo));
379         } else {
380             return LocaleListCompat.getEmptyLocaleList();
381         }
382     }
383 
384     /**
385      * Gets the title of the window.
386      *
387      * @return The title of the window, or the application label for the window if no title was
388      * explicitly set, or {@code null} if neither is available.
389      */
getTitle()390     public @Nullable CharSequence getTitle() {
391         if (SDK_INT >= 24) {
392             return Api24Impl.getTitle((AccessibilityWindowInfo) mInfo);
393         } else {
394             return null;
395         }
396     }
397 
398     /**
399      * Gets the node that anchors this window to another.
400      *
401      * @return The anchor node, or {@code null} if none exists.
402      */
getAnchor()403     public @Nullable AccessibilityNodeInfoCompat getAnchor() {
404         if (SDK_INT >= 24) {
405             return AccessibilityNodeInfoCompat.wrapNonNullInstance(
406                     Api24Impl.getAnchor((AccessibilityWindowInfo) mInfo));
407         } else {
408             return null;
409         }
410     }
411 
412     /**
413      * Returns a cached instance if such is available or a new one is
414      * created.
415      *
416      * @return An instance.
417      */
obtain()418     public static @Nullable AccessibilityWindowInfoCompat obtain() {
419         if (SDK_INT >= 21) {
420             return wrapNonNullInstance(Api21Impl.obtain());
421         } else {
422             return null;
423         }
424     }
425 
426     /**
427      * Returns a cached instance if such is available or a new one is
428      * created. The returned instance is initialized from the given
429      * <code>info</code>.
430      *
431      * @param info The other info.
432      * @return An instance.
433      */
obtain( @ullable AccessibilityWindowInfoCompat info)434     public static @Nullable AccessibilityWindowInfoCompat obtain(
435             @Nullable AccessibilityWindowInfoCompat info) {
436         if (SDK_INT >= 21) {
437             return info == null
438                     ? null
439                     : wrapNonNullInstance(
440                             Api21Impl.obtain((AccessibilityWindowInfo) info.mInfo));
441         } else {
442             return null;
443         }
444     }
445 
446     /**
447      * Return an instance back to be reused.
448      * <p>
449      * <strong>Note:</strong> You must not touch the object after calling this function.
450      * </p>
451      *
452      * @deprecated Accessibility Object recycling is no longer necessary or functional.
453      */
454     @Deprecated
recycle()455     public void recycle() { }
456 
457     /**
458      * @return The unwrapped {@link android.view.accessibility.AccessibilityWindowInfo}.
459      */
unwrap()460     public @Nullable AccessibilityWindowInfo unwrap() {
461         if (SDK_INT >= 21) {
462             return (AccessibilityWindowInfo) mInfo;
463         } else {
464             return null;
465         }
466     }
467 
468     @Override
hashCode()469     public int hashCode() {
470         return (mInfo == null) ? 0 : mInfo.hashCode();
471     }
472 
473     @Override
equals(Object obj)474     public boolean equals(Object obj) {
475         if (this == obj) {
476             return true;
477         }
478         if (obj == null) {
479             return false;
480         }
481         if (!(obj instanceof AccessibilityWindowInfoCompat)) {
482             return false;
483         }
484         AccessibilityWindowInfoCompat other = (AccessibilityWindowInfoCompat) obj;
485         if (mInfo == null) {
486             return other.mInfo == null;
487         }
488         return mInfo.equals(other.mInfo);
489     }
490 
491     @Override
toString()492     public @NonNull String toString() {
493         StringBuilder builder = new StringBuilder();
494         Rect bounds = new Rect();
495         getBoundsInScreen(bounds);
496         builder.append("AccessibilityWindowInfo[");
497         builder.append("id=").append(getId());
498         builder.append(", type=").append(typeToString(getType()));
499         builder.append(", layer=").append(getLayer());
500         builder.append(", bounds=").append(bounds);
501         builder.append(", focused=").append(isFocused());
502         builder.append(", active=").append(isActive());
503         builder.append(", hasParent=").append(getParent() != null);
504         builder.append(", hasChildren=").append(getChildCount() > 0);
505         builder.append(", transitionTime=").append(getTransitionTimeMillis());
506         builder.append(", locales=").append(getLocales());
507         builder.append(']');
508         return builder.toString();
509     }
510 
typeToString(int type)511     private static String typeToString(int type) {
512         switch (type) {
513             case TYPE_APPLICATION: {
514                 return "TYPE_APPLICATION";
515             }
516             case TYPE_INPUT_METHOD: {
517                 return "TYPE_INPUT_METHOD";
518             }
519             case TYPE_SYSTEM: {
520                 return "TYPE_SYSTEM";
521             }
522             case TYPE_ACCESSIBILITY_OVERLAY: {
523                 return "TYPE_ACCESSIBILITY_OVERLAY";
524             }
525             default:
526                 return "<UNKNOWN>";
527         }
528     }
529 
530     @RequiresApi(21)
531     private static class Api21Impl {
Api21Impl()532         private Api21Impl() {
533             // This class is not instantiable.
534         }
535 
getBoundsInScreen(AccessibilityWindowInfo info, Rect outBounds)536         static void getBoundsInScreen(AccessibilityWindowInfo info, Rect outBounds) {
537             info.getBoundsInScreen(outBounds);
538         }
539 
getChild(AccessibilityWindowInfo info, int index)540         static AccessibilityWindowInfo getChild(AccessibilityWindowInfo info, int index) {
541             return info.getChild(index);
542         }
543 
getChildCount(AccessibilityWindowInfo info)544         static int getChildCount(AccessibilityWindowInfo info) {
545             return info.getChildCount();
546         }
547 
getId(AccessibilityWindowInfo info)548         static int getId(AccessibilityWindowInfo info) {
549             return info.getId();
550         }
551 
getLayer(AccessibilityWindowInfo info)552         static int getLayer(AccessibilityWindowInfo info) {
553             return info.getLayer();
554         }
555 
getParent(AccessibilityWindowInfo info)556         static AccessibilityWindowInfo getParent(AccessibilityWindowInfo info) {
557             return info.getParent();
558         }
559 
getRoot(AccessibilityWindowInfo info)560         static AccessibilityNodeInfo getRoot(AccessibilityWindowInfo info) {
561             return info.getRoot();
562         }
563 
getType(AccessibilityWindowInfo info)564         static int getType(AccessibilityWindowInfo info) {
565             return info.getType();
566         }
567 
isAccessibilityFocused(AccessibilityWindowInfo info)568         static boolean isAccessibilityFocused(AccessibilityWindowInfo info) {
569             return info.isAccessibilityFocused();
570         }
571 
isActive(AccessibilityWindowInfo info)572         static boolean isActive(AccessibilityWindowInfo info) {
573             return info.isActive();
574         }
575 
isFocused(AccessibilityWindowInfo info)576         static boolean isFocused(AccessibilityWindowInfo info) {
577             return info.isFocused();
578         }
579 
obtain()580         static AccessibilityWindowInfo obtain() {
581             return AccessibilityWindowInfo.obtain();
582         }
583 
obtain(AccessibilityWindowInfo info)584         static AccessibilityWindowInfo obtain(AccessibilityWindowInfo info) {
585             return AccessibilityWindowInfo.obtain(info);
586         }
587     }
588 
589     @RequiresApi(24)
590     private static class Api24Impl {
Api24Impl()591         private Api24Impl() {
592             // This class is not instantiable.
593         }
594 
getAnchor(AccessibilityWindowInfo info)595         static AccessibilityNodeInfo getAnchor(AccessibilityWindowInfo info) {
596             return info.getAnchor();
597         }
598 
getTitle(AccessibilityWindowInfo info)599         static CharSequence getTitle(AccessibilityWindowInfo info) {
600             return info.getTitle();
601         }
602     }
603 
604     @RequiresApi(26)
605     private static class Api26Impl {
Api26Impl()606         private Api26Impl() {
607             // This class is non instantiable.
608         }
609 
isInPictureInPictureMode(AccessibilityWindowInfo info)610         static boolean isInPictureInPictureMode(AccessibilityWindowInfo info) {
611             return info.isInPictureInPictureMode();
612         }
613     }
614 
615     @RequiresApi(30)
616     private static class Api30Impl {
Api30Impl()617         private Api30Impl() {
618             // This class is non instantiable.
619         }
620 
instantiateAccessibilityWindowInfo()621         static AccessibilityWindowInfo instantiateAccessibilityWindowInfo() {
622             return new AccessibilityWindowInfo();
623         }
624     }
625 
626     @RequiresApi(33)
627     private static class Api33Impl {
Api33Impl()628         private Api33Impl() {
629             // This class is non instantiable.
630         }
631 
getDisplayId(AccessibilityWindowInfo info)632         static int getDisplayId(AccessibilityWindowInfo info) {
633             return info.getDisplayId();
634         }
635 
getRegionInScreen(AccessibilityWindowInfo info, Region outRegion)636         static void getRegionInScreen(AccessibilityWindowInfo info, Region outRegion) {
637             info.getRegionInScreen(outRegion);
638         }
639 
getRoot(Object info, int prefetchingStrategy)640         public static AccessibilityNodeInfoCompat getRoot(Object info, int prefetchingStrategy) {
641             return AccessibilityNodeInfoCompat.wrapNonNullInstance(
642                     ((AccessibilityWindowInfo) info).getRoot(prefetchingStrategy));
643         }
644     }
645 
646     @RequiresApi(34)
647     private static class Api34Impl {
Api34Impl()648         private Api34Impl() {
649             // This class is non instantiable.
650         }
651 
getTransitionTimeMillis(AccessibilityWindowInfo info)652         public static long getTransitionTimeMillis(AccessibilityWindowInfo info) {
653             return info.getTransitionTimeMillis();
654         }
655 
getLocales(AccessibilityWindowInfo info)656         static LocaleList getLocales(AccessibilityWindowInfo info) {
657             return info.getLocales();
658         }
659     }
660 }