ResearchLogger.java revision 4acdd3ad692affd09d4d722be689f302e94b17d0
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"); 1167 public static void latinIME_punctuationSuggestion(final int index, final String suggestion, 1168 final boolean isBatchMode) { 1169 final ResearchLogger researchLogger = getInstance(); 1170 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion, 1171 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); 1172 researchLogger.commitCurrentLogUnitAsWord(suggestion, Long.MAX_VALUE, isBatchMode); 1173 } 1174 1175 /** 1176 * Log a call to LatinIME.sendKeyCodePoint(). 1177 * 1178 * SystemResponse: The IME is simulating a hardware keypress. This happens for numbers; other 1179 * input typically goes through RichInputConnection.setComposingText() and 1180 * RichInputConnection.commitText(). 1181 */ 1182 private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT = 1183 new LogStatement("LatinIMESendKeyCodePoint", true, false, "code"); 1184 public static void latinIME_sendKeyCodePoint(final int code) { 1185 final ResearchLogger researchLogger = getInstance(); 1186 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT, 1187 Constants.printableCode(scrubDigitFromCodePoint(code))); 1188 if (Character.isDigit(code)) { 1189 researchLogger.setCurrentLogUnitContainsDigitFlag(); 1190 } 1191 } 1192 1193 /** 1194 * Log a call to LatinIME.swapSwapperAndSpace(). 1195 * 1196 * SystemResponse: A symbol has been swapped with a space character. E.g. punctuation may swap 1197 * if a soft space is inserted after a word. 1198 */ 1199 private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE = 1200 new LogStatement("LatinIMESwapSwapperAndSpace", false, false, "originalCharacters", 1201 "charactersAfterSwap"); 1202 public static void latinIME_swapSwapperAndSpace(final CharSequence originalCharacters, 1203 final String charactersAfterSwap) { 1204 final ResearchLogger researchLogger = getInstance(); 1205 final LogUnit logUnit; 1206 if (researchLogger.mMainLogBuffer == null) { 1207 logUnit = null; 1208 } else { 1209 logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); 1210 } 1211 if (logUnit != null) { 1212 researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE, 1213 originalCharacters, charactersAfterSwap); 1214 } 1215 } 1216 1217 /** 1218 * Log a call to LatinIME.maybeDoubleSpacePeriod(). 1219 * 1220 * SystemResponse: Two spaces have been replaced by period space. 1221 */ 1222 public static void latinIME_maybeDoubleSpacePeriod(final String text, 1223 final boolean isBatchMode) { 1224 final ResearchLogger researchLogger = getInstance(); 1225 researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode); 1226 } 1227 1228 /** 1229 * Log a call to MainKeyboardView.onLongPress(). 1230 * 1231 * UserAction: The user has performed a long-press on a key. 1232 */ 1233 private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS = 1234 new LogStatement("MainKeyboardViewOnLongPress", false, false); 1235 public static void mainKeyboardView_onLongPress() { 1236 getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS); 1237 } 1238 1239 /** 1240 * Log a call to MainKeyboardView.setKeyboard(). 1241 * 1242 * SystemResponse: The IME has switched to a new keyboard (e.g. French, English). 1243 * This is typically called right after LatinIME.onStartInputViewInternal (when starting a new 1244 * IME), but may happen at other times if the user explicitly requests a keyboard change. 1245 */ 1246 private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD = 1247 new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale", 1248 "orientation", "width", "modeName", "action", "navigateNext", 1249 "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled", 1250 "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th", 1251 "keys"); 1252 public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) { 1253 final KeyboardId kid = keyboard.mId; 1254 final boolean isPasswordView = kid.passwordInput(); 1255 final ResearchLogger researchLogger = getInstance(); 1256 researchLogger.setIsPasswordView(isPasswordView); 1257 researchLogger.enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD, 1258 KeyboardId.elementIdToName(kid.mElementId), 1259 kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), 1260 kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(), 1261 kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey, 1262 isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey, 1263 kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth, 1264 keyboard.mOccupiedHeight, keyboard.mKeys); 1265 } 1266 1267 /** 1268 * Log a call to LatinIME.revertCommit(). 1269 * 1270 * SystemResponse: The IME has reverted commited text. This happens when the user enters 1271 * a word, commits it by pressing space or punctuation, and then reverts the commit by hitting 1272 * backspace. 1273 */ 1274 private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT = 1275 new LogStatement("LatinIMERevertCommit", true, false, "committedWord", 1276 "originallyTypedWord"); 1277 public static void latinIME_revertCommit(final String committedWord, 1278 final String originallyTypedWord, final boolean isBatchMode) { 1279 final ResearchLogger researchLogger = getInstance(); 1280 // TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word. 1281 final LogUnit logUnit; 1282 if (researchLogger.mMainLogBuffer == null) { 1283 logUnit = null; 1284 } else { 1285 logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); 1286 } 1287 if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) { 1288 if (logUnit != null) { 1289 logUnit.setWord(originallyTypedWord); 1290 } 1291 } 1292 researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit, 1293 LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, originallyTypedWord); 1294 researchLogger.mStatistics.recordRevertCommit(); 1295 researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE, isBatchMode); 1296 } 1297 1298 /** 1299 * Log a call to PointerTracker.callListenerOnCancelInput(). 1300 * 1301 * UserAction: The user has canceled the input, e.g., by pressing down, but then removing 1302 * outside the keyboard area. 1303 * TODO: Verify 1304 */ 1305 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT = 1306 new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false); 1307 public static void pointerTracker_callListenerOnCancelInput() { 1308 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT); 1309 } 1310 1311 /** 1312 * Log a call to PointerTracker.callListenerOnCodeInput(). 1313 * 1314 * SystemResponse: The user has entered a key through the normal tapping mechanism. 1315 * LatinIME.onCodeInput will also be called. 1316 */ 1317 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT = 1318 new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "code", 1319 "outputText", "x", "y", "ignoreModifierKey", "altersCode", "isEnabled"); 1320 public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x, 1321 final int y, final boolean ignoreModifierKey, final boolean altersCode, 1322 final int code) { 1323 if (key != null) { 1324 String outputText = key.getOutputText(); 1325 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT, 1326 Constants.printableCode(scrubDigitFromCodePoint(code)), 1327 outputText == null ? null : scrubDigitsFromString(outputText.toString()), 1328 x, y, ignoreModifierKey, altersCode, key.isEnabled()); 1329 } 1330 } 1331 1332 /** 1333 * Log a call to PointerTracker.callListenerCallListenerOnRelease(). 1334 * 1335 * UserAction: The user has released their finger or thumb from the screen. 1336 */ 1337 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE = 1338 new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code", 1339 "withSliding", "ignoreModifierKey", "isEnabled"); 1340 public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode, 1341 final boolean withSliding, final boolean ignoreModifierKey) { 1342 if (key != null) { 1343 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE, 1344 Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding, 1345 ignoreModifierKey, key.isEnabled()); 1346 } 1347 } 1348 1349 /** 1350 * Log a call to PointerTracker.onDownEvent(). 1351 * 1352 * UserAction: The user has pressed down on a key. 1353 * TODO: Differentiate with LatinIME.processMotionEvent. 1354 */ 1355 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT = 1356 new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared"); 1357 public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) { 1358 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT, deltaT, 1359 distanceSquared); 1360 } 1361 1362 /** 1363 * Log a call to PointerTracker.onMoveEvent(). 1364 * 1365 * UserAction: The user has moved their finger while pressing on the screen. 1366 * TODO: Differentiate with LatinIME.processMotionEvent(). 1367 */ 1368 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT = 1369 new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY"); 1370 public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX, 1371 final int lastY) { 1372 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY); 1373 } 1374 1375 /** 1376 * Log a call to RichInputConnection.commitCompletion(). 1377 * 1378 * SystemResponse: The IME has committed a completion. A completion is an application- 1379 * specific suggestion that is presented in a pop-up menu in the TextView. 1380 */ 1381 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION = 1382 new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo"); 1383 public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) { 1384 final ResearchLogger researchLogger = getInstance(); 1385 researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION, 1386 completionInfo); 1387 } 1388 1389 /** 1390 * Log a call to RichInputConnection.revertDoubleSpacePeriod(). 1391 * 1392 * SystemResponse: The IME has reverted ". ", which had previously replaced two typed spaces. 1393 */ 1394 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD = 1395 new LogStatement("RichInputConnectionRevertDoubleSpacePeriod", false, false); 1396 public static void richInputConnection_revertDoubleSpacePeriod() { 1397 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD); 1398 } 1399 1400 /** 1401 * Log a call to RichInputConnection.revertSwapPunctuation(). 1402 * 1403 * SystemResponse: The IME has reverted a punctuation swap. 1404 */ 1405 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION = 1406 new LogStatement("RichInputConnectionRevertSwapPunctuation", false, false); 1407 public static void richInputConnection_revertSwapPunctuation() { 1408 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION); 1409 } 1410 1411 /** 1412 * Log a call to LatinIME.commitCurrentAutoCorrection(). 1413 * 1414 * SystemResponse: The IME has committed an auto-correction. An auto-correction changes the raw 1415 * text input to another word that the user more likely desired to type. 1416 */ 1417 private static final LogStatement LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION = 1418 new LogStatement("LatinIMECommitCurrentAutoCorrection", true, true, "typedWord", 1419 "autoCorrection", "separatorString"); 1420 public static void latinIme_commitCurrentAutoCorrection(final String typedWord, 1421 final String autoCorrection, final String separatorString, final boolean isBatchMode) { 1422 final String scrubbedTypedWord = scrubDigitsFromString(typedWord); 1423 final String scrubbedAutoCorrection = scrubDigitsFromString(autoCorrection); 1424 final ResearchLogger researchLogger = getInstance(); 1425 researchLogger.commitCurrentLogUnitAsWord(scrubbedAutoCorrection, Long.MAX_VALUE, 1426 isBatchMode); 1427 1428 // Add the autocorrection logStatement at the end of the logUnit for the committed word. 1429 // We have to do this after calling commitCurrentLogUnitAsWord, because it may split the 1430 // current logUnit, and then we have to peek to get the logUnit reference back. 1431 final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); 1432 // TODO: Add test to confirm that the commitCurrentAutoCorrection log statement should 1433 // always be added to logUnit (if non-null) and not mCurrentLogUnit. 1434 researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION, 1435 scrubbedTypedWord, scrubbedAutoCorrection, separatorString); 1436 } 1437 1438 private boolean isExpectingCommitText = false; 1439 /** 1440 * Log a call to RichInputConnection.commitPartialText 1441 * 1442 * SystemResponse: The IME is committing part of a word. This happens if a space is 1443 * automatically inserted to split a single typed string into two or more words. 1444 */ 1445 // TODO: This method is currently unused. Find where it should be called from in the IME and 1446 // add invocations. 1447 private static final LogStatement LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT = 1448 new LogStatement("LatinIMECommitPartialText", true, false, "newCursorPosition"); 1449 public static void latinIME_commitPartialText(final String committedWord, 1450 final long lastTimestampOfWordData, final boolean isBatchMode) { 1451 final ResearchLogger researchLogger = getInstance(); 1452 final String scrubbedWord = scrubDigitsFromString(committedWord); 1453 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT); 1454 researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData, 1455 isBatchMode); 1456 } 1457 1458 /** 1459 * Log a call to RichInputConnection.commitText(). 1460 * 1461 * SystemResponse: The IME is committing text. This happens after the user has typed a word 1462 * and then a space or punctuation key. 1463 */ 1464 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT = 1465 new LogStatement("RichInputConnectionCommitText", true, false, "newCursorPosition"); 1466 public static void richInputConnection_commitText(final String committedWord, 1467 final int newCursorPosition, final boolean isBatchMode) { 1468 final ResearchLogger researchLogger = getInstance(); 1469 final String scrubbedWord = scrubDigitsFromString(committedWord); 1470 if (!researchLogger.isExpectingCommitText) { 1471 researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT, 1472 newCursorPosition); 1473 researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode); 1474 } 1475 researchLogger.isExpectingCommitText = false; 1476 } 1477 1478 /** 1479 * Shared event for logging committed text. 1480 */ 1481 private static final LogStatement LOGSTATEMENT_COMMITTEXT = 1482 new LogStatement("CommitText", true, false, "committedText", "isBatchMode"); 1483 private void enqueueCommitText(final String word, final boolean isBatchMode) { 1484 enqueueEvent(LOGSTATEMENT_COMMITTEXT, word, isBatchMode); 1485 } 1486 1487 /** 1488 * Log a call to RichInputConnection.deleteSurroundingText(). 1489 * 1490 * SystemResponse: The IME has deleted text. 1491 */ 1492 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = 1493 new LogStatement("RichInputConnectionDeleteSurroundingText", true, false, 1494 "beforeLength", "afterLength"); 1495 public static void richInputConnection_deleteSurroundingText(final int beforeLength, 1496 final int afterLength) { 1497 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, 1498 beforeLength, afterLength); 1499 } 1500 1501 /** 1502 * Log a call to RichInputConnection.finishComposingText(). 1503 * 1504 * SystemResponse: The IME has left the composing text as-is. 1505 */ 1506 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = 1507 new LogStatement("RichInputConnectionFinishComposingText", false, false); 1508 public static void richInputConnection_finishComposingText() { 1509 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT); 1510 } 1511 1512 /** 1513 * Log a call to RichInputConnection.performEditorAction(). 1514 * 1515 * SystemResponse: The IME is invoking an action specific to the editor. 1516 */ 1517 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION = 1518 new LogStatement("RichInputConnectionPerformEditorAction", false, false, 1519 "imeActionId"); 1520 public static void richInputConnection_performEditorAction(final int imeActionId) { 1521 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION, 1522 imeActionId); 1523 } 1524 1525 /** 1526 * Log a call to RichInputConnection.sendKeyEvent(). 1527 * 1528 * SystemResponse: The IME is telling the TextView that a key is being pressed through an 1529 * alternate channel. 1530 * TODO: only for hardware keys? 1531 */ 1532 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT = 1533 new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action", 1534 "code"); 1535 public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) { 1536 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT, 1537 keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode()); 1538 } 1539 1540 /** 1541 * Log a call to RichInputConnection.setComposingText(). 1542 * 1543 * SystemResponse: The IME is setting the composing text. Happens each time a character is 1544 * entered. 1545 */ 1546 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = 1547 new LogStatement("RichInputConnectionSetComposingText", true, true, "text", 1548 "newCursorPosition"); 1549 public static void richInputConnection_setComposingText(final CharSequence text, 1550 final int newCursorPosition) { 1551 if (text == null) { 1552 throw new RuntimeException("setComposingText is null"); 1553 } 1554 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, text, 1555 newCursorPosition); 1556 } 1557 1558 /** 1559 * Log a call to RichInputConnection.setSelection(). 1560 * 1561 * SystemResponse: The IME is requesting that the selection change. User-initiated selection- 1562 * change requests do not go through this method -- it's only when the system wants to change 1563 * the selection. 1564 */ 1565 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION = 1566 new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to"); 1567 public static void richInputConnection_setSelection(final int from, final int to) { 1568 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to); 1569 } 1570 1571 /** 1572 * Log a call to SuddenJumpingTouchEventHandler.onTouchEvent(). 1573 * 1574 * SystemResponse: The IME has filtered input events in case of an erroneous sensor reading. 1575 */ 1576 private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = 1577 new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false, 1578 "motionEvent"); 1579 public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) { 1580 if (me != null) { 1581 getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, 1582 me.toString()); 1583 } 1584 } 1585 1586 /** 1587 * Log a call to SuggestionsView.setSuggestions(). 1588 * 1589 * SystemResponse: The IME is setting the suggestions in the suggestion strip. 1590 */ 1591 private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS = 1592 new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords"); 1593 public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) { 1594 if (suggestedWords != null) { 1595 getInstance().enqueueEvent(LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS, 1596 suggestedWords); 1597 } 1598 } 1599 1600 /** 1601 * The user has indicated a particular point in the log that is of interest. 1602 * 1603 * UserAction: From direct menu invocation. 1604 */ 1605 private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP = 1606 new LogStatement("UserTimestamp", false, false); 1607 public void userTimestamp() { 1608 getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP); 1609 } 1610 1611 /** 1612 * Log a call to LatinIME.onEndBatchInput(). 1613 * 1614 * SystemResponse: The system has completed a gesture. 1615 */ 1616 private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT = 1617 new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText", 1618 "enteredWordPos"); 1619 public static void latinIME_onEndBatchInput(final CharSequence enteredText, 1620 final int enteredWordPos) { 1621 final ResearchLogger researchLogger = getInstance(); 1622 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText, 1623 enteredWordPos); 1624 researchLogger.mStatistics.recordGestureInput(enteredText.length()); 1625 } 1626 1627 /** 1628 * Log a call to LatinIME.handleBackspace(). 1629 * 1630 * UserInput: The user is deleting a gestured word by hitting the backspace key once. 1631 */ 1632 private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH = 1633 new LogStatement("LatinIMEHandleBackspaceBatch", true, false, "deletedText"); 1634 public static void latinIME_handleBackspace_batch(final CharSequence deletedText) { 1635 final ResearchLogger researchLogger = getInstance(); 1636 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText); 1637 researchLogger.mStatistics.recordGestureDelete(); 1638 } 1639 1640 /** 1641 * Log statistics. 1642 * 1643 * ContextualData, recorded at the end of a session. 1644 */ 1645 private static final LogStatement LOGSTATEMENT_STATISTICS = 1646 new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount", 1647 "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting", 1648 "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete", 1649 "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete", 1650 "dictionaryWordCount", "splitWordsCount", "gestureInputCount", 1651 "gestureCharsCount", "gesturesDeletedCount", "manualSuggestionsCount", 1652 "revertCommitsCount"); 1653 private static void logStatistics() { 1654 final ResearchLogger researchLogger = getInstance(); 1655 final Statistics statistics = researchLogger.mStatistics; 1656 researchLogger.enqueueEvent(LOGSTATEMENT_STATISTICS, statistics.mCharCount, 1657 statistics.mLetterCount, statistics.mNumberCount, statistics.mSpaceCount, 1658 statistics.mDeleteKeyCount, statistics.mWordCount, statistics.mIsEmptyUponStarting, 1659 statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(), 1660 statistics.mBeforeDeleteKeyCounter.getAverageTime(), 1661 statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(), 1662 statistics.mAfterDeleteKeyCounter.getAverageTime(), 1663 statistics.mDictionaryWordCount, statistics.mSplitWordsCount, 1664 statistics.mGesturesInputCount, statistics.mGesturesCharsCount, 1665 statistics.mGesturesDeletedCount, statistics.mManualSuggestionsCount, 1666 statistics.mRevertCommitsCount); 1667 } 1668} 1669