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