ResearchLogger.java revision 3338703a2fe8fa3ae549e1d884d9fb5a579a7f74
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.net.Uri;
38import android.os.Build;
39import android.os.IBinder;
40import android.os.SystemClock;
41import android.preference.PreferenceManager;
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    // Whether all n-grams should be logged.  true will disclose private info.
88    public static final boolean IS_LOGGING_EVERYTHING = false
89            && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
90    // Whether the TextView contents are logged at the end of the session.  true will disclose
91    // private info.
92    private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false
93            && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
94    public static final boolean DEFAULT_USABILITY_STUDY_MODE = false;
95    /* package */ static boolean sIsLogging = false;
96    private static final int OUTPUT_FORMAT_VERSION = 5;
97    private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
98    private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash";
99    /* package */ static final String FILENAME_PREFIX = "researchLog";
100    private static final String FILENAME_SUFFIX = ".txt";
101    private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
102            new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
103    // Whether to show an indicator on the screen that logging is on.  Currently a very small red
104    // dot in the lower right hand corner.  Most users should not notice it.
105    private static final boolean IS_SHOWING_INDICATOR = true;
106    // Change the default indicator to something very visible.  Currently two red vertical bars on
107    // either side of they keyboard.
108    private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false || IS_LOGGING_EVERYTHING;
109    public static final int FEEDBACK_WORD_BUFFER_SIZE = 5;
110
111    // constants related to specific log points
112    private static final String WHITESPACE_SEPARATORS = " \t\n\r";
113    private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
114    private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid";
115
116    private static final ResearchLogger sInstance = new ResearchLogger();
117    // to write to a different filename, e.g., for testing, set mFile before calling start()
118    /* package */ File mFilesDir;
119    /* package */ String mUUIDString;
120    /* package */ ResearchLog mMainResearchLog;
121    // mFeedbackLog records all events for the session, private or not (excepting
122    // passwords).  It is written to permanent storage only if the user explicitly commands
123    // the system to do so.
124    // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
125    // complete.
126    /* package */ ResearchLog mFeedbackLog;
127    /* package */ MainLogBuffer mMainLogBuffer;
128    /* package */ LogBuffer mFeedbackLogBuffer;
129
130    private boolean mIsPasswordView = false;
131    private boolean mIsLoggingSuspended = false;
132    private SharedPreferences mPrefs;
133
134    // digits entered by the user are replaced with this codepoint.
135    /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT =
136            Character.codePointAt("\uE000", 0);  // U+E000 is in the "private-use area"
137    // U+E001 is in the "private-use area"
138    /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
139    private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time";
140    private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
141    private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS;
142    protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
143    // set when LatinIME should ignore an onUpdateSelection() callback that
144    // arises from operations in this class
145    private static boolean sLatinIMEExpectingUpdateSelection = false;
146
147    // used to check whether words are not unique
148    private Suggest mSuggest;
149    private MainKeyboardView mMainKeyboardView;
150    private LatinIME mLatinIME;
151    private final Statistics mStatistics;
152
153    private Intent mUploadIntent;
154
155    private LogUnit mCurrentLogUnit = new LogUnit();
156
157    // Gestured or tapped words may be committed after the gesture of the next word has started.
158    // To ensure that the gesture data of the next word is not associated with the previous word,
159    // thereby leaking private data, we store the time of the down event that started the second
160    // gesture, and when committing the earlier word, split the LogUnit.
161    private long mSavedDownEventTime;
162    private ResearchLogger() {
163        mStatistics = Statistics.getInstance();
164    }
165
166    public static ResearchLogger getInstance() {
167        return sInstance;
168    }
169
170    public void init(final LatinIME latinIME) {
171        assert latinIME != null;
172        if (latinIME == null) {
173            Log.w(TAG, "IMS is null; logging is off");
174        } else {
175            mFilesDir = latinIME.getFilesDir();
176            if (mFilesDir == null || !mFilesDir.exists()) {
177                Log.w(TAG, "IME storage directory does not exist.");
178            }
179        }
180        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(latinIME);
181        if (prefs != null) {
182            mUUIDString = getUUID(prefs);
183            if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) {
184                Editor e = prefs.edit();
185                e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE);
186                e.apply();
187            }
188            sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
189            prefs.registerOnSharedPreferenceChangeListener(this);
190
191            final long lastCleanupTime = prefs.getLong(PREF_LAST_CLEANUP_TIME, 0L);
192            final long now = System.currentTimeMillis();
193            if (lastCleanupTime + DURATION_BETWEEN_DIR_CLEANUP_IN_MS < now) {
194                final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS;
195                cleanupLoggingDir(mFilesDir, timeHorizon);
196                Editor e = prefs.edit();
197                e.putLong(PREF_LAST_CLEANUP_TIME, now);
198                e.apply();
199            }
200        }
201        mLatinIME = latinIME;
202        mPrefs = prefs;
203        mUploadIntent = new Intent(mLatinIME, UploaderService.class);
204
205        if (ProductionFlag.IS_EXPERIMENTAL) {
206            scheduleUploadingService(mLatinIME);
207        }
208    }
209
210    /**
211     * Arrange for the UploaderService to be run on a regular basis.
212     *
213     * Any existing scheduled invocation of UploaderService is removed and rescheduled.  This may
214     * cause problems if this method is called often and frequent updates are required, but since
215     * the user will likely be sleeping at some point, if the interval is less that the expected
216     * sleep duration and this method is not called during that time, the service should be invoked
217     * at some point.
218     */
219    public static void scheduleUploadingService(Context context) {
220        final Intent intent = new Intent(context, UploaderService.class);
221        final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
222        final AlarmManager manager =
223                (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
224        manager.cancel(pendingIntent);
225        manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
226                UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent);
227    }
228
229    private void cleanupLoggingDir(final File dir, final long time) {
230        for (File file : dir.listFiles()) {
231            if (file.getName().startsWith(ResearchLogger.FILENAME_PREFIX) &&
232                    file.lastModified() < time) {
233                file.delete();
234            }
235        }
236    }
237
238    public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) {
239        mMainKeyboardView = mainKeyboardView;
240        maybeShowSplashScreen();
241    }
242
243    public void mainKeyboardView_onDetachedFromWindow() {
244        mMainKeyboardView = null;
245    }
246
247    private boolean hasSeenSplash() {
248        return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false);
249    }
250
251    private Dialog mSplashDialog = null;
252
253    private void maybeShowSplashScreen() {
254        if (hasSeenSplash()) {
255            return;
256        }
257        if (mSplashDialog != null && mSplashDialog.isShowing()) {
258            return;
259        }
260        final IBinder windowToken = mMainKeyboardView != null
261                ? mMainKeyboardView.getWindowToken() : null;
262        if (windowToken == null) {
263            return;
264        }
265        final AlertDialog.Builder builder = new AlertDialog.Builder(mLatinIME)
266                .setTitle(R.string.research_splash_title)
267                .setMessage(R.string.research_splash_content)
268                .setPositiveButton(android.R.string.yes,
269                        new DialogInterface.OnClickListener() {
270                            @Override
271                            public void onClick(DialogInterface dialog, int which) {
272                                onUserLoggingConsent();
273                                mSplashDialog.dismiss();
274                            }
275                })
276                .setNegativeButton(android.R.string.no,
277                        new DialogInterface.OnClickListener() {
278                            @Override
279                            public void onClick(DialogInterface dialog, int which) {
280                                final String packageName = mLatinIME.getPackageName();
281                                final Uri packageUri = Uri.parse("package:" + packageName);
282                                final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE,
283                                        packageUri);
284                                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
285                                mLatinIME.startActivity(intent);
286                            }
287                })
288                .setCancelable(true)
289                .setOnCancelListener(
290                        new OnCancelListener() {
291                            @Override
292                            public void onCancel(DialogInterface dialog) {
293                                mLatinIME.requestHideSelf(0);
294                            }
295                });
296        mSplashDialog = builder.create();
297        final Window w = mSplashDialog.getWindow();
298        final WindowManager.LayoutParams lp = w.getAttributes();
299        lp.token = windowToken;
300        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
301        w.setAttributes(lp);
302        w.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
303        mSplashDialog.show();
304    }
305
306    public void onUserLoggingConsent() {
307        setLoggingAllowed(true);
308        if (mPrefs == null) {
309            return;
310        }
311        final Editor e = mPrefs.edit();
312        e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true);
313        e.apply();
314        restart();
315    }
316
317    private void setLoggingAllowed(boolean enableLogging) {
318        if (mPrefs == null) {
319            return;
320        }
321        Editor e = mPrefs.edit();
322        e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging);
323        e.apply();
324        sIsLogging = enableLogging;
325    }
326
327    private File createLogFile(File filesDir) {
328        final StringBuilder sb = new StringBuilder();
329        sb.append(FILENAME_PREFIX).append('-');
330        sb.append(mUUIDString).append('-');
331        sb.append(TIMESTAMP_DATEFORMAT.format(new Date()));
332        sb.append(FILENAME_SUFFIX);
333        return new File(filesDir, sb.toString());
334    }
335
336    private void checkForEmptyEditor() {
337        if (mLatinIME == null) {
338            return;
339        }
340        final InputConnection ic = mLatinIME.getCurrentInputConnection();
341        if (ic == null) {
342            return;
343        }
344        final CharSequence textBefore = ic.getTextBeforeCursor(1, 0);
345        if (!TextUtils.isEmpty(textBefore)) {
346            mStatistics.setIsEmptyUponStarting(false);
347            return;
348        }
349        final CharSequence textAfter = ic.getTextAfterCursor(1, 0);
350        if (!TextUtils.isEmpty(textAfter)) {
351            mStatistics.setIsEmptyUponStarting(false);
352            return;
353        }
354        if (textBefore != null && textAfter != null) {
355            mStatistics.setIsEmptyUponStarting(true);
356        }
357    }
358
359    private void start() {
360        if (DEBUG) {
361            Log.d(TAG, "start called");
362        }
363        maybeShowSplashScreen();
364        updateSuspendedState();
365        requestIndicatorRedraw();
366        mStatistics.reset();
367        checkForEmptyEditor();
368        if (!isAllowedToLog()) {
369            // Log.w(TAG, "not in usability mode; not logging");
370            return;
371        }
372        if (mFilesDir == null || !mFilesDir.exists()) {
373            Log.w(TAG, "IME storage directory does not exist.  Cannot start logging.");
374            return;
375        }
376        if (mMainLogBuffer == null) {
377            mMainResearchLog = new ResearchLog(createLogFile(mFilesDir));
378            mMainLogBuffer = new MainLogBuffer(mMainResearchLog);
379            mMainLogBuffer.setSuggest(mSuggest);
380        }
381        if (mFeedbackLogBuffer == null) {
382            mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
383            // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold
384            // the feedback LogUnit itself.
385            mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1);
386        }
387    }
388
389    /* package */ void stop() {
390        if (DEBUG) {
391            Log.d(TAG, "stop called");
392        }
393        // Commit mCurrentLogUnit before closing.
394        commitCurrentLogUnit();
395
396        if (mMainLogBuffer != null) {
397            while (!mMainLogBuffer.isEmpty()) {
398                if ((mMainLogBuffer.isNGramSafe() || IS_LOGGING_EVERYTHING) &&
399                        mMainResearchLog != null) {
400                    publishLogBuffer(mMainLogBuffer, mMainResearchLog,
401                            true /* isIncludingPrivateData */);
402                    mMainLogBuffer.resetWordCounter();
403                } else {
404                    mMainLogBuffer.shiftOutThroughFirstWord();
405                }
406            }
407            mMainResearchLog.close(null /* callback */);
408            mMainLogBuffer = null;
409        }
410        if (mFeedbackLogBuffer != null) {
411            mFeedbackLog.close(null /* callback */);
412            mFeedbackLogBuffer = null;
413        }
414    }
415
416    public boolean abort() {
417        if (DEBUG) {
418            Log.d(TAG, "abort called");
419        }
420        boolean didAbortMainLog = false;
421        if (mMainLogBuffer != null) {
422            mMainLogBuffer.clear();
423            try {
424                didAbortMainLog = mMainResearchLog.blockingAbort();
425            } catch (InterruptedException e) {
426                // Don't know whether this succeeded or not.  We assume not; this is reported
427                // to the caller.
428            }
429            mMainLogBuffer = null;
430        }
431        boolean didAbortFeedbackLog = false;
432        if (mFeedbackLogBuffer != null) {
433            mFeedbackLogBuffer.clear();
434            try {
435                didAbortFeedbackLog = mFeedbackLog.blockingAbort();
436            } catch (InterruptedException e) {
437                // Don't know whether this succeeded or not.  We assume not; this is reported
438                // to the caller.
439            }
440            mFeedbackLogBuffer = null;
441        }
442        return didAbortMainLog && didAbortFeedbackLog;
443    }
444
445    private void restart() {
446        stop();
447        start();
448    }
449
450    private long mResumeTime = 0L;
451    private void updateSuspendedState() {
452        final long time = System.currentTimeMillis();
453        if (time > mResumeTime) {
454            mIsLoggingSuspended = false;
455        }
456    }
457
458    @Override
459    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
460        if (key == null || prefs == null) {
461            return;
462        }
463        sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
464        if (sIsLogging == false) {
465            abort();
466        }
467        requestIndicatorRedraw();
468        mPrefs = prefs;
469        prefsChanged(prefs);
470    }
471
472    public void onResearchKeySelected(final LatinIME latinIME) {
473        if (mInFeedbackDialog) {
474            Toast.makeText(latinIME, R.string.research_please_exit_feedback_form,
475                    Toast.LENGTH_LONG).show();
476            return;
477        }
478        presentFeedbackDialog(latinIME);
479    }
480
481    // TODO: currently unreachable.  Remove after being sure no menu is needed.
482    /*
483    public void presentResearchDialog(final LatinIME latinIME) {
484        final CharSequence title = latinIME.getString(R.string.english_ime_research_log);
485        final boolean showEnable = mIsLoggingSuspended || !sIsLogging;
486        final CharSequence[] items = new CharSequence[] {
487                latinIME.getString(R.string.research_feedback_menu_option),
488                showEnable ? latinIME.getString(R.string.research_enable_session_logging) :
489                        latinIME.getString(R.string.research_do_not_log_this_session)
490        };
491        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
492            @Override
493            public void onClick(DialogInterface di, int position) {
494                di.dismiss();
495                switch (position) {
496                    case 0:
497                        presentFeedbackDialog(latinIME);
498                        break;
499                    case 1:
500                        enableOrDisable(showEnable, latinIME);
501                        break;
502                }
503            }
504
505        };
506        final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
507                .setItems(items, listener)
508                .setTitle(title);
509        latinIME.showOptionDialog(builder.create());
510    }
511    */
512
513    private boolean mInFeedbackDialog = false;
514    public void presentFeedbackDialog(LatinIME latinIME) {
515        mInFeedbackDialog = true;
516        latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
517    }
518
519    // TODO: currently unreachable.  Remove after being sure enable/disable is
520    // not needed.
521    /*
522    public void enableOrDisable(final boolean showEnable, final LatinIME latinIME) {
523        if (showEnable) {
524            if (!sIsLogging) {
525                setLoggingAllowed(true);
526            }
527            resumeLogging();
528            Toast.makeText(latinIME,
529                    R.string.research_notify_session_logging_enabled,
530                    Toast.LENGTH_LONG).show();
531        } else {
532            Toast toast = Toast.makeText(latinIME,
533                    R.string.research_notify_session_log_deleting,
534                    Toast.LENGTH_LONG);
535            toast.show();
536            boolean isLogDeleted = abort();
537            final long currentTime = System.currentTimeMillis();
538            final long resumeTime = currentTime + 1000 * 60 *
539                    SUSPEND_DURATION_IN_MINUTES;
540            suspendLoggingUntil(resumeTime);
541            toast.cancel();
542            Toast.makeText(latinIME, R.string.research_notify_logging_suspended,
543                    Toast.LENGTH_LONG).show();
544        }
545    }
546    */
547
548    static class LogStatement {
549        final String mName;
550
551        // mIsPotentiallyPrivate indicates that event contains potentially private information.  If
552        // the word that this event is a part of is determined to be privacy-sensitive, then this
553        // event should not be included in the output log.  The system waits to output until the
554        // containing word is known.
555        final boolean mIsPotentiallyPrivate;
556
557        // mIsPotentiallyRevealing indicates that this statement may disclose details about other
558        // words typed in other LogUnits.  This can happen if the user is not inserting spaces, and
559        // data from Suggestions and/or Composing text reveals the entire "megaword".  For example,
560        // say the user is typing "for the win", and the system wants to record the bigram "the
561        // win".  If the user types "forthe", omitting the space, the system will give "for the" as
562        // a suggestion.  If the user accepts the autocorrection, the suggestion for "for the" is
563        // included in the log for the word "the", disclosing that the previous word had been "for".
564        // For now, we simply do not include this data when logging part of a "megaword".
565        final boolean mIsPotentiallyRevealing;
566
567        // mKeys stores the names that are the attributes in the output json objects
568        final String[] mKeys;
569        private static final String[] NULL_KEYS = new String[0];
570
571        LogStatement(final String name, final boolean isPotentiallyPrivate,
572                final boolean isPotentiallyRevealing, final String... keys) {
573            mName = name;
574            mIsPotentiallyPrivate = isPotentiallyPrivate;
575            mIsPotentiallyRevealing = isPotentiallyRevealing;
576            mKeys = (keys == null) ? NULL_KEYS : keys;
577        }
578    }
579
580    private static final LogStatement LOGSTATEMENT_FEEDBACK =
581            new LogStatement("UserTimestamp", false, false, "contents");
582    public void sendFeedback(final String feedbackContents, final boolean includeHistory) {
583        if (mFeedbackLogBuffer == null) {
584            return;
585        }
586        if (includeHistory) {
587            commitCurrentLogUnit();
588        } else {
589            mFeedbackLogBuffer.clear();
590        }
591        final LogUnit feedbackLogUnit = new LogUnit();
592        feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
593                feedbackContents);
594        mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
595        publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */);
596        mFeedbackLog.close(new Runnable() {
597            @Override
598            public void run() {
599                uploadNow();
600            }
601        });
602        mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
603    }
604
605    public void uploadNow() {
606        if (DEBUG) {
607            Log.d(TAG, "calling uploadNow()");
608        }
609        mLatinIME.startService(mUploadIntent);
610    }
611
612    public void onLeavingSendFeedbackDialog() {
613        mInFeedbackDialog = false;
614    }
615
616    public void initSuggest(Suggest suggest) {
617        mSuggest = suggest;
618        if (mMainLogBuffer != null) {
619            mMainLogBuffer.setSuggest(mSuggest);
620        }
621    }
622
623    private Dictionary getDictionary() {
624        if (mSuggest == null) {
625            return null;
626        }
627        return mSuggest.getMainDictionary();
628    }
629
630    private void setIsPasswordView(boolean isPasswordView) {
631        mIsPasswordView = isPasswordView;
632    }
633
634    private boolean isAllowedToLog() {
635        if (DEBUG) {
636            Log.d(TAG, "iatl: " +
637                "mipw=" + mIsPasswordView +
638                ", mils=" + mIsLoggingSuspended +
639                ", sil=" + sIsLogging +
640                ", mInFeedbackDialog=" + mInFeedbackDialog);
641        }
642        return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
643    }
644
645    public void requestIndicatorRedraw() {
646        if (!IS_SHOWING_INDICATOR) {
647            return;
648        }
649        if (mMainKeyboardView == null) {
650            return;
651        }
652        mMainKeyboardView.invalidateAllKeys();
653    }
654
655    public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width,
656            int height) {
657        // TODO: Reimplement using a keyboard background image specific to the ResearchLogger
658        // and remove this method.
659        // The check for MainKeyboardView ensures that a red border is only placed around
660        // the main keyboard, not every keyboard.
661        if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) {
662            final int savedColor = paint.getColor();
663            paint.setColor(Color.RED);
664            final Style savedStyle = paint.getStyle();
665            paint.setStyle(Style.STROKE);
666            final float savedStrokeWidth = paint.getStrokeWidth();
667            if (IS_SHOWING_INDICATOR_CLEARLY) {
668                paint.setStrokeWidth(5);
669                canvas.drawLine(0, 0, 0, height, paint);
670                canvas.drawLine(width, 0, width, height, paint);
671            } else {
672                // Put a tiny red dot on the screen so a knowledgeable user can check whether
673                // it is enabled.  The dot is actually a zero-width, zero-height rectangle,
674                // placed at the lower-right corner of the canvas, painted with a non-zero border
675                // width.
676                paint.setStrokeWidth(3);
677                canvas.drawRect(width, height, width, height, paint);
678            }
679            paint.setColor(savedColor);
680            paint.setStyle(savedStyle);
681            paint.setStrokeWidth(savedStrokeWidth);
682        }
683    }
684
685    /**
686     * Buffer a research log event, flagging it as privacy-sensitive.
687     */
688    private synchronized void enqueueEvent(final LogStatement logStatement,
689            final Object... values) {
690        enqueueEvent(mCurrentLogUnit, logStatement, values);
691    }
692
693    private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement,
694            final Object... values) {
695        assert values.length == logStatement.mKeys.length;
696        if (isAllowedToLog() && logUnit != null) {
697            final long time = SystemClock.uptimeMillis();
698            logUnit.addLogStatement(logStatement, time, values);
699        }
700    }
701
702    private void setCurrentLogUnitContainsDigitFlag() {
703        mCurrentLogUnit.setMayContainDigit();
704    }
705
706    /* package for test */ void commitCurrentLogUnit() {
707        if (DEBUG) {
708            Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasWord() ?
709                    ": " + mCurrentLogUnit.getWord() : ""));
710        }
711        if (!mCurrentLogUnit.isEmpty()) {
712            if (mMainLogBuffer != null) {
713                if ((mMainLogBuffer.isNGramSafe() || IS_LOGGING_EVERYTHING) &&
714                        mMainLogBuffer.isNGramComplete() &&
715                        mMainResearchLog != null) {
716                    publishLogBuffer(mMainLogBuffer, mMainResearchLog,
717                            true /* isIncludingPrivateData */);
718                    mMainLogBuffer.resetWordCounter();
719                }
720                mMainLogBuffer.shiftIn(mCurrentLogUnit);
721            }
722            if (mFeedbackLogBuffer != null) {
723                mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
724            }
725            mCurrentLogUnit = new LogUnit();
726        } else {
727            if (DEBUG) {
728                Log.d(TAG, "Warning: tried to commit empty log unit.");
729            }
730        }
731    }
732
733    public void uncommitCurrentLogUnit(final String expectedWord,
734            final boolean dumpCurrentLogUnit) {
735        // The user has deleted this word and returned to the previous.  Check that the word in the
736        // logUnit matches the expected word.  If so, restore the last log unit committed to be the
737        // current logUnit.  I.e., pull out the last LogUnit from all the LogBuffers, and make
738        // restore it to mCurrentLogUnit so the new edits are captured with the word.  Optionally
739        // dump the contents of mCurrentLogUnit (useful if they contain deletions of the next word
740        // that should not be reported to protect user privacy)
741        //
742        // Note that we don't use mLastLogUnit here, because it only goes one word back and is only
743        // needed for reverts, which only happen one back.
744        if (mMainLogBuffer == null) {
745            return;
746        }
747        final LogUnit oldLogUnit = mMainLogBuffer.peekLastLogUnit();
748
749        // Check that expected word matches.
750        if (oldLogUnit != null) {
751            final String oldLogUnitWord = oldLogUnit.getWord();
752            if (!oldLogUnitWord.equals(expectedWord)) {
753                return;
754            }
755        }
756
757        // Uncommit, merging if necessary.
758        mMainLogBuffer.unshiftIn();
759        if (oldLogUnit != null && !dumpCurrentLogUnit) {
760            oldLogUnit.append(mCurrentLogUnit);
761            mSavedDownEventTime = Long.MAX_VALUE;
762        }
763        if (oldLogUnit == null) {
764            mCurrentLogUnit = new LogUnit();
765        } else {
766            mCurrentLogUnit = oldLogUnit;
767        }
768        if (mFeedbackLogBuffer != null) {
769            mFeedbackLogBuffer.unshiftIn();
770        }
771        if (DEBUG) {
772            Log.d(TAG, "uncommitCurrentLogUnit (dump=" + dumpCurrentLogUnit + ") back to "
773                    + (mCurrentLogUnit.hasWord() ? ": '" + mCurrentLogUnit.getWord() + "'" : ""));
774        }
775    }
776
777    private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_OPENING =
778            new LogStatement("logSegmentStart", false, false, "isIncludingPrivateData");
779    private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_CLOSING =
780            new LogStatement("logSegmentEnd", false, false);
781    /* package for test */ void publishLogBuffer(final LogBuffer logBuffer,
782            final ResearchLog researchLog, final boolean isIncludingPrivateData) {
783        final LogUnit openingLogUnit = new LogUnit();
784        if (logBuffer.isEmpty()) return;
785        openingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_OPENING, SystemClock.uptimeMillis(),
786                isIncludingPrivateData);
787        researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */);
788        LogUnit logUnit;
789        int numWordsToPublish = MainLogBuffer.N_GRAM_SIZE;
790        while ((logUnit = logBuffer.shiftOut()) != null && numWordsToPublish > 0) {
791            if (DEBUG) {
792                Log.d(TAG, "publishLogBuffer: " + (logUnit.hasWord() ? logUnit.getWord()
793                        : "<wordless>"));
794            }
795            researchLog.publish(logUnit, isIncludingPrivateData);
796            if (logUnit.getWord() != null) {
797                numWordsToPublish--;
798            }
799        }
800        final LogUnit closingLogUnit = new LogUnit();
801        closingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_CLOSING,
802                SystemClock.uptimeMillis());
803        researchLog.publish(closingLogUnit, true /* isIncludingPrivateData */);
804    }
805
806    public static boolean hasLetters(final String word) {
807        final int length = word.length();
808        for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
809            final int codePoint = word.codePointAt(i);
810            if (Character.isLetter(codePoint)) {
811                return true;
812            }
813        }
814        return false;
815    }
816
817    /**
818     * Commit the portion of mCurrentLogUnit before maxTime as a worded logUnit.
819     *
820     * After this operation completes, mCurrentLogUnit will hold any logStatements that happened
821     * after maxTime.
822     */
823    /* package for test */ void commitCurrentLogUnitAsWord(final String word, final long maxTime,
824            final boolean isBatchMode) {
825        if (word == null) {
826            return;
827        }
828        final Dictionary dictionary = getDictionary();
829        if (word.length() > 0 && hasLetters(word)) {
830            mCurrentLogUnit.setWord(word);
831            final boolean isDictionaryWord = dictionary != null
832                    && dictionary.isValidWord(word);
833            mStatistics.recordWordEntered(isDictionaryWord);
834        }
835        final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime);
836        enqueueCommitText(word, isBatchMode);
837        commitCurrentLogUnit();
838        mCurrentLogUnit = newLogUnit;
839    }
840
841    public void onWordFinished(final String word, final boolean isBatchMode) {
842        commitCurrentLogUnitAsWord(word, mSavedDownEventTime, isBatchMode);
843        mSavedDownEventTime = Long.MAX_VALUE;
844    }
845
846    private static int scrubDigitFromCodePoint(int codePoint) {
847        return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint;
848    }
849
850    /* package for test */ static String scrubDigitsFromString(String s) {
851        StringBuilder sb = null;
852        final int length = s.length();
853        for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) {
854            final int codePoint = Character.codePointAt(s, i);
855            if (Character.isDigit(codePoint)) {
856                if (sb == null) {
857                    sb = new StringBuilder(length);
858                    sb.append(s.substring(0, i));
859                }
860                sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT);
861            } else {
862                if (sb != null) {
863                    sb.appendCodePoint(codePoint);
864                }
865            }
866        }
867        if (sb == null) {
868            return s;
869        } else {
870            return sb.toString();
871        }
872    }
873
874    private static String getUUID(final SharedPreferences prefs) {
875        String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null);
876        if (null == uuidString) {
877            UUID uuid = UUID.randomUUID();
878            uuidString = uuid.toString();
879            Editor editor = prefs.edit();
880            editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString);
881            editor.apply();
882        }
883        return uuidString;
884    }
885
886    private String scrubWord(String word) {
887        final Dictionary dictionary = getDictionary();
888        if (dictionary == null) {
889            return WORD_REPLACEMENT_STRING;
890        }
891        if (dictionary.isValidWord(word)) {
892            return word;
893        }
894        return WORD_REPLACEMENT_STRING;
895    }
896
897    // Specific logging methods follow below.  The comments for each logging method should
898    // indicate what specific method is logged, and how to trigger it from the user interface.
899    //
900    // Logging methods can be generally classified into two flavors, "UserAction", which should
901    // correspond closely to an event that is sensed by the IME, and is usually generated
902    // directly by the user, and "SystemResponse" which corresponds to an event that the IME
903    // generates, often after much processing of user input.  SystemResponses should correspond
904    // closely to user-visible events.
905    // TODO: Consider exposing the UserAction classification in the log output.
906
907    /**
908     * Log a call to LatinIME.onStartInputViewInternal().
909     *
910     * UserAction: called each time the keyboard is opened up.
911     */
912    private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL =
913            new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid",
914                    "packageName", "inputType", "imeOptions", "fieldId", "display", "model",
915                    "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything",
916                    "isExperimentalDebug");
917    public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
918            final SharedPreferences prefs) {
919        final ResearchLogger researchLogger = getInstance();
920        if (editorInfo != null) {
921            final boolean isPassword = InputTypeUtils.isPasswordInputType(editorInfo.inputType)
922                    || InputTypeUtils.isVisiblePasswordInputType(editorInfo.inputType);
923            getInstance().setIsPasswordView(isPassword);
924            researchLogger.start();
925            final Context context = researchLogger.mLatinIME;
926            try {
927                final PackageInfo packageInfo;
928                packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),
929                        0);
930                final Integer versionCode = packageInfo.versionCode;
931                final String versionName = packageInfo.versionName;
932                researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL,
933                        researchLogger.mUUIDString, editorInfo.packageName,
934                        Integer.toHexString(editorInfo.inputType),
935                        Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId,
936                        Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName,
937                        OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING,
938                        ProductionFlag.IS_EXPERIMENTAL_DEBUG);
939            } catch (NameNotFoundException e) {
940                e.printStackTrace();
941            }
942        }
943    }
944
945    public void latinIME_onFinishInputViewInternal() {
946        logStatistics();
947        stop();
948    }
949
950    /**
951     * Log a change in preferences.
952     *
953     * UserAction: called when the user changes the settings.
954     */
955    private static final LogStatement LOGSTATEMENT_PREFS_CHANGED =
956            new LogStatement("PrefsChanged", false, false, "prefs");
957    public static void prefsChanged(final SharedPreferences prefs) {
958        final ResearchLogger researchLogger = getInstance();
959        researchLogger.enqueueEvent(LOGSTATEMENT_PREFS_CHANGED, prefs);
960    }
961
962    /**
963     * Log a call to MainKeyboardView.processMotionEvent().
964     *
965     * UserAction: called when the user puts their finger onto the screen (ACTION_DOWN).
966     *
967     */
968    private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT =
969            new LogStatement("MotionEvent", true, false, "action", "MotionEvent");
970    public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action,
971            final long eventTime, final int index, final int id, final int x, final int y) {
972        if (me != null) {
973            final String actionString;
974            switch (action) {
975                case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break;
976                case MotionEvent.ACTION_UP: actionString = "UP"; break;
977                case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break;
978                case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break;
979                case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break;
980                case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break;
981                case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break;
982                default: actionString = "ACTION_" + action; break;
983            }
984            final ResearchLogger researchLogger = getInstance();
985            researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
986                    actionString, MotionEvent.obtain(me));
987            if (action == MotionEvent.ACTION_DOWN) {
988                // Subtract 1 from eventTime so the down event is included in the later
989                // LogUnit, not the earlier (the test is for inequality).
990                researchLogger.mSavedDownEventTime = eventTime - 1;
991            }
992        }
993    }
994
995    /**
996     * Log a call to LatinIME.onCodeInput().
997     *
998     * SystemResponse: The main processing step for entering text.  Called when the user performs a
999     * tap, a flick, a long press, releases a gesture, or taps a punctuation suggestion.
1000     */
1001    private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT =
1002            new LogStatement("LatinImeOnCodeInput", true, false, "code", "x", "y");
1003    public static void latinIME_onCodeInput(final int code, final int x, final int y) {
1004        final long time = SystemClock.uptimeMillis();
1005        final ResearchLogger researchLogger = getInstance();
1006        researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT,
1007                Constants.printableCode(scrubDigitFromCodePoint(code)), x, y);
1008        if (Character.isDigit(code)) {
1009            researchLogger.setCurrentLogUnitContainsDigitFlag();
1010        }
1011        researchLogger.mStatistics.recordChar(code, time);
1012    }
1013    /**
1014     * Log a call to LatinIME.onDisplayCompletions().
1015     *
1016     * SystemResponse: The IME has displayed application-specific completions.  They may show up
1017     * in the suggestion strip, such as a landscape phone.
1018     */
1019    private static final LogStatement LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS =
1020            new LogStatement("LatinIMEOnDisplayCompletions", true, true,
1021                    "applicationSpecifiedCompletions");
1022    public static void latinIME_onDisplayCompletions(
1023            final CompletionInfo[] applicationSpecifiedCompletions) {
1024        // Note; passing an array as a single element in a vararg list.  Must create a new
1025        // dummy array around it or it will get expanded.
1026        getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS,
1027                new Object[] { applicationSpecifiedCompletions });
1028    }
1029
1030    public static boolean getAndClearLatinIMEExpectingUpdateSelection() {
1031        boolean returnValue = sLatinIMEExpectingUpdateSelection;
1032        sLatinIMEExpectingUpdateSelection = false;
1033        return returnValue;
1034    }
1035
1036    /**
1037     * Log a call to LatinIME.onWindowHidden().
1038     *
1039     * UserAction: The user has performed an action that has caused the IME to be closed.  They may
1040     * have focused on something other than a text field, or explicitly closed it.
1041     */
1042    private static final LogStatement LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN =
1043            new LogStatement("LatinIMEOnWindowHidden", false, false, "isTextTruncated", "text");
1044    public static void latinIME_onWindowHidden(final int savedSelectionStart,
1045            final int savedSelectionEnd, final InputConnection ic) {
1046        if (ic != null) {
1047            final boolean isTextTruncated;
1048            final String text;
1049            if (LOG_FULL_TEXTVIEW_CONTENTS) {
1050                // Capture the TextView contents.  This will trigger onUpdateSelection(), so we
1051                // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called,
1052                // it can tell that it was generated by the logging code, and not by the user, and
1053                // therefore keep user-visible state as is.
1054                ic.beginBatchEdit();
1055                ic.performContextMenuAction(android.R.id.selectAll);
1056                CharSequence charSequence = ic.getSelectedText(0);
1057                if (savedSelectionStart != -1 && savedSelectionEnd != -1) {
1058                    ic.setSelection(savedSelectionStart, savedSelectionEnd);
1059                }
1060                ic.endBatchEdit();
1061                sLatinIMEExpectingUpdateSelection = true;
1062                if (TextUtils.isEmpty(charSequence)) {
1063                    isTextTruncated = false;
1064                    text = "";
1065                } else {
1066                    if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) {
1067                        int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE;
1068                        // do not cut in the middle of a supplementary character
1069                        final char c = charSequence.charAt(length - 1);
1070                        if (Character.isHighSurrogate(c)) {
1071                            length--;
1072                        }
1073                        final CharSequence truncatedCharSequence = charSequence.subSequence(0,
1074                                length);
1075                        isTextTruncated = true;
1076                        text = truncatedCharSequence.toString();
1077                    } else {
1078                        isTextTruncated = false;
1079                        text = charSequence.toString();
1080                    }
1081                }
1082            } else {
1083                isTextTruncated = true;
1084                text = "";
1085            }
1086            final ResearchLogger researchLogger = getInstance();
1087            // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g.
1088            // during a live user test), so the normal isPotentiallyPrivate and
1089            // isPotentiallyRevealing flags do not apply
1090            researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN, isTextTruncated,
1091                    text);
1092            researchLogger.commitCurrentLogUnit();
1093            getInstance().stop();
1094        }
1095    }
1096
1097    /**
1098     * Log a call to LatinIME.onUpdateSelection().
1099     *
1100     * UserAction/SystemResponse: The user has moved the cursor or selection.  This function may
1101     * be called, however, when the system has moved the cursor, say by inserting a character.
1102     */
1103    private static final LogStatement LOGSTATEMENT_LATINIME_ONUPDATESELECTION =
1104            new LogStatement("LatinIMEOnUpdateSelection", true, false, "lastSelectionStart",
1105                    "lastSelectionEnd", "oldSelStart", "oldSelEnd", "newSelStart", "newSelEnd",
1106                    "composingSpanStart", "composingSpanEnd", "expectingUpdateSelection",
1107                    "expectingUpdateSelectionFromLogger", "context");
1108    public static void latinIME_onUpdateSelection(final int lastSelectionStart,
1109            final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
1110            final int newSelStart, final int newSelEnd, final int composingSpanStart,
1111            final int composingSpanEnd, final boolean expectingUpdateSelection,
1112            final boolean expectingUpdateSelectionFromLogger,
1113            final RichInputConnection connection) {
1114        String word = "";
1115        if (connection != null) {
1116            Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
1117            if (range != null) {
1118                word = range.mWord;
1119            }
1120        }
1121        final ResearchLogger researchLogger = getInstance();
1122        final String scrubbedWord = researchLogger.scrubWord(word);
1123        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONUPDATESELECTION, lastSelectionStart,
1124                lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd,
1125                composingSpanStart, composingSpanEnd, expectingUpdateSelection,
1126                expectingUpdateSelectionFromLogger, scrubbedWord);
1127    }
1128
1129    /**
1130     * Log a call to LatinIME.onTextInput().
1131     *
1132     * SystemResponse: Raw text is added to the TextView.
1133     */
1134    public static void latinIME_onTextInput(final String text, final boolean isBatchMode) {
1135        final ResearchLogger researchLogger = getInstance();
1136        researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode);
1137    }
1138
1139    /**
1140     * Log a call to LatinIME.pickSuggestionManually().
1141     *
1142     * UserAction: The user has chosen a specific word from the suggestion strip.
1143     */
1144    private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY =
1145            new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index",
1146                    "suggestion", "x", "y");
1147    public static void latinIME_pickSuggestionManually(final String replacedWord,
1148            final int index, final String suggestion, final boolean isBatchMode) {
1149        final String scrubbedWord = scrubDigitsFromString(suggestion);
1150        final ResearchLogger researchLogger = getInstance();
1151        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY,
1152                scrubDigitsFromString(replacedWord), index,
1153                suggestion == null ? null : scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE,
1154                Constants.SUGGESTION_STRIP_COORDINATE);
1155        researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode);
1156        researchLogger.mStatistics.recordManualSuggestion();
1157    }
1158
1159    /**
1160     * Log a call to LatinIME.punctuationSuggestion().
1161     *
1162     * UserAction: The user has chosen punctuation from the punctuation suggestion strip.
1163     */
1164    private static final LogStatement LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION =
1165            new LogStatement("LatinIMEPunctuationSuggestion", false, false, "index", "suggestion",
1166                    "x", "y", "isPrediction");
1167    public static void latinIME_punctuationSuggestion(final int index, final String suggestion,
1168            final boolean isBatchMode, final boolean isPrediction) {
1169        final ResearchLogger researchLogger = getInstance();
1170        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion,
1171                Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE,
1172                isPrediction);
1173        researchLogger.commitCurrentLogUnitAsWord(suggestion, Long.MAX_VALUE, isBatchMode);
1174    }
1175
1176    /**
1177     * Log a call to LatinIME.sendKeyCodePoint().
1178     *
1179     * SystemResponse: The IME is simulating a hardware keypress.  This happens for numbers; other
1180     * input typically goes through RichInputConnection.setComposingText() and
1181     * RichInputConnection.commitText().
1182     */
1183    private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT =
1184            new LogStatement("LatinIMESendKeyCodePoint", true, false, "code");
1185    public static void latinIME_sendKeyCodePoint(final int code) {
1186        final ResearchLogger researchLogger = getInstance();
1187        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT,
1188                Constants.printableCode(scrubDigitFromCodePoint(code)));
1189        if (Character.isDigit(code)) {
1190            researchLogger.setCurrentLogUnitContainsDigitFlag();
1191        }
1192    }
1193
1194    /**
1195     * Log a call to LatinIME.swapSwapperAndSpace().
1196     *
1197     * SystemResponse: A symbol has been swapped with a space character.  E.g. punctuation may swap
1198     * if a soft space is inserted after a word.
1199     */
1200    private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE =
1201            new LogStatement("LatinIMESwapSwapperAndSpace", false, false, "originalCharacters",
1202                    "charactersAfterSwap");
1203    public static void latinIME_swapSwapperAndSpace(final CharSequence originalCharacters,
1204            final String charactersAfterSwap) {
1205        final ResearchLogger researchLogger = getInstance();
1206        final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
1207        if (logUnit != null) {
1208            researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE,
1209                    originalCharacters, charactersAfterSwap);
1210        }
1211    }
1212
1213    /**
1214     * Log a call to LatinIME.maybeDoubleSpacePeriod().
1215     *
1216     * SystemResponse: Two spaces have been replaced by period space.
1217     */
1218    public static void latinIME_maybeDoubleSpacePeriod(final String text,
1219            final boolean isBatchMode) {
1220        final ResearchLogger researchLogger = getInstance();
1221        researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode);
1222    }
1223
1224    /**
1225     * Log a call to MainKeyboardView.onLongPress().
1226     *
1227     * UserAction: The user has performed a long-press on a key.
1228     */
1229    private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS =
1230            new LogStatement("MainKeyboardViewOnLongPress", false, false);
1231    public static void mainKeyboardView_onLongPress() {
1232        getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS);
1233    }
1234
1235    /**
1236     * Log a call to MainKeyboardView.setKeyboard().
1237     *
1238     * SystemResponse: The IME has switched to a new keyboard (e.g. French, English).
1239     * This is typically called right after LatinIME.onStartInputViewInternal (when starting a new
1240     * IME), but may happen at other times if the user explicitly requests a keyboard change.
1241     */
1242    private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD =
1243            new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale",
1244                    "orientation", "width", "modeName", "action", "navigateNext",
1245                    "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled",
1246                    "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th",
1247                    "keys");
1248    public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) {
1249        final KeyboardId kid = keyboard.mId;
1250        final boolean isPasswordView = kid.passwordInput();
1251        final ResearchLogger researchLogger = getInstance();
1252        researchLogger.setIsPasswordView(isPasswordView);
1253        researchLogger.enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD,
1254                KeyboardId.elementIdToName(kid.mElementId),
1255                kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
1256                kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(),
1257                kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey,
1258                isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey,
1259                kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth,
1260                keyboard.mOccupiedHeight, keyboard.mKeys);
1261    }
1262
1263    /**
1264     * Log a call to LatinIME.revertCommit().
1265     *
1266     * SystemResponse: The IME has reverted commited text.  This happens when the user enters
1267     * a word, commits it by pressing space or punctuation, and then reverts the commit by hitting
1268     * backspace.
1269     */
1270    private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT =
1271            new LogStatement("LatinIMERevertCommit", true, false, "committedWord",
1272                    "originallyTypedWord");
1273    public static void latinIME_revertCommit(final String committedWord,
1274            final String originallyTypedWord, final boolean isBatchMode) {
1275        final ResearchLogger researchLogger = getInstance();
1276        // TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word.
1277        final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
1278        if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) {
1279            if (logUnit != null) {
1280                logUnit.setWord(originallyTypedWord);
1281            }
1282        }
1283        researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit,
1284                LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, originallyTypedWord);
1285        researchLogger.mStatistics.recordRevertCommit();
1286        researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE, isBatchMode);
1287    }
1288
1289    /**
1290     * Log a call to PointerTracker.callListenerOnCancelInput().
1291     *
1292     * UserAction: The user has canceled the input, e.g., by pressing down, but then removing
1293     * outside the keyboard area.
1294     * TODO: Verify
1295     */
1296    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT =
1297            new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false);
1298    public static void pointerTracker_callListenerOnCancelInput() {
1299        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT);
1300    }
1301
1302    /**
1303     * Log a call to PointerTracker.callListenerOnCodeInput().
1304     *
1305     * SystemResponse: The user has entered a key through the normal tapping mechanism.
1306     * LatinIME.onCodeInput will also be called.
1307     */
1308    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT =
1309            new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "code",
1310                    "outputText", "x", "y", "ignoreModifierKey", "altersCode", "isEnabled");
1311    public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
1312            final int y, final boolean ignoreModifierKey, final boolean altersCode,
1313            final int code) {
1314        if (key != null) {
1315            String outputText = key.getOutputText();
1316            getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
1317                    Constants.printableCode(scrubDigitFromCodePoint(code)),
1318                    outputText == null ? null : scrubDigitsFromString(outputText.toString()),
1319                    x, y, ignoreModifierKey, altersCode, key.isEnabled());
1320        }
1321    }
1322
1323    /**
1324     * Log a call to PointerTracker.callListenerCallListenerOnRelease().
1325     *
1326     * UserAction: The user has released their finger or thumb from the screen.
1327     */
1328    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE =
1329            new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code",
1330                    "withSliding", "ignoreModifierKey", "isEnabled");
1331    public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
1332            final boolean withSliding, final boolean ignoreModifierKey) {
1333        if (key != null) {
1334            getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE,
1335                    Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding,
1336                    ignoreModifierKey, key.isEnabled());
1337        }
1338    }
1339
1340    /**
1341     * Log a call to PointerTracker.onDownEvent().
1342     *
1343     * UserAction: The user has pressed down on a key.
1344     * TODO: Differentiate with LatinIME.processMotionEvent.
1345     */
1346    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT =
1347            new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared");
1348    public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
1349        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT, deltaT,
1350                distanceSquared);
1351    }
1352
1353    /**
1354     * Log a call to PointerTracker.onMoveEvent().
1355     *
1356     * UserAction: The user has moved their finger while pressing on the screen.
1357     * TODO: Differentiate with LatinIME.processMotionEvent().
1358     */
1359    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT =
1360            new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY");
1361    public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
1362            final int lastY) {
1363        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY);
1364    }
1365
1366    /**
1367     * Log a call to RichInputConnection.commitCompletion().
1368     *
1369     * SystemResponse: The IME has committed a completion.  A completion is an application-
1370     * specific suggestion that is presented in a pop-up menu in the TextView.
1371     */
1372    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION =
1373            new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo");
1374    public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) {
1375        final ResearchLogger researchLogger = getInstance();
1376        researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION,
1377                completionInfo);
1378    }
1379
1380    /**
1381     * Log a call to RichInputConnection.revertDoubleSpacePeriod().
1382     *
1383     * SystemResponse: The IME has reverted ". ", which had previously replaced two typed spaces.
1384     */
1385    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD =
1386            new LogStatement("RichInputConnectionRevertDoubleSpacePeriod", false, false);
1387    public static void richInputConnection_revertDoubleSpacePeriod() {
1388        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD);
1389    }
1390
1391    /**
1392     * Log a call to RichInputConnection.revertSwapPunctuation().
1393     *
1394     * SystemResponse: The IME has reverted a punctuation swap.
1395     */
1396    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION =
1397            new LogStatement("RichInputConnectionRevertSwapPunctuation", false, false);
1398    public static void richInputConnection_revertSwapPunctuation() {
1399        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION);
1400    }
1401
1402    /**
1403     * Log a call to LatinIME.commitCurrentAutoCorrection().
1404     *
1405     * SystemResponse: The IME has committed an auto-correction.  An auto-correction changes the raw
1406     * text input to another word that the user more likely desired to type.
1407     */
1408    private static final LogStatement LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION =
1409            new LogStatement("LatinIMECommitCurrentAutoCorrection", true, true, "typedWord",
1410                    "autoCorrection", "separatorString");
1411    public static void latinIme_commitCurrentAutoCorrection(final String typedWord,
1412            final String autoCorrection, final String separatorString, final boolean isBatchMode) {
1413        final String scrubbedTypedWord = scrubDigitsFromString(typedWord);
1414        final String scrubbedAutoCorrection = scrubDigitsFromString(autoCorrection);
1415        final ResearchLogger researchLogger = getInstance();
1416        researchLogger.commitCurrentLogUnitAsWord(scrubbedAutoCorrection, Long.MAX_VALUE,
1417                isBatchMode);
1418
1419        // Add the autocorrection logStatement at the end of the logUnit for the committed word.
1420        // We have to do this after calling commitCurrentLogUnitAsWord, because it may split the
1421        // current logUnit, and then we have to peek to get the logUnit reference back.
1422        final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
1423        // TODO: Add test to confirm that the commitCurrentAutoCorrection log statement should
1424        // always be added to logUnit (if non-null) and not mCurrentLogUnit.
1425        researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION,
1426                scrubbedTypedWord, scrubbedAutoCorrection, separatorString);
1427    }
1428
1429    private boolean isExpectingCommitText = false;
1430    /**
1431     * Log a call to RichInputConnection.commitPartialText
1432     *
1433     * SystemResponse: The IME is committing part of a word.  This happens if a space is
1434     * automatically inserted to split a single typed string into two or more words.
1435     */
1436    // TODO: This method is currently unused.  Find where it should be called from in the IME and
1437    // add invocations.
1438    private static final LogStatement LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT =
1439            new LogStatement("LatinIMECommitPartialText", true, false, "newCursorPosition");
1440    public static void latinIME_commitPartialText(final String committedWord,
1441            final long lastTimestampOfWordData, final boolean isBatchMode) {
1442        final ResearchLogger researchLogger = getInstance();
1443        final String scrubbedWord = scrubDigitsFromString(committedWord);
1444        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT);
1445        researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData,
1446                isBatchMode);
1447    }
1448
1449    /**
1450     * Log a call to RichInputConnection.commitText().
1451     *
1452     * SystemResponse: The IME is committing text.  This happens after the user has typed a word
1453     * and then a space or punctuation key.
1454     */
1455    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT =
1456            new LogStatement("RichInputConnectionCommitText", true, false, "newCursorPosition");
1457    public static void richInputConnection_commitText(final String committedWord,
1458            final int newCursorPosition, final boolean isBatchMode) {
1459        final ResearchLogger researchLogger = getInstance();
1460        final String scrubbedWord = scrubDigitsFromString(committedWord);
1461        if (!researchLogger.isExpectingCommitText) {
1462            researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT,
1463                    newCursorPosition);
1464            researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode);
1465        }
1466        researchLogger.isExpectingCommitText = false;
1467    }
1468
1469    /**
1470     * Shared event for logging committed text.
1471     */
1472    private static final LogStatement LOGSTATEMENT_COMMITTEXT =
1473            new LogStatement("CommitText", true, false, "committedText", "isBatchMode");
1474    private void enqueueCommitText(final String word, final boolean isBatchMode) {
1475        enqueueEvent(LOGSTATEMENT_COMMITTEXT, word, isBatchMode);
1476    }
1477
1478    /**
1479     * Log a call to RichInputConnection.deleteSurroundingText().
1480     *
1481     * SystemResponse: The IME has deleted text.
1482     */
1483    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT =
1484            new LogStatement("RichInputConnectionDeleteSurroundingText", true, false,
1485                    "beforeLength", "afterLength");
1486    public static void richInputConnection_deleteSurroundingText(final int beforeLength,
1487            final int afterLength) {
1488        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT,
1489                beforeLength, afterLength);
1490    }
1491
1492    /**
1493     * Log a call to RichInputConnection.finishComposingText().
1494     *
1495     * SystemResponse: The IME has left the composing text as-is.
1496     */
1497    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT =
1498            new LogStatement("RichInputConnectionFinishComposingText", false, false);
1499    public static void richInputConnection_finishComposingText() {
1500        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT);
1501    }
1502
1503    /**
1504     * Log a call to RichInputConnection.performEditorAction().
1505     *
1506     * SystemResponse: The IME is invoking an action specific to the editor.
1507     */
1508    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION =
1509            new LogStatement("RichInputConnectionPerformEditorAction", false, false,
1510                    "imeActionId");
1511    public static void richInputConnection_performEditorAction(final int imeActionId) {
1512        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION,
1513                imeActionId);
1514    }
1515
1516    /**
1517     * Log a call to RichInputConnection.sendKeyEvent().
1518     *
1519     * SystemResponse: The IME is telling the TextView that a key is being pressed through an
1520     * alternate channel.
1521     * TODO: only for hardware keys?
1522     */
1523    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT =
1524            new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action",
1525                    "code");
1526    public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) {
1527        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT,
1528                keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode());
1529    }
1530
1531    /**
1532     * Log a call to RichInputConnection.setComposingText().
1533     *
1534     * SystemResponse: The IME is setting the composing text.  Happens each time a character is
1535     * entered.
1536     */
1537    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT =
1538            new LogStatement("RichInputConnectionSetComposingText", true, true, "text",
1539                    "newCursorPosition");
1540    public static void richInputConnection_setComposingText(final CharSequence text,
1541            final int newCursorPosition) {
1542        if (text == null) {
1543            throw new RuntimeException("setComposingText is null");
1544        }
1545        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, text,
1546                newCursorPosition);
1547    }
1548
1549    /**
1550     * Log a call to RichInputConnection.setSelection().
1551     *
1552     * SystemResponse: The IME is requesting that the selection change.  User-initiated selection-
1553     * change requests do not go through this method -- it's only when the system wants to change
1554     * the selection.
1555     */
1556    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION =
1557            new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to");
1558    public static void richInputConnection_setSelection(final int from, final int to) {
1559        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to);
1560    }
1561
1562    /**
1563     * Log a call to SuddenJumpingTouchEventHandler.onTouchEvent().
1564     *
1565     * SystemResponse: The IME has filtered input events in case of an erroneous sensor reading.
1566     */
1567    private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT =
1568            new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false,
1569                    "motionEvent");
1570    public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
1571        if (me != null) {
1572            getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT,
1573                    me.toString());
1574        }
1575    }
1576
1577    /**
1578     * Log a call to SuggestionsView.setSuggestions().
1579     *
1580     * SystemResponse: The IME is setting the suggestions in the suggestion strip.
1581     */
1582    private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS =
1583            new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords");
1584    public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) {
1585        if (suggestedWords != null) {
1586            getInstance().enqueueEvent(LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS,
1587                    suggestedWords);
1588        }
1589    }
1590
1591    /**
1592     * The user has indicated a particular point in the log that is of interest.
1593     *
1594     * UserAction: From direct menu invocation.
1595     */
1596    private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP =
1597            new LogStatement("UserTimestamp", false, false);
1598    public void userTimestamp() {
1599        getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP);
1600    }
1601
1602    /**
1603     * Log a call to LatinIME.onEndBatchInput().
1604     *
1605     * SystemResponse: The system has completed a gesture.
1606     */
1607    private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT =
1608            new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText",
1609                    "enteredWordPos");
1610    public static void latinIME_onEndBatchInput(final CharSequence enteredText,
1611            final int enteredWordPos) {
1612        final ResearchLogger researchLogger = getInstance();
1613        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText,
1614                enteredWordPos);
1615        researchLogger.mStatistics.recordGestureInput(enteredText.length());
1616    }
1617
1618    /**
1619     * Log a call to LatinIME.handleBackspace().
1620     *
1621     * UserInput: The user is deleting a gestured word by hitting the backspace key once.
1622     */
1623    private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH =
1624            new LogStatement("LatinIMEHandleBackspaceBatch", true, false, "deletedText");
1625    public static void latinIME_handleBackspace_batch(final CharSequence deletedText) {
1626        final ResearchLogger researchLogger = getInstance();
1627        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText);
1628        researchLogger.mStatistics.recordGestureDelete();
1629    }
1630
1631    /**
1632     * Log statistics.
1633     *
1634     * ContextualData, recorded at the end of a session.
1635     */
1636    private static final LogStatement LOGSTATEMENT_STATISTICS =
1637            new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount",
1638                    "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting",
1639                    "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete",
1640                    "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete",
1641                    "dictionaryWordCount", "splitWordsCount", "gestureInputCount",
1642                    "gestureCharsCount", "gesturesDeletedCount", "manualSuggestionsCount",
1643                    "revertCommitsCount");
1644    private static void logStatistics() {
1645        final ResearchLogger researchLogger = getInstance();
1646        final Statistics statistics = researchLogger.mStatistics;
1647        researchLogger.enqueueEvent(LOGSTATEMENT_STATISTICS, statistics.mCharCount,
1648                statistics.mLetterCount, statistics.mNumberCount, statistics.mSpaceCount,
1649                statistics.mDeleteKeyCount, statistics.mWordCount, statistics.mIsEmptyUponStarting,
1650                statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(),
1651                statistics.mBeforeDeleteKeyCounter.getAverageTime(),
1652                statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(),
1653                statistics.mAfterDeleteKeyCounter.getAverageTime(),
1654                statistics.mDictionaryWordCount, statistics.mSplitWordsCount,
1655                statistics.mGesturesInputCount, statistics.mGesturesCharsCount,
1656                statistics.mGesturesDeletedCount, statistics.mManualSuggestionsCount,
1657                statistics.mRevertCommitsCount);
1658    }
1659}
1660