/* * Copyright (C) 2013 The CyanogenMod 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.gesture; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.content.Context; import android.hardware.input.InputManager; import android.os.IBinder; import android.os.RemoteException; import android.os.SystemClock; import android.util.Slog; import android.view.Display; import android.view.GestureDetector; import android.view.GestureDetector.OnDoubleTapListener; import android.view.IInputFilter; import android.view.IInputFilterHost; import android.view.InputDevice; import android.view.InputEvent; import android.view.MotionEvent; import android.view.OrientationEventListener; import android.view.ViewConfiguration; import android.view.WindowManager; import java.io.PrintWriter; /** * A simple input filter that listens for gesture sensor events and converts * them to input events to be injected into the input stream. */ public class GestureInputFilter implements IInputFilter, GestureDetector.OnGestureListener, OnDoubleTapListener { private static final String TAG = "GestureInputFilter"; private static final boolean DEBUG = false; private IInputFilterHost mHost = null; private GestureDetector mGestureDetector; private InputManager mInputManager; private OrientationEventListener mOrientationListener; private final int mScreenWidth, mScreenHeight; private float mGesturePadWidth, mGesturePadHeight; private int mTouchSlop, mOrientation; private Context mContext; private PendingIntent mLongPressPendingIntent; private PendingIntent mDoubleClickPendingIntent; public GestureInputFilter(Context context) { mInputManager = InputManager.getInstance(); mContext = context; for (int id : mInputManager.getInputDeviceIds()) { InputDevice inputDevice = mInputManager.getInputDevice(id); if ((inputDevice.getSources() & InputDevice.SOURCE_GESTURE_SENSOR) == mInputManager.getInputDevice(id).getSources()) { mGesturePadWidth = inputDevice.getMotionRange(MotionEvent.AXIS_X).getMax(); mGesturePadHeight = inputDevice.getMotionRange(MotionEvent.AXIS_Y).getMax(); break; } } ViewConfiguration vc = ViewConfiguration.get(context); mTouchSlop = vc.getScaledTouchSlop(); WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); mScreenWidth = display.getWidth(); mScreenHeight = display.getHeight(); mGestureDetector = new GestureDetector(context, this); mGestureDetector.setOnDoubleTapListener(this); mOrientationListener = new OrientationEventListener(context) { @Override public void onOrientationChanged(int orientation) { if (orientation == -1) { return; } mOrientation = (orientation + 45) / 90 * 90; } }; } /** * Called to enqueue the input event for filtering. * The event must be recycled after the input filter processed it. * This method is guaranteed to be non-reentrant. * * @see InputFilter#filterInputEvent(InputEvent, int) * @param event The input event to enqueue. */ // called by the input dispatcher thread public void filterInputEvent(InputEvent event, int policyFlags) throws RemoteException { if (DEBUG) Slog.d(TAG, event.toString()); try { if (event.getSource() != InputDevice.SOURCE_GESTURE_SENSOR || !(event instanceof MotionEvent)) { try { mHost.sendInputEvent(event, policyFlags); } catch (RemoteException e) { /* ignore */ } return; } MotionEvent motionEvent = (MotionEvent) event; mGestureDetector.onTouchEvent(motionEvent); } finally { event.recycle(); } } // called by the input dispatcher thread public void install(IInputFilterHost host) throws RemoteException { if (DEBUG) { Slog.d(TAG, "Gesture input filter installed."); } mHost = host; mOrientationListener.enable(); } // called by the input dispatcher thread public void uninstall() throws RemoteException { if (DEBUG) { Slog.d(TAG, "Gesture input filter uninstalled."); } mHost = null; mOrientationListener.disable(); mContext = null; } // should never be called public IBinder asBinder() { throw new UnsupportedOperationException(); } // called by a Binder thread public void dump(PrintWriter pw, String prefix) { } private boolean generateSwipe(MotionEvent e1, MotionEvent e2) { switch (mOrientation) { case 90: Slog.d(TAG, "Adjusting motion for 90 degrees"); e1.setLocation(e1.getY(), e1.getX()); e2.setLocation(e2.getY(), e2.getX()); break; case 180: Slog.d(TAG, "Adjusting motion for 180 degrees"); e1.setLocation(mGesturePadWidth - e1.getX(), mGesturePadHeight - e1.getY()); e2.setLocation(mGesturePadWidth - e2.getX(), mGesturePadHeight - e2.getY()); break; case 270: Slog.d(TAG, "Adjusting motion for 270 degrees"); e1.setLocation(mGesturePadHeight - e1.getY(), e1.getX()); e2.setLocation(mGesturePadHeight - e2.getY(), e2.getX()); break; } float deltaX = Math.abs(e1.getX() - e2.getX()); float deltaY = Math.abs(e1.getY() - e2.getY()); if (deltaX < mTouchSlop && deltaY < mTouchSlop) { return false; } if (deltaX > deltaY) { e2.setLocation(e2.getX(), e1.getY()); } else if (deltaY > deltaX) { e2.setLocation(e1.getX(), e2.getY()); } float scaleX = mScreenWidth / mGesturePadWidth; float scaleY = mScreenHeight / mGesturePadHeight; float magnitudeX = deltaX * scaleX; float magnitudeY = deltaY * scaleY; float origX = mScreenWidth / 2; float origY = mScreenHeight / 2; float endX = 0.0f; float endY = 0.0f; if (e2.getY() > e1.getY()) { if (DEBUG) Slog.d(TAG, "Detected down motion"); // Ensure selection does not occur endX = origX + mTouchSlop + 5; endY = origY + magnitudeY; } else if (e2.getY() < e1.getY()) { if (DEBUG) Slog.d(TAG, "Detected up motion"); endX = origX + mTouchSlop + 5; endY = origY - magnitudeY; } else if (e2.getX() > e1.getX()) { if (DEBUG) Slog.d(TAG, "Detected left motion"); endX = origX + magnitudeX; endY = origY + mTouchSlop + 5; } else if (e2.getX() < e1.getX()) { if (DEBUG) Slog.d(TAG, "Detected right motion"); endX = origX - magnitudeX; endY = origY + mTouchSlop + 5; } else { return false; } sendSwipe(origX, origY, endX, endY); return true; } private void sendSwipe(float x1, float y1, float x2, float y2) { final long duration = 100; long now = SystemClock.uptimeMillis(); final long startTime = now; final long endTime = startTime + duration; sendMotionEvent(MotionEvent.ACTION_DOWN, now, x1, y1, 1.0f); while (now < endTime) { long elapsedTime = now - startTime; float alpha = (float) elapsedTime / duration; sendMotionEvent(MotionEvent.ACTION_MOVE, now, lerp(x1, x2, alpha), lerp(y1, y2, alpha), 1.0f); now = SystemClock.uptimeMillis(); } sendMotionEvent(MotionEvent.ACTION_UP, now, x2, y2, 1.0f); } private void sendMotionEvent(int action, long when, float x, float y, float pressure) { final float DEFAULT_SIZE = 1.0f; final int DEFAULT_META_STATE = 0; final float DEFAULT_PRECISION_X = 1.0f; final float DEFAULT_PRECISION_Y = 1.0f; final int DEFAULT_DEVICE_ID = 0; final int DEFAULT_EDGE_FLAGS = 0; MotionEvent e = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE, DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID, DEFAULT_EDGE_FLAGS); e.setSource(InputDevice.SOURCE_TOUCHSCREEN); sendInputEvent(e); } private void sendInputEvent(InputEvent event) { mInputManager.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH); } private static final float lerp(float a, float b, float alpha) { return (b - a) * alpha + a; } @Override public boolean onDown(MotionEvent e) { return false; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; } @Override public void onLongPress(MotionEvent e) { if (mLongPressPendingIntent != null) { try { mLongPressPendingIntent.send(); } catch (CanceledException e1) { e1.printStackTrace(); } } } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return generateSwipe(e1, e2); } @Override public boolean onSingleTapConfirmed(MotionEvent e) { return false; } @Override public boolean onDoubleTap(MotionEvent e) { if (mDoubleClickPendingIntent != null) { try { mDoubleClickPendingIntent.send(); return true; } catch (CanceledException e1) { e1.printStackTrace(); } } return false; } @Override public boolean onDoubleTapEvent(MotionEvent e) { return false; } public void setOnLongPressPendingIntent(PendingIntent pendingIntent) { mLongPressPendingIntent = pendingIntent; } public void setOnDoubleClickPendingIntent(PendingIntent pendingIntent) { mDoubleClickPendingIntent = pendingIntent; } }