/* * 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.systemui.statusbar; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.util.Log; import android.view.MotionEvent; import java.io.BufferedWriter; import java.io.FileDescriptor; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.HashSet; import java.util.LinkedList; /** * Convenience class for capturing gestures for later analysis. */ public class GestureRecorder { public static final boolean DEBUG = true; // for now public static final String TAG = GestureRecorder.class.getSimpleName(); public class Gesture { public abstract class Record { long time; public abstract String toJson(); } public class MotionEventRecord extends Record { public MotionEvent event; public MotionEventRecord(long when, MotionEvent event) { this.time = when; this.event = MotionEvent.obtain(event); } String actionName(int action) { switch (action) { case MotionEvent.ACTION_DOWN: return "down"; case MotionEvent.ACTION_UP: return "up"; case MotionEvent.ACTION_MOVE: return "move"; case MotionEvent.ACTION_CANCEL: return "cancel"; default: return String.valueOf(action); } } public String toJson() { return String.format( ("{\"type\":\"motion\", \"time\":%d, \"action\":\"%s\", " + "\"x\":%.2f, \"y\":%.2f, \"s\":%.2f, \"p\":%.2f}"), this.time, actionName(this.event.getAction()), this.event.getRawX(), this.event.getRawY(), this.event.getSize(), this.event.getPressure() ); } } public class TagRecord extends Record { public String tag, info; public TagRecord(long when, String tag, String info) { this.time = when; this.tag = tag; this.info = info; } public String toJson() { return String.format("{\"type\":\"tag\", \"time\":%d, \"tag\":\"%s\", \"info\":\"%s\"}", this.time, this.tag, this.info ); } } private LinkedList<Record> mRecords = new LinkedList<Record>(); private HashSet<String> mTags = new HashSet<String>(); long mDownTime = -1; boolean mComplete = false; public void add(MotionEvent ev) { mRecords.add(new MotionEventRecord(ev.getEventTime(), ev)); if (mDownTime < 0) { mDownTime = ev.getDownTime(); } else { if (mDownTime != ev.getDownTime()) { Log.w(TAG, "Assertion failure in GestureRecorder: event downTime (" +ev.getDownTime()+") does not match gesture downTime ("+mDownTime+")"); } } switch (ev.getActionMasked()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mComplete = true; } } public void tag(long when, String tag, String info) { mRecords.add(new TagRecord(when, tag, info)); mTags.add(tag); } public boolean isComplete() { return mComplete; } public String toJson() { StringBuilder sb = new StringBuilder(); boolean first = true; sb.append("["); for (Record r : mRecords) { if (!first) sb.append(", "); first = false; sb.append(r.toJson()); } sb.append("]"); return sb.toString(); } } // -=-=-=-=-=-=-=-=-=-=-=- static final long SAVE_DELAY = 5000; // ms static final int SAVE_MESSAGE = 6351; private LinkedList<Gesture> mGestures; private Gesture mCurrentGesture; private int mLastSaveLen = -1; private String mLogfile; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == SAVE_MESSAGE) { save(); } } }; public GestureRecorder(String filename) { mLogfile = filename; mGestures = new LinkedList<Gesture>(); mCurrentGesture = null; } public void add(MotionEvent ev) { synchronized (mGestures) { if (mCurrentGesture == null || mCurrentGesture.isComplete()) { mCurrentGesture = new Gesture(); mGestures.add(mCurrentGesture); } mCurrentGesture.add(ev); } saveLater(); } public void tag(long when, String tag, String info) { synchronized (mGestures) { if (mCurrentGesture == null) { mCurrentGesture = new Gesture(); mGestures.add(mCurrentGesture); } mCurrentGesture.tag(when, tag, info); } saveLater(); } public void tag(long when, String tag) { tag(when, tag, null); } public void tag(String tag) { tag(SystemClock.uptimeMillis(), tag, null); } public void tag(String tag, String info) { tag(SystemClock.uptimeMillis(), tag, info); } /** * Generates a JSON string capturing all completed gestures. * Not threadsafe; call with a lock. */ public String toJsonLocked() { StringBuilder sb = new StringBuilder(); boolean first = true; sb.append("["); int count = 0; for (Gesture g : mGestures) { if (!g.isComplete()) continue; if (!first) sb.append("," ); first = false; sb.append(g.toJson()); count++; } mLastSaveLen = count; sb.append("]"); return sb.toString(); } public String toJson() { String s; synchronized (mGestures) { s = toJsonLocked(); } return s; } public void saveLater() { mHandler.removeMessages(SAVE_MESSAGE); mHandler.sendEmptyMessageDelayed(SAVE_MESSAGE, SAVE_DELAY); } public void save() { synchronized (mGestures) { try { BufferedWriter w = new BufferedWriter(new FileWriter(mLogfile, /*append=*/ true)); w.append(toJsonLocked() + "\n"); w.close(); mGestures.clear(); // If we have a pending gesture, push it back if (mCurrentGesture != null && !mCurrentGesture.isComplete()) { mGestures.add(mCurrentGesture); } if (DEBUG) { Log.v(TAG, String.format("Wrote %d complete gestures to %s", mLastSaveLen, mLogfile)); } } catch (IOException e) { Log.e(TAG, String.format("Couldn't write gestures to %s", mLogfile), e); mLastSaveLen = -1; } } } public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { save(); if (mLastSaveLen >= 0) { pw.println(String.valueOf(mLastSaveLen) + " gestures written to " + mLogfile); } else { pw.println("error writing gestures"); } } }