ResearchLog.java revision f170f145afa821537b2e97a02a00da96723bb84e
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.latin.define.ProductionFlag; 24 25import java.io.BufferedWriter; 26import java.io.File; 27import java.io.FileOutputStream; 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 private static final int ABORT_TIMEOUT_IN_MS = 1000 * 4; 58 59 /* package */ final ScheduledExecutorService mExecutor; 60 /* package */ final File mFile; 61 private final Context mContext; 62 63 private JsonWriter mJsonWriter = NULL_JSON_WRITER; 64 // true if at least one byte of data has been written out to the log file. This must be 65 // remembered because JsonWriter requires that calls matching calls to beginObject and 66 // endObject, as well as beginArray and endArray, and the file is opened lazily, only when 67 // it is certain that data will be written. Alternatively, the matching call exceptions 68 // could be caught, but this might suppress other errors. 69 private boolean mHasWrittenData = false; 70 71 private static final JsonWriter NULL_JSON_WRITER = new JsonWriter( 72 new OutputStreamWriter(new NullOutputStream())); 73 private static class NullOutputStream extends OutputStream { 74 /** {@inheritDoc} */ 75 @Override 76 public void write(byte[] buffer, int offset, int count) { 77 // nop 78 } 79 80 /** {@inheritDoc} */ 81 @Override 82 public void write(byte[] buffer) { 83 // nop 84 } 85 86 @Override 87 public void write(int oneByte) { 88 } 89 } 90 91 public ResearchLog(final File outputFile, final Context context) { 92 mExecutor = Executors.newSingleThreadScheduledExecutor(); 93 mFile = outputFile; 94 mContext = context; 95 } 96 97 /** 98 * Waits for any publication requests to finish and closes the {@link JsonWriter} used for 99 * output. 100 * 101 * See class comment for details about {@code JsonWriter} construction. 102 */ 103 public synchronized void close(final Runnable onClosed) { 104 mExecutor.submit(new Callable<Object>() { 105 @Override 106 public Object call() throws Exception { 107 try { 108 if (mHasWrittenData) { 109 mJsonWriter.endArray(); 110 mHasWrittenData = false; 111 } 112 mJsonWriter.flush(); 113 mJsonWriter.close(); 114 if (DEBUG) { 115 Log.d(TAG, "wrote log to " + mFile); 116 } 117 } catch (Exception e) { 118 Log.d(TAG, "error when closing ResearchLog:", e); 119 } finally { 120 if (mFile != null && mFile.exists()) { 121 mFile.setWritable(false, false); 122 } 123 if (onClosed != null) { 124 onClosed.run(); 125 } 126 } 127 return null; 128 } 129 }); 130 removeAnyScheduledFlush(); 131 mExecutor.shutdown(); 132 } 133 134 private boolean mIsAbortSuccessful; 135 136 /** 137 * Waits for publication requests to finish, closes the {@link JsonWriter}, but then deletes the 138 * backing file used for output. 139 * 140 * See class comment for details about {@code JsonWriter} construction. 141 */ 142 public synchronized void abort() { 143 mExecutor.submit(new Callable<Object>() { 144 @Override 145 public Object call() throws Exception { 146 try { 147 if (mHasWrittenData) { 148 mJsonWriter.endArray(); 149 mJsonWriter.close(); 150 mHasWrittenData = false; 151 } 152 } finally { 153 if (mFile != null) { 154 mIsAbortSuccessful = mFile.delete(); 155 } 156 } 157 return null; 158 } 159 }); 160 removeAnyScheduledFlush(); 161 mExecutor.shutdown(); 162 } 163 164 public boolean blockingAbort() throws InterruptedException { 165 abort(); 166 mExecutor.awaitTermination(ABORT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS); 167 return mIsAbortSuccessful; 168 } 169 170 public void awaitTermination(int delay, TimeUnit timeUnit) throws InterruptedException { 171 mExecutor.awaitTermination(delay, timeUnit); 172 } 173 174 /* package */ synchronized void flush() { 175 removeAnyScheduledFlush(); 176 mExecutor.submit(mFlushCallable); 177 } 178 179 private final Callable<Object> mFlushCallable = new Callable<Object>() { 180 @Override 181 public Object call() throws Exception { 182 mJsonWriter.flush(); 183 return null; 184 } 185 }; 186 187 private ScheduledFuture<Object> mFlushFuture; 188 189 private void removeAnyScheduledFlush() { 190 if (mFlushFuture != null) { 191 mFlushFuture.cancel(false); 192 mFlushFuture = null; 193 } 194 } 195 196 private void scheduleFlush() { 197 removeAnyScheduledFlush(); 198 mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS); 199 } 200 201 /** 202 * Queues up {@code logUnit} to be published in the background. 203 * 204 * @param logUnit the {@link LogUnit} to be published 205 * @param canIncludePrivateData whether private data in the LogUnit should be included 206 */ 207 public synchronized void publish(final LogUnit logUnit, final boolean canIncludePrivateData) { 208 try { 209 mExecutor.submit(new Callable<Object>() { 210 @Override 211 public Object call() throws Exception { 212 logUnit.publishTo(ResearchLog.this, canIncludePrivateData); 213 scheduleFlush(); 214 return null; 215 } 216 }); 217 } catch (RejectedExecutionException e) { 218 // TODO: Add code to record loss of data, and report. 219 if (DEBUG) { 220 Log.d(TAG, "ResearchLog.publish() rejecting scheduled execution"); 221 } 222 } 223 } 224 225 /** 226 * Return a JsonWriter for this ResearchLog. It is initialized the first time this method is 227 * called. The cached value is returned in future calls. 228 */ 229 public JsonWriter getInitializedJsonWriterLocked() { 230 if (mJsonWriter != NULL_JSON_WRITER || mFile == null) return mJsonWriter; 231 try { 232 final JsonWriter jsonWriter = createJsonWriter(mContext, mFile); 233 if (jsonWriter != null) { 234 jsonWriter.beginArray(); 235 mJsonWriter = jsonWriter; 236 mHasWrittenData = true; 237 } 238 } catch (final IOException e) { 239 Log.w(TAG, "Error in JsonWriter; disabling logging", e); 240 try { 241 mJsonWriter.close(); 242 } catch (final IllegalStateException e1) { 243 // Assume that this is just the json not being terminated properly. 244 // Ignore 245 } catch (final IOException e1) { 246 Log.w(TAG, "Error in closing JsonWriter; disabling logging", e1); 247 } finally { 248 mJsonWriter = NULL_JSON_WRITER; 249 } 250 } 251 return mJsonWriter; 252 } 253 254 /** 255 * Create the JsonWriter to write the ResearchLog to. 256 * 257 * This method may be overriden in testing to redirect the output. 258 */ 259 /* package for test */ JsonWriter createJsonWriter(final Context context, final File file) 260 throws IOException { 261 return new JsonWriter(new BufferedWriter(new OutputStreamWriter( 262 context.openFileOutput(file.getName(), Context.MODE_PRIVATE)))); 263 } 264} 265