ResearchLogger.java revision 3338703a2fe8fa3ae549e1d884d9fb5a579a7f74
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * 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.app.AlarmManager; 22import android.app.AlertDialog; 23import android.app.Dialog; 24import android.app.PendingIntent; 25import android.content.Context; 26import android.content.DialogInterface; 27import android.content.DialogInterface.OnCancelListener; 28import android.content.Intent; 29import android.content.SharedPreferences; 30import android.content.SharedPreferences.Editor; 31import android.content.pm.PackageInfo; 32import android.content.pm.PackageManager.NameNotFoundException; 33import android.graphics.Canvas; 34import android.graphics.Color; 35import android.graphics.Paint; 36import android.graphics.Paint.Style; 37import android.net.Uri; 38import android.os.Build; 39import android.os.IBinder; 40import android.os.SystemClock; 41import android.preference.PreferenceManager; 42import android.text.TextUtils; 43import android.text.format.DateUtils; 44import android.util.Log; 45import android.view.KeyEvent; 46import android.view.MotionEvent; 47import android.view.Window; 48import android.view.WindowManager; 49import android.view.inputmethod.CompletionInfo; 50import android.view.inputmethod.EditorInfo; 51import android.view.inputmethod.InputConnection; 52import android.widget.Toast; 53 54import com.android.inputmethod.keyboard.Key; 55import com.android.inputmethod.keyboard.Keyboard; 56import com.android.inputmethod.keyboard.KeyboardId; 57import com.android.inputmethod.keyboard.KeyboardView; 58import com.android.inputmethod.keyboard.MainKeyboardView; 59import com.android.inputmethod.latin.Constants; 60import com.android.inputmethod.latin.Dictionary; 61import com.android.inputmethod.latin.InputTypeUtils; 62import com.android.inputmethod.latin.LatinIME; 63import com.android.inputmethod.latin.R; 64import com.android.inputmethod.latin.RichInputConnection; 65import com.android.inputmethod.latin.RichInputConnection.Range; 66import com.android.inputmethod.latin.Suggest; 67import com.android.inputmethod.latin.SuggestedWords; 68import com.android.inputmethod.latin.define.ProductionFlag; 69 70import java.io.File; 71import java.text.SimpleDateFormat; 72import java.util.Date; 73import java.util.Locale; 74import java.util.UUID; 75 76/** 77 * Logs the use of the LatinIME keyboard. 78 * 79 * This class logs operations on the IME keyboard, including what the user has typed. 80 * Data is stored locally in a file in app-specific storage. 81 * 82 * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}. 83 */ 84public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener { 85 private static final String TAG = ResearchLogger.class.getSimpleName(); 86 private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; 87 // Whether all n-grams should be logged. true will disclose private info. 88 public static final boolean IS_LOGGING_EVERYTHING = false 89 && ProductionFlag.IS_EXPERIMENTAL_DEBUG; 90 // Whether the TextView contents are logged at the end of the session. true will disclose 91 // private info. 92 private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false 93 && ProductionFlag.IS_EXPERIMENTAL_DEBUG; 94 public static final boolean DEFAULT_USABILITY_STUDY_MODE = false; 95 /* package */ static boolean sIsLogging = false; 96 private static final int OUTPUT_FORMAT_VERSION = 5; 97 private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; 98 private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash"; 99 /* package */ static final String FILENAME_PREFIX = "researchLog"; 100 private static final String FILENAME_SUFFIX = ".txt"; 101 private static final SimpleDateFormat TIMESTAMP_DATEFORMAT = 102 new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); 103 // Whether to show an indicator on the screen that logging is on. Currently a very small red 104 // dot in the lower right hand corner. Most users should not notice it. 105 private static final boolean IS_SHOWING_INDICATOR = true; 106 // Change the default indicator to something very visible. Currently two red vertical bars on 107 // either side of they keyboard. 108 private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false || IS_LOGGING_EVERYTHING; 109 public static final int FEEDBACK_WORD_BUFFER_SIZE = 5; 110 111 // constants related to specific log points 112 private static final String WHITESPACE_SEPARATORS = " \t\n\r"; 113 private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 114 private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid"; 115 116 private static final ResearchLogger sInstance = new ResearchLogger(); 117 // to write to a different filename, e.g., for testing, set mFile before calling start() 118 /* package */ File mFilesDir; 119 /* package */ String mUUIDString; 120 /* package */ ResearchLog mMainResearchLog; 121 // mFeedbackLog records all events for the session, private or not (excepting 122 // passwords). It is written to permanent storage only if the user explicitly commands 123 // the system to do so. 124 // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are 125 // complete. 126 /* package */ ResearchLog mFeedbackLog; 127 /* package */ MainLogBuffer mMainLogBuffer; 128 /* package */ LogBuffer mFeedbackLogBuffer; 129 130 private boolean mIsPasswordView = false; 131 private boolean mIsLoggingSuspended = false; 132 private SharedPreferences mPrefs; 133 134 // digits entered by the user are replaced with this codepoint. 135 /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT = 136 Character.codePointAt("\uE000", 0); // U+E000 is in the "private-use area" 137 // U+E001 is in the "private-use area" 138 /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001"; 139 private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time"; 140 private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS; 141 private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS; 142 protected static final int SUSPEND_DURATION_IN_MINUTES = 1; 143 // set when LatinIME should ignore an onUpdateSelection() callback that 144 // arises from operations in this class 145 private static boolean sLatinIMEExpectingUpdateSelection = false; 146 147 // used to check whether words are not unique 148 private Suggest mSuggest; 149 private MainKeyboardView mMainKeyboardView; 150 private LatinIME mLatinIME; 151 private final Statistics mStatistics; 152 153 private Intent mUploadIntent; 154 155 private LogUnit mCurrentLogUnit = new LogUnit(); 156 157 // Gestured or tapped words may be committed after the gesture of the next word has started. 158 // To ensure that the gesture data of the next word is not associated with the previous word, 159 // thereby leaking private data, we store the time of the down event that started the second 160 // gesture, and when committing the earlier word, split the LogUnit. 161 private long mSavedDownEventTime; 162 private ResearchLogger() { 163 mStatistics = Statistics.getInstance(); 164 } 165 166 public static ResearchLogger getInstance() { 167 return sInstance; 168 } 169 170 public void init(final LatinIME latinIME) { 171 assert latinIME != null; 172 if (latinIME == null) { 173 Log.w(TAG, "IMS is null; logging is off"); 174 } else { 175 mFilesDir = latinIME.getFilesDir(); 176 if (mFilesDir == null || !mFilesDir.exists()) { 177 Log.w(TAG, "IME storage directory does not exist."); 178 } 179 } 180 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(latinIME); 181 if (prefs != null) { 182 mUUIDString = getUUID(prefs); 183 if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) { 184 Editor e = prefs.edit(); 185 e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE); 186 e.apply(); 187 } 188 sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); 189 prefs.registerOnSharedPreferenceChangeListener(this); 190 191 final long lastCleanupTime = prefs.getLong(PREF_LAST_CLEANUP_TIME, 0L); 192 final long now = System.currentTimeMillis(); 193 if (lastCleanupTime + DURATION_BETWEEN_DIR_CLEANUP_IN_MS < now) { 194 final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS; 195 cleanupLoggingDir(mFilesDir, timeHorizon); 196 Editor e = prefs.edit(); 197 e.putLong(PREF_LAST_CLEANUP_TIME, now); 198 e.apply(); 199 } 200 } 201 mLatinIME = latinIME; 202 mPrefs = prefs; 203 mUploadIntent = new Intent(mLatinIME, UploaderService.class); 204 205 if (ProductionFlag.IS_EXPERIMENTAL) { 206 scheduleUploadingService(mLatinIME); 207 } 208 } 209 210 /** 211 * Arrange for the UploaderService to be run on a regular basis. 212 * 213 * Any existing scheduled invocation of UploaderService is removed and rescheduled. This may 214 * cause problems if this method is called often and frequent updates are required, but since 215 * the user will likely be sleeping at some point, if the interval is less that the expected 216 * sleep duration and this method is not called during that time, the service should be invoked 217 * at some point. 218 */ 219 public static void scheduleUploadingService(Context context) { 220 final Intent intent = new Intent(context, UploaderService.class); 221 final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0); 222 final AlarmManager manager = 223 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 224 manager.cancel(pendingIntent); 225 manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 226 UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent); 227 } 228 229 private void cleanupLoggingDir(final File dir, final long time) { 230 for (File file : dir.listFiles()) { 231 if (file.getName().startsWith(ResearchLogger.FILENAME_PREFIX) && 232 file.lastModified() < time) { 233 file.delete(); 234 } 235 } 236 } 237 238 public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) { 239 mMainKeyboardView = mainKeyboardView; 240 maybeShowSplashScreen(); 241 } 242 243 public void mainKeyboardView_onDetachedFromWindow() { 244 mMainKeyboardView = null; 245 } 246 247 private boolean hasSeenSplash() { 248 return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false); 249 } 250 251 private Dialog mSplashDialog = null; 252 253 private void maybeShowSplashScreen() { 254 if (hasSeenSplash()) { 255 return; 256 } 257 if (mSplashDialog != null && mSplashDialog.isShowing()) { 258 return; 259 } 260 final IBinder windowToken = mMainKeyboardView != null 261 ? mMainKeyboardView.getWindowToken() : null; 262 if (windowToken == null) { 263 return; 264 } 265 final AlertDialog.Builder builder = new AlertDialog.Builder(mLatinIME) 266 .setTitle(R.string.research_splash_title) 267 .setMessage(R.string.research_splash_content) 268 .setPositiveButton(android.R.string.yes, 269 new DialogInterface.OnClickListener() { 270 @Override 271 public void onClick(DialogInterface dialog, int which) { 272 onUserLoggingConsent(); 273 mSplashDialog.dismiss(); 274 } 275 }) 276 .setNegativeButton(android.R.string.no, 277 new DialogInterface.OnClickListener() { 278 @Override 279 public void onClick(DialogInterface dialog, int which) { 280 final String packageName = mLatinIME.getPackageName(); 281 final Uri packageUri = Uri.parse("package:" + packageName); 282 final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, 283 packageUri); 284 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 285 mLatinIME.startActivity(intent); 286 } 287 }) 288 .setCancelable(true) 289 .setOnCancelListener( 290 new OnCancelListener() { 291 @Override 292 public void onCancel(DialogInterface dialog) { 293 mLatinIME.requestHideSelf(0); 294 } 295 }); 296 mSplashDialog = builder.create(); 297 final Window w = mSplashDialog.getWindow(); 298 final WindowManager.LayoutParams lp = w.getAttributes(); 299 lp.token = windowToken; 300 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 301 w.setAttributes(lp); 302 w.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 303 mSplashDialog.show(); 304 } 305 306 public void onUserLoggingConsent() { 307 setLoggingAllowed(true); 308 if (mPrefs == null) { 309 return; 310 } 311 final Editor e = mPrefs.edit(); 312 e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true); 313 e.apply(); 314 restart(); 315 } 316 317 private void setLoggingAllowed(boolean enableLogging) { 318 if (mPrefs == null) { 319 return; 320 } 321 Editor e = mPrefs.edit(); 322 e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); 323 e.apply(); 324 sIsLogging = enableLogging; 325 } 326 327 private File createLogFile(File filesDir) { 328 final StringBuilder sb = new StringBuilder(); 329 sb.append(FILENAME_PREFIX).append('-'); 330 sb.append(mUUIDString).append('-'); 331 sb.append(TIMESTAMP_DATEFORMAT.format(new Date())); 332 sb.append(FILENAME_SUFFIX); 333 return new File(filesDir, sb.toString()); 334 } 335 336 private void checkForEmptyEditor() { 337 if (mLatinIME == null) { 338 return; 339 } 340 final InputConnection ic = mLatinIME.getCurrentInputConnection(); 341 if (ic == null) { 342 return; 343 } 344 final CharSequence textBefore = ic.getTextBeforeCursor(1, 0); 345 if (!TextUtils.isEmpty(textBefore)) { 346 mStatistics.setIsEmptyUponStarting(false); 347 return; 348 } 349 final CharSequence textAfter = ic.getTextAfterCursor(1, 0); 350 if (!TextUtils.isEmpty(textAfter)) { 351 mStatistics.setIsEmptyUponStarting(false); 352 return; 353 } 354 if (textBefore != null && textAfter != null) { 355 mStatistics.setIsEmptyUponStarting(true); 356 } 357 } 358 359 private void start() { 360 if (DEBUG) { 361 Log.d(TAG, "start called"); 362 } 363 maybeShowSplashScreen(); 364 updateSuspendedState(); 365 requestIndicatorRedraw(); 366 mStatistics.reset(); 367 checkForEmptyEditor(); 368 if (!isAllowedToLog()) { 369 // Log.w(TAG, "not in usability mode; not logging"); 370 return; 371 } 372 if (mFilesDir == null || !mFilesDir.exists()) { 373 Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); 374 return; 375 } 376 if (mMainLogBuffer == null) { 377 mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); 378 mMainLogBuffer = new MainLogBuffer(mMainResearchLog); 379 mMainLogBuffer.setSuggest(mSuggest); 380 } 381 if (mFeedbackLogBuffer == null) { 382 mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); 383 // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold 384 // the feedback LogUnit itself. 385 mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1); 386 } 387 } 388 389 /* package */ void stop() { 390 if (DEBUG) { 391 Log.d(TAG, "stop called"); 392 } 393 // Commit mCurrentLogUnit before closing. 394 commitCurrentLogUnit(); 395 396 if (mMainLogBuffer != null) { 397 while (!mMainLogBuffer.isEmpty()) { 398 if ((mMainLogBuffer.isNGramSafe() || IS_LOGGING_EVERYTHING) && 399 mMainResearchLog != null) { 400 publishLogBuffer(mMainLogBuffer, mMainResearchLog, 401 true /* isIncludingPrivateData */); 402 mMainLogBuffer.resetWordCounter(); 403 } else { 404 mMainLogBuffer.shiftOutThroughFirstWord(); 405 } 406 } 407 mMainResearchLog.close(null /* callback */); 408 mMainLogBuffer = null; 409 } 410 if (mFeedbackLogBuffer != null) { 411 mFeedbackLog.close(null /* callback */); 412 mFeedbackLogBuffer = null; 413 } 414 } 415 416 public boolean abort() { 417 if (DEBUG) { 418 Log.d(TAG, "abort called"); 419 } 420 boolean didAbortMainLog = false; 421 if (mMainLogBuffer != null) { 422 mMainLogBuffer.clear(); 423 try { 424 didAbortMainLog = mMainResearchLog.blockingAbort(); 425 } catch (InterruptedException e) { 426 // Don't know whether this succeeded or not. We assume not; this is reported 427 // to the caller. 428 } 429 mMainLogBuffer = null; 430 } 431 boolean didAbortFeedbackLog = false; 432 if (mFeedbackLogBuffer != null) { 433 mFeedbackLogBuffer.clear(); 434 try { 435 didAbortFeedbackLog = mFeedbackLog.blockingAbort(); 436 } catch (InterruptedException e) { 437 // Don't know whether this succeeded or not. We assume not; this is reported 438 // to the caller. 439 } 440 mFeedbackLogBuffer = null; 441 } 442 return didAbortMainLog && didAbortFeedbackLog; 443 } 444 445 private void restart() { 446 stop(); 447 start(); 448 } 449 450 private long mResumeTime = 0L; 451 private void updateSuspendedState() { 452 final long time = System.currentTimeMillis(); 453 if (time > mResumeTime) { 454 mIsLoggingSuspended = false; 455 } 456 } 457 458 @Override 459 public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 460 if (key == null || prefs == null) { 461 return; 462 } 463 sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); 464 if (sIsLogging == false) { 465 abort(); 466 } 467 requestIndicatorRedraw(); 468 mPrefs = prefs; 469 prefsChanged(prefs); 470 } 471 472 public void onResearchKeySelected(final LatinIME latinIME) { 473 if (mInFeedbackDialog) { 474 Toast.makeText(latinIME, R.string.research_please_exit_feedback_form, 475 Toast.LENGTH_LONG).show(); 476 return; 477 } 478 presentFeedbackDialog(latinIME); 479 } 480 481 // TODO: currently unreachable. Remove after being sure no menu is needed. 482 /* 483 public void presentResearchDialog(final LatinIME latinIME) { 484 final CharSequence title = latinIME.getString(R.string.english_ime_research_log); 485 final boolean showEnable = mIsLoggingSuspended || !sIsLogging; 486 final CharSequence[] items = new CharSequence[] { 487 latinIME.getString(R.string.research_feedback_menu_option), 488 showEnable ? latinIME.getString(R.string.research_enable_session_logging) : 489 latinIME.getString(R.string.research_do_not_log_this_session) 490 }; 491 final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 492 @Override 493 public void onClick(DialogInterface di, int position) { 494 di.dismiss(); 495 switch (position) { 496 case 0: 497 presentFeedbackDialog(latinIME); 498 break; 499 case 1: 500 enableOrDisable(showEnable, latinIME); 501 break; 502 } 503 } 504 505 }; 506 final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME) 507 .setItems(items, listener) 508 .setTitle(title); 509 latinIME.showOptionDialog(builder.create()); 510 } 511 */ 512 513 private boolean mInFeedbackDialog = false; 514 public void presentFeedbackDialog(LatinIME latinIME) { 515 mInFeedbackDialog = true; 516 latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class); 517 } 518 519 // TODO: currently unreachable. Remove after being sure enable/disable is 520 // not needed. 521 /* 522 public void enableOrDisable(final boolean showEnable, final LatinIME latinIME) { 523 if (showEnable) { 524 if (!sIsLogging) { 525 setLoggingAllowed(true); 526 } 527 resumeLogging(); 528 Toast.makeText(latinIME, 529 R.string.research_notify_session_logging_enabled, 530 Toast.LENGTH_LONG).show(); 531 } else { 532 Toast toast = Toast.makeText(latinIME, 533 R.string.research_notify_session_log_deleting, 534 Toast.LENGTH_LONG); 535 toast.show(); 536 boolean isLogDeleted = abort(); 537 final long currentTime = System.currentTimeMillis(); 538 final long resumeTime = currentTime + 1000 * 60 * 539 SUSPEND_DURATION_IN_MINUTES; 540 suspendLoggingUntil(resumeTime); 541 toast.cancel(); 542 Toast.makeText(latinIME, R.string.research_notify_logging_suspended, 543 Toast.LENGTH_LONG).show(); 544 } 545 } 546 */ 547 548 static class LogStatement { 549 final String mName; 550 551 // mIsPotentiallyPrivate indicates that event contains potentially private information. If 552 // the word that this event is a part of is determined to be privacy-sensitive, then this 553 // event should not be included in the output log. The system waits to output until the 554 // containing word is known. 555 final boolean mIsPotentiallyPrivate; 556 557 // mIsPotentiallyRevealing indicates that this statement may disclose details about other 558 // words typed in other LogUnits. This can happen if the user is not inserting spaces, and 559 // data from Suggestions and/or Composing text reveals the entire "megaword". For example, 560 // say the user is typing "for the win", and the system wants to record the bigram "the 561 // win". If the user types "forthe", omitting the space, the system will give "for the" as 562 // a suggestion. If the user accepts the autocorrection, the suggestion for "for the" is 563 // included in the log for the word "the", disclosing that the previous word had been "for". 564 // For now, we simply do not include this data when logging part of a "megaword". 565 final boolean mIsPotentiallyRevealing; 566 567 // mKeys stores the names that are the attributes in the output json objects 568 final String[] mKeys; 569 private static final String[] NULL_KEYS = new String[0]; 570 571 LogStatement(final String name, final boolean isPotentiallyPrivate, 572 final boolean isPotentiallyRevealing, final String... keys) { 573 mName = name; 574 mIsPotentiallyPrivate = isPotentiallyPrivate; 575 mIsPotentiallyRevealing = isPotentiallyRevealing; 576 mKeys = (keys == null) ? NULL_KEYS : keys; 577 } 578 } 579 580 private static final LogStatement LOGSTATEMENT_FEEDBACK = 581 new LogStatement("UserTimestamp", false, false, "contents"); 582 public void sendFeedback(final String feedbackContents, final boolean includeHistory) { 583 if (mFeedbackLogBuffer == null) { 584 return; 585 } 586 if (includeHistory) { 587 commitCurrentLogUnit(); 588 } else { 589 mFeedbackLogBuffer.clear(); 590 } 591 final LogUnit feedbackLogUnit = new LogUnit(); 592 feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(), 593 feedbackContents); 594 mFeedbackLogBuffer.shiftIn(feedbackLogUnit); 595 publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */); 596 mFeedbackLog.close(new Runnable() { 597 @Override 598 public void run() { 599 uploadNow(); 600 } 601 }); 602 mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); 603 } 604 605 public void uploadNow() { 606 if (DEBUG) { 607 Log.d(TAG, "calling uploadNow()"); 608 } 609 mLatinIME.startService(mUploadIntent); 610 } 611 612 public void onLeavingSendFeedbackDialog() { 613 mInFeedbackDialog = false; 614 } 615 616 public void initSuggest(Suggest suggest) { 617 mSuggest = suggest; 618 if (mMainLogBuffer != null) { 619 mMainLogBuffer.setSuggest(mSuggest); 620 } 621 } 622 623 private Dictionary getDictionary() { 624 if (mSuggest == null) { 625 return null; 626 } 627 return mSuggest.getMainDictionary(); 628 } 629 630 private void setIsPasswordView(boolean isPasswordView) { 631 mIsPasswordView = isPasswordView; 632 } 633 634 private boolean isAllowedToLog() { 635 if (DEBUG) { 636 Log.d(TAG, "iatl: " + 637 "mipw=" + mIsPasswordView + 638 ", mils=" + mIsLoggingSuspended + 639 ", sil=" + sIsLogging + 640 ", mInFeedbackDialog=" + mInFeedbackDialog); 641 } 642 return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog; 643 } 644 645 public void requestIndicatorRedraw() { 646 if (!IS_SHOWING_INDICATOR) { 647 return; 648 } 649 if (mMainKeyboardView == null) { 650 return; 651 } 652 mMainKeyboardView.invalidateAllKeys(); 653 } 654 655 public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width, 656 int height) { 657 // TODO: Reimplement using a keyboard background image specific to the ResearchLogger 658 // and remove this method. 659 // The check for MainKeyboardView ensures that a red border is only placed around 660 // the main keyboard, not every keyboard. 661 if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) { 662 final int savedColor = paint.getColor(); 663 paint.setColor(Color.RED); 664 final Style savedStyle = paint.getStyle(); 665 paint.setStyle(Style.STROKE); 666 final float savedStrokeWidth = paint.getStrokeWidth(); 667 if (IS_SHOWING_INDICATOR_CLEARLY) { 668 paint.setStrokeWidth(5); 669 canvas.drawLine(0, 0, 0, height, paint); 670 canvas.drawLine(width, 0, width, height, paint); 671 } else { 672 // Put a tiny red dot on the screen so a knowledgeable user can check whether 673 // it is enabled. The dot is actually a zero-width, zero-height rectangle, 674 // placed at the lower-right corner of the canvas, painted with a non-zero border 675 // width. 676 paint.setStrokeWidth(3); 677 canvas.drawRect(width, height, width, height, paint); 678 } 679 paint.setColor(savedColor); 680 paint.setStyle(savedStyle); 681 paint.setStrokeWidth(savedStrokeWidth); 682 } 683 } 684 685 /** 686 * Buffer a research log event, flagging it as privacy-sensitive. 687 */ 688 private synchronized void enqueueEvent(final LogStatement logStatement, 689 final Object... values) { 690 enqueueEvent(mCurrentLogUnit, logStatement, values); 691 } 692 693 private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement, 694 final Object... values) { 695 assert values.length == logStatement.mKeys.length; 696 if (isAllowedToLog() && logUnit != null) { 697 final long time = SystemClock.uptimeMillis(); 698 logUnit.addLogStatement(logStatement, time, values); 699 } 700 } 701 702 private void setCurrentLogUnitContainsDigitFlag() { 703 mCurrentLogUnit.setMayContainDigit(); 704 } 705 706 /* package for test */ void commitCurrentLogUnit() { 707 if (DEBUG) { 708 Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasWord() ? 709 ": " + mCurrentLogUnit.getWord() : "")); 710 } 711 if (!mCurrentLogUnit.isEmpty()) { 712 if (mMainLogBuffer != null) { 713 if ((mMainLogBuffer.isNGramSafe() || IS_LOGGING_EVERYTHING) && 714 mMainLogBuffer.isNGramComplete() && 715 mMainResearchLog != null) { 716 publishLogBuffer(mMainLogBuffer, mMainResearchLog, 717 true /* isIncludingPrivateData */); 718 mMainLogBuffer.resetWordCounter(); 719 } 720 mMainLogBuffer.shiftIn(mCurrentLogUnit); 721 } 722 if (mFeedbackLogBuffer != null) { 723 mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); 724 } 725 mCurrentLogUnit = new LogUnit(); 726 } else { 727 if (DEBUG) { 728 Log.d(TAG, "Warning: tried to commit empty log unit."); 729 } 730 } 731 } 732 733 public void uncommitCurrentLogUnit(final String expectedWord, 734 final boolean dumpCurrentLogUnit) { 735 // The user has deleted this word and returned to the previous. Check that the word in the 736 // logUnit matches the expected word. If so, restore the last log unit committed to be the 737 // current logUnit. I.e., pull out the last LogUnit from all the LogBuffers, and make 738 // restore it to mCurrentLogUnit so the new edits are captured with the word. Optionally 739 // dump the contents of mCurrentLogUnit (useful if they contain deletions of the next word 740 // that should not be reported to protect user privacy) 741 // 742 // Note that we don't use mLastLogUnit here, because it only goes one word back and is only 743 // needed for reverts, which only happen one back. 744 if (mMainLogBuffer == null) { 745 return; 746 } 747 final LogUnit oldLogUnit = mMainLogBuffer.peekLastLogUnit(); 748 749 // Check that expected word matches. 750 if (oldLogUnit != null) { 751 final String oldLogUnitWord = oldLogUnit.getWord(); 752 if (!oldLogUnitWord.equals(expectedWord)) { 753 return; 754 } 755 } 756 757 // Uncommit, merging if necessary. 758 mMainLogBuffer.unshiftIn(); 759 if (oldLogUnit != null && !dumpCurrentLogUnit) { 760 oldLogUnit.append(mCurrentLogUnit); 761 mSavedDownEventTime = Long.MAX_VALUE; 762 } 763 if (oldLogUnit == null) { 764 mCurrentLogUnit = new LogUnit(); 765 } else { 766 mCurrentLogUnit = oldLogUnit; 767 } 768 if (mFeedbackLogBuffer != null) { 769 mFeedbackLogBuffer.unshiftIn(); 770 } 771 if (DEBUG) { 772 Log.d(TAG, "uncommitCurrentLogUnit (dump=" + dumpCurrentLogUnit + ") back to " 773 + (mCurrentLogUnit.hasWord() ? ": '" + mCurrentLogUnit.getWord() + "'" : "")); 774 } 775 } 776 777 private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_OPENING = 778 new LogStatement("logSegmentStart", false, false, "isIncludingPrivateData"); 779 private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_CLOSING = 780 new LogStatement("logSegmentEnd", false, false); 781 /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, 782 final ResearchLog researchLog, final boolean isIncludingPrivateData) { 783 final LogUnit openingLogUnit = new LogUnit(); 784 if (logBuffer.isEmpty()) return; 785 openingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_OPENING, SystemClock.uptimeMillis(), 786 isIncludingPrivateData); 787 researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */); 788 LogUnit logUnit; 789 int numWordsToPublish = MainLogBuffer.N_GRAM_SIZE; 790 while ((logUnit = logBuffer.shiftOut()) != null && numWordsToPublish > 0) { 791 if (DEBUG) { 792 Log.d(TAG, "publishLogBuffer: " + (logUnit.hasWord() ? logUnit.getWord() 793 : "<wordless>")); 794 } 795 researchLog.publish(logUnit, isIncludingPrivateData); 796 if (logUnit.getWord() != null) { 797 numWordsToPublish--; 798 } 799 } 800 final LogUnit closingLogUnit = new LogUnit(); 801 closingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_CLOSING, 802 SystemClock.uptimeMillis()); 803 researchLog.publish(closingLogUnit, true /* isIncludingPrivateData */); 804 } 805 806 public static boolean hasLetters(final String word) { 807 final int length = word.length(); 808 for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { 809 final int codePoint = word.codePointAt(i); 810 if (Character.isLetter(codePoint)) { 811 return true; 812 } 813 } 814 return false; 815 } 816 817 /** 818 * Commit the portion of mCurrentLogUnit before maxTime as a worded logUnit. 819 * 820 * After this operation completes, mCurrentLogUnit will hold any logStatements that happened 821 * after maxTime. 822 */ 823 /* package for test */ void commitCurrentLogUnitAsWord(final String word, final long maxTime, 824 final boolean isBatchMode) { 825 if (word == null) { 826 return; 827 } 828 final Dictionary dictionary = getDictionary(); 829 if (word.length() > 0 && hasLetters(word)) { 830 mCurrentLogUnit.setWord(word); 831 final boolean isDictionaryWord = dictionary != null 832 && dictionary.isValidWord(word); 833 mStatistics.recordWordEntered(isDictionaryWord); 834 } 835 final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime); 836 enqueueCommitText(word, isBatchMode); 837 commitCurrentLogUnit(); 838 mCurrentLogUnit = newLogUnit; 839 } 840 841 public void onWordFinished(final String word, final boolean isBatchMode) { 842 commitCurrentLogUnitAsWord(word, mSavedDownEventTime, isBatchMode); 843 mSavedDownEventTime = Long.MAX_VALUE; 844 } 845 846 private static int scrubDigitFromCodePoint(int codePoint) { 847 return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint; 848 } 849 850 /* package for test */ static String scrubDigitsFromString(String s) { 851 StringBuilder sb = null; 852 final int length = s.length(); 853 for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) { 854 final int codePoint = Character.codePointAt(s, i); 855 if (Character.isDigit(codePoint)) { 856 if (sb == null) { 857 sb = new StringBuilder(length); 858 sb.append(s.substring(0, i)); 859 } 860 sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT); 861 } else { 862 if (sb != null) { 863 sb.appendCodePoint(codePoint); 864 } 865 } 866 } 867 if (sb == null) { 868 return s; 869 } else { 870 return sb.toString(); 871 } 872 } 873 874 private static String getUUID(final SharedPreferences prefs) { 875 String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null); 876 if (null == uuidString) { 877 UUID uuid = UUID.randomUUID(); 878 uuidString = uuid.toString(); 879 Editor editor = prefs.edit(); 880 editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString); 881 editor.apply(); 882 } 883 return uuidString; 884 } 885 886 private String scrubWord(String word) { 887 final Dictionary dictionary = getDictionary(); 888 if (dictionary == null) { 889 return WORD_REPLACEMENT_STRING; 890 } 891 if (dictionary.isValidWord(word)) { 892 return word; 893 } 894 return WORD_REPLACEMENT_STRING; 895 } 896 897 // Specific logging methods follow below. The comments for each logging method should 898 // indicate what specific method is logged, and how to trigger it from the user interface. 899 // 900 // Logging methods can be generally classified into two flavors, "UserAction", which should 901 // correspond closely to an event that is sensed by the IME, and is usually generated 902 // directly by the user, and "SystemResponse" which corresponds to an event that the IME 903 // generates, often after much processing of user input. SystemResponses should correspond 904 // closely to user-visible events. 905 // TODO: Consider exposing the UserAction classification in the log output. 906 907 /** 908 * Log a call to LatinIME.onStartInputViewInternal(). 909 * 910 * UserAction: called each time the keyboard is opened up. 911 */ 912 private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL = 913 new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid", 914 "packageName", "inputType", "imeOptions", "fieldId", "display", "model", 915 "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything", 916 "isExperimentalDebug"); 917 public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, 918 final SharedPreferences prefs) { 919 final ResearchLogger researchLogger = getInstance(); 920 if (editorInfo != null) { 921 final boolean isPassword = InputTypeUtils.isPasswordInputType(editorInfo.inputType) 922 || InputTypeUtils.isVisiblePasswordInputType(editorInfo.inputType); 923 getInstance().setIsPasswordView(isPassword); 924 researchLogger.start(); 925 final Context context = researchLogger.mLatinIME; 926 try { 927 final PackageInfo packageInfo; 928 packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 929 0); 930 final Integer versionCode = packageInfo.versionCode; 931 final String versionName = packageInfo.versionName; 932 researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL, 933 researchLogger.mUUIDString, editorInfo.packageName, 934 Integer.toHexString(editorInfo.inputType), 935 Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, 936 Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, 937 OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING, 938 ProductionFlag.IS_EXPERIMENTAL_DEBUG); 939 } catch (NameNotFoundException e) { 940 e.printStackTrace(); 941 } 942 } 943 } 944 945 public void latinIME_onFinishInputViewInternal() { 946 logStatistics(); 947 stop(); 948 } 949 950 /** 951 * Log a change in preferences. 952 * 953 * UserAction: called when the user changes the settings. 954 */ 955 private static final LogStatement LOGSTATEMENT_PREFS_CHANGED = 956 new LogStatement("PrefsChanged", false, false, "prefs"); 957 public static void prefsChanged(final SharedPreferences prefs) { 958 final ResearchLogger researchLogger = getInstance(); 959 researchLogger.enqueueEvent(LOGSTATEMENT_PREFS_CHANGED, prefs); 960 } 961 962 /** 963 * Log a call to MainKeyboardView.processMotionEvent(). 964 * 965 * UserAction: called when the user puts their finger onto the screen (ACTION_DOWN). 966 * 967 */ 968 private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT = 969 new LogStatement("MotionEvent", true, false, "action", "MotionEvent"); 970 public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action, 971 final long eventTime, final int index, final int id, final int x, final int y) { 972 if (me != null) { 973 final String actionString; 974 switch (action) { 975 case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break; 976 case MotionEvent.ACTION_UP: actionString = "UP"; break; 977 case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break; 978 case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break; 979 case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break; 980 case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break; 981 case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break; 982 default: actionString = "ACTION_" + action; break; 983 } 984 final ResearchLogger researchLogger = getInstance(); 985 researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT, 986 actionString, MotionEvent.obtain(me)); 987 if (action == MotionEvent.ACTION_DOWN) { 988 // Subtract 1 from eventTime so the down event is included in the later 989 // LogUnit, not the earlier (the test is for inequality). 990 researchLogger.mSavedDownEventTime = eventTime - 1; 991 } 992 } 993 } 994 995 /** 996 * Log a call to LatinIME.onCodeInput(). 997 * 998 * SystemResponse: The main processing step for entering text. Called when the user performs a 999 * tap, a flick, a long press, releases a gesture, or taps a punctuation suggestion. 1000 */ 1001 private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT = 1002 new LogStatement("LatinImeOnCodeInput", true, false, "code", "x", "y"); 1003 public static void latinIME_onCodeInput(final int code, final int x, final int y) { 1004 final long time = SystemClock.uptimeMillis(); 1005 final ResearchLogger researchLogger = getInstance(); 1006 researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT, 1007 Constants.printableCode(scrubDigitFromCodePoint(code)), x, y); 1008 if (Character.isDigit(code)) { 1009 researchLogger.setCurrentLogUnitContainsDigitFlag(); 1010 } 1011 researchLogger.mStatistics.recordChar(code, time); 1012 } 1013 /** 1014 * Log a call to LatinIME.onDisplayCompletions(). 1015 * 1016 * SystemResponse: The IME has displayed application-specific completions. They may show up 1017 * in the suggestion strip, such as a landscape phone. 1018 */ 1019 private static final LogStatement LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS = 1020 new LogStatement("LatinIMEOnDisplayCompletions", true, true, 1021 "applicationSpecifiedCompletions"); 1022 public static void latinIME_onDisplayCompletions( 1023 final CompletionInfo[] applicationSpecifiedCompletions) { 1024 // Note; passing an array as a single element in a vararg list. Must create a new 1025 // dummy array around it or it will get expanded. 1026 getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS, 1027 new Object[] { applicationSpecifiedCompletions }); 1028 } 1029 1030 public static boolean getAndClearLatinIMEExpectingUpdateSelection() { 1031 boolean returnValue = sLatinIMEExpectingUpdateSelection; 1032 sLatinIMEExpectingUpdateSelection = false; 1033 return returnValue; 1034 } 1035 1036 /** 1037 * Log a call to LatinIME.onWindowHidden(). 1038 * 1039 * UserAction: The user has performed an action that has caused the IME to be closed. They may 1040 * have focused on something other than a text field, or explicitly closed it. 1041 */ 1042 private static final LogStatement LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN = 1043 new LogStatement("LatinIMEOnWindowHidden", false, false, "isTextTruncated", "text"); 1044 public static void latinIME_onWindowHidden(final int savedSelectionStart, 1045 final int savedSelectionEnd, final InputConnection ic) { 1046 if (ic != null) { 1047 final boolean isTextTruncated; 1048 final String text; 1049 if (LOG_FULL_TEXTVIEW_CONTENTS) { 1050 // Capture the TextView contents. This will trigger onUpdateSelection(), so we 1051 // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called, 1052 // it can tell that it was generated by the logging code, and not by the user, and 1053 // therefore keep user-visible state as is. 1054 ic.beginBatchEdit(); 1055 ic.performContextMenuAction(android.R.id.selectAll); 1056 CharSequence charSequence = ic.getSelectedText(0); 1057 if (savedSelectionStart != -1 && savedSelectionEnd != -1) { 1058 ic.setSelection(savedSelectionStart, savedSelectionEnd); 1059 } 1060 ic.endBatchEdit(); 1061 sLatinIMEExpectingUpdateSelection = true; 1062 if (TextUtils.isEmpty(charSequence)) { 1063 isTextTruncated = false; 1064 text = ""; 1065 } else { 1066 if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) { 1067 int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE; 1068 // do not cut in the middle of a supplementary character 1069 final char c = charSequence.charAt(length - 1); 1070 if (Character.isHighSurrogate(c)) { 1071 length--; 1072 } 1073 final CharSequence truncatedCharSequence = charSequence.subSequence(0, 1074 length); 1075 isTextTruncated = true; 1076 text = truncatedCharSequence.toString(); 1077 } else { 1078 isTextTruncated = false; 1079 text = charSequence.toString(); 1080 } 1081 } 1082 } else { 1083 isTextTruncated = true; 1084 text = ""; 1085 } 1086 final ResearchLogger researchLogger = getInstance(); 1087 // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g. 1088 // during a live user test), so the normal isPotentiallyPrivate and 1089 // isPotentiallyRevealing flags do not apply 1090 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN, isTextTruncated, 1091 text); 1092 researchLogger.commitCurrentLogUnit(); 1093 getInstance().stop(); 1094 } 1095 } 1096 1097 /** 1098 * Log a call to LatinIME.onUpdateSelection(). 1099 * 1100 * UserAction/SystemResponse: The user has moved the cursor or selection. This function may 1101 * be called, however, when the system has moved the cursor, say by inserting a character. 1102 */ 1103 private static final LogStatement LOGSTATEMENT_LATINIME_ONUPDATESELECTION = 1104 new LogStatement("LatinIMEOnUpdateSelection", true, false, "lastSelectionStart", 1105 "lastSelectionEnd", "oldSelStart", "oldSelEnd", "newSelStart", "newSelEnd", 1106 "composingSpanStart", "composingSpanEnd", "expectingUpdateSelection", 1107 "expectingUpdateSelectionFromLogger", "context"); 1108 public static void latinIME_onUpdateSelection(final int lastSelectionStart, 1109 final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd, 1110 final int newSelStart, final int newSelEnd, final int composingSpanStart, 1111 final int composingSpanEnd, final boolean expectingUpdateSelection, 1112 final boolean expectingUpdateSelectionFromLogger, 1113 final RichInputConnection connection) { 1114 String word = ""; 1115 if (connection != null) { 1116 Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1); 1117 if (range != null) { 1118 word = range.mWord; 1119 } 1120 } 1121 final ResearchLogger researchLogger = getInstance(); 1122 final String scrubbedWord = researchLogger.scrubWord(word); 1123 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONUPDATESELECTION, lastSelectionStart, 1124 lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd, 1125 composingSpanStart, composingSpanEnd, expectingUpdateSelection, 1126 expectingUpdateSelectionFromLogger, scrubbedWord); 1127 } 1128 1129 /** 1130 * Log a call to LatinIME.onTextInput(). 1131 * 1132 * SystemResponse: Raw text is added to the TextView. 1133 */ 1134 public static void latinIME_onTextInput(final String text, final boolean isBatchMode) { 1135 final ResearchLogger researchLogger = getInstance(); 1136 researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode); 1137 } 1138 1139 /** 1140 * Log a call to LatinIME.pickSuggestionManually(). 1141 * 1142 * UserAction: The user has chosen a specific word from the suggestion strip. 1143 */ 1144 private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY = 1145 new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index", 1146 "suggestion", "x", "y"); 1147 public static void latinIME_pickSuggestionManually(final String replacedWord, 1148 final int index, final String suggestion, final boolean isBatchMode) { 1149 final String scrubbedWord = scrubDigitsFromString(suggestion); 1150 final ResearchLogger researchLogger = getInstance(); 1151 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY, 1152 scrubDigitsFromString(replacedWord), index, 1153 suggestion == null ? null : scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE, 1154 Constants.SUGGESTION_STRIP_COORDINATE); 1155 researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode); 1156 researchLogger.mStatistics.recordManualSuggestion(); 1157 } 1158 1159 /** 1160 * Log a call to LatinIME.punctuationSuggestion(). 1161 * 1162 * UserAction: The user has chosen punctuation from the punctuation suggestion strip. 1163 */ 1164 private static final LogStatement LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION = 1165 new LogStatement("LatinIMEPunctuationSuggestion", false, false, "index", "suggestion", 1166 "x", "y", "isPrediction"); 1167 public static void latinIME_punctuationSuggestion(final int index, final String suggestion, 1168 final boolean isBatchMode, final boolean isPrediction) { 1169 final ResearchLogger researchLogger = getInstance(); 1170 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion, 1171 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, 1172 isPrediction); 1173 researchLogger.commitCurrentLogUnitAsWord(suggestion, Long.MAX_VALUE, isBatchMode); 1174 } 1175 1176 /** 1177 * Log a call to LatinIME.sendKeyCodePoint(). 1178 * 1179 * SystemResponse: The IME is simulating a hardware keypress. This happens for numbers; other 1180 * input typically goes through RichInputConnection.setComposingText() and 1181 * RichInputConnection.commitText(). 1182 */ 1183 private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT = 1184 new LogStatement("LatinIMESendKeyCodePoint", true, false, "code"); 1185 public static void latinIME_sendKeyCodePoint(final int code) { 1186 final ResearchLogger researchLogger = getInstance(); 1187 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT, 1188 Constants.printableCode(scrubDigitFromCodePoint(code))); 1189 if (Character.isDigit(code)) { 1190 researchLogger.setCurrentLogUnitContainsDigitFlag(); 1191 } 1192 } 1193 1194 /** 1195 * Log a call to LatinIME.swapSwapperAndSpace(). 1196 * 1197 * SystemResponse: A symbol has been swapped with a space character. E.g. punctuation may swap 1198 * if a soft space is inserted after a word. 1199 */ 1200 private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE = 1201 new LogStatement("LatinIMESwapSwapperAndSpace", false, false, "originalCharacters", 1202 "charactersAfterSwap"); 1203 public static void latinIME_swapSwapperAndSpace(final CharSequence originalCharacters, 1204 final String charactersAfterSwap) { 1205 final ResearchLogger researchLogger = getInstance(); 1206 final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); 1207 if (logUnit != null) { 1208 researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE, 1209 originalCharacters, charactersAfterSwap); 1210 } 1211 } 1212 1213 /** 1214 * Log a call to LatinIME.maybeDoubleSpacePeriod(). 1215 * 1216 * SystemResponse: Two spaces have been replaced by period space. 1217 */ 1218 public static void latinIME_maybeDoubleSpacePeriod(final String text, 1219 final boolean isBatchMode) { 1220 final ResearchLogger researchLogger = getInstance(); 1221 researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode); 1222 } 1223 1224 /** 1225 * Log a call to MainKeyboardView.onLongPress(). 1226 * 1227 * UserAction: The user has performed a long-press on a key. 1228 */ 1229 private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS = 1230 new LogStatement("MainKeyboardViewOnLongPress", false, false); 1231 public static void mainKeyboardView_onLongPress() { 1232 getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS); 1233 } 1234 1235 /** 1236 * Log a call to MainKeyboardView.setKeyboard(). 1237 * 1238 * SystemResponse: The IME has switched to a new keyboard (e.g. French, English). 1239 * This is typically called right after LatinIME.onStartInputViewInternal (when starting a new 1240 * IME), but may happen at other times if the user explicitly requests a keyboard change. 1241 */ 1242 private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD = 1243 new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale", 1244 "orientation", "width", "modeName", "action", "navigateNext", 1245 "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled", 1246 "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th", 1247 "keys"); 1248 public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) { 1249 final KeyboardId kid = keyboard.mId; 1250 final boolean isPasswordView = kid.passwordInput(); 1251 final ResearchLogger researchLogger = getInstance(); 1252 researchLogger.setIsPasswordView(isPasswordView); 1253 researchLogger.enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD, 1254 KeyboardId.elementIdToName(kid.mElementId), 1255 kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), 1256 kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(), 1257 kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey, 1258 isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey, 1259 kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth, 1260 keyboard.mOccupiedHeight, keyboard.mKeys); 1261 } 1262 1263 /** 1264 * Log a call to LatinIME.revertCommit(). 1265 * 1266 * SystemResponse: The IME has reverted commited text. This happens when the user enters 1267 * a word, commits it by pressing space or punctuation, and then reverts the commit by hitting 1268 * backspace. 1269 */ 1270 private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT = 1271 new LogStatement("LatinIMERevertCommit", true, false, "committedWord", 1272 "originallyTypedWord"); 1273 public static void latinIME_revertCommit(final String committedWord, 1274 final String originallyTypedWord, final boolean isBatchMode) { 1275 final ResearchLogger researchLogger = getInstance(); 1276 // TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word. 1277 final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); 1278 if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) { 1279 if (logUnit != null) { 1280 logUnit.setWord(originallyTypedWord); 1281 } 1282 } 1283 researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit, 1284 LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, originallyTypedWord); 1285 researchLogger.mStatistics.recordRevertCommit(); 1286 researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE, isBatchMode); 1287 } 1288 1289 /** 1290 * Log a call to PointerTracker.callListenerOnCancelInput(). 1291 * 1292 * UserAction: The user has canceled the input, e.g., by pressing down, but then removing 1293 * outside the keyboard area. 1294 * TODO: Verify 1295 */ 1296 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT = 1297 new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false); 1298 public static void pointerTracker_callListenerOnCancelInput() { 1299 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT); 1300 } 1301 1302 /** 1303 * Log a call to PointerTracker.callListenerOnCodeInput(). 1304 * 1305 * SystemResponse: The user has entered a key through the normal tapping mechanism. 1306 * LatinIME.onCodeInput will also be called. 1307 */ 1308 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT = 1309 new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "code", 1310 "outputText", "x", "y", "ignoreModifierKey", "altersCode", "isEnabled"); 1311 public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x, 1312 final int y, final boolean ignoreModifierKey, final boolean altersCode, 1313 final int code) { 1314 if (key != null) { 1315 String outputText = key.getOutputText(); 1316 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT, 1317 Constants.printableCode(scrubDigitFromCodePoint(code)), 1318 outputText == null ? null : scrubDigitsFromString(outputText.toString()), 1319 x, y, ignoreModifierKey, altersCode, key.isEnabled()); 1320 } 1321 } 1322 1323 /** 1324 * Log a call to PointerTracker.callListenerCallListenerOnRelease(). 1325 * 1326 * UserAction: The user has released their finger or thumb from the screen. 1327 */ 1328 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE = 1329 new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code", 1330 "withSliding", "ignoreModifierKey", "isEnabled"); 1331 public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode, 1332 final boolean withSliding, final boolean ignoreModifierKey) { 1333 if (key != null) { 1334 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE, 1335 Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding, 1336 ignoreModifierKey, key.isEnabled()); 1337 } 1338 } 1339 1340 /** 1341 * Log a call to PointerTracker.onDownEvent(). 1342 * 1343 * UserAction: The user has pressed down on a key. 1344 * TODO: Differentiate with LatinIME.processMotionEvent. 1345 */ 1346 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT = 1347 new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared"); 1348 public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) { 1349 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT, deltaT, 1350 distanceSquared); 1351 } 1352 1353 /** 1354 * Log a call to PointerTracker.onMoveEvent(). 1355 * 1356 * UserAction: The user has moved their finger while pressing on the screen. 1357 * TODO: Differentiate with LatinIME.processMotionEvent(). 1358 */ 1359 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT = 1360 new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY"); 1361 public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX, 1362 final int lastY) { 1363 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY); 1364 } 1365 1366 /** 1367 * Log a call to RichInputConnection.commitCompletion(). 1368 * 1369 * SystemResponse: The IME has committed a completion. A completion is an application- 1370 * specific suggestion that is presented in a pop-up menu in the TextView. 1371 */ 1372 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION = 1373 new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo"); 1374 public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) { 1375 final ResearchLogger researchLogger = getInstance(); 1376 researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION, 1377 completionInfo); 1378 } 1379 1380 /** 1381 * Log a call to RichInputConnection.revertDoubleSpacePeriod(). 1382 * 1383 * SystemResponse: The IME has reverted ". ", which had previously replaced two typed spaces. 1384 */ 1385 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD = 1386 new LogStatement("RichInputConnectionRevertDoubleSpacePeriod", false, false); 1387 public static void richInputConnection_revertDoubleSpacePeriod() { 1388 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD); 1389 } 1390 1391 /** 1392 * Log a call to RichInputConnection.revertSwapPunctuation(). 1393 * 1394 * SystemResponse: The IME has reverted a punctuation swap. 1395 */ 1396 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION = 1397 new LogStatement("RichInputConnectionRevertSwapPunctuation", false, false); 1398 public static void richInputConnection_revertSwapPunctuation() { 1399 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION); 1400 } 1401 1402 /** 1403 * Log a call to LatinIME.commitCurrentAutoCorrection(). 1404 * 1405 * SystemResponse: The IME has committed an auto-correction. An auto-correction changes the raw 1406 * text input to another word that the user more likely desired to type. 1407 */ 1408 private static final LogStatement LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION = 1409 new LogStatement("LatinIMECommitCurrentAutoCorrection", true, true, "typedWord", 1410 "autoCorrection", "separatorString"); 1411 public static void latinIme_commitCurrentAutoCorrection(final String typedWord, 1412 final String autoCorrection, final String separatorString, final boolean isBatchMode) { 1413 final String scrubbedTypedWord = scrubDigitsFromString(typedWord); 1414 final String scrubbedAutoCorrection = scrubDigitsFromString(autoCorrection); 1415 final ResearchLogger researchLogger = getInstance(); 1416 researchLogger.commitCurrentLogUnitAsWord(scrubbedAutoCorrection, Long.MAX_VALUE, 1417 isBatchMode); 1418 1419 // Add the autocorrection logStatement at the end of the logUnit for the committed word. 1420 // We have to do this after calling commitCurrentLogUnitAsWord, because it may split the 1421 // current logUnit, and then we have to peek to get the logUnit reference back. 1422 final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); 1423 // TODO: Add test to confirm that the commitCurrentAutoCorrection log statement should 1424 // always be added to logUnit (if non-null) and not mCurrentLogUnit. 1425 researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION, 1426 scrubbedTypedWord, scrubbedAutoCorrection, separatorString); 1427 } 1428 1429 private boolean isExpectingCommitText = false; 1430 /** 1431 * Log a call to RichInputConnection.commitPartialText 1432 * 1433 * SystemResponse: The IME is committing part of a word. This happens if a space is 1434 * automatically inserted to split a single typed string into two or more words. 1435 */ 1436 // TODO: This method is currently unused. Find where it should be called from in the IME and 1437 // add invocations. 1438 private static final LogStatement LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT = 1439 new LogStatement("LatinIMECommitPartialText", true, false, "newCursorPosition"); 1440 public static void latinIME_commitPartialText(final String committedWord, 1441 final long lastTimestampOfWordData, final boolean isBatchMode) { 1442 final ResearchLogger researchLogger = getInstance(); 1443 final String scrubbedWord = scrubDigitsFromString(committedWord); 1444 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT); 1445 researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData, 1446 isBatchMode); 1447 } 1448 1449 /** 1450 * Log a call to RichInputConnection.commitText(). 1451 * 1452 * SystemResponse: The IME is committing text. This happens after the user has typed a word 1453 * and then a space or punctuation key. 1454 */ 1455 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT = 1456 new LogStatement("RichInputConnectionCommitText", true, false, "newCursorPosition"); 1457 public static void richInputConnection_commitText(final String committedWord, 1458 final int newCursorPosition, final boolean isBatchMode) { 1459 final ResearchLogger researchLogger = getInstance(); 1460 final String scrubbedWord = scrubDigitsFromString(committedWord); 1461 if (!researchLogger.isExpectingCommitText) { 1462 researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT, 1463 newCursorPosition); 1464 researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode); 1465 } 1466 researchLogger.isExpectingCommitText = false; 1467 } 1468 1469 /** 1470 * Shared event for logging committed text. 1471 */ 1472 private static final LogStatement LOGSTATEMENT_COMMITTEXT = 1473 new LogStatement("CommitText", true, false, "committedText", "isBatchMode"); 1474 private void enqueueCommitText(final String word, final boolean isBatchMode) { 1475 enqueueEvent(LOGSTATEMENT_COMMITTEXT, word, isBatchMode); 1476 } 1477 1478 /** 1479 * Log a call to RichInputConnection.deleteSurroundingText(). 1480 * 1481 * SystemResponse: The IME has deleted text. 1482 */ 1483 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = 1484 new LogStatement("RichInputConnectionDeleteSurroundingText", true, false, 1485 "beforeLength", "afterLength"); 1486 public static void richInputConnection_deleteSurroundingText(final int beforeLength, 1487 final int afterLength) { 1488 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, 1489 beforeLength, afterLength); 1490 } 1491 1492 /** 1493 * Log a call to RichInputConnection.finishComposingText(). 1494 * 1495 * SystemResponse: The IME has left the composing text as-is. 1496 */ 1497 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = 1498 new LogStatement("RichInputConnectionFinishComposingText", false, false); 1499 public static void richInputConnection_finishComposingText() { 1500 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT); 1501 } 1502 1503 /** 1504 * Log a call to RichInputConnection.performEditorAction(). 1505 * 1506 * SystemResponse: The IME is invoking an action specific to the editor. 1507 */ 1508 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION = 1509 new LogStatement("RichInputConnectionPerformEditorAction", false, false, 1510 "imeActionId"); 1511 public static void richInputConnection_performEditorAction(final int imeActionId) { 1512 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION, 1513 imeActionId); 1514 } 1515 1516 /** 1517 * Log a call to RichInputConnection.sendKeyEvent(). 1518 * 1519 * SystemResponse: The IME is telling the TextView that a key is being pressed through an 1520 * alternate channel. 1521 * TODO: only for hardware keys? 1522 */ 1523 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT = 1524 new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action", 1525 "code"); 1526 public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) { 1527 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT, 1528 keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode()); 1529 } 1530 1531 /** 1532 * Log a call to RichInputConnection.setComposingText(). 1533 * 1534 * SystemResponse: The IME is setting the composing text. Happens each time a character is 1535 * entered. 1536 */ 1537 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = 1538 new LogStatement("RichInputConnectionSetComposingText", true, true, "text", 1539 "newCursorPosition"); 1540 public static void richInputConnection_setComposingText(final CharSequence text, 1541 final int newCursorPosition) { 1542 if (text == null) { 1543 throw new RuntimeException("setComposingText is null"); 1544 } 1545 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, text, 1546 newCursorPosition); 1547 } 1548 1549 /** 1550 * Log a call to RichInputConnection.setSelection(). 1551 * 1552 * SystemResponse: The IME is requesting that the selection change. User-initiated selection- 1553 * change requests do not go through this method -- it's only when the system wants to change 1554 * the selection. 1555 */ 1556 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION = 1557 new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to"); 1558 public static void richInputConnection_setSelection(final int from, final int to) { 1559 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to); 1560 } 1561 1562 /** 1563 * Log a call to SuddenJumpingTouchEventHandler.onTouchEvent(). 1564 * 1565 * SystemResponse: The IME has filtered input events in case of an erroneous sensor reading. 1566 */ 1567 private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = 1568 new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false, 1569 "motionEvent"); 1570 public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) { 1571 if (me != null) { 1572 getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, 1573 me.toString()); 1574 } 1575 } 1576 1577 /** 1578 * Log a call to SuggestionsView.setSuggestions(). 1579 * 1580 * SystemResponse: The IME is setting the suggestions in the suggestion strip. 1581 */ 1582 private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS = 1583 new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords"); 1584 public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) { 1585 if (suggestedWords != null) { 1586 getInstance().enqueueEvent(LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS, 1587 suggestedWords); 1588 } 1589 } 1590 1591 /** 1592 * The user has indicated a particular point in the log that is of interest. 1593 * 1594 * UserAction: From direct menu invocation. 1595 */ 1596 private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP = 1597 new LogStatement("UserTimestamp", false, false); 1598 public void userTimestamp() { 1599 getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP); 1600 } 1601 1602 /** 1603 * Log a call to LatinIME.onEndBatchInput(). 1604 * 1605 * SystemResponse: The system has completed a gesture. 1606 */ 1607 private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT = 1608 new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText", 1609 "enteredWordPos"); 1610 public static void latinIME_onEndBatchInput(final CharSequence enteredText, 1611 final int enteredWordPos) { 1612 final ResearchLogger researchLogger = getInstance(); 1613 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText, 1614 enteredWordPos); 1615 researchLogger.mStatistics.recordGestureInput(enteredText.length()); 1616 } 1617 1618 /** 1619 * Log a call to LatinIME.handleBackspace(). 1620 * 1621 * UserInput: The user is deleting a gestured word by hitting the backspace key once. 1622 */ 1623 private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH = 1624 new LogStatement("LatinIMEHandleBackspaceBatch", true, false, "deletedText"); 1625 public static void latinIME_handleBackspace_batch(final CharSequence deletedText) { 1626 final ResearchLogger researchLogger = getInstance(); 1627 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText); 1628 researchLogger.mStatistics.recordGestureDelete(); 1629 } 1630 1631 /** 1632 * Log statistics. 1633 * 1634 * ContextualData, recorded at the end of a session. 1635 */ 1636 private static final LogStatement LOGSTATEMENT_STATISTICS = 1637 new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount", 1638 "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting", 1639 "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete", 1640 "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete", 1641 "dictionaryWordCount", "splitWordsCount", "gestureInputCount", 1642 "gestureCharsCount", "gesturesDeletedCount", "manualSuggestionsCount", 1643 "revertCommitsCount"); 1644 private static void logStatistics() { 1645 final ResearchLogger researchLogger = getInstance(); 1646 final Statistics statistics = researchLogger.mStatistics; 1647 researchLogger.enqueueEvent(LOGSTATEMENT_STATISTICS, statistics.mCharCount, 1648 statistics.mLetterCount, statistics.mNumberCount, statistics.mSpaceCount, 1649 statistics.mDeleteKeyCount, statistics.mWordCount, statistics.mIsEmptyUponStarting, 1650 statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(), 1651 statistics.mBeforeDeleteKeyCounter.getAverageTime(), 1652 statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(), 1653 statistics.mAfterDeleteKeyCounter.getAverageTime(), 1654 statistics.mDictionaryWordCount, statistics.mSplitWordsCount, 1655 statistics.mGesturesInputCount, statistics.mGesturesCharsCount, 1656 statistics.mGesturesDeletedCount, statistics.mManualSuggestionsCount, 1657 statistics.mRevertCommitsCount); 1658 } 1659} 1660