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