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