LogUnit.java revision 0c16a5c6eef645fd536671994e0b4f05864ac338
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.SharedPreferences; 20import android.text.TextUtils; 21import android.util.JsonWriter; 22import android.util.Log; 23import android.view.MotionEvent; 24import android.view.inputmethod.CompletionInfo; 25 26import com.android.inputmethod.keyboard.Key; 27import com.android.inputmethod.latin.SuggestedWords; 28import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 29import com.android.inputmethod.latin.Utils; 30import com.android.inputmethod.latin.define.ProductionFlag; 31import com.android.inputmethod.research.ResearchLogger.LogStatement; 32 33import java.io.IOException; 34import java.io.StringWriter; 35import java.util.ArrayList; 36import java.util.List; 37import java.util.Map; 38 39/** 40 * A group of log statements related to each other. 41 * 42 * A LogUnit is collection of LogStatements, each of which is generated by at a particular point 43 * in the code. (There is no LogStatement class; the data is stored across the instance variables 44 * here.) A single LogUnit's statements can correspond to all the calls made while in the same 45 * composing region, or all the calls between committing the last composing region, and the first 46 * character of the next composing region. 47 * 48 * Individual statements in a log may be marked as potentially private. If so, then they are only 49 * published to a ResearchLog if the ResearchLogger determines that publishing the entire LogUnit 50 * will not violate the user's privacy. Checks for this may include whether other LogUnits have 51 * been published recently, or whether the LogUnit contains numbers, etc. 52 */ 53/* package */ class LogUnit { 54 private static final String TAG = LogUnit.class.getSimpleName(); 55 private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; 56 private final ArrayList<LogStatement> mLogStatementList; 57 private final ArrayList<Object[]> mValuesList; 58 // Assume that mTimeList is sorted in increasing order. Do not insert null values into 59 // mTimeList. 60 private final ArrayList<Long> mTimeList; 61 // Word that this LogUnit generates. Should be null if the LogUnit does not generate a genuine 62 // word (i.e. separators alone do not count as a word). Should never be empty. 63 private String mWord; 64 private boolean mMayContainDigit; 65 private boolean mIsPartOfMegaword; 66 private boolean mContainsCorrection; 67 68 // mCorrectionType indicates whether the word was corrected at all, and if so, whether it was 69 // to a different word or just a "typo" correction. It is considered a "typo" if the final 70 // word was listed in the suggestions available the first time the word was gestured or 71 // tapped. 72 private int mCorrectionType; 73 public static final int CORRECTIONTYPE_NO_CORRECTION = 0; 74 public static final int CORRECTIONTYPE_CORRECTION = 1; 75 public static final int CORRECTIONTYPE_DIFFERENT_WORD = 2; 76 public static final int CORRECTIONTYPE_TYPO = 3; 77 78 private SuggestedWords mSuggestedWords; 79 80 public LogUnit() { 81 mLogStatementList = new ArrayList<LogStatement>(); 82 mValuesList = new ArrayList<Object[]>(); 83 mTimeList = new ArrayList<Long>(); 84 mIsPartOfMegaword = false; 85 mCorrectionType = CORRECTIONTYPE_NO_CORRECTION; 86 mSuggestedWords = null; 87 } 88 89 private LogUnit(final ArrayList<LogStatement> logStatementList, 90 final ArrayList<Object[]> valuesList, 91 final ArrayList<Long> timeList, 92 final boolean isPartOfMegaword) { 93 mLogStatementList = logStatementList; 94 mValuesList = valuesList; 95 mTimeList = timeList; 96 mIsPartOfMegaword = isPartOfMegaword; 97 mCorrectionType = CORRECTIONTYPE_NO_CORRECTION; 98 mSuggestedWords = null; 99 } 100 101 private static final Object[] NULL_VALUES = new Object[0]; 102 /** 103 * Adds a new log statement. The time parameter in successive calls to this method must be 104 * monotonically increasing, or splitByTime() will not work. 105 */ 106 public void addLogStatement(final LogStatement logStatement, final long time, 107 Object... values) { 108 if (values == null) { 109 values = NULL_VALUES; 110 } 111 mLogStatementList.add(logStatement); 112 mValuesList.add(values); 113 mTimeList.add(time); 114 } 115 116 /** 117 * Publish the contents of this LogUnit to researchLog. 118 */ 119 public synchronized void publishTo(final ResearchLog researchLog, 120 final boolean canIncludePrivateData) { 121 // Prepare debugging output if necessary 122 final StringWriter debugStringWriter; 123 final JsonWriter debugJsonWriter; 124 if (DEBUG) { 125 debugStringWriter = new StringWriter(); 126 debugJsonWriter = new JsonWriter(debugStringWriter); 127 debugJsonWriter.setIndent(" "); 128 try { 129 debugJsonWriter.beginArray(); 130 } catch (IOException e) { 131 Log.e(TAG, "Could not open array in JsonWriter", e); 132 } 133 } else { 134 debugStringWriter = null; 135 debugJsonWriter = null; 136 } 137 // Write out any logStatement that passes the privacy filter. 138 final int size = mLogStatementList.size(); 139 if (size != 0) { 140 // Note that jsonWriter is only set to a non-null value if the logUnit start text is 141 // output and at least one logStatement is output. 142 JsonWriter jsonWriter = null; 143 for (int i = 0; i < size; i++) { 144 final LogStatement logStatement = mLogStatementList.get(i); 145 if (!canIncludePrivateData && logStatement.mIsPotentiallyPrivate) { 146 continue; 147 } 148 if (mIsPartOfMegaword && logStatement.mIsPotentiallyRevealing) { 149 continue; 150 } 151 // Only retrieve the jsonWriter if we need to. If we don't get this far, then 152 // researchLog.getValidJsonWriterLocked() will not ever be called, and the file 153 // will not have been opened for writing. 154 if (jsonWriter == null) { 155 jsonWriter = researchLog.getValidJsonWriterLocked(); 156 outputLogUnitStart(jsonWriter, canIncludePrivateData); 157 } 158 outputLogStatementToLocked(jsonWriter, mLogStatementList.get(i), mValuesList.get(i), 159 mTimeList.get(i)); 160 if (DEBUG) { 161 outputLogStatementToLocked(debugJsonWriter, mLogStatementList.get(i), 162 mValuesList.get(i), mTimeList.get(i)); 163 } 164 } 165 if (jsonWriter != null) { 166 // We must have called logUnitStart earlier, so emit a logUnitStop. 167 outputLogUnitStop(jsonWriter); 168 } 169 } 170 if (DEBUG) { 171 try { 172 debugJsonWriter.endArray(); 173 debugJsonWriter.flush(); 174 } catch (IOException e) { 175 Log.e(TAG, "Could not close array in JsonWriter", e); 176 } 177 final String bigString = debugStringWriter.getBuffer().toString(); 178 final String[] lines = bigString.split("\n"); 179 for (String line : lines) { 180 Log.d(TAG, line); 181 } 182 } 183 } 184 185 private static final String CURRENT_TIME_KEY = "_ct"; 186 private static final String UPTIME_KEY = "_ut"; 187 private static final String EVENT_TYPE_KEY = "_ty"; 188 private static final String WORD_KEY = "_wo"; 189 private static final String CORRECTION_TYPE_KEY = "_corType"; 190 private static final String LOG_UNIT_BEGIN_KEY = "logUnitStart"; 191 private static final String LOG_UNIT_END_KEY = "logUnitEnd"; 192 193 private void outputLogUnitStart(final JsonWriter jsonWriter, 194 final boolean canIncludePrivateData) { 195 try { 196 jsonWriter.beginObject(); 197 jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); 198 if (canIncludePrivateData) { 199 jsonWriter.name(WORD_KEY).value(getWord()); 200 jsonWriter.name(CORRECTION_TYPE_KEY).value(getCorrectionType()); 201 } 202 jsonWriter.name(EVENT_TYPE_KEY).value(LOG_UNIT_BEGIN_KEY); 203 jsonWriter.endObject(); 204 } catch (IOException e) { 205 e.printStackTrace(); 206 Log.w(TAG, "Error in JsonWriter; cannot write LogUnitStart"); 207 } 208 } 209 210 private void outputLogUnitStop(final JsonWriter jsonWriter) { 211 try { 212 jsonWriter.beginObject(); 213 jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); 214 jsonWriter.name(EVENT_TYPE_KEY).value(LOG_UNIT_END_KEY); 215 jsonWriter.endObject(); 216 } catch (IOException e) { 217 e.printStackTrace(); 218 Log.w(TAG, "Error in JsonWriter; cannot write LogUnitStop"); 219 } 220 } 221 222 /** 223 * Write the logStatement and its contents out through jsonWriter. 224 * 225 * Note that this method is not thread safe for the same jsonWriter. Callers must ensure 226 * thread safety. 227 */ 228 private boolean outputLogStatementToLocked(final JsonWriter jsonWriter, 229 final LogStatement logStatement, final Object[] values, final Long time) { 230 if (DEBUG) { 231 if (logStatement.mKeys.length != values.length) { 232 Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.mName); 233 } 234 } 235 try { 236 jsonWriter.beginObject(); 237 jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); 238 jsonWriter.name(UPTIME_KEY).value(time); 239 jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.mName); 240 final String[] keys = logStatement.mKeys; 241 final int length = values.length; 242 for (int i = 0; i < length; i++) { 243 jsonWriter.name(keys[i]); 244 final Object value = values[i]; 245 if (value instanceof CharSequence) { 246 jsonWriter.value(value.toString()); 247 } else if (value instanceof Number) { 248 jsonWriter.value((Number) value); 249 } else if (value instanceof Boolean) { 250 jsonWriter.value((Boolean) value); 251 } else if (value instanceof CompletionInfo[]) { 252 JsonUtils.writeJson((CompletionInfo[]) value, jsonWriter); 253 } else if (value instanceof SharedPreferences) { 254 JsonUtils.writeJson((SharedPreferences) value, jsonWriter); 255 } else if (value instanceof Key[]) { 256 JsonUtils.writeJson((Key[]) value, jsonWriter); 257 } else if (value instanceof SuggestedWords) { 258 JsonUtils.writeJson((SuggestedWords) value, jsonWriter); 259 } else if (value instanceof MotionEvent) { 260 JsonUtils.writeJson((MotionEvent) value, jsonWriter); 261 } else if (value == null) { 262 jsonWriter.nullValue(); 263 } else { 264 Log.w(TAG, "Unrecognized type to be logged: " + 265 (value == null ? "<null>" : value.getClass().getName())); 266 jsonWriter.nullValue(); 267 } 268 } 269 jsonWriter.endObject(); 270 } catch (IOException e) { 271 e.printStackTrace(); 272 Log.w(TAG, "Error in JsonWriter; skipping LogStatement"); 273 return false; 274 } 275 return true; 276 } 277 278 /** 279 * Mark the current logUnit as containing data to generate {@code word}. 280 * 281 * If {@code setWord()} was previously called for this LogUnit, then the method will try to 282 * determine what kind of correction it is, and update its internal state of the correctionType 283 * accordingly. 284 * 285 * @param word The word this LogUnit generates. Caller should not pass null or the empty 286 * string. 287 */ 288 public void setWord(final String word) { 289 if (mWord != null) { 290 // The word was already set once, and it is now being changed. See if the new word 291 // is close to the old word. If so, then the change is probably a typo correction. 292 // If not, the user may have decided to enter a different word, so flag it. 293 if (mSuggestedWords != null) { 294 if (isInSuggestedWords(word, mSuggestedWords)) { 295 mCorrectionType = CORRECTIONTYPE_TYPO; 296 } else { 297 mCorrectionType = CORRECTIONTYPE_DIFFERENT_WORD; 298 } 299 } else { 300 // No suggested words, so it's not clear whether it's a typo or different word. 301 // Mark it as a generic correction. 302 mCorrectionType = CORRECTIONTYPE_CORRECTION; 303 } 304 } 305 mWord = word; 306 } 307 308 public String getWord() { 309 return mWord; 310 } 311 312 public boolean hasWord() { 313 return mWord != null; 314 } 315 316 public void setMayContainDigit() { 317 mMayContainDigit = true; 318 } 319 320 public boolean mayContainDigit() { 321 return mMayContainDigit; 322 } 323 324 public void setContainsCorrection() { 325 mContainsCorrection = true; 326 } 327 328 public boolean containsCorrection() { 329 return mContainsCorrection; 330 } 331 332 public void setCorrectionType(final int correctionType) { 333 mCorrectionType = correctionType; 334 } 335 336 public int getCorrectionType() { 337 return mCorrectionType; 338 } 339 340 public boolean isEmpty() { 341 return mLogStatementList.isEmpty(); 342 } 343 344 /** 345 * Split this logUnit, with all events before maxTime staying in the current logUnit, and all 346 * events after maxTime going into a new LogUnit that is returned. 347 */ 348 public LogUnit splitByTime(final long maxTime) { 349 // Assume that mTimeList is in sorted order. 350 final int length = mTimeList.size(); 351 // TODO: find time by binary search, e.g. using Collections#binarySearch() 352 for (int index = 0; index < length; index++) { 353 if (mTimeList.get(index) > maxTime) { 354 final List<LogStatement> laterLogStatements = 355 mLogStatementList.subList(index, length); 356 final List<Object[]> laterValues = mValuesList.subList(index, length); 357 final List<Long> laterTimes = mTimeList.subList(index, length); 358 359 // Create the LogUnit containing the later logStatements and associated data. 360 final LogUnit newLogUnit = new LogUnit( 361 new ArrayList<LogStatement>(laterLogStatements), 362 new ArrayList<Object[]>(laterValues), 363 new ArrayList<Long>(laterTimes), 364 true /* isPartOfMegaword */); 365 newLogUnit.mWord = null; 366 newLogUnit.mMayContainDigit = mMayContainDigit; 367 newLogUnit.mContainsCorrection = mContainsCorrection; 368 369 // Purge the logStatements and associated data from this LogUnit. 370 laterLogStatements.clear(); 371 laterValues.clear(); 372 laterTimes.clear(); 373 mIsPartOfMegaword = true; 374 375 return newLogUnit; 376 } 377 } 378 return new LogUnit(); 379 } 380 381 public void append(final LogUnit logUnit) { 382 mLogStatementList.addAll(logUnit.mLogStatementList); 383 mValuesList.addAll(logUnit.mValuesList); 384 mTimeList.addAll(logUnit.mTimeList); 385 mWord = null; 386 if (logUnit.mWord != null) { 387 setWord(logUnit.mWord); 388 } 389 mMayContainDigit = mMayContainDigit || logUnit.mMayContainDigit; 390 mContainsCorrection = mContainsCorrection || logUnit.mContainsCorrection; 391 mIsPartOfMegaword = false; 392 } 393 394 public SuggestedWords getSuggestions() { 395 return mSuggestedWords; 396 } 397 398 /** 399 * Initialize the suggestions. 400 * 401 * Once set to a non-null value, the suggestions may not be changed again. This is to keep 402 * track of the list of words that are close to the user's initial effort to type the word. 403 * Only words that are close to the initial effort are considered typo corrections. 404 */ 405 public void initializeSuggestions(final SuggestedWords suggestedWords) { 406 if (mSuggestedWords == null) { 407 mSuggestedWords = suggestedWords; 408 } 409 } 410 411 private static boolean isInSuggestedWords(final String queryWord, 412 final SuggestedWords suggestedWords) { 413 if (TextUtils.isEmpty(queryWord)) { 414 return false; 415 } 416 final int size = suggestedWords.size(); 417 for (int i = 0; i < size; i++) { 418 final SuggestedWordInfo wordInfo = suggestedWords.getInfo(i); 419 if (queryWord.equals(wordInfo.mWord)) { 420 return true; 421 } 422 } 423 return false; 424 } 425} 426