ResearchLogger.java revision f33f1cab2fd57fe39955593e68902554ed57a053
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under 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.accounts.Account;
22import android.accounts.AccountManager;
23import android.app.AlarmManager;
24import android.app.AlertDialog;
25import android.app.Dialog;
26import android.app.PendingIntent;
27import android.content.Context;
28import android.content.DialogInterface;
29import android.content.DialogInterface.OnCancelListener;
30import android.content.Intent;
31import android.content.SharedPreferences;
32import android.content.SharedPreferences.Editor;
33import android.content.pm.PackageInfo;
34import android.content.pm.PackageManager.NameNotFoundException;
35import android.content.res.Resources;
36import android.graphics.Canvas;
37import android.graphics.Color;
38import android.graphics.Paint;
39import android.graphics.Paint.Style;
40import android.net.Uri;
41import android.os.Build;
42import android.os.Bundle;
43import android.os.Handler;
44import android.os.IBinder;
45import android.os.SystemClock;
46import android.preference.PreferenceManager;
47import android.text.TextUtils;
48import android.text.format.DateUtils;
49import android.util.Log;
50import android.view.KeyEvent;
51import android.view.MotionEvent;
52import android.view.Window;
53import android.view.WindowManager;
54import android.view.inputmethod.CompletionInfo;
55import android.view.inputmethod.EditorInfo;
56import android.view.inputmethod.InputConnection;
57import android.widget.Toast;
58
59import com.android.inputmethod.keyboard.Key;
60import com.android.inputmethod.keyboard.Keyboard;
61import com.android.inputmethod.keyboard.KeyboardId;
62import com.android.inputmethod.keyboard.KeyboardSwitcher;
63import com.android.inputmethod.keyboard.KeyboardView;
64import com.android.inputmethod.keyboard.MainKeyboardView;
65import com.android.inputmethod.latin.Constants;
66import com.android.inputmethod.latin.Dictionary;
67import com.android.inputmethod.latin.InputTypeUtils;
68import com.android.inputmethod.latin.LatinIME;
69import com.android.inputmethod.latin.R;
70import com.android.inputmethod.latin.RichInputConnection;
71import com.android.inputmethod.latin.RichInputConnection.Range;
72import com.android.inputmethod.latin.Suggest;
73import com.android.inputmethod.latin.SuggestedWords;
74import com.android.inputmethod.latin.define.ProductionFlag;
75import com.android.inputmethod.research.MotionEventReader.ReplayData;
76
77import java.io.BufferedReader;
78import java.io.File;
79import java.io.FileInputStream;
80import java.io.FileNotFoundException;
81import java.io.IOException;
82import java.io.InputStreamReader;
83import java.nio.MappedByteBuffer;
84import java.nio.channels.FileChannel;
85import java.nio.charset.Charset;
86import java.text.SimpleDateFormat;
87import java.util.ArrayList;
88import java.util.Date;
89import java.util.List;
90import java.util.Locale;
91import java.util.Random;
92import java.util.UUID;
93
94/**
95 * Logs the use of the LatinIME keyboard.
96 *
97 * This class logs operations on the IME keyboard, including what the user has typed.
98 * Data is stored locally in a file in app-specific storage.
99 *
100 * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
101 */
102public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
103    // TODO: This class has grown quite large and combines several concerns that should be
104    // separated.  The following refactorings will be applied as soon as possible after adding
105    // support for replaying historical events, fixing some replay bugs, adding some ui constraints
106    // on the feedback dialog, and adding the survey dialog.
107    // TODO: Refactor.  Move splash screen code into separate class.
108    // TODO: Refactor.  Move feedback screen code into separate class.
109    // TODO: Refactor.  Move logging invocations into their own class.
110    // TODO: Refactor.  Move currentLogUnit management into separate class.
111    private static final String TAG = ResearchLogger.class.getSimpleName();
112    private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
113    private static final boolean DEBUG_REPLAY_AFTER_FEEDBACK = false
114            && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
115    // Whether the TextView contents are logged at the end of the session.  true will disclose
116    // private info.
117    private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false
118            && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
119    // Whether the feedback dialog preserves the editable text across invocations.  Should be false
120    // for normal research builds so users do not have to delete the same feedback string they
121    // entered earlier.  Should be true for builds internal to a development team so when the text
122    // field holds a channel name, the developer does not have to re-enter it when using the
123    // feedback mechanism to generate multiple tests.
124    private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false;
125    /* package */ static boolean sIsLogging = false;
126    private static final int OUTPUT_FORMAT_VERSION = 5;
127    private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
128    // Whether all words should be recorded, leaving unsampled word between bigrams.  Useful for
129    // testing.
130    /* package for test */ static final boolean IS_LOGGING_EVERYTHING = false
131            && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
132    // The number of words between n-grams to omit from the log.
133    private static final int NUMBER_OF_WORDS_BETWEEN_SAMPLES =
134            IS_LOGGING_EVERYTHING ? 0 : (DEBUG ? 2 : 18);
135
136    // Whether to show an indicator on the screen that logging is on.  Currently a very small red
137    // dot in the lower right hand corner.  Most users should not notice it.
138    private static final boolean IS_SHOWING_INDICATOR = true;
139    // Change the default indicator to something very visible.  Currently two red vertical bars on
140    // either side of they keyboard.
141    private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false ||
142            (IS_LOGGING_EVERYTHING && ProductionFlag.IS_EXPERIMENTAL_DEBUG);
143    // FEEDBACK_WORD_BUFFER_SIZE should add 1 because it must also hold the feedback LogUnit itself.
144    public static final int FEEDBACK_WORD_BUFFER_SIZE = (Integer.MAX_VALUE - 1) + 1;
145
146    // constants related to specific log points
147    private static final String WHITESPACE_SEPARATORS = " \t\n\r";
148    private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
149    private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel";
150
151    private static final long RESEARCHLOG_CLOSE_TIMEOUT_IN_MS = 5 * 1000;
152    private static final long RESEARCHLOG_ABORT_TIMEOUT_IN_MS = 5 * 1000;
153    private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
154    private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS;
155
156    private static final ResearchLogger sInstance = new ResearchLogger();
157    private static String sAccountType = null;
158    private static String sAllowedAccountDomain = null;
159    /* package */ ResearchLog mMainResearchLog;
160    // mFeedbackLog records all events for the session, private or not (excepting
161    // passwords).  It is written to permanent storage only if the user explicitly commands
162    // the system to do so.
163    // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
164    // complete.
165    /* package */ MainLogBuffer mMainLogBuffer;
166    // TODO: Remove the feedback log.  The feedback log continuously captured user data in case the
167    // user wanted to submit it.  We now use the mUserRecordingLogBuffer to allow the user to
168    // explicitly reproduce a problem.
169    /* package */ ResearchLog mFeedbackLog;
170    /* package */ LogBuffer mFeedbackLogBuffer;
171    /* package */ ResearchLog mUserRecordingLog;
172    /* package */ LogBuffer mUserRecordingLogBuffer;
173    private File mUserRecordingFile = null;
174
175    private boolean mIsPasswordView = false;
176    private boolean mIsLoggingSuspended = false;
177    private SharedPreferences mPrefs;
178
179    // digits entered by the user are replaced with this codepoint.
180    /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT =
181            Character.codePointAt("\uE000", 0);  // U+E000 is in the "private-use area"
182    // U+E001 is in the "private-use area"
183    /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
184    protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
185    // set when LatinIME should ignore an onUpdateSelection() callback that
186    // arises from operations in this class
187    private static boolean sLatinIMEExpectingUpdateSelection = false;
188
189    // used to check whether words are not unique
190    private Suggest mSuggest;
191    private MainKeyboardView mMainKeyboardView;
192    // TODO: Check whether a superclass can be used instead of LatinIME.
193    /* package for test */ LatinIME mLatinIME;
194    private final Statistics mStatistics;
195    private final MotionEventReader mMotionEventReader = new MotionEventReader();
196    private final Replayer mReplayer = Replayer.getInstance();
197    private ResearchLogDirectory mResearchLogDirectory;
198
199    private Intent mUploadIntent;
200    private Intent mUploadNowIntent;
201
202    /* package for test */ LogUnit mCurrentLogUnit = new LogUnit();
203
204    // Gestured or tapped words may be committed after the gesture of the next word has started.
205    // To ensure that the gesture data of the next word is not associated with the previous word,
206    // thereby leaking private data, we store the time of the down event that started the second
207    // gesture, and when committing the earlier word, split the LogUnit.
208    private long mSavedDownEventTime;
209    private Bundle mFeedbackDialogBundle = null;
210    private boolean mInFeedbackDialog = false;
211    // The feedback dialog causes stop() to be called for the keyboard connected to the original
212    // window.  This is because the feedback dialog must present its own EditText box that displays
213    // a keyboard.  stop() normally causes mFeedbackLogBuffer, which contains the user's data, to be
214    // cleared, and causes mFeedbackLog, which is ready to collect information in case the user
215    // wants to upload, to be closed.  This is good because we don't need to log information about
216    // what the user is typing in the feedback dialog, but bad because this data must be uploaded.
217    // Here we save the LogBuffer and Log so the feedback dialog can later access their data.
218    private LogBuffer mSavedFeedbackLogBuffer;
219    private ResearchLog mSavedFeedbackLog;
220    private Handler mUserRecordingTimeoutHandler;
221    private static final long USER_RECORDING_TIMEOUT_MS = 30L * DateUtils.SECOND_IN_MILLIS;
222
223    private ResearchLogger() {
224        mStatistics = Statistics.getInstance();
225    }
226
227    public static ResearchLogger getInstance() {
228        return sInstance;
229    }
230
231    public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher,
232            final Suggest suggest) {
233        assert latinIME != null;
234        mLatinIME = latinIME;
235        mPrefs = PreferenceManager.getDefaultSharedPreferences(latinIME);
236        mPrefs.registerOnSharedPreferenceChangeListener(this);
237
238        // Initialize fields from preferences
239        sIsLogging = ResearchSettings.readResearchLoggerEnabledFlag(mPrefs);
240
241        // Initialize fields from resources
242        final Resources res = latinIME.getResources();
243        sAccountType = res.getString(R.string.research_account_type);
244        sAllowedAccountDomain = res.getString(R.string.research_allowed_account_domain);
245
246        // Initialize directory manager
247        mResearchLogDirectory = new ResearchLogDirectory(mLatinIME);
248        cleanLogDirectoryIfNeeded(mResearchLogDirectory, System.currentTimeMillis());
249
250        // Initialize external services
251        mUploadIntent = new Intent(mLatinIME, UploaderService.class);
252        mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
253        mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true);
254        if (ProductionFlag.IS_EXPERIMENTAL) {
255            scheduleUploadingService(mLatinIME);
256        }
257        mReplayer.setKeyboardSwitcher(keyboardSwitcher);
258    }
259
260    private void cleanLogDirectoryIfNeeded(final ResearchLogDirectory researchLogDirectory,
261            final long now) {
262        final long lastCleanupTime = ResearchSettings.readResearchLastDirCleanupTime(mPrefs);
263        if (now - lastCleanupTime < DURATION_BETWEEN_DIR_CLEANUP_IN_MS) return;
264        final long oldestAllowedFileTime = now - MAX_LOGFILE_AGE_IN_MS;
265        mResearchLogDirectory.cleanupLogFilesOlderThan(oldestAllowedFileTime);
266        ResearchSettings.writeResearchLastDirCleanupTime(mPrefs, now);
267    }
268
269    /**
270     * Arrange for the UploaderService to be run on a regular basis.
271     *
272     * Any existing scheduled invocation of UploaderService is removed and rescheduled.  This may
273     * cause problems if this method is called often and frequent updates are required, but since
274     * the user will likely be sleeping at some point, if the interval is less that the expected
275     * sleep duration and this method is not called during that time, the service should be invoked
276     * at some point.
277     */
278    public static void scheduleUploadingService(Context context) {
279        final Intent intent = new Intent(context, UploaderService.class);
280        final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
281        final AlarmManager manager =
282                (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
283        manager.cancel(pendingIntent);
284        manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
285                UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent);
286    }
287
288    public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) {
289        mMainKeyboardView = mainKeyboardView;
290        maybeShowSplashScreen();
291    }
292
293    public void mainKeyboardView_onDetachedFromWindow() {
294        mMainKeyboardView = null;
295    }
296
297    public void onDestroy() {
298        if (mPrefs != null) {
299            mPrefs.unregisterOnSharedPreferenceChangeListener(this);
300        }
301    }
302
303    private Dialog mSplashDialog = null;
304
305    private void maybeShowSplashScreen() {
306        if (ResearchSettings.readHasSeenSplash(mPrefs)) {
307            return;
308        }
309        if (mSplashDialog != null && mSplashDialog.isShowing()) {
310            return;
311        }
312        final IBinder windowToken = mMainKeyboardView != null
313                ? mMainKeyboardView.getWindowToken() : null;
314        if (windowToken == null) {
315            return;
316        }
317        final AlertDialog.Builder builder = new AlertDialog.Builder(mLatinIME)
318                .setTitle(R.string.research_splash_title)
319                .setMessage(R.string.research_splash_content)
320                .setPositiveButton(android.R.string.yes,
321                        new DialogInterface.OnClickListener() {
322                            @Override
323                            public void onClick(DialogInterface dialog, int which) {
324                                onUserLoggingConsent();
325                                mSplashDialog.dismiss();
326                            }
327                })
328                .setNegativeButton(android.R.string.no,
329                        new DialogInterface.OnClickListener() {
330                            @Override
331                            public void onClick(DialogInterface dialog, int which) {
332                                final String packageName = mLatinIME.getPackageName();
333                                final Uri packageUri = Uri.parse("package:" + packageName);
334                                final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE,
335                                        packageUri);
336                                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
337                                mLatinIME.startActivity(intent);
338                            }
339                })
340                .setCancelable(true)
341                .setOnCancelListener(
342                        new OnCancelListener() {
343                            @Override
344                            public void onCancel(DialogInterface dialog) {
345                                mLatinIME.requestHideSelf(0);
346                            }
347                });
348        mSplashDialog = builder.create();
349        final Window w = mSplashDialog.getWindow();
350        final WindowManager.LayoutParams lp = w.getAttributes();
351        lp.token = windowToken;
352        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
353        w.setAttributes(lp);
354        w.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
355        mSplashDialog.show();
356    }
357
358    public void onUserLoggingConsent() {
359        if (mPrefs == null) {
360            mPrefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME);
361            if (mPrefs == null) return;
362        }
363        sIsLogging = true;
364        ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, true);
365        ResearchSettings.writeHasSeenSplash(mPrefs, true);
366        restart();
367    }
368
369    private void setLoggingAllowed(final boolean enableLogging) {
370        if (mPrefs == null) return;
371        sIsLogging = enableLogging;
372        ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, enableLogging);
373    }
374
375    private void checkForEmptyEditor() {
376        if (mLatinIME == null) {
377            return;
378        }
379        final InputConnection ic = mLatinIME.getCurrentInputConnection();
380        if (ic == null) {
381            return;
382        }
383        final CharSequence textBefore = ic.getTextBeforeCursor(1, 0);
384        if (!TextUtils.isEmpty(textBefore)) {
385            mStatistics.setIsEmptyUponStarting(false);
386            return;
387        }
388        final CharSequence textAfter = ic.getTextAfterCursor(1, 0);
389        if (!TextUtils.isEmpty(textAfter)) {
390            mStatistics.setIsEmptyUponStarting(false);
391            return;
392        }
393        if (textBefore != null && textAfter != null) {
394            mStatistics.setIsEmptyUponStarting(true);
395        }
396    }
397
398    private void start() {
399        if (DEBUG) {
400            Log.d(TAG, "start called");
401        }
402        maybeShowSplashScreen();
403        updateSuspendedState();
404        requestIndicatorRedraw();
405        mStatistics.reset();
406        checkForEmptyEditor();
407        if (!isAllowedToLog()) {
408            // Log.w(TAG, "not in usability mode; not logging");
409            return;
410        }
411        if (mMainLogBuffer == null) {
412            mMainResearchLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
413                    System.currentTimeMillis()), mLatinIME);
414            final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1);
415            mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore,
416                    mSuggest) {
417                @Override
418                protected void publish(final ArrayList<LogUnit> logUnits,
419                        boolean canIncludePrivateData) {
420                    canIncludePrivateData |= IS_LOGGING_EVERYTHING;
421                    final int length = logUnits.size();
422                    for (int i = 0; i < length; i++) {
423                        final LogUnit logUnit = logUnits.get(i);
424                        final String word = logUnit.getWord();
425                        if (word != null && word.length() > 0 && hasLetters(word)) {
426                            Log.d(TAG, "onPublish: " + word + ", hc: "
427                                    + logUnit.containsCorrection());
428                            final Dictionary dictionary = getDictionary();
429                            mStatistics.recordWordEntered(
430                                    dictionary != null && dictionary.isValidWord(word),
431                                    logUnit.containsCorrection());
432                        }
433                    }
434                    if (mMainResearchLog != null) {
435                        publishLogUnits(logUnits, mMainResearchLog, canIncludePrivateData);
436                    }
437                }
438            };
439        }
440        if (mFeedbackLogBuffer == null) {
441            resetFeedbackLogging();
442        }
443    }
444
445    private void resetFeedbackLogging() {
446        mFeedbackLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
447                System.currentTimeMillis()), mLatinIME);
448        mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE);
449    }
450
451    /* package */ void stop() {
452        if (DEBUG) {
453            Log.d(TAG, "stop called");
454        }
455        // Commit mCurrentLogUnit before closing.
456        commitCurrentLogUnit();
457
458        if (mMainLogBuffer != null) {
459            mMainLogBuffer.shiftAndPublishAll();
460            logStatistics();
461            commitCurrentLogUnit();
462            mMainLogBuffer.setIsStopping();
463            mMainLogBuffer.shiftAndPublishAll();
464            mMainResearchLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
465            mMainLogBuffer = null;
466        }
467        if (mFeedbackLogBuffer != null) {
468            mFeedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
469            mFeedbackLogBuffer = null;
470        }
471    }
472
473    public void abort() {
474        if (DEBUG) {
475            Log.d(TAG, "abort called");
476        }
477        if (mMainLogBuffer != null) {
478            mMainLogBuffer.clear();
479            mMainResearchLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
480            mMainLogBuffer = null;
481        }
482        if (mFeedbackLogBuffer != null) {
483            mFeedbackLogBuffer.clear();
484            mFeedbackLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
485            mFeedbackLogBuffer = null;
486        }
487    }
488
489    private void restart() {
490        stop();
491        start();
492    }
493
494    private long mResumeTime = 0L;
495    private void updateSuspendedState() {
496        final long time = System.currentTimeMillis();
497        if (time > mResumeTime) {
498            mIsLoggingSuspended = false;
499        }
500    }
501
502    @Override
503    public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
504        if (key == null || prefs == null) {
505            return;
506        }
507        sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
508        if (sIsLogging == false) {
509            abort();
510        }
511        requestIndicatorRedraw();
512        mPrefs = prefs;
513        prefsChanged(prefs);
514    }
515
516    public void onResearchKeySelected(final LatinIME latinIME) {
517        if (mInFeedbackDialog) {
518            Toast.makeText(latinIME, R.string.research_please_exit_feedback_form,
519                    Toast.LENGTH_LONG).show();
520            return;
521        }
522        presentFeedbackDialog(latinIME);
523    }
524
525    public void presentFeedbackDialog(final LatinIME latinIME) {
526        if (isMakingUserRecording()) {
527            saveRecording();
528        }
529        mInFeedbackDialog = true;
530        mSavedFeedbackLogBuffer = mFeedbackLogBuffer;
531        mSavedFeedbackLog = mFeedbackLog;
532        // Set the non-saved versions to null so that the stop() caused by switching to the
533        // Feedback dialog will not close them.
534        mFeedbackLogBuffer = null;
535        mFeedbackLog = null;
536
537        final Intent intent = new Intent();
538        intent.setClass(mLatinIME, FeedbackActivity.class);
539        if (mFeedbackDialogBundle == null) {
540            // Restore feedback field with channel name
541            final Bundle bundle = new Bundle();
542            bundle.putBoolean(FeedbackFragment.KEY_INCLUDE_ACCOUNT_NAME, true);
543            bundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, false);
544            if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) {
545                final String savedChannelName = mPrefs.getString(PREF_RESEARCH_SAVED_CHANNEL, "");
546                bundle.putString(FeedbackFragment.KEY_FEEDBACK_STRING, savedChannelName);
547            }
548            mFeedbackDialogBundle = bundle;
549        }
550        intent.putExtras(mFeedbackDialogBundle);
551        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
552        latinIME.startActivity(intent);
553    }
554
555    public void setFeedbackDialogBundle(final Bundle bundle) {
556        mFeedbackDialogBundle = bundle;
557    }
558
559    public void startRecording() {
560        final Resources res = mLatinIME.getResources();
561        Toast.makeText(mLatinIME,
562                res.getString(R.string.research_feedback_demonstration_instructions),
563                Toast.LENGTH_LONG).show();
564        startRecordingInternal();
565    }
566
567    private void startRecordingInternal() {
568        if (mUserRecordingLog != null) {
569            mUserRecordingLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
570        }
571        mUserRecordingFile = mResearchLogDirectory.getUserRecordingFilePath(
572                System.currentTimeMillis());
573        mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME);
574        mUserRecordingLogBuffer = new LogBuffer();
575        resetRecordingTimer();
576    }
577
578    private boolean isMakingUserRecording() {
579        return mUserRecordingLog != null;
580    }
581
582    private void resetRecordingTimer() {
583        if (mUserRecordingTimeoutHandler == null) {
584            mUserRecordingTimeoutHandler = new Handler();
585        }
586        clearRecordingTimer();
587        mUserRecordingTimeoutHandler.postDelayed(mRecordingHandlerTimeoutRunnable,
588                USER_RECORDING_TIMEOUT_MS);
589    }
590
591    private void clearRecordingTimer() {
592        mUserRecordingTimeoutHandler.removeCallbacks(mRecordingHandlerTimeoutRunnable);
593    }
594
595    private Runnable mRecordingHandlerTimeoutRunnable = new Runnable() {
596        @Override
597        public void run() {
598            cancelRecording();
599            requestIndicatorRedraw();
600            final Resources res = mLatinIME.getResources();
601            Toast.makeText(mLatinIME, res.getString(R.string.research_feedback_recording_failure),
602                    Toast.LENGTH_LONG).show();
603        }
604    };
605
606    private void cancelRecording() {
607        if (mUserRecordingLog != null) {
608            mUserRecordingLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
609        }
610        mUserRecordingLog = null;
611        mUserRecordingLogBuffer = null;
612        if (mFeedbackDialogBundle != null) {
613            mFeedbackDialogBundle.putBoolean("HasRecording", false);
614        }
615    }
616
617    private void saveRecording() {
618        commitCurrentLogUnit();
619        publishLogBuffer(mUserRecordingLogBuffer, mUserRecordingLog, true);
620        mUserRecordingLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
621        mUserRecordingLog = null;
622        mUserRecordingLogBuffer = null;
623
624        if (mFeedbackDialogBundle != null) {
625            mFeedbackDialogBundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, true);
626        }
627        clearRecordingTimer();
628    }
629
630    // TODO: currently unreachable.  Remove after being sure enable/disable is
631    // not needed.
632    /*
633    public void enableOrDisable(final boolean showEnable, final LatinIME latinIME) {
634        if (showEnable) {
635            if (!sIsLogging) {
636                setLoggingAllowed(true);
637            }
638            resumeLogging();
639            Toast.makeText(latinIME,
640                    R.string.research_notify_session_logging_enabled,
641                    Toast.LENGTH_LONG).show();
642        } else {
643            Toast toast = Toast.makeText(latinIME,
644                    R.string.research_notify_session_log_deleting,
645                    Toast.LENGTH_LONG);
646            toast.show();
647            boolean isLogDeleted = abort();
648            final long currentTime = System.currentTimeMillis();
649            final long resumeTime = currentTime + 1000 * 60 *
650                    SUSPEND_DURATION_IN_MINUTES;
651            suspendLoggingUntil(resumeTime);
652            toast.cancel();
653            Toast.makeText(latinIME, R.string.research_notify_logging_suspended,
654                    Toast.LENGTH_LONG).show();
655        }
656    }
657    */
658
659    /**
660     * Get the name of the first allowed account on the device.
661     *
662     * Allowed accounts must be in the domain given by ALLOWED_ACCOUNT_DOMAIN.
663     *
664     * @return The user's account name.
665     */
666    public String getAccountName() {
667        if (sAccountType == null || sAccountType.isEmpty()) {
668            return null;
669        }
670        if (sAllowedAccountDomain == null || sAllowedAccountDomain.isEmpty()) {
671            return null;
672        }
673        final AccountManager manager = AccountManager.get(mLatinIME);
674        // Filter first by account type.
675        final Account[] accounts = manager.getAccountsByType(sAccountType);
676
677        for (final Account account : accounts) {
678            if (DEBUG) {
679                Log.d(TAG, account.name);
680            }
681            final String[] parts = account.name.split("@");
682            if (parts.length > 1 && parts[1].equals(sAllowedAccountDomain)) {
683                return parts[0];
684            }
685        }
686        return null;
687    }
688
689    private static final LogStatement LOGSTATEMENT_FEEDBACK =
690            new LogStatement("UserFeedback", false, false, "contents", "accountName", "recording");
691    public void sendFeedback(final String feedbackContents, final boolean includeHistory,
692            final boolean isIncludingAccountName, final boolean isIncludingRecording) {
693        if (mSavedFeedbackLogBuffer == null) {
694            return;
695        }
696        if (!includeHistory) {
697            mSavedFeedbackLogBuffer.clear();
698        }
699        String recording = "";
700        if (isIncludingRecording) {
701            // Try to read recording from recently written json file
702            if (mUserRecordingFile != null) {
703                FileChannel channel = null;
704                try {
705                    channel = new FileInputStream(mUserRecordingFile).getChannel();
706                    final MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0,
707                            channel.size());
708                    // Android's openFileOutput() creates the file, so we use Android's default
709                    // Charset (UTF-8) here to read it.
710                    recording = Charset.defaultCharset().decode(buffer).toString();
711                } catch (FileNotFoundException e) {
712                    Log.e(TAG, "Could not find recording file", e);
713                } catch (IOException e) {
714                    Log.e(TAG, "Error reading recording file", e);
715                } finally {
716                    if (channel != null) {
717                        try {
718                            channel.close();
719                        } catch (IOException e) {
720                            Log.e(TAG, "Error closing recording file", e);
721                        }
722                    }
723                }
724            }
725        }
726        final LogUnit feedbackLogUnit = new LogUnit();
727        final String accountName = isIncludingAccountName ? getAccountName() : "";
728        feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
729                feedbackContents, accountName, recording);
730        mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
731        publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */);
732        mSavedFeedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
733        uploadNow();
734
735        if (isIncludingRecording && DEBUG_REPLAY_AFTER_FEEDBACK) {
736            final Handler handler = new Handler();
737            handler.postDelayed(new Runnable() {
738                @Override
739                public void run() {
740                    final ReplayData replayData =
741                            mMotionEventReader.readMotionEventData(mUserRecordingFile);
742                    mReplayer.replay(replayData, null);
743                }
744            }, 1000);
745        }
746
747        if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) {
748            // Use feedback string as a channel name to label feedback strings.  Here we record the
749            // string for prepopulating the field next time.
750            final String channelName = feedbackContents;
751            if (mPrefs == null) {
752                return;
753            }
754            mPrefs.edit().putString(PREF_RESEARCH_SAVED_CHANNEL, channelName).apply();
755        }
756    }
757
758    public void uploadNow() {
759        if (DEBUG) {
760            Log.d(TAG, "calling uploadNow()");
761        }
762        mLatinIME.startService(mUploadNowIntent);
763    }
764
765    public void onLeavingSendFeedbackDialog() {
766        mInFeedbackDialog = false;
767    }
768
769    public void initSuggest(final Suggest suggest) {
770        mSuggest = suggest;
771        // MainLogBuffer has out-of-date Suggest object.  Need to close it down and create a new
772        // one.
773        if (mMainLogBuffer != null) {
774            stop();
775            start();
776        }
777    }
778
779    private Dictionary getDictionary() {
780        if (mSuggest == null) {
781            return null;
782        }
783        return mSuggest.getMainDictionary();
784    }
785
786    private void setIsPasswordView(boolean isPasswordView) {
787        mIsPasswordView = isPasswordView;
788    }
789
790    private boolean isAllowedToLog() {
791        return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog
792                && !isReplaying();
793    }
794
795    public void requestIndicatorRedraw() {
796        if (!IS_SHOWING_INDICATOR) {
797            return;
798        }
799        if (mMainKeyboardView == null) {
800            return;
801        }
802        mMainKeyboardView.invalidateAllKeys();
803    }
804
805    private boolean isReplaying() {
806        return mReplayer.isReplaying();
807    }
808
809    private int getIndicatorColor() {
810        if (isMakingUserRecording()) {
811            return Color.YELLOW;
812        }
813        if (isReplaying()) {
814            return Color.GREEN;
815        }
816        return Color.RED;
817    }
818
819    public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width,
820            int height) {
821        // TODO: Reimplement using a keyboard background image specific to the ResearchLogger
822        // and remove this method.
823        // The check for MainKeyboardView ensures that the indicator only decorates the main
824        // keyboard, not every keyboard.
825        if (IS_SHOWING_INDICATOR && (isAllowedToLog() || isReplaying())
826                && view instanceof MainKeyboardView) {
827            final int savedColor = paint.getColor();
828            paint.setColor(getIndicatorColor());
829            final Style savedStyle = paint.getStyle();
830            paint.setStyle(Style.STROKE);
831            final float savedStrokeWidth = paint.getStrokeWidth();
832            if (IS_SHOWING_INDICATOR_CLEARLY) {
833                paint.setStrokeWidth(5);
834                canvas.drawLine(0, 0, 0, height, paint);
835                canvas.drawLine(width, 0, width, height, paint);
836            } else {
837                // Put a tiny dot on the screen so a knowledgeable user can check whether it is
838                // enabled.  The dot is actually a zero-width, zero-height rectangle, placed at the
839                // lower-right corner of the canvas, painted with a non-zero border width.
840                paint.setStrokeWidth(3);
841                canvas.drawRect(width, height, width, height, paint);
842            }
843            paint.setColor(savedColor);
844            paint.setStyle(savedStyle);
845            paint.setStrokeWidth(savedStrokeWidth);
846        }
847    }
848
849    /**
850     * Buffer a research log event, flagging it as privacy-sensitive.
851     */
852    private synchronized void enqueueEvent(final LogStatement logStatement,
853            final Object... values) {
854        enqueueEvent(mCurrentLogUnit, logStatement, values);
855    }
856
857    private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement,
858            final Object... values) {
859        assert values.length == logStatement.getKeys().length;
860        if (isAllowedToLog() && logUnit != null) {
861            final long time = SystemClock.uptimeMillis();
862            logUnit.addLogStatement(logStatement, time, values);
863        }
864    }
865
866    private void setCurrentLogUnitContainsDigitFlag() {
867        mCurrentLogUnit.setMayContainDigit();
868    }
869
870    private void setCurrentLogUnitContainsCorrection() {
871        mCurrentLogUnit.setContainsCorrection();
872    }
873
874    private void setCurrentLogUnitCorrectionType(final int correctionType) {
875        mCurrentLogUnit.setCorrectionType(correctionType);
876    }
877
878    /* package for test */ void commitCurrentLogUnit() {
879        if (DEBUG) {
880            Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasWord() ?
881                    ": " + mCurrentLogUnit.getWord() : ""));
882        }
883        if (!mCurrentLogUnit.isEmpty()) {
884            if (mMainLogBuffer != null) {
885                mMainLogBuffer.shiftIn(mCurrentLogUnit);
886            }
887            if (mFeedbackLogBuffer != null) {
888                mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
889            }
890            if (mUserRecordingLogBuffer != null) {
891                mUserRecordingLogBuffer.shiftIn(mCurrentLogUnit);
892            }
893            mCurrentLogUnit = new LogUnit();
894        } else {
895            if (DEBUG) {
896                Log.d(TAG, "Warning: tried to commit empty log unit.");
897            }
898        }
899    }
900
901    private static final LogStatement LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT =
902            new LogStatement("UncommitCurrentLogUnit", false, false);
903    public void uncommitCurrentLogUnit(final String expectedWord,
904            final boolean dumpCurrentLogUnit) {
905        // The user has deleted this word and returned to the previous.  Check that the word in the
906        // logUnit matches the expected word.  If so, restore the last log unit committed to be the
907        // current logUnit.  I.e., pull out the last LogUnit from all the LogBuffers, and make
908        // restore it to mCurrentLogUnit so the new edits are captured with the word.  Optionally
909        // dump the contents of mCurrentLogUnit (useful if they contain deletions of the next word
910        // that should not be reported to protect user privacy)
911        //
912        // Note that we don't use mLastLogUnit here, because it only goes one word back and is only
913        // needed for reverts, which only happen one back.
914        if (mMainLogBuffer == null) {
915            return;
916        }
917        final LogUnit oldLogUnit = mMainLogBuffer.peekLastLogUnit();
918
919        // Check that expected word matches.
920        if (oldLogUnit != null) {
921            final String oldLogUnitWord = oldLogUnit.getWord();
922            if (!oldLogUnitWord.equals(expectedWord)) {
923                return;
924            }
925        }
926
927        // Uncommit, merging if necessary.
928        mMainLogBuffer.unshiftIn();
929        if (oldLogUnit != null && !dumpCurrentLogUnit) {
930            oldLogUnit.append(mCurrentLogUnit);
931            mSavedDownEventTime = Long.MAX_VALUE;
932        }
933        if (oldLogUnit == null) {
934            mCurrentLogUnit = new LogUnit();
935        } else {
936            mCurrentLogUnit = oldLogUnit;
937        }
938        if (mFeedbackLogBuffer != null) {
939            mFeedbackLogBuffer.unshiftIn();
940        }
941        enqueueEvent(LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT);
942        if (DEBUG) {
943            Log.d(TAG, "uncommitCurrentLogUnit (dump=" + dumpCurrentLogUnit + ") back to "
944                    + (mCurrentLogUnit.hasWord() ? ": '" + mCurrentLogUnit.getWord() + "'" : ""));
945        }
946    }
947
948    /**
949     * Publish all the logUnits in the logBuffer, without doing any privacy filtering.
950     */
951    /* package for test */ void publishLogBuffer(final LogBuffer logBuffer,
952            final ResearchLog researchLog, final boolean canIncludePrivateData) {
953        publishLogUnits(logBuffer.getLogUnits(), researchLog, canIncludePrivateData);
954    }
955
956    private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_OPENING =
957            new LogStatement("logSegmentStart", false, false, "isIncludingPrivateData");
958    private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_CLOSING =
959            new LogStatement("logSegmentEnd", false, false);
960    /**
961     * Publish all LogUnits in a list.
962     *
963     * Any privacy checks should be performed before calling this method.
964     */
965    /* package for test */ void publishLogUnits(final List<LogUnit> logUnits,
966            final ResearchLog researchLog, final boolean canIncludePrivateData) {
967        final LogUnit openingLogUnit = new LogUnit();
968        if (logUnits.isEmpty()) return;
969        // LogUnits not containing private data, such as contextual data for the log, do not require
970        // logSegment boundary statements.
971        if (canIncludePrivateData) {
972            openingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_OPENING,
973                    SystemClock.uptimeMillis(), canIncludePrivateData);
974            researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */);
975        }
976        for (LogUnit logUnit : logUnits) {
977            if (DEBUG) {
978                Log.d(TAG, "publishLogBuffer: " + (logUnit.hasWord() ? logUnit.getWord()
979                        : "<wordless>") + ", correction?: " + logUnit.containsCorrection());
980            }
981            researchLog.publish(logUnit, canIncludePrivateData);
982        }
983        if (canIncludePrivateData) {
984            final LogUnit closingLogUnit = new LogUnit();
985            closingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_CLOSING,
986                    SystemClock.uptimeMillis());
987            researchLog.publish(closingLogUnit, true /* isIncludingPrivateData */);
988        }
989    }
990
991    public static boolean hasLetters(final String word) {
992        final int length = word.length();
993        for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
994            final int codePoint = word.codePointAt(i);
995            if (Character.isLetter(codePoint)) {
996                return true;
997            }
998        }
999        return false;
1000    }
1001
1002    /**
1003     * Commit the portion of mCurrentLogUnit before maxTime as a worded logUnit.
1004     *
1005     * After this operation completes, mCurrentLogUnit will hold any logStatements that happened
1006     * after maxTime.
1007     */
1008    /* package for test */ void commitCurrentLogUnitAsWord(final String word, final long maxTime,
1009            final boolean isBatchMode) {
1010        if (word == null) {
1011            return;
1012        }
1013        if (word.length() > 0 && hasLetters(word)) {
1014            mCurrentLogUnit.setWord(word);
1015        }
1016        final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime);
1017        enqueueCommitText(word, isBatchMode);
1018        commitCurrentLogUnit();
1019        mCurrentLogUnit = newLogUnit;
1020    }
1021
1022    /**
1023     * Record the time of a MotionEvent.ACTION_DOWN.
1024     *
1025     * Warning: Not thread safe.  Only call from the main thread.
1026     */
1027    private void setSavedDownEventTime(final long time) {
1028        mSavedDownEventTime = time;
1029    }
1030
1031    public void onWordFinished(final String word, final boolean isBatchMode) {
1032        commitCurrentLogUnitAsWord(word, mSavedDownEventTime, isBatchMode);
1033        mSavedDownEventTime = Long.MAX_VALUE;
1034    }
1035
1036    private static int scrubDigitFromCodePoint(int codePoint) {
1037        return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint;
1038    }
1039
1040    /* package for test */ static String scrubDigitsFromString(String s) {
1041        StringBuilder sb = null;
1042        final int length = s.length();
1043        for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) {
1044            final int codePoint = Character.codePointAt(s, i);
1045            if (Character.isDigit(codePoint)) {
1046                if (sb == null) {
1047                    sb = new StringBuilder(length);
1048                    sb.append(s.substring(0, i));
1049                }
1050                sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT);
1051            } else {
1052                if (sb != null) {
1053                    sb.appendCodePoint(codePoint);
1054                }
1055            }
1056        }
1057        if (sb == null) {
1058            return s;
1059        } else {
1060            return sb.toString();
1061        }
1062    }
1063
1064    private String scrubWord(String word) {
1065        final Dictionary dictionary = getDictionary();
1066        if (dictionary == null) {
1067            return WORD_REPLACEMENT_STRING;
1068        }
1069        if (dictionary.isValidWord(word)) {
1070            return word;
1071        }
1072        return WORD_REPLACEMENT_STRING;
1073    }
1074
1075    // Specific logging methods follow below.  The comments for each logging method should
1076    // indicate what specific method is logged, and how to trigger it from the user interface.
1077    //
1078    // Logging methods can be generally classified into two flavors, "UserAction", which should
1079    // correspond closely to an event that is sensed by the IME, and is usually generated
1080    // directly by the user, and "SystemResponse" which corresponds to an event that the IME
1081    // generates, often after much processing of user input.  SystemResponses should correspond
1082    // closely to user-visible events.
1083    // TODO: Consider exposing the UserAction classification in the log output.
1084
1085    /**
1086     * Log a call to LatinIME.onStartInputViewInternal().
1087     *
1088     * UserAction: called each time the keyboard is opened up.
1089     */
1090    private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL =
1091            new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid",
1092                    "packageName", "inputType", "imeOptions", "fieldId", "display", "model",
1093                    "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything",
1094                    "isExperimentalDebug");
1095    public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
1096            final SharedPreferences prefs) {
1097        final ResearchLogger researchLogger = getInstance();
1098        if (editorInfo != null) {
1099            final boolean isPassword = InputTypeUtils.isPasswordInputType(editorInfo.inputType)
1100                    || InputTypeUtils.isVisiblePasswordInputType(editorInfo.inputType);
1101            getInstance().setIsPasswordView(isPassword);
1102            researchLogger.start();
1103            final Context context = researchLogger.mLatinIME;
1104            try {
1105                final PackageInfo packageInfo;
1106                packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),
1107                        0);
1108                final Integer versionCode = packageInfo.versionCode;
1109                final String versionName = packageInfo.versionName;
1110                final String uuid = ResearchSettings.readResearchLoggerUuid(researchLogger.mPrefs);
1111                researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL,
1112                        uuid, editorInfo.packageName, Integer.toHexString(editorInfo.inputType),
1113                        Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId,
1114                        Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName,
1115                        OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING,
1116                        ProductionFlag.IS_EXPERIMENTAL_DEBUG);
1117            } catch (NameNotFoundException e) {
1118                e.printStackTrace();
1119            }
1120        }
1121    }
1122
1123    public void latinIME_onFinishInputViewInternal() {
1124        stop();
1125    }
1126
1127    /**
1128     * Log a change in preferences.
1129     *
1130     * UserAction: called when the user changes the settings.
1131     */
1132    private static final LogStatement LOGSTATEMENT_PREFS_CHANGED =
1133            new LogStatement("PrefsChanged", false, false, "prefs");
1134    public static void prefsChanged(final SharedPreferences prefs) {
1135        final ResearchLogger researchLogger = getInstance();
1136        researchLogger.enqueueEvent(LOGSTATEMENT_PREFS_CHANGED, prefs);
1137    }
1138
1139    /**
1140     * Log a call to MainKeyboardView.processMotionEvent().
1141     *
1142     * UserAction: called when the user puts their finger onto the screen (ACTION_DOWN).
1143     *
1144     */
1145    private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT =
1146            new LogStatement("MotionEvent", true, false, "action",
1147                    LogStatement.KEY_IS_LOGGING_RELATED, "motionEvent");
1148    public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action,
1149            final long eventTime, final int index, final int id, final int x, final int y) {
1150        if (me != null) {
1151            final String actionString = LoggingUtils.getMotionEventActionTypeString(action);
1152            final ResearchLogger researchLogger = getInstance();
1153            researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
1154                    actionString, false /* IS_LOGGING_RELATED */, MotionEvent.obtain(me));
1155            if (action == MotionEvent.ACTION_DOWN) {
1156                // Subtract 1 from eventTime so the down event is included in the later
1157                // LogUnit, not the earlier (the test is for inequality).
1158                researchLogger.setSavedDownEventTime(eventTime - 1);
1159            }
1160            // Refresh the timer in case we are capturing user feedback.
1161            if (researchLogger.isMakingUserRecording()) {
1162                researchLogger.resetRecordingTimer();
1163            }
1164        }
1165    }
1166
1167    /**
1168     * Log a call to LatinIME.onCodeInput().
1169     *
1170     * SystemResponse: The main processing step for entering text.  Called when the user performs a
1171     * tap, a flick, a long press, releases a gesture, or taps a punctuation suggestion.
1172     */
1173    private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT =
1174            new LogStatement("LatinImeOnCodeInput", true, false, "code", "x", "y");
1175    public static void latinIME_onCodeInput(final int code, final int x, final int y) {
1176        final long time = SystemClock.uptimeMillis();
1177        final ResearchLogger researchLogger = getInstance();
1178        researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT,
1179                Constants.printableCode(scrubDigitFromCodePoint(code)), x, y);
1180        if (Character.isDigit(code)) {
1181            researchLogger.setCurrentLogUnitContainsDigitFlag();
1182        }
1183        researchLogger.mStatistics.recordChar(code, time);
1184    }
1185    /**
1186     * Log a call to LatinIME.onDisplayCompletions().
1187     *
1188     * SystemResponse: The IME has displayed application-specific completions.  They may show up
1189     * in the suggestion strip, such as a landscape phone.
1190     */
1191    private static final LogStatement LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS =
1192            new LogStatement("LatinIMEOnDisplayCompletions", true, true,
1193                    "applicationSpecifiedCompletions");
1194    public static void latinIME_onDisplayCompletions(
1195            final CompletionInfo[] applicationSpecifiedCompletions) {
1196        // Note; passing an array as a single element in a vararg list.  Must create a new
1197        // dummy array around it or it will get expanded.
1198        getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS,
1199                new Object[] { applicationSpecifiedCompletions });
1200    }
1201
1202    public static boolean getAndClearLatinIMEExpectingUpdateSelection() {
1203        boolean returnValue = sLatinIMEExpectingUpdateSelection;
1204        sLatinIMEExpectingUpdateSelection = false;
1205        return returnValue;
1206    }
1207
1208    /**
1209     * Log a call to LatinIME.onWindowHidden().
1210     *
1211     * UserAction: The user has performed an action that has caused the IME to be closed.  They may
1212     * have focused on something other than a text field, or explicitly closed it.
1213     */
1214    private static final LogStatement LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN =
1215            new LogStatement("LatinIMEOnWindowHidden", false, false, "isTextTruncated", "text");
1216    public static void latinIME_onWindowHidden(final int savedSelectionStart,
1217            final int savedSelectionEnd, final InputConnection ic) {
1218        if (ic != null) {
1219            final boolean isTextTruncated;
1220            final String text;
1221            if (LOG_FULL_TEXTVIEW_CONTENTS) {
1222                // Capture the TextView contents.  This will trigger onUpdateSelection(), so we
1223                // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called,
1224                // it can tell that it was generated by the logging code, and not by the user, and
1225                // therefore keep user-visible state as is.
1226                ic.beginBatchEdit();
1227                ic.performContextMenuAction(android.R.id.selectAll);
1228                CharSequence charSequence = ic.getSelectedText(0);
1229                if (savedSelectionStart != -1 && savedSelectionEnd != -1) {
1230                    ic.setSelection(savedSelectionStart, savedSelectionEnd);
1231                }
1232                ic.endBatchEdit();
1233                sLatinIMEExpectingUpdateSelection = true;
1234                if (TextUtils.isEmpty(charSequence)) {
1235                    isTextTruncated = false;
1236                    text = "";
1237                } else {
1238                    if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) {
1239                        int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE;
1240                        // do not cut in the middle of a supplementary character
1241                        final char c = charSequence.charAt(length - 1);
1242                        if (Character.isHighSurrogate(c)) {
1243                            length--;
1244                        }
1245                        final CharSequence truncatedCharSequence = charSequence.subSequence(0,
1246                                length);
1247                        isTextTruncated = true;
1248                        text = truncatedCharSequence.toString();
1249                    } else {
1250                        isTextTruncated = false;
1251                        text = charSequence.toString();
1252                    }
1253                }
1254            } else {
1255                isTextTruncated = true;
1256                text = "";
1257            }
1258            final ResearchLogger researchLogger = getInstance();
1259            // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g.
1260            // during a live user test), so the normal isPotentiallyPrivate and
1261            // isPotentiallyRevealing flags do not apply
1262            researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN, isTextTruncated,
1263                    text);
1264            researchLogger.commitCurrentLogUnit();
1265            getInstance().stop();
1266        }
1267    }
1268
1269    /**
1270     * Log a call to LatinIME.onUpdateSelection().
1271     *
1272     * UserAction/SystemResponse: The user has moved the cursor or selection.  This function may
1273     * be called, however, when the system has moved the cursor, say by inserting a character.
1274     */
1275    private static final LogStatement LOGSTATEMENT_LATINIME_ONUPDATESELECTION =
1276            new LogStatement("LatinIMEOnUpdateSelection", true, false, "lastSelectionStart",
1277                    "lastSelectionEnd", "oldSelStart", "oldSelEnd", "newSelStart", "newSelEnd",
1278                    "composingSpanStart", "composingSpanEnd", "expectingUpdateSelection",
1279                    "expectingUpdateSelectionFromLogger", "context");
1280    public static void latinIME_onUpdateSelection(final int lastSelectionStart,
1281            final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
1282            final int newSelStart, final int newSelEnd, final int composingSpanStart,
1283            final int composingSpanEnd, final boolean expectingUpdateSelection,
1284            final boolean expectingUpdateSelectionFromLogger,
1285            final RichInputConnection connection) {
1286        String word = "";
1287        if (connection != null) {
1288            Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
1289            if (range != null) {
1290                word = range.mWord;
1291            }
1292        }
1293        final ResearchLogger researchLogger = getInstance();
1294        final String scrubbedWord = researchLogger.scrubWord(word);
1295        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONUPDATESELECTION, lastSelectionStart,
1296                lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd,
1297                composingSpanStart, composingSpanEnd, expectingUpdateSelection,
1298                expectingUpdateSelectionFromLogger, scrubbedWord);
1299    }
1300
1301    /**
1302     * Log a call to LatinIME.onTextInput().
1303     *
1304     * SystemResponse: Raw text is added to the TextView.
1305     */
1306    public static void latinIME_onTextInput(final String text, final boolean isBatchMode) {
1307        final ResearchLogger researchLogger = getInstance();
1308        researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode);
1309    }
1310
1311    /**
1312     * Log a call to LatinIME.pickSuggestionManually().
1313     *
1314     * UserAction: The user has chosen a specific word from the suggestion strip.
1315     */
1316    private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY =
1317            new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index",
1318                    "suggestion", "x", "y", "isBatchMode");
1319    public static void latinIME_pickSuggestionManually(final String replacedWord,
1320            final int index, final String suggestion, final boolean isBatchMode) {
1321        final ResearchLogger researchLogger = getInstance();
1322        if (!replacedWord.equals(suggestion.toString())) {
1323            // The user chose something other than what was already there.
1324            researchLogger.setCurrentLogUnitContainsCorrection();
1325            researchLogger.setCurrentLogUnitCorrectionType(LogUnit.CORRECTIONTYPE_TYPO);
1326        }
1327        final String scrubbedWord = scrubDigitsFromString(suggestion);
1328        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY,
1329                scrubDigitsFromString(replacedWord), index,
1330                suggestion == null ? null : scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE,
1331                Constants.SUGGESTION_STRIP_COORDINATE, isBatchMode);
1332        researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode);
1333        researchLogger.mStatistics.recordManualSuggestion(SystemClock.uptimeMillis());
1334    }
1335
1336    /**
1337     * Log a call to LatinIME.punctuationSuggestion().
1338     *
1339     * UserAction: The user has chosen punctuation from the punctuation suggestion strip.
1340     */
1341    private static final LogStatement LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION =
1342            new LogStatement("LatinIMEPunctuationSuggestion", false, false, "index", "suggestion",
1343                    "x", "y", "isPrediction");
1344    public static void latinIME_punctuationSuggestion(final int index, final String suggestion,
1345            final boolean isBatchMode, final boolean isPrediction) {
1346        final ResearchLogger researchLogger = getInstance();
1347        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion,
1348                Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE,
1349                isPrediction);
1350        researchLogger.commitCurrentLogUnitAsWord(suggestion, Long.MAX_VALUE, isBatchMode);
1351    }
1352
1353    /**
1354     * Log a call to LatinIME.sendKeyCodePoint().
1355     *
1356     * SystemResponse: The IME is inserting text into the TextView for numbers, fixed strings, or
1357     * some other unusual mechanism.
1358     */
1359    private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT =
1360            new LogStatement("LatinIMESendKeyCodePoint", true, false, "code");
1361    public static void latinIME_sendKeyCodePoint(final int code) {
1362        final ResearchLogger researchLogger = getInstance();
1363        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT,
1364                Constants.printableCode(scrubDigitFromCodePoint(code)));
1365        if (Character.isDigit(code)) {
1366            researchLogger.setCurrentLogUnitContainsDigitFlag();
1367        }
1368    }
1369
1370    /**
1371     * Log a call to LatinIME.promotePhantomSpace().
1372     *
1373     * SystemResponse: The IME is inserting a real space in place of a phantom space.
1374     */
1375    private static final LogStatement LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE =
1376            new LogStatement("LatinIMEPromotPhantomSpace", false, false);
1377    public static void latinIME_promotePhantomSpace() {
1378        final ResearchLogger researchLogger = getInstance();
1379        final LogUnit logUnit;
1380        if (researchLogger.mMainLogBuffer == null) {
1381            logUnit = researchLogger.mCurrentLogUnit;
1382        } else {
1383            logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
1384        }
1385        researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE);
1386    }
1387
1388    /**
1389     * Log a call to LatinIME.swapSwapperAndSpace().
1390     *
1391     * SystemResponse: A symbol has been swapped with a space character.  E.g. punctuation may swap
1392     * if a soft space is inserted after a word.
1393     */
1394    private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE =
1395            new LogStatement("LatinIMESwapSwapperAndSpace", false, false, "originalCharacters",
1396                    "charactersAfterSwap");
1397    public static void latinIME_swapSwapperAndSpace(final CharSequence originalCharacters,
1398            final String charactersAfterSwap) {
1399        final ResearchLogger researchLogger = getInstance();
1400        final LogUnit logUnit;
1401        if (researchLogger.mMainLogBuffer == null) {
1402            logUnit = null;
1403        } else {
1404            logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
1405        }
1406        if (logUnit != null) {
1407            researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE,
1408                    originalCharacters, charactersAfterSwap);
1409        }
1410    }
1411
1412    /**
1413     * Log a call to LatinIME.maybeDoubleSpacePeriod().
1414     *
1415     * SystemResponse: Two spaces have been replaced by period space.
1416     */
1417    public static void latinIME_maybeDoubleSpacePeriod(final String text,
1418            final boolean isBatchMode) {
1419        final ResearchLogger researchLogger = getInstance();
1420        researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode);
1421    }
1422
1423    /**
1424     * Log a call to MainKeyboardView.onLongPress().
1425     *
1426     * UserAction: The user has performed a long-press on a key.
1427     */
1428    private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS =
1429            new LogStatement("MainKeyboardViewOnLongPress", false, false);
1430    public static void mainKeyboardView_onLongPress() {
1431        getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS);
1432    }
1433
1434    /**
1435     * Log a call to MainKeyboardView.setKeyboard().
1436     *
1437     * SystemResponse: The IME has switched to a new keyboard (e.g. French, English).
1438     * This is typically called right after LatinIME.onStartInputViewInternal (when starting a new
1439     * IME), but may happen at other times if the user explicitly requests a keyboard change.
1440     */
1441    private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD =
1442            new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale",
1443                    "orientation", "width", "modeName", "action", "navigateNext",
1444                    "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled",
1445                    "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th",
1446                    "keys");
1447    public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) {
1448        final KeyboardId kid = keyboard.mId;
1449        final boolean isPasswordView = kid.passwordInput();
1450        final ResearchLogger researchLogger = getInstance();
1451        researchLogger.setIsPasswordView(isPasswordView);
1452        researchLogger.enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD,
1453                KeyboardId.elementIdToName(kid.mElementId),
1454                kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
1455                kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(),
1456                kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey,
1457                isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey,
1458                kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth,
1459                keyboard.mOccupiedHeight, keyboard.mKeys);
1460    }
1461
1462    /**
1463     * Log a call to LatinIME.revertCommit().
1464     *
1465     * SystemResponse: The IME has reverted commited text.  This happens when the user enters
1466     * a word, commits it by pressing space or punctuation, and then reverts the commit by hitting
1467     * backspace.
1468     */
1469    private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT =
1470            new LogStatement("LatinIMERevertCommit", true, false, "committedWord",
1471                    "originallyTypedWord", "separatorString");
1472    public static void latinIME_revertCommit(final String committedWord,
1473            final String originallyTypedWord, final boolean isBatchMode,
1474            final String separatorString) {
1475        final ResearchLogger researchLogger = getInstance();
1476        // TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word.
1477        final LogUnit logUnit;
1478        if (researchLogger.mMainLogBuffer == null) {
1479            logUnit = null;
1480        } else {
1481            logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
1482        }
1483        if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) {
1484            if (logUnit != null) {
1485                logUnit.setWord(originallyTypedWord);
1486            }
1487        }
1488        researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit,
1489                LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, originallyTypedWord,
1490                separatorString);
1491        if (logUnit != null) {
1492            logUnit.setContainsCorrection();
1493        }
1494        researchLogger.mStatistics.recordRevertCommit(SystemClock.uptimeMillis());
1495        researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE, isBatchMode);
1496    }
1497
1498    /**
1499     * Log a call to PointerTracker.callListenerOnCancelInput().
1500     *
1501     * UserAction: The user has canceled the input, e.g., by pressing down, but then removing
1502     * outside the keyboard area.
1503     * TODO: Verify
1504     */
1505    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT =
1506            new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false);
1507    public static void pointerTracker_callListenerOnCancelInput() {
1508        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT);
1509    }
1510
1511    /**
1512     * Log a call to PointerTracker.callListenerOnCodeInput().
1513     *
1514     * SystemResponse: The user has entered a key through the normal tapping mechanism.
1515     * LatinIME.onCodeInput will also be called.
1516     */
1517    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT =
1518            new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "code",
1519                    "outputText", "x", "y", "ignoreModifierKey", "altersCode", "isEnabled");
1520    public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
1521            final int y, final boolean ignoreModifierKey, final boolean altersCode,
1522            final int code) {
1523        if (key != null) {
1524            String outputText = key.getOutputText();
1525            final ResearchLogger researchLogger = getInstance();
1526            researchLogger.enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
1527                    Constants.printableCode(scrubDigitFromCodePoint(code)),
1528                    outputText == null ? null : scrubDigitsFromString(outputText.toString()),
1529                    x, y, ignoreModifierKey, altersCode, key.isEnabled());
1530            if (code == Constants.CODE_RESEARCH) {
1531                researchLogger.suppressResearchKeyMotionData();
1532            }
1533        }
1534    }
1535
1536    private void suppressResearchKeyMotionData() {
1537        mCurrentLogUnit.removeResearchButtonInvocation();
1538    }
1539
1540    /**
1541     * Log a call to PointerTracker.callListenerCallListenerOnRelease().
1542     *
1543     * UserAction: The user has released their finger or thumb from the screen.
1544     */
1545    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE =
1546            new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code",
1547                    "withSliding", "ignoreModifierKey", "isEnabled");
1548    public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
1549            final boolean withSliding, final boolean ignoreModifierKey) {
1550        if (key != null) {
1551            getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE,
1552                    Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding,
1553                    ignoreModifierKey, key.isEnabled());
1554        }
1555    }
1556
1557    /**
1558     * Log a call to PointerTracker.onDownEvent().
1559     *
1560     * UserAction: The user has pressed down on a key.
1561     * TODO: Differentiate with LatinIME.processMotionEvent.
1562     */
1563    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT =
1564            new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared");
1565    public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
1566        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT, deltaT,
1567                distanceSquared);
1568    }
1569
1570    /**
1571     * Log a call to PointerTracker.onMoveEvent().
1572     *
1573     * UserAction: The user has moved their finger while pressing on the screen.
1574     * TODO: Differentiate with LatinIME.processMotionEvent().
1575     */
1576    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT =
1577            new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY");
1578    public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
1579            final int lastY) {
1580        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY);
1581    }
1582
1583    /**
1584     * Log a call to RichInputConnection.commitCompletion().
1585     *
1586     * SystemResponse: The IME has committed a completion.  A completion is an application-
1587     * specific suggestion that is presented in a pop-up menu in the TextView.
1588     */
1589    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION =
1590            new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo");
1591    public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) {
1592        final ResearchLogger researchLogger = getInstance();
1593        researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION,
1594                completionInfo);
1595    }
1596
1597    /**
1598     * Log a call to RichInputConnection.revertDoubleSpacePeriod().
1599     *
1600     * SystemResponse: The IME has reverted ". ", which had previously replaced two typed spaces.
1601     */
1602    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD =
1603            new LogStatement("RichInputConnectionRevertDoubleSpacePeriod", false, false);
1604    public static void richInputConnection_revertDoubleSpacePeriod() {
1605        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD);
1606    }
1607
1608    /**
1609     * Log a call to RichInputConnection.revertSwapPunctuation().
1610     *
1611     * SystemResponse: The IME has reverted a punctuation swap.
1612     */
1613    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION =
1614            new LogStatement("RichInputConnectionRevertSwapPunctuation", false, false);
1615    public static void richInputConnection_revertSwapPunctuation() {
1616        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION);
1617    }
1618
1619    /**
1620     * Log a call to LatinIME.commitCurrentAutoCorrection().
1621     *
1622     * SystemResponse: The IME has committed an auto-correction.  An auto-correction changes the raw
1623     * text input to another word that the user more likely desired to type.
1624     */
1625    private static final LogStatement LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION =
1626            new LogStatement("LatinIMECommitCurrentAutoCorrection", true, true, "typedWord",
1627                    "autoCorrection", "separatorString");
1628    public static void latinIme_commitCurrentAutoCorrection(final String typedWord,
1629            final String autoCorrection, final String separatorString, final boolean isBatchMode,
1630            final SuggestedWords suggestedWords) {
1631        final String scrubbedTypedWord = scrubDigitsFromString(typedWord);
1632        final String scrubbedAutoCorrection = scrubDigitsFromString(autoCorrection);
1633        final ResearchLogger researchLogger = getInstance();
1634        researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords);
1635        researchLogger.commitCurrentLogUnitAsWord(scrubbedAutoCorrection, Long.MAX_VALUE,
1636                isBatchMode);
1637
1638        // Add the autocorrection logStatement at the end of the logUnit for the committed word.
1639        // We have to do this after calling commitCurrentLogUnitAsWord, because it may split the
1640        // current logUnit, and then we have to peek to get the logUnit reference back.
1641        final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
1642        // TODO: Add test to confirm that the commitCurrentAutoCorrection log statement should
1643        // always be added to logUnit (if non-null) and not mCurrentLogUnit.
1644        researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION,
1645                scrubbedTypedWord, scrubbedAutoCorrection, separatorString);
1646    }
1647
1648    private boolean isExpectingCommitText = false;
1649    /**
1650     * Log a call to (UnknownClass).commitPartialText
1651     *
1652     * SystemResponse: The IME is committing part of a word.  This happens if a space is
1653     * automatically inserted to split a single typed string into two or more words.
1654     */
1655    // TODO: This method is currently unused.  Find where it should be called from in the IME and
1656    // add invocations.
1657    private static final LogStatement LOGSTATEMENT_COMMIT_PARTIAL_TEXT =
1658            new LogStatement("CommitPartialText", true, false, "newCursorPosition");
1659    public static void commitPartialText(final String committedWord,
1660            final long lastTimestampOfWordData, final boolean isBatchMode) {
1661        final ResearchLogger researchLogger = getInstance();
1662        final String scrubbedWord = scrubDigitsFromString(committedWord);
1663        researchLogger.enqueueEvent(LOGSTATEMENT_COMMIT_PARTIAL_TEXT);
1664        researchLogger.mStatistics.recordAutoCorrection(SystemClock.uptimeMillis());
1665        researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData,
1666                isBatchMode);
1667    }
1668
1669    /**
1670     * Log a call to RichInputConnection.commitText().
1671     *
1672     * SystemResponse: The IME is committing text.  This happens after the user has typed a word
1673     * and then a space or punctuation key.
1674     */
1675    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT =
1676            new LogStatement("RichInputConnectionCommitText", true, false, "newCursorPosition");
1677    public static void richInputConnection_commitText(final String committedWord,
1678            final int newCursorPosition, final boolean isBatchMode) {
1679        final ResearchLogger researchLogger = getInstance();
1680        // Only include opening and closing logSegments if private data is included
1681        final String scrubbedWord = scrubDigitsFromString(committedWord);
1682        if (!researchLogger.isExpectingCommitText) {
1683            researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT,
1684                    newCursorPosition);
1685            researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode);
1686        }
1687        researchLogger.isExpectingCommitText = false;
1688    }
1689
1690    /**
1691     * Shared event for logging committed text.
1692     */
1693    private static final LogStatement LOGSTATEMENT_COMMITTEXT =
1694            new LogStatement("CommitText", true, false, "committedText", "isBatchMode");
1695    private void enqueueCommitText(final String word, final boolean isBatchMode) {
1696        enqueueEvent(LOGSTATEMENT_COMMITTEXT, word, isBatchMode);
1697    }
1698
1699    /**
1700     * Log a call to RichInputConnection.deleteSurroundingText().
1701     *
1702     * SystemResponse: The IME has deleted text.
1703     */
1704    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT =
1705            new LogStatement("RichInputConnectionDeleteSurroundingText", true, false,
1706                    "beforeLength", "afterLength");
1707    public static void richInputConnection_deleteSurroundingText(final int beforeLength,
1708            final int afterLength) {
1709        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT,
1710                beforeLength, afterLength);
1711    }
1712
1713    /**
1714     * Log a call to RichInputConnection.finishComposingText().
1715     *
1716     * SystemResponse: The IME has left the composing text as-is.
1717     */
1718    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT =
1719            new LogStatement("RichInputConnectionFinishComposingText", false, false);
1720    public static void richInputConnection_finishComposingText() {
1721        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT);
1722    }
1723
1724    /**
1725     * Log a call to RichInputConnection.performEditorAction().
1726     *
1727     * SystemResponse: The IME is invoking an action specific to the editor.
1728     */
1729    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION =
1730            new LogStatement("RichInputConnectionPerformEditorAction", false, false,
1731                    "imeActionId");
1732    public static void richInputConnection_performEditorAction(final int imeActionId) {
1733        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION,
1734                imeActionId);
1735    }
1736
1737    /**
1738     * Log a call to RichInputConnection.sendKeyEvent().
1739     *
1740     * SystemResponse: The IME is telling the TextView that a key is being pressed through an
1741     * alternate channel.
1742     * TODO: only for hardware keys?
1743     */
1744    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT =
1745            new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action",
1746                    "code");
1747    public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) {
1748        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT,
1749                keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode());
1750    }
1751
1752    /**
1753     * Log a call to RichInputConnection.setComposingText().
1754     *
1755     * SystemResponse: The IME is setting the composing text.  Happens each time a character is
1756     * entered.
1757     */
1758    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT =
1759            new LogStatement("RichInputConnectionSetComposingText", true, true, "text",
1760                    "newCursorPosition");
1761    public static void richInputConnection_setComposingText(final CharSequence text,
1762            final int newCursorPosition) {
1763        if (text == null) {
1764            throw new RuntimeException("setComposingText is null");
1765        }
1766        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, text,
1767                newCursorPosition);
1768    }
1769
1770    /**
1771     * Log a call to RichInputConnection.setSelection().
1772     *
1773     * SystemResponse: The IME is requesting that the selection change.  User-initiated selection-
1774     * change requests do not go through this method -- it's only when the system wants to change
1775     * the selection.
1776     */
1777    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION =
1778            new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to");
1779    public static void richInputConnection_setSelection(final int from, final int to) {
1780        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to);
1781    }
1782
1783    /**
1784     * Log a call to SuddenJumpingTouchEventHandler.onTouchEvent().
1785     *
1786     * SystemResponse: The IME has filtered input events in case of an erroneous sensor reading.
1787     */
1788    private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT =
1789            new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false,
1790                    "motionEvent");
1791    public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
1792        if (me != null) {
1793            getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT,
1794                    me.toString());
1795        }
1796    }
1797
1798    /**
1799     * Log a call to SuggestionsView.setSuggestions().
1800     *
1801     * SystemResponse: The IME is setting the suggestions in the suggestion strip.
1802     */
1803    private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS =
1804            new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords");
1805    public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) {
1806        if (suggestedWords != null) {
1807            getInstance().enqueueEvent(LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS,
1808                    suggestedWords);
1809        }
1810    }
1811
1812    /**
1813     * The user has indicated a particular point in the log that is of interest.
1814     *
1815     * UserAction: From direct menu invocation.
1816     */
1817    private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP =
1818            new LogStatement("UserTimestamp", false, false);
1819    public void userTimestamp() {
1820        getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP);
1821    }
1822
1823    /**
1824     * Log a call to LatinIME.onEndBatchInput().
1825     *
1826     * SystemResponse: The system has completed a gesture.
1827     */
1828    private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT =
1829            new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText",
1830                    "enteredWordPos");
1831    public static void latinIME_onEndBatchInput(final CharSequence enteredText,
1832            final int enteredWordPos, final SuggestedWords suggestedWords) {
1833        final ResearchLogger researchLogger = getInstance();
1834        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText,
1835                enteredWordPos);
1836        researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords);
1837        researchLogger.mStatistics.recordGestureInput(enteredText.length(),
1838                SystemClock.uptimeMillis());
1839    }
1840
1841    /**
1842     * Log a call to LatinIME.handleBackspace() that is not a batch delete.
1843     *
1844     * UserInput: The user is deleting one or more characters by hitting the backspace key once.
1845     * The covers single character deletes as well as deleting selections.
1846     */
1847    private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE =
1848            new LogStatement("LatinIMEHandleBackspace", true, false, "numCharacters");
1849    public static void latinIME_handleBackspace(final int numCharacters) {
1850        final ResearchLogger researchLogger = getInstance();
1851        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE, numCharacters);
1852    }
1853
1854    /**
1855     * Log a call to LatinIME.handleBackspace() that is a batch delete.
1856     *
1857     * UserInput: The user is deleting a gestured word by hitting the backspace key once.
1858     */
1859    private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH =
1860            new LogStatement("LatinIMEHandleBackspaceBatch", true, false, "deletedText",
1861                    "numCharacters");
1862    public static void latinIME_handleBackspace_batch(final CharSequence deletedText,
1863            final int numCharacters) {
1864        final ResearchLogger researchLogger = getInstance();
1865        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText,
1866                numCharacters);
1867        researchLogger.mStatistics.recordGestureDelete(deletedText.length(),
1868                SystemClock.uptimeMillis());
1869    }
1870
1871    /**
1872     * Log a long interval between user operation.
1873     *
1874     * UserInput: The user has not done anything for a while.
1875     */
1876    private static final LogStatement LOGSTATEMENT_ONUSERPAUSE = new LogStatement("OnUserPause",
1877            false, false, "intervalInMs");
1878    public static void onUserPause(final long interval) {
1879        final ResearchLogger researchLogger = getInstance();
1880        researchLogger.enqueueEvent(LOGSTATEMENT_ONUSERPAUSE, interval);
1881    }
1882
1883    /**
1884     * Record the current time in case the LogUnit is later split.
1885     *
1886     * If the current logUnit is split, then tapping, motion events, etc. before this time should
1887     * be assigned to one LogUnit, and events after this time should go into the following LogUnit.
1888     */
1889    public static void recordTimeForLogUnitSplit() {
1890        final ResearchLogger researchLogger = getInstance();
1891        researchLogger.setSavedDownEventTime(SystemClock.uptimeMillis());
1892        researchLogger.mSavedDownEventTime = Long.MAX_VALUE;
1893    }
1894
1895    /**
1896     * Log a call to LatinIME.handleSeparator()
1897     *
1898     * SystemResponse: The system is inserting a separator character, possibly performing auto-
1899     * correction or other actions appropriate at the end of a word.
1900     */
1901    private static final LogStatement LOGSTATEMENT_LATINIME_HANDLESEPARATOR =
1902            new LogStatement("LatinIMEHandleSeparator", false, false, "primaryCode",
1903                    "isComposingWord");
1904    public static void latinIME_handleSeparator(final int primaryCode,
1905            final boolean isComposingWord) {
1906        final ResearchLogger researchLogger = getInstance();
1907        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLESEPARATOR, primaryCode,
1908                isComposingWord);
1909    }
1910
1911    /**
1912     * Log statistics.
1913     *
1914     * ContextualData, recorded at the end of a session.
1915     */
1916    private static final LogStatement LOGSTATEMENT_STATISTICS =
1917            new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount",
1918                    "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting",
1919                    "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete",
1920                    "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete",
1921                    "dictionaryWordCount", "splitWordsCount", "gestureInputCount",
1922                    "gestureCharsCount", "gesturesDeletedCount", "manualSuggestionsCount",
1923                    "revertCommitsCount", "correctedWordsCount", "autoCorrectionsCount");
1924    private static void logStatistics() {
1925        final ResearchLogger researchLogger = getInstance();
1926        final Statistics statistics = researchLogger.mStatistics;
1927        researchLogger.enqueueEvent(LOGSTATEMENT_STATISTICS, statistics.mCharCount,
1928                statistics.mLetterCount, statistics.mNumberCount, statistics.mSpaceCount,
1929                statistics.mDeleteKeyCount, statistics.mWordCount, statistics.mIsEmptyUponStarting,
1930                statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(),
1931                statistics.mBeforeDeleteKeyCounter.getAverageTime(),
1932                statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(),
1933                statistics.mAfterDeleteKeyCounter.getAverageTime(),
1934                statistics.mDictionaryWordCount, statistics.mSplitWordsCount,
1935                statistics.mGesturesInputCount, statistics.mGesturesCharsCount,
1936                statistics.mGesturesDeletedCount, statistics.mManualSuggestionsCount,
1937                statistics.mRevertCommitsCount, statistics.mCorrectedWordsCount,
1938                statistics.mAutoCorrectionsCount);
1939    }
1940}
1941