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