ResearchLog.java revision 6b966160ac8570271547bf63217efa5e228d4acc
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.inputmethod.research;
18
19import android.content.SharedPreferences;
20import android.os.SystemClock;
21import android.util.JsonWriter;
22import android.util.Log;
23import android.view.inputmethod.CompletionInfo;
24
25import com.android.inputmethod.keyboard.Key;
26import com.android.inputmethod.latin.SuggestedWords;
27import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
28import com.android.inputmethod.latin.define.ProductionFlag;
29import com.android.inputmethod.research.ResearchLogger.LogUnit;
30
31import java.io.BufferedWriter;
32import java.io.File;
33import java.io.FileWriter;
34import java.io.IOException;
35import java.io.OutputStream;
36import java.io.OutputStreamWriter;
37import java.util.Map;
38import java.util.concurrent.Callable;
39import java.util.concurrent.Executors;
40import java.util.concurrent.ScheduledExecutorService;
41import java.util.concurrent.ScheduledFuture;
42import java.util.concurrent.TimeUnit;
43
44/**
45 * Logs the use of the LatinIME keyboard.
46 *
47 * This class logs operations on the IME keyboard, including what the user has typed.
48 * Data is stored locally in a file in app-specific storage.
49 *
50 * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
51 */
52public class ResearchLog {
53    private static final String TAG = ResearchLog.class.getSimpleName();
54    private static final JsonWriter NULL_JSON_WRITER = new JsonWriter(
55            new OutputStreamWriter(new NullOutputStream()));
56
57    final ScheduledExecutorService mExecutor;
58    /* package */ final File mFile;
59    private JsonWriter mJsonWriter = NULL_JSON_WRITER;
60
61    private int mLoggingState;
62    private static final int LOGGING_STATE_UNSTARTED = 0;
63    private static final int LOGGING_STATE_READY = 1;   // don't create file until necessary
64    private static final int LOGGING_STATE_RUNNING = 2;
65    private static final int LOGGING_STATE_STOPPING = 3;
66    private static final int LOGGING_STATE_STOPPED = 4;
67    private static final long FLUSH_DELAY_IN_MS = 1000 * 5;
68
69    private static class NullOutputStream extends OutputStream {
70        /** {@inheritDoc} */
71        @Override
72        public void write(byte[] buffer, int offset, int count) {
73            // nop
74        }
75
76        /** {@inheritDoc} */
77        @Override
78        public void write(byte[] buffer) {
79            // nop
80        }
81
82        @Override
83        public void write(int oneByte) {
84        }
85    }
86
87    public ResearchLog(File outputFile) {
88        mExecutor = Executors.newSingleThreadScheduledExecutor();
89        if (outputFile == null) {
90            throw new IllegalArgumentException();
91        }
92        mFile = outputFile;
93        mLoggingState = LOGGING_STATE_UNSTARTED;
94    }
95
96    public synchronized void start() throws IOException {
97        switch (mLoggingState) {
98            case LOGGING_STATE_UNSTARTED:
99                mLoggingState = LOGGING_STATE_READY;
100                break;
101            case LOGGING_STATE_READY:
102            case LOGGING_STATE_RUNNING:
103            case LOGGING_STATE_STOPPING:
104            case LOGGING_STATE_STOPPED:
105                break;
106        }
107    }
108
109    public synchronized void stop() {
110        switch (mLoggingState) {
111            case LOGGING_STATE_UNSTARTED:
112                mLoggingState = LOGGING_STATE_STOPPED;
113                break;
114            case LOGGING_STATE_READY:
115            case LOGGING_STATE_RUNNING:
116                mExecutor.submit(new Callable<Object>() {
117                    @Override
118                    public Object call() throws Exception {
119                        try {
120                            mJsonWriter.endArray();
121                            mJsonWriter.flush();
122                            mJsonWriter.close();
123                        } finally {
124                            boolean success = mFile.setWritable(false, false);
125                            mLoggingState = LOGGING_STATE_STOPPED;
126                        }
127                        return null;
128                    }
129                });
130                removeAnyScheduledFlush();
131                mExecutor.shutdown();
132                mLoggingState = LOGGING_STATE_STOPPING;
133                break;
134            case LOGGING_STATE_STOPPING:
135            case LOGGING_STATE_STOPPED:
136        }
137    }
138
139    public boolean isAlive() {
140        switch (mLoggingState) {
141            case LOGGING_STATE_UNSTARTED:
142            case LOGGING_STATE_READY:
143            case LOGGING_STATE_RUNNING:
144                return true;
145        }
146        return false;
147    }
148
149    public void waitUntilStopped(final int timeoutInMs) throws InterruptedException {
150        removeAnyScheduledFlush();
151        mExecutor.shutdown();
152        mExecutor.awaitTermination(timeoutInMs, TimeUnit.MILLISECONDS);
153    }
154
155    public synchronized void abort() {
156        switch (mLoggingState) {
157            case LOGGING_STATE_UNSTARTED:
158                mLoggingState = LOGGING_STATE_STOPPED;
159                isAbortSuccessful = true;
160                break;
161            case LOGGING_STATE_READY:
162            case LOGGING_STATE_RUNNING:
163                mExecutor.submit(new Callable<Object>() {
164                    @Override
165                    public Object call() throws Exception {
166                        try {
167                            mJsonWriter.endArray();
168                            mJsonWriter.close();
169                        } finally {
170                            isAbortSuccessful = mFile.delete();
171                        }
172                        return null;
173                    }
174                });
175                removeAnyScheduledFlush();
176                mExecutor.shutdown();
177                mLoggingState = LOGGING_STATE_STOPPING;
178                break;
179            case LOGGING_STATE_STOPPING:
180            case LOGGING_STATE_STOPPED:
181        }
182    }
183
184    private boolean isAbortSuccessful;
185    public boolean isAbortSuccessful() {
186        return isAbortSuccessful;
187    }
188
189    /* package */ synchronized void flush() {
190        switch (mLoggingState) {
191            case LOGGING_STATE_UNSTARTED:
192                break;
193            case LOGGING_STATE_READY:
194            case LOGGING_STATE_RUNNING:
195                removeAnyScheduledFlush();
196                mExecutor.submit(mFlushCallable);
197                break;
198            case LOGGING_STATE_STOPPING:
199            case LOGGING_STATE_STOPPED:
200        }
201    }
202
203    private Callable<Object> mFlushCallable = new Callable<Object>() {
204        @Override
205        public Object call() throws Exception {
206            if (mLoggingState == LOGGING_STATE_RUNNING) {
207                mJsonWriter.flush();
208            }
209            return null;
210        }
211    };
212
213    private ScheduledFuture<Object> mFlushFuture;
214
215    private void removeAnyScheduledFlush() {
216        if (mFlushFuture != null) {
217            mFlushFuture.cancel(false);
218            mFlushFuture = null;
219        }
220    }
221
222    private void scheduleFlush() {
223        removeAnyScheduledFlush();
224        mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS);
225    }
226
227    public synchronized void publishPublicEvents(final LogUnit logUnit) {
228        switch (mLoggingState) {
229            case LOGGING_STATE_UNSTARTED:
230                break;
231            case LOGGING_STATE_READY:
232            case LOGGING_STATE_RUNNING:
233                mExecutor.submit(new Callable<Object>() {
234                    @Override
235                    public Object call() throws Exception {
236                        logUnit.publishPublicEventsTo(ResearchLog.this);
237                        scheduleFlush();
238                        return null;
239                    }
240                });
241                break;
242            case LOGGING_STATE_STOPPING:
243            case LOGGING_STATE_STOPPED:
244        }
245    }
246
247    public synchronized void publishAllEvents(final LogUnit logUnit) {
248        switch (mLoggingState) {
249            case LOGGING_STATE_UNSTARTED:
250                break;
251            case LOGGING_STATE_READY:
252            case LOGGING_STATE_RUNNING:
253                mExecutor.submit(new Callable<Object>() {
254                    @Override
255                    public Object call() throws Exception {
256                        logUnit.publishAllEventsTo(ResearchLog.this);
257                        scheduleFlush();
258                        return null;
259                    }
260                });
261                break;
262            case LOGGING_STATE_STOPPING:
263            case LOGGING_STATE_STOPPED:
264        }
265    }
266
267    private static final String CURRENT_TIME_KEY = "_ct";
268    private static final String UPTIME_KEY = "_ut";
269    private static final String EVENT_TYPE_KEY = "_ty";
270    void outputEvent(final String[] keys, final Object[] values) {
271        // not thread safe.
272        try {
273            if (mJsonWriter == NULL_JSON_WRITER) {
274                mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile)));
275                mJsonWriter.setLenient(true);
276                mJsonWriter.beginArray();
277            }
278            mJsonWriter.beginObject();
279            mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
280            mJsonWriter.name(UPTIME_KEY).value(SystemClock.uptimeMillis());
281            mJsonWriter.name(EVENT_TYPE_KEY).value(keys[0]);
282            final int length = values.length;
283            for (int i = 0; i < length; i++) {
284                mJsonWriter.name(keys[i + 1]);
285                Object value = values[i];
286                if (value instanceof String) {
287                    mJsonWriter.value((String) value);
288                } else if (value instanceof Number) {
289                    mJsonWriter.value((Number) value);
290                } else if (value instanceof Boolean) {
291                    mJsonWriter.value((Boolean) value);
292                } else if (value instanceof CompletionInfo[]) {
293                    CompletionInfo[] ci = (CompletionInfo[]) value;
294                    mJsonWriter.beginArray();
295                    for (int j = 0; j < ci.length; j++) {
296                        mJsonWriter.value(ci[j].toString());
297                    }
298                    mJsonWriter.endArray();
299                } else if (value instanceof SharedPreferences) {
300                    SharedPreferences prefs = (SharedPreferences) value;
301                    mJsonWriter.beginObject();
302                    for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
303                        mJsonWriter.name(entry.getKey());
304                        final Object innerValue = entry.getValue();
305                        if (innerValue == null) {
306                            mJsonWriter.nullValue();
307                        } else if (innerValue instanceof Boolean) {
308                            mJsonWriter.value((Boolean) innerValue);
309                        } else if (innerValue instanceof Number) {
310                            mJsonWriter.value((Number) innerValue);
311                        } else {
312                            mJsonWriter.value(innerValue.toString());
313                        }
314                    }
315                    mJsonWriter.endObject();
316                } else if (value instanceof Key[]) {
317                    Key[] keyboardKeys = (Key[]) value;
318                    mJsonWriter.beginArray();
319                    for (Key keyboardKey : keyboardKeys) {
320                        mJsonWriter.beginObject();
321                        mJsonWriter.name("code").value(keyboardKey.mCode);
322                        mJsonWriter.name("altCode").value(keyboardKey.mAltCode);
323                        mJsonWriter.name("x").value(keyboardKey.mX);
324                        mJsonWriter.name("y").value(keyboardKey.mY);
325                        mJsonWriter.name("w").value(keyboardKey.mWidth);
326                        mJsonWriter.name("h").value(keyboardKey.mHeight);
327                        mJsonWriter.endObject();
328                    }
329                    mJsonWriter.endArray();
330                } else if (value instanceof SuggestedWords) {
331                    SuggestedWords words = (SuggestedWords) value;
332                    mJsonWriter.beginObject();
333                    mJsonWriter.name("typedWordValid").value(words.mTypedWordValid);
334                    mJsonWriter.name("willAutoCorrect")
335                        .value(words.mWillAutoCorrect);
336                    mJsonWriter.name("isPunctuationSuggestions")
337                        .value(words.mIsPunctuationSuggestions);
338                    mJsonWriter.name("isObsoleteSuggestions")
339                        .value(words.mIsObsoleteSuggestions);
340                    mJsonWriter.name("isPrediction")
341                        .value(words.mIsPrediction);
342                    mJsonWriter.name("words");
343                    mJsonWriter.beginArray();
344                    final int size = words.size();
345                    for (int j = 0; j < size; j++) {
346                        SuggestedWordInfo wordInfo = words.getWordInfo(j);
347                        mJsonWriter.value(wordInfo.toString());
348                    }
349                    mJsonWriter.endArray();
350                    mJsonWriter.endObject();
351                } else if (value == null) {
352                    mJsonWriter.nullValue();
353                } else {
354                    Log.w(TAG, "Unrecognized type to be logged: " +
355                            (value == null ? "<null>" : value.getClass().getName()));
356                    mJsonWriter.nullValue();
357                }
358            }
359            mJsonWriter.endObject();
360        } catch (IOException e) {
361            e.printStackTrace();
362            Log.w(TAG, "Error in JsonWriter; disabling logging");
363            try {
364                mJsonWriter.close();
365            } catch (IllegalStateException e1) {
366                // assume that this is just the json not being terminated properly.
367                // ignore
368            } catch (IOException e1) {
369                e1.printStackTrace();
370            } finally {
371                mJsonWriter = NULL_JSON_WRITER;
372            }
373        }
374    }
375}
376