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