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