ResearchLogger.java revision 40417aed94a32dcd4d6a4c469320e2d6ddf83865
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.inputmethodservice.InputMethodService; 38import android.net.Uri; 39import android.os.Build; 40import android.os.IBinder; 41import android.os.SystemClock; 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; 87 private static final boolean LOG_EVERYTHING = false; // true will disclose private info 88 public static final boolean DEFAULT_USABILITY_STUDY_MODE = false; 89 /* package */ static boolean sIsLogging = false; 90 private static final int OUTPUT_FORMAT_VERSION = 5; 91 private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; 92 private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash"; 93 /* package */ static final String FILENAME_PREFIX = "researchLog"; 94 private static final String FILENAME_SUFFIX = ".txt"; 95 private static final SimpleDateFormat TIMESTAMP_DATEFORMAT = 96 new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); 97 private static final boolean IS_SHOWING_INDICATOR = true; 98 private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false; 99 public static final int FEEDBACK_WORD_BUFFER_SIZE = 5; 100 101 // constants related to specific log points 102 private static final String WHITESPACE_SEPARATORS = " \t\n\r"; 103 private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 104 private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid"; 105 106 private static final ResearchLogger sInstance = new ResearchLogger(); 107 // to write to a different filename, e.g., for testing, set mFile before calling start() 108 /* package */ File mFilesDir; 109 /* package */ String mUUIDString; 110 /* package */ ResearchLog mMainResearchLog; 111 // mFeedbackLog records all events for the session, private or not (excepting 112 // passwords). It is written to permanent storage only if the user explicitly commands 113 // the system to do so. 114 // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are 115 // complete. 116 /* package */ ResearchLog mFeedbackLog; 117 /* package */ MainLogBuffer mMainLogBuffer; 118 /* package */ LogBuffer mFeedbackLogBuffer; 119 120 private boolean mIsPasswordView = false; 121 private boolean mIsLoggingSuspended = false; 122 private SharedPreferences mPrefs; 123 124 // digits entered by the user are replaced with this codepoint. 125 /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT = 126 Character.codePointAt("\uE000", 0); // U+E000 is in the "private-use area" 127 // U+E001 is in the "private-use area" 128 /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001"; 129 private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time"; 130 private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS; 131 private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS; 132 protected static final int SUSPEND_DURATION_IN_MINUTES = 1; 133 // set when LatinIME should ignore an onUpdateSelection() callback that 134 // arises from operations in this class 135 private static boolean sLatinIMEExpectingUpdateSelection = false; 136 137 // used to check whether words are not unique 138 private Suggest mSuggest; 139 private MainKeyboardView mMainKeyboardView; 140 private InputMethodService mInputMethodService; 141 private final Statistics mStatistics; 142 143 private Intent mUploadIntent; 144 145 private LogUnit mCurrentLogUnit = new LogUnit(); 146 147 private ResearchLogger() { 148 mStatistics = Statistics.getInstance(); 149 } 150 151 public static ResearchLogger getInstance() { 152 return sInstance; 153 } 154 155 public void init(final InputMethodService ims, final SharedPreferences prefs) { 156 assert ims != null; 157 if (ims == null) { 158 Log.w(TAG, "IMS is null; logging is off"); 159 } else { 160 mFilesDir = ims.getFilesDir(); 161 if (mFilesDir == null || !mFilesDir.exists()) { 162 Log.w(TAG, "IME storage directory does not exist."); 163 } 164 } 165 if (prefs != null) { 166 mUUIDString = getUUID(prefs); 167 if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) { 168 Editor e = prefs.edit(); 169 e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE); 170 e.apply(); 171 } 172 sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); 173 prefs.registerOnSharedPreferenceChangeListener(this); 174 175 final long lastCleanupTime = prefs.getLong(PREF_LAST_CLEANUP_TIME, 0L); 176 final long now = System.currentTimeMillis(); 177 if (lastCleanupTime + DURATION_BETWEEN_DIR_CLEANUP_IN_MS < now) { 178 final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS; 179 cleanupLoggingDir(mFilesDir, timeHorizon); 180 Editor e = prefs.edit(); 181 e.putLong(PREF_LAST_CLEANUP_TIME, now); 182 e.apply(); 183 } 184 } 185 mInputMethodService = ims; 186 mPrefs = prefs; 187 mUploadIntent = new Intent(mInputMethodService, UploaderService.class); 188 189 if (ProductionFlag.IS_EXPERIMENTAL) { 190 scheduleUploadingService(mInputMethodService); 191 } 192 } 193 194 /** 195 * Arrange for the UploaderService to be run on a regular basis. 196 * 197 * Any existing scheduled invocation of UploaderService is removed and rescheduled. This may 198 * cause problems if this method is called often and frequent updates are required, but since 199 * the user will likely be sleeping at some point, if the interval is less that the expected 200 * sleep duration and this method is not called during that time, the service should be invoked 201 * at some point. 202 */ 203 public static void scheduleUploadingService(Context context) { 204 final Intent intent = new Intent(context, UploaderService.class); 205 final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0); 206 final AlarmManager manager = 207 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 208 manager.cancel(pendingIntent); 209 manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 210 UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent); 211 } 212 213 private void cleanupLoggingDir(final File dir, final long time) { 214 for (File file : dir.listFiles()) { 215 if (file.getName().startsWith(ResearchLogger.FILENAME_PREFIX) && 216 file.lastModified() < time) { 217 file.delete(); 218 } 219 } 220 } 221 222 public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) { 223 mMainKeyboardView = mainKeyboardView; 224 maybeShowSplashScreen(); 225 } 226 227 public void mainKeyboardView_onDetachedFromWindow() { 228 mMainKeyboardView = null; 229 } 230 231 private boolean hasSeenSplash() { 232 return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false); 233 } 234 235 private Dialog mSplashDialog = null; 236 237 private void maybeShowSplashScreen() { 238 if (hasSeenSplash()) { 239 return; 240 } 241 if (mSplashDialog != null && mSplashDialog.isShowing()) { 242 return; 243 } 244 final IBinder windowToken = mMainKeyboardView != null 245 ? mMainKeyboardView.getWindowToken() : null; 246 if (windowToken == null) { 247 return; 248 } 249 final AlertDialog.Builder builder = new AlertDialog.Builder(mInputMethodService) 250 .setTitle(R.string.research_splash_title) 251 .setMessage(R.string.research_splash_content) 252 .setPositiveButton(android.R.string.yes, 253 new DialogInterface.OnClickListener() { 254 @Override 255 public void onClick(DialogInterface dialog, int which) { 256 onUserLoggingConsent(); 257 mSplashDialog.dismiss(); 258 } 259 }) 260 .setNegativeButton(android.R.string.no, 261 new DialogInterface.OnClickListener() { 262 @Override 263 public void onClick(DialogInterface dialog, int which) { 264 final String packageName = mInputMethodService.getPackageName(); 265 final Uri packageUri = Uri.parse("package:" + packageName); 266 final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, 267 packageUri); 268 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 269 mInputMethodService.startActivity(intent); 270 } 271 }) 272 .setCancelable(true) 273 .setOnCancelListener( 274 new OnCancelListener() { 275 @Override 276 public void onCancel(DialogInterface dialog) { 277 mInputMethodService.requestHideSelf(0); 278 } 279 }); 280 mSplashDialog = builder.create(); 281 final Window w = mSplashDialog.getWindow(); 282 final WindowManager.LayoutParams lp = w.getAttributes(); 283 lp.token = windowToken; 284 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 285 w.setAttributes(lp); 286 w.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 287 mSplashDialog.show(); 288 } 289 290 public void onUserLoggingConsent() { 291 setLoggingAllowed(true); 292 if (mPrefs == null) { 293 return; 294 } 295 final Editor e = mPrefs.edit(); 296 e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true); 297 e.apply(); 298 restart(); 299 } 300 301 private void setLoggingAllowed(boolean enableLogging) { 302 if (mPrefs == null) { 303 return; 304 } 305 Editor e = mPrefs.edit(); 306 e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); 307 e.apply(); 308 sIsLogging = enableLogging; 309 } 310 311 private File createLogFile(File filesDir) { 312 final StringBuilder sb = new StringBuilder(); 313 sb.append(FILENAME_PREFIX).append('-'); 314 sb.append(mUUIDString).append('-'); 315 sb.append(TIMESTAMP_DATEFORMAT.format(new Date())); 316 sb.append(FILENAME_SUFFIX); 317 return new File(filesDir, sb.toString()); 318 } 319 320 private void checkForEmptyEditor() { 321 if (mInputMethodService == null) { 322 return; 323 } 324 final InputConnection ic = mInputMethodService.getCurrentInputConnection(); 325 if (ic == null) { 326 return; 327 } 328 final CharSequence textBefore = ic.getTextBeforeCursor(1, 0); 329 if (!TextUtils.isEmpty(textBefore)) { 330 mStatistics.setIsEmptyUponStarting(false); 331 return; 332 } 333 final CharSequence textAfter = ic.getTextAfterCursor(1, 0); 334 if (!TextUtils.isEmpty(textAfter)) { 335 mStatistics.setIsEmptyUponStarting(false); 336 return; 337 } 338 if (textBefore != null && textAfter != null) { 339 mStatistics.setIsEmptyUponStarting(true); 340 } 341 } 342 343 private void start() { 344 if (DEBUG) { 345 Log.d(TAG, "start called"); 346 } 347 maybeShowSplashScreen(); 348 updateSuspendedState(); 349 requestIndicatorRedraw(); 350 mStatistics.reset(); 351 checkForEmptyEditor(); 352 if (!isAllowedToLog()) { 353 // Log.w(TAG, "not in usability mode; not logging"); 354 return; 355 } 356 if (mFilesDir == null || !mFilesDir.exists()) { 357 Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); 358 return; 359 } 360 if (mMainLogBuffer == null) { 361 mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); 362 mMainLogBuffer = new MainLogBuffer(mMainResearchLog); 363 mMainLogBuffer.setSuggest(mSuggest); 364 } 365 if (mFeedbackLogBuffer == null) { 366 mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); 367 // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold 368 // the feedback LogUnit itself. 369 mFeedbackLogBuffer = new LogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1); 370 } 371 } 372 373 /* package */ void stop() { 374 if (DEBUG) { 375 Log.d(TAG, "stop called"); 376 } 377 logStatistics(); 378 commitCurrentLogUnit(); 379 380 if (mMainLogBuffer != null) { 381 publishLogBuffer(mMainLogBuffer, mMainResearchLog, 382 LOG_EVERYTHING /* isIncludingPrivateData */); 383 mMainResearchLog.close(null /* callback */); 384 mMainLogBuffer = null; 385 } 386 if (mFeedbackLogBuffer != null) { 387 mFeedbackLog.close(null /* callback */); 388 mFeedbackLogBuffer = null; 389 } 390 } 391 392 public boolean abort() { 393 if (DEBUG) { 394 Log.d(TAG, "abort called"); 395 } 396 boolean didAbortMainLog = false; 397 if (mMainLogBuffer != null) { 398 mMainLogBuffer.clear(); 399 try { 400 didAbortMainLog = mMainResearchLog.blockingAbort(); 401 } catch (InterruptedException e) { 402 // Don't know whether this succeeded or not. We assume not; this is reported 403 // to the caller. 404 } 405 mMainLogBuffer = null; 406 } 407 boolean didAbortFeedbackLog = false; 408 if (mFeedbackLogBuffer != null) { 409 mFeedbackLogBuffer.clear(); 410 try { 411 didAbortFeedbackLog = mFeedbackLog.blockingAbort(); 412 } catch (InterruptedException e) { 413 // Don't know whether this succeeded or not. We assume not; this is reported 414 // to the caller. 415 } 416 mFeedbackLogBuffer = null; 417 } 418 return didAbortMainLog && didAbortFeedbackLog; 419 } 420 421 private void restart() { 422 stop(); 423 start(); 424 } 425 426 private long mResumeTime = 0L; 427 private void updateSuspendedState() { 428 final long time = System.currentTimeMillis(); 429 if (time > mResumeTime) { 430 mIsLoggingSuspended = false; 431 } 432 } 433 434 @Override 435 public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 436 if (key == null || prefs == null) { 437 return; 438 } 439 sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); 440 if (sIsLogging == false) { 441 abort(); 442 } 443 requestIndicatorRedraw(); 444 mPrefs = prefs; 445 prefsChanged(prefs); 446 } 447 448 public void onResearchKeySelected(final LatinIME latinIME) { 449 if (mInFeedbackDialog) { 450 Toast.makeText(latinIME, R.string.research_please_exit_feedback_form, 451 Toast.LENGTH_LONG).show(); 452 return; 453 } 454 presentFeedbackDialog(latinIME); 455 } 456 457 // TODO: currently unreachable. Remove after being sure no menu is needed. 458 /* 459 public void presentResearchDialog(final LatinIME latinIME) { 460 final CharSequence title = latinIME.getString(R.string.english_ime_research_log); 461 final boolean showEnable = mIsLoggingSuspended || !sIsLogging; 462 final CharSequence[] items = new CharSequence[] { 463 latinIME.getString(R.string.research_feedback_menu_option), 464 showEnable ? latinIME.getString(R.string.research_enable_session_logging) : 465 latinIME.getString(R.string.research_do_not_log_this_session) 466 }; 467 final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 468 @Override 469 public void onClick(DialogInterface di, int position) { 470 di.dismiss(); 471 switch (position) { 472 case 0: 473 presentFeedbackDialog(latinIME); 474 break; 475 case 1: 476 enableOrDisable(showEnable, latinIME); 477 break; 478 } 479 } 480 481 }; 482 final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME) 483 .setItems(items, listener) 484 .setTitle(title); 485 latinIME.showOptionDialog(builder.create()); 486 } 487 */ 488 489 private boolean mInFeedbackDialog = false; 490 public void presentFeedbackDialog(LatinIME latinIME) { 491 mInFeedbackDialog = true; 492 latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class); 493 } 494 495 // TODO: currently unreachable. Remove after being sure enable/disable is 496 // not needed. 497 /* 498 public void enableOrDisable(final boolean showEnable, final LatinIME latinIME) { 499 if (showEnable) { 500 if (!sIsLogging) { 501 setLoggingAllowed(true); 502 } 503 resumeLogging(); 504 Toast.makeText(latinIME, 505 R.string.research_notify_session_logging_enabled, 506 Toast.LENGTH_LONG).show(); 507 } else { 508 Toast toast = Toast.makeText(latinIME, 509 R.string.research_notify_session_log_deleting, 510 Toast.LENGTH_LONG); 511 toast.show(); 512 boolean isLogDeleted = abort(); 513 final long currentTime = System.currentTimeMillis(); 514 final long resumeTime = currentTime + 1000 * 60 * 515 SUSPEND_DURATION_IN_MINUTES; 516 suspendLoggingUntil(resumeTime); 517 toast.cancel(); 518 Toast.makeText(latinIME, R.string.research_notify_logging_suspended, 519 Toast.LENGTH_LONG).show(); 520 } 521 } 522 */ 523 524 static class LogStatement { 525 final String mName; 526 527 // mIsPotentiallyPrivate indicates that event contains potentially private information. If 528 // the word that this event is a part of is determined to be privacy-sensitive, then this 529 // event should not be included in the output log. The system waits to output until the 530 // containing word is known. 531 final boolean mIsPotentiallyPrivate; 532 533 // mIsPotentiallyRevealing indicates that this statement may disclose details about other 534 // words typed in other LogUnits. This can happen if the user is not inserting spaces, and 535 // data from Suggestions and/or Composing text reveals the entire "megaword". For example, 536 // say the user is typing "for the win", and the system wants to record the bigram "the 537 // win". If the user types "forthe", omitting the space, the system will give "for the" as 538 // a suggestion. If the user accepts the autocorrection, the suggestion for "for the" is 539 // included in the log for the word "the", disclosing that the previous word had been "for". 540 // For now, we simply do not include this data when logging part of a "megaword". 541 final boolean mIsPotentiallyRevealing; 542 543 // mKeys stores the names that are the attributes in the output json objects 544 final String[] mKeys; 545 private static final String[] NULL_KEYS = new String[0]; 546 547 LogStatement(final String name, final boolean isPotentiallyPrivate, 548 final boolean isPotentiallyRevealing, final String... keys) { 549 mName = name; 550 mIsPotentiallyPrivate = isPotentiallyPrivate; 551 mIsPotentiallyRevealing = isPotentiallyRevealing; 552 mKeys = (keys == null) ? NULL_KEYS : keys; 553 } 554 } 555 556 private static final LogStatement LOGSTATEMENT_FEEDBACK = 557 new LogStatement("UserTimestamp", false, false, "contents"); 558 public void sendFeedback(final String feedbackContents, final boolean includeHistory) { 559 if (mFeedbackLogBuffer == null) { 560 return; 561 } 562 if (includeHistory) { 563 commitCurrentLogUnit(); 564 } else { 565 mFeedbackLogBuffer.clear(); 566 } 567 final LogUnit feedbackLogUnit = new LogUnit(); 568 feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(), 569 feedbackContents); 570 mFeedbackLogBuffer.shiftIn(feedbackLogUnit); 571 publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */); 572 mFeedbackLog.close(new Runnable() { 573 @Override 574 public void run() { 575 uploadNow(); 576 } 577 }); 578 mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); 579 } 580 581 public void uploadNow() { 582 if (DEBUG) { 583 Log.d(TAG, "calling uploadNow()"); 584 } 585 mInputMethodService.startService(mUploadIntent); 586 } 587 588 public void onLeavingSendFeedbackDialog() { 589 mInFeedbackDialog = false; 590 } 591 592 public void initSuggest(Suggest suggest) { 593 mSuggest = suggest; 594 if (mMainLogBuffer != null) { 595 mMainLogBuffer.setSuggest(mSuggest); 596 } 597 } 598 599 private Dictionary getDictionary() { 600 if (mSuggest == null) { 601 return null; 602 } 603 return mSuggest.getMainDictionary(); 604 } 605 606 private void setIsPasswordView(boolean isPasswordView) { 607 mIsPasswordView = isPasswordView; 608 } 609 610 private boolean isAllowedToLog() { 611 if (DEBUG) { 612 Log.d(TAG, "iatl: " + 613 "mipw=" + mIsPasswordView + 614 ", mils=" + mIsLoggingSuspended + 615 ", sil=" + sIsLogging + 616 ", mInFeedbackDialog=" + mInFeedbackDialog); 617 } 618 return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog; 619 } 620 621 public void requestIndicatorRedraw() { 622 if (!IS_SHOWING_INDICATOR) { 623 return; 624 } 625 if (mMainKeyboardView == null) { 626 return; 627 } 628 mMainKeyboardView.invalidateAllKeys(); 629 } 630 631 632 public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width, 633 int height) { 634 // TODO: Reimplement using a keyboard background image specific to the ResearchLogger 635 // and remove this method. 636 // The check for MainKeyboardView ensures that a red border is only placed around 637 // the main keyboard, not every keyboard. 638 if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) { 639 final int savedColor = paint.getColor(); 640 paint.setColor(Color.RED); 641 final Style savedStyle = paint.getStyle(); 642 paint.setStyle(Style.STROKE); 643 final float savedStrokeWidth = paint.getStrokeWidth(); 644 if (IS_SHOWING_INDICATOR_CLEARLY) { 645 paint.setStrokeWidth(5); 646 canvas.drawRect(0, 0, width, height, paint); 647 } else { 648 // Put a tiny red dot on the screen so a knowledgeable user can check whether 649 // it is enabled. The dot is actually a zero-width, zero-height rectangle, 650 // placed at the lower-right corner of the canvas, painted with a non-zero border 651 // width. 652 paint.setStrokeWidth(3); 653 canvas.drawRect(width, height, width, height, paint); 654 } 655 paint.setColor(savedColor); 656 paint.setStyle(savedStyle); 657 paint.setStrokeWidth(savedStrokeWidth); 658 } 659 } 660 661 /** 662 * Buffer a research log event, flagging it as privacy-sensitive. 663 */ 664 private synchronized void enqueueEvent(LogStatement logStatement, Object... values) { 665 assert values.length == logStatement.mKeys.length; 666 if (isAllowedToLog()) { 667 final long time = SystemClock.uptimeMillis(); 668 mCurrentLogUnit.addLogStatement(logStatement, time, values); 669 } 670 } 671 672 private void setCurrentLogUnitContainsDigitFlag() { 673 mCurrentLogUnit.setMayContainDigit(); 674 } 675 676 /* package for test */ void commitCurrentLogUnit() { 677 if (DEBUG) { 678 Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasWord() ? 679 ": " + mCurrentLogUnit.getWord() : "")); 680 } 681 if (!mCurrentLogUnit.isEmpty()) { 682 if (mMainLogBuffer != null) { 683 mMainLogBuffer.shiftIn(mCurrentLogUnit); 684 if (mMainLogBuffer.isSafeToLog() && mMainResearchLog != null) { 685 publishLogBuffer(mMainLogBuffer, mMainResearchLog, 686 true /* isIncludingPrivateData */); 687 mMainLogBuffer.resetWordCounter(); 688 } 689 } 690 if (mFeedbackLogBuffer != null) { 691 mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); 692 } 693 mCurrentLogUnit = new LogUnit(); 694 } 695 } 696 697 private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_OPENING = 698 new LogStatement("logSegmentStart", false, false, "isIncludingPrivateData"); 699 private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_CLOSING = 700 new LogStatement("logSegmentEnd", false, false); 701 /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, 702 final ResearchLog researchLog, final boolean isIncludingPrivateData) { 703 final LogUnit openingLogUnit = new LogUnit(); 704 openingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_OPENING, SystemClock.uptimeMillis(), 705 isIncludingPrivateData); 706 researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */); 707 LogUnit logUnit; 708 while ((logUnit = logBuffer.shiftOut()) != null) { 709 if (DEBUG) { 710 Log.d(TAG, "publishLogBuffer: " + (logUnit.hasWord() ? logUnit.getWord() 711 : "<wordless>")); 712 } 713 researchLog.publish(logUnit, isIncludingPrivateData); 714 } 715 final LogUnit closingLogUnit = new LogUnit(); 716 closingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_CLOSING, 717 SystemClock.uptimeMillis()); 718 researchLog.publish(closingLogUnit, true /* isIncludingPrivateData */); 719 } 720 721 public static boolean hasLetters(final String word) { 722 final int length = word.length(); 723 for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { 724 final int codePoint = word.codePointAt(i); 725 if (Character.isLetter(codePoint)) { 726 return true; 727 } 728 } 729 return false; 730 } 731 732 private static final LogStatement LOGSTATEMENT_COMMIT_RECORD_SPLIT_WORDS = 733 new LogStatement("recordSplitWords", true, false); 734 private void onWordComplete(final String word, final long maxTime, final boolean isSplitWords) { 735 final Dictionary dictionary = getDictionary(); 736 if (word != null && word.length() > 0 && hasLetters(word)) { 737 mCurrentLogUnit.setWord(word); 738 final boolean isDictionaryWord = dictionary != null 739 && dictionary.isValidWord(word); 740 mStatistics.recordWordEntered(isDictionaryWord); 741 } 742 final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime); 743 enqueueCommitText(word); 744 if (isSplitWords) { 745 enqueueEvent(LOGSTATEMENT_COMMIT_RECORD_SPLIT_WORDS); 746 enqueueCommitText(" "); 747 mStatistics.recordSplitWords(); 748 } 749 commitCurrentLogUnit(); 750 mCurrentLogUnit = newLogUnit; 751 } 752 753 private static int scrubDigitFromCodePoint(int codePoint) { 754 return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint; 755 } 756 757 /* package for test */ static String scrubDigitsFromString(String s) { 758 StringBuilder sb = null; 759 final int length = s.length(); 760 for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) { 761 final int codePoint = Character.codePointAt(s, i); 762 if (Character.isDigit(codePoint)) { 763 if (sb == null) { 764 sb = new StringBuilder(length); 765 sb.append(s.substring(0, i)); 766 } 767 sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT); 768 } else { 769 if (sb != null) { 770 sb.appendCodePoint(codePoint); 771 } 772 } 773 } 774 if (sb == null) { 775 return s; 776 } else { 777 return sb.toString(); 778 } 779 } 780 781 private static String getUUID(final SharedPreferences prefs) { 782 String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null); 783 if (null == uuidString) { 784 UUID uuid = UUID.randomUUID(); 785 uuidString = uuid.toString(); 786 Editor editor = prefs.edit(); 787 editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString); 788 editor.apply(); 789 } 790 return uuidString; 791 } 792 793 private String scrubWord(String word) { 794 final Dictionary dictionary = getDictionary(); 795 if (dictionary == null) { 796 return WORD_REPLACEMENT_STRING; 797 } 798 if (dictionary.isValidWord(word)) { 799 return word; 800 } 801 return WORD_REPLACEMENT_STRING; 802 } 803 804 private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL = 805 new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid", 806 "packageName", "inputType", "imeOptions", "fieldId", "display", "model", 807 "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything"); 808 public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, 809 final SharedPreferences prefs) { 810 final ResearchLogger researchLogger = getInstance(); 811 if (editorInfo != null) { 812 final boolean isPassword = InputTypeUtils.isPasswordInputType(editorInfo.inputType) 813 || InputTypeUtils.isVisiblePasswordInputType(editorInfo.inputType); 814 getInstance().setIsPasswordView(isPassword); 815 researchLogger.start(); 816 final Context context = researchLogger.mInputMethodService; 817 try { 818 final PackageInfo packageInfo; 819 packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 820 0); 821 final Integer versionCode = packageInfo.versionCode; 822 final String versionName = packageInfo.versionName; 823 researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL, 824 researchLogger.mUUIDString, editorInfo.packageName, 825 Integer.toHexString(editorInfo.inputType), 826 Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, 827 Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, 828 OUTPUT_FORMAT_VERSION, LOG_EVERYTHING); 829 } catch (NameNotFoundException e) { 830 e.printStackTrace(); 831 } 832 } 833 } 834 835 public void latinIME_onFinishInputViewInternal() { 836 stop(); 837 } 838 839 private static final LogStatement LOGSTATEMENT_PREFS_CHANGED = 840 new LogStatement("PrefsChanged", false, false, "prefs"); 841 public static void prefsChanged(final SharedPreferences prefs) { 842 final ResearchLogger researchLogger = getInstance(); 843 researchLogger.enqueueEvent(LOGSTATEMENT_PREFS_CHANGED, prefs); 844 } 845 846 // Regular logging methods 847 848 private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT = 849 new LogStatement("MainKeyboardViewProcessMotionEvent", true, false, "action", 850 "eventTime", "id", "x", "y", "size", "pressure"); 851 public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action, 852 final long eventTime, final int index, final int id, final int x, final int y) { 853 if (me != null) { 854 final String actionString; 855 switch (action) { 856 case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break; 857 case MotionEvent.ACTION_UP: actionString = "UP"; break; 858 case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break; 859 case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break; 860 case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break; 861 case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break; 862 case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break; 863 default: actionString = "ACTION_" + action; break; 864 } 865 final float size = me.getSize(index); 866 final float pressure = me.getPressure(index); 867 getInstance().enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT, 868 actionString, eventTime, id, x, y, size, pressure); 869 } 870 } 871 872 private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT = 873 new LogStatement("LatinImeOnCodeInput", true, false, "code", "x", "y"); 874 public static void latinIME_onCodeInput(final int code, final int x, final int y) { 875 final long time = SystemClock.uptimeMillis(); 876 final ResearchLogger researchLogger = getInstance(); 877 researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT, 878 Constants.printableCode(scrubDigitFromCodePoint(code)), x, y); 879 if (Character.isDigit(code)) { 880 researchLogger.setCurrentLogUnitContainsDigitFlag(); 881 } 882 researchLogger.mStatistics.recordChar(code, time); 883 } 884 private static final LogStatement LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS = 885 new LogStatement("LatinIMEOnDisplayCompletions", true, true, 886 "applicationSpecifiedCompletions"); 887 public static void latinIME_onDisplayCompletions( 888 final CompletionInfo[] applicationSpecifiedCompletions) { 889 // Note; passing an array as a single element in a vararg list. Must create a new 890 // dummy array around it or it will get expanded. 891 getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS, 892 new Object[] { applicationSpecifiedCompletions }); 893 } 894 895 public static boolean getAndClearLatinIMEExpectingUpdateSelection() { 896 boolean returnValue = sLatinIMEExpectingUpdateSelection; 897 sLatinIMEExpectingUpdateSelection = false; 898 return returnValue; 899 } 900 901 private static final LogStatement LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN = 902 new LogStatement("LatinIMEOnWindowHidden", false, false, "isTextTruncated", "text"); 903 public static void latinIME_onWindowHidden(final int savedSelectionStart, 904 final int savedSelectionEnd, final InputConnection ic) { 905 if (ic != null) { 906 final boolean isTextTruncated; 907 final String text; 908 if (LOG_EVERYTHING) { 909 // Capture the TextView contents. This will trigger onUpdateSelection(), so we 910 // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called, 911 // it can tell that it was generated by the logging code, and not by the user, and 912 // therefore keep user-visible state as is. 913 ic.beginBatchEdit(); 914 ic.performContextMenuAction(android.R.id.selectAll); 915 CharSequence charSequence = ic.getSelectedText(0); 916 if (savedSelectionStart != -1 && savedSelectionEnd != -1) { 917 ic.setSelection(savedSelectionStart, savedSelectionEnd); 918 } 919 ic.endBatchEdit(); 920 sLatinIMEExpectingUpdateSelection = true; 921 if (TextUtils.isEmpty(charSequence)) { 922 isTextTruncated = false; 923 text = ""; 924 } else { 925 if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) { 926 int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE; 927 // do not cut in the middle of a supplementary character 928 final char c = charSequence.charAt(length - 1); 929 if (Character.isHighSurrogate(c)) { 930 length--; 931 } 932 final CharSequence truncatedCharSequence = charSequence.subSequence(0, 933 length); 934 isTextTruncated = true; 935 text = truncatedCharSequence.toString(); 936 } else { 937 isTextTruncated = false; 938 text = charSequence.toString(); 939 } 940 } 941 } else { 942 isTextTruncated = true; 943 text = ""; 944 } 945 final ResearchLogger researchLogger = getInstance(); 946 // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g. 947 // during a live user test), so the normal isPotentiallyPrivate and 948 // isPotentiallyRevealing flags do not apply 949 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN, isTextTruncated, 950 text); 951 researchLogger.commitCurrentLogUnit(); 952 getInstance().stop(); 953 } 954 } 955 956 private static final LogStatement LOGSTATEMENT_LATINIME_ONUPDATESELECTION = 957 new LogStatement("LatinIMEOnUpdateSelection", true, false, "lastSelectionStart", 958 "lastSelectionEnd", "oldSelStart", "oldSelEnd", "newSelStart", "newSelEnd", 959 "composingSpanStart", "composingSpanEnd", "expectingUpdateSelection", 960 "expectingUpdateSelectionFromLogger", "context"); 961 public static void latinIME_onUpdateSelection(final int lastSelectionStart, 962 final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd, 963 final int newSelStart, final int newSelEnd, final int composingSpanStart, 964 final int composingSpanEnd, final boolean expectingUpdateSelection, 965 final boolean expectingUpdateSelectionFromLogger, 966 final RichInputConnection connection) { 967 String word = ""; 968 if (connection != null) { 969 Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1); 970 if (range != null) { 971 word = range.mWord; 972 } 973 } 974 final ResearchLogger researchLogger = getInstance(); 975 final String scrubbedWord = researchLogger.scrubWord(word); 976 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONUPDATESELECTION, lastSelectionStart, 977 lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd, 978 composingSpanStart, composingSpanEnd, expectingUpdateSelection, 979 expectingUpdateSelectionFromLogger, scrubbedWord); 980 } 981 982 private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY = 983 new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index", 984 "suggestion", "x", "y"); 985 public static void latinIME_pickSuggestionManually(final String replacedWord, 986 final int index, CharSequence suggestion) { 987 final ResearchLogger researchLogger = getInstance(); 988 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY, 989 scrubDigitsFromString(replacedWord), index, 990 suggestion == null ? null : scrubDigitsFromString(suggestion.toString()), 991 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); 992 } 993 994 private static final LogStatement LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION = 995 new LogStatement("LatinIMEPunctuationSuggestion", false, false, "index", "suggestion", 996 "x", "y"); 997 public static void latinIME_punctuationSuggestion(final int index, 998 final CharSequence suggestion) { 999 getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion, 1000 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); 1001 } 1002 1003 private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT = 1004 new LogStatement("LatinIMESendKeyCodePoint", true, false, "code"); 1005 public static void latinIME_sendKeyCodePoint(final int code) { 1006 final ResearchLogger researchLogger = getInstance(); 1007 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT, 1008 Constants.printableCode(scrubDigitFromCodePoint(code))); 1009 if (Character.isDigit(code)) { 1010 researchLogger.setCurrentLogUnitContainsDigitFlag(); 1011 } 1012 } 1013 1014 private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE = 1015 new LogStatement("LatinIMESwapSwapperAndSpace", false, false); 1016 public static void latinIME_swapSwapperAndSpace() { 1017 getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE); 1018 } 1019 1020 private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS = 1021 new LogStatement("MainKeyboardViewOnLongPress", false, false); 1022 public static void mainKeyboardView_onLongPress() { 1023 getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS); 1024 } 1025 1026 private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD = 1027 new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale", 1028 "orientation", "width", "modeName", "action", "navigateNext", 1029 "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled", 1030 "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th", 1031 "keys"); 1032 public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) { 1033 final KeyboardId kid = keyboard.mId; 1034 final boolean isPasswordView = kid.passwordInput(); 1035 getInstance().setIsPasswordView(isPasswordView); 1036 getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD, 1037 KeyboardId.elementIdToName(kid.mElementId), 1038 kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), 1039 kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(), 1040 kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey, 1041 isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey, 1042 kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth, 1043 keyboard.mOccupiedHeight, keyboard.mKeys); 1044 } 1045 1046 private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT = 1047 new LogStatement("LatinIMERevertCommit", true, false, "originallyTypedWord"); 1048 public static void latinIME_revertCommit(final String originallyTypedWord) { 1049 getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_REVERTCOMMIT, originallyTypedWord); 1050 } 1051 1052 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT = 1053 new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false); 1054 public static void pointerTracker_callListenerOnCancelInput() { 1055 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT); 1056 } 1057 1058 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT = 1059 new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "code", 1060 "outputText", "x", "y", "ignoreModifierKey", "altersCode", "isEnabled"); 1061 public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x, 1062 final int y, final boolean ignoreModifierKey, final boolean altersCode, 1063 final int code) { 1064 if (key != null) { 1065 String outputText = key.getOutputText(); 1066 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT, 1067 Constants.printableCode(scrubDigitFromCodePoint(code)), 1068 outputText == null ? null : scrubDigitsFromString(outputText.toString()), 1069 x, y, ignoreModifierKey, altersCode, key.isEnabled()); 1070 } 1071 } 1072 1073 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE = 1074 new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code", 1075 "withSliding", "ignoreModifierKey", "isEnabled"); 1076 public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode, 1077 final boolean withSliding, final boolean ignoreModifierKey) { 1078 if (key != null) { 1079 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE, 1080 Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding, 1081 ignoreModifierKey, key.isEnabled()); 1082 } 1083 } 1084 1085 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT = 1086 new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared"); 1087 public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) { 1088 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT, deltaT, 1089 distanceSquared); 1090 } 1091 1092 private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT = 1093 new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY"); 1094 public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX, 1095 final int lastY) { 1096 getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY); 1097 } 1098 1099 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION = 1100 new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo"); 1101 public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) { 1102 final ResearchLogger researchLogger = getInstance(); 1103 researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION, 1104 completionInfo); 1105 } 1106 1107 private boolean isExpectingCommitText = false; 1108 public static void latinIME_commitPartialText(final CharSequence committedWord, 1109 final long lastTimestampOfWordData) { 1110 final ResearchLogger researchLogger = getInstance(); 1111 final String scrubbedWord = scrubDigitsFromString(committedWord.toString()); 1112 researchLogger.onWordComplete(scrubbedWord, lastTimestampOfWordData, true /* isPartial */); 1113 researchLogger.isExpectingCommitText = true; 1114 } 1115 1116 private static final LogStatement LOGSTATEMENT_COMMITTEXT_UPDATECURSOR = 1117 new LogStatement("CommitTextUpdateCursor", true, false, "newCursorPosition"); 1118 public static void richInputConnection_commitText(final CharSequence committedWord, 1119 final int newCursorPosition) { 1120 final ResearchLogger researchLogger = getInstance(); 1121 final String scrubbedWord = scrubDigitsFromString(committedWord.toString()); 1122 if (!researchLogger.isExpectingCommitText) { 1123 researchLogger.onWordComplete(scrubbedWord, Long.MAX_VALUE, false /* isPartial */); 1124 researchLogger.enqueueEvent(LOGSTATEMENT_COMMITTEXT_UPDATECURSOR, newCursorPosition); 1125 } 1126 researchLogger.isExpectingCommitText = false; 1127 } 1128 1129 private static final LogStatement LOGSTATEMENT_COMMITTEXT = 1130 new LogStatement("CommitText", true, false, "committedText"); 1131 private void enqueueCommitText(final CharSequence word) { 1132 enqueueEvent(LOGSTATEMENT_COMMITTEXT, word); 1133 } 1134 1135 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = 1136 new LogStatement("RichInputConnectionDeleteSurroundingText", true, false, 1137 "beforeLength", "afterLength"); 1138 public static void richInputConnection_deleteSurroundingText(final int beforeLength, 1139 final int afterLength) { 1140 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, 1141 beforeLength, afterLength); 1142 } 1143 1144 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = 1145 new LogStatement("RichInputConnectionFinishComposingText", false, false); 1146 public static void richInputConnection_finishComposingText() { 1147 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT); 1148 } 1149 1150 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION = 1151 new LogStatement("RichInputConnectionPerformEditorAction", false, false, 1152 "imeActionNext"); 1153 public static void richInputConnection_performEditorAction(final int imeActionNext) { 1154 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION, 1155 imeActionNext); 1156 } 1157 1158 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT = 1159 new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action", 1160 "code"); 1161 public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) { 1162 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT, 1163 keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode()); 1164 } 1165 1166 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = 1167 new LogStatement("RichInputConnectionSetComposingText", true, true, "text", 1168 "newCursorPosition"); 1169 public static void richInputConnection_setComposingText(final CharSequence text, 1170 final int newCursorPosition) { 1171 if (text == null) { 1172 throw new RuntimeException("setComposingText is null"); 1173 } 1174 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, text, 1175 newCursorPosition); 1176 } 1177 1178 private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION = 1179 new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to"); 1180 public static void richInputConnection_setSelection(final int from, final int to) { 1181 getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to); 1182 } 1183 1184 private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = 1185 new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false, 1186 "motionEvent"); 1187 public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) { 1188 if (me != null) { 1189 getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, 1190 me.toString()); 1191 } 1192 } 1193 1194 private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS = 1195 new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords"); 1196 public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) { 1197 if (suggestedWords != null) { 1198 getInstance().enqueueEvent(LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS, 1199 suggestedWords); 1200 } 1201 } 1202 1203 private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP = 1204 new LogStatement("UserTimestamp", false, false); 1205 public void userTimestamp() { 1206 getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP); 1207 } 1208 1209 private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT = 1210 new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText", 1211 "enteredWordPos"); 1212 public static void latinIME_onEndBatchInput(final CharSequence enteredText, 1213 final int enteredWordPos) { 1214 final ResearchLogger researchLogger = getInstance(); 1215 researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText, 1216 enteredWordPos); 1217 researchLogger.mStatistics.recordGestureInput(); 1218 } 1219 1220 private static final LogStatement LOGSTATEMENT_STATISTICS = 1221 new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount", 1222 "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting", 1223 "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete", 1224 "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete", 1225 "dictionaryWordCount", "splitWordsCount", "gestureInputCount"); 1226 private static void logStatistics() { 1227 final ResearchLogger researchLogger = getInstance(); 1228 final Statistics statistics = researchLogger.mStatistics; 1229 researchLogger.enqueueEvent(LOGSTATEMENT_STATISTICS, statistics.mCharCount, 1230 statistics.mLetterCount, statistics.mNumberCount, statistics.mSpaceCount, 1231 statistics.mDeleteKeyCount, statistics.mWordCount, statistics.mIsEmptyUponStarting, 1232 statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(), 1233 statistics.mBeforeDeleteKeyCounter.getAverageTime(), 1234 statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(), 1235 statistics.mAfterDeleteKeyCounter.getAverageTime(), 1236 statistics.mDictionaryWordCount, statistics.mSplitWordsCount, 1237 statistics.mGestureInputCount); 1238 } 1239} 1240