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