ResearchLogger.java revision 9c3860ce461c3791891bf667edc77fe798c8d332
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 static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET; 20 21import android.accounts.Account; 22import android.accounts.AccountManager; 23import android.app.AlarmManager; 24import android.app.AlertDialog; 25import android.app.Dialog; 26import android.app.PendingIntent; 27import android.content.Context; 28import android.content.DialogInterface; 29import android.content.DialogInterface.OnCancelListener; 30import android.content.Intent; 31import android.content.SharedPreferences; 32import android.content.SharedPreferences.Editor; 33import android.content.pm.PackageInfo; 34import android.content.pm.PackageManager.NameNotFoundException; 35import android.content.res.Resources; 36import android.graphics.Canvas; 37import android.graphics.Color; 38import android.graphics.Paint; 39import android.graphics.Paint.Style; 40import android.net.Uri; 41import android.os.Build; 42import android.os.Bundle; 43import android.os.Handler; 44import android.os.IBinder; 45import android.os.SystemClock; 46import android.preference.PreferenceManager; 47import android.text.TextUtils; 48import android.text.format.DateUtils; 49import android.util.Log; 50import android.view.KeyEvent; 51import android.view.MotionEvent; 52import android.view.Window; 53import android.view.WindowManager; 54import android.view.inputmethod.CompletionInfo; 55import android.view.inputmethod.EditorInfo; 56import android.view.inputmethod.InputConnection; 57import android.widget.Toast; 58 59import com.android.inputmethod.keyboard.Key; 60import com.android.inputmethod.keyboard.Keyboard; 61import com.android.inputmethod.keyboard.KeyboardId; 62import com.android.inputmethod.keyboard.KeyboardSwitcher; 63import com.android.inputmethod.keyboard.KeyboardView; 64import com.android.inputmethod.keyboard.MainKeyboardView; 65import com.android.inputmethod.latin.Constants; 66import com.android.inputmethod.latin.Dictionary; 67import com.android.inputmethod.latin.InputTypeUtils; 68import com.android.inputmethod.latin.LatinIME; 69import com.android.inputmethod.latin.R; 70import com.android.inputmethod.latin.RichInputConnection; 71import com.android.inputmethod.latin.RichInputConnection.Range; 72import com.android.inputmethod.latin.Suggest; 73import com.android.inputmethod.latin.SuggestedWords; 74import com.android.inputmethod.latin.define.ProductionFlag; 75import com.android.inputmethod.research.MotionEventReader.ReplayData; 76 77import java.io.BufferedReader; 78import java.io.File; 79import java.io.FileInputStream; 80import java.io.FileNotFoundException; 81import java.io.IOException; 82import java.io.InputStreamReader; 83import java.nio.MappedByteBuffer; 84import java.nio.channels.FileChannel; 85import java.nio.charset.Charset; 86import java.text.SimpleDateFormat; 87import java.util.ArrayList; 88import java.util.Date; 89import java.util.List; 90import java.util.Locale; 91import java.util.Random; 92import java.util.UUID; 93 94/** 95 * Logs the use of the LatinIME keyboard. 96 * 97 * This class logs operations on the IME keyboard, including what the user has typed. 98 * Data is stored locally in a file in app-specific storage. 99 * 100 * This functionality is off by default. See 101 * {@link ProductionFlag#USES_DEVELOPMENT_ONLY_DIAGNOSTICS}. 102 */ 103public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener { 104 // TODO: This class has grown quite large and combines several concerns that should be 105 // separated. The following refactorings will be applied as soon as possible after adding 106 // support for replaying historical events, fixing some replay bugs, adding some ui constraints 107 // on the feedback dialog, and adding the survey dialog. 108 // TODO: Refactor. Move splash screen code into separate class. 109 // TODO: Refactor. Move feedback screen code into separate class. 110 // TODO: Refactor. Move logging invocations into their own class. 111 // TODO: Refactor. Move currentLogUnit management into separate class. 112 private static final String TAG = ResearchLogger.class.getSimpleName(); 113 private static final boolean DEBUG = false 114 && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; 115 private static final boolean DEBUG_REPLAY_AFTER_FEEDBACK = false 116 && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; 117 // Whether the TextView contents are logged at the end of the session. true will disclose 118 // private info. 119 private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false 120 && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; 121 // Whether the feedback dialog preserves the editable text across invocations. Should be false 122 // for normal research builds so users do not have to delete the same feedback string they 123 // entered earlier. Should be true for builds internal to a development team so when the text 124 // field holds a channel name, the developer does not have to re-enter it when using the 125 // feedback mechanism to generate multiple tests. 126 private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false; 127 public static final boolean DEFAULT_USABILITY_STUDY_MODE = false; 128 /* package */ static boolean sIsLogging = false; 129 private static final int OUTPUT_FORMAT_VERSION = 5; 130 private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; 131 private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash"; 132 /* package */ static final String LOG_FILENAME_PREFIX = "researchLog"; 133 private static final String LOG_FILENAME_SUFFIX = ".txt"; 134 /* package */ static final String USER_RECORDING_FILENAME_PREFIX = "recording"; 135 private static final String USER_RECORDING_FILENAME_SUFFIX = ".txt"; 136 private static final SimpleDateFormat TIMESTAMP_DATEFORMAT = 137 new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); 138 // Whether all words should be recorded, leaving unsampled word between bigrams. Useful for 139 // testing. 140 /* package for test */ static final boolean IS_LOGGING_EVERYTHING = false 141 && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; 142 // The number of words between n-grams to omit from the log. 143 private static final int NUMBER_OF_WORDS_BETWEEN_SAMPLES = 144 IS_LOGGING_EVERYTHING ? 0 : (DEBUG ? 2 : 18); 145 146 // Whether to show an indicator on the screen that logging is on. Currently a very small red 147 // dot in the lower right hand corner. Most users should not notice it. 148 private static final boolean IS_SHOWING_INDICATOR = true; 149 // Change the default indicator to something very visible. Currently two red vertical bars on 150 // either side of they keyboard. 151 private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false || 152 (IS_LOGGING_EVERYTHING && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG); 153 // FEEDBACK_WORD_BUFFER_SIZE should add 1 because it must also hold the feedback LogUnit itself. 154 public static final int FEEDBACK_WORD_BUFFER_SIZE = (Integer.MAX_VALUE - 1) + 1; 155 156 // constants related to specific log points 157 private static final String WHITESPACE_SEPARATORS = " \t\n\r"; 158 private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 159 private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid"; 160 private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel"; 161 162 private static final ResearchLogger sInstance = new ResearchLogger(); 163 private static String sAccountType = null; 164 private static String sAllowedAccountDomain = null; 165 // to write to a different filename, e.g., for testing, set mFile before calling start() 166 /* package */ File mFilesDir; 167 /* package */ String mUUIDString; 168 /* package */ ResearchLog mMainResearchLog; 169 // mFeedbackLog records all events for the session, private or not (excepting 170 // passwords). It is written to permanent storage only if the user explicitly commands 171 // the system to do so. 172 // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are 173 // complete. 174 /* package */ MainLogBuffer mMainLogBuffer; 175 // TODO: Remove the feedback log. The feedback log continuously captured user data in case the 176 // user wanted to submit it. We now use the mUserRecordingLogBuffer to allow the user to 177 // explicitly reproduce a problem. 178 /* package */ ResearchLog mFeedbackLog; 179 /* package */ LogBuffer mFeedbackLogBuffer; 180 /* package */ ResearchLog mUserRecordingLog; 181 /* package */ LogBuffer mUserRecordingLogBuffer; 182 private File mUserRecordingFile = null; 183 184 private boolean mIsPasswordView = false; 185 private boolean mIsLoggingSuspended = false; 186 private SharedPreferences mPrefs; 187 188 // digits entered by the user are replaced with this codepoint. 189 /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT = 190 Character.codePointAt("\uE000", 0); // U+E000 is in the "private-use area" 191 // U+E001 is in the "private-use area" 192 /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001"; 193 private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time"; 194 private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS; 195 private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS; 196 protected static final int SUSPEND_DURATION_IN_MINUTES = 1; 197 // set when LatinIME should ignore an onUpdateSelection() callback that 198 // arises from operations in this class 199 private static boolean sLatinIMEExpectingUpdateSelection = false; 200 201 // used to check whether words are not unique 202 private Suggest mSuggest; 203 private MainKeyboardView mMainKeyboardView; 204 // TODO: Check whether a superclass can be used instead of LatinIME. 205 /* package for test */ LatinIME mLatinIME; 206 private final Statistics mStatistics; 207 private final MotionEventReader mMotionEventReader = new MotionEventReader(); 208 private final Replayer mReplayer = Replayer.getInstance(); 209 210 private Intent mUploadIntent; 211 private Intent mUploadNowIntent; 212 213 private LogUnit mCurrentLogUnit = new LogUnit(); 214 215 // Gestured or tapped words may be committed after the gesture of the next word has started. 216 // To ensure that the gesture data of the next word is not associated with the previous word, 217 // thereby leaking private data, we store the time of the down event that started the second 218 // gesture, and when committing the earlier word, split the LogUnit. 219 private long mSavedDownEventTime; 220 private Bundle mFeedbackDialogBundle = null; 221 private boolean mInFeedbackDialog = false; 222 // The feedback dialog causes stop() to be called for the keyboard connected to the original 223 // window. This is because the feedback dialog must present its own EditText box that displays 224 // a keyboard. stop() normally causes mFeedbackLogBuffer, which contains the user's data, to be 225 // cleared, and causes mFeedbackLog, which is ready to collect information in case the user 226 // wants to upload, to be closed. This is good because we don't need to log information about 227 // what the user is typing in the feedback dialog, but bad because this data must be uploaded. 228 // Here we save the LogBuffer and Log so the feedback dialog can later access their data. 229 private LogBuffer mSavedFeedbackLogBuffer; 230 private ResearchLog mSavedFeedbackLog; 231 private Handler mUserRecordingTimeoutHandler; 232 private static final long USER_RECORDING_TIMEOUT_MS = 30L * DateUtils.SECOND_IN_MILLIS; 233 234 private ResearchLogger() { 235 mStatistics = Statistics.getInstance(); 236 } 237 238 public static ResearchLogger getInstance() { 239 return sInstance; 240 } 241 242 public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher) { 243 assert latinIME != null; 244 if (latinIME == null) { 245 Log.w(TAG, "IMS is null; logging is off"); 246 } else { 247 mFilesDir = latinIME.getFilesDir(); 248 if (mFilesDir == null || !mFilesDir.exists()) { 249 Log.w(TAG, "IME storage directory does not exist."); 250 } 251 } 252 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(latinIME); 253 if (prefs != null) { 254 mUUIDString = getUUID(prefs); 255 if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) { 256 Editor e = prefs.edit(); 257 e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE); 258 e.apply(); 259 } 260 sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); 261 prefs.registerOnSharedPreferenceChangeListener(this); 262 263 final long lastCleanupTime = prefs.getLong(PREF_LAST_CLEANUP_TIME, 0L); 264 final long now = System.currentTimeMillis(); 265 if (lastCleanupTime + DURATION_BETWEEN_DIR_CLEANUP_IN_MS < now) { 266 final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS; 267 cleanupLoggingDir(mFilesDir, timeHorizon); 268 Editor e = prefs.edit(); 269 e.putLong(PREF_LAST_CLEANUP_TIME, now); 270 e.apply(); 271 } 272 } 273 final Resources res = latinIME.getResources(); 274 sAccountType = res.getString(R.string.research_account_type); 275 sAllowedAccountDomain = res.getString(R.string.research_allowed_account_domain); 276 mLatinIME = latinIME; 277 mPrefs = prefs; 278 mUploadIntent = new Intent(mLatinIME, UploaderService.class); 279 mUploadNowIntent = new Intent(mLatinIME, UploaderService.class); 280 mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true); 281 mReplayer.setKeyboardSwitcher(keyboardSwitcher); 282 283 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 284 scheduleUploadingService(mLatinIME); 285 } 286 } 287 288 /** 289 * Arrange for the UploaderService to be run on a regular basis. 290 * 291 * Any existing scheduled invocation of UploaderService is removed and rescheduled. This may 292 * cause problems if this method is called often and frequent updates are required, but since 293 * the user will likely be sleeping at some point, if the interval is less that the expected 294 * sleep duration and this method is not called during that time, the service should be invoked 295 * at some point. 296 */ 297 public static void scheduleUploadingService(Context context) { 298 final Intent intent = new Intent(context, UploaderService.class); 299 final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0); 300 final AlarmManager manager = 301 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 302 manager.cancel(pendingIntent); 303 manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 304 UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent); 305 } 306 307 private void cleanupLoggingDir(final File dir, final long time) { 308 for (File file : dir.listFiles()) { 309 final String filename = file.getName(); 310 if ((filename.startsWith(ResearchLogger.LOG_FILENAME_PREFIX) 311 || filename.startsWith(ResearchLogger.USER_RECORDING_FILENAME_PREFIX)) 312 && file.lastModified() < time) { 313 file.delete(); 314 } 315 } 316 } 317 318 public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) { 319 mMainKeyboardView = mainKeyboardView; 320 maybeShowSplashScreen(); 321 } 322 323 public void mainKeyboardView_onDetachedFromWindow() { 324 mMainKeyboardView = null; 325 } 326 327 private boolean hasSeenSplash() { 328 return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false); 329 } 330 331 private Dialog mSplashDialog = null; 332 333 private void maybeShowSplashScreen() { 334 if (hasSeenSplash()) { 335 return; 336 } 337 if (mSplashDialog != null && mSplashDialog.isShowing()) { 338 return; 339 } 340 final IBinder windowToken = mMainKeyboardView != null 341 ? mMainKeyboardView.getWindowToken() : null; 342 if (windowToken == null) { 343 return; 344 } 345 final AlertDialog.Builder builder = new AlertDialog.Builder(mLatinIME) 346 .setTitle(R.string.research_splash_title) 347 .setMessage(R.string.research_splash_content) 348 .setPositiveButton(android.R.string.yes, 349 new DialogInterface.OnClickListener() { 350 @Override 351 public void onClick(DialogInterface dialog, int which) { 352 onUserLoggingConsent(); 353 mSplashDialog.dismiss(); 354 } 355 }) 356 .setNegativeButton(android.R.string.no, 357 new DialogInterface.OnClickListener() { 358 @Override 359 public void onClick(DialogInterface dialog, int which) { 360 final String packageName = mLatinIME.getPackageName(); 361 final Uri packageUri = Uri.parse("package:" + packageName); 362 final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, 363 packageUri); 364 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 365 mLatinIME.startActivity(intent); 366 } 367 }) 368 .setCancelable(true) 369 .setOnCancelListener( 370 new OnCancelListener() { 371 @Override 372 public void onCancel(DialogInterface dialog) { 373 mLatinIME.requestHideSelf(0); 374 } 375 }); 376 mSplashDialog = builder.create(); 377 final Window w = mSplashDialog.getWindow(); 378 final WindowManager.LayoutParams lp = w.getAttributes(); 379 lp.token = windowToken; 380 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 381 w.setAttributes(lp); 382 w.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 383 mSplashDialog.show(); 384 } 385 386 public void onUserLoggingConsent() { 387 setLoggingAllowed(true); 388 if (mPrefs == null) { 389 return; 390 } 391 final Editor e = mPrefs.edit(); 392 e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true); 393 e.apply(); 394 restart(); 395 } 396 397 private void setLoggingAllowed(boolean enableLogging) { 398 if (mPrefs == null) { 399 return; 400 } 401 Editor e = mPrefs.edit(); 402 e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); 403 e.apply(); 404 sIsLogging = enableLogging; 405 } 406 407 private static int sLogFileCounter = 0; 408 409 private File createLogFile(final File filesDir) { 410 final StringBuilder sb = new StringBuilder(); 411 sb.append(LOG_FILENAME_PREFIX).append('-'); 412 sb.append(mUUIDString).append('-'); 413 sb.append(TIMESTAMP_DATEFORMAT.format(new Date())).append('-'); 414 // Sometimes logFiles are created within milliseconds of each other. Append a counter to 415 // separate these. 416 if (sLogFileCounter < Integer.MAX_VALUE) { 417 sLogFileCounter++; 418 } else { 419 // Wrap the counter, in the unlikely event of overflow. 420 sLogFileCounter = 0; 421 } 422 sb.append(sLogFileCounter); 423 sb.append(LOG_FILENAME_SUFFIX); 424 return new File(filesDir, sb.toString()); 425 } 426 427 private File createUserRecordingFile(final File filesDir) { 428 final StringBuilder sb = new StringBuilder(); 429 sb.append(USER_RECORDING_FILENAME_PREFIX).append('-'); 430 sb.append(mUUIDString).append('-'); 431 sb.append(TIMESTAMP_DATEFORMAT.format(new Date())); 432 sb.append(USER_RECORDING_FILENAME_SUFFIX); 433 return new File(filesDir, sb.toString()); 434 } 435 436 private void checkForEmptyEditor() { 437 if (mLatinIME == null) { 438 return; 439 } 440 final InputConnection ic = mLatinIME.getCurrentInputConnection(); 441 if (ic == null) { 442 return; 443 } 444 final CharSequence textBefore = ic.getTextBeforeCursor(1, 0); 445 if (!TextUtils.isEmpty(textBefore)) { 446 mStatistics.setIsEmptyUponStarting(false); 447 return; 448 } 449 final CharSequence textAfter = ic.getTextAfterCursor(1, 0); 450 if (!TextUtils.isEmpty(textAfter)) { 451 mStatistics.setIsEmptyUponStarting(false); 452 return; 453 } 454 if (textBefore != null && textAfter != null) { 455 mStatistics.setIsEmptyUponStarting(true); 456 } 457 } 458 459 private void start() { 460 if (DEBUG) { 461 Log.d(TAG, "start called"); 462 } 463 maybeShowSplashScreen(); 464 updateSuspendedState(); 465 requestIndicatorRedraw(); 466 mStatistics.reset(); 467 checkForEmptyEditor(); 468 if (!isAllowedToLog()) { 469 // Log.w(TAG, "not in usability mode; not logging"); 470 return; 471 } 472 if (mFilesDir == null || !mFilesDir.exists()) { 473 Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); 474 return; 475 } 476 if (mMainLogBuffer == null) { 477 mMainResearchLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME); 478 final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1); 479 mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore) { 480 @Override 481 protected void publish(final ArrayList<LogUnit> logUnits, 482 boolean canIncludePrivateData) { 483 canIncludePrivateData |= IS_LOGGING_EVERYTHING; 484 final int length = logUnits.size(); 485 for (int i = 0; i < length; i++) { 486 final LogUnit logUnit = logUnits.get(i); 487 final String word = logUnit.getWord(); 488 if (word != null && word.length() > 0 && hasLetters(word)) { 489 Log.d(TAG, "onPublish: " + word + ", hc: " 490 + logUnit.containsCorrection()); 491 final Dictionary dictionary = getDictionary(); 492 mStatistics.recordWordEntered( 493 dictionary != null && dictionary.isValidWord(word), 494 logUnit.containsCorrection()); 495 } 496 } 497 if (mMainResearchLog != null) { 498 publishLogUnits(logUnits, mMainResearchLog, canIncludePrivateData); 499 } 500 } 501 }; 502 mMainLogBuffer.setSuggest(mSuggest); 503 } 504 if (mFeedbackLogBuffer == null) { 505 resetFeedbackLogging(); 506 } 507 } 508 509 private void resetFeedbackLogging() { 510 mFeedbackLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME); 511 mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE); 512 } 513 514 /* package */ void stop() { 515 if (DEBUG) { 516 Log.d(TAG, "stop called"); 517 } 518 // Commit mCurrentLogUnit before closing. 519 commitCurrentLogUnit(); 520 521 if (mMainLogBuffer != null) { 522 mMainLogBuffer.shiftAndPublishAll(); 523 logStatistics(); 524 commitCurrentLogUnit(); 525 mMainLogBuffer.setIsStopping(); 526 mMainLogBuffer.shiftAndPublishAll(); 527 mMainResearchLog.close(null /* callback */); 528 mMainLogBuffer = null; 529 } 530 if (mFeedbackLogBuffer != null) { 531 mFeedbackLog.close(null /* callback */); 532 mFeedbackLogBuffer = null; 533 } 534 } 535 536 public boolean abort() { 537 if (DEBUG) { 538 Log.d(TAG, "abort called"); 539 } 540 boolean didAbortMainLog = false; 541 if (mMainLogBuffer != null) { 542 mMainLogBuffer.clear(); 543 try { 544 didAbortMainLog = mMainResearchLog.blockingAbort(); 545 } catch (InterruptedException e) { 546 // Don't know whether this succeeded or not. We assume not; this is reported 547 // to the caller. 548 } 549 mMainLogBuffer = null; 550 } 551 boolean didAbortFeedbackLog = false; 552 if (mFeedbackLogBuffer != null) { 553 mFeedbackLogBuffer.clear(); 554 try { 555 didAbortFeedbackLog = mFeedbackLog.blockingAbort(); 556 } catch (InterruptedException e) { 557 // Don't know whether this succeeded or not. We assume not; this is reported 558 // to the caller. 559 } 560 mFeedbackLogBuffer = null; 561 } 562 return didAbortMainLog && didAbortFeedbackLog; 563 } 564 565 private void restart() { 566 stop(); 567 start(); 568 } 569 570 private long mResumeTime = 0L; 571 private void updateSuspendedState() { 572 final long time = System.currentTimeMillis(); 573 if (time > mResumeTime) { 574 mIsLoggingSuspended = false; 575 } 576 } 577 578 @Override 579 public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 580 if (key == null || prefs == null) { 581 return; 582 } 583 sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); 584 if (sIsLogging == false) { 585 abort(); 586 } 587 requestIndicatorRedraw(); 588 mPrefs = prefs; 589 prefsChanged(prefs); 590 } 591 592 public void onResearchKeySelected(final LatinIME latinIME) { 593 if (mInFeedbackDialog) { 594 Toast.makeText(latinIME, R.string.research_please_exit_feedback_form, 595 Toast.LENGTH_LONG).show(); 596 return; 597 } 598 presentFeedbackDialog(latinIME); 599 } 600 601 public void presentFeedbackDialog(LatinIME latinIME) { 602 if (isMakingUserRecording()) { 603 saveRecording(); 604 } 605 mInFeedbackDialog = true; 606 mSavedFeedbackLogBuffer = mFeedbackLogBuffer; 607 mSavedFeedbackLog = mFeedbackLog; 608 // Set the non-saved versions to null so that the stop() caused by switching to the 609 // Feedback dialog will not close them. 610 mFeedbackLogBuffer = null; 611 mFeedbackLog = null; 612 613 final Intent intent = new Intent(); 614 intent.setClass(mLatinIME, FeedbackActivity.class); 615 if (mFeedbackDialogBundle == null) { 616 // Restore feedback field with channel name 617 final Bundle bundle = new Bundle(); 618 bundle.putBoolean(FeedbackFragment.KEY_INCLUDE_ACCOUNT_NAME, true); 619 bundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, false); 620 if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) { 621 final String savedChannelName = mPrefs.getString(PREF_RESEARCH_SAVED_CHANNEL, ""); 622 bundle.putString(FeedbackFragment.KEY_FEEDBACK_STRING, savedChannelName); 623 } 624 mFeedbackDialogBundle = bundle; 625 } 626 intent.putExtras(mFeedbackDialogBundle); 627 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 628 latinIME.startActivity(intent); 629 } 630 631 public void setFeedbackDialogBundle(final Bundle bundle) { 632 mFeedbackDialogBundle = bundle; 633 } 634 635 public void startRecording() { 636 final Resources res = mLatinIME.getResources(); 637 Toast.makeText(mLatinIME, 638 res.getString(R.string.research_feedback_demonstration_instructions), 639 Toast.LENGTH_LONG).show(); 640 startRecordingInternal(); 641 } 642 643 private void startRecordingInternal() { 644 if (mUserRecordingLog != null) { 645 mUserRecordingLog.abort(); 646 } 647 mUserRecordingFile = createUserRecordingFile(mFilesDir); 648 mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME); 649 mUserRecordingLogBuffer = new LogBuffer(); 650 resetRecordingTimer(); 651 } 652 653 private boolean isMakingUserRecording() { 654 return mUserRecordingLog != null; 655 } 656 657 private void resetRecordingTimer() { 658 if (mUserRecordingTimeoutHandler == null) { 659 mUserRecordingTimeoutHandler = new Handler(); 660 } 661 clearRecordingTimer(); 662 mUserRecordingTimeoutHandler.postDelayed(mRecordingHandlerTimeoutRunnable, 663 USER_RECORDING_TIMEOUT_MS); 664 } 665 666 private void clearRecordingTimer() { 667 mUserRecordingTimeoutHandler.removeCallbacks(mRecordingHandlerTimeoutRunnable); 668 } 669 670 private Runnable mRecordingHandlerTimeoutRunnable = new Runnable() { 671 @Override 672 public void run() { 673 cancelRecording(); 674 requestIndicatorRedraw(); 675 final Resources res = mLatinIME.getResources(); 676 Toast.makeText(mLatinIME, res.getString(R.string.research_feedback_recording_failure), 677 Toast.LENGTH_LONG).show(); 678 } 679 }; 680 681 private void cancelRecording() { 682 if (mUserRecordingLog != null) { 683 mUserRecordingLog.abort(); 684 } 685 mUserRecordingLog = null; 686 mUserRecordingLogBuffer = null; 687 if (mFeedbackDialogBundle != null) { 688 mFeedbackDialogBundle.putBoolean("HasRecording", false); 689 } 690 } 691 692 private void saveRecording() { 693 commitCurrentLogUnit(); 694 publishLogBuffer(mUserRecordingLogBuffer, mUserRecordingLog, true); 695 mUserRecordingLog.close(null); 696 mUserRecordingLog = null; 697 mUserRecordingLogBuffer = null; 698 699 if (mFeedbackDialogBundle != null) { 700 mFeedbackDialogBundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, true); 701 } 702 clearRecordingTimer(); 703 } 704 705 // TODO: currently unreachable. Remove after being sure enable/disable is 706 // not needed. 707 /* 708 public void enableOrDisable(final boolean showEnable, final LatinIME latinIME) { 709 if (showEnable) { 710 if (!sIsLogging) { 711 setLoggingAllowed(true); 712 } 713 resumeLogging(); 714 Toast.makeText(latinIME, 715 R.string.research_notify_session_logging_enabled, 716 Toast.LENGTH_LONG).show(); 717 } else { 718 Toast toast = Toast.makeText(latinIME, 719 R.string.research_notify_session_log_deleting, 720 Toast.LENGTH_LONG); 721 toast.show(); 722 boolean isLogDeleted = abort(); 723 final long currentTime = System.currentTimeMillis(); 724 final long resumeTime = currentTime + 1000 * 60 * 725 SUSPEND_DURATION_IN_MINUTES; 726 suspendLoggingUntil(resumeTime); 727 toast.cancel(); 728 Toast.makeText(latinIME, R.string.research_notify_logging_suspended, 729 Toast.LENGTH_LONG).show(); 730 } 731 } 732 */ 733 734 /** 735 * Get the name of the first allowed account on the device. 736 * 737 * Allowed accounts must be in the domain given by ALLOWED_ACCOUNT_DOMAIN. 738 * 739 * @return The user's account name. 740 */ 741 public String getAccountName() { 742 if (sAccountType == null || sAccountType.isEmpty()) { 743 return null; 744 } 745 if (sAllowedAccountDomain == null || sAllowedAccountDomain.isEmpty()) { 746 return null; 747 } 748 final AccountManager manager = AccountManager.get(mLatinIME); 749 // Filter first by account type. 750 final Account[] accounts = manager.getAccountsByType(sAccountType); 751 752 for (final Account account : accounts) { 753 if (DEBUG) { 754 Log.d(TAG, account.name); 755 } 756 final String[] parts = account.name.split("@"); 757 if (parts.length > 1 && parts[1].equals(sAllowedAccountDomain)) { 758 return parts[0]; 759 } 760 } 761 return null; 762 } 763 764 private static final LogStatement LOGSTATEMENT_FEEDBACK = 765 new LogStatement("UserFeedback", false, false, "contents", "accountName", "recording"); 766 public void sendFeedback(final String feedbackContents, final boolean includeHistory, 767 final boolean isIncludingAccountName, final boolean isIncludingRecording) { 768 if (mSavedFeedbackLogBuffer == null) { 769 return; 770 } 771 if (!includeHistory) { 772 mSavedFeedbackLogBuffer.clear(); 773 } 774 String recording = ""; 775 if (isIncludingRecording) { 776 // Try to read recording from recently written json file 777 if (mUserRecordingFile != null) { 778 FileChannel channel = null; 779 try { 780 channel = new FileInputStream(mUserRecordingFile).getChannel(); 781 final MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, 782 channel.size()); 783 // Android's openFileOutput() creates the file, so we use Android's default 784 // Charset (UTF-8) here to read it. 785 recording = Charset.defaultCharset().decode(buffer).toString(); 786 } catch (FileNotFoundException e) { 787 Log.e(TAG, "Could not find recording file", e); 788 } catch (IOException e) { 789 Log.e(TAG, "Error reading recording file", e); 790 } finally { 791 if (channel != null) { 792 try { 793 channel.close(); 794 } catch (IOException e) { 795 Log.e(TAG, "Error closing recording file", e); 796 } 797 } 798 } 799 } 800 } 801 final LogUnit feedbackLogUnit = new LogUnit(); 802 final String accountName = isIncludingAccountName ? getAccountName() : ""; 803 feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(), 804 feedbackContents, accountName, recording); 805 mFeedbackLogBuffer.shiftIn(feedbackLogUnit); 806 publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */); 807 mSavedFeedbackLog.close(new Runnable() { 808 @Override 809 public void run() { 810 uploadNow(); 811 } 812 }); 813 814 if (isIncludingRecording && DEBUG_REPLAY_AFTER_FEEDBACK) { 815 final Handler handler = new Handler(); 816 handler.postDelayed(new Runnable() { 817 @Override 818 public void run() { 819 final ReplayData replayData = 820 mMotionEventReader.readMotionEventData(mUserRecordingFile); 821 mReplayer.replay(replayData, null); 822 } 823 }, 1000); 824 } 825 826 if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) { 827 // Use feedback string as a channel name to label feedback strings. Here we record the 828 // string for prepopulating the field next time. 829 final String channelName = feedbackContents; 830 if (mPrefs == null) { 831 return; 832 } 833 final Editor e = mPrefs.edit(); 834 e.putString(PREF_RESEARCH_SAVED_CHANNEL, channelName); 835 e.apply(); 836 } 837 } 838 839 public void uploadNow() { 840 if (DEBUG) { 841 Log.d(TAG, "calling uploadNow()"); 842 } 843 mLatinIME.startService(mUploadNowIntent); 844 } 845 846 public void onLeavingSendFeedbackDialog() { 847 mInFeedbackDialog = false; 848 } 849 850 public void initSuggest(Suggest suggest) { 851 mSuggest = suggest; 852 if (mMainLogBuffer != null) { 853 mMainLogBuffer.setSuggest(mSuggest); 854 } 855 } 856 857 private Dictionary getDictionary() { 858 if (mSuggest == null) { 859 return null; 860 } 861 return mSuggest.getMainDictionary(); 862 } 863 864 private void setIsPasswordView(boolean isPasswordView) { 865 mIsPasswordView = isPasswordView; 866 } 867 868 private boolean isAllowedToLog() { 869 return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog 870 && !isReplaying(); 871 } 872 873 public void requestIndicatorRedraw() { 874 if (!IS_SHOWING_INDICATOR) { 875 return; 876 } 877 if (mMainKeyboardView == null) { 878 return; 879 } 880 mMainKeyboardView.invalidateAllKeys(); 881 } 882 883 private boolean isReplaying() { 884 return mReplayer.isReplaying(); 885 } 886 887 private int getIndicatorColor() { 888 if (isMakingUserRecording()) { 889 return Color.YELLOW; 890 } 891 if (isReplaying()) { 892 return Color.GREEN; 893 } 894 return Color.RED; 895 } 896 897 public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width, 898 int height) { 899 // TODO: Reimplement using a keyboard background image specific to the ResearchLogger 900 // and remove this method. 901 // The check for MainKeyboardView ensures that the indicator only decorates the main 902 // keyboard, not every keyboard. 903 if (IS_SHOWING_INDICATOR && (isAllowedToLog() || isReplaying()) 904 && view instanceof MainKeyboardView) { 905 final int savedColor = paint.getColor(); 906 paint.setColor(getIndicatorColor()); 907 final Style savedStyle = paint.getStyle(); 908 paint.setStyle(Style.STROKE); 909 final float savedStrokeWidth = paint.getStrokeWidth(); 910 if (IS_SHOWING_INDICATOR_CLEARLY) { 911 paint.setStrokeWidth(5); 912 canvas.drawLine(0, 0, 0, height, paint); 913 canvas.drawLine(width, 0, width, height, paint); 914 } else { 915 // Put a tiny dot on the screen so a knowledgeable user can check whether it is 916 // enabled. The dot is actually a zero-width, zero-height rectangle, placed at the 917 // lower-right corner of the canvas, painted with a non-zero border width. 918 paint.setStrokeWidth(3); 919 canvas.drawRect(width, height, width, height, paint); 920 } 921 paint.setColor(savedColor); 922 paint.setStyle(savedStyle); 923 paint.setStrokeWidth(savedStrokeWidth); 924 } 925 } 926 927 /** 928 * Buffer a research log event, flagging it as privacy-sensitive. 929 */ 930 private synchronized void enqueueEvent(final LogStatement logStatement, 931 final Object... values) { 932 enqueueEvent(mCurrentLogUnit, logStatement, values); 933 } 934 935 private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement, 936 final Object... values) { 937 assert values.length == logStatement.getKeys().length; 938 if (isAllowedToLog() && logUnit != null) { 939 final long time = SystemClock.uptimeMillis(); 940 logUnit.addLogStatement(logStatement, time, values); 941 } 942 } 943 944 private void setCurrentLogUnitContainsDigitFlag() { 945 mCurrentLogUnit.setMayContainDigit(); 946 } 947 948 private void setCurrentLogUnitContainsCorrection() { 949 mCurrentLogUnit.setContainsCorrection(); 950 } 951 952 private void setCurrentLogUnitCorrectionType(final int correctionType) { 953 mCurrentLogUnit.setCorrectionType(correctionType); 954 } 955 956 /* package for test */ void commitCurrentLogUnit() { 957 if (DEBUG) { 958 Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasWord() ? 959 ": " + mCurrentLogUnit.getWord() : "")); 960 } 961 if (!mCurrentLogUnit.isEmpty()) { 962 if (mMainLogBuffer != null) { 963 mMainLogBuffer.shiftIn(mCurrentLogUnit); 964 } 965 if (mFeedbackLogBuffer != null) { 966 mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); 967 } 968 if (mUserRecordingLogBuffer != null) { 969 mUserRecordingLogBuffer.shiftIn(mCurrentLogUnit); 970 } 971 mCurrentLogUnit = new LogUnit(); 972 } else { 973 if (DEBUG) { 974 Log.d(TAG, "Warning: tried to commit empty log unit."); 975 } 976 } 977 } 978 979 private static final LogStatement LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT = 980 new LogStatement("UncommitCurrentLogUnit", false, false); 981 public void uncommitCurrentLogUnit(final String expectedWord, 982 final boolean dumpCurrentLogUnit) { 983 // The user has deleted this word and returned to the previous. Check that the word in the 984 // logUnit matches the expected word. If so, restore the last log unit committed to be the 985 // current logUnit. I.e., pull out the last LogUnit from all the LogBuffers, and make 986 // restore it to mCurrentLogUnit so the new edits are captured with the word. Optionally 987 // dump the contents of mCurrentLogUnit (useful if they contain deletions of the next word 988 // that should not be reported to protect user privacy) 989 // 990 // Note that we don't use mLastLogUnit here, because it only goes one word back and is only 991 // needed for reverts, which only happen one back. 992 if (mMainLogBuffer == null) { 993 return; 994 } 995 final LogUnit oldLogUnit = mMainLogBuffer.peekLastLogUnit(); 996 997 // Check that expected word matches. 998 if (oldLogUnit != null) { 999 final String oldLogUnitWord = oldLogUnit.getWord(); 1000 if (!oldLogUnitWord.equals(expectedWord)) { 1001 return; 1002 } 1003 } 1004 1005 // Uncommit, merging if necessary. 1006 mMainLogBuffer.unshiftIn(); 1007 if (oldLogUnit != null && !dumpCurrentLogUnit) { 1008 oldLogUnit.append(mCurrentLogUnit); 1009 mSavedDownEventTime = Long.MAX_VALUE; 1010 } 1011 if (oldLogUnit == null) { 1012 mCurrentLogUnit = new LogUnit(); 1013 } else { 1014 mCurrentLogUnit = oldLogUnit; 1015 } 1016 if (mFeedbackLogBuffer != null) { 1017 mFeedbackLogBuffer.unshiftIn(); 1018 } 1019 enqueueEvent(LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT); 1020 if (DEBUG) { 1021 Log.d(TAG, "uncommitCurrentLogUnit (dump=" + dumpCurrentLogUnit + ") back to " 1022 + (mCurrentLogUnit.hasWord() ? ": '" + mCurrentLogUnit.getWord() + "'" : "")); 1023 } 1024 } 1025 1026 /** 1027 * Publish all the logUnits in the logBuffer, without doing any privacy filtering. 1028 */ 1029 /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, 1030 final ResearchLog researchLog, final boolean canIncludePrivateData) { 1031 publishLogUnits(logBuffer.getLogUnits(), researchLog, canIncludePrivateData); 1032 } 1033 1034 private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_OPENING = 1035 new LogStatement("logSegmentStart", false, false, "isIncludingPrivateData"); 1036 private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_CLOSING = 1037 new LogStatement("logSegmentEnd", false, false); 1038 /** 1039 * Publish all LogUnits in a list. 1040 * 1041 * Any privacy checks should be performed before calling this method. 1042 */ 1043 /* package for test */ void publishLogUnits(final List<LogUnit> logUnits, 1044 final ResearchLog researchLog, final boolean canIncludePrivateData) { 1045 final LogUnit openingLogUnit = new LogUnit(); 1046 if (logUnits.isEmpty()) return; 1047 // LogUnits not containing private data, such as contextual data for the log, do not require 1048 // logSegment boundary statements. 1049 if (canIncludePrivateData) { 1050 openingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_OPENING, 1051 SystemClock.uptimeMillis(), canIncludePrivateData); 1052 researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */); 1053 } 1054 for (LogUnit logUnit : logUnits) { 1055 if (DEBUG) { 1056 Log.d(TAG, "publishLogBuffer: " + (logUnit.hasWord() ? logUnit.getWord() 1057 : "<wordless>") + ", correction?: " + logUnit.containsCorrection()); 1058 } 1059 researchLog.publish(logUnit, canIncludePrivateData); 1060 } 1061 if (canIncludePrivateData) { 1062 final LogUnit closingLogUnit = new LogUnit(); 1063 closingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_CLOSING, 1064 SystemClock.uptimeMillis()); 1065 researchLog.publish(closingLogUnit, true /* isIncludingPrivateData */); 1066 } 1067 } 1068 1069 public static boolean hasLetters(final String word) { 1070 final int length = word.length(); 1071 for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { 1072 final int codePoint = word.codePointAt(i); 1073 if (Character.isLetter(codePoint)) { 1074 return true; 1075 } 1076 } 1077 return false; 1078 } 1079 1080 /** 1081 * Commit the portion of mCurrentLogUnit before maxTime as a worded logUnit. 1082 * 1083 * After this operation completes, mCurrentLogUnit will hold any logStatements that happened 1084 * after maxTime. 1085 */ 1086 /* package for test */ void commitCurrentLogUnitAsWord(final String word, final long maxTime, 1087 final boolean isBatchMode) { 1088 if (word == null) { 1089 return; 1090 } 1091 if (word.length() > 0 && hasLetters(word)) { 1092 mCurrentLogUnit.setWord(word); 1093 } 1094 final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime); 1095 enqueueCommitText(word, isBatchMode); 1096 commitCurrentLogUnit(); 1097 mCurrentLogUnit = newLogUnit; 1098 } 1099 1100 /** 1101 * Record the time of a MotionEvent.ACTION_DOWN. 1102 * 1103 * Warning: Not thread safe. Only call from the main thread. 1104 */ 1105 private void setSavedDownEventTime(final long time) { 1106 mSavedDownEventTime = time; 1107 } 1108 1109 public void onWordFinished(final String word, final boolean isBatchMode) { 1110 commitCurrentLogUnitAsWord(word, mSavedDownEventTime, isBatchMode); 1111 mSavedDownEventTime = Long.MAX_VALUE; 1112 } 1113 1114 private static int scrubDigitFromCodePoint(int codePoint) { 1115 return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint; 1116 } 1117 1118 /* package for test */ static String scrubDigitsFromString(String s) { 1119 StringBuilder sb = null; 1120 final int length = s.length(); 1121 for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) { 1122 final int codePoint = Character.codePointAt(s, i); 1123 if (Character.isDigit(codePoint)) { 1124 if (sb == null) { 1125 sb = new StringBuilder(length); 1126 sb.append(s.substring(0, i)); 1127 } 1128 sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT); 1129 } else { 1130 if (sb != null) { 1131 sb.appendCodePoint(codePoint); 1132 } 1133 } 1134 } 1135 if (sb == null) { 1136 return s; 1137 } else { 1138 return sb.toString(); 1139 } 1140 } 1141 1142 private static String getUUID(final SharedPreferences prefs) { 1143 String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null); 1144 if (null == uuidString) { 1145 UUID uuid = UUID.randomUUID(); 1146 uuidString = uuid.toString(); 1147 Editor editor = prefs.edit(); 1148 editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString); 1149 editor.apply(); 1150 } 1151 return uuidString; 1152 } 1153 1154 private String scrubWord(String word) { 1155 final Dictionary dictionary = getDictionary(); 1156 if (dictionary == null) { 1157 return WORD_REPLACEMENT_STRING; 1158 } 1159 if (dictionary.isValidWord(word)) { 1160 return word; 1161 } 1162 return WORD_REPLACEMENT_STRING; 1163 } 1164 1165 // Specific logging methods follow below. The comments for each logging method should 1166 // indicate what specific method is logged, and how to trigger it from the user interface. 1167 // 1168 // Logging methods can be generally classified into two flavors, "UserAction", which should 1169 // correspond closely to an event that is sensed by the IME, and is usually generated 1170 // directly by the user, and "SystemResponse" which corresponds to an event that the IME 1171 // generates, often after much processing of user input. SystemResponses should correspond 1172 // closely to user-visible events. 1173 // TODO: Consider exposing the UserAction classification in the log output. 1174 1175 /** 1176 * Log a call to LatinIME.onStartInputViewInternal(). 1177 * 1178 * UserAction: called each time the keyboard is opened up. 1179 */ 1180 private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL = 1181 new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid", 1182 "packageName", "inputType", "imeOptions", "fieldId", "display", "model", 1183 "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything", 1184 "isUsingDevelopmentOnlyDiagnosticsDebug"); 1185 public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, 1186 final SharedPreferences prefs) { 1187 final ResearchLogger researchLogger = getInstance(); 1188 if (editorInfo != null) { 1189 final boolean isPassword = InputTypeUtils.isPasswordInputType(editorInfo.inputType) 1190 || InputTypeUtils.isVisiblePasswordInputType(editorInfo.inputType); 1191 getInstance().setIsPasswordView(isPassword); 1192 researchLogger.start(); 1193 final Context context = researchLogger.mLatinIME; 1194 try { 1195 final PackageInfo packageInfo; 1196 packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 1197 0); 1198 final Integer versionCode = packageInfo.versionCode; 1199 final String versionName = packageInfo.versionName; 1200 researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL, 1201 researchLogger.mUUIDString, editorInfo.packageName, 1202 Integer.toHexString(editorInfo.inputType), 1203 Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, 1204 Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, 1205 OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING, 1206 ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG); 1207 } catch (NameNotFoundException e) { 1208 e.printStackTrace(); 1209 } 1210 } 1211 } 1212 1213 public void latinIME_onFinishInputViewInternal() { 1214 stop(); 1215 } 1216 1217 /** 1218 * Log a change in preferences. 1219 * 1220 * UserAction: called when the user changes the settings. 1221 */ 1222 private static final LogStatement LOGSTATEMENT_PREFS_CHANGED = 1223 new LogStatement("PrefsChanged", false, false, "prefs"); 1224 public static void prefsChanged(final SharedPreferences prefs) { 1225 final ResearchLogger researchLogger = getInstance(); 1226 researchLogger.enqueueEvent(LOGSTATEMENT_PREFS_CHANGED, prefs); 1227 } 1228 1229 /** 1230 * Log a call to MainKeyboardView.processMotionEvent(). 1231 * 1232 * UserAction: called when the user puts their finger onto the screen (ACTION_DOWN). 1233 * 1234 */ 1235 private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT = 1236 new LogStatement("MotionEvent", true, false, "action", 1237 LogStatement.KEY_IS_LOGGING_RELATED, "motionEvent"); 1238 public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action, 1239 final long eventTime, final int index, final int id, final int x, final int y) { 1240 if (me != null) { 1241 final String actionString = LoggingUtils.getMotionEventActionTypeString(action); 1242 final ResearchLogger researchLogger = getInstance(); 1243 researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT, 1244 actionString, false /* IS_LOGGING_RELATED */, MotionEvent.obtain(me)); 1245 if (action == MotionEvent.ACTION_DOWN) { 1246 // Subtract 1 from eventTime so the down event is included in the later 1247 // LogUnit, not the earlier (the test is for inequality). 1248 researchLogger.setSavedDownEventTime(eventTime - 1); 1249 } 1250 // Refresh the timer in case we are capturing user feedback. 1251 if (researchLogger.isMakingUserRecording()) { 1252 researchLogger.resetRecordingTimer(); 1253 } 1254 } 1255 } 1256 1257 /** 1258 * Log a call to LatinIME.onCodeInput(). 1259 * 1260 * SystemResponse: The main processing step for entering text. Called when the user performs a 1261 * tap, a flick, a long press, releases a gesture, or taps a punctuation suggestion. 1262 */ 1263 private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT = 1264 new LogStatement("LatinImeOnCodeInput", true, false, "code", "x", "y"); 1265 public static void latinIME_onCodeInput(final int code, final int x, final int y) { 1266 final long time = SystemClock.uptimeMillis(); 1267 final ResearchLogger researchLogger = getInstance(); 1268 researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT, 1269 Constants.printableCode(scrubDigitFromCodePoint(code)), x, y); 1270 if (Character.isDigit(code)) { 1271 researchLogger.setCurrentLogUnitContainsDigitFlag(); 1272 } 1273 researchLogger.mStatistics.recordChar(code, time); 1274 } 1275 /** 1276 * Log a call to LatinIME.onDisplayCompletions(). 1277 * 1278 * SystemResponse: The IME has displayed application-specific completions. They may show up 1279 * in the suggestion strip, such as a landscape phone. 1280 */ 1281 private static final LogStatement LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS = 1282 new LogStatement("LatinIMEOnDisplayCompletions", true, true, 1283 "applicationSpecifiedCompletions"); 1284 public static void latinIME_onDisplayCompletions( 1285 final CompletionInfo[] applicationSpecifiedCompletions) { 1286 // Note; passing an array as a single element in a vararg list. Must create a new 1287 // dummy array around it or it will get expanded. 1288 getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS, 1289 new Object[] { applicationSpecifiedCompletions }); 1290 } 1291 1292 public static boolean getAndClearLatinIMEExpectingUpdateSelection() { 1293 boolean returnValue = sLatinIMEExpectingUpdateSelection; 1294 sLatinIMEExpectingUpdateSelection = false; 1295 return returnValue; 1296 } 1297 1298 /** 1299 * Log a call to LatinIME.onWindowHidden(). 1300 * 1301 * UserAction: The user has performed an action that has caused the IME to be closed. They may 1302 * have focused on something other than a text field, or explicitly closed it. 1303 */ 1304 private static final LogStatement LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN = 1305 new LogStatement("LatinIMEOnWindowHidden", false, false, "isTextTruncated", "text"); 1306 public static void latinIME_onWindowHidden(final int savedSelectionStart, 1307 final int savedSelectionEnd, final InputConnection ic) { 1308 if (ic != null) { 1309 final boolean isTextTruncated; 1310 final String text; 1311 if (LOG_FULL_TEXTVIEW_CONTENTS) { 1312 // Capture the TextView contents. This will trigger onUpdateSelection(), so we 1313 // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called, 1314 // it can tell that it was generated by the logging code, and not by the user, and 1315 // therefore keep user-visible state as is. 1316 ic.beginBatchEdit(); 1317 ic.performContextMenuAction(android.R.id.selectAll); 1318 CharSequence charSequence = ic.getSelectedText(0); 1319 if (savedSelectionStart != -1 && savedSelectionEnd != -1) { 1320 ic.setSelection(savedSelectionStart, savedSelectionEnd); 1321 } 1322 ic.endBatchEdit(); 1323 sLatinIMEExpectingUpdateSelection = true; 1324 if (TextUtils.isEmpty(charSequence)) { 1325 isTextTruncated = false; 1326 text = ""; 1327 } else { 1328 if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) { 1329 int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE; 1330 // do not cut in the middle of a supplementary character 1331 final char c = charSequence.charAt(length - 1); 1332 if (Character.isHighSurrogate(c)) { 1333 length--; 1334 } 1335 final CharSequence truncatedCharSequence = charSequence.subSequence(0, 1336 length); 1337 isTextTruncated = true; 1338 text = truncatedCharSequence.toString(); 1339 } else { 1340 isTextTruncated = false; 1341 text = charSequence.toString(); 1342 } 1343 } 1344 } else { 1345 isTextTruncated = true; 1346 text = ""; 1347 } 1348 final ResearchLogger researchLogger = getInstance(); 1349 // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g. 1350 // during a live user test), so the normal isPotentiallyPrivate and 1351 // isPotentiallyRevealing flags do not apply 1352 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN, isTextTruncated, 1353 text); 1354 researchLogger.commitCurrentLogUnit(); 1355 getInstance().stop(); 1356 } 1357 } 1358 1359 /** 1360 * Log a call to LatinIME.onUpdateSelection(). 1361 * 1362 * UserAction/SystemResponse: The user has moved the cursor or selection. This function may 1363 * be called, however, when the system has moved the cursor, say by inserting a character. 1364 */ 1365 private static final LogStatement LOGSTATEMENT_LATINIME_ONUPDATESELECTION = 1366 new LogStatement("LatinIMEOnUpdateSelection", true, false, "lastSelectionStart", 1367 "lastSelectionEnd", "oldSelStart", "oldSelEnd", "newSelStart", "newSelEnd", 1368 "composingSpanStart", "composingSpanEnd", "expectingUpdateSelection", 1369 "expectingUpdateSelectionFromLogger", "context"); 1370 public static void latinIME_onUpdateSelection(final int lastSelectionStart, 1371 final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd, 1372 final int newSelStart, final int newSelEnd, final int composingSpanStart, 1373 final int composingSpanEnd, final boolean expectingUpdateSelection, 1374 final boolean expectingUpdateSelectionFromLogger, 1375 final RichInputConnection connection) { 1376 String word = ""; 1377 if (connection != null) { 1378 Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1); 1379 if (range != null) { 1380 word = range.mWord; 1381 } 1382 } 1383 final ResearchLogger researchLogger = getInstance(); 1384 final String scrubbedWord = researchLogger.scrubWord(word); 1385 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONUPDATESELECTION, lastSelectionStart, 1386 lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd, 1387 composingSpanStart, composingSpanEnd, expectingUpdateSelection, 1388 expectingUpdateSelectionFromLogger, scrubbedWord); 1389 } 1390 1391 /** 1392 * Log a call to LatinIME.onTextInput(). 1393 * 1394 * SystemResponse: Raw text is added to the TextView. 1395 */ 1396 public static void latinIME_onTextInput(final String text, final boolean isBatchMode) { 1397 final ResearchLogger researchLogger = getInstance(); 1398 researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode); 1399 } 1400 1401 /** 1402 * Log a call to LatinIME.pickSuggestionManually(). 1403 * 1404 * UserAction: The user has chosen a specific word from the suggestion strip. 1405 */ 1406 private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY = 1407 new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index", 1408 "suggestion", "x", "y", "isBatchMode"); 1409 public static void latinIME_pickSuggestionManually(final String replacedWord, 1410 final int index, final String suggestion, final boolean isBatchMode) { 1411 final ResearchLogger researchLogger = getInstance(); 1412 if (!replacedWord.equals(suggestion.toString())) { 1413 // The user chose something other than what was already there. 1414 researchLogger.setCurrentLogUnitContainsCorrection(); 1415 researchLogger.setCurrentLogUnitCorrectionType(LogUnit.CORRECTIONTYPE_TYPO); 1416 } 1417 final String scrubbedWord = scrubDigitsFromString(suggestion); 1418 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY, 1419 scrubDigitsFromString(replacedWord), index, 1420 suggestion == null ? null : scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE, 1421 Constants.SUGGESTION_STRIP_COORDINATE, isBatchMode); 1422 researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode); 1423 researchLogger.mStatistics.recordManualSuggestion(SystemClock.uptimeMillis()); 1424 } 1425 1426 /** 1427 * Log a call to LatinIME.punctuationSuggestion(). 1428 * 1429 * UserAction: The user has chosen punctuation from the punctuation suggestion strip. 1430 */ 1431 private static final LogStatement LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION = 1432 new LogStatement("LatinIMEPunctuationSuggestion", false, false, "index", "suggestion", 1433 "x", "y", "isPrediction"); 1434 public static void latinIME_punctuationSuggestion(final int index, final String suggestion, 1435 final boolean isBatchMode, final boolean isPrediction) { 1436 final ResearchLogger researchLogger = getInstance(); 1437 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion, 1438 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, 1439 isPrediction); 1440 researchLogger.commitCurrentLogUnitAsWord(suggestion, Long.MAX_VALUE, isBatchMode); 1441 } 1442 1443 /** 1444 * Log a call to LatinIME.sendKeyCodePoint(). 1445 * 1446 * SystemResponse: The IME is inserting text into the TextView for numbers, fixed strings, or 1447 * some other unusual mechanism. 1448 */ 1449 private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT = 1450 new LogStatement("LatinIMESendKeyCodePoint", true, false, "code"); 1451 public static void latinIME_sendKeyCodePoint(final int code) { 1452 final ResearchLogger researchLogger = getInstance(); 1453 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT, 1454 Constants.printableCode(scrubDigitFromCodePoint(code))); 1455 if (Character.isDigit(code)) { 1456 researchLogger.setCurrentLogUnitContainsDigitFlag(); 1457 } 1458 } 1459 1460 /** 1461 * Log a call to LatinIME.promotePhantomSpace(). 1462 * 1463 * SystemResponse: The IME is inserting a real space in place of a phantom space. 1464 */ 1465 private static final LogStatement LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE = 1466 new LogStatement("LatinIMEPromotPhantomSpace", false, false); 1467 public static void latinIME_promotePhantomSpace() { 1468 final ResearchLogger researchLogger = getInstance(); 1469 final LogUnit logUnit; 1470 if (researchLogger.mMainLogBuffer == null) { 1471 logUnit = researchLogger.mCurrentLogUnit; 1472 } else { 1473 logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); 1474 } 1475 researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE); 1476 } 1477 1478 /** 1479 * Log a call to LatinIME.swapSwapperAndSpace(). 1480 * 1481 * SystemResponse: A symbol has been swapped with a space character. E.g. punctuation may swap 1482 * if a soft space is inserted after a word. 1483 */ 1484 private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE = 1485 new LogStatement("LatinIMESwapSwapperAndSpace", false, false, "originalCharacters", 1486 "charactersAfterSwap"); 1487 public static void latinIME_swapSwapperAndSpace(final CharSequence originalCharacters, 1488 final String charactersAfterSwap) { 1489 final ResearchLogger researchLogger = getInstance(); 1490 final LogUnit logUnit; 1491 if (researchLogger.mMainLogBuffer == null) { 1492 logUnit = null; 1493 } else { 1494 logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); 1495 } 1496 if (logUnit != null) { 1497 researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE, 1498 originalCharacters, charactersAfterSwap); 1499 } 1500 } 1501 1502 /** 1503 * Log a call to LatinIME.maybeDoubleSpacePeriod(). 1504 * 1505 * SystemResponse: Two spaces have been replaced by period space. 1506 */ 1507 public static void latinIME_maybeDoubleSpacePeriod(final String text, 1508 final boolean isBatchMode) { 1509 final ResearchLogger researchLogger = getInstance(); 1510 researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode); 1511 } 1512 1513 /** 1514 * Log a call to MainKeyboardView.onLongPress(). 1515 * 1516 * UserAction: The user has performed a long-press on a key. 1517 */ 1518 private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS = 1519 new LogStatement("MainKeyboardViewOnLongPress", false, false); 1520 public static void mainKeyboardView_onLongPress() { 1521 getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS); 1522 } 1523 1524 /** 1525 * Log a call to MainKeyboardView.setKeyboard(). 1526 * 1527 * SystemResponse: The IME has switched to a new keyboard (e.g. French, English). 1528 * This is typically called right after LatinIME.onStartInputViewInternal (when starting a new 1529 * IME), but may happen at other times if the user explicitly requests a keyboard change. 1530 */ 1531 private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD = 1532 new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale", 1533 "orientation", "width", "modeName", "action", "navigateNext", 1534 "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled", 1535 "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th", 1536 "keys"); 1537 public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) { 1538 final KeyboardId kid = keyboard.mId; 1539 final boolean isPasswordView = kid.passwordInput(); 1540 final ResearchLogger researchLogger = getInstance(); 1541 researchLogger.setIsPasswordView(isPasswordView); 1542 researchLogger.enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD, 1543 KeyboardId.elementIdToName(kid.mElementId), 1544 kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), 1545 kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(), 1546 kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey, 1547 isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey, 1548 kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth, 1549 keyboard.mOccupiedHeight, keyboard.mKeys); 1550 } 1551 1552 /** 1553 * Log a call to LatinIME.revertCommit(). 1554 * 1555 * SystemResponse: The IME has reverted commited text. This happens when the user enters 1556 * a word, commits it by pressing space or punctuation, and then reverts the commit by hitting 1557 * backspace. 1558 */ 1559 private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT = 1560 new LogStatement("LatinIMERevertCommit", true, false, "committedWord", 1561 "originallyTypedWord", "separatorString"); 1562 public static void latinIME_revertCommit(final String committedWord, 1563 final String originallyTypedWord, final boolean isBatchMode, 1564 final String separatorString) { 1565 final ResearchLogger researchLogger = getInstance(); 1566 // TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word. 1567 final LogUnit logUnit; 1568 if (researchLogger.mMainLogBuffer == null) { 1569 logUnit = null; 1570 } else { 1571 logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); 1572 } 1573 if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) { 1574 if (logUnit != null) { 1575 logUnit.setWord(originallyTypedWord); 1576 } 1577 } 1578 researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit, 1579 LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, originallyTypedWord, 1580 separatorString); 1581 if (logUnit != null) { 1582 logUnit.setContainsCorrection(); 1583 } 1584 researchLogger.mStatistics.recordRevertCommit(SystemClock.uptimeMillis()); 1585 researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE, isBatchMode); 1586 } 1587 1588 /** 1589 * Log a call to PointerTracker.callListenerOnCancelInput(). 1590 * 1591 * UserAction: The user has canceled the input, e.g., by pressing down, but then removing 1592 * outside the keyboard area. 1593 * TODO: Verify 1594 */ 1595 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT = 1596 new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false); 1597 public static void pointerTracker_callListenerOnCancelInput() { 1598 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT); 1599 } 1600 1601 /** 1602 * Log a call to PointerTracker.callListenerOnCodeInput(). 1603 * 1604 * SystemResponse: The user has entered a key through the normal tapping mechanism. 1605 * LatinIME.onCodeInput will also be called. 1606 */ 1607 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT = 1608 new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "code", 1609 "outputText", "x", "y", "ignoreModifierKey", "altersCode", "isEnabled"); 1610 public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x, 1611 final int y, final boolean ignoreModifierKey, final boolean altersCode, 1612 final int code) { 1613 if (key != null) { 1614 String outputText = key.getOutputText(); 1615 final ResearchLogger researchLogger = getInstance(); 1616 researchLogger.enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT, 1617 Constants.printableCode(scrubDigitFromCodePoint(code)), 1618 outputText == null ? null : scrubDigitsFromString(outputText.toString()), 1619 x, y, ignoreModifierKey, altersCode, key.isEnabled()); 1620 if (code == Constants.CODE_RESEARCH) { 1621 researchLogger.suppressResearchKeyMotionData(); 1622 } 1623 } 1624 } 1625 1626 private void suppressResearchKeyMotionData() { 1627 mCurrentLogUnit.removeResearchButtonInvocation(); 1628 } 1629 1630 /** 1631 * Log a call to PointerTracker.callListenerCallListenerOnRelease(). 1632 * 1633 * UserAction: The user has released their finger or thumb from the screen. 1634 */ 1635 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE = 1636 new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code", 1637 "withSliding", "ignoreModifierKey", "isEnabled"); 1638 public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode, 1639 final boolean withSliding, final boolean ignoreModifierKey) { 1640 if (key != null) { 1641 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE, 1642 Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding, 1643 ignoreModifierKey, key.isEnabled()); 1644 } 1645 } 1646 1647 /** 1648 * Log a call to PointerTracker.onDownEvent(). 1649 * 1650 * UserAction: The user has pressed down on a key. 1651 * TODO: Differentiate with LatinIME.processMotionEvent. 1652 */ 1653 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT = 1654 new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared"); 1655 public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) { 1656 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT, deltaT, 1657 distanceSquared); 1658 } 1659 1660 /** 1661 * Log a call to PointerTracker.onMoveEvent(). 1662 * 1663 * UserAction: The user has moved their finger while pressing on the screen. 1664 * TODO: Differentiate with LatinIME.processMotionEvent(). 1665 */ 1666 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT = 1667 new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY"); 1668 public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX, 1669 final int lastY) { 1670 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY); 1671 } 1672 1673 /** 1674 * Log a call to RichInputConnection.commitCompletion(). 1675 * 1676 * SystemResponse: The IME has committed a completion. A completion is an application- 1677 * specific suggestion that is presented in a pop-up menu in the TextView. 1678 */ 1679 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION = 1680 new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo"); 1681 public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) { 1682 final ResearchLogger researchLogger = getInstance(); 1683 researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION, 1684 completionInfo); 1685 } 1686 1687 /** 1688 * Log a call to RichInputConnection.revertDoubleSpacePeriod(). 1689 * 1690 * SystemResponse: The IME has reverted ". ", which had previously replaced two typed spaces. 1691 */ 1692 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD = 1693 new LogStatement("RichInputConnectionRevertDoubleSpacePeriod", false, false); 1694 public static void richInputConnection_revertDoubleSpacePeriod() { 1695 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD); 1696 } 1697 1698 /** 1699 * Log a call to RichInputConnection.revertSwapPunctuation(). 1700 * 1701 * SystemResponse: The IME has reverted a punctuation swap. 1702 */ 1703 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION = 1704 new LogStatement("RichInputConnectionRevertSwapPunctuation", false, false); 1705 public static void richInputConnection_revertSwapPunctuation() { 1706 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION); 1707 } 1708 1709 /** 1710 * Log a call to LatinIME.commitCurrentAutoCorrection(). 1711 * 1712 * SystemResponse: The IME has committed an auto-correction. An auto-correction changes the raw 1713 * text input to another word that the user more likely desired to type. 1714 */ 1715 private static final LogStatement LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION = 1716 new LogStatement("LatinIMECommitCurrentAutoCorrection", true, true, "typedWord", 1717 "autoCorrection", "separatorString"); 1718 public static void latinIme_commitCurrentAutoCorrection(final String typedWord, 1719 final String autoCorrection, final String separatorString, final boolean isBatchMode, 1720 final SuggestedWords suggestedWords) { 1721 final String scrubbedTypedWord = scrubDigitsFromString(typedWord); 1722 final String scrubbedAutoCorrection = scrubDigitsFromString(autoCorrection); 1723 final ResearchLogger researchLogger = getInstance(); 1724 researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords); 1725 researchLogger.commitCurrentLogUnitAsWord(scrubbedAutoCorrection, Long.MAX_VALUE, 1726 isBatchMode); 1727 1728 // Add the autocorrection logStatement at the end of the logUnit for the committed word. 1729 // We have to do this after calling commitCurrentLogUnitAsWord, because it may split the 1730 // current logUnit, and then we have to peek to get the logUnit reference back. 1731 final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); 1732 // TODO: Add test to confirm that the commitCurrentAutoCorrection log statement should 1733 // always be added to logUnit (if non-null) and not mCurrentLogUnit. 1734 researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION, 1735 scrubbedTypedWord, scrubbedAutoCorrection, separatorString); 1736 } 1737 1738 private boolean isExpectingCommitText = false; 1739 /** 1740 * Log a call to (UnknownClass).commitPartialText 1741 * 1742 * SystemResponse: The IME is committing part of a word. This happens if a space is 1743 * automatically inserted to split a single typed string into two or more words. 1744 */ 1745 // TODO: This method is currently unused. Find where it should be called from in the IME and 1746 // add invocations. 1747 private static final LogStatement LOGSTATEMENT_COMMIT_PARTIAL_TEXT = 1748 new LogStatement("CommitPartialText", true, false, "newCursorPosition"); 1749 public static void commitPartialText(final String committedWord, 1750 final long lastTimestampOfWordData, final boolean isBatchMode) { 1751 final ResearchLogger researchLogger = getInstance(); 1752 final String scrubbedWord = scrubDigitsFromString(committedWord); 1753 researchLogger.enqueueEvent(LOGSTATEMENT_COMMIT_PARTIAL_TEXT); 1754 researchLogger.mStatistics.recordAutoCorrection(SystemClock.uptimeMillis()); 1755 researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData, 1756 isBatchMode); 1757 } 1758 1759 /** 1760 * Log a call to RichInputConnection.commitText(). 1761 * 1762 * SystemResponse: The IME is committing text. This happens after the user has typed a word 1763 * and then a space or punctuation key. 1764 */ 1765 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT = 1766 new LogStatement("RichInputConnectionCommitText", true, false, "newCursorPosition"); 1767 public static void richInputConnection_commitText(final String committedWord, 1768 final int newCursorPosition, final boolean isBatchMode) { 1769 final ResearchLogger researchLogger = getInstance(); 1770 // Only include opening and closing logSegments if private data is included 1771 final String scrubbedWord = scrubDigitsFromString(committedWord); 1772 if (!researchLogger.isExpectingCommitText) { 1773 researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT, 1774 newCursorPosition); 1775 researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode); 1776 } 1777 researchLogger.isExpectingCommitText = false; 1778 } 1779 1780 /** 1781 * Shared event for logging committed text. 1782 */ 1783 private static final LogStatement LOGSTATEMENT_COMMITTEXT = 1784 new LogStatement("CommitText", true, false, "committedText", "isBatchMode"); 1785 private void enqueueCommitText(final String word, final boolean isBatchMode) { 1786 enqueueEvent(LOGSTATEMENT_COMMITTEXT, word, isBatchMode); 1787 } 1788 1789 /** 1790 * Log a call to RichInputConnection.deleteSurroundingText(). 1791 * 1792 * SystemResponse: The IME has deleted text. 1793 */ 1794 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = 1795 new LogStatement("RichInputConnectionDeleteSurroundingText", true, false, 1796 "beforeLength", "afterLength"); 1797 public static void richInputConnection_deleteSurroundingText(final int beforeLength, 1798 final int afterLength) { 1799 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, 1800 beforeLength, afterLength); 1801 } 1802 1803 /** 1804 * Log a call to RichInputConnection.finishComposingText(). 1805 * 1806 * SystemResponse: The IME has left the composing text as-is. 1807 */ 1808 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = 1809 new LogStatement("RichInputConnectionFinishComposingText", false, false); 1810 public static void richInputConnection_finishComposingText() { 1811 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT); 1812 } 1813 1814 /** 1815 * Log a call to RichInputConnection.performEditorAction(). 1816 * 1817 * SystemResponse: The IME is invoking an action specific to the editor. 1818 */ 1819 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION = 1820 new LogStatement("RichInputConnectionPerformEditorAction", false, false, 1821 "imeActionId"); 1822 public static void richInputConnection_performEditorAction(final int imeActionId) { 1823 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION, 1824 imeActionId); 1825 } 1826 1827 /** 1828 * Log a call to RichInputConnection.sendKeyEvent(). 1829 * 1830 * SystemResponse: The IME is telling the TextView that a key is being pressed through an 1831 * alternate channel. 1832 * TODO: only for hardware keys? 1833 */ 1834 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT = 1835 new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action", 1836 "code"); 1837 public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) { 1838 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT, 1839 keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode()); 1840 } 1841 1842 /** 1843 * Log a call to RichInputConnection.setComposingText(). 1844 * 1845 * SystemResponse: The IME is setting the composing text. Happens each time a character is 1846 * entered. 1847 */ 1848 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = 1849 new LogStatement("RichInputConnectionSetComposingText", true, true, "text", 1850 "newCursorPosition"); 1851 public static void richInputConnection_setComposingText(final CharSequence text, 1852 final int newCursorPosition) { 1853 if (text == null) { 1854 throw new RuntimeException("setComposingText is null"); 1855 } 1856 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, text, 1857 newCursorPosition); 1858 } 1859 1860 /** 1861 * Log a call to RichInputConnection.setSelection(). 1862 * 1863 * SystemResponse: The IME is requesting that the selection change. User-initiated selection- 1864 * change requests do not go through this method -- it's only when the system wants to change 1865 * the selection. 1866 */ 1867 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION = 1868 new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to"); 1869 public static void richInputConnection_setSelection(final int from, final int to) { 1870 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to); 1871 } 1872 1873 /** 1874 * Log a call to SuddenJumpingTouchEventHandler.onTouchEvent(). 1875 * 1876 * SystemResponse: The IME has filtered input events in case of an erroneous sensor reading. 1877 */ 1878 private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = 1879 new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false, 1880 "motionEvent"); 1881 public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) { 1882 if (me != null) { 1883 getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, 1884 me.toString()); 1885 } 1886 } 1887 1888 /** 1889 * Log a call to SuggestionsView.setSuggestions(). 1890 * 1891 * SystemResponse: The IME is setting the suggestions in the suggestion strip. 1892 */ 1893 private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS = 1894 new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords"); 1895 public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) { 1896 if (suggestedWords != null) { 1897 getInstance().enqueueEvent(LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS, 1898 suggestedWords); 1899 } 1900 } 1901 1902 /** 1903 * The user has indicated a particular point in the log that is of interest. 1904 * 1905 * UserAction: From direct menu invocation. 1906 */ 1907 private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP = 1908 new LogStatement("UserTimestamp", false, false); 1909 public void userTimestamp() { 1910 getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP); 1911 } 1912 1913 /** 1914 * Log a call to LatinIME.onEndBatchInput(). 1915 * 1916 * SystemResponse: The system has completed a gesture. 1917 */ 1918 private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT = 1919 new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText", 1920 "enteredWordPos"); 1921 public static void latinIME_onEndBatchInput(final CharSequence enteredText, 1922 final int enteredWordPos, final SuggestedWords suggestedWords) { 1923 final ResearchLogger researchLogger = getInstance(); 1924 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText, 1925 enteredWordPos); 1926 researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords); 1927 researchLogger.mStatistics.recordGestureInput(enteredText.length(), 1928 SystemClock.uptimeMillis()); 1929 } 1930 1931 /** 1932 * Log a call to LatinIME.handleBackspace() that is not a batch delete. 1933 * 1934 * UserInput: The user is deleting one or more characters by hitting the backspace key once. 1935 * The covers single character deletes as well as deleting selections. 1936 */ 1937 private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE = 1938 new LogStatement("LatinIMEHandleBackspace", true, false, "numCharacters"); 1939 public static void latinIME_handleBackspace(final int numCharacters) { 1940 final ResearchLogger researchLogger = getInstance(); 1941 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE, numCharacters); 1942 } 1943 1944 /** 1945 * Log a call to LatinIME.handleBackspace() that is a batch delete. 1946 * 1947 * UserInput: The user is deleting a gestured word by hitting the backspace key once. 1948 */ 1949 private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH = 1950 new LogStatement("LatinIMEHandleBackspaceBatch", true, false, "deletedText", 1951 "numCharacters"); 1952 public static void latinIME_handleBackspace_batch(final CharSequence deletedText, 1953 final int numCharacters) { 1954 final ResearchLogger researchLogger = getInstance(); 1955 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText, 1956 numCharacters); 1957 researchLogger.mStatistics.recordGestureDelete(deletedText.length(), 1958 SystemClock.uptimeMillis()); 1959 } 1960 1961 /** 1962 * Log a long interval between user operation. 1963 * 1964 * UserInput: The user has not done anything for a while. 1965 */ 1966 private static final LogStatement LOGSTATEMENT_ONUSERPAUSE = new LogStatement("OnUserPause", 1967 false, false, "intervalInMs"); 1968 public static void onUserPause(final long interval) { 1969 final ResearchLogger researchLogger = getInstance(); 1970 researchLogger.enqueueEvent(LOGSTATEMENT_ONUSERPAUSE, interval); 1971 } 1972 1973 /** 1974 * Record the current time in case the LogUnit is later split. 1975 * 1976 * If the current logUnit is split, then tapping, motion events, etc. before this time should 1977 * be assigned to one LogUnit, and events after this time should go into the following LogUnit. 1978 */ 1979 public static void recordTimeForLogUnitSplit() { 1980 final ResearchLogger researchLogger = getInstance(); 1981 researchLogger.setSavedDownEventTime(SystemClock.uptimeMillis()); 1982 researchLogger.mSavedDownEventTime = Long.MAX_VALUE; 1983 } 1984 1985 /** 1986 * Log a call to LatinIME.handleSeparator() 1987 * 1988 * SystemResponse: The system is inserting a separator character, possibly performing auto- 1989 * correction or other actions appropriate at the end of a word. 1990 */ 1991 private static final LogStatement LOGSTATEMENT_LATINIME_HANDLESEPARATOR = 1992 new LogStatement("LatinIMEHandleSeparator", false, false, "primaryCode", 1993 "isComposingWord"); 1994 public static void latinIME_handleSeparator(final int primaryCode, 1995 final boolean isComposingWord) { 1996 final ResearchLogger researchLogger = getInstance(); 1997 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLESEPARATOR, primaryCode, 1998 isComposingWord); 1999 } 2000 2001 /** 2002 * Log statistics. 2003 * 2004 * ContextualData, recorded at the end of a session. 2005 */ 2006 private static final LogStatement LOGSTATEMENT_STATISTICS = 2007 new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount", 2008 "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting", 2009 "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete", 2010 "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete", 2011 "dictionaryWordCount", "splitWordsCount", "gestureInputCount", 2012 "gestureCharsCount", "gesturesDeletedCount", "manualSuggestionsCount", 2013 "revertCommitsCount", "correctedWordsCount", "autoCorrectionsCount"); 2014 private static void logStatistics() { 2015 final ResearchLogger researchLogger = getInstance(); 2016 final Statistics statistics = researchLogger.mStatistics; 2017 researchLogger.enqueueEvent(LOGSTATEMENT_STATISTICS, statistics.mCharCount, 2018 statistics.mLetterCount, statistics.mNumberCount, statistics.mSpaceCount, 2019 statistics.mDeleteKeyCount, statistics.mWordCount, statistics.mIsEmptyUponStarting, 2020 statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(), 2021 statistics.mBeforeDeleteKeyCounter.getAverageTime(), 2022 statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(), 2023 statistics.mAfterDeleteKeyCounter.getAverageTime(), 2024 statistics.mDictionaryWordCount, statistics.mSplitWordsCount, 2025 statistics.mGesturesInputCount, statistics.mGesturesCharsCount, 2026 statistics.mGesturesDeletedCount, statistics.mManualSuggestionsCount, 2027 statistics.mRevertCommitsCount, statistics.mCorrectedWordsCount, 2028 statistics.mAutoCorrectionsCount); 2029 } 2030} 2031