Home | History | Annotate | Download | only in accessibility
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.accessibility;
     18 
     19 import android.content.Context;
     20 import android.os.Handler;
     21 import android.os.PowerManager;
     22 import android.util.DebugUtils;
     23 import android.util.ExceptionUtils;
     24 import android.util.Log;
     25 import android.util.Pools.SimplePool;
     26 import android.util.Slog;
     27 import android.util.SparseBooleanArray;
     28 import android.view.Choreographer;
     29 import android.view.InputDevice;
     30 import android.view.InputEvent;
     31 import android.view.InputFilter;
     32 import android.view.KeyEvent;
     33 import android.view.MotionEvent;
     34 import android.view.accessibility.AccessibilityEvent;
     35 
     36 import com.android.internal.util.BitUtils;
     37 import com.android.server.LocalServices;
     38 import com.android.server.policy.WindowManagerPolicy;
     39 
     40 /**
     41  * This class is an input filter for implementing accessibility features such
     42  * as display magnification and explore by touch.
     43  *
     44  * NOTE: This class has to be created and poked only from the main thread.
     45  */
     46 class AccessibilityInputFilter extends InputFilter implements EventStreamTransformation {
     47 
     48     private static final String TAG = AccessibilityInputFilter.class.getSimpleName();
     49 
     50     private static final boolean DEBUG = false;
     51 
     52     /**
     53      * Flag for enabling the screen magnification feature.
     54      *
     55      * @see #setUserAndEnabledFeatures(int, int)
     56      */
     57     static final int FLAG_FEATURE_SCREEN_MAGNIFIER = 0x00000001;
     58 
     59     /**
     60      * Flag for enabling the touch exploration feature.
     61      *
     62      * @see #setUserAndEnabledFeatures(int, int)
     63      */
     64     static final int FLAG_FEATURE_TOUCH_EXPLORATION = 0x00000002;
     65 
     66     /**
     67      * Flag for enabling the filtering key events feature.
     68      *
     69      * @see #setUserAndEnabledFeatures(int, int)
     70      */
     71     static final int FLAG_FEATURE_FILTER_KEY_EVENTS = 0x00000004;
     72 
     73     /**
     74      * Flag for enabling "Automatically click on mouse stop" feature.
     75      *
     76      * @see #setUserAndEnabledFeatures(int, int)
     77      */
     78     static final int FLAG_FEATURE_AUTOCLICK = 0x00000008;
     79 
     80     /**
     81      * Flag for enabling motion event injection.
     82      *
     83      * @see #setUserAndEnabledFeatures(int, int)
     84      */
     85     static final int FLAG_FEATURE_INJECT_MOTION_EVENTS = 0x00000010;
     86 
     87     /**
     88      * Flag for enabling the feature to control the screen magnifier. If
     89      * {@link #FLAG_FEATURE_SCREEN_MAGNIFIER} is set this flag is ignored
     90      * as the screen magnifier feature performs a super set of the work
     91      * performed by this feature.
     92      *
     93      * @see #setUserAndEnabledFeatures(int, int)
     94      */
     95     static final int FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER = 0x00000020;
     96 
     97     /**
     98      * Flag for enabling the feature to trigger the screen magnifier
     99      * from another on-device interaction.
    100      */
    101     static final int FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER = 0x00000040;
    102 
    103     static final int FEATURES_AFFECTING_MOTION_EVENTS = FLAG_FEATURE_INJECT_MOTION_EVENTS
    104             | FLAG_FEATURE_AUTOCLICK | FLAG_FEATURE_TOUCH_EXPLORATION
    105             | FLAG_FEATURE_SCREEN_MAGNIFIER | FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER;
    106 
    107     private final Runnable mProcessBatchedEventsRunnable = new Runnable() {
    108         @Override
    109         public void run() {
    110             final long frameTimeNanos = mChoreographer.getFrameTimeNanos();
    111             if (DEBUG) {
    112                 Slog.i(TAG, "Begin batch processing for frame: " + frameTimeNanos);
    113             }
    114             processBatchedEvents(frameTimeNanos);
    115             if (DEBUG) {
    116                 Slog.i(TAG, "End batch processing.");
    117             }
    118             if (mEventQueue != null) {
    119                 scheduleProcessBatchedEvents();
    120             }
    121         }
    122     };
    123 
    124     private final Context mContext;
    125 
    126     private final PowerManager mPm;
    127 
    128     private final AccessibilityManagerService mAms;
    129 
    130     private final Choreographer mChoreographer;
    131 
    132     private boolean mInstalled;
    133 
    134     private int mUserId;
    135 
    136     private int mEnabledFeatures;
    137 
    138     private TouchExplorer mTouchExplorer;
    139 
    140     private MagnificationGestureHandler mMagnificationGestureHandler;
    141 
    142     private MotionEventInjector mMotionEventInjector;
    143 
    144     private AutoclickController mAutoclickController;
    145 
    146     private KeyboardInterceptor mKeyboardInterceptor;
    147 
    148     private EventStreamTransformation mEventHandler;
    149 
    150     private MotionEventHolder mEventQueue;
    151 
    152     private EventStreamState mMouseStreamState;
    153 
    154     private EventStreamState mTouchScreenStreamState;
    155 
    156     private EventStreamState mKeyboardStreamState;
    157 
    158     AccessibilityInputFilter(Context context, AccessibilityManagerService service) {
    159         super(context.getMainLooper());
    160         mContext = context;
    161         mAms = service;
    162         mPm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
    163         mChoreographer = Choreographer.getInstance();
    164     }
    165 
    166     @Override
    167     public void onInstalled() {
    168         if (DEBUG) {
    169             Slog.d(TAG, "Accessibility input filter installed.");
    170         }
    171         mInstalled = true;
    172         disableFeatures();
    173         enableFeatures();
    174         super.onInstalled();
    175     }
    176 
    177     @Override
    178     public void onUninstalled() {
    179         if (DEBUG) {
    180             Slog.d(TAG, "Accessibility input filter uninstalled.");
    181         }
    182         mInstalled = false;
    183         disableFeatures();
    184         super.onUninstalled();
    185     }
    186 
    187     @Override
    188     public void onInputEvent(InputEvent event, int policyFlags) {
    189         if (DEBUG) {
    190             Slog.d(TAG, "Received event: " + event + ", policyFlags=0x"
    191                     + Integer.toHexString(policyFlags));
    192         }
    193 
    194         if (mEventHandler == null) {
    195             if (DEBUG) Slog.d(TAG, "mEventHandler == null for event " + event);
    196             super.onInputEvent(event, policyFlags);
    197             return;
    198         }
    199 
    200         EventStreamState state = getEventStreamState(event);
    201         if (state == null) {
    202             super.onInputEvent(event, policyFlags);
    203             return;
    204         }
    205 
    206         int eventSource = event.getSource();
    207         if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) {
    208             state.reset();
    209             mEventHandler.clearEvents(eventSource);
    210             super.onInputEvent(event, policyFlags);
    211             return;
    212         }
    213 
    214         if (state.updateDeviceId(event.getDeviceId())) {
    215             mEventHandler.clearEvents(eventSource);
    216         }
    217 
    218         if (!state.deviceIdValid()) {
    219             super.onInputEvent(event, policyFlags);
    220             return;
    221         }
    222 
    223         if (event instanceof MotionEvent) {
    224             if ((mEnabledFeatures & FEATURES_AFFECTING_MOTION_EVENTS) != 0) {
    225                 MotionEvent motionEvent = (MotionEvent) event;
    226                 processMotionEvent(state, motionEvent, policyFlags);
    227                 return;
    228             } else {
    229                 super.onInputEvent(event, policyFlags);
    230             }
    231         } else if (event instanceof KeyEvent) {
    232             KeyEvent keyEvent = (KeyEvent) event;
    233             processKeyEvent(state, keyEvent, policyFlags);
    234         }
    235     }
    236 
    237     /**
    238      * Gets current event stream state associated with an input event.
    239      * @return The event stream state that should be used for the event. Null if the event should
    240      *     not be handled by #AccessibilityInputFilter.
    241      */
    242     private EventStreamState getEventStreamState(InputEvent event) {
    243         if (event instanceof MotionEvent) {
    244           if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
    245               if (mTouchScreenStreamState == null) {
    246                   mTouchScreenStreamState = new TouchScreenEventStreamState();
    247               }
    248               return mTouchScreenStreamState;
    249           }
    250           if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
    251               if (mMouseStreamState == null) {
    252                   mMouseStreamState = new MouseEventStreamState();
    253               }
    254               return mMouseStreamState;
    255           }
    256         } else if (event instanceof KeyEvent) {
    257           if (event.isFromSource(InputDevice.SOURCE_KEYBOARD)) {
    258               if (mKeyboardStreamState == null) {
    259                   mKeyboardStreamState = new KeyboardEventStreamState();
    260               }
    261               return mKeyboardStreamState;
    262           }
    263         }
    264         return null;
    265     }
    266 
    267     private void processMotionEvent(EventStreamState state, MotionEvent event, int policyFlags) {
    268         if (!state.shouldProcessScroll() && event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
    269             super.onInputEvent(event, policyFlags);
    270             return;
    271         }
    272 
    273         if (!state.shouldProcessMotionEvent(event)) {
    274             return;
    275         }
    276 
    277         batchMotionEvent(event, policyFlags);
    278     }
    279 
    280     private void processKeyEvent(EventStreamState state, KeyEvent event, int policyFlags) {
    281         if (!state.shouldProcessKeyEvent(event)) {
    282             super.onInputEvent(event, policyFlags);
    283             return;
    284         }
    285         mEventHandler.onKeyEvent(event, policyFlags);
    286     }
    287 
    288     private void scheduleProcessBatchedEvents() {
    289         mChoreographer.postCallback(Choreographer.CALLBACK_INPUT,
    290                 mProcessBatchedEventsRunnable, null);
    291     }
    292 
    293     private void batchMotionEvent(MotionEvent event, int policyFlags) {
    294         if (DEBUG) {
    295             Slog.i(TAG, "Batching event: " + event + ", policyFlags: " + policyFlags);
    296         }
    297         if (mEventQueue == null) {
    298             mEventQueue = MotionEventHolder.obtain(event, policyFlags);
    299             scheduleProcessBatchedEvents();
    300             return;
    301         }
    302         if (mEventQueue.event.addBatch(event)) {
    303             return;
    304         }
    305         MotionEventHolder holder = MotionEventHolder.obtain(event, policyFlags);
    306         holder.next = mEventQueue;
    307         mEventQueue.previous = holder;
    308         mEventQueue = holder;
    309     }
    310 
    311     private void processBatchedEvents(long frameNanos) {
    312         MotionEventHolder current = mEventQueue;
    313         if (current == null) {
    314             return;
    315         }
    316         while (current.next != null) {
    317             current = current.next;
    318         }
    319         while (true) {
    320             if (current == null) {
    321                 mEventQueue = null;
    322                 break;
    323             }
    324             if (current.event.getEventTimeNano() >= frameNanos) {
    325                 // Finished with this choreographer frame. Do the rest on the next one.
    326                 current.next = null;
    327                 break;
    328             }
    329             handleMotionEvent(current.event, current.policyFlags);
    330             MotionEventHolder prior = current;
    331             current = current.previous;
    332             prior.recycle();
    333         }
    334     }
    335 
    336     private void handleMotionEvent(MotionEvent event, int policyFlags) {
    337         if (DEBUG) {
    338             Slog.i(TAG, "Handling batched event: " + event + ", policyFlags: " + policyFlags);
    339         }
    340         // Since we do batch processing it is possible that by the time the
    341         // next batch is processed the event handle had been set to null.
    342         if (mEventHandler != null) {
    343             mPm.userActivity(event.getEventTime(), false);
    344             MotionEvent transformedEvent = MotionEvent.obtain(event);
    345             mEventHandler.onMotionEvent(transformedEvent, event, policyFlags);
    346             transformedEvent.recycle();
    347         } else {
    348             if (DEBUG) Slog.d(TAG, "mEventHandler == null for " + event);
    349         }
    350     }
    351 
    352     @Override
    353     public void onMotionEvent(MotionEvent transformedEvent, MotionEvent rawEvent,
    354             int policyFlags) {
    355         sendInputEvent(transformedEvent, policyFlags);
    356     }
    357 
    358     @Override
    359     public void onKeyEvent(KeyEvent event, int policyFlags) {
    360         sendInputEvent(event, policyFlags);
    361     }
    362 
    363     @Override
    364     public void onAccessibilityEvent(AccessibilityEvent event) {
    365         // TODO Implement this to inject the accessibility event
    366         //      into the accessibility manager service similarly
    367         //      to how this is done for input events.
    368     }
    369 
    370     @Override
    371     public void setNext(EventStreamTransformation sink) {
    372         /* do nothing */
    373     }
    374 
    375     @Override
    376     public EventStreamTransformation getNext() {
    377         return null;
    378     }
    379 
    380     @Override
    381     public void clearEvents(int inputSource) {
    382         /* do nothing */
    383     }
    384 
    385     void setUserAndEnabledFeatures(int userId, int enabledFeatures) {
    386         if (DEBUG) {
    387             Slog.i(TAG, "setUserAndEnabledFeatures(userId = " + userId + ", enabledFeatures = 0x"
    388                     + Integer.toHexString(enabledFeatures) + ")");
    389         }
    390         if (mEnabledFeatures == enabledFeatures && mUserId == userId) {
    391             return;
    392         }
    393         if (mInstalled) {
    394             disableFeatures();
    395         }
    396         mUserId = userId;
    397         mEnabledFeatures = enabledFeatures;
    398         if (mInstalled) {
    399             enableFeatures();
    400         }
    401     }
    402 
    403     void notifyAccessibilityEvent(AccessibilityEvent event) {
    404         if (mEventHandler != null) {
    405             mEventHandler.onAccessibilityEvent(event);
    406         }
    407     }
    408 
    409     void notifyAccessibilityButtonClicked() {
    410         if (mMagnificationGestureHandler != null) {
    411             mMagnificationGestureHandler.notifyShortcutTriggered();
    412         }
    413     }
    414 
    415     private void enableFeatures() {
    416         if (DEBUG) Slog.i(TAG, "enableFeatures()");
    417 
    418         resetStreamState();
    419 
    420         if ((mEnabledFeatures & FLAG_FEATURE_AUTOCLICK) != 0) {
    421             mAutoclickController = new AutoclickController(mContext, mUserId);
    422             addFirstEventHandler(mAutoclickController);
    423         }
    424 
    425         if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
    426             mTouchExplorer = new TouchExplorer(mContext, mAms);
    427             addFirstEventHandler(mTouchExplorer);
    428         }
    429 
    430         if ((mEnabledFeatures & FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER) != 0
    431                 || ((mEnabledFeatures & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0)
    432                 || ((mEnabledFeatures & FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER) != 0)) {
    433             final boolean detectControlGestures = (mEnabledFeatures
    434                     & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0;
    435             final boolean triggerable = (mEnabledFeatures
    436                     & FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER) != 0;
    437             mMagnificationGestureHandler = new MagnificationGestureHandler(
    438                     mContext, mAms.getMagnificationController(),
    439                     detectControlGestures, triggerable);
    440             addFirstEventHandler(mMagnificationGestureHandler);
    441         }
    442 
    443         if ((mEnabledFeatures & FLAG_FEATURE_INJECT_MOTION_EVENTS) != 0) {
    444             mMotionEventInjector = new MotionEventInjector(mContext.getMainLooper());
    445             addFirstEventHandler(mMotionEventInjector);
    446             mAms.setMotionEventInjector(mMotionEventInjector);
    447         }
    448 
    449         if ((mEnabledFeatures & FLAG_FEATURE_FILTER_KEY_EVENTS) != 0) {
    450             mKeyboardInterceptor = new KeyboardInterceptor(mAms,
    451                     LocalServices.getService(WindowManagerPolicy.class));
    452             addFirstEventHandler(mKeyboardInterceptor);
    453         }
    454     }
    455 
    456     /**
    457      * Adds an event handler to the event handler chain. The handler is added at the beginning of
    458      * the chain.
    459      *
    460      * @param handler The handler to be added to the event handlers list.
    461      */
    462     private void addFirstEventHandler(EventStreamTransformation handler) {
    463         if (mEventHandler != null) {
    464             handler.setNext(mEventHandler);
    465         } else {
    466             handler.setNext(this);
    467         }
    468         mEventHandler = handler;
    469     }
    470 
    471     private void disableFeatures() {
    472         // Give the features a chance to process any batched events so we'll keep a consistent
    473         // event stream
    474         processBatchedEvents(Long.MAX_VALUE);
    475         if (mMotionEventInjector != null) {
    476             mAms.setMotionEventInjector(null);
    477             mMotionEventInjector.onDestroy();
    478             mMotionEventInjector = null;
    479         }
    480         if (mAutoclickController != null) {
    481             mAutoclickController.onDestroy();
    482             mAutoclickController = null;
    483         }
    484         if (mTouchExplorer != null) {
    485             mTouchExplorer.onDestroy();
    486             mTouchExplorer = null;
    487         }
    488         if (mMagnificationGestureHandler != null) {
    489             mMagnificationGestureHandler.onDestroy();
    490             mMagnificationGestureHandler = null;
    491         }
    492         if (mKeyboardInterceptor != null) {
    493             mKeyboardInterceptor.onDestroy();
    494             mKeyboardInterceptor = null;
    495         }
    496 
    497         mEventHandler = null;
    498         resetStreamState();
    499     }
    500 
    501     void resetStreamState() {
    502         if (mTouchScreenStreamState != null) {
    503             mTouchScreenStreamState.reset();
    504         }
    505         if (mMouseStreamState != null) {
    506             mMouseStreamState.reset();
    507         }
    508         if (mKeyboardStreamState != null) {
    509             mKeyboardStreamState.reset();
    510         }
    511     }
    512 
    513     @Override
    514     public void onDestroy() {
    515         /* ignore */
    516     }
    517 
    518     private static class MotionEventHolder {
    519         private static final int MAX_POOL_SIZE = 32;
    520         private static final SimplePool<MotionEventHolder> sPool =
    521                 new SimplePool<MotionEventHolder>(MAX_POOL_SIZE);
    522 
    523         public int policyFlags;
    524         public MotionEvent event;
    525         public MotionEventHolder next;
    526         public MotionEventHolder previous;
    527 
    528         public static MotionEventHolder obtain(MotionEvent event, int policyFlags) {
    529             MotionEventHolder holder = sPool.acquire();
    530             if (holder == null) {
    531                 holder = new MotionEventHolder();
    532             }
    533             holder.event = MotionEvent.obtain(event);
    534             holder.policyFlags = policyFlags;
    535             return holder;
    536         }
    537 
    538         public void recycle() {
    539             event.recycle();
    540             event = null;
    541             policyFlags = 0;
    542             next = null;
    543             previous = null;
    544             sPool.release(this);
    545         }
    546     }
    547 
    548     /**
    549      * Keeps state of event streams observed for an input device with a certain source.
    550      * Provides information about whether motion and key events should be processed by accessibility
    551      * #EventStreamTransformations. Base implementation describes behaviour for event sources that
    552      * whose events should not be handled by a11y event stream transformations.
    553      */
    554     private static class EventStreamState {
    555         private int mDeviceId;
    556 
    557         EventStreamState() {
    558             mDeviceId = -1;
    559         }
    560 
    561         /**
    562          * Updates the ID of the device associated with the state. If the ID changes, resets
    563          * internal state.
    564          *
    565          * @param deviceId Updated input device ID.
    566          * @return Whether the device ID has changed.
    567          */
    568         public boolean updateDeviceId(int deviceId) {
    569             if (mDeviceId == deviceId) {
    570                 return false;
    571             }
    572             // Reset clears internal state, so make sure it's called before |mDeviceId| is updated.
    573             reset();
    574             mDeviceId = deviceId;
    575             return true;
    576         }
    577 
    578         /**
    579          * @return Whether device ID is valid.
    580          */
    581         public boolean deviceIdValid() {
    582             return mDeviceId >= 0;
    583         }
    584 
    585         /**
    586          * Resets the event stream state.
    587          */
    588         public void reset() {
    589             mDeviceId = -1;
    590         }
    591 
    592         /**
    593          * @return Whether scroll events for device should be handled by event transformations.
    594          */
    595         public boolean shouldProcessScroll() {
    596             return false;
    597         }
    598 
    599         /**
    600          * @param event An observed motion event.
    601          * @return Whether the event should be handled by event transformations.
    602          */
    603         public boolean shouldProcessMotionEvent(MotionEvent event) {
    604             return false;
    605         }
    606 
    607         /**
    608          * @param event An observed key event.
    609          * @return Whether the event should be handled by event transformations.
    610          */
    611         public boolean shouldProcessKeyEvent(KeyEvent event) {
    612             return false;
    613         }
    614     }
    615 
    616     /**
    617      * Keeps state of stream of events from a mouse device.
    618      */
    619     private static class MouseEventStreamState extends EventStreamState {
    620         private boolean mMotionSequenceStarted;
    621 
    622         public MouseEventStreamState() {
    623             reset();
    624         }
    625 
    626         @Override
    627         final public void reset() {
    628             super.reset();
    629             mMotionSequenceStarted = false;
    630         }
    631 
    632         @Override
    633         final public boolean shouldProcessScroll() {
    634             return true;
    635         }
    636 
    637         @Override
    638         final public boolean shouldProcessMotionEvent(MotionEvent event) {
    639             if (mMotionSequenceStarted) {
    640                 return true;
    641             }
    642             // Wait for down or move event to start processing mouse events.
    643             int action = event.getActionMasked();
    644             mMotionSequenceStarted =
    645                     action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_HOVER_MOVE;
    646             return mMotionSequenceStarted;
    647         }
    648     }
    649 
    650     /**
    651      * Keeps state of stream of events from a touch screen device.
    652      */
    653     private static class TouchScreenEventStreamState extends EventStreamState {
    654         private boolean mTouchSequenceStarted;
    655         private boolean mHoverSequenceStarted;
    656 
    657         public TouchScreenEventStreamState() {
    658             reset();
    659         }
    660 
    661         @Override
    662         final public void reset() {
    663             super.reset();
    664             mTouchSequenceStarted = false;
    665             mHoverSequenceStarted = false;
    666         }
    667 
    668         @Override
    669         final public boolean shouldProcessMotionEvent(MotionEvent event) {
    670             // Wait for a down touch event to start processing.
    671             if (event.isTouchEvent()) {
    672                 if (mTouchSequenceStarted) {
    673                     return true;
    674                 }
    675                 mTouchSequenceStarted = event.getActionMasked() == MotionEvent.ACTION_DOWN;
    676                 return mTouchSequenceStarted;
    677             }
    678 
    679             // Wait for an enter hover event to start processing.
    680             if (mHoverSequenceStarted) {
    681                 return true;
    682             }
    683             mHoverSequenceStarted = event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER;
    684             return mHoverSequenceStarted;
    685         }
    686     }
    687 
    688     /**
    689      * Keeps state of streams of events from all keyboard devices.
    690      */
    691     private static class KeyboardEventStreamState extends EventStreamState {
    692         private SparseBooleanArray mEventSequenceStartedMap = new SparseBooleanArray();
    693 
    694         public KeyboardEventStreamState() {
    695             reset();
    696         }
    697 
    698         @Override
    699         final public void reset() {
    700             super.reset();
    701             mEventSequenceStartedMap.clear();
    702         }
    703 
    704         /*
    705          * Key events from different devices may be interleaved. For example, the volume up and
    706          * down keys can come from different device IDs.
    707          */
    708         @Override
    709         public boolean updateDeviceId(int deviceId) {
    710             return false;
    711         }
    712 
    713         // We manage all device ids simultaneously; there is no concept of validity.
    714         @Override
    715         public boolean deviceIdValid() {
    716             return true;
    717         }
    718 
    719 
    720         @Override
    721         final public boolean shouldProcessKeyEvent(KeyEvent event) {
    722             // For each keyboard device, wait for a down event from a device to start processing
    723             int deviceId = event.getDeviceId();
    724             if (mEventSequenceStartedMap.get(deviceId, false)) {
    725                 return true;
    726             }
    727             boolean shouldProcess = event.getAction() == KeyEvent.ACTION_DOWN;
    728             mEventSequenceStartedMap.put(deviceId, shouldProcess);
    729             return shouldProcess;
    730         }
    731     }
    732 }
    733