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 android.os.Handler;
20import android.os.Message;
21import android.os.SystemClock;
22import android.util.Log;
23import android.view.MotionEvent;
24
25import java.io.BufferedWriter;
26import java.io.FileDescriptor;
27import java.io.FileWriter;
28import java.io.IOException;
29import java.io.PrintWriter;
30import java.util.HashSet;
31import java.util.LinkedList;
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 = 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