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