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