1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.systemui.statusbar; 18 19 import android.os.Handler; 20 import android.os.Message; 21 import android.os.SystemClock; 22 import android.util.Log; 23 import android.view.MotionEvent; 24 25 import java.io.BufferedWriter; 26 import java.io.FileWriter; 27 import java.io.IOException; 28 import java.io.PrintWriter; 29 import java.util.HashSet; 30 import java.util.LinkedList; 31 32 /** 33 * Convenience class for capturing gestures for later analysis. 34 */ 35 public class GestureRecorder { 36 public static final boolean DEBUG = true; // for now 37 public static final String TAG = GestureRecorder.class.getSimpleName(); 38 39 public class Gesture { 40 public abstract class Record { 41 long time; toJson()42 public abstract String toJson(); 43 } 44 public class MotionEventRecord extends Record { 45 public MotionEvent event; MotionEventRecord(long when, MotionEvent event)46 public MotionEventRecord(long when, MotionEvent event) { 47 this.time = when; 48 this.event = MotionEvent.obtain(event); 49 } actionName(int action)50 String actionName(int action) { 51 switch (action) { 52 case MotionEvent.ACTION_DOWN: 53 return "down"; 54 case MotionEvent.ACTION_UP: 55 return "up"; 56 case MotionEvent.ACTION_MOVE: 57 return "move"; 58 case MotionEvent.ACTION_CANCEL: 59 return "cancel"; 60 default: 61 return String.valueOf(action); 62 } 63 } toJson()64 public String toJson() { 65 return String.format( 66 ("{\"type\":\"motion\", \"time\":%d, \"action\":\"%s\", " 67 + "\"x\":%.2f, \"y\":%.2f, \"s\":%.2f, \"p\":%.2f}"), 68 this.time, 69 actionName(this.event.getAction()), 70 this.event.getRawX(), 71 this.event.getRawY(), 72 this.event.getSize(), 73 this.event.getPressure() 74 ); 75 } 76 } 77 public class TagRecord extends Record { 78 public String tag, info; TagRecord(long when, String tag, String info)79 public TagRecord(long when, String tag, String info) { 80 this.time = when; 81 this.tag = tag; 82 this.info = info; 83 } toJson()84 public String toJson() { 85 return String.format("{\"type\":\"tag\", \"time\":%d, \"tag\":\"%s\", \"info\":\"%s\"}", 86 this.time, 87 this.tag, 88 this.info 89 ); 90 } 91 } 92 private LinkedList<Record> mRecords = new LinkedList<Record>(); 93 private HashSet<String> mTags = new HashSet<String>(); 94 long mDownTime = -1; 95 boolean mComplete = false; 96 add(MotionEvent ev)97 public void add(MotionEvent ev) { 98 mRecords.add(new MotionEventRecord(ev.getEventTime(), ev)); 99 if (mDownTime < 0) { 100 mDownTime = ev.getDownTime(); 101 } else { 102 if (mDownTime != ev.getDownTime()) { 103 Log.w(TAG, "Assertion failure in GestureRecorder: event downTime (" 104 +ev.getDownTime()+") does not match gesture downTime ("+mDownTime+")"); 105 } 106 } 107 switch (ev.getActionMasked()) { 108 case MotionEvent.ACTION_UP: 109 case MotionEvent.ACTION_CANCEL: 110 mComplete = true; 111 } 112 } tag(long when, String tag, String info)113 public void tag(long when, String tag, String info) { 114 mRecords.add(new TagRecord(when, tag, info)); 115 mTags.add(tag); 116 } isComplete()117 public boolean isComplete() { 118 return mComplete; 119 } toJson()120 public String toJson() { 121 StringBuilder sb = new StringBuilder(); 122 boolean first = true; 123 sb.append("["); 124 for (Record r : mRecords) { 125 if (!first) sb.append(", "); 126 first = false; 127 sb.append(r.toJson()); 128 } 129 sb.append("]"); 130 return sb.toString(); 131 } 132 } 133 134 // -=-=-=-=-=-=-=-=-=-=-=- 135 136 static final long SAVE_DELAY = 5000; // ms 137 static final int SAVE_MESSAGE = 6351; 138 139 private LinkedList<Gesture> mGestures; 140 private Gesture mCurrentGesture; 141 private int mLastSaveLen = -1; 142 private String mLogfile; 143 144 private Handler mHandler = new Handler() { 145 @Override 146 public void handleMessage(Message msg) { 147 if (msg.what == SAVE_MESSAGE) { 148 save(); 149 } 150 } 151 }; 152 GestureRecorder(String filename)153 public GestureRecorder(String filename) { 154 mLogfile = filename; 155 mGestures = new LinkedList<Gesture>(); 156 mCurrentGesture = null; 157 } 158 add(MotionEvent ev)159 public void add(MotionEvent ev) { 160 synchronized (mGestures) { 161 if (mCurrentGesture == null || mCurrentGesture.isComplete()) { 162 mCurrentGesture = new Gesture(); 163 mGestures.add(mCurrentGesture); 164 } 165 mCurrentGesture.add(ev); 166 } 167 saveLater(); 168 } 169 tag(long when, String tag, String info)170 public void tag(long when, String tag, String info) { 171 synchronized (mGestures) { 172 if (mCurrentGesture == null) { 173 mCurrentGesture = new Gesture(); 174 mGestures.add(mCurrentGesture); 175 } 176 mCurrentGesture.tag(when, tag, info); 177 } 178 saveLater(); 179 } 180 tag(long when, String tag)181 public void tag(long when, String tag) { 182 tag(when, tag, null); 183 } 184 tag(String tag)185 public void tag(String tag) { 186 tag(SystemClock.uptimeMillis(), tag, null); 187 } 188 tag(String tag, String info)189 public void tag(String tag, String info) { 190 tag(SystemClock.uptimeMillis(), tag, info); 191 } 192 193 /** 194 * Generates a JSON string capturing all completed gestures. 195 * Not threadsafe; call with a lock. 196 */ toJsonLocked()197 public String toJsonLocked() { 198 StringBuilder sb = new StringBuilder(); 199 boolean first = true; 200 sb.append("["); 201 int count = 0; 202 for (Gesture g : mGestures) { 203 if (!g.isComplete()) continue; 204 if (!first) sb.append("," ); 205 first = false; 206 sb.append(g.toJson()); 207 count++; 208 } 209 mLastSaveLen = count; 210 sb.append("]"); 211 return sb.toString(); 212 } 213 toJson()214 public String toJson() { 215 String s; 216 synchronized (mGestures) { 217 s = toJsonLocked(); 218 } 219 return s; 220 } 221 saveLater()222 public void saveLater() { 223 mHandler.removeMessages(SAVE_MESSAGE); 224 mHandler.sendEmptyMessageDelayed(SAVE_MESSAGE, SAVE_DELAY); 225 } 226 save()227 public void save() { 228 synchronized (mGestures) { 229 try { 230 BufferedWriter w = new BufferedWriter(new FileWriter(mLogfile, /*append=*/ true)); 231 w.append(toJsonLocked() + "\n"); 232 w.close(); 233 mGestures.clear(); 234 // If we have a pending gesture, push it back 235 if (mCurrentGesture != null && !mCurrentGesture.isComplete()) { 236 mGestures.add(mCurrentGesture); 237 } 238 if (DEBUG) { 239 Log.v(TAG, String.format("Wrote %d complete gestures to %s", mLastSaveLen, mLogfile)); 240 } 241 } catch (IOException e) { 242 Log.e(TAG, String.format("Couldn't write gestures to %s", mLogfile), e); 243 mLastSaveLen = -1; 244 } 245 } 246 } 247 dump(PrintWriter pw, String[] args)248 public void dump(PrintWriter pw, String[] args) { 249 save(); 250 if (mLastSaveLen >= 0) { 251 pw.println(String.valueOf(mLastSaveLen) + " gestures written to " + mLogfile); 252 } else { 253 pw.println("error writing gestures"); 254 } 255 } 256 } 257