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