ResearchLog.java revision 7423005b96b3afc3a6ff55ae40d143920d5a6221
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.research;
18
19import android.content.Context;
20import android.util.JsonWriter;
21import android.util.Log;
22
23import com.android.inputmethod.annotations.UsedForTesting;
24import com.android.inputmethod.latin.define.ProductionFlag;
25
26import java.io.BufferedWriter;
27import java.io.File;
28import java.io.IOException;
29import java.io.OutputStream;
30import java.io.OutputStreamWriter;
31import java.util.concurrent.Callable;
32import java.util.concurrent.Executors;
33import java.util.concurrent.RejectedExecutionException;
34import java.util.concurrent.ScheduledExecutorService;
35import java.util.concurrent.ScheduledFuture;
36import java.util.concurrent.TimeUnit;
37
38/**
39 * Logs the use of the LatinIME keyboard.
40 *
41 * This class logs operations on the IME keyboard, including what the user has typed.  Data is
42 * written to a {@link JsonWriter}, which will write to a local file.
43 *
44 * The JsonWriter is created on-demand by calling {@link #getInitializedJsonWriterLocked}.
45 *
46 * This class uses an executor to perform file-writing operations on a separate thread.  It also
47 * tries to avoid creating unnecessary files if there is nothing to write.  It also handles
48 * flushing, making sure it happens, but not too frequently.
49 *
50 * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
51 */
52public class ResearchLog {
53    // TODO: Automatically initialize the JsonWriter rather than requiring the caller to manage it.
54    private static final String TAG = ResearchLog.class.getSimpleName();
55    private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
56    private static final long FLUSH_DELAY_IN_MS = 1000 * 5;
57
58    /* package */ final ScheduledExecutorService mExecutor;
59    /* package */ final File mFile;
60    private final Context mContext;
61
62    private JsonWriter mJsonWriter = NULL_JSON_WRITER;
63    // true if at least one byte of data has been written out to the log file.  This must be
64    // remembered because JsonWriter requires that calls matching calls to beginObject and
65    // endObject, as well as beginArray and endArray, and the file is opened lazily, only when
66    // it is certain that data will be written.  Alternatively, the matching call exceptions
67    // could be caught, but this might suppress other errors.
68    private boolean mHasWrittenData = false;
69
70    private static final JsonWriter NULL_JSON_WRITER = new JsonWriter(
71            new OutputStreamWriter(new NullOutputStream()));
72    private static class NullOutputStream extends OutputStream {
73        /** {@inheritDoc} */
74        @Override
75        public void write(byte[] buffer, int offset, int count) {
76            // nop
77        }
78
79        /** {@inheritDoc} */
80        @Override
81        public void write(byte[] buffer) {
82            // nop
83        }
84
85        @Override
86        public void write(int oneByte) {
87        }
88    }
89
90    public ResearchLog(final File outputFile, final Context context) {
91        mExecutor = Executors.newSingleThreadScheduledExecutor();
92        mFile = outputFile;
93        mContext = context;
94    }
95
96    /**
97     * Waits for any publication requests to finish and closes the {@link JsonWriter} used for
98     * output.
99     *
100     * See class comment for details about {@code JsonWriter} construction.
101     */
102    private synchronized void close(final Runnable onClosed) {
103        mExecutor.submit(new Callable<Object>() {
104            @Override
105            public Object call() throws Exception {
106                try {
107                    if (mHasWrittenData) {
108                        mJsonWriter.endArray();
109                        mHasWrittenData = false;
110                    }
111                    mJsonWriter.flush();
112                    mJsonWriter.close();
113                    if (DEBUG) {
114                        Log.d(TAG, "wrote log to " + mFile);
115                    }
116                } catch (Exception e) {
117                    Log.d(TAG, "error when closing ResearchLog:", e);
118                } finally {
119                    if (mFile != null && mFile.exists()) {
120                        mFile.setWritable(false, false);
121                    }
122                    if (onClosed != null) {
123                        onClosed.run();
124                    }
125                }
126                return null;
127            }
128        });
129        removeAnyScheduledFlush();
130        mExecutor.shutdown();
131    }
132
133    /**
134     * Block until the research log has shut down and spooled out all output or {@code timeout}
135     * occurs.
136     *
137     * @param timeout time to wait for close in milliseconds
138     */
139    public void blockingClose(final long timeout) {
140        close(null);
141        awaitTermination(timeout, TimeUnit.MILLISECONDS);
142    }
143
144    /**
145     * Waits for publication requests to finish, closes the JsonWriter, but then deletes the backing
146     * output file.
147     */
148    private synchronized void abort() {
149        mExecutor.submit(new Callable<Object>() {
150            @Override
151            public Object call() throws Exception {
152                try {
153                    if (mHasWrittenData) {
154                        mJsonWriter.endArray();
155                        mJsonWriter.close();
156                        mHasWrittenData = false;
157                    }
158                } finally {
159                    if (mFile != null) {
160                        mFile.delete();
161                    }
162                }
163                return null;
164            }
165        });
166        removeAnyScheduledFlush();
167        mExecutor.shutdown();
168    }
169
170    /**
171     * Block until the research log has aborted or {@code timeout} occurs.
172     *
173     * @param timeout time to wait for close in milliseconds
174     */
175    public void blockingAbort(final long timeout) {
176        abort();
177        awaitTermination(timeout, TimeUnit.MILLISECONDS);
178    }
179
180    @UsedForTesting
181    public void awaitTermination(final long delay, final TimeUnit timeUnit) {
182        try {
183            if (!mExecutor.awaitTermination(delay, timeUnit)) {
184                Log.e(TAG, "ResearchLog executor timed out while awaiting terminaion");
185            }
186        } catch (final InterruptedException e) {
187            Log.e(TAG, "ResearchLog executor interrupted while awaiting terminaion", e);
188        }
189    }
190
191    /* package */ synchronized void flush() {
192        removeAnyScheduledFlush();
193        mExecutor.submit(mFlushCallable);
194    }
195
196    private final Callable<Object> mFlushCallable = new Callable<Object>() {
197        @Override
198        public Object call() throws Exception {
199            mJsonWriter.flush();
200            return null;
201        }
202    };
203
204    private ScheduledFuture<Object> mFlushFuture;
205
206    private void removeAnyScheduledFlush() {
207        if (mFlushFuture != null) {
208            mFlushFuture.cancel(false);
209            mFlushFuture = null;
210        }
211    }
212
213    private void scheduleFlush() {
214        removeAnyScheduledFlush();
215        mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS);
216    }
217
218    /**
219     * Queues up {@code logUnit} to be published in the background.
220     *
221     * @param logUnit the {@link LogUnit} to be published
222     * @param canIncludePrivateData whether private data in the LogUnit should be included
223     */
224    public synchronized void publish(final LogUnit logUnit, final boolean canIncludePrivateData) {
225        try {
226            mExecutor.submit(new Callable<Object>() {
227                @Override
228                public Object call() throws Exception {
229                    logUnit.publishTo(ResearchLog.this, canIncludePrivateData);
230                    scheduleFlush();
231                    return null;
232                }
233            });
234        } catch (RejectedExecutionException e) {
235            // TODO: Add code to record loss of data, and report.
236            if (DEBUG) {
237                Log.d(TAG, "ResearchLog.publish() rejecting scheduled execution");
238            }
239        }
240    }
241
242    /**
243     * Return a JsonWriter for this ResearchLog.  It is initialized the first time this method is
244     * called.  The cached value is returned in future calls.
245     */
246    public JsonWriter getInitializedJsonWriterLocked() {
247        if (mJsonWriter != NULL_JSON_WRITER || mFile == null) return mJsonWriter;
248        try {
249            final JsonWriter jsonWriter = createJsonWriter(mContext, mFile);
250            if (jsonWriter != null) {
251                jsonWriter.beginArray();
252                mJsonWriter = jsonWriter;
253                mHasWrittenData = true;
254            }
255        } catch (final IOException e) {
256            Log.w(TAG, "Error in JsonWriter; disabling logging", e);
257            try {
258                mJsonWriter.close();
259            } catch (final IllegalStateException e1) {
260                // Assume that this is just the json not being terminated properly.
261                // Ignore
262            } catch (final IOException e1) {
263                Log.w(TAG, "Error in closing JsonWriter; disabling logging", e1);
264            } finally {
265                mJsonWriter = NULL_JSON_WRITER;
266            }
267        }
268        return mJsonWriter;
269    }
270
271    /**
272     * Create the JsonWriter to write the ResearchLog to.
273     *
274     * This method may be overriden in testing to redirect the output.
275     */
276    /* package for test */ JsonWriter createJsonWriter(final Context context, final File file)
277            throws IOException {
278        return new JsonWriter(new BufferedWriter(new OutputStreamWriter(
279                context.openFileOutput(file.getName(), Context.MODE_PRIVATE))));
280    }
281}
282