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