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