1 /*
2  * Copyright (C) 2011 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 android.view.accessibility;
18 
19 import static com.android.internal.util.CollectionUtils.isEmpty;
20 
21 import android.annotation.Nullable;
22 import android.os.Parcelable;
23 import android.view.View;
24 
25 import java.util.ArrayList;
26 import java.util.List;
27 
28 /**
29  * Represents a record in an {@link AccessibilityEvent} and contains information
30  * about state change of its source {@link android.view.View}. When a view fires
31  * an accessibility event it requests from its parent to dispatch the
32  * constructed event. The parent may optionally append a record for itself
33  * for providing more context to
34  * {@link android.accessibilityservice.AccessibilityService}s. Hence,
35  * accessibility services can facilitate additional accessibility records
36  * to enhance feedback.
37  * </p>
38  * <p>
39  * Once the accessibility event containing a record is dispatched the record is
40  * made immutable and calling a state mutation method generates an error.
41  * </p>
42  * <p>
43  * <strong>Note:</strong> Not all properties are applicable to all accessibility
44  * event types. For detailed information please refer to {@link AccessibilityEvent}.
45  * </p>
46  *
47  * <div class="special reference">
48  * <h3>Developer Guides</h3>
49  * <p>For more information about creating and processing AccessibilityRecords, read the
50  * <a href="{@docRoot}guide/topics/ui/accessibility/index.html">Accessibility</a>
51  * developer guide.</p>
52  * </div>
53  *
54  * @see AccessibilityEvent
55  * @see AccessibilityManager
56  * @see android.accessibilityservice.AccessibilityService
57  * @see AccessibilityNodeInfo
58  */
59 public class AccessibilityRecord {
60     /** @hide */
61     protected static final boolean DEBUG_CONCISE_TOSTRING = false;
62 
63     private static final int UNDEFINED = -1;
64 
65     private static final int PROPERTY_CHECKED = 0x00000001;
66     private static final int PROPERTY_ENABLED = 0x00000002;
67     private static final int PROPERTY_PASSWORD = 0x00000004;
68     private static final int PROPERTY_FULL_SCREEN = 0x00000080;
69     private static final int PROPERTY_SCROLLABLE = 0x00000100;
70     private static final int PROPERTY_IMPORTANT_FOR_ACCESSIBILITY = 0x00000200;
71 
72     private static final int GET_SOURCE_PREFETCH_FLAGS =
73         AccessibilityNodeInfo.FLAG_PREFETCH_PREDECESSORS
74         | AccessibilityNodeInfo.FLAG_PREFETCH_SIBLINGS
75         | AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS;
76 
77     // Housekeeping
78     private static final int MAX_POOL_SIZE = 10;
79     private static final Object sPoolLock = new Object();
80     private static AccessibilityRecord sPool;
81     private static int sPoolSize;
82     private AccessibilityRecord mNext;
83     private boolean mIsInPool;
84 
85     boolean mSealed;
86     int mBooleanProperties = 0;
87     int mCurrentItemIndex = UNDEFINED;
88     int mItemCount = UNDEFINED;
89     int mFromIndex = UNDEFINED;
90     int mToIndex = UNDEFINED;
91     int mScrollX = UNDEFINED;
92     int mScrollY = UNDEFINED;
93 
94     int mScrollDeltaX = UNDEFINED;
95     int mScrollDeltaY = UNDEFINED;
96     int mMaxScrollX = UNDEFINED;
97     int mMaxScrollY = UNDEFINED;
98 
99     int mAddedCount= UNDEFINED;
100     int mRemovedCount = UNDEFINED;
101     long mSourceNodeId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
102     int mSourceWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
103 
104     CharSequence mClassName;
105     CharSequence mContentDescription;
106     CharSequence mBeforeText;
107     Parcelable mParcelableData;
108 
109     final List<CharSequence> mText = new ArrayList<CharSequence>();
110 
111     int mConnectionId = UNDEFINED;
112 
113     /*
114      * Hide constructor.
115      */
AccessibilityRecord()116     AccessibilityRecord() {
117     }
118 
119     /**
120      * Sets the event source.
121      *
122      * @param source The source.
123      *
124      * @throws IllegalStateException If called from an AccessibilityService.
125      */
setSource(View source)126     public void setSource(View source) {
127         setSource(source, AccessibilityNodeProvider.HOST_VIEW_ID);
128     }
129 
130     /**
131      * Sets the source to be a virtual descendant of the given <code>root</code>.
132      * If <code>virtualDescendantId</code> equals to {@link View#NO_ID} the root
133      * is set as the source.
134      * <p>
135      * A virtual descendant is an imaginary View that is reported as a part of the view
136      * hierarchy for accessibility purposes. This enables custom views that draw complex
137      * content to report them selves as a tree of virtual views, thus conveying their
138      * logical structure.
139      * </p>
140      *
141      * @param root The root of the virtual subtree.
142      * @param virtualDescendantId The id of the virtual descendant.
143      */
setSource(@ullable View root, int virtualDescendantId)144     public void setSource(@Nullable View root, int virtualDescendantId) {
145         enforceNotSealed();
146         boolean important = true;
147         int rootViewId = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
148         mSourceWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
149         if (root != null) {
150             important = root.isImportantForAccessibility();
151             rootViewId = root.getAccessibilityViewId();
152             mSourceWindowId = root.getAccessibilityWindowId();
153         }
154         setBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY, important);
155         mSourceNodeId = AccessibilityNodeInfo.makeNodeId(rootViewId, virtualDescendantId);
156     }
157 
158     /**
159      * Set the source node ID directly
160      *
161      * @param sourceNodeId The source node Id
162      * @hide
163      */
setSourceNodeId(long sourceNodeId)164     public void setSourceNodeId(long sourceNodeId) {
165         mSourceNodeId = sourceNodeId;
166     }
167 
168     /**
169      * Gets the {@link AccessibilityNodeInfo} of the event source.
170      * <p>
171      *   <strong>Note:</strong> It is a client responsibility to recycle the received info
172      *   by calling {@link AccessibilityNodeInfo#recycle() AccessibilityNodeInfo#recycle()}
173      *   to avoid creating of multiple instances.
174      * </p>
175      * @return The info of the source.
176      */
getSource()177     public AccessibilityNodeInfo getSource() {
178         enforceSealed();
179         if ((mConnectionId == UNDEFINED)
180                 || (mSourceWindowId == AccessibilityWindowInfo.UNDEFINED_WINDOW_ID)
181                 || (AccessibilityNodeInfo.getAccessibilityViewId(mSourceNodeId)
182                         == AccessibilityNodeInfo.UNDEFINED_ITEM_ID)) {
183             return null;
184         }
185         AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
186         return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, mSourceWindowId,
187                 mSourceNodeId, false, GET_SOURCE_PREFETCH_FLAGS, null);
188     }
189 
190     /**
191      * Sets the window id.
192      *
193      * @param windowId The window id.
194      *
195      * @hide
196      */
setWindowId(int windowId)197     public void setWindowId(int windowId) {
198         mSourceWindowId = windowId;
199     }
200 
201     /**
202      * Gets the id of the window from which the event comes from.
203      *
204      * @return The window id.
205      */
getWindowId()206     public int getWindowId() {
207         return mSourceWindowId;
208     }
209 
210     /**
211      * Gets if the source is checked.
212      *
213      * @return True if the view is checked, false otherwise.
214      */
isChecked()215     public boolean isChecked() {
216         return getBooleanProperty(PROPERTY_CHECKED);
217     }
218 
219     /**
220      * Sets if the source is checked.
221      *
222      * @param isChecked True if the view is checked, false otherwise.
223      *
224      * @throws IllegalStateException If called from an AccessibilityService.
225      */
setChecked(boolean isChecked)226     public void setChecked(boolean isChecked) {
227         enforceNotSealed();
228         setBooleanProperty(PROPERTY_CHECKED, isChecked);
229     }
230 
231     /**
232      * Gets if the source is enabled.
233      *
234      * @return True if the view is enabled, false otherwise.
235      */
isEnabled()236     public boolean isEnabled() {
237         return getBooleanProperty(PROPERTY_ENABLED);
238     }
239 
240     /**
241      * Sets if the source is enabled.
242      *
243      * @param isEnabled True if the view is enabled, false otherwise.
244      *
245      * @throws IllegalStateException If called from an AccessibilityService.
246      */
setEnabled(boolean isEnabled)247     public void setEnabled(boolean isEnabled) {
248         enforceNotSealed();
249         setBooleanProperty(PROPERTY_ENABLED, isEnabled);
250     }
251 
252     /**
253      * Gets if the source is a password field.
254      *
255      * @return True if the view is a password field, false otherwise.
256      */
isPassword()257     public boolean isPassword() {
258         return getBooleanProperty(PROPERTY_PASSWORD);
259     }
260 
261     /**
262      * Sets if the source is a password field.
263      *
264      * @param isPassword True if the view is a password field, false otherwise.
265      *
266      * @throws IllegalStateException If called from an AccessibilityService.
267      */
setPassword(boolean isPassword)268     public void setPassword(boolean isPassword) {
269         enforceNotSealed();
270         setBooleanProperty(PROPERTY_PASSWORD, isPassword);
271     }
272 
273     /**
274      * Gets if the source is taking the entire screen.
275      *
276      * @return True if the source is full screen, false otherwise.
277      */
isFullScreen()278     public boolean isFullScreen() {
279         return getBooleanProperty(PROPERTY_FULL_SCREEN);
280     }
281 
282     /**
283      * Sets if the source is taking the entire screen.
284      *
285      * @param isFullScreen True if the source is full screen, false otherwise.
286      *
287      * @throws IllegalStateException If called from an AccessibilityService.
288      */
setFullScreen(boolean isFullScreen)289     public void setFullScreen(boolean isFullScreen) {
290         enforceNotSealed();
291         setBooleanProperty(PROPERTY_FULL_SCREEN, isFullScreen);
292     }
293 
294     /**
295      * Gets if the source is scrollable.
296      *
297      * @return True if the source is scrollable, false otherwise.
298      */
isScrollable()299     public boolean isScrollable() {
300         return getBooleanProperty(PROPERTY_SCROLLABLE);
301     }
302 
303     /**
304      * Sets if the source is scrollable.
305      *
306      * @param scrollable True if the source is scrollable, false otherwise.
307      *
308      * @throws IllegalStateException If called from an AccessibilityService.
309      */
setScrollable(boolean scrollable)310     public void setScrollable(boolean scrollable) {
311         enforceNotSealed();
312         setBooleanProperty(PROPERTY_SCROLLABLE, scrollable);
313     }
314 
315     /**
316      * Gets if the source is important for accessibility.
317      *
318      * <strong>Note:</strong> Used only internally to determine whether
319      * to deliver the event to a given accessibility service since some
320      * services may want to regard all views for accessibility while others
321      * may want to regard only the important views for accessibility.
322      *
323      * @return True if the source is important for accessibility,
324      *        false otherwise.
325      *
326      * @hide
327      */
isImportantForAccessibility()328     public boolean isImportantForAccessibility() {
329         return getBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY);
330     }
331 
332     /**
333      * Sets if the source is important for accessibility.
334      *
335      * @param importantForAccessibility True if the source is important for accessibility,
336      *                                  false otherwise.
337      *
338      * @throws IllegalStateException If called from an AccessibilityService.
339      * @hide
340      */
setImportantForAccessibility(boolean importantForAccessibility)341     public void setImportantForAccessibility(boolean importantForAccessibility) {
342         enforceNotSealed();
343         setBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY, importantForAccessibility);
344     }
345 
346     /**
347      * Gets the number of items that can be visited.
348      *
349      * @return The number of items.
350      */
getItemCount()351     public int getItemCount() {
352         return mItemCount;
353     }
354 
355     /**
356      * Sets the number of items that can be visited.
357      *
358      * @param itemCount The number of items.
359      *
360      * @throws IllegalStateException If called from an AccessibilityService.
361      */
setItemCount(int itemCount)362     public void setItemCount(int itemCount) {
363         enforceNotSealed();
364         mItemCount = itemCount;
365     }
366 
367     /**
368      * Gets the index of the source in the list of items the can be visited.
369      *
370      * @return The current item index.
371      */
getCurrentItemIndex()372     public int getCurrentItemIndex() {
373         return mCurrentItemIndex;
374     }
375 
376     /**
377      * Sets the index of the source in the list of items that can be visited.
378      *
379      * @param currentItemIndex The current item index.
380      *
381      * @throws IllegalStateException If called from an AccessibilityService.
382      */
setCurrentItemIndex(int currentItemIndex)383     public void setCurrentItemIndex(int currentItemIndex) {
384         enforceNotSealed();
385         mCurrentItemIndex = currentItemIndex;
386     }
387 
388     /**
389      * Gets the index of the first character of the changed sequence,
390      * or the beginning of a text selection or the index of the first
391      * visible item when scrolling.
392      *
393      * @return The index of the first character or selection
394      *        start or the first visible item.
395      */
getFromIndex()396     public int getFromIndex() {
397         return mFromIndex;
398     }
399 
400     /**
401      * Sets the index of the first character of the changed sequence
402      * or the beginning of a text selection or the index of the first
403      * visible item when scrolling.
404      *
405      * @param fromIndex The index of the first character or selection
406      *        start or the first visible item.
407      *
408      * @throws IllegalStateException If called from an AccessibilityService.
409      */
setFromIndex(int fromIndex)410     public void setFromIndex(int fromIndex) {
411         enforceNotSealed();
412         mFromIndex = fromIndex;
413     }
414 
415     /**
416      * Gets the index of text selection end or the index of the last
417      * visible item when scrolling.
418      *
419      * @return The index of selection end or last item index.
420      */
getToIndex()421     public int getToIndex() {
422         return mToIndex;
423     }
424 
425     /**
426      * Sets the index of text selection end or the index of the last
427      * visible item when scrolling.
428      *
429      * @param toIndex The index of selection end or last item index.
430      */
setToIndex(int toIndex)431     public void setToIndex(int toIndex) {
432         enforceNotSealed();
433         mToIndex = toIndex;
434     }
435 
436     /**
437      * Gets the scroll offset of the source left edge in pixels.
438      *
439      * @return The scroll.
440      */
getScrollX()441     public int getScrollX() {
442         return mScrollX;
443     }
444 
445     /**
446      * Sets the scroll offset of the source left edge in pixels.
447      *
448      * @param scrollX The scroll.
449      */
setScrollX(int scrollX)450     public void setScrollX(int scrollX) {
451         enforceNotSealed();
452         mScrollX = scrollX;
453     }
454 
455     /**
456      * Gets the scroll offset of the source top edge in pixels.
457      *
458      * @return The scroll.
459      */
getScrollY()460     public int getScrollY() {
461         return mScrollY;
462     }
463 
464     /**
465      * Sets the scroll offset of the source top edge in pixels.
466      *
467      * @param scrollY The scroll.
468      */
setScrollY(int scrollY)469     public void setScrollY(int scrollY) {
470         enforceNotSealed();
471         mScrollY = scrollY;
472     }
473 
474     /**
475      * Gets the difference in pixels between the horizontal position before the scroll and the
476      * current horizontal position
477      *
478      * @return the scroll delta x
479      */
getScrollDeltaX()480     public int getScrollDeltaX() {
481         return mScrollDeltaX;
482     }
483 
484     /**
485      * Sets the difference in pixels between the horizontal position before the scroll and the
486      * current horizontal position
487      *
488      * @param scrollDeltaX the scroll delta x
489      */
setScrollDeltaX(int scrollDeltaX)490     public void setScrollDeltaX(int scrollDeltaX) {
491         enforceNotSealed();
492         mScrollDeltaX = scrollDeltaX;
493     }
494 
495     /**
496      * Gets the difference in pixels between the vertical position before the scroll and the
497      * current vertical position
498      *
499      * @return the scroll delta y
500      */
getScrollDeltaY()501     public int getScrollDeltaY() {
502         return mScrollDeltaY;
503     }
504 
505     /**
506      * Sets the difference in pixels between the vertical position before the scroll and the
507      * current vertical position
508      *
509      * @param scrollDeltaY the scroll delta y
510      */
setScrollDeltaY(int scrollDeltaY)511     public void setScrollDeltaY(int scrollDeltaY) {
512         enforceNotSealed();
513         mScrollDeltaY = scrollDeltaY;
514     }
515 
516     /**
517      * Gets the max scroll offset of the source left edge in pixels.
518      *
519      * @return The max scroll.
520      */
getMaxScrollX()521     public int getMaxScrollX() {
522         return mMaxScrollX;
523     }
524 
525     /**
526      * Sets the max scroll offset of the source left edge in pixels.
527      *
528      * @param maxScrollX The max scroll.
529      */
setMaxScrollX(int maxScrollX)530     public void setMaxScrollX(int maxScrollX) {
531         enforceNotSealed();
532         mMaxScrollX = maxScrollX;
533     }
534 
535     /**
536      * Gets the max scroll offset of the source top edge in pixels.
537      *
538      * @return The max scroll.
539      */
getMaxScrollY()540     public int getMaxScrollY() {
541         return mMaxScrollY;
542     }
543 
544     /**
545      * Sets the max scroll offset of the source top edge in pixels.
546      *
547      * @param maxScrollY The max scroll.
548      */
setMaxScrollY(int maxScrollY)549     public void setMaxScrollY(int maxScrollY) {
550         enforceNotSealed();
551         mMaxScrollY = maxScrollY;
552     }
553 
554     /**
555      * Gets the number of added characters.
556      *
557      * @return The number of added characters.
558      */
getAddedCount()559     public int getAddedCount() {
560         return mAddedCount;
561     }
562 
563     /**
564      * Sets the number of added characters.
565      *
566      * @param addedCount The number of added characters.
567      *
568      * @throws IllegalStateException If called from an AccessibilityService.
569      */
setAddedCount(int addedCount)570     public void setAddedCount(int addedCount) {
571         enforceNotSealed();
572         mAddedCount = addedCount;
573     }
574 
575     /**
576      * Gets the number of removed characters.
577      *
578      * @return The number of removed characters.
579      */
getRemovedCount()580     public int getRemovedCount() {
581         return mRemovedCount;
582     }
583 
584     /**
585      * Sets the number of removed characters.
586      *
587      * @param removedCount The number of removed characters.
588      *
589      * @throws IllegalStateException If called from an AccessibilityService.
590      */
setRemovedCount(int removedCount)591     public void setRemovedCount(int removedCount) {
592         enforceNotSealed();
593         mRemovedCount = removedCount;
594     }
595 
596     /**
597      * Gets the class name of the source.
598      *
599      * @return The class name.
600      */
getClassName()601     public CharSequence getClassName() {
602         return mClassName;
603     }
604 
605     /**
606      * Sets the class name of the source.
607      *
608      * @param className The lass name.
609      *
610      * @throws IllegalStateException If called from an AccessibilityService.
611      */
setClassName(CharSequence className)612     public void setClassName(CharSequence className) {
613         enforceNotSealed();
614         mClassName = className;
615     }
616 
617     /**
618      * Gets the text of the event. The index in the list represents the priority
619      * of the text. Specifically, the lower the index the higher the priority.
620      *
621      * @return The text.
622      */
getText()623     public List<CharSequence> getText() {
624         return mText;
625     }
626 
627     /**
628      * Sets the text before a change.
629      *
630      * @return The text before the change.
631      */
getBeforeText()632     public CharSequence getBeforeText() {
633         return mBeforeText;
634     }
635 
636     /**
637      * Sets the text before a change.
638      *
639      * @param beforeText The text before the change.
640      *
641      * @throws IllegalStateException If called from an AccessibilityService.
642      */
setBeforeText(CharSequence beforeText)643     public void setBeforeText(CharSequence beforeText) {
644         enforceNotSealed();
645         mBeforeText = (beforeText == null) ? null
646                 : beforeText.subSequence(0, beforeText.length());
647     }
648 
649     /**
650      * Gets the description of the source.
651      *
652      * @return The description.
653      */
getContentDescription()654     public CharSequence getContentDescription() {
655         return mContentDescription;
656     }
657 
658     /**
659      * Sets the description of the source.
660      *
661      * @param contentDescription The description.
662      *
663      * @throws IllegalStateException If called from an AccessibilityService.
664      */
setContentDescription(CharSequence contentDescription)665     public void setContentDescription(CharSequence contentDescription) {
666         enforceNotSealed();
667         mContentDescription = (contentDescription == null) ? null
668                 : contentDescription.subSequence(0, contentDescription.length());
669     }
670 
671     /**
672      * Gets the {@link Parcelable} data.
673      *
674      * @return The parcelable data.
675      */
getParcelableData()676     public Parcelable getParcelableData() {
677         return mParcelableData;
678     }
679 
680     /**
681      * Sets the {@link Parcelable} data of the event.
682      *
683      * @param parcelableData The parcelable data.
684      *
685      * @throws IllegalStateException If called from an AccessibilityService.
686      */
setParcelableData(Parcelable parcelableData)687     public void setParcelableData(Parcelable parcelableData) {
688         enforceNotSealed();
689         mParcelableData = parcelableData;
690     }
691 
692     /**
693      * Gets the id of the source node.
694      *
695      * @return The id.
696      *
697      * @hide
698      */
getSourceNodeId()699     public long getSourceNodeId() {
700         return mSourceNodeId;
701     }
702 
703     /**
704      * Sets the unique id of the IAccessibilityServiceConnection over which
705      * this instance can send requests to the system.
706      *
707      * @param connectionId The connection id.
708      *
709      * @hide
710      */
setConnectionId(int connectionId)711     public void setConnectionId(int connectionId) {
712         enforceNotSealed();
713         mConnectionId = connectionId;
714     }
715 
716     /**
717      * Sets if this instance is sealed.
718      *
719      * @param sealed Whether is sealed.
720      *
721      * @hide
722      */
setSealed(boolean sealed)723     public void setSealed(boolean sealed) {
724         mSealed = sealed;
725     }
726 
727     /**
728      * Gets if this instance is sealed.
729      *
730      * @return Whether is sealed.
731      */
isSealed()732     boolean isSealed() {
733         return mSealed;
734     }
735 
736     /**
737      * Enforces that this instance is sealed.
738      *
739      * @throws IllegalStateException If this instance is not sealed.
740      */
enforceSealed()741     void enforceSealed() {
742         if (!isSealed()) {
743             throw new IllegalStateException("Cannot perform this "
744                     + "action on a not sealed instance.");
745         }
746     }
747 
748     /**
749      * Enforces that this instance is not sealed.
750      *
751      * @throws IllegalStateException If this instance is sealed.
752      */
enforceNotSealed()753     void enforceNotSealed() {
754         if (isSealed()) {
755             throw new IllegalStateException("Cannot perform this "
756                     + "action on a sealed instance.");
757         }
758     }
759 
760     /**
761      * Gets the value of a boolean property.
762      *
763      * @param property The property.
764      * @return The value.
765      */
getBooleanProperty(int property)766     private boolean getBooleanProperty(int property) {
767         return (mBooleanProperties & property) == property;
768     }
769 
770     /**
771      * Sets a boolean property.
772      *
773      * @param property The property.
774      * @param value The value.
775      */
setBooleanProperty(int property, boolean value)776     private void setBooleanProperty(int property, boolean value) {
777         if (value) {
778             mBooleanProperties |= property;
779         } else {
780             mBooleanProperties &= ~property;
781         }
782     }
783 
784     /**
785      * Returns a cached instance if such is available or a new one is
786      * instantiated. The instance is initialized with data from the
787      * given record.
788      *
789      * @return An instance.
790      */
obtain(AccessibilityRecord record)791     public static AccessibilityRecord obtain(AccessibilityRecord record) {
792        AccessibilityRecord clone = AccessibilityRecord.obtain();
793        clone.init(record);
794        return clone;
795     }
796 
797     /**
798      * Returns a cached instance if such is available or a new one is
799      * instantiated.
800      *
801      * @return An instance.
802      */
obtain()803     public static AccessibilityRecord obtain() {
804         synchronized (sPoolLock) {
805             if (sPool != null) {
806                 AccessibilityRecord record = sPool;
807                 sPool = sPool.mNext;
808                 sPoolSize--;
809                 record.mNext = null;
810                 record.mIsInPool = false;
811                 return record;
812             }
813             return new AccessibilityRecord();
814         }
815     }
816 
817     /**
818      * Return an instance back to be reused.
819      * <p>
820      * <strong>Note:</strong> You must not touch the object after calling this function.
821      *
822      * @throws IllegalStateException If the record is already recycled.
823      */
recycle()824     public void recycle() {
825         if (mIsInPool) {
826             throw new IllegalStateException("Record already recycled!");
827         }
828         clear();
829         synchronized (sPoolLock) {
830             if (sPoolSize <= MAX_POOL_SIZE) {
831                 mNext = sPool;
832                 sPool = this;
833                 mIsInPool = true;
834                 sPoolSize++;
835             }
836         }
837     }
838 
839     /**
840      * Initialize this record from another one.
841      *
842      * @param record The to initialize from.
843      */
init(AccessibilityRecord record)844     void init(AccessibilityRecord record) {
845         mSealed = record.mSealed;
846         mBooleanProperties = record.mBooleanProperties;
847         mCurrentItemIndex = record.mCurrentItemIndex;
848         mItemCount = record.mItemCount;
849         mFromIndex = record.mFromIndex;
850         mToIndex = record.mToIndex;
851         mScrollX = record.mScrollX;
852         mScrollY = record.mScrollY;
853         mMaxScrollX = record.mMaxScrollX;
854         mMaxScrollY = record.mMaxScrollY;
855         mAddedCount = record.mAddedCount;
856         mRemovedCount = record.mRemovedCount;
857         mClassName = record.mClassName;
858         mContentDescription = record.mContentDescription;
859         mBeforeText = record.mBeforeText;
860         mParcelableData = record.mParcelableData;
861         mText.addAll(record.mText);
862         mSourceWindowId = record.mSourceWindowId;
863         mSourceNodeId = record.mSourceNodeId;
864         mConnectionId = record.mConnectionId;
865     }
866 
867     /**
868      * Clears the state of this instance.
869      */
clear()870     void clear() {
871         mSealed = false;
872         mBooleanProperties = 0;
873         mCurrentItemIndex = UNDEFINED;
874         mItemCount = UNDEFINED;
875         mFromIndex = UNDEFINED;
876         mToIndex = UNDEFINED;
877         mScrollX = UNDEFINED;
878         mScrollY = UNDEFINED;
879         mMaxScrollX = UNDEFINED;
880         mMaxScrollY = UNDEFINED;
881         mAddedCount = UNDEFINED;
882         mRemovedCount = UNDEFINED;
883         mClassName = null;
884         mContentDescription = null;
885         mBeforeText = null;
886         mParcelableData = null;
887         mText.clear();
888         mSourceNodeId = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
889         mSourceWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
890         mConnectionId = UNDEFINED;
891     }
892 
893     @Override
toString()894     public String toString() {
895         return appendTo(new StringBuilder()).toString();
896     }
897 
appendTo(StringBuilder builder)898     StringBuilder appendTo(StringBuilder builder) {
899         builder.append(" [ ClassName: ").append(mClassName);
900         if (!DEBUG_CONCISE_TOSTRING || !isEmpty(mText)) {
901             appendPropName(builder, "Text").append(mText);
902         }
903         append(builder, "ContentDescription", mContentDescription);
904         append(builder, "ItemCount", mItemCount);
905         append(builder, "CurrentItemIndex", mCurrentItemIndex);
906 
907         appendUnless(true, PROPERTY_ENABLED, builder);
908         appendUnless(false, PROPERTY_PASSWORD, builder);
909         appendUnless(false, PROPERTY_CHECKED, builder);
910         appendUnless(false, PROPERTY_FULL_SCREEN, builder);
911         appendUnless(false, PROPERTY_SCROLLABLE, builder);
912 
913         append(builder, "BeforeText", mBeforeText);
914         append(builder, "FromIndex", mFromIndex);
915         append(builder, "ToIndex", mToIndex);
916         append(builder, "ScrollX", mScrollX);
917         append(builder, "ScrollY", mScrollY);
918         append(builder, "MaxScrollX", mMaxScrollX);
919         append(builder, "MaxScrollY", mMaxScrollY);
920         append(builder, "AddedCount", mAddedCount);
921         append(builder, "RemovedCount", mRemovedCount);
922         append(builder, "ParcelableData", mParcelableData);
923         builder.append(" ]");
924         return builder;
925     }
926 
appendUnless(boolean defValue, int prop, StringBuilder builder)927     private void appendUnless(boolean defValue, int prop, StringBuilder builder) {
928         boolean value = getBooleanProperty(prop);
929         if (DEBUG_CONCISE_TOSTRING && value == defValue) return;
930         appendPropName(builder, singleBooleanPropertyToString(prop))
931                 .append(value);
932     }
933 
singleBooleanPropertyToString(int prop)934     private static String singleBooleanPropertyToString(int prop) {
935         switch (prop) {
936             case PROPERTY_CHECKED: return "Checked";
937             case PROPERTY_ENABLED: return "Enabled";
938             case PROPERTY_PASSWORD: return "Password";
939             case PROPERTY_FULL_SCREEN: return "FullScreen";
940             case PROPERTY_SCROLLABLE: return "Scrollable";
941             case PROPERTY_IMPORTANT_FOR_ACCESSIBILITY:
942                 return "ImportantForAccessibility";
943             default: return Integer.toHexString(prop);
944         }
945     }
946 
append(StringBuilder builder, String propName, int propValue)947     private void append(StringBuilder builder, String propName, int propValue) {
948         if (DEBUG_CONCISE_TOSTRING && propValue == UNDEFINED) return;
949         appendPropName(builder, propName).append(propValue);
950     }
951 
append(StringBuilder builder, String propName, Object propValue)952     private void append(StringBuilder builder, String propName, Object propValue) {
953         if (DEBUG_CONCISE_TOSTRING && propValue == null) return;
954         appendPropName(builder, propName).append(propValue);
955     }
956 
appendPropName(StringBuilder builder, String propName)957     private StringBuilder appendPropName(StringBuilder builder, String propName) {
958         return builder.append("; ").append(propName).append(": ");
959     }
960 }
961