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