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