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