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