content: // Copyright 2015 The Chromium Authors
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  package com.termux.x11.input;
  
  import android.content.Context;
  import android.graphics.PointF;
  import android.os.SystemClock;
  import android.view.MotionEvent;
  import android.view.ViewConfiguration;
  
  /**
   * This interface defines the methods used to customize input handling for
   * a particular strategy.  The implementing class is responsible for sending
   * remote input events and defining implementation specific behavior.
   */
  public interface InputStrategyInterface {
      /**
       * Called when a user tap has been detected.
       *
       * @param button The button value for the tap event.
       */
      void onTap(int button);
  
      /**
       * Called when the user has put one or more fingers down on the screen for a period of time.
       *
       * @param button The button value for the tap event.
       * @return A boolean representing whether the event was handled.
       */
      boolean onPressAndHold(int button);
  
      /**
       * Called when a MotionEvent is received.  This method allows the input strategy to store or
       * react to specific MotionEvents as needed.
       *
       * @param event The original event for the current touch motion.
       */
      void onMotionEvent(MotionEvent event);
  
      /**
       * Called when the user is attempting to scroll/pan the remote UI.
       *
       * @param distanceX The distance moved along the x-axis.
       * @param distanceY The distance moved along the y-axis.
       */
      void onScroll(float distanceX, float distanceY);
  
      class NullInputStrategy implements InputStrategyInterface {
          @Override public void onTap(int button) {}
          @Override public boolean onPressAndHold(int button) { return false; }
          @Override public void onScroll(float distanceX, float distanceY) {}
          @Override public void onMotionEvent(MotionEvent event) {}
      }
  
      /**
       * This class receives local touch events and translates them into the appropriate mouse based
       * events for the remote host.  The net result is that the local input method feels like a touch
       * interface but the remote host will be given mouse events to inject.
       */
      class SimulatedTouchInputStrategy implements InputStrategyInterface {
          /** Used to adjust the size of the region used for double tap detection. */
          private static final float DOUBLE_TAP_SLOP_SCALE_FACTOR = 0.25f;
  
          private final RenderData mRenderData;
          private final InputEventSender mInjector;
  
          /**
           * Stores the time of the most recent left button single tap processed.
           */
          private long mLastTapTimeInMs;
  
          /**
           * Stores the position of the last left button single tap processed.
           */
          private PointF mLastTapPoint;
  
          /**
           * The maximum distance, in pixels, between two points in order for them to be considered a
           * double tap gesture.
           */
          private final int mDoubleTapSlopSquareInPx;
  
          /**
           * The interval, measured in milliseconds, in which two consecutive left button taps must
           * occur in order to be considered a double tap gesture.
           */
          private final long mDoubleTapDurationInMs;
  
          /** Mouse-button currently held down, or BUTTON_UNDEFINED otherwise. */
          private int mHeldButton = InputStub.BUTTON_UNDEFINED;
  
          public SimulatedTouchInputStrategy(
                  RenderData renderData, InputEventSender injector, Context context) {
              if (injector == null)
                  throw new NullPointerException();
              mRenderData = renderData;
              mInjector = injector;
  
              mDoubleTapDurationInMs = ViewConfiguration.getDoubleTapTimeout();
  
              // In order to detect whether the user is attempting to double tap a target, we define a
              // region around the first point within which the second tap must occur.  The standard way
              // to do this in an Android UI (meaning a UI comprised of UI elements which conform to the
              // visual guidelines for the platform which are 'Touch Friendly') is to use the
              // getScaledDoubleTapSlop() value for checking this distance (or use a GestureDetector).
              // Our scenario is a bit different as our UI consists of an image of a remote machine where
              // the UI elements were probably designed for mouse and keyboard (meaning smaller targets)
              // and the image itself which can be zoomed to change the size of the targets.  Ths adds up
              // to the target to be invoked often being either larger or much smaller than a standard
              // Android UI element.  Our approach to this problem is to make double-tap detection
              // consistent regardless of the zoom level or remote target size so that the user can rely
              // on their muscle memory when interacting with our UI.  With respect to the original
              // problem, getScaledDoubleTapSlop() gives a value which is optimized for an Android based
              // UI however this value is too large for interacting with remote elements in our app.
              // Our solution is to use the original value from getScaledDoubleTapSlop() (which includes
              // scaling to account for x11 differences between devices) and apply a fudge/scale
              // factor to make the interaction more intuitive and useful for our scenario.
              ViewConfiguration config = ViewConfiguration.get(context);
              int scaledDoubleTapSlopInPx = config.getScaledDoubleTapSlop();
              scaledDoubleTapSlopInPx = (int) (scaledDoubleTapSlopInPx * DOUBLE_TAP_SLOP_SCALE_FACTOR);
              mDoubleTapSlopSquareInPx = scaledDoubleTapSlopInPx * scaledDoubleTapSlopInPx;
          }
  
          @Override
          public void onTap(int button) {
              PointF currentTapPoint = mRenderData.getCursorPosition();
              if (button == InputStub.BUTTON_LEFT) {
                  // Left clicks are handled a little differently than the events for other buttons.
                  // This is needed because translating touch events to mouse events has a problem with
                  // location consistency for double clicks.  If you take the center location of each tap
                  // and inject them as mouse clicks, the distance between those two points will often
                  // cause the remote OS to recognize the gesture as two distinct clicks instead of a
                  // double click.  In order to increase the success rate of double taps/clicks, we
                  // squirrel away the time and coordinates of each single tap and if we detect the user
                  // attempting a double tap, we use the original event's location for that second tap.
                  long tapInterval = SystemClock.uptimeMillis() - mLastTapTimeInMs;
                  if (isDoubleTap(currentTapPoint.x, currentTapPoint.y, tapInterval)) {
                      mLastTapPoint = null;
                      mLastTapTimeInMs = 0;
                  } else {
                      mLastTapPoint = currentTapPoint;
                      mLastTapTimeInMs = SystemClock.uptimeMillis();
                  }
              } else {
                  mLastTapPoint = null;
                  mLastTapTimeInMs = 0;
              }
  
              mInjector.sendMouseClick(button, false);
          }
  
          @Override
          public boolean onPressAndHold(int button) {
              mInjector.sendMouseDown(button, false);
              mHeldButton = button;
              return true;
          }
  
          @Override
          public void onScroll(float distanceX, float distanceY) {
              mInjector.sendMouseWheelEvent(distanceX, distanceY);
          }
  
          @Override
          public void onMotionEvent(MotionEvent event) {
              if (event.getActionMasked() == MotionEvent.ACTION_UP && mHeldButton != InputStub.BUTTON_UNDEFINED) {
                  mInjector.sendMouseUp(mHeldButton, false);
                  mHeldButton = InputStub.BUTTON_UNDEFINED;
              }
          }
  
          private boolean isDoubleTap(float currentX, float currentY, long tapInterval) {
              if (tapInterval > mDoubleTapDurationInMs || mLastTapPoint == null) {
                  return false;
              }
  
              // Convert the image based coordinates back to screen coordinates so the user experiences
              // consistent double tap behavior regardless of zoom level.
              //
              float[] currentValues = {currentX * mRenderData.scale.x, currentY * mRenderData.scale.y};
              float[] previousValues = {mLastTapPoint.x * mRenderData.scale.x, mLastTapPoint.y * mRenderData.scale.y};
  
              int deltaX = (int) (currentValues[0] - previousValues[0]);
              int deltaY = (int) (currentValues[1] - previousValues[1]);
              return ((deltaX * deltaX + deltaY * deltaY) <= mDoubleTapSlopSquareInPx);
          }
      }
  
      /**
       * Defines a set of behavior and methods to simulate trackpad behavior when responding to
       * local input event data.  This class is also responsible for forwarding input event data
       * to the remote host for injection there.
       */
      class TrackpadInputStrategy implements InputStrategyInterface {
          private final InputEventSender mInjector;
  
          /** Mouse-button currently held down, or BUTTON_UNDEFINED otherwise. */
          private int mHeldButton = InputStub.BUTTON_UNDEFINED;
  
          public TrackpadInputStrategy(InputEventSender injector) {
              if ((mInjector = injector) == null)
                  throw new NullPointerException();
          }
  
          @Override
          public void onTap(int button) {
              mInjector.sendMouseClick(button, true);
          }
  
          @Override
          public boolean onPressAndHold(int button) {
              mInjector.sendMouseDown(button, true);
              mHeldButton = button;
              return true;
          }
  
          @Override
          public void onScroll(float distanceX, float distanceY) {
              mInjector.sendMouseWheelEvent(distanceX, distanceY);
          }
  
          @Override
          public void onMotionEvent(MotionEvent event) {
              if (event.getActionMasked() == MotionEvent.ACTION_UP && mHeldButton != InputStub.BUTTON_UNDEFINED) {
                  mInjector.sendMouseUp(mHeldButton, true);
                  mHeldButton = InputStub.BUTTON_UNDEFINED;
              }
          }
      }
  }
  