ResearchLogger.java revision 4acdd3ad692affd09d4d722be689f302e94b17d0
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");
1167    public static void latinIME_punctuationSuggestion(final int index, final String suggestion,
1168            final boolean isBatchMode) {
1169        final ResearchLogger researchLogger = getInstance();
1170        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion,
1171                Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
1172        researchLogger.commitCurrentLogUnitAsWord(suggestion, Long.MAX_VALUE, isBatchMode);
1173    }
1174
1175    /**
1176     * Log a call to LatinIME.sendKeyCodePoint().
1177     *
1178     * SystemResponse: The IME is simulating a hardware keypress.  This happens for numbers; other
1179     * input typically goes through RichInputConnection.setComposingText() and
1180     * RichInputConnection.commitText().
1181     */
1182    private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT =
1183            new LogStatement("LatinIMESendKeyCodePoint", true, false, "code");
1184    public static void latinIME_sendKeyCodePoint(final int code) {
1185        final ResearchLogger researchLogger = getInstance();
1186        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT,
1187                Constants.printableCode(scrubDigitFromCodePoint(code)));
1188        if (Character.isDigit(code)) {
1189            researchLogger.setCurrentLogUnitContainsDigitFlag();
1190        }
1191    }
1192
1193    /**
1194     * Log a call to LatinIME.swapSwapperAndSpace().
1195     *
1196     * SystemResponse: A symbol has been swapped with a space character.  E.g. punctuation may swap
1197     * if a soft space is inserted after a word.
1198     */
1199    private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE =
1200            new LogStatement("LatinIMESwapSwapperAndSpace", false, false, "originalCharacters",
1201                    "charactersAfterSwap");
1202    public static void latinIME_swapSwapperAndSpace(final CharSequence originalCharacters,
1203            final String charactersAfterSwap) {
1204        final ResearchLogger researchLogger = getInstance();
1205        final LogUnit logUnit;
1206        if (researchLogger.mMainLogBuffer == null) {
1207            logUnit = null;
1208        } else {
1209            logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
1210        }
1211        if (logUnit != null) {
1212            researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE,
1213                    originalCharacters, charactersAfterSwap);
1214        }
1215    }
1216
1217    /**
1218     * Log a call to LatinIME.maybeDoubleSpacePeriod().
1219     *
1220     * SystemResponse: Two spaces have been replaced by period space.
1221     */
1222    public static void latinIME_maybeDoubleSpacePeriod(final String text,
1223            final boolean isBatchMode) {
1224        final ResearchLogger researchLogger = getInstance();
1225        researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode);
1226    }
1227
1228    /**
1229     * Log a call to MainKeyboardView.onLongPress().
1230     *
1231     * UserAction: The user has performed a long-press on a key.
1232     */
1233    private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS =
1234            new LogStatement("MainKeyboardViewOnLongPress", false, false);
1235    public static void mainKeyboardView_onLongPress() {
1236        getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS);
1237    }
1238
1239    /**
1240     * Log a call to MainKeyboardView.setKeyboard().
1241     *
1242     * SystemResponse: The IME has switched to a new keyboard (e.g. French, English).
1243     * This is typically called right after LatinIME.onStartInputViewInternal (when starting a new
1244     * IME), but may happen at other times if the user explicitly requests a keyboard change.
1245     */
1246    private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD =
1247            new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale",
1248                    "orientation", "width", "modeName", "action", "navigateNext",
1249                    "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled",
1250                    "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th",
1251                    "keys");
1252    public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) {
1253        final KeyboardId kid = keyboard.mId;
1254        final boolean isPasswordView = kid.passwordInput();
1255        final ResearchLogger researchLogger = getInstance();
1256        researchLogger.setIsPasswordView(isPasswordView);
1257        researchLogger.enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD,
1258                KeyboardId.elementIdToName(kid.mElementId),
1259                kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
1260                kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(),
1261                kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey,
1262                isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey,
1263                kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth,
1264                keyboard.mOccupiedHeight, keyboard.mKeys);
1265    }
1266
1267    /**
1268     * Log a call to LatinIME.revertCommit().
1269     *
1270     * SystemResponse: The IME has reverted commited text.  This happens when the user enters
1271     * a word, commits it by pressing space or punctuation, and then reverts the commit by hitting
1272     * backspace.
1273     */
1274    private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT =
1275            new LogStatement("LatinIMERevertCommit", true, false, "committedWord",
1276                    "originallyTypedWord");
1277    public static void latinIME_revertCommit(final String committedWord,
1278            final String originallyTypedWord, final boolean isBatchMode) {
1279        final ResearchLogger researchLogger = getInstance();
1280        // TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word.
1281        final LogUnit logUnit;
1282        if (researchLogger.mMainLogBuffer == null) {
1283            logUnit = null;
1284        } else {
1285            logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
1286        }
1287        if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) {
1288            if (logUnit != null) {
1289                logUnit.setWord(originallyTypedWord);
1290            }
1291        }
1292        researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit,
1293                LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, originallyTypedWord);
1294        researchLogger.mStatistics.recordRevertCommit();
1295        researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE, isBatchMode);
1296    }
1297
1298    /**
1299     * Log a call to PointerTracker.callListenerOnCancelInput().
1300     *
1301     * UserAction: The user has canceled the input, e.g., by pressing down, but then removing
1302     * outside the keyboard area.
1303     * TODO: Verify
1304     */
1305    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT =
1306            new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false);
1307    public static void pointerTracker_callListenerOnCancelInput() {
1308        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT);
1309    }
1310
1311    /**
1312     * Log a call to PointerTracker.callListenerOnCodeInput().
1313     *
1314     * SystemResponse: The user has entered a key through the normal tapping mechanism.
1315     * LatinIME.onCodeInput will also be called.
1316     */
1317    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT =
1318            new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "code",
1319                    "outputText", "x", "y", "ignoreModifierKey", "altersCode", "isEnabled");
1320    public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
1321            final int y, final boolean ignoreModifierKey, final boolean altersCode,
1322            final int code) {
1323        if (key != null) {
1324            String outputText = key.getOutputText();
1325            getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
1326                    Constants.printableCode(scrubDigitFromCodePoint(code)),
1327                    outputText == null ? null : scrubDigitsFromString(outputText.toString()),
1328                    x, y, ignoreModifierKey, altersCode, key.isEnabled());
1329        }
1330    }
1331
1332    /**
1333     * Log a call to PointerTracker.callListenerCallListenerOnRelease().
1334     *
1335     * UserAction: The user has released their finger or thumb from the screen.
1336     */
1337    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE =
1338            new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code",
1339                    "withSliding", "ignoreModifierKey", "isEnabled");
1340    public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
1341            final boolean withSliding, final boolean ignoreModifierKey) {
1342        if (key != null) {
1343            getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE,
1344                    Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding,
1345                    ignoreModifierKey, key.isEnabled());
1346        }
1347    }
1348
1349    /**
1350     * Log a call to PointerTracker.onDownEvent().
1351     *
1352     * UserAction: The user has pressed down on a key.
1353     * TODO: Differentiate with LatinIME.processMotionEvent.
1354     */
1355    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT =
1356            new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared");
1357    public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
1358        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT, deltaT,
1359                distanceSquared);
1360    }
1361
1362    /**
1363     * Log a call to PointerTracker.onMoveEvent().
1364     *
1365     * UserAction: The user has moved their finger while pressing on the screen.
1366     * TODO: Differentiate with LatinIME.processMotionEvent().
1367     */
1368    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT =
1369            new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY");
1370    public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
1371            final int lastY) {
1372        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY);
1373    }
1374
1375    /**
1376     * Log a call to RichInputConnection.commitCompletion().
1377     *
1378     * SystemResponse: The IME has committed a completion.  A completion is an application-
1379     * specific suggestion that is presented in a pop-up menu in the TextView.
1380     */
1381    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION =
1382            new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo");
1383    public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) {
1384        final ResearchLogger researchLogger = getInstance();
1385        researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION,
1386                completionInfo);
1387    }
1388
1389    /**
1390     * Log a call to RichInputConnection.revertDoubleSpacePeriod().
1391     *
1392     * SystemResponse: The IME has reverted ". ", which had previously replaced two typed spaces.
1393     */
1394    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD =
1395            new LogStatement("RichInputConnectionRevertDoubleSpacePeriod", false, false);
1396    public static void richInputConnection_revertDoubleSpacePeriod() {
1397        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD);
1398    }
1399
1400    /**
1401     * Log a call to RichInputConnection.revertSwapPunctuation().
1402     *
1403     * SystemResponse: The IME has reverted a punctuation swap.
1404     */
1405    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION =
1406            new LogStatement("RichInputConnectionRevertSwapPunctuation", false, false);
1407    public static void richInputConnection_revertSwapPunctuation() {
1408        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION);
1409    }
1410
1411    /**
1412     * Log a call to LatinIME.commitCurrentAutoCorrection().
1413     *
1414     * SystemResponse: The IME has committed an auto-correction.  An auto-correction changes the raw
1415     * text input to another word that the user more likely desired to type.
1416     */
1417    private static final LogStatement LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION =
1418            new LogStatement("LatinIMECommitCurrentAutoCorrection", true, true, "typedWord",
1419                    "autoCorrection", "separatorString");
1420    public static void latinIme_commitCurrentAutoCorrection(final String typedWord,
1421            final String autoCorrection, final String separatorString, final boolean isBatchMode) {
1422        final String scrubbedTypedWord = scrubDigitsFromString(typedWord);
1423        final String scrubbedAutoCorrection = scrubDigitsFromString(autoCorrection);
1424        final ResearchLogger researchLogger = getInstance();
1425        researchLogger.commitCurrentLogUnitAsWord(scrubbedAutoCorrection, Long.MAX_VALUE,
1426                isBatchMode);
1427
1428        // Add the autocorrection logStatement at the end of the logUnit for the committed word.
1429        // We have to do this after calling commitCurrentLogUnitAsWord, because it may split the
1430        // current logUnit, and then we have to peek to get the logUnit reference back.
1431        final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
1432        // TODO: Add test to confirm that the commitCurrentAutoCorrection log statement should
1433        // always be added to logUnit (if non-null) and not mCurrentLogUnit.
1434        researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION,
1435                scrubbedTypedWord, scrubbedAutoCorrection, separatorString);
1436    }
1437
1438    private boolean isExpectingCommitText = false;
1439    /**
1440     * Log a call to RichInputConnection.commitPartialText
1441     *
1442     * SystemResponse: The IME is committing part of a word.  This happens if a space is
1443     * automatically inserted to split a single typed string into two or more words.
1444     */
1445    // TODO: This method is currently unused.  Find where it should be called from in the IME and
1446    // add invocations.
1447    private static final LogStatement LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT =
1448            new LogStatement("LatinIMECommitPartialText", true, false, "newCursorPosition");
1449    public static void latinIME_commitPartialText(final String committedWord,
1450            final long lastTimestampOfWordData, final boolean isBatchMode) {
1451        final ResearchLogger researchLogger = getInstance();
1452        final String scrubbedWord = scrubDigitsFromString(committedWord);
1453        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT);
1454        researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData,
1455                isBatchMode);
1456    }
1457
1458    /**
1459     * Log a call to RichInputConnection.commitText().
1460     *
1461     * SystemResponse: The IME is committing text.  This happens after the user has typed a word
1462     * and then a space or punctuation key.
1463     */
1464    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT =
1465            new LogStatement("RichInputConnectionCommitText", true, false, "newCursorPosition");
1466    public static void richInputConnection_commitText(final String committedWord,
1467            final int newCursorPosition, final boolean isBatchMode) {
1468        final ResearchLogger researchLogger = getInstance();
1469        final String scrubbedWord = scrubDigitsFromString(committedWord);
1470        if (!researchLogger.isExpectingCommitText) {
1471            researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT,
1472                    newCursorPosition);
1473            researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode);
1474        }
1475        researchLogger.isExpectingCommitText = false;
1476    }
1477
1478    /**
1479     * Shared event for logging committed text.
1480     */
1481    private static final LogStatement LOGSTATEMENT_COMMITTEXT =
1482            new LogStatement("CommitText", true, false, "committedText", "isBatchMode");
1483    private void enqueueCommitText(final String word, final boolean isBatchMode) {
1484        enqueueEvent(LOGSTATEMENT_COMMITTEXT, word, isBatchMode);
1485    }
1486
1487    /**
1488     * Log a call to RichInputConnection.deleteSurroundingText().
1489     *
1490     * SystemResponse: The IME has deleted text.
1491     */
1492    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT =
1493            new LogStatement("RichInputConnectionDeleteSurroundingText", true, false,
1494                    "beforeLength", "afterLength");
1495    public static void richInputConnection_deleteSurroundingText(final int beforeLength,
1496            final int afterLength) {
1497        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT,
1498                beforeLength, afterLength);
1499    }
1500
1501    /**
1502     * Log a call to RichInputConnection.finishComposingText().
1503     *
1504     * SystemResponse: The IME has left the composing text as-is.
1505     */
1506    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT =
1507            new LogStatement("RichInputConnectionFinishComposingText", false, false);
1508    public static void richInputConnection_finishComposingText() {
1509        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT);
1510    }
1511
1512    /**
1513     * Log a call to RichInputConnection.performEditorAction().
1514     *
1515     * SystemResponse: The IME is invoking an action specific to the editor.
1516     */
1517    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION =
1518            new LogStatement("RichInputConnectionPerformEditorAction", false, false,
1519                    "imeActionId");
1520    public static void richInputConnection_performEditorAction(final int imeActionId) {
1521        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION,
1522                imeActionId);
1523    }
1524
1525    /**
1526     * Log a call to RichInputConnection.sendKeyEvent().
1527     *
1528     * SystemResponse: The IME is telling the TextView that a key is being pressed through an
1529     * alternate channel.
1530     * TODO: only for hardware keys?
1531     */
1532    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT =
1533            new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action",
1534                    "code");
1535    public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) {
1536        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT,
1537                keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode());
1538    }
1539
1540    /**
1541     * Log a call to RichInputConnection.setComposingText().
1542     *
1543     * SystemResponse: The IME is setting the composing text.  Happens each time a character is
1544     * entered.
1545     */
1546    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT =
1547            new LogStatement("RichInputConnectionSetComposingText", true, true, "text",
1548                    "newCursorPosition");
1549    public static void richInputConnection_setComposingText(final CharSequence text,
1550            final int newCursorPosition) {
1551        if (text == null) {
1552            throw new RuntimeException("setComposingText is null");
1553        }
1554        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, text,
1555                newCursorPosition);
1556    }
1557
1558    /**
1559     * Log a call to RichInputConnection.setSelection().
1560     *
1561     * SystemResponse: The IME is requesting that the selection change.  User-initiated selection-
1562     * change requests do not go through this method -- it's only when the system wants to change
1563     * the selection.
1564     */
1565    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION =
1566            new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to");
1567    public static void richInputConnection_setSelection(final int from, final int to) {
1568        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to);
1569    }
1570
1571    /**
1572     * Log a call to SuddenJumpingTouchEventHandler.onTouchEvent().
1573     *
1574     * SystemResponse: The IME has filtered input events in case of an erroneous sensor reading.
1575     */
1576    private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT =
1577            new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false,
1578                    "motionEvent");
1579    public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
1580        if (me != null) {
1581            getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT,
1582                    me.toString());
1583        }
1584    }
1585
1586    /**
1587     * Log a call to SuggestionsView.setSuggestions().
1588     *
1589     * SystemResponse: The IME is setting the suggestions in the suggestion strip.
1590     */
1591    private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS =
1592            new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords");
1593    public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) {
1594        if (suggestedWords != null) {
1595            getInstance().enqueueEvent(LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS,
1596                    suggestedWords);
1597        }
1598    }
1599
1600    /**
1601     * The user has indicated a particular point in the log that is of interest.
1602     *
1603     * UserAction: From direct menu invocation.
1604     */
1605    private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP =
1606            new LogStatement("UserTimestamp", false, false);
1607    public void userTimestamp() {
1608        getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP);
1609    }
1610
1611    /**
1612     * Log a call to LatinIME.onEndBatchInput().
1613     *
1614     * SystemResponse: The system has completed a gesture.
1615     */
1616    private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT =
1617            new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText",
1618                    "enteredWordPos");
1619    public static void latinIME_onEndBatchInput(final CharSequence enteredText,
1620            final int enteredWordPos) {
1621        final ResearchLogger researchLogger = getInstance();
1622        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText,
1623                enteredWordPos);
1624        researchLogger.mStatistics.recordGestureInput(enteredText.length());
1625    }
1626
1627    /**
1628     * Log a call to LatinIME.handleBackspace().
1629     *
1630     * UserInput: The user is deleting a gestured word by hitting the backspace key once.
1631     */
1632    private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH =
1633            new LogStatement("LatinIMEHandleBackspaceBatch", true, false, "deletedText");
1634    public static void latinIME_handleBackspace_batch(final CharSequence deletedText) {
1635        final ResearchLogger researchLogger = getInstance();
1636        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText);
1637        researchLogger.mStatistics.recordGestureDelete();
1638    }
1639
1640    /**
1641     * Log statistics.
1642     *
1643     * ContextualData, recorded at the end of a session.
1644     */
1645    private static final LogStatement LOGSTATEMENT_STATISTICS =
1646            new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount",
1647                    "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting",
1648                    "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete",
1649                    "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete",
1650                    "dictionaryWordCount", "splitWordsCount", "gestureInputCount",
1651                    "gestureCharsCount", "gesturesDeletedCount", "manualSuggestionsCount",
1652                    "revertCommitsCount");
1653    private static void logStatistics() {
1654        final ResearchLogger researchLogger = getInstance();
1655        final Statistics statistics = researchLogger.mStatistics;
1656        researchLogger.enqueueEvent(LOGSTATEMENT_STATISTICS, statistics.mCharCount,
1657                statistics.mLetterCount, statistics.mNumberCount, statistics.mSpaceCount,
1658                statistics.mDeleteKeyCount, statistics.mWordCount, statistics.mIsEmptyUponStarting,
1659                statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(),
1660                statistics.mBeforeDeleteKeyCounter.getAverageTime(),
1661                statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(),
1662                statistics.mAfterDeleteKeyCounter.getAverageTime(),
1663                statistics.mDictionaryWordCount, statistics.mSplitWordsCount,
1664                statistics.mGesturesInputCount, statistics.mGesturesCharsCount,
1665                statistics.mGesturesDeletedCount, statistics.mManualSuggestionsCount,
1666                statistics.mRevertCommitsCount);
1667    }
1668}
1669