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