/* * Copyright (C) 2012 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.inputmethod.research; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; import android.util.Log; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.research.MotionEventReader.ReplayData; /** * Replays a sequence of motion events in realtime on the screen. * * Useful for user inspection of logged data. */ public class Replayer { private static final String TAG = Replayer.class.getSimpleName(); private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; private static final long START_TIME_DELAY_MS = 500; private boolean mIsReplaying = false; private KeyboardSwitcher mKeyboardSwitcher; private Replayer() { } private static final Replayer sInstance = new Replayer(); public static Replayer getInstance() { return sInstance; } public void setKeyboardSwitcher(final KeyboardSwitcher keyboardSwitcher) { mKeyboardSwitcher = keyboardSwitcher; } private static final int MSG_MOTION_EVENT = 0; private static final int MSG_DONE = 1; private static final int COMPLETION_TIME_MS = 500; // TODO: Support historical events and multi-touch. public void replay(final ReplayData replayData, final Runnable callback) { if (mIsReplaying) { return; } mIsReplaying = true; final int numActions = replayData.mActions.size(); if (DEBUG) { Log.d(TAG, "replaying " + numActions + " actions"); } if (numActions == 0) { mIsReplaying = false; return; } final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); // The reference time relative to the times stored in events. final long origStartTime = replayData.mTimes.get(0); // The reference time relative to which events are replayed in the present. final long currentStartTime = SystemClock.uptimeMillis() + START_TIME_DELAY_MS; // The adjustment needed to translate times from the original recorded time to the current // time. final long timeAdjustment = currentStartTime - origStartTime; final Handler handler = new Handler(Looper.getMainLooper()) { // Track the time of the most recent DOWN event, to be passed as a parameter when // constructing a MotionEvent. It's initialized here to the origStartTime, but this is // only a precaution. The value should be overwritten by the first ACTION_DOWN event // before the first use of the variable. Note that this may cause the first few events // to have incorrect {@code downTime}s. private long mOrigDownTime = origStartTime; @Override public void handleMessage(final Message msg) { switch (msg.what) { case MSG_MOTION_EVENT: final int index = msg.arg1; final int action = replayData.mActions.get(index); final PointerProperties[] pointerPropertiesArray = replayData.mPointerPropertiesArrays.get(index); final PointerCoords[] pointerCoordsArray = replayData.mPointerCoordsArrays.get(index); final long origTime = replayData.mTimes.get(index); if (action == MotionEvent.ACTION_DOWN) { mOrigDownTime = origTime; } final MotionEvent me = MotionEvent.obtain(mOrigDownTime + timeAdjustment, origTime + timeAdjustment, action, pointerPropertiesArray.length, pointerPropertiesArray, pointerCoordsArray, 0, 0, 1.0f, 1.0f, 0, 0, 0, 0); mainKeyboardView.processMotionEvent(me); me.recycle(); break; case MSG_DONE: mIsReplaying = false; ResearchLogger.getInstance().requestIndicatorRedraw(); break; } } }; handler.post(new Runnable() { @Override public void run() { ResearchLogger.getInstance().requestIndicatorRedraw(); } }); for (int i = 0; i < numActions; i++) { final Message msg = Message.obtain(handler, MSG_MOTION_EVENT, i, 0); final long msgTime = replayData.mTimes.get(i) + timeAdjustment; handler.sendMessageAtTime(msg, msgTime); if (DEBUG) { Log.d(TAG, "queuing event at " + msgTime); } } final long presentDoneTime = replayData.mTimes.get(numActions - 1) + timeAdjustment + COMPLETION_TIME_MS; handler.sendMessageAtTime(Message.obtain(handler, MSG_DONE), presentDoneTime); if (callback != null) { handler.postAtTime(callback, presentDoneTime + 1); } } public boolean isReplaying() { return mIsReplaying; } }