GestureRecorder.java revision 75fcac4eebdf7ff68a534e9af1664c571f40ef30
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
17package com.android.systemui.statusbar;
18
19import java.io.BufferedWriter;
20import java.io.FileDescriptor;
21import java.io.FileWriter;
22import java.io.IOException;
23import java.io.PrintWriter;
24import java.util.HashSet;
25import java.util.LinkedList;
26
27import android.os.Handler;
28import android.os.Message;
29import android.os.SystemClock;
30import android.util.Slog;
31import android.view.MotionEvent;
32
33/**
34 * Convenience class for capturing gestures for later analysis.
35 */
36public 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 = event.copy();
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                    // TODO: remove
105                    throw new RuntimeException("Assertion failure in GestureRecorder: event downTime ("
106                            +ev.getDownTime()+") does not match gesture downTime ("+mDownTime+")");
107                }
108            }
109            switch (ev.getActionMasked()) {
110                case MotionEvent.ACTION_UP:
111                case MotionEvent.ACTION_CANCEL:
112                    mComplete = true;
113            }
114        }
115        public void tag(long when, String tag, String info) {
116            mRecords.add(new TagRecord(when, tag, info));
117            mTags.add(tag);
118        }
119        public boolean isComplete() {
120            return mComplete;
121        }
122        public String toJson() {
123            StringBuilder sb = new StringBuilder();
124            boolean first = true;
125            sb.append("[");
126            for (Record r : mRecords) {
127                if (!first) sb.append(", ");
128                first = false;
129                sb.append(r.toJson());
130            }
131            sb.append("]");
132            return sb.toString();
133        }
134    }
135
136    // -=-=-=-=-=-=-=-=-=-=-=-
137
138    static final long SAVE_DELAY = 5000; // ms
139    static final int SAVE_MESSAGE = 6351;
140
141    private LinkedList<Gesture> mGestures;
142    private Gesture mCurrentGesture;
143    private int mLastSaveLen = -1;
144    private String mLogfile;
145
146    private Handler mHandler = new Handler() {
147        @Override
148        public void handleMessage(Message msg) {
149            if (msg.what == SAVE_MESSAGE) {
150                save();
151            }
152        }
153    };
154
155    public GestureRecorder(String filename) {
156        mLogfile = filename;
157        mGestures = new LinkedList<Gesture>();
158        mCurrentGesture = null;
159    }
160
161    public void add(MotionEvent ev) {
162        synchronized (mGestures) {
163            if (mCurrentGesture == null || mCurrentGesture.isComplete()) {
164                mCurrentGesture = new Gesture();
165                mGestures.add(mCurrentGesture);
166            }
167            mCurrentGesture.add(ev);
168        }
169        saveLater();
170    }
171
172    public void tag(long when, String tag, String info) {
173        synchronized (mGestures) {
174            if (mCurrentGesture == null) {
175                mCurrentGesture = new Gesture();
176                mGestures.add(mCurrentGesture);
177            }
178            mCurrentGesture.tag(when, tag, info);
179        }
180        saveLater();
181    }
182
183    public void tag(long when, String tag) {
184        tag(when, tag, null);
185    }
186
187    public void tag(String tag) {
188        tag(SystemClock.uptimeMillis(), tag, null);
189    }
190
191    public void tag(String tag, String info) {
192        tag(SystemClock.uptimeMillis(), tag, info);
193    }
194
195    /**
196     * Generates a JSON string capturing all completed gestures.
197     * Not threadsafe; call with a lock.
198     */
199    public String toJsonLocked() {
200        StringBuilder sb = new StringBuilder();
201        boolean first = true;
202        sb.append("[");
203        int count = 0;
204        for (Gesture g : mGestures) {
205            if (!g.isComplete()) continue;
206            if (!first) sb.append("," );
207            first = false;
208            sb.append(g.toJson());
209            count++;
210        }
211        mLastSaveLen = count;
212        sb.append("]");
213        return sb.toString();
214    }
215
216    public String toJson() {
217        String s;
218        synchronized (mGestures) {
219            s = toJsonLocked();
220        }
221        return s;
222    }
223
224    public void saveLater() {
225        mHandler.removeMessages(SAVE_MESSAGE);
226        mHandler.sendEmptyMessageDelayed(SAVE_MESSAGE, SAVE_DELAY);
227    }
228
229    public void save() {
230        synchronized (mGestures) {
231            try {
232                BufferedWriter w = new BufferedWriter(new FileWriter(mLogfile, /*append=*/ true));
233                w.append(toJsonLocked() + "\n");
234                w.close();
235                mGestures.clear();
236                // If we have a pending gesture, push it back
237                if (mCurrentGesture != null && !mCurrentGesture.isComplete()) {
238                    mGestures.add(mCurrentGesture);
239                }
240                if (DEBUG) {
241                    Slog.v(TAG, String.format("Wrote %d complete gestures to %s", mLastSaveLen, mLogfile));
242                }
243            } catch (IOException e) {
244                Slog.e(TAG, String.format("Couldn't write gestures to %s", mLogfile), e);
245                mLastSaveLen = -1;
246            }
247        }
248    }
249
250    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
251        save();
252        if (mLastSaveLen >= 0) {
253            pw.println(String.valueOf(mLastSaveLen) + " gestures written to " + mLogfile);
254        } else {
255            pw.println("error writing gestures");
256        }
257    }
258}
259