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