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