ResearchLogger.java revision 33d5da590f8b733b7bae980f919705b9d975c568
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.inputmethod.research; 18 19import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET; 20 21import android.app.AlarmManager; 22import android.app.AlertDialog; 23import android.app.Dialog; 24import android.app.PendingIntent; 25import android.content.Context; 26import android.content.DialogInterface; 27import android.content.DialogInterface.OnCancelListener; 28import android.content.Intent; 29import android.content.SharedPreferences; 30import android.content.SharedPreferences.Editor; 31import android.content.pm.PackageInfo; 32import android.content.pm.PackageManager.NameNotFoundException; 33import android.graphics.Canvas; 34import android.graphics.Color; 35import android.graphics.Paint; 36import android.graphics.Paint.Style; 37import android.inputmethodservice.InputMethodService; 38import android.net.Uri; 39import android.os.Build; 40import android.os.IBinder; 41import android.os.SystemClock; 42import android.text.TextUtils; 43import android.text.format.DateUtils; 44import android.util.Log; 45import android.view.KeyEvent; 46import android.view.MotionEvent; 47import android.view.Window; 48import android.view.WindowManager; 49import android.view.inputmethod.CompletionInfo; 50import android.view.inputmethod.CorrectionInfo; 51import android.view.inputmethod.EditorInfo; 52import android.view.inputmethod.InputConnection; 53import android.widget.Toast; 54 55import com.android.inputmethod.keyboard.Key; 56import com.android.inputmethod.keyboard.Keyboard; 57import com.android.inputmethod.keyboard.KeyboardId; 58import com.android.inputmethod.keyboard.KeyboardView; 59import com.android.inputmethod.keyboard.MainKeyboardView; 60import com.android.inputmethod.latin.CollectionUtils; 61import com.android.inputmethod.latin.Constants; 62import com.android.inputmethod.latin.Dictionary; 63import com.android.inputmethod.latin.LatinIME; 64import com.android.inputmethod.latin.R; 65import com.android.inputmethod.latin.RichInputConnection; 66import com.android.inputmethod.latin.RichInputConnection.Range; 67import com.android.inputmethod.latin.Suggest; 68import com.android.inputmethod.latin.SuggestedWords; 69import com.android.inputmethod.latin.define.ProductionFlag; 70 71import java.io.File; 72import java.text.SimpleDateFormat; 73import java.util.Date; 74import java.util.Locale; 75import java.util.UUID; 76 77/** 78 * Logs the use of the LatinIME keyboard. 79 * 80 * This class logs operations on the IME keyboard, including what the user has typed. 81 * Data is stored locally in a file in app-specific storage. 82 * 83 * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}. 84 */ 85public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener { 86 private static final String TAG = ResearchLogger.class.getSimpleName(); 87 private static final boolean DEBUG = false; 88 private static final boolean OUTPUT_ENTIRE_BUFFER = false; // true may disclose private info 89 public static final boolean DEFAULT_USABILITY_STUDY_MODE = false; 90 /* package */ static boolean sIsLogging = false; 91 private static final int OUTPUT_FORMAT_VERSION = 1; 92 private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; 93 private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash"; 94 /* package */ static final String FILENAME_PREFIX = "researchLog"; 95 private static final String FILENAME_SUFFIX = ".txt"; 96 private static final SimpleDateFormat TIMESTAMP_DATEFORMAT = 97 new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); 98 private static final boolean IS_SHOWING_INDICATOR = true; 99 private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false; 100 public static final int FEEDBACK_WORD_BUFFER_SIZE = 5; 101 102 // constants related to specific log points 103 private static final String WHITESPACE_SEPARATORS = " \t\n\r"; 104 private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 105 private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid"; 106 107 private static final ResearchLogger sInstance = new ResearchLogger(); 108 // to write to a different filename, e.g., for testing, set mFile before calling start() 109 /* package */ File mFilesDir; 110 /* package */ String mUUIDString; 111 /* package */ ResearchLog mMainResearchLog; 112 // mFeedbackLog records all events for the session, private or not (excepting 113 // passwords). It is written to permanent storage only if the user explicitly commands 114 // the system to do so. 115 // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are 116 // complete. 117 /* package */ ResearchLog mFeedbackLog; 118 /* package */ MainLogBuffer mMainLogBuffer; 119 /* package */ LogBuffer mFeedbackLogBuffer; 120 121 private boolean mIsPasswordView = false; 122 private boolean mIsLoggingSuspended = false; 123 private SharedPreferences mPrefs; 124 125 // digits entered by the user are replaced with this codepoint. 126 /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT = 127 Character.codePointAt("\uE000", 0); // U+E000 is in the "private-use area" 128 // U+E001 is in the "private-use area" 129 /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001"; 130 private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time"; 131 private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS; 132 private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS; 133 protected static final int SUSPEND_DURATION_IN_MINUTES = 1; 134 // set when LatinIME should ignore an onUpdateSelection() callback that 135 // arises from operations in this class 136 private static boolean sLatinIMEExpectingUpdateSelection = false; 137 138 // used to check whether words are not unique 139 private Suggest mSuggest; 140 private Dictionary mDictionary; 141 private MainKeyboardView mMainKeyboardView; 142 private InputMethodService mInputMethodService; 143 private final Statistics mStatistics; 144 145 private Intent mUploadIntent; 146 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 private static final String[] EVENTKEYS_LOG_SEGMENT_START = { 710 "logSegmentStart", "isIncludingPrivateData" 711 }; 712 private static final String[] EVENTKEYS_LOG_SEGMENT_END = { 713 "logSegmentEnd" 714 }; 715 /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, 716 final ResearchLog researchLog, final boolean isIncludingPrivateData) { 717 final LogUnit openingLogUnit = new LogUnit(); 718 final Object[] values = { 719 isIncludingPrivateData 720 }; 721 openingLogUnit.addLogStatement(EVENTKEYS_LOG_SEGMENT_START, values, 722 false /* isPotentiallyPrivate */); 723 researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */); 724 LogUnit logUnit; 725 while ((logUnit = logBuffer.shiftOut()) != null) { 726 researchLog.publish(logUnit, isIncludingPrivateData); 727 } 728 final LogUnit closingLogUnit = new LogUnit(); 729 closingLogUnit.addLogStatement(EVENTKEYS_LOG_SEGMENT_END, EVENTKEYS_NULLVALUES, 730 false /* isPotentiallyPrivate */); 731 researchLog.publish(closingLogUnit, true /* isIncludingPrivateData */); 732 } 733 734 private boolean hasLetters(final String word) { 735 final int length = word.length(); 736 for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { 737 final int codePoint = word.codePointAt(i); 738 if (Character.isLetter(codePoint)) { 739 return true; 740 } 741 } 742 return false; 743 } 744 745 private void onWordComplete(final String word) { 746 Log.d(TAG, "onWordComplete: " + word); 747 if (word != null && word.length() > 0 && hasLetters(word)) { 748 mCurrentLogUnit.setWord(word); 749 mStatistics.recordWordEntered(); 750 } 751 commitCurrentLogUnit(); 752 } 753 754 private static int scrubDigitFromCodePoint(int codePoint) { 755 return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint; 756 } 757 758 /* package for test */ static String scrubDigitsFromString(String s) { 759 StringBuilder sb = null; 760 final int length = s.length(); 761 for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) { 762 final int codePoint = Character.codePointAt(s, i); 763 if (Character.isDigit(codePoint)) { 764 if (sb == null) { 765 sb = new StringBuilder(length); 766 sb.append(s.substring(0, i)); 767 } 768 sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT); 769 } else { 770 if (sb != null) { 771 sb.appendCodePoint(codePoint); 772 } 773 } 774 } 775 if (sb == null) { 776 return s; 777 } else { 778 return sb.toString(); 779 } 780 } 781 782 private static String getUUID(final SharedPreferences prefs) { 783 String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null); 784 if (null == uuidString) { 785 UUID uuid = UUID.randomUUID(); 786 uuidString = uuid.toString(); 787 Editor editor = prefs.edit(); 788 editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString); 789 editor.apply(); 790 } 791 return uuidString; 792 } 793 794 private String scrubWord(String word) { 795 if (mDictionary == null) { 796 return WORD_REPLACEMENT_STRING; 797 } 798 if (mDictionary.isValidWord(word)) { 799 return word; 800 } 801 return WORD_REPLACEMENT_STRING; 802 } 803 804 private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = { 805 "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions", 806 "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion" 807 }; 808 public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, 809 final SharedPreferences prefs) { 810 final ResearchLogger researchLogger = getInstance(); 811 researchLogger.start(); 812 if (editorInfo != null) { 813 final Context context = researchLogger.mInputMethodService; 814 try { 815 final PackageInfo packageInfo; 816 packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 817 0); 818 final Integer versionCode = packageInfo.versionCode; 819 final String versionName = packageInfo.versionName; 820 final Object[] values = { 821 researchLogger.mUUIDString, editorInfo.packageName, 822 Integer.toHexString(editorInfo.inputType), 823 Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, 824 Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, 825 OUTPUT_FORMAT_VERSION 826 }; 827 researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values); 828 } catch (NameNotFoundException e) { 829 e.printStackTrace(); 830 } 831 } 832 } 833 834 public void latinIME_onFinishInputInternal() { 835 stop(); 836 } 837 838 private static final String[] EVENTKEYS_USER_FEEDBACK = { 839 "UserFeedback", "FeedbackContents" 840 }; 841 842 private static final String[] EVENTKEYS_PREFS_CHANGED = { 843 "PrefsChanged", "prefs" 844 }; 845 public static void prefsChanged(final SharedPreferences prefs) { 846 final ResearchLogger researchLogger = getInstance(); 847 final Object[] values = { 848 prefs 849 }; 850 researchLogger.enqueueEvent(EVENTKEYS_PREFS_CHANGED, values); 851 } 852 853 // Regular logging methods 854 855 private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT = { 856 "MainKeyboardViewProcessMotionEvent", "action", "eventTime", "id", "x", "y", "size", 857 "pressure" 858 }; 859 public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action, 860 final long eventTime, final int index, final int id, final int x, final int y) { 861 if (me != null) { 862 final String actionString; 863 switch (action) { 864 case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break; 865 case MotionEvent.ACTION_UP: actionString = "UP"; break; 866 case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break; 867 case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break; 868 case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break; 869 case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break; 870 case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break; 871 default: actionString = "ACTION_" + action; break; 872 } 873 final float size = me.getSize(index); 874 final float pressure = me.getPressure(index); 875 final Object[] values = { 876 actionString, eventTime, id, x, y, size, pressure 877 }; 878 getInstance().enqueuePotentiallyPrivateEvent( 879 EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT, values); 880 } 881 } 882 883 private static final String[] EVENTKEYS_LATINIME_ONCODEINPUT = { 884 "LatinIMEOnCodeInput", "code", "x", "y" 885 }; 886 public static void latinIME_onCodeInput(final int code, final int x, final int y) { 887 final long time = SystemClock.uptimeMillis(); 888 final ResearchLogger researchLogger = getInstance(); 889 final Object[] values = { 890 Constants.printableCode(scrubDigitFromCodePoint(code)), x, y 891 }; 892 researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values); 893 if (Character.isDigit(code)) { 894 researchLogger.setCurrentLogUnitContainsDigitFlag(); 895 } 896 researchLogger.mStatistics.recordChar(code, time); 897 } 898 899 private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = { 900 "LatinIMEOnDisplayCompletions", "applicationSpecifiedCompletions" 901 }; 902 public static void latinIME_onDisplayCompletions( 903 final CompletionInfo[] applicationSpecifiedCompletions) { 904 final Object[] values = { 905 applicationSpecifiedCompletions 906 }; 907 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS, 908 values); 909 } 910 911 public static boolean getAndClearLatinIMEExpectingUpdateSelection() { 912 boolean returnValue = sLatinIMEExpectingUpdateSelection; 913 sLatinIMEExpectingUpdateSelection = false; 914 return returnValue; 915 } 916 917 private static final String[] EVENTKEYS_LATINIME_ONWINDOWHIDDEN = { 918 "LatinIMEOnWindowHidden", "isTextTruncated", "text" 919 }; 920 public static void latinIME_onWindowHidden(final int savedSelectionStart, 921 final int savedSelectionEnd, final InputConnection ic) { 922 if (ic != null) { 923 // Capture the TextView contents. This will trigger onUpdateSelection(), so we 924 // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called, 925 // it can tell that it was generated by the logging code, and not by the user, and 926 // therefore keep user-visible state as is. 927 ic.beginBatchEdit(); 928 ic.performContextMenuAction(android.R.id.selectAll); 929 CharSequence charSequence = ic.getSelectedText(0); 930 ic.setSelection(savedSelectionStart, savedSelectionEnd); 931 ic.endBatchEdit(); 932 sLatinIMEExpectingUpdateSelection = true; 933 final Object[] values = new Object[2]; 934 if (OUTPUT_ENTIRE_BUFFER) { 935 if (TextUtils.isEmpty(charSequence)) { 936 values[0] = false; 937 values[1] = ""; 938 } else { 939 if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) { 940 int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE; 941 // do not cut in the middle of a supplementary character 942 final char c = charSequence.charAt(length - 1); 943 if (Character.isHighSurrogate(c)) { 944 length--; 945 } 946 final CharSequence truncatedCharSequence = charSequence.subSequence(0, 947 length); 948 values[0] = true; 949 values[1] = truncatedCharSequence.toString(); 950 } else { 951 values[0] = false; 952 values[1] = charSequence.toString(); 953 } 954 } 955 } else { 956 values[0] = true; 957 values[1] = ""; 958 } 959 final ResearchLogger researchLogger = getInstance(); 960 researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values); 961 researchLogger.commitCurrentLogUnit(); 962 getInstance().stop(); 963 } 964 } 965 966 private static final String[] EVENTKEYS_LATINIME_ONUPDATESELECTION = { 967 "LatinIMEOnUpdateSelection", "lastSelectionStart", "lastSelectionEnd", "oldSelStart", 968 "oldSelEnd", "newSelStart", "newSelEnd", "composingSpanStart", "composingSpanEnd", 969 "expectingUpdateSelection", "expectingUpdateSelectionFromLogger", "context" 970 }; 971 public static void latinIME_onUpdateSelection(final int lastSelectionStart, 972 final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd, 973 final int newSelStart, final int newSelEnd, final int composingSpanStart, 974 final int composingSpanEnd, final boolean expectingUpdateSelection, 975 final boolean expectingUpdateSelectionFromLogger, 976 final RichInputConnection connection) { 977 String word = ""; 978 if (connection != null) { 979 Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1); 980 if (range != null) { 981 word = range.mWord; 982 } 983 } 984 final ResearchLogger researchLogger = getInstance(); 985 final String scrubbedWord = researchLogger.scrubWord(word); 986 final Object[] values = { 987 lastSelectionStart, lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, 988 newSelEnd, composingSpanStart, composingSpanEnd, expectingUpdateSelection, 989 expectingUpdateSelectionFromLogger, scrubbedWord 990 }; 991 researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values); 992 } 993 994 private static final String[] EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY = { 995 "LatinIMEPickSuggestionManually", "replacedWord", "index", "suggestion", "x", "y" 996 }; 997 public static void latinIME_pickSuggestionManually(final String replacedWord, 998 final int index, CharSequence suggestion) { 999 final Object[] values = { 1000 scrubDigitsFromString(replacedWord), index, 1001 (suggestion == null ? null : scrubDigitsFromString(suggestion.toString())), 1002 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE 1003 }; 1004 final ResearchLogger researchLogger = getInstance(); 1005 researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY, 1006 values); 1007 } 1008 1009 private static final String[] EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION = { 1010 "LatinIMEPunctuationSuggestion", "index", "suggestion", "x", "y" 1011 }; 1012 public static void latinIME_punctuationSuggestion(final int index, 1013 final CharSequence suggestion) { 1014 final Object[] values = { 1015 index, suggestion, 1016 Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE 1017 }; 1018 getInstance().enqueueEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values); 1019 } 1020 1021 private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = { 1022 "LatinIMESendKeyCodePoint", "code" 1023 }; 1024 public static void latinIME_sendKeyCodePoint(final int code) { 1025 final Object[] values = { 1026 Constants.printableCode(scrubDigitFromCodePoint(code)) 1027 }; 1028 final ResearchLogger researchLogger = getInstance(); 1029 researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values); 1030 if (Character.isDigit(code)) { 1031 researchLogger.setCurrentLogUnitContainsDigitFlag(); 1032 } 1033 } 1034 1035 private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = { 1036 "LatinIMESwapSwapperAndSpace" 1037 }; 1038 public static void latinIME_swapSwapperAndSpace() { 1039 getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE, EVENTKEYS_NULLVALUES); 1040 } 1041 1042 private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS = { 1043 "MainKeyboardViewOnLongPress" 1044 }; 1045 public static void mainKeyboardView_onLongPress() { 1046 getInstance().enqueueEvent(EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS, EVENTKEYS_NULLVALUES); 1047 } 1048 1049 private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_SETKEYBOARD = { 1050 "MainKeyboardViewSetKeyboard", "elementId", "locale", "orientation", "width", 1051 "modeName", "action", "navigateNext", "navigatePrevious", "clobberSettingsKey", 1052 "passwordInput", "shortcutKeyEnabled", "hasShortcutKey", "languageSwitchKeyEnabled", 1053 "isMultiLine", "tw", "th", "keys" 1054 }; 1055 public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) { 1056 if (keyboard != null) { 1057 final KeyboardId kid = keyboard.mId; 1058 final boolean isPasswordView = kid.passwordInput(); 1059 getInstance().setIsPasswordView(isPasswordView); 1060 final Object[] values = { 1061 KeyboardId.elementIdToName(kid.mElementId), 1062 kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), 1063 kid.mOrientation, 1064 kid.mWidth, 1065 KeyboardId.modeName(kid.mMode), 1066 kid.imeAction(), 1067 kid.navigateNext(), 1068 kid.navigatePrevious(), 1069 kid.mClobberSettingsKey, 1070 isPasswordView, 1071 kid.mShortcutKeyEnabled, 1072 kid.mHasShortcutKey, 1073 kid.mLanguageSwitchKeyEnabled, 1074 kid.isMultiLine(), 1075 keyboard.mOccupiedWidth, 1076 keyboard.mOccupiedHeight, 1077 keyboard.mKeys 1078 }; 1079 getInstance().setIsPasswordView(isPasswordView); 1080 getInstance().enqueueEvent(EVENTKEYS_MAINKEYBOARDVIEW_SETKEYBOARD, values); 1081 } 1082 } 1083 1084 private static final String[] EVENTKEYS_LATINIME_REVERTCOMMIT = { 1085 "LatinIMERevertCommit", "originallyTypedWord" 1086 }; 1087 public static void latinIME_revertCommit(final String originallyTypedWord) { 1088 final Object[] values = { 1089 originallyTypedWord 1090 }; 1091 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_REVERTCOMMIT, values); 1092 } 1093 1094 private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT = { 1095 "PointerTrackerCallListenerOnCancelInput" 1096 }; 1097 public static void pointerTracker_callListenerOnCancelInput() { 1098 getInstance().enqueueEvent(EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT, 1099 EVENTKEYS_NULLVALUES); 1100 } 1101 1102 private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT = { 1103 "PointerTrackerCallListenerOnCodeInput", "code", "outputText", "x", "y", 1104 "ignoreModifierKey", "altersCode", "isEnabled" 1105 }; 1106 public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x, 1107 final int y, final boolean ignoreModifierKey, final boolean altersCode, 1108 final int code) { 1109 if (key != null) { 1110 String outputText = key.getOutputText(); 1111 final Object[] values = { 1112 Constants.printableCode(scrubDigitFromCodePoint(code)), outputText == null ? null 1113 : scrubDigitsFromString(outputText.toString()), 1114 x, y, ignoreModifierKey, altersCode, key.isEnabled() 1115 }; 1116 getInstance().enqueuePotentiallyPrivateEvent( 1117 EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT, values); 1118 } 1119 } 1120 1121 private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE = { 1122 "PointerTrackerCallListenerOnRelease", "code", "withSliding", "ignoreModifierKey", 1123 "isEnabled" 1124 }; 1125 public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode, 1126 final boolean withSliding, final boolean ignoreModifierKey) { 1127 if (key != null) { 1128 final Object[] values = { 1129 Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding, 1130 ignoreModifierKey, key.isEnabled() 1131 }; 1132 getInstance().enqueuePotentiallyPrivateEvent( 1133 EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE, values); 1134 } 1135 } 1136 1137 private static final String[] EVENTKEYS_POINTERTRACKER_ONDOWNEVENT = { 1138 "PointerTrackerOnDownEvent", "deltaT", "distanceSquared" 1139 }; 1140 public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) { 1141 final Object[] values = { 1142 deltaT, distanceSquared 1143 }; 1144 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONDOWNEVENT, values); 1145 } 1146 1147 private static final String[] EVENTKEYS_POINTERTRACKER_ONMOVEEVENT = { 1148 "PointerTrackerOnMoveEvent", "x", "y", "lastX", "lastY" 1149 }; 1150 public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX, 1151 final int lastY) { 1152 final Object[] values = { 1153 x, y, lastX, lastY 1154 }; 1155 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONMOVEEVENT, values); 1156 } 1157 1158 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION = { 1159 "RichInputConnectionCommitCompletion", "completionInfo" 1160 }; 1161 public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) { 1162 final Object[] values = { 1163 completionInfo 1164 }; 1165 final ResearchLogger researchLogger = getInstance(); 1166 researchLogger.enqueuePotentiallyPrivateEvent( 1167 EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION, values); 1168 } 1169 1170 // Disabled for privacy-protection reasons. Because this event comes after 1171 // richInputConnection_commitText, which is the event used to separate LogUnits, the 1172 // data in this event can be associated with the next LogUnit, revealing information 1173 // about the current word even if it was supposed to be suppressed. The occurrance of 1174 // autocorrection can be determined by examining the difference between the text strings in 1175 // the last call to richInputConnection_setComposingText before 1176 // richInputConnection_commitText, so it's not a data loss. 1177 // TODO: Figure out how to log this event without loss of privacy. 1178 /* 1179 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION = { 1180 "RichInputConnectionCommitCorrection", "typedWord", "autoCorrection" 1181 }; 1182 */ 1183 public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) { 1184 /* 1185 final String typedWord = correctionInfo.getOldText().toString(); 1186 final String autoCorrection = correctionInfo.getNewText().toString(); 1187 final Object[] values = { 1188 scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection) 1189 }; 1190 final ResearchLogger researchLogger = getInstance(); 1191 researchLogger.enqueuePotentiallyPrivateEvent( 1192 EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values); 1193 */ 1194 } 1195 1196 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = { 1197 "RichInputConnectionCommitText", "typedWord", "newCursorPosition" 1198 }; 1199 public static void richInputConnection_commitText(final CharSequence typedWord, 1200 final int newCursorPosition) { 1201 final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); 1202 final Object[] values = { 1203 scrubbedWord, newCursorPosition 1204 }; 1205 final ResearchLogger researchLogger = getInstance(); 1206 researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT, 1207 values); 1208 researchLogger.onWordComplete(scrubbedWord); 1209 } 1210 1211 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = { 1212 "RichInputConnectionDeleteSurroundingText", "beforeLength", "afterLength" 1213 }; 1214 public static void richInputConnection_deleteSurroundingText(final int beforeLength, 1215 final int afterLength) { 1216 final Object[] values = { 1217 beforeLength, afterLength 1218 }; 1219 getInstance().enqueuePotentiallyPrivateEvent( 1220 EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values); 1221 } 1222 1223 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = { 1224 "RichInputConnectionFinishComposingText" 1225 }; 1226 public static void richInputConnection_finishComposingText() { 1227 getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT, 1228 EVENTKEYS_NULLVALUES); 1229 } 1230 1231 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION = { 1232 "RichInputConnectionPerformEditorAction", "imeActionNext" 1233 }; 1234 public static void richInputConnection_performEditorAction(final int imeActionNext) { 1235 final Object[] values = { 1236 imeActionNext 1237 }; 1238 getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION, values); 1239 } 1240 1241 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT = { 1242 "RichInputConnectionSendKeyEvent", "eventTime", "action", "code" 1243 }; 1244 public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) { 1245 final Object[] values = { 1246 keyEvent.getEventTime(), 1247 keyEvent.getAction(), 1248 keyEvent.getKeyCode() 1249 }; 1250 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT, 1251 values); 1252 } 1253 1254 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = { 1255 "RichInputConnectionSetComposingText", "text", "newCursorPosition" 1256 }; 1257 public static void richInputConnection_setComposingText(final CharSequence text, 1258 final int newCursorPosition) { 1259 if (text == null) { 1260 throw new RuntimeException("setComposingText is null"); 1261 } 1262 final Object[] values = { 1263 text, newCursorPosition 1264 }; 1265 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, 1266 values); 1267 } 1268 1269 private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = { 1270 "RichInputConnectionSetSelection", "from", "to" 1271 }; 1272 public static void richInputConnection_setSelection(final int from, final int to) { 1273 final Object[] values = { 1274 from, to 1275 }; 1276 getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION, 1277 values); 1278 } 1279 1280 private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = { 1281 "SuddenJumpingTouchEventHandlerOnTouchEvent", "motionEvent" 1282 }; 1283 public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) { 1284 if (me != null) { 1285 final Object[] values = { 1286 me.toString() 1287 }; 1288 getInstance().enqueuePotentiallyPrivateEvent( 1289 EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, values); 1290 } 1291 } 1292 1293 private static final String[] EVENTKEYS_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS = { 1294 "SuggestionStripViewSetSuggestions", "suggestedWords" 1295 }; 1296 public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) { 1297 if (suggestedWords != null) { 1298 final Object[] values = { 1299 suggestedWords 1300 }; 1301 getInstance().enqueuePotentiallyPrivateEvent( 1302 EVENTKEYS_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS, values); 1303 } 1304 } 1305 1306 private static final String[] EVENTKEYS_USER_TIMESTAMP = { 1307 "UserTimestamp" 1308 }; 1309 public void userTimestamp() { 1310 getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES); 1311 } 1312 1313 private static final String[] EVENTKEYS_STATISTICS = { 1314 "Statistics", "charCount", "letterCount", "numberCount", "spaceCount", "deleteOpsCount", 1315 "wordCount", "isEmptyUponStarting", "isEmptinessStateKnown", "averageTimeBetweenKeys", 1316 "averageTimeBeforeDelete", "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete" 1317 }; 1318 private static void logStatistics() { 1319 final ResearchLogger researchLogger = getInstance(); 1320 final Statistics statistics = researchLogger.mStatistics; 1321 final Object[] values = { 1322 statistics.mCharCount, statistics.mLetterCount, statistics.mNumberCount, 1323 statistics.mSpaceCount, statistics.mDeleteKeyCount, 1324 statistics.mWordCount, statistics.mIsEmptyUponStarting, 1325 statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(), 1326 statistics.mBeforeDeleteKeyCounter.getAverageTime(), 1327 statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(), 1328 statistics.mAfterDeleteKeyCounter.getAverageTime() 1329 }; 1330 researchLogger.enqueueEvent(EVENTKEYS_STATISTICS, values); 1331 } 1332} 1333