ResearchLogger.java revision 522d739524d51ab1c4ecae36067068081c07927f
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.EditorInfo;
51import android.view.inputmethod.InputConnection;
52import android.widget.Toast;
53
54import com.android.inputmethod.keyboard.Key;
55import com.android.inputmethod.keyboard.Keyboard;
56import com.android.inputmethod.keyboard.KeyboardId;
57import com.android.inputmethod.keyboard.KeyboardView;
58import com.android.inputmethod.keyboard.MainKeyboardView;
59import com.android.inputmethod.latin.Constants;
60import com.android.inputmethod.latin.Dictionary;
61import com.android.inputmethod.latin.InputTypeUtils;
62import com.android.inputmethod.latin.LatinIME;
63import com.android.inputmethod.latin.R;
64import com.android.inputmethod.latin.RichInputConnection;
65import com.android.inputmethod.latin.RichInputConnection.Range;
66import com.android.inputmethod.latin.Suggest;
67import com.android.inputmethod.latin.SuggestedWords;
68import com.android.inputmethod.latin.define.ProductionFlag;
69
70import java.io.File;
71import java.text.SimpleDateFormat;
72import java.util.Date;
73import java.util.Locale;
74import java.util.UUID;
75
76/**
77 * Logs the use of the LatinIME keyboard.
78 *
79 * This class logs operations on the IME keyboard, including what the user has typed.
80 * Data is stored locally in a file in app-specific storage.
81 *
82 * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
83 */
84public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
85    private static final String TAG = ResearchLogger.class.getSimpleName();
86    private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
87    private static final boolean LOG_EVERYTHING = false;  // true will disclose private info
88    public static final boolean DEFAULT_USABILITY_STUDY_MODE = false;
89    /* package */ static boolean sIsLogging = false;
90    private static final int OUTPUT_FORMAT_VERSION = 5;
91    private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
92    private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash";
93    /* package */ static final String FILENAME_PREFIX = "researchLog";
94    private static final String FILENAME_SUFFIX = ".txt";
95    private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
96            new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
97    // Whether to show an indicator on the screen that logging is on.  Currently a very small red
98    // dot in the lower right hand corner.  Most users should not notice it.
99    private static final boolean IS_SHOWING_INDICATOR = true;
100    // Change the default indicator to something very visible.  Currently two red vertical bars on
101    // either side of they keyboard.
102    private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false || LOG_EVERYTHING;
103    public static final int FEEDBACK_WORD_BUFFER_SIZE = 5;
104
105    // constants related to specific log points
106    private static final String WHITESPACE_SEPARATORS = " \t\n\r";
107    private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
108    private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid";
109
110    private static final ResearchLogger sInstance = new ResearchLogger();
111    // to write to a different filename, e.g., for testing, set mFile before calling start()
112    /* package */ File mFilesDir;
113    /* package */ String mUUIDString;
114    /* package */ ResearchLog mMainResearchLog;
115    // mFeedbackLog records all events for the session, private or not (excepting
116    // passwords).  It is written to permanent storage only if the user explicitly commands
117    // the system to do so.
118    // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
119    // complete.
120    /* package */ ResearchLog mFeedbackLog;
121    /* package */ MainLogBuffer mMainLogBuffer;
122    /* package */ LogBuffer mFeedbackLogBuffer;
123
124    private boolean mIsPasswordView = false;
125    private boolean mIsLoggingSuspended = false;
126    private SharedPreferences mPrefs;
127
128    // digits entered by the user are replaced with this codepoint.
129    /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT =
130            Character.codePointAt("\uE000", 0);  // U+E000 is in the "private-use area"
131    // U+E001 is in the "private-use area"
132    /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
133    private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time";
134    private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
135    private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS;
136    protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
137    // set when LatinIME should ignore an onUpdateSelection() callback that
138    // arises from operations in this class
139    private static boolean sLatinIMEExpectingUpdateSelection = false;
140
141    // used to check whether words are not unique
142    private Suggest mSuggest;
143    private MainKeyboardView mMainKeyboardView;
144    private InputMethodService mInputMethodService;
145    private final Statistics mStatistics;
146
147    private Intent mUploadIntent;
148
149    private LogUnit mCurrentLogUnit = new LogUnit();
150
151    private ResearchLogger() {
152        mStatistics = Statistics.getInstance();
153    }
154
155    public static ResearchLogger getInstance() {
156        return sInstance;
157    }
158
159    public void init(final InputMethodService ims, final SharedPreferences prefs) {
160        assert ims != null;
161        if (ims == null) {
162            Log.w(TAG, "IMS is null; logging is off");
163        } else {
164            mFilesDir = ims.getFilesDir();
165            if (mFilesDir == null || !mFilesDir.exists()) {
166                Log.w(TAG, "IME storage directory does not exist.");
167            }
168        }
169        if (prefs != null) {
170            mUUIDString = getUUID(prefs);
171            if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) {
172                Editor e = prefs.edit();
173                e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE);
174                e.apply();
175            }
176            sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
177            prefs.registerOnSharedPreferenceChangeListener(this);
178
179            final long lastCleanupTime = prefs.getLong(PREF_LAST_CLEANUP_TIME, 0L);
180            final long now = System.currentTimeMillis();
181            if (lastCleanupTime + DURATION_BETWEEN_DIR_CLEANUP_IN_MS < now) {
182                final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS;
183                cleanupLoggingDir(mFilesDir, timeHorizon);
184                Editor e = prefs.edit();
185                e.putLong(PREF_LAST_CLEANUP_TIME, now);
186                e.apply();
187            }
188        }
189        mInputMethodService = ims;
190        mPrefs = prefs;
191        mUploadIntent = new Intent(mInputMethodService, UploaderService.class);
192
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        commitCurrentLogUnit();
382
383        if (mMainLogBuffer != null) {
384            publishLogBuffer(mMainLogBuffer, mMainResearchLog,
385                    LOG_EVERYTHING /* 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 updateSuspendedState() {
431        final long time = System.currentTimeMillis();
432        if (time > mResumeTime) {
433            mIsLoggingSuspended = false;
434        }
435    }
436
437    @Override
438    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
439        if (key == null || prefs == null) {
440            return;
441        }
442        sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
443        if (sIsLogging == false) {
444            abort();
445        }
446        requestIndicatorRedraw();
447        mPrefs = prefs;
448        prefsChanged(prefs);
449    }
450
451    public void onResearchKeySelected(final LatinIME latinIME) {
452        if (mInFeedbackDialog) {
453            Toast.makeText(latinIME, R.string.research_please_exit_feedback_form,
454                    Toast.LENGTH_LONG).show();
455            return;
456        }
457        presentFeedbackDialog(latinIME);
458    }
459
460    // TODO: currently unreachable.  Remove after being sure no menu is needed.
461    /*
462    public void presentResearchDialog(final LatinIME latinIME) {
463        final CharSequence title = latinIME.getString(R.string.english_ime_research_log);
464        final boolean showEnable = mIsLoggingSuspended || !sIsLogging;
465        final CharSequence[] items = new CharSequence[] {
466                latinIME.getString(R.string.research_feedback_menu_option),
467                showEnable ? latinIME.getString(R.string.research_enable_session_logging) :
468                        latinIME.getString(R.string.research_do_not_log_this_session)
469        };
470        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
471            @Override
472            public void onClick(DialogInterface di, int position) {
473                di.dismiss();
474                switch (position) {
475                    case 0:
476                        presentFeedbackDialog(latinIME);
477                        break;
478                    case 1:
479                        enableOrDisable(showEnable, latinIME);
480                        break;
481                }
482            }
483
484        };
485        final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
486                .setItems(items, listener)
487                .setTitle(title);
488        latinIME.showOptionDialog(builder.create());
489    }
490    */
491
492    private boolean mInFeedbackDialog = false;
493    public void presentFeedbackDialog(LatinIME latinIME) {
494        mInFeedbackDialog = true;
495        latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
496    }
497
498    // TODO: currently unreachable.  Remove after being sure enable/disable is
499    // not needed.
500    /*
501    public void enableOrDisable(final boolean showEnable, final LatinIME latinIME) {
502        if (showEnable) {
503            if (!sIsLogging) {
504                setLoggingAllowed(true);
505            }
506            resumeLogging();
507            Toast.makeText(latinIME,
508                    R.string.research_notify_session_logging_enabled,
509                    Toast.LENGTH_LONG).show();
510        } else {
511            Toast toast = Toast.makeText(latinIME,
512                    R.string.research_notify_session_log_deleting,
513                    Toast.LENGTH_LONG);
514            toast.show();
515            boolean isLogDeleted = abort();
516            final long currentTime = System.currentTimeMillis();
517            final long resumeTime = currentTime + 1000 * 60 *
518                    SUSPEND_DURATION_IN_MINUTES;
519            suspendLoggingUntil(resumeTime);
520            toast.cancel();
521            Toast.makeText(latinIME, R.string.research_notify_logging_suspended,
522                    Toast.LENGTH_LONG).show();
523        }
524    }
525    */
526
527    static class LogStatement {
528        final String mName;
529
530        // mIsPotentiallyPrivate indicates that event contains potentially private information.  If
531        // the word that this event is a part of is determined to be privacy-sensitive, then this
532        // event should not be included in the output log.  The system waits to output until the
533        // containing word is known.
534        final boolean mIsPotentiallyPrivate;
535
536        // mIsPotentiallyRevealing indicates that this statement may disclose details about other
537        // words typed in other LogUnits.  This can happen if the user is not inserting spaces, and
538        // data from Suggestions and/or Composing text reveals the entire "megaword".  For example,
539        // say the user is typing "for the win", and the system wants to record the bigram "the
540        // win".  If the user types "forthe", omitting the space, the system will give "for the" as
541        // a suggestion.  If the user accepts the autocorrection, the suggestion for "for the" is
542        // included in the log for the word "the", disclosing that the previous word had been "for".
543        // For now, we simply do not include this data when logging part of a "megaword".
544        final boolean mIsPotentiallyRevealing;
545
546        // mKeys stores the names that are the attributes in the output json objects
547        final String[] mKeys;
548        private static final String[] NULL_KEYS = new String[0];
549
550        LogStatement(final String name, final boolean isPotentiallyPrivate,
551                final boolean isPotentiallyRevealing, final String... keys) {
552            mName = name;
553            mIsPotentiallyPrivate = isPotentiallyPrivate;
554            mIsPotentiallyRevealing = isPotentiallyRevealing;
555            mKeys = (keys == null) ? NULL_KEYS : keys;
556        }
557    }
558
559    private static final LogStatement LOGSTATEMENT_FEEDBACK =
560            new LogStatement("UserTimestamp", false, false, "contents");
561    public void sendFeedback(final String feedbackContents, final boolean includeHistory) {
562        if (mFeedbackLogBuffer == null) {
563            return;
564        }
565        if (includeHistory) {
566            commitCurrentLogUnit();
567        } else {
568            mFeedbackLogBuffer.clear();
569        }
570        final LogUnit feedbackLogUnit = new LogUnit();
571        feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
572                feedbackContents);
573        mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
574        publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */);
575        mFeedbackLog.close(new Runnable() {
576            @Override
577            public void run() {
578                uploadNow();
579            }
580        });
581        mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
582    }
583
584    public void uploadNow() {
585        if (DEBUG) {
586            Log.d(TAG, "calling uploadNow()");
587        }
588        mInputMethodService.startService(mUploadIntent);
589    }
590
591    public void onLeavingSendFeedbackDialog() {
592        mInFeedbackDialog = false;
593    }
594
595    public void initSuggest(Suggest suggest) {
596        mSuggest = suggest;
597        if (mMainLogBuffer != null) {
598            mMainLogBuffer.setSuggest(mSuggest);
599        }
600    }
601
602    private Dictionary getDictionary() {
603        if (mSuggest == null) {
604            return null;
605        }
606        return mSuggest.getMainDictionary();
607    }
608
609    private void setIsPasswordView(boolean isPasswordView) {
610        mIsPasswordView = isPasswordView;
611    }
612
613    private boolean isAllowedToLog() {
614        if (DEBUG) {
615            Log.d(TAG, "iatl: " +
616                "mipw=" + mIsPasswordView +
617                ", mils=" + mIsLoggingSuspended +
618                ", sil=" + sIsLogging +
619                ", mInFeedbackDialog=" + mInFeedbackDialog);
620        }
621        return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
622    }
623
624    public void requestIndicatorRedraw() {
625        if (!IS_SHOWING_INDICATOR) {
626            return;
627        }
628        if (mMainKeyboardView == null) {
629            return;
630        }
631        mMainKeyboardView.invalidateAllKeys();
632    }
633
634
635    public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width,
636            int height) {
637        // TODO: Reimplement using a keyboard background image specific to the ResearchLogger
638        // and remove this method.
639        // The check for MainKeyboardView ensures that a red border is only placed around
640        // the main keyboard, not every keyboard.
641        if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) {
642            final int savedColor = paint.getColor();
643            paint.setColor(Color.RED);
644            final Style savedStyle = paint.getStyle();
645            paint.setStyle(Style.STROKE);
646            final float savedStrokeWidth = paint.getStrokeWidth();
647            if (IS_SHOWING_INDICATOR_CLEARLY) {
648                paint.setStrokeWidth(5);
649                canvas.drawLine(0, 0, 0, height, paint);
650                canvas.drawLine(width, 0, width, height, paint);
651            } else {
652                // Put a tiny red dot on the screen so a knowledgeable user can check whether
653                // it is enabled.  The dot is actually a zero-width, zero-height rectangle,
654                // placed at the lower-right corner of the canvas, painted with a non-zero border
655                // width.
656                paint.setStrokeWidth(3);
657                canvas.drawRect(width, height, width, height, paint);
658            }
659            paint.setColor(savedColor);
660            paint.setStyle(savedStyle);
661            paint.setStrokeWidth(savedStrokeWidth);
662        }
663    }
664
665    /**
666     * Buffer a research log event, flagging it as privacy-sensitive.
667     */
668    private synchronized void enqueueEvent(LogStatement logStatement, Object... values) {
669        assert values.length == logStatement.mKeys.length;
670        if (isAllowedToLog()) {
671            final long time = SystemClock.uptimeMillis();
672            mCurrentLogUnit.addLogStatement(logStatement, time, values);
673        }
674    }
675
676    private void setCurrentLogUnitContainsDigitFlag() {
677        mCurrentLogUnit.setMayContainDigit();
678    }
679
680    /* package for test */ void commitCurrentLogUnit() {
681        if (DEBUG) {
682            Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasWord() ?
683                    ": " + mCurrentLogUnit.getWord() : ""));
684        }
685        if (!mCurrentLogUnit.isEmpty()) {
686            if (mMainLogBuffer != null) {
687                mMainLogBuffer.shiftIn(mCurrentLogUnit);
688                if ((mMainLogBuffer.isSafeToLog() || LOG_EVERYTHING) && mMainResearchLog != null) {
689                    publishLogBuffer(mMainLogBuffer, mMainResearchLog,
690                            true /* isIncludingPrivateData */);
691                    mMainLogBuffer.resetWordCounter();
692                }
693            }
694            if (mFeedbackLogBuffer != null) {
695                mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
696            }
697            mCurrentLogUnit = new LogUnit();
698        }
699    }
700
701    private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_OPENING =
702            new LogStatement("logSegmentStart", false, false, "isIncludingPrivateData");
703    private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_CLOSING =
704            new LogStatement("logSegmentEnd", false, false);
705    /* package for test */ void publishLogBuffer(final LogBuffer logBuffer,
706            final ResearchLog researchLog, final boolean isIncludingPrivateData) {
707        final LogUnit openingLogUnit = new LogUnit();
708        openingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_OPENING, SystemClock.uptimeMillis(),
709                isIncludingPrivateData);
710        researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */);
711        LogUnit logUnit;
712        while ((logUnit = logBuffer.shiftOut()) != null) {
713            if (DEBUG) {
714                Log.d(TAG, "publishLogBuffer: " + (logUnit.hasWord() ? logUnit.getWord()
715                        : "<wordless>"));
716            }
717            researchLog.publish(logUnit, isIncludingPrivateData);
718        }
719        final LogUnit closingLogUnit = new LogUnit();
720        closingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_CLOSING,
721                SystemClock.uptimeMillis());
722        researchLog.publish(closingLogUnit, true /* isIncludingPrivateData */);
723    }
724
725    public static boolean hasLetters(final String word) {
726        final int length = word.length();
727        for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
728            final int codePoint = word.codePointAt(i);
729            if (Character.isLetter(codePoint)) {
730                return true;
731            }
732        }
733        return false;
734    }
735
736    private static final LogStatement LOGSTATEMENT_COMMIT_RECORD_SPLIT_WORDS =
737            new LogStatement("recordSplitWords", true, false);
738    private void onWordComplete(final String word, final long maxTime, final boolean isSplitWords) {
739        final Dictionary dictionary = getDictionary();
740        if (word != null && word.length() > 0 && hasLetters(word)) {
741            mCurrentLogUnit.setWord(word);
742            final boolean isDictionaryWord = dictionary != null
743                    && dictionary.isValidWord(word);
744            mStatistics.recordWordEntered(isDictionaryWord);
745        }
746        final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime);
747        enqueueCommitText(word);
748        if (isSplitWords) {
749            enqueueEvent(LOGSTATEMENT_COMMIT_RECORD_SPLIT_WORDS);
750            enqueueCommitText(" ");
751            mStatistics.recordSplitWords();
752        }
753        commitCurrentLogUnit();
754        mCurrentLogUnit = newLogUnit;
755    }
756
757    private static int scrubDigitFromCodePoint(int codePoint) {
758        return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint;
759    }
760
761    /* package for test */ static String scrubDigitsFromString(String s) {
762        StringBuilder sb = null;
763        final int length = s.length();
764        for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) {
765            final int codePoint = Character.codePointAt(s, i);
766            if (Character.isDigit(codePoint)) {
767                if (sb == null) {
768                    sb = new StringBuilder(length);
769                    sb.append(s.substring(0, i));
770                }
771                sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT);
772            } else {
773                if (sb != null) {
774                    sb.appendCodePoint(codePoint);
775                }
776            }
777        }
778        if (sb == null) {
779            return s;
780        } else {
781            return sb.toString();
782        }
783    }
784
785    private static String getUUID(final SharedPreferences prefs) {
786        String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null);
787        if (null == uuidString) {
788            UUID uuid = UUID.randomUUID();
789            uuidString = uuid.toString();
790            Editor editor = prefs.edit();
791            editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString);
792            editor.apply();
793        }
794        return uuidString;
795    }
796
797    private String scrubWord(String word) {
798        final Dictionary dictionary = getDictionary();
799        if (dictionary == null) {
800            return WORD_REPLACEMENT_STRING;
801        }
802        if (dictionary.isValidWord(word)) {
803            return word;
804        }
805        return WORD_REPLACEMENT_STRING;
806    }
807
808    private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL =
809            new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid",
810                    "packageName", "inputType", "imeOptions", "fieldId", "display", "model",
811                    "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything",
812                    "isExperimentalDebug");
813    public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
814            final SharedPreferences prefs) {
815        final ResearchLogger researchLogger = getInstance();
816        if (editorInfo != null) {
817            final boolean isPassword = InputTypeUtils.isPasswordInputType(editorInfo.inputType)
818                    || InputTypeUtils.isVisiblePasswordInputType(editorInfo.inputType);
819            getInstance().setIsPasswordView(isPassword);
820            researchLogger.start();
821            final Context context = researchLogger.mInputMethodService;
822            try {
823                final PackageInfo packageInfo;
824                packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),
825                        0);
826                final Integer versionCode = packageInfo.versionCode;
827                final String versionName = packageInfo.versionName;
828                researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL,
829                        researchLogger.mUUIDString, editorInfo.packageName,
830                        Integer.toHexString(editorInfo.inputType),
831                        Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId,
832                        Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName,
833                        OUTPUT_FORMAT_VERSION, LOG_EVERYTHING,
834                        ProductionFlag.IS_EXPERIMENTAL_DEBUG);
835            } catch (NameNotFoundException e) {
836                e.printStackTrace();
837            }
838        }
839    }
840
841    public void latinIME_onFinishInputViewInternal() {
842        logStatistics();
843        stop();
844    }
845
846    private static final LogStatement LOGSTATEMENT_PREFS_CHANGED =
847            new LogStatement("PrefsChanged", false, false, "prefs");
848    public static void prefsChanged(final SharedPreferences prefs) {
849        final ResearchLogger researchLogger = getInstance();
850        researchLogger.enqueueEvent(LOGSTATEMENT_PREFS_CHANGED, prefs);
851    }
852
853    // Regular logging methods
854
855    private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT =
856            new LogStatement("MainKeyboardViewProcessMotionEvent", true, false, "action",
857                    "eventTime", "id", "x", "y", "size", "pressure");
858    public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action,
859            final long eventTime, final int index, final int id, final int x, final int y) {
860        if (me != null) {
861            final String actionString;
862            switch (action) {
863                case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break;
864                case MotionEvent.ACTION_UP: actionString = "UP"; break;
865                case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break;
866                case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break;
867                case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break;
868                case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break;
869                case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break;
870                default: actionString = "ACTION_" + action; break;
871            }
872            final float size = me.getSize(index);
873            final float pressure = me.getPressure(index);
874            getInstance().enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
875                    actionString, eventTime, id, x, y, size, pressure);
876        }
877    }
878
879    private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT =
880            new LogStatement("LatinImeOnCodeInput", true, false, "code", "x", "y");
881    public static void latinIME_onCodeInput(final int code, final int x, final int y) {
882        final long time = SystemClock.uptimeMillis();
883        final ResearchLogger researchLogger = getInstance();
884        researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT,
885                Constants.printableCode(scrubDigitFromCodePoint(code)), x, y);
886        if (Character.isDigit(code)) {
887            researchLogger.setCurrentLogUnitContainsDigitFlag();
888        }
889        researchLogger.mStatistics.recordChar(code, time);
890    }
891    private static final LogStatement LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS =
892            new LogStatement("LatinIMEOnDisplayCompletions", true, true,
893                    "applicationSpecifiedCompletions");
894    public static void latinIME_onDisplayCompletions(
895            final CompletionInfo[] applicationSpecifiedCompletions) {
896        // Note; passing an array as a single element in a vararg list.  Must create a new
897        // dummy array around it or it will get expanded.
898        getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS,
899                new Object[] { applicationSpecifiedCompletions });
900    }
901
902    public static boolean getAndClearLatinIMEExpectingUpdateSelection() {
903        boolean returnValue = sLatinIMEExpectingUpdateSelection;
904        sLatinIMEExpectingUpdateSelection = false;
905        return returnValue;
906    }
907
908    private static final LogStatement LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN =
909            new LogStatement("LatinIMEOnWindowHidden", false, false, "isTextTruncated", "text");
910    public static void latinIME_onWindowHidden(final int savedSelectionStart,
911            final int savedSelectionEnd, final InputConnection ic) {
912        if (ic != null) {
913            final boolean isTextTruncated;
914            final String text;
915            if (LOG_EVERYTHING) {
916                // Capture the TextView contents.  This will trigger onUpdateSelection(), so we
917                // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called,
918                // it can tell that it was generated by the logging code, and not by the user, and
919                // therefore keep user-visible state as is.
920                ic.beginBatchEdit();
921                ic.performContextMenuAction(android.R.id.selectAll);
922                CharSequence charSequence = ic.getSelectedText(0);
923                if (savedSelectionStart != -1 && savedSelectionEnd != -1) {
924                    ic.setSelection(savedSelectionStart, savedSelectionEnd);
925                }
926                ic.endBatchEdit();
927                sLatinIMEExpectingUpdateSelection = true;
928                if (TextUtils.isEmpty(charSequence)) {
929                    isTextTruncated = false;
930                    text = "";
931                } else {
932                    if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) {
933                        int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE;
934                        // do not cut in the middle of a supplementary character
935                        final char c = charSequence.charAt(length - 1);
936                        if (Character.isHighSurrogate(c)) {
937                            length--;
938                        }
939                        final CharSequence truncatedCharSequence = charSequence.subSequence(0,
940                                length);
941                        isTextTruncated = true;
942                        text = truncatedCharSequence.toString();
943                    } else {
944                        isTextTruncated = false;
945                        text = charSequence.toString();
946                    }
947                }
948            } else {
949                isTextTruncated = true;
950                text = "";
951            }
952            final ResearchLogger researchLogger = getInstance();
953            // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g.
954            // during a live user test), so the normal isPotentiallyPrivate and
955            // isPotentiallyRevealing flags do not apply
956            researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN, isTextTruncated,
957                    text);
958            researchLogger.commitCurrentLogUnit();
959            getInstance().stop();
960        }
961    }
962
963    private static final LogStatement LOGSTATEMENT_LATINIME_ONUPDATESELECTION =
964            new LogStatement("LatinIMEOnUpdateSelection", true, false, "lastSelectionStart",
965                    "lastSelectionEnd", "oldSelStart", "oldSelEnd", "newSelStart", "newSelEnd",
966                    "composingSpanStart", "composingSpanEnd", "expectingUpdateSelection",
967                    "expectingUpdateSelectionFromLogger", "context");
968    public static void latinIME_onUpdateSelection(final int lastSelectionStart,
969            final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
970            final int newSelStart, final int newSelEnd, final int composingSpanStart,
971            final int composingSpanEnd, final boolean expectingUpdateSelection,
972            final boolean expectingUpdateSelectionFromLogger,
973            final RichInputConnection connection) {
974        String word = "";
975        if (connection != null) {
976            Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
977            if (range != null) {
978                word = range.mWord;
979            }
980        }
981        final ResearchLogger researchLogger = getInstance();
982        final String scrubbedWord = researchLogger.scrubWord(word);
983        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONUPDATESELECTION, lastSelectionStart,
984                lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd,
985                composingSpanStart, composingSpanEnd, expectingUpdateSelection,
986                expectingUpdateSelectionFromLogger, scrubbedWord);
987    }
988
989    private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY =
990            new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index",
991                    "suggestion", "x", "y");
992    public static void latinIME_pickSuggestionManually(final String replacedWord,
993            final int index, CharSequence suggestion) {
994        final ResearchLogger researchLogger = getInstance();
995        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY,
996                scrubDigitsFromString(replacedWord), index,
997                suggestion == null ? null : scrubDigitsFromString(suggestion.toString()),
998                Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
999    }
1000
1001    private static final LogStatement LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION =
1002            new LogStatement("LatinIMEPunctuationSuggestion", false, false, "index", "suggestion",
1003                    "x", "y");
1004    public static void latinIME_punctuationSuggestion(final int index,
1005            final CharSequence suggestion) {
1006        getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion,
1007                Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
1008    }
1009
1010    private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT =
1011            new LogStatement("LatinIMESendKeyCodePoint", true, false, "code");
1012    public static void latinIME_sendKeyCodePoint(final int code) {
1013        final ResearchLogger researchLogger = getInstance();
1014        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT,
1015                Constants.printableCode(scrubDigitFromCodePoint(code)));
1016        if (Character.isDigit(code)) {
1017            researchLogger.setCurrentLogUnitContainsDigitFlag();
1018        }
1019    }
1020
1021    private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE =
1022            new LogStatement("LatinIMESwapSwapperAndSpace", false, false);
1023    public static void latinIME_swapSwapperAndSpace() {
1024        getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE);
1025    }
1026
1027    private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS =
1028            new LogStatement("MainKeyboardViewOnLongPress", false, false);
1029    public static void mainKeyboardView_onLongPress() {
1030        getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS);
1031    }
1032
1033    private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD =
1034            new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale",
1035                    "orientation", "width", "modeName", "action", "navigateNext",
1036                    "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled",
1037                    "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th",
1038                    "keys");
1039    public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) {
1040        final KeyboardId kid = keyboard.mId;
1041        final boolean isPasswordView = kid.passwordInput();
1042        getInstance().setIsPasswordView(isPasswordView);
1043        getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD,
1044                KeyboardId.elementIdToName(kid.mElementId),
1045                kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
1046                kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(),
1047                kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey,
1048                isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey,
1049                kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth,
1050                keyboard.mOccupiedHeight, keyboard.mKeys);
1051    }
1052
1053    private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT =
1054            new LogStatement("LatinIMERevertCommit", true, false, "originallyTypedWord");
1055    public static void latinIME_revertCommit(final String originallyTypedWord) {
1056        getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_REVERTCOMMIT, originallyTypedWord);
1057    }
1058
1059    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT =
1060            new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false);
1061    public static void pointerTracker_callListenerOnCancelInput() {
1062        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT);
1063    }
1064
1065    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT =
1066            new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "code",
1067                    "outputText", "x", "y", "ignoreModifierKey", "altersCode", "isEnabled");
1068    public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
1069            final int y, final boolean ignoreModifierKey, final boolean altersCode,
1070            final int code) {
1071        if (key != null) {
1072            String outputText = key.getOutputText();
1073            getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
1074                    Constants.printableCode(scrubDigitFromCodePoint(code)),
1075                    outputText == null ? null : scrubDigitsFromString(outputText.toString()),
1076                    x, y, ignoreModifierKey, altersCode, key.isEnabled());
1077        }
1078    }
1079
1080    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE =
1081            new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code",
1082                    "withSliding", "ignoreModifierKey", "isEnabled");
1083    public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
1084            final boolean withSliding, final boolean ignoreModifierKey) {
1085        if (key != null) {
1086            getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE,
1087                    Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding,
1088                    ignoreModifierKey, key.isEnabled());
1089        }
1090    }
1091
1092    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT =
1093            new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared");
1094    public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
1095        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT, deltaT,
1096                distanceSquared);
1097    }
1098
1099    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT =
1100            new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY");
1101    public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
1102            final int lastY) {
1103        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY);
1104    }
1105
1106    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION =
1107            new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo");
1108    public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) {
1109        final ResearchLogger researchLogger = getInstance();
1110        researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION,
1111                completionInfo);
1112    }
1113
1114    private boolean isExpectingCommitText = false;
1115    public static void latinIME_commitPartialText(final CharSequence committedWord,
1116            final long lastTimestampOfWordData) {
1117        final ResearchLogger researchLogger = getInstance();
1118        final String scrubbedWord = scrubDigitsFromString(committedWord.toString());
1119        researchLogger.onWordComplete(scrubbedWord, lastTimestampOfWordData, true /* isPartial */);
1120        researchLogger.isExpectingCommitText = true;
1121    }
1122
1123    private static final LogStatement LOGSTATEMENT_COMMITTEXT_UPDATECURSOR =
1124            new LogStatement("CommitTextUpdateCursor", true, false, "newCursorPosition");
1125    public static void richInputConnection_commitText(final CharSequence committedWord,
1126            final int newCursorPosition) {
1127        final ResearchLogger researchLogger = getInstance();
1128        final String scrubbedWord = scrubDigitsFromString(committedWord.toString());
1129        if (!researchLogger.isExpectingCommitText) {
1130            researchLogger.onWordComplete(scrubbedWord, Long.MAX_VALUE, false /* isPartial */);
1131            researchLogger.enqueueEvent(LOGSTATEMENT_COMMITTEXT_UPDATECURSOR, newCursorPosition);
1132        }
1133        researchLogger.isExpectingCommitText = false;
1134    }
1135
1136    private static final LogStatement LOGSTATEMENT_COMMITTEXT =
1137            new LogStatement("CommitText", true, false, "committedText");
1138    private void enqueueCommitText(final CharSequence word) {
1139        enqueueEvent(LOGSTATEMENT_COMMITTEXT, word);
1140    }
1141
1142    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT =
1143            new LogStatement("RichInputConnectionDeleteSurroundingText", true, false,
1144                    "beforeLength", "afterLength");
1145    public static void richInputConnection_deleteSurroundingText(final int beforeLength,
1146            final int afterLength) {
1147        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT,
1148                beforeLength, afterLength);
1149    }
1150
1151    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT =
1152            new LogStatement("RichInputConnectionFinishComposingText", false, false);
1153    public static void richInputConnection_finishComposingText() {
1154        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT);
1155    }
1156
1157    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION =
1158            new LogStatement("RichInputConnectionPerformEditorAction", false, false,
1159                    "imeActionNext");
1160    public static void richInputConnection_performEditorAction(final int imeActionNext) {
1161        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION,
1162                imeActionNext);
1163    }
1164
1165    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT =
1166            new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action",
1167                    "code");
1168    public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) {
1169        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT,
1170                keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode());
1171    }
1172
1173    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT =
1174            new LogStatement("RichInputConnectionSetComposingText", true, true, "text",
1175                    "newCursorPosition");
1176    public static void richInputConnection_setComposingText(final CharSequence text,
1177            final int newCursorPosition) {
1178        if (text == null) {
1179            throw new RuntimeException("setComposingText is null");
1180        }
1181        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, text,
1182                newCursorPosition);
1183    }
1184
1185    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION =
1186            new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to");
1187    public static void richInputConnection_setSelection(final int from, final int to) {
1188        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to);
1189    }
1190
1191    private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT =
1192            new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false,
1193                    "motionEvent");
1194    public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
1195        if (me != null) {
1196            getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT,
1197                    me.toString());
1198        }
1199    }
1200
1201    private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS =
1202            new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords");
1203    public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) {
1204        if (suggestedWords != null) {
1205            getInstance().enqueueEvent(LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS,
1206                    suggestedWords);
1207        }
1208    }
1209
1210    private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP =
1211            new LogStatement("UserTimestamp", false, false);
1212    public void userTimestamp() {
1213        getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP);
1214    }
1215
1216    private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT =
1217            new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText",
1218                    "enteredWordPos");
1219    public static void latinIME_onEndBatchInput(final CharSequence enteredText,
1220            final int enteredWordPos) {
1221        final ResearchLogger researchLogger = getInstance();
1222        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText,
1223                enteredWordPos);
1224        researchLogger.mStatistics.recordGestureInput();
1225    }
1226
1227    private static final LogStatement LOGSTATEMENT_STATISTICS =
1228            new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount",
1229                    "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting",
1230                    "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete",
1231                    "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete",
1232                    "dictionaryWordCount", "splitWordsCount", "gestureInputCount");
1233    private static void logStatistics() {
1234        final ResearchLogger researchLogger = getInstance();
1235        final Statistics statistics = researchLogger.mStatistics;
1236        researchLogger.enqueueEvent(LOGSTATEMENT_STATISTICS, statistics.mCharCount,
1237                statistics.mLetterCount, statistics.mNumberCount, statistics.mSpaceCount,
1238                statistics.mDeleteKeyCount, statistics.mWordCount, statistics.mIsEmptyUponStarting,
1239                statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(),
1240                statistics.mBeforeDeleteKeyCounter.getAverageTime(),
1241                statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(),
1242                statistics.mAfterDeleteKeyCounter.getAverageTime(),
1243                statistics.mDictionaryWordCount, statistics.mSplitWordsCount,
1244                statistics.mGestureInputCount);
1245    }
1246}
1247