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