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