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