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