content: /*
   * Copyright (C) 2011 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.accessibility;
  
  import android.content.Context;
  import android.os.PowerManager;
  import android.util.Slog;
  import android.util.SparseArray;
  import android.util.SparseBooleanArray;
  import android.view.Display;
  import android.view.InputDevice;
  import android.view.InputEvent;
  import android.view.InputFilter;
  import android.view.KeyEvent;
  import android.view.MotionEvent;
  import android.view.accessibility.AccessibilityEvent;
  
  import com.android.server.LocalServices;
  import com.android.server.policy.WindowManagerPolicy;
  
  import java.util.ArrayList;
  
  /**
   * This class is an input filter for implementing accessibility features such
   * as display magnification and explore by touch.
   *
   * NOTE: This class has to be created and poked only from the main thread.
   */
  class AccessibilityInputFilter extends InputFilter implements EventStreamTransformation {
  
      private static final String TAG = AccessibilityInputFilter.class.getSimpleName();
  
      private static final boolean DEBUG = false;
  
      /**
       * Flag for enabling the screen magnification feature.
       *
       * @see #setUserAndEnabledFeatures(int, int)
       */
      static final int FLAG_FEATURE_SCREEN_MAGNIFIER = 0x00000001;
  
      /**
       * Flag for enabling the touch exploration feature.
       *
       * @see #setUserAndEnabledFeatures(int, int)
       */
      static final int FLAG_FEATURE_TOUCH_EXPLORATION = 0x00000002;
  
      /**
       * Flag for enabling the filtering key events feature.
       *
       * @see #setUserAndEnabledFeatures(int, int)
       */
      static final int FLAG_FEATURE_FILTER_KEY_EVENTS = 0x00000004;
  
      /**
       * Flag for enabling "Automatically click on mouse stop" feature.
       *
       * @see #setUserAndEnabledFeatures(int, int)
       */
      static final int FLAG_FEATURE_AUTOCLICK = 0x00000008;
  
      /**
       * Flag for enabling motion event injection.
       *
       * @see #setUserAndEnabledFeatures(int, int)
       */
      static final int FLAG_FEATURE_INJECT_MOTION_EVENTS = 0x00000010;
  
      /**
       * Flag for enabling the feature to control the screen magnifier. If
       * {@link #FLAG_FEATURE_SCREEN_MAGNIFIER} is set this flag is ignored
       * as the screen magnifier feature performs a super set of the work
       * performed by this feature.
       *
       * @see #setUserAndEnabledFeatures(int, int)
       */
      static final int FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER = 0x00000020;
  
      /**
       * Flag for enabling the feature to trigger the screen magnifier
       * from another on-device interaction.
       */
      static final int FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER = 0x00000040;
  
      static final int FEATURES_AFFECTING_MOTION_EVENTS = FLAG_FEATURE_INJECT_MOTION_EVENTS
              | FLAG_FEATURE_AUTOCLICK | FLAG_FEATURE_TOUCH_EXPLORATION
              | FLAG_FEATURE_SCREEN_MAGNIFIER | FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER;
  
      private final Context mContext;
  
      private final PowerManager mPm;
  
      private final AccessibilityManagerService mAms;
  
      private final SparseArray<EventStreamTransformation> mEventHandler;
  
      private final SparseArray<TouchExplorer> mTouchExplorer = new SparseArray<>(0);
  
      private final SparseArray<MagnificationGestureHandler> mMagnificationGestureHandler =
              new SparseArray<>(0);
  
      private final SparseArray<MotionEventInjector> mMotionEventInjector = new SparseArray<>(0);
  
      private AutoclickController mAutoclickController;
  
      private KeyboardInterceptor mKeyboardInterceptor;
  
      private boolean mInstalled;
  
      private int mUserId;
  
      private int mEnabledFeatures;
  
      private EventStreamState mMouseStreamState;
  
      private EventStreamState mTouchScreenStreamState;
  
      private EventStreamState mKeyboardStreamState;
  
      AccessibilityInputFilter(Context context, AccessibilityManagerService service) {
          this(context, service, new SparseArray<>(0));
      }
  
      AccessibilityInputFilter(Context context, AccessibilityManagerService service,
              SparseArray<EventStreamTransformation> eventHandler) {
          super(context.getMainLooper());
          mContext = context;
          mAms = service;
          mPm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
          mEventHandler = eventHandler;
      }
  
      @Override
      public void onInstalled() {
          if (DEBUG) {
              Slog.d(TAG, "Accessibility input filter installed.");
          }
          mInstalled = true;
          disableFeatures();
          enableFeatures();
          super.onInstalled();
      }
  
      @Override
      public void onUninstalled() {
          if (DEBUG) {
              Slog.d(TAG, "Accessibility input filter uninstalled.");
          }
          mInstalled = false;
          disableFeatures();
          super.onUninstalled();
      }
  
      void onDisplayChanged() {
          if (mInstalled) {
              disableFeatures();
              enableFeatures();
          }
      }
  
      @Override
      public void onInputEvent(InputEvent event, int policyFlags) {
          if (DEBUG) {
              Slog.d(TAG, "Received event: " + event + ", policyFlags=0x"
                      + Integer.toHexString(policyFlags));
          }
  
          if (mEventHandler.size() == 0) {
              if (DEBUG) Slog.d(TAG, "No mEventHandler for event " + event);
              super.onInputEvent(event, policyFlags);
              return;
          }
  
          EventStreamState state = getEventStreamState(event);
          if (state == null) {
              super.onInputEvent(event, policyFlags);
              return;
          }
  
          int eventSource = event.getSource();
          if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) {
              state.reset();
              clearEventsForAllEventHandlers(eventSource);
              super.onInputEvent(event, policyFlags);
              return;
          }
  
          if (state.updateInputSource(event.getSource())) {
              clearEventsForAllEventHandlers(eventSource);
          }
  
          if (!state.inputSourceValid()) {
              super.onInputEvent(event, policyFlags);
              return;
          }
  
          if (event instanceof MotionEvent) {
              if ((mEnabledFeatures & FEATURES_AFFECTING_MOTION_EVENTS) != 0) {
                  MotionEvent motionEvent = (MotionEvent) event;
                  processMotionEvent(state, motionEvent, policyFlags);
                  return;
              } else {
                  super.onInputEvent(event, policyFlags);
              }
          } else if (event instanceof KeyEvent) {
              KeyEvent keyEvent = (KeyEvent) event;
              processKeyEvent(state, keyEvent, policyFlags);
          }
      }
  
      /**
       * Gets current event stream state associated with an input event.
       * @return The event stream state that should be used for the event. Null if the event should
       *     not be handled by #AccessibilityInputFilter.
       */
      private EventStreamState getEventStreamState(InputEvent event) {
          if (event instanceof MotionEvent) {
            if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
                if (mTouchScreenStreamState == null) {
                    mTouchScreenStreamState = new TouchScreenEventStreamState();
                }
                return mTouchScreenStreamState;
            }
            if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
                if (mMouseStreamState == null) {
                    mMouseStreamState = new MouseEventStreamState();
                }
                return mMouseStreamState;
            }
          } else if (event instanceof KeyEvent) {
            if (event.isFromSource(InputDevice.SOURCE_KEYBOARD)) {
                if (mKeyboardStreamState == null) {
                    mKeyboardStreamState = new KeyboardEventStreamState();
                }
                return mKeyboardStreamState;
            }
          }
          return null;
      }
  
      private void clearEventsForAllEventHandlers(int eventSource) {
          for (int i = 0; i < mEventHandler.size(); i++) {
              final EventStreamTransformation eventHandler = mEventHandler.valueAt(i);
              if (eventHandler != null) {
                  eventHandler.clearEvents(eventSource);
              }
          }
      }
  
      private void processMotionEvent(EventStreamState state, MotionEvent event, int policyFlags) {
          if (!state.shouldProcessScroll() && event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
              super.onInputEvent(event, policyFlags);
              return;
          }
  
          if (!state.shouldProcessMotionEvent(event)) {
              return;
          }
  
          handleMotionEvent(event, policyFlags);
      }
  
      private void processKeyEvent(EventStreamState state, KeyEvent event, int policyFlags) {
          if (!state.shouldProcessKeyEvent(event)) {
              super.onInputEvent(event, policyFlags);
              return;
          }
          // Since the display id of KeyEvent always would be -1 and there is only one
          // KeyboardInterceptor for all display, pass KeyEvent to the mEventHandler of
          // DEFAULT_DISPLAY to handle.
          mEventHandler.get(Display.DEFAULT_DISPLAY).onKeyEvent(event, policyFlags);
      }
  
      private void handleMotionEvent(MotionEvent event, int policyFlags) {
          if (DEBUG) {
              Slog.i(TAG, "Handling motion event: " + event + ", policyFlags: " + policyFlags);
          }
          mPm.userActivity(event.getEventTime(), false);
          MotionEvent transformedEvent = MotionEvent.obtain(event);
          final int displayId = event.getDisplayId();
          mEventHandler.get(isDisplayIdValid(displayId) ? displayId : Display.DEFAULT_DISPLAY)
                  .onMotionEvent(transformedEvent, event, policyFlags);
          transformedEvent.recycle();
      }
  
      private boolean isDisplayIdValid(int displayId) {
          return mEventHandler.get(displayId) != null;
      }
  
      @Override
      public void onMotionEvent(MotionEvent transformedEvent, MotionEvent rawEvent,
              int policyFlags) {
          sendInputEvent(transformedEvent, policyFlags);
      }
  
      @Override
      public void onKeyEvent(KeyEvent event, int policyFlags) {
          sendInputEvent(event, policyFlags);
      }
  
      @Override
      public void onAccessibilityEvent(AccessibilityEvent event) {
          // TODO Implement this to inject the accessibility event
          //      into the accessibility manager service similarly
          //      to how this is done for input events.
      }
  
      @Override
      public void setNext(EventStreamTransformation sink) {
          /* do nothing */
      }
  
      @Override
      public EventStreamTransformation getNext() {
          return null;
      }
  
      @Override
      public void clearEvents(int inputSource) {
          /* do nothing */
      }
  
      void setUserAndEnabledFeatures(int userId, int enabledFeatures) {
          if (DEBUG) {
              Slog.i(TAG, "setUserAndEnabledFeatures(userId = " + userId + ", enabledFeatures = 0x"
                      + Integer.toHexString(enabledFeatures) + ")");
          }
          if (mEnabledFeatures == enabledFeatures && mUserId == userId) {
              return;
          }
          if (mInstalled) {
              disableFeatures();
          }
          mUserId = userId;
          mEnabledFeatures = enabledFeatures;
          if (mInstalled) {
              enableFeatures();
          }
      }
  
      void notifyAccessibilityEvent(AccessibilityEvent event) {
          for (int i = 0; i < mEventHandler.size(); i++) {
              final EventStreamTransformation eventHandler = mEventHandler.valueAt(i);
              if (eventHandler != null) {
                  eventHandler.onAccessibilityEvent(event);
              }
          }
      }
  
      void notifyAccessibilityButtonClicked(int displayId) {
          if (mMagnificationGestureHandler.size() != 0) {
              final MagnificationGestureHandler handler = mMagnificationGestureHandler.get(displayId);
              if (handler != null) {
                  handler.notifyShortcutTriggered();
              }
          }
      }
  
      private void enableFeatures() {
          if (DEBUG) Slog.i(TAG, "enableFeatures()");
  
          resetStreamState();
  
          final ArrayList<Display> displaysList = mAms.getValidDisplayList();
  
          if ((mEnabledFeatures & FLAG_FEATURE_AUTOCLICK) != 0) {
              mAutoclickController = new AutoclickController(mContext, mUserId);
              addFirstEventHandlerForAllDisplays(displaysList, mAutoclickController);
          }
  
          for (int i = displaysList.size() - 1; i >= 0; i--) {
              final int displayId = displaysList.get(i).getDisplayId();
  
              if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
                  TouchExplorer explorer = new TouchExplorer(mContext, mAms);
                  addFirstEventHandler(displayId, explorer);
                  mTouchExplorer.put(displayId, explorer);
              }
  
              if ((mEnabledFeatures & FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER) != 0
                      || ((mEnabledFeatures & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0)
                      || ((mEnabledFeatures & FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER) != 0)) {
                  final boolean detectControlGestures = (mEnabledFeatures
                          & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0;
                  final boolean triggerable = (mEnabledFeatures
                          & FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER) != 0;
                  MagnificationGestureHandler magnificationGestureHandler =
                          new MagnificationGestureHandler(mContext,
                                  mAms.getMagnificationController(),
                                  detectControlGestures, triggerable, displayId);
                  addFirstEventHandler(displayId, magnificationGestureHandler);
                  mMagnificationGestureHandler.put(displayId, magnificationGestureHandler);
              }
  
              if ((mEnabledFeatures & FLAG_FEATURE_INJECT_MOTION_EVENTS) != 0) {
                  MotionEventInjector injector = new MotionEventInjector(
                          mContext.getMainLooper());
                  addFirstEventHandler(displayId, injector);
                  // TODO: Need to set MotionEventInjector per display.
                  mAms.setMotionEventInjector(injector);
                  mMotionEventInjector.put(displayId, injector);
              }
          }
  
          if ((mEnabledFeatures & FLAG_FEATURE_FILTER_KEY_EVENTS) != 0) {
              mKeyboardInterceptor = new KeyboardInterceptor(mAms,
                      LocalServices.getService(WindowManagerPolicy.class));
              // Since the display id of KeyEvent always would be -1 and it would be dispatched to
              // the display with input focus directly, we only need one KeyboardInterceptor for
              // default display.
              addFirstEventHandler(Display.DEFAULT_DISPLAY, mKeyboardInterceptor);
          }
      }
  
      /**
       * Adds an event handler to the event handler chain for giving display. The handler is added at
       * the beginning of the chain.
       *
       * @param displayId The logical display id.
       * @param handler The handler to be added to the event handlers list.
       */
      private void addFirstEventHandler(int displayId, EventStreamTransformation handler) {
          EventStreamTransformation eventHandler = mEventHandler.get(displayId);
          if (eventHandler != null) {
              handler.setNext(eventHandler);
          } else {
              handler.setNext(this);
          }
          eventHandler = handler;
          mEventHandler.put(displayId, eventHandler);
      }
  
      /**
       * Adds an event handler to the event handler chain for all displays. The handler is added at
       * the beginning of the chain.
       *
       * @param displayList The list of displays
       * @param handler The handler to be added to the event handlers list.
       */
      private void addFirstEventHandlerForAllDisplays(ArrayList<Display> displayList,
              EventStreamTransformation handler) {
          for (int i = 0; i < displayList.size(); i++) {
              final int displayId = displayList.get(i).getDisplayId();
              addFirstEventHandler(displayId, handler);
          }
      }
  
      private void disableFeatures() {
          for (int i = mMotionEventInjector.size() - 1; i >= 0; i--) {
              final MotionEventInjector injector = mMotionEventInjector.valueAt(i);
              // TODO: Need to set MotionEventInjector per display.
              mAms.setMotionEventInjector(null);
              if (injector != null) {
                  injector.onDestroy();
              }
          }
          mMotionEventInjector.clear();
          if (mAutoclickController != null) {
              mAutoclickController.onDestroy();
              mAutoclickController = null;
          }
          for (int i = mTouchExplorer.size() - 1; i >= 0; i--) {
              final TouchExplorer explorer = mTouchExplorer.valueAt(i);
              if (explorer != null) {
                  explorer.onDestroy();
              }
          }
          mTouchExplorer.clear();
          for (int i = mMagnificationGestureHandler.size() - 1; i >= 0; i--) {
              final MagnificationGestureHandler handler = mMagnificationGestureHandler.valueAt(i);
              if (handler != null) {
                  handler.onDestroy();
              }
          }
          mMagnificationGestureHandler.clear();
          if (mKeyboardInterceptor != null) {
              mKeyboardInterceptor.onDestroy();
              mKeyboardInterceptor = null;
          }
  
          mEventHandler.clear();
          resetStreamState();
      }
  
      void resetStreamState() {
          if (mTouchScreenStreamState != null) {
              mTouchScreenStreamState.reset();
          }
          if (mMouseStreamState != null) {
              mMouseStreamState.reset();
          }
          if (mKeyboardStreamState != null) {
              mKeyboardStreamState.reset();
          }
      }
  
      @Override
      public void onDestroy() {
          /* ignore */
      }
  
      /**
       * Keeps state of event streams observed for an input device with a certain source.
       * Provides information about whether motion and key events should be processed by accessibility
       * #EventStreamTransformations. Base implementation describes behaviour for event sources that
       * whose events should not be handled by a11y event stream transformations.
       */
      private static class EventStreamState {
          private int mSource;
  
          EventStreamState() {
              mSource = -1;
          }
  
          /**
           * Updates the input source of the device associated with the state. If the source changes,
           * resets internal state.
           *
           * @param source Updated input source.
           * @return Whether the input source has changed.
           */
          public boolean updateInputSource(int source) {
              if (mSource == source) {
                  return false;
              }
              // Reset clears internal state, so make sure it's called before |mSource| is updated.
              reset();
              mSource = source;
              return true;
          }
  
          /**
           * @return Whether input source is valid.
           */
          public boolean inputSourceValid() {
              return mSource >= 0;
          }
  
          /**
           * Resets the event stream state.
           */
          public void reset() {
              mSource = -1;
          }
  
          /**
           * @return Whether scroll events for device should be handled by event transformations.
           */
          public boolean shouldProcessScroll() {
              return false;
          }
  
          /**
           * @param event An observed motion event.
           * @return Whether the event should be handled by event transformations.
           */
          public boolean shouldProcessMotionEvent(MotionEvent event) {
              return false;
          }
  
          /**
           * @param event An observed key event.
           * @return Whether the event should be handled by event transformations.
           */
          public boolean shouldProcessKeyEvent(KeyEvent event) {
              return false;
          }
      }
  
      /**
       * Keeps state of stream of events from a mouse device.
       */
      private static class MouseEventStreamState extends EventStreamState {
          private boolean mMotionSequenceStarted;
  
          public MouseEventStreamState() {
              reset();
          }
  
          @Override
          final public void reset() {
              super.reset();
              mMotionSequenceStarted = false;
          }
  
          @Override
          final public boolean shouldProcessScroll() {
              return true;
          }
  
          @Override
          final public boolean shouldProcessMotionEvent(MotionEvent event) {
              if (mMotionSequenceStarted) {
                  return true;
              }
              // Wait for down or move event to start processing mouse events.
              int action = event.getActionMasked();
              mMotionSequenceStarted =
                      action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_HOVER_MOVE;
              return mMotionSequenceStarted;
          }
      }
  
      /**
       * Keeps state of stream of events from a touch screen device.
       */
      private static class TouchScreenEventStreamState extends EventStreamState {
          private boolean mTouchSequenceStarted;
          private boolean mHoverSequenceStarted;
  
          public TouchScreenEventStreamState() {
              reset();
          }
  
          @Override
          final public void reset() {
              super.reset();
              mTouchSequenceStarted = false;
              mHoverSequenceStarted = false;
          }
  
          @Override
          final public boolean shouldProcessMotionEvent(MotionEvent event) {
              // Wait for a down touch event to start processing.
              if (event.isTouchEvent()) {
                  if (mTouchSequenceStarted) {
                      return true;
                  }
                  mTouchSequenceStarted = event.getActionMasked() == MotionEvent.ACTION_DOWN;
                  return mTouchSequenceStarted;
              }
  
              // Wait for an enter hover event to start processing.
              if (mHoverSequenceStarted) {
                  return true;
              }
              mHoverSequenceStarted = event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER;
              return mHoverSequenceStarted;
          }
      }
  
      /**
       * Keeps state of streams of events from all keyboard devices.
       */
      private static class KeyboardEventStreamState extends EventStreamState {
          private SparseBooleanArray mEventSequenceStartedMap = new SparseBooleanArray();
  
          public KeyboardEventStreamState() {
              reset();
          }
  
          @Override
          final public void reset() {
              super.reset();
              mEventSequenceStartedMap.clear();
          }
  
          /*
           * Key events from different devices may be interleaved. For example, the volume up and
           * down keys can come from different input sources.
           */
          @Override
          public boolean updateInputSource(int deviceId) {
              return false;
          }
  
          // We manage all input source simultaneously; there is no concept of validity.
          @Override
          public boolean inputSourceValid() {
              return true;
          }
  
          @Override
          final public boolean shouldProcessKeyEvent(KeyEvent event) {
              // For each keyboard device, wait for a down event from a device to start processing
              int deviceId = event.getDeviceId();
              if (mEventSequenceStartedMap.get(deviceId, false)) {
                  return true;
              }
              boolean shouldProcess = event.getAction() == KeyEvent.ACTION_DOWN;
              mEventSequenceStartedMap.put(deviceId, shouldProcess);
              return shouldProcess;
          }
      }
  }
  