ResearchLogger.java revision b5ace11a2e7b88e6b89b6297d5d540f351e48a4a
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.os.Build; 39import android.os.IBinder; 40import android.os.SystemClock; 41import android.text.TextUtils; 42import android.text.format.DateUtils; 43import android.util.Log; 44import android.view.KeyEvent; 45import android.view.MotionEvent; 46import android.view.View; 47import android.view.View.OnClickListener; 48import android.view.Window; 49import android.view.WindowManager; 50import android.view.inputmethod.CompletionInfo; 51import android.view.inputmethod.CorrectionInfo; 52import android.view.inputmethod.EditorInfo; 53import android.view.inputmethod.InputConnection; 54import android.widget.Button; 55import android.widget.Toast; 56 57import com.android.inputmethod.keyboard.Key; 58import com.android.inputmethod.keyboard.Keyboard; 59import com.android.inputmethod.keyboard.KeyboardId; 60import com.android.inputmethod.keyboard.KeyboardView; 61import com.android.inputmethod.keyboard.MainKeyboardView; 62import com.android.inputmethod.latin.CollectionUtils; 63import com.android.inputmethod.latin.Constants; 64import com.android.inputmethod.latin.Dictionary; 65import com.android.inputmethod.latin.LatinIME; 66import com.android.inputmethod.latin.R; 67import com.android.inputmethod.latin.RichInputConnection; 68import com.android.inputmethod.latin.RichInputConnection.Range; 69import com.android.inputmethod.latin.Suggest; 70import com.android.inputmethod.latin.SuggestedWords; 71import com.android.inputmethod.latin.define.ProductionFlag; 72 73import java.io.File; 74import java.text.SimpleDateFormat; 75import java.util.Date; 76import java.util.Locale; 77import java.util.UUID; 78 79/** 80 * Logs the use of the LatinIME keyboard. 81 * 82 * This class logs operations on the IME keyboard, including what the user has typed. 83 * Data is stored locally in a file in app-specific storage. 84 * 85 * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}. 86 */ 87public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener { 88 private static final String TAG = ResearchLogger.class.getSimpleName(); 89 private static final boolean OUTPUT_ENTIRE_BUFFER = false; // true may disclose private info 90 public static final boolean DEFAULT_USABILITY_STUDY_MODE = false; 91 /* package */ static boolean sIsLogging = false; 92 private static final int OUTPUT_FORMAT_VERSION = 1; 93 private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; 94 private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash"; 95 /* package */ static final String FILENAME_PREFIX = "researchLog"; 96 private static final String FILENAME_SUFFIX = ".txt"; 97 private static final SimpleDateFormat TIMESTAMP_DATEFORMAT = 98 new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); 99 private static final boolean IS_SHOWING_INDICATOR = true; 100 private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false; 101 public static final int FEEDBACK_WORD_BUFFER_SIZE = 5; 102 103 // constants related to specific log points 104 private static final String WHITESPACE_SEPARATORS = " \t\n\r"; 105 private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 106 private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid"; 107 108 private static final ResearchLogger sInstance = new ResearchLogger(); 109 // to write to a different filename, e.g., for testing, set mFile before calling start() 110 /* package */ File mFilesDir; 111 /* package */ String mUUIDString; 112 /* package */ ResearchLog mMainResearchLog; 113 // mFeedbackLog records all events for the session, private or not (excepting 114 // passwords). It is written to permanent storage only if the user explicitly commands 115 // the system to do so. 116 // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are 117 // complete. 118 /* package */ ResearchLog mFeedbackLog; 119 /* package */ MainLogBuffer mMainLogBuffer; 120 /* package */ LogBuffer mFeedbackLogBuffer; 121 122 private boolean mIsPasswordView = false; 123 private boolean mIsLoggingSuspended = false; 124 private SharedPreferences mPrefs; 125 126 // digits entered by the user are replaced with this codepoint. 127 /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT = 128 Character.codePointAt("\uE000", 0); // U+E000 is in the "private-use area" 129 // U+E001 is in the "private-use area" 130 /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001"; 131 private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time"; 132 private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS; 133 private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS; 134 protected static final int SUSPEND_DURATION_IN_MINUTES = 1; 135 // set when LatinIME should ignore an onUpdateSelection() callback that 136 // arises from operations in this class 137 private static boolean sLatinIMEExpectingUpdateSelection = false; 138 139 // used to check whether words are not unique 140 private Suggest mSuggest; 141 private Dictionary mDictionary; 142 private MainKeyboardView mMainKeyboardView; 143 private InputMethodService mInputMethodService; 144 private final Statistics mStatistics; 145 146 private Intent mUploadIntent; 147 private PendingIntent mUploadPendingIntent; 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 mUploadPendingIntent = PendingIntent.getService(mInputMethodService, 0, mUploadIntent, 0); 193 194 if (ProductionFlag.IS_EXPERIMENTAL) { 195 scheduleUploadingService(mInputMethodService); 196 } 197 } 198 199 /** 200 * Arrange for the UploaderService to be run on a regular basis. 201 * 202 * Any existing scheduled invocation of UploaderService is removed and rescheduled. This may 203 * cause problems if this method is called often and frequent updates are required, but since 204 * the user will likely be sleeping at some point, if the interval is less that the expected 205 * sleep duration and this method is not called during that time, the service should be invoked 206 * at some point. 207 */ 208 public static void scheduleUploadingService(Context context) { 209 final Intent intent = new Intent(context, UploaderService.class); 210 final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0); 211 final AlarmManager manager = 212 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 213 manager.cancel(pendingIntent); 214 manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 215 UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent); 216 } 217 218 private void cleanupLoggingDir(final File dir, final long time) { 219 for (File file : dir.listFiles()) { 220 if (file.getName().startsWith(ResearchLogger.FILENAME_PREFIX) && 221 file.lastModified() < time) { 222 file.delete(); 223 } 224 } 225 } 226 227 public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) { 228 mMainKeyboardView = mainKeyboardView; 229 maybeShowSplashScreen(); 230 } 231 232 public void mainKeyboardView_onDetachedFromWindow() { 233 mMainKeyboardView = null; 234 } 235 236 private boolean hasSeenSplash() { 237 return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false); 238 } 239 240 private Dialog mSplashDialog = null; 241 242 private void maybeShowSplashScreen() { 243 if (hasSeenSplash()) { 244 return; 245 } 246 if (mSplashDialog != null && mSplashDialog.isShowing()) { 247 return; 248 } 249 final IBinder windowToken = mMainKeyboardView != null 250 ? mMainKeyboardView.getWindowToken() : null; 251 if (windowToken == null) { 252 return; 253 } 254 mSplashDialog = new Dialog(mInputMethodService, android.R.style.Theme_Holo_Dialog); 255 mSplashDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); 256 mSplashDialog.setContentView(R.layout.research_splash); 257 mSplashDialog.setCancelable(true); 258 final Window w = mSplashDialog.getWindow(); 259 final WindowManager.LayoutParams lp = w.getAttributes(); 260 lp.token = windowToken; 261 lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; 262 w.setAttributes(lp); 263 w.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 264 mSplashDialog.setOnCancelListener(new OnCancelListener() { 265 @Override 266 public void onCancel(DialogInterface dialog) { 267 mInputMethodService.requestHideSelf(0); 268 } 269 }); 270 final Button doNotLogButton = (Button) mSplashDialog.findViewById( 271 R.id.research_do_not_log_button); 272 doNotLogButton.setOnClickListener(new OnClickListener() { 273 @Override 274 public void onClick(View v) { 275 onUserLoggingElection(false); 276 mSplashDialog.dismiss(); 277 } 278 }); 279 final Button doLogButton = (Button) mSplashDialog.findViewById(R.id.research_do_log_button); 280 doLogButton.setOnClickListener(new OnClickListener() { 281 @Override 282 public void onClick(View v) { 283 onUserLoggingElection(true); 284 mSplashDialog.dismiss(); 285 } 286 }); 287 mSplashDialog.show(); 288 } 289 290 public void onUserLoggingElection(final boolean enableLogging) { 291 setLoggingAllowed(enableLogging); 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 maybeShowSplashScreen(); 345 updateSuspendedState(); 346 requestIndicatorRedraw(); 347 mStatistics.reset(); 348 checkForEmptyEditor(); 349 if (!isAllowedToLog()) { 350 // Log.w(TAG, "not in usability mode; not logging"); 351 return; 352 } 353 if (mFilesDir == null || !mFilesDir.exists()) { 354 Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); 355 return; 356 } 357 if (mMainLogBuffer == null) { 358 mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); 359 mMainLogBuffer = new MainLogBuffer(mMainResearchLog); 360 mMainLogBuffer.setSuggest(mSuggest); 361 } 362 if (mFeedbackLogBuffer == null) { 363 mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); 364 // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold 365 // the feedback LogUnit itself. 366 mFeedbackLogBuffer = new LogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1); 367 } 368 } 369 370 /* package */ void stop() { 371 logStatistics(); 372 commitCurrentLogUnit(); 373 374 if (mMainLogBuffer != null) { 375 publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */); 376 mMainResearchLog.close(); 377 mMainLogBuffer = null; 378 } 379 if (mFeedbackLogBuffer != null) { 380 mFeedbackLog.close(); 381 mFeedbackLogBuffer = null; 382 } 383 } 384 385 public boolean abort() { 386 boolean didAbortMainLog = false; 387 if (mMainLogBuffer != null) { 388 mMainLogBuffer.clear(); 389 try { 390 didAbortMainLog = mMainResearchLog.blockingAbort(); 391 } catch (InterruptedException e) { 392 // Don't know whether this succeeded or not. We assume not; this is reported 393 // to the caller. 394 } 395 mMainLogBuffer = null; 396 } 397 boolean didAbortFeedbackLog = false; 398 if (mFeedbackLogBuffer != null) { 399 mFeedbackLogBuffer.clear(); 400 try { 401 didAbortFeedbackLog = mFeedbackLog.blockingAbort(); 402 } catch (InterruptedException e) { 403 // Don't know whether this succeeded or not. We assume not; this is reported 404 // to the caller. 405 } 406 mFeedbackLogBuffer = null; 407 } 408 return didAbortMainLog && didAbortFeedbackLog; 409 } 410 411 private void restart() { 412 stop(); 413 start(); 414 } 415 416 private long mResumeTime = 0L; 417 private void suspendLoggingUntil(long time) { 418 mIsLoggingSuspended = true; 419 mResumeTime = time; 420 requestIndicatorRedraw(); 421 } 422 423 private void resumeLogging() { 424 mResumeTime = 0L; 425 updateSuspendedState(); 426 requestIndicatorRedraw(); 427 if (isAllowedToLog()) { 428 restart(); 429 } 430 } 431 432 private void updateSuspendedState() { 433 final long time = System.currentTimeMillis(); 434 if (time > mResumeTime) { 435 mIsLoggingSuspended = false; 436 } 437 } 438 439 @Override 440 public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 441 if (key == null || prefs == null) { 442 return; 443 } 444 sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); 445 if (sIsLogging == false) { 446 abort(); 447 } 448 requestIndicatorRedraw(); 449 mPrefs = prefs; 450 prefsChanged(prefs); 451 } 452 453 public void presentResearchDialog(final LatinIME latinIME) { 454 if (mInFeedbackDialog) { 455 Toast.makeText(latinIME, R.string.research_please_exit_feedback_form, 456 Toast.LENGTH_LONG).show(); 457 return; 458 } 459 final CharSequence title = latinIME.getString(R.string.english_ime_research_log); 460 final boolean showEnable = mIsLoggingSuspended || !sIsLogging; 461 final CharSequence[] items = new CharSequence[] { 462 latinIME.getString(R.string.research_feedback_menu_option), 463 showEnable ? latinIME.getString(R.string.research_enable_session_logging) : 464 latinIME.getString(R.string.research_do_not_log_this_session) 465 }; 466 final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { 467 @Override 468 public void onClick(DialogInterface di, int position) { 469 di.dismiss(); 470 switch (position) { 471 case 0: 472 presentFeedbackDialog(latinIME); 473 break; 474 case 1: 475 if (showEnable) { 476 if (!sIsLogging) { 477 setLoggingAllowed(true); 478 } 479 resumeLogging(); 480 Toast.makeText(latinIME, 481 R.string.research_notify_session_logging_enabled, 482 Toast.LENGTH_LONG).show(); 483 } else { 484 Toast toast = Toast.makeText(latinIME, 485 R.string.research_notify_session_log_deleting, 486 Toast.LENGTH_LONG); 487 toast.show(); 488 boolean isLogDeleted = abort(); 489 final long currentTime = System.currentTimeMillis(); 490 final long resumeTime = currentTime + 1000 * 60 * 491 SUSPEND_DURATION_IN_MINUTES; 492 suspendLoggingUntil(resumeTime); 493 toast.cancel(); 494 Toast.makeText(latinIME, R.string.research_notify_logging_suspended, 495 Toast.LENGTH_LONG).show(); 496 } 497 break; 498 } 499 } 500 501 }; 502 final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME) 503 .setItems(items, listener) 504 .setTitle(title); 505 latinIME.showOptionDialog(builder.create()); 506 } 507 508 private boolean mInFeedbackDialog = false; 509 public void presentFeedbackDialog(LatinIME latinIME) { 510 mInFeedbackDialog = true; 511 latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class); 512 } 513 514 private static final String[] EVENTKEYS_FEEDBACK = { 515 "UserTimestamp", "contents" 516 }; 517 public void sendFeedback(final String feedbackContents, final boolean includeHistory) { 518 if (mFeedbackLogBuffer == null) { 519 return; 520 } 521 if (includeHistory) { 522 commitCurrentLogUnit(); 523 } else { 524 mFeedbackLogBuffer.clear(); 525 } 526 final LogUnit feedbackLogUnit = new LogUnit(); 527 final Object[] values = { 528 feedbackContents 529 }; 530 feedbackLogUnit.addLogStatement(EVENTKEYS_FEEDBACK, values, 531 false /* isPotentiallyPrivate */); 532 mFeedbackLogBuffer.shiftIn(feedbackLogUnit); 533 publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */); 534 mFeedbackLog.close(); 535 uploadNow(); 536 mFeedbackLog = new ResearchLog(createLogFile(mFilesDir)); 537 } 538 539 public void uploadNow() { 540 mInputMethodService.startService(mUploadIntent); 541 } 542 543 public void onLeavingSendFeedbackDialog() { 544 mInFeedbackDialog = false; 545 } 546 547 public void initSuggest(Suggest suggest) { 548 mSuggest = suggest; 549 if (mMainLogBuffer != null) { 550 mMainLogBuffer.setSuggest(mSuggest); 551 } 552 } 553 554 private void setIsPasswordView(boolean isPasswordView) { 555 mIsPasswordView = isPasswordView; 556 } 557 558 private boolean isAllowedToLog() { 559 return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog; 560 } 561 562 public void requestIndicatorRedraw() { 563 if (!IS_SHOWING_INDICATOR) { 564 return; 565 } 566 if (mMainKeyboardView == null) { 567 return; 568 } 569 mMainKeyboardView.invalidateAllKeys(); 570 } 571 572 573 public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width, 574 int height) { 575 // TODO: Reimplement using a keyboard background image specific to the ResearchLogger 576 // and remove this method. 577 // The check for MainKeyboardView ensures that a red border is only placed around 578 // the main keyboard, not every keyboard. 579 if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) { 580 final int savedColor = paint.getColor(); 581 paint.setColor(Color.RED); 582 final Style savedStyle = paint.getStyle(); 583 paint.setStyle(Style.STROKE); 584 final float savedStrokeWidth = paint.getStrokeWidth(); 585 if (IS_SHOWING_INDICATOR_CLEARLY) { 586 paint.setStrokeWidth(5); 587 canvas.drawRect(0, 0, width, height, paint); 588 } else { 589 // Put a tiny red dot on the screen so a knowledgeable user can check whether 590 // it is enabled. The dot is actually a zero-width, zero-height rectangle, 591 // placed at the lower-right corner of the canvas, painted with a non-zero border 592 // width. 593 paint.setStrokeWidth(3); 594 canvas.drawRect(width, height, width, height, paint); 595 } 596 paint.setColor(savedColor); 597 paint.setStyle(savedStyle); 598 paint.setStrokeWidth(savedStrokeWidth); 599 } 600 } 601 602 private static final Object[] EVENTKEYS_NULLVALUES = {}; 603 604 /** 605 * Buffer a research log event, flagging it as privacy-sensitive. 606 * 607 * This event contains potentially private information. If the word that this event is a part 608 * of is determined to be privacy-sensitive, then this event should not be included in the 609 * output log. The system waits to output until the containing word is known. 610 * 611 * @param keys an array containing a descriptive name for the event, followed by the keys 612 * @param values an array of values, either a String or Number. length should be one 613 * less than the keys array 614 */ 615 private synchronized void enqueuePotentiallyPrivateEvent(final String[] keys, 616 final Object[] values) { 617 assert values.length + 1 == keys.length; 618 if (isAllowedToLog()) { 619 mCurrentLogUnit.addLogStatement(keys, values, true /* isPotentiallyPrivate */); 620 } 621 } 622 623 private void setCurrentLogUnitContainsDigitFlag() { 624 mCurrentLogUnit.setContainsDigit(); 625 } 626 627 /** 628 * Buffer a research log event, flaggint it as not privacy-sensitive. 629 * 630 * This event contains no potentially private information. Even if the word that this event 631 * is privacy-sensitive, this event can still safely be sent to the output log. The system 632 * waits until the containing word is known so that this event can be written in the proper 633 * temporal order with other events that may be privacy sensitive. 634 * 635 * @param keys an array containing a descriptive name for the event, followed by the keys 636 * @param values an array of values, either a String or Number. length should be one 637 * less than the keys array 638 */ 639 private synchronized void enqueueEvent(final String[] keys, final Object[] values) { 640 assert values.length + 1 == keys.length; 641 if (isAllowedToLog()) { 642 mCurrentLogUnit.addLogStatement(keys, values, false /* isPotentiallyPrivate */); 643 } 644 } 645 646 /* package for test */ void commitCurrentLogUnit() { 647 if (!mCurrentLogUnit.isEmpty()) { 648 if (mMainLogBuffer != null) { 649 mMainLogBuffer.shiftIn(mCurrentLogUnit); 650 if (mMainLogBuffer.isSafeToLog() && mMainResearchLog != null) { 651 publishLogBuffer(mMainLogBuffer, mMainResearchLog, 652 true /* isIncludingPrivateData */); 653 mMainLogBuffer.resetWordCounter(); 654 } 655 } 656 if (mFeedbackLogBuffer != null) { 657 mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); 658 } 659 mCurrentLogUnit = new LogUnit(); 660 Log.d(TAG, "commitCurrentLogUnit"); 661 } 662 } 663 664 /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, 665 final ResearchLog researchLog, final boolean isIncludingPrivateData) { 666 LogUnit logUnit; 667 while ((logUnit = logBuffer.shiftOut()) != null) { 668 researchLog.publish(logUnit, isIncludingPrivateData); 669 } 670 } 671 672 private boolean hasOnlyLetters(final String word) { 673 final int length = word.length(); 674 for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { 675 final int codePoint = word.codePointAt(i); 676 if (!Character.isLetter(codePoint)) { 677 return false; 678 } 679 } 680 return true; 681 } 682 683 private void onWordComplete(final String word) { 684 Log.d(TAG, "onWordComplete: " + word); 685 if (word != null && word.length() > 0 && hasOnlyLetters(word)) { 686 mCurrentLogUnit.setWord(word); 687 mStatistics.recordWordEntered(); 688 } 689 commitCurrentLogUnit(); 690 } 691 692 private static int scrubDigitFromCodePoint(int codePoint) { 693 return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint; 694 } 695 696 /* package for test */ static String scrubDigitsFromString(String s) { 697 StringBuilder sb = null; 698 final int length = s.length(); 699 for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) { 700 final int codePoint = Character.codePointAt(s, i); 701 if (Character.isDigit(codePoint)) { 702 if (sb == null) { 703 sb = new StringBuilder(length); 704 sb.append(s.substring(0, i)); 705 } 706 sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT); 707 } else { 708 if (sb != null) { 709 sb.appendCodePoint(codePoint); 710 } 711 } 712 } 713 if (sb == null) { 714 return s; 715 } else { 716 return sb.toString(); 717 } 718 } 719 720 private static String getUUID(final SharedPreferences prefs) { 721 String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null); 722 if (null == uuidString) { 723 UUID uuid = UUID.randomUUID(); 724 uuidString = uuid.toString(); 725 Editor editor = prefs.edit(); 726 editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString); 727 editor.apply(); 728 } 729 return uuidString; 730 } 731 732 private String scrubWord(String word) { 733 if (mDictionary == null) { 734 return WORD_REPLACEMENT_STRING; 735 } 736 if (mDictionary.isValidWord(word)) { 737 return word; 738 } 739 return WORD_REPLACEMENT_STRING; 740 } 741 742 private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = { 743 "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions", 744 "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion" 745 }; 746 public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, 747 final SharedPreferences prefs) { 748 final ResearchLogger researchLogger = getInstance(); 749 researchLogger.start(); 750 if (editorInfo != null) { 751 final Context context = researchLogger.mInputMethodService; 752 try { 753 final PackageInfo packageInfo; 754 packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 755 0); 756 final Integer versionCode = packageInfo.versionCode; 757 final String versionName = packageInfo.versionName; 758 final Object[] values = { 759 researchLogger.mUUIDString, editorInfo.packageName, 760 Integer.toHexString(editorInfo.inputType), 761 Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, 762 Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, 763 OUTPUT_FORMAT_VERSION 764 }; 765 researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values); 766 } catch (NameNotFoundException e) { 767 e.printStackTrace(); 768 } 769 } 770 } 771 772 public void latinIME_onFinishInputInternal() { 773 stop(); 774 } 775 776 private static final String[] EVENTKEYS_USER_FEEDBACK = { 777 "UserFeedback", "FeedbackContents" 778 }; 779 780 private static final String[] EVENTKEYS_PREFS_CHANGED = { 781 "PrefsChanged", "prefs" 782 }; 783 public static void prefsChanged(final SharedPreferences prefs) { 784 final ResearchLogger researchLogger = getInstance(); 785 final Object[] values = { 786 prefs 787 }; 788 researchLogger.enqueueEvent(EVENTKEYS_PREFS_CHANGED, values); 789 } 790 791 // Regular logging methods 792 793 private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT = { 794 "MainKeyboardViewProcessMotionEvent", "action", "eventTime", "id", "x", "y", "size", 795 "pressure" 796 }; 797 public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action, 798 final long eventTime, final int index, final int id, final int x, final int y) { 799 if (me != null) { 800 final String actionString; 801 switch (action) { 802 case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break; 803 case MotionEvent.ACTION_UP: actionString = "UP"; break; 804 case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break; 805 case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break; 806 case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break; 807 case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break; 808 case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break; 809 default: actionString = "ACTION_" + action; break; 810 } 811 final float size = me.getSize(index); 812 final float pressure = me.getPressure(index); 813 final Object[] values = { 814 actionString, eventTime, id, x, y, size, pressure 815 }; 816 getInstance().enqueuePotentiallyPrivateEvent( 817 EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT, values); 818 } 819 } 820 821 private static final String[] EVENTKEYS_LATINIME_ONCODEINPUT = { 822 "LatinIMEOnCodeInput", "code", "x", "y" 823 }; 824 public static void latinIME_onCodeInput(final int code, final int x, final int y) { 825 final long time = SystemClock.uptimeMillis(); 826 final ResearchLogger researchLogger = getInstance(); 827 final Object[] values = { 828 Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y 829 }; 830 researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values); 831 if (Character.isDigit(code)) { 832 researchLogger.setCurrentLogUnitContainsDigitFlag(); 833 } 834 researchLogger.mStatistics.recordChar(code, time); 835 } 836 837 private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = { 838 "LatinIMEOnDisplayCompletions", "applicationSpecifiedCompletions" 839 }; 840 public static void latinIME_onDisplayCompletions( 841 final CompletionInfo[] applicationSpecifiedCompletions) { 842 final Object[] values = { 843 applicationSpecifiedCompletions 844 }; 845 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS, 846 values); 847 } 848 849 public static boolean getAndClearLatinIMEExpectingUpdateSelection() { 850 boolean returnValue = sLatinIMEExpectingUpdateSelection; 851 sLatinIMEExpectingUpdateSelection = false; 852 return returnValue; 853 } 854 855 private static final String[] EVENTKEYS_LATINIME_ONWINDOWHIDDEN = { 856 "LatinIMEOnWindowHidden", "isTextTruncated", "text" 857 }; 858 public static void latinIME_onWindowHidden(final int savedSelectionStart, 859 final int savedSelectionEnd, final InputConnection ic) { 860 if (ic != null) { 861 // Capture the TextView contents. This will trigger onUpdateSelection(), so we 862 // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called, 863 // it can tell that it was generated by the logging code, and not by the user, and 864 // therefore keep user-visible state as is. 865 ic.beginBatchEdit(); 866 ic.performContextMenuAction(android.R.id.selectAll); 867 CharSequence charSequence = ic.getSelectedText(0); 868 ic.setSelection(savedSelectionStart, savedSelectionEnd); 869 ic.endBatchEdit(); 870 sLatinIMEExpectingUpdateSelection = true; 871 final Object[] values = new Object[2]; 872 if (OUTPUT_ENTIRE_BUFFER) { 873 if (TextUtils.isEmpty(charSequence)) { 874 values[0] = false; 875 values[1] = ""; 876 } else { 877 if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) { 878 int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE; 879 // do not cut in the middle of a supplementary character 880 final char c = charSequence.charAt(length - 1); 881 if (Character.isHighSurrogate(c)) { 882 length--; 883 } 884 final CharSequence truncatedCharSequence = charSequence.subSequence(0, 885 length); 886 values[0] = true; 887 values[1] = truncatedCharSequence.toString(); 888 } else { 889 values[0] = false; 890 values[1] = charSequence.toString(); 891 } 892 } 893 } else { 894 values[0] = true; 895 values[1] = ""; 896 } 897 final ResearchLogger researchLogger = getInstance(); 898 researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values); 899 researchLogger.commitCurrentLogUnit(); 900 getInstance().stop(); 901 } 902 } 903 904 private static final String[] EVENTKEYS_LATINIME_ONUPDATESELECTION = { 905 "LatinIMEOnUpdateSelection", "lastSelectionStart", "lastSelectionEnd", "oldSelStart", 906 "oldSelEnd", "newSelStart", "newSelEnd", "composingSpanStart", "composingSpanEnd", 907 "expectingUpdateSelection", "expectingUpdateSelectionFromLogger", "context" 908 }; 909 public static void latinIME_onUpdateSelection(final int lastSelectionStart, 910 final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd, 911 final int newSelStart, final int newSelEnd, final int composingSpanStart, 912 final int composingSpanEnd, final boolean expectingUpdateSelection, 913 final boolean expectingUpdateSelectionFromLogger, 914 final RichInputConnection connection) { 915 String word = ""; 916 if (connection != null) { 917 Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1); 918 if (range != null) { 919 word = range.mWord; 920 } 921 } 922 final ResearchLogger researchLogger = getInstance(); 923 final String scrubbedWord = researchLogger.scrubWord(word); 924 final Object[] values = { 925 lastSelectionStart, lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, 926 newSelEnd, composingSpanStart, composingSpanEnd, expectingUpdateSelection, 927 expectingUpdateSelectionFromLogger, scrubbedWord 928 }; 929 researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values); 930 } 931 932 private static final String[] EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY = { 933 "LatinIMEPickSuggestionManually", "replacedWord", "index", "suggestion", "x", "y" 934 }; 935 public static void latinIME_pickSuggestionManually(final String replacedWord, 936 final int index, CharSequence suggestion) { 937 final Object[] values = { 938 scrubDigitsFromString(replacedWord), index, 939 (suggestion == null ? null : scrubDigitsFromString(suggestion.toString())), 940 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE 941 }; 942 final ResearchLogger researchLogger = getInstance(); 943 researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY, 944 values); 945 } 946 947 private static final String[] EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION = { 948 "LatinIMEPunctuationSuggestion", "index", "suggestion", "x", "y" 949 }; 950 public static void latinIME_punctuationSuggestion(final int index, 951 final CharSequence suggestion) { 952 final Object[] values = { 953 index, suggestion, 954 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE 955 }; 956 getInstance().enqueueEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values); 957 } 958 959 private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = { 960 "LatinIMESendKeyCodePoint", "code" 961 }; 962 public static void latinIME_sendKeyCodePoint(final int code) { 963 final Object[] values = { 964 Keyboard.printableCode(scrubDigitFromCodePoint(code)) 965 }; 966 final ResearchLogger researchLogger = getInstance(); 967 researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); 968 if (Character.isDigit(code)) { 969 researchLogger.setCurrentLogUnitContainsDigitFlag(); 970 } 971 } 972 973 private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = { 974 "LatinIMESwapSwapperAndSpace" 975 }; 976 public static void latinIME_swapSwapperAndSpace() { 977 getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE, EVENTKEYS_NULLVALUES); 978 } 979 980 private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS = { 981 "MainKeyboardViewOnLongPress" 982 }; 983 public static void mainKeyboardView_onLongPress() { 984 getInstance().enqueueEvent(EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS, EVENTKEYS_NULLVALUES); 985 } 986 987 private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_SETKEYBOARD = { 988 "MainKeyboardViewSetKeyboard", "elementId", "locale", "orientation", "width", 989 "modeName", "action", "navigateNext", "navigatePrevious", "clobberSettingsKey", 990 "passwordInput", "shortcutKeyEnabled", "hasShortcutKey", "languageSwitchKeyEnabled", 991 "isMultiLine", "tw", "th", "keys" 992 }; 993 public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) { 994 if (keyboard != null) { 995 final KeyboardId kid = keyboard.mId; 996 final boolean isPasswordView = kid.passwordInput(); 997 getInstance().setIsPasswordView(isPasswordView); 998 final Object[] values = { 999 KeyboardId.elementIdToName(kid.mElementId), 1000 kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), 1001 kid.mOrientation, 1002 kid.mWidth, 1003 KeyboardId.modeName(kid.mMode), 1004 kid.imeAction(), 1005 kid.navigateNext(), 1006 kid.navigatePrevious(), 1007 kid.mClobberSettingsKey, 1008 isPasswordView, 1009 kid.mShortcutKeyEnabled, 1010 kid.mHasShortcutKey, 1011 kid.mLanguageSwitchKeyEnabled, 1012 kid.isMultiLine(), 1013 keyboard.mOccupiedWidth, 1014 keyboard.mOccupiedHeight, 1015 keyboard.mKeys 1016 }; 1017 getInstance().enqueueEvent(EVENTKEYS_MAINKEYBOARDVIEW_SETKEYBOARD, values); 1018 getInstance().setIsPasswordView(isPasswordView); 1019 } 1020 } 1021 1022 private static final String[] EVENTKEYS_LATINIME_REVERTCOMMIT = { 1023 "LatinIMERevertCommit", "originallyTypedWord" 1024 }; 1025 public static void latinIME_revertCommit(final String originallyTypedWord) { 1026 final Object[] values = { 1027 originallyTypedWord 1028 }; 1029 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_REVERTCOMMIT, values); 1030 } 1031 1032 private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT = { 1033 "PointerTrackerCallListenerOnCancelInput" 1034 }; 1035 public static void pointerTracker_callListenerOnCancelInput() { 1036 getInstance().enqueueEvent(EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT, 1037 EVENTKEYS_NULLVALUES); 1038 } 1039 1040 private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT = { 1041 "PointerTrackerCallListenerOnCodeInput", "code", "outputText", "x", "y", 1042 "ignoreModifierKey", "altersCode", "isEnabled" 1043 }; 1044 public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x, 1045 final int y, final boolean ignoreModifierKey, final boolean altersCode, 1046 final int code) { 1047 if (key != null) { 1048 CharSequence outputText = key.mOutputText; 1049 final Object[] values = { 1050 Keyboard.printableCode(scrubDigitFromCodePoint(code)), outputText == null ? null 1051 : scrubDigitsFromString(outputText.toString()), 1052 x, y, ignoreModifierKey, altersCode, key.isEnabled() 1053 }; 1054 getInstance().enqueuePotentiallyPrivateEvent( 1055 EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT, values); 1056 } 1057 } 1058 1059 private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE = { 1060 "PointerTrackerCallListenerOnRelease", "code", "withSliding", "ignoreModifierKey", 1061 "isEnabled" 1062 }; 1063 public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode, 1064 final boolean withSliding, final boolean ignoreModifierKey) { 1065 if (key != null) { 1066 final Object[] values = { 1067 Keyboard.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding, 1068 ignoreModifierKey, key.isEnabled() 1069 }; 1070 getInstance().enqueuePotentiallyPrivateEvent( 1071 EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE, values); 1072 } 1073 } 1074 1075 private static final String[] EVENTKEYS_POINTERTRACKER_ONDOWNEVENT = { 1076 "PointerTrackerOnDownEvent", "deltaT", "distanceSquared" 1077 }; 1078 public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) { 1079 final Object[] values = { 1080 deltaT, distanceSquared 1081 }; 1082 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONDOWNEVENT, values); 1083 } 1084 1085 private static final String[] EVENTKEYS_POINTERTRACKER_ONMOVEEVENT = { 1086 "PointerTrackerOnMoveEvent", "x", "y", "lastX", "lastY" 1087 }; 1088 public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX, 1089 final int lastY) { 1090 final Object[] values = { 1091 x, y, lastX, lastY 1092 }; 1093 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONMOVEEVENT, values); 1094 } 1095 1096 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION = { 1097 "RichInputConnectionCommitCompletion", "completionInfo" 1098 }; 1099 public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) { 1100 final Object[] values = { 1101 completionInfo 1102 }; 1103 final ResearchLogger researchLogger = getInstance(); 1104 researchLogger.enqueuePotentiallyPrivateEvent( 1105 EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION, values); 1106 } 1107 1108 // Disabled for privacy-protection reasons. Because this event comes after 1109 // richInputConnection_commitText, which is the event used to separate LogUnits, the 1110 // data in this event can be associated with the next LogUnit, revealing information 1111 // about the current word even if it was supposed to be suppressed. The occurrance of 1112 // autocorrection can be determined by examining the difference between the text strings in 1113 // the last call to richInputConnection_setComposingText before 1114 // richInputConnection_commitText, so it's not a data loss. 1115 // TODO: Figure out how to log this event without loss of privacy. 1116 /* 1117 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION = { 1118 "RichInputConnectionCommitCorrection", "typedWord", "autoCorrection" 1119 }; 1120 */ 1121 public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) { 1122 /* 1123 final String typedWord = correctionInfo.getOldText().toString(); 1124 final String autoCorrection = correctionInfo.getNewText().toString(); 1125 final Object[] values = { 1126 scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection) 1127 }; 1128 final ResearchLogger researchLogger = getInstance(); 1129 researchLogger.enqueuePotentiallyPrivateEvent( 1130 EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values); 1131 */ 1132 } 1133 1134 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = { 1135 "RichInputConnectionCommitText", "typedWord", "newCursorPosition" 1136 }; 1137 public static void richInputConnection_commitText(final CharSequence typedWord, 1138 final int newCursorPosition) { 1139 final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); 1140 final Object[] values = { 1141 scrubbedWord, newCursorPosition 1142 }; 1143 final ResearchLogger researchLogger = getInstance(); 1144 researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT, 1145 values); 1146 researchLogger.onWordComplete(scrubbedWord); 1147 } 1148 1149 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = { 1150 "RichInputConnectionDeleteSurroundingText", "beforeLength", "afterLength" 1151 }; 1152 public static void richInputConnection_deleteSurroundingText(final int beforeLength, 1153 final int afterLength) { 1154 final Object[] values = { 1155 beforeLength, afterLength 1156 }; 1157 getInstance().enqueuePotentiallyPrivateEvent( 1158 EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values); 1159 } 1160 1161 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = { 1162 "RichInputConnectionFinishComposingText" 1163 }; 1164 public static void richInputConnection_finishComposingText() { 1165 getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT, 1166 EVENTKEYS_NULLVALUES); 1167 } 1168 1169 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION = { 1170 "RichInputConnectionPerformEditorAction", "imeActionNext" 1171 }; 1172 public static void richInputConnection_performEditorAction(final int imeActionNext) { 1173 final Object[] values = { 1174 imeActionNext 1175 }; 1176 getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION, values); 1177 } 1178 1179 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT = { 1180 "RichInputConnectionSendKeyEvent", "eventTime", "action", "code" 1181 }; 1182 public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) { 1183 final Object[] values = { 1184 keyEvent.getEventTime(), 1185 keyEvent.getAction(), 1186 keyEvent.getKeyCode() 1187 }; 1188 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT, 1189 values); 1190 } 1191 1192 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = { 1193 "RichInputConnectionSetComposingText", "text", "newCursorPosition" 1194 }; 1195 public static void richInputConnection_setComposingText(final CharSequence text, 1196 final int newCursorPosition) { 1197 if (text == null) { 1198 throw new RuntimeException("setComposingText is null"); 1199 } 1200 final Object[] values = { 1201 text, newCursorPosition 1202 }; 1203 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, 1204 values); 1205 } 1206 1207 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = { 1208 "RichInputConnectionSetSelection", "from", "to" 1209 }; 1210 public static void richInputConnection_setSelection(final int from, final int to) { 1211 final Object[] values = { 1212 from, to 1213 }; 1214 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION, 1215 values); 1216 } 1217 1218 private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = { 1219 "SuddenJumpingTouchEventHandlerOnTouchEvent", "motionEvent" 1220 }; 1221 public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) { 1222 if (me != null) { 1223 final Object[] values = { 1224 me.toString() 1225 }; 1226 getInstance().enqueuePotentiallyPrivateEvent( 1227 EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, values); 1228 } 1229 } 1230 1231 private static final String[] EVENTKEYS_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS = { 1232 "SuggestionStripViewSetSuggestions", "suggestedWords" 1233 }; 1234 public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) { 1235 if (suggestedWords != null) { 1236 final Object[] values = { 1237 suggestedWords 1238 }; 1239 getInstance().enqueuePotentiallyPrivateEvent( 1240 EVENTKEYS_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS, values); 1241 } 1242 } 1243 1244 private static final String[] EVENTKEYS_USER_TIMESTAMP = { 1245 "UserTimestamp" 1246 }; 1247 public void userTimestamp() { 1248 getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES); 1249 } 1250 1251 private static final String[] EVENTKEYS_STATISTICS = { 1252 "Statistics", "charCount", "letterCount", "numberCount", "spaceCount", "deleteOpsCount", 1253 "wordCount", "isEmptyUponStarting", "isEmptinessStateKnown", "averageTimeBetweenKeys", 1254 "averageTimeBeforeDelete", "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete" 1255 }; 1256 private static void logStatistics() { 1257 final ResearchLogger researchLogger = getInstance(); 1258 final Statistics statistics = researchLogger.mStatistics; 1259 final Object[] values = { 1260 statistics.mCharCount, statistics.mLetterCount, statistics.mNumberCount, 1261 statistics.mSpaceCount, statistics.mDeleteKeyCount, 1262 statistics.mWordCount, statistics.mIsEmptyUponStarting, 1263 statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(), 1264 statistics.mBeforeDeleteKeyCounter.getAverageTime(), 1265 statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(), 1266 statistics.mAfterDeleteKeyCounter.getAverageTime() 1267 }; 1268 researchLogger.enqueueEvent(EVENTKEYS_STATISTICS, values); 1269 } 1270} 1271