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