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