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 = UNDEFINED;
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 = UNDEFINED;
140        clearSourceNode();
141        if (root != null) {
142            if (virtualDescendantId == UNDEFINED ||
143                    virtualDescendantId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
144                important = root.isImportantForAccessibility();
145                mSourceNode = root.createAccessibilityNodeInfo();
146            } else {
147                AccessibilityNodeProvider provider = root.getAccessibilityNodeProvider();
148                if (provider != null) {
149                    mSourceNode = provider.createAccessibilityNodeInfo(virtualDescendantId);
150                }
151            }
152
153            mSourceWindowId = root.getAccessibilityWindowId();
154        }
155        setBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY, important);
156    }
157
158    /**
159     * Gets the {@link AccessibilityNodeInfo} of the event source.
160     * <p>
161     *   <strong>Note:</strong> It is a client responsibility to recycle the received info
162     *   by calling {@link AccessibilityNodeInfo#recycle() AccessibilityNodeInfo#recycle()}
163     *   to avoid creating of multiple instances.
164     * </p>
165     * @return The info of the source.
166     */
167    public AccessibilityNodeInfo getSource() {
168        enforceSealed();
169        if (mSourceNode != null) {
170            return AccessibilityNodeInfo.obtain(mSourceNode);
171        }
172
173        return null;
174    }
175
176    /**
177     * Sets the window id.
178     *
179     * @param windowId The window id.
180     *
181     * @hide
182     */
183    public void setWindowId(int windowId) {
184        mSourceWindowId = windowId;
185    }
186
187    /**
188     * Gets the id of the window from which the event comes from.
189     *
190     * @return The window id.
191     */
192    public int getWindowId() {
193        return mSourceWindowId;
194    }
195
196    /**
197     * Gets if the source is checked.
198     *
199     * @return True if the view is checked, false otherwise.
200     */
201    public boolean isChecked() {
202        return getBooleanProperty(PROPERTY_CHECKED);
203    }
204
205    /**
206     * Sets if the source is checked.
207     *
208     * @param isChecked True if the view is checked, false otherwise.
209     *
210     * @throws IllegalStateException If called from an AccessibilityService.
211     */
212    public void setChecked(boolean isChecked) {
213        enforceNotSealed();
214        setBooleanProperty(PROPERTY_CHECKED, isChecked);
215    }
216
217    /**
218     * Gets if the source is enabled.
219     *
220     * @return True if the view is enabled, false otherwise.
221     */
222    public boolean isEnabled() {
223        return getBooleanProperty(PROPERTY_ENABLED);
224    }
225
226    /**
227     * Sets if the source is enabled.
228     *
229     * @param isEnabled True if the view is enabled, false otherwise.
230     *
231     * @throws IllegalStateException If called from an AccessibilityService.
232     */
233    public void setEnabled(boolean isEnabled) {
234        enforceNotSealed();
235        setBooleanProperty(PROPERTY_ENABLED, isEnabled);
236    }
237
238    /**
239     * Gets if the source is a password field.
240     *
241     * @return True if the view is a password field, false otherwise.
242     */
243    public boolean isPassword() {
244        return getBooleanProperty(PROPERTY_PASSWORD);
245    }
246
247    /**
248     * Sets if the source is a password field.
249     *
250     * @param isPassword True if the view is a password field, false otherwise.
251     *
252     * @throws IllegalStateException If called from an AccessibilityService.
253     */
254    public void setPassword(boolean isPassword) {
255        enforceNotSealed();
256        setBooleanProperty(PROPERTY_PASSWORD, isPassword);
257    }
258
259    /**
260     * Gets if the source is taking the entire screen.
261     *
262     * @return True if the source is full screen, false otherwise.
263     */
264    public boolean isFullScreen() {
265        return getBooleanProperty(PROPERTY_FULL_SCREEN);
266    }
267
268    /**
269     * Sets if the source is taking the entire screen.
270     *
271     * @param isFullScreen True if the source is full screen, false otherwise.
272     *
273     * @throws IllegalStateException If called from an AccessibilityService.
274     */
275    public void setFullScreen(boolean isFullScreen) {
276        enforceNotSealed();
277        setBooleanProperty(PROPERTY_FULL_SCREEN, isFullScreen);
278    }
279
280    /**
281     * Gets if the source is scrollable.
282     *
283     * @return True if the source is scrollable, false otherwise.
284     */
285    public boolean isScrollable() {
286        return getBooleanProperty(PROPERTY_SCROLLABLE);
287    }
288
289    /**
290     * Sets if the source is scrollable.
291     *
292     * @param scrollable True if the source is scrollable, false otherwise.
293     *
294     * @throws IllegalStateException If called from an AccessibilityService.
295     */
296    public void setScrollable(boolean scrollable) {
297        enforceNotSealed();
298        setBooleanProperty(PROPERTY_SCROLLABLE, scrollable);
299    }
300
301    /**
302     * Gets if the source is important for accessibility.
303     *
304     * <strong>Note:</strong> Used only internally to determine whether
305     * to deliver the event to a given accessibility service since some
306     * services may want to regard all views for accessibility while others
307     * may want to regard only the important views for accessibility.
308     *
309     * @return True if the source is important for accessibility,
310     *        false otherwise.
311     *
312     * @hide
313     */
314    public boolean isImportantForAccessibility() {
315        return getBooleanProperty(PROPERTY_IMPORTANT_FOR_ACCESSIBILITY);
316    }
317
318    /**
319     * Gets the number of items that can be visited.
320     *
321     * @return The number of items.
322     */
323    public int getItemCount() {
324        return mItemCount;
325    }
326
327    /**
328     * Sets the number of items that can be visited.
329     *
330     * @param itemCount The number of items.
331     *
332     * @throws IllegalStateException If called from an AccessibilityService.
333     */
334    public void setItemCount(int itemCount) {
335        enforceNotSealed();
336        mItemCount = itemCount;
337    }
338
339    /**
340     * Gets the index of the source in the list of items the can be visited.
341     *
342     * @return The current item index.
343     */
344    public int getCurrentItemIndex() {
345        return mCurrentItemIndex;
346    }
347
348    /**
349     * Sets the index of the source in the list of items that can be visited.
350     *
351     * @param currentItemIndex The current item index.
352     *
353     * @throws IllegalStateException If called from an AccessibilityService.
354     */
355    public void setCurrentItemIndex(int currentItemIndex) {
356        enforceNotSealed();
357        mCurrentItemIndex = currentItemIndex;
358    }
359
360    /**
361     * Gets the index of the first character of the changed sequence,
362     * or the beginning of a text selection or the index of the first
363     * visible item when scrolling.
364     *
365     * @return The index of the first character or selection
366     *        start or the first visible item.
367     */
368    public int getFromIndex() {
369        return mFromIndex;
370    }
371
372    /**
373     * Sets the index of the first character of the changed sequence
374     * or the beginning of a text selection or the index of the first
375     * visible item when scrolling.
376     *
377     * @param fromIndex The index of the first character or selection
378     *        start or the first visible item.
379     *
380     * @throws IllegalStateException If called from an AccessibilityService.
381     */
382    public void setFromIndex(int fromIndex) {
383        enforceNotSealed();
384        mFromIndex = fromIndex;
385    }
386
387    /**
388     * Gets the index of text selection end or the index of the last
389     * visible item when scrolling.
390     *
391     * @return The index of selection end or last item index.
392     */
393    public int getToIndex() {
394        return mToIndex;
395    }
396
397    /**
398     * Sets the index of text selection end or the index of the last
399     * visible item when scrolling.
400     *
401     * @param toIndex The index of selection end or last item index.
402     */
403    public void setToIndex(int toIndex) {
404        enforceNotSealed();
405        mToIndex = toIndex;
406    }
407
408    /**
409     * Gets the scroll offset of the source left edge in pixels.
410     *
411     * @return The scroll.
412     */
413    public int getScrollX() {
414        return mScrollX;
415    }
416
417    /**
418     * Sets the scroll offset of the source left edge in pixels.
419     *
420     * @param scrollX The scroll.
421     */
422    public void setScrollX(int scrollX) {
423        enforceNotSealed();
424        mScrollX = scrollX;
425    }
426
427    /**
428     * Gets the scroll offset of the source top edge in pixels.
429     *
430     * @return The scroll.
431     */
432    public int getScrollY() {
433        return mScrollY;
434    }
435
436    /**
437     * Sets the scroll offset of the source top edge in pixels.
438     *
439     * @param scrollY The scroll.
440     */
441    public void setScrollY(int scrollY) {
442        enforceNotSealed();
443        mScrollY = scrollY;
444    }
445
446    /**
447     * Gets the max scroll offset of the source left edge in pixels.
448     *
449     * @return The max scroll.
450     */
451    public int getMaxScrollX() {
452        return mMaxScrollX;
453    }
454
455    /**
456     * Sets the max scroll offset of the source left edge in pixels.
457     *
458     * @param maxScrollX The max scroll.
459     */
460    public void setMaxScrollX(int maxScrollX) {
461        enforceNotSealed();
462        mMaxScrollX = maxScrollX;
463    }
464
465    /**
466     * Gets the max scroll offset of the source top edge in pixels.
467     *
468     * @return The max scroll.
469     */
470    public int getMaxScrollY() {
471        return mMaxScrollY;
472    }
473
474    /**
475     * Sets the max scroll offset of the source top edge in pixels.
476     *
477     * @param maxScrollY The max scroll.
478     */
479    public void setMaxScrollY(int maxScrollY) {
480        enforceNotSealed();
481        mMaxScrollY = maxScrollY;
482    }
483
484    /**
485     * Gets the number of added characters.
486     *
487     * @return The number of added characters.
488     */
489    public int getAddedCount() {
490        return mAddedCount;
491    }
492
493    /**
494     * Sets the number of added characters.
495     *
496     * @param addedCount The number of added characters.
497     *
498     * @throws IllegalStateException If called from an AccessibilityService.
499     */
500    public void setAddedCount(int addedCount) {
501        enforceNotSealed();
502        mAddedCount = addedCount;
503    }
504
505    /**
506     * Gets the number of removed characters.
507     *
508     * @return The number of removed characters.
509     */
510    public int getRemovedCount() {
511        return mRemovedCount;
512    }
513
514    /**
515     * Sets the number of removed characters.
516     *
517     * @param removedCount The number of removed characters.
518     *
519     * @throws IllegalStateException If called from an AccessibilityService.
520     */
521    public void setRemovedCount(int removedCount) {
522        enforceNotSealed();
523        mRemovedCount = removedCount;
524    }
525
526    /**
527     * Gets the class name of the source.
528     *
529     * @return The class name.
530     */
531    public CharSequence getClassName() {
532        return mClassName;
533    }
534
535    /**
536     * Sets the class name of the source.
537     *
538     * @param className The lass name.
539     *
540     * @throws IllegalStateException If called from an AccessibilityService.
541     */
542    public void setClassName(CharSequence className) {
543        enforceNotSealed();
544        mClassName = className;
545    }
546
547    /**
548     * Gets the text of the event. The index in the list represents the priority
549     * of the text. Specifically, the lower the index the higher the priority.
550     *
551     * @return The text.
552     */
553    public List<CharSequence> getText() {
554        return mText;
555    }
556
557    /**
558     * Sets the text before a change.
559     *
560     * @return The text before the change.
561     */
562    public CharSequence getBeforeText() {
563        return mBeforeText;
564    }
565
566    /**
567     * Sets the text before a change.
568     *
569     * @param beforeText The text before the change.
570     *
571     * @throws IllegalStateException If called from an AccessibilityService.
572     */
573    public void setBeforeText(CharSequence beforeText) {
574        enforceNotSealed();
575        mBeforeText = beforeText;
576    }
577
578    /**
579     * Gets the description of the source.
580     *
581     * @return The description.
582     */
583    public CharSequence getContentDescription() {
584        return mContentDescription;
585    }
586
587    /**
588     * Sets the description of the source.
589     *
590     * @param contentDescription The description.
591     *
592     * @throws IllegalStateException If called from an AccessibilityService.
593     */
594    public void setContentDescription(CharSequence contentDescription) {
595        enforceNotSealed();
596        mContentDescription = contentDescription;
597    }
598
599    /**
600     * Gets the {@link Parcelable} data.
601     *
602     * @return The parcelable data.
603     */
604    public Parcelable getParcelableData() {
605        return mParcelableData;
606    }
607
608    /**
609     * Sets the {@link Parcelable} data of the event.
610     *
611     * @param parcelableData The parcelable data.
612     *
613     * @throws IllegalStateException If called from an AccessibilityService.
614     */
615    public void setParcelableData(Parcelable parcelableData) {
616        enforceNotSealed();
617        mParcelableData = parcelableData;
618    }
619
620    /**
621     * Gets the id of the source node.
622     *
623     * @return The id.
624     *
625     * @hide
626     */
627    public long getSourceNodeId() {
628        return mSourceNode != null ? mSourceNode.getSourceNodeId() : UNDEFINED;
629    }
630
631    /**
632     * Sets the unique id of the IAccessibilityServiceConnection over which
633     * this instance can send requests to the system.
634     *
635     * @param connectionId The connection id.
636     *
637     * @hide
638     */
639    public void setConnectionId(int connectionId) {
640        enforceNotSealed();
641        mConnectionId = connectionId;
642        if (mSourceNode != null) {
643            mSourceNode.setConnectionId(mConnectionId);
644        }
645    }
646
647    /**
648     * Sets if this instance is sealed.
649     *
650     * @param sealed Whether is sealed.
651     *
652     * @hide
653     */
654    public void setSealed(boolean sealed) {
655        mSealed = sealed;
656        if (mSourceNode != null) {
657            mSourceNode.setSealed(sealed);
658        }
659    }
660
661    /**
662     * Gets if this instance is sealed.
663     *
664     * @return Whether is sealed.
665     */
666    boolean isSealed() {
667        return mSealed;
668    }
669
670    /**
671     * Enforces that this instance is sealed.
672     *
673     * @throws IllegalStateException If this instance is not sealed.
674     */
675    void enforceSealed() {
676        if (!isSealed()) {
677            throw new IllegalStateException("Cannot perform this "
678                    + "action on a not sealed instance.");
679        }
680    }
681
682    /**
683     * Enforces that this instance is not sealed.
684     *
685     * @throws IllegalStateException If this instance is sealed.
686     */
687    void enforceNotSealed() {
688        if (isSealed()) {
689            throw new IllegalStateException("Cannot perform this "
690                    + "action on a sealed instance.");
691        }
692    }
693
694    /**
695     * Gets the value of a boolean property.
696     *
697     * @param property The property.
698     * @return The value.
699     */
700    private boolean getBooleanProperty(int property) {
701        return (mBooleanProperties & property) == property;
702    }
703
704    /**
705     * Sets a boolean property.
706     *
707     * @param property The property.
708     * @param value The value.
709     */
710    private void setBooleanProperty(int property, boolean value) {
711        if (value) {
712            mBooleanProperties |= property;
713        } else {
714            mBooleanProperties &= ~property;
715        }
716    }
717
718    /**
719     * Returns a cached instance if such is available or a new one is
720     * instantiated. The instance is initialized with data from the
721     * given record.
722     *
723     * @return An instance.
724     */
725    public static AccessibilityRecord obtain(AccessibilityRecord record) {
726       AccessibilityRecord clone = AccessibilityRecord.obtain();
727       clone.init(record);
728       return clone;
729    }
730
731    /**
732     * Returns a cached instance if such is available or a new one is
733     * instantiated.
734     *
735     * @return An instance.
736     */
737    public static AccessibilityRecord obtain() {
738        synchronized (sPoolLock) {
739            if (sPool != null) {
740                AccessibilityRecord record = sPool;
741                sPool = sPool.mNext;
742                sPoolSize--;
743                record.mNext = null;
744                record.mIsInPool = false;
745                return record;
746            }
747            return new AccessibilityRecord();
748        }
749    }
750
751    /**
752     * Return an instance back to be reused.
753     * <p>
754     * <strong>Note:</strong> You must not touch the object after calling this function.
755     *
756     * @throws IllegalStateException If the record is already recycled.
757     */
758    public void recycle() {
759        if (mIsInPool) {
760            throw new IllegalStateException("Record already recycled!");
761        }
762        clear();
763        synchronized (sPoolLock) {
764            if (sPoolSize <= MAX_POOL_SIZE) {
765                mNext = sPool;
766                sPool = this;
767                mIsInPool = true;
768                sPoolSize++;
769            }
770        }
771    }
772
773    /**
774     * Initialize this record from another one.
775     *
776     * @param record The to initialize from.
777     */
778    void init(AccessibilityRecord record) {
779        mSealed = record.mSealed;
780        mBooleanProperties = record.mBooleanProperties;
781        mCurrentItemIndex = record.mCurrentItemIndex;
782        mItemCount = record.mItemCount;
783        mFromIndex = record.mFromIndex;
784        mToIndex = record.mToIndex;
785        mScrollX = record.mScrollX;
786        mScrollY = record.mScrollY;
787        mMaxScrollX = record.mMaxScrollX;
788        mMaxScrollY = record.mMaxScrollY;
789        mAddedCount = record.mAddedCount;
790        mRemovedCount = record.mRemovedCount;
791        mClassName = record.mClassName;
792        mContentDescription = record.mContentDescription;
793        mBeforeText = record.mBeforeText;
794        mParcelableData = record.mParcelableData;
795        mText.addAll(record.mText);
796        mSourceWindowId = record.mSourceWindowId;
797        if (record.mSourceNode != null) {
798            mSourceNode = AccessibilityNodeInfo.obtain(record.mSourceNode);
799        }
800        mConnectionId = record.mConnectionId;
801    }
802
803    /**
804     * Clears the state of this instance.
805     */
806    void clear() {
807        mSealed = false;
808        mBooleanProperties = 0;
809        mCurrentItemIndex = UNDEFINED;
810        mItemCount = UNDEFINED;
811        mFromIndex = UNDEFINED;
812        mToIndex = UNDEFINED;
813        mScrollX = UNDEFINED;
814        mScrollY = UNDEFINED;
815        mMaxScrollX = UNDEFINED;
816        mMaxScrollY = UNDEFINED;
817        mAddedCount = UNDEFINED;
818        mRemovedCount = UNDEFINED;
819        mClassName = null;
820        mContentDescription = null;
821        mBeforeText = null;
822        mParcelableData = null;
823        mText.clear();
824        clearSourceNode();
825        mSourceWindowId = UNDEFINED;
826        mConnectionId = UNDEFINED;
827    }
828
829    private void clearSourceNode() {
830        if (mSourceNode != null) {
831            mSourceNode.recycle();
832            mSourceNode = null;
833        }
834    }
835
836    @Override
837    public String toString() {
838        StringBuilder builder = new StringBuilder();
839        builder.append(" [ ClassName: " + mClassName);
840        builder.append("; Text: " + mText);
841        builder.append("; ContentDescription: " + mContentDescription);
842        builder.append("; ItemCount: " + mItemCount);
843        builder.append("; CurrentItemIndex: " + mCurrentItemIndex);
844        builder.append("; IsEnabled: " + getBooleanProperty(PROPERTY_ENABLED));
845        builder.append("; IsPassword: " + getBooleanProperty(PROPERTY_PASSWORD));
846        builder.append("; IsChecked: " + getBooleanProperty(PROPERTY_CHECKED));
847        builder.append("; IsFullScreen: " + getBooleanProperty(PROPERTY_FULL_SCREEN));
848        builder.append("; Scrollable: " + getBooleanProperty(PROPERTY_SCROLLABLE));
849        builder.append("; BeforeText: " + mBeforeText);
850        builder.append("; FromIndex: " + mFromIndex);
851        builder.append("; ToIndex: " + mToIndex);
852        builder.append("; ScrollX: " + mScrollX);
853        builder.append("; ScrollY: " + mScrollY);
854        builder.append("; MaxScrollX: " + mMaxScrollX);
855        builder.append("; MaxScrollY: " + mMaxScrollY);
856        builder.append("; AddedCount: " + mAddedCount);
857        builder.append("; RemovedCount: " + mRemovedCount);
858        builder.append("; ParcelableData: " + mParcelableData);
859        builder.append(" ]");
860        return builder.toString();
861    }
862}
863