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