| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.inputmethod.research; |
| |
| import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET; |
| |
| import android.accounts.Account; |
| import android.accounts.AccountManager; |
| import android.app.AlertDialog; |
| import android.app.Dialog; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.DialogInterface.OnCancelListener; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.Paint.Style; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.SystemClock; |
| import android.preference.PreferenceManager; |
| import android.text.TextUtils; |
| import android.text.format.DateUtils; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.Window; |
| import android.view.WindowManager; |
| import android.view.inputmethod.CompletionInfo; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.InputConnection; |
| import android.widget.Toast; |
| |
| import com.android.inputmethod.keyboard.Key; |
| import com.android.inputmethod.keyboard.Keyboard; |
| import com.android.inputmethod.keyboard.KeyboardId; |
| import com.android.inputmethod.keyboard.KeyboardSwitcher; |
| import com.android.inputmethod.keyboard.KeyboardView; |
| import com.android.inputmethod.keyboard.MainKeyboardView; |
| import com.android.inputmethod.latin.Constants; |
| import com.android.inputmethod.latin.Dictionary; |
| import com.android.inputmethod.latin.InputTypeUtils; |
| import com.android.inputmethod.latin.LatinIME; |
| import com.android.inputmethod.latin.R; |
| import com.android.inputmethod.latin.RichInputConnection; |
| import com.android.inputmethod.latin.RichInputConnection.Range; |
| import com.android.inputmethod.latin.Suggest; |
| import com.android.inputmethod.latin.SuggestedWords; |
| import com.android.inputmethod.latin.define.ProductionFlag; |
| import com.android.inputmethod.research.MotionEventReader.ReplayData; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.nio.MappedByteBuffer; |
| import java.nio.channels.FileChannel; |
| import java.nio.charset.Charset; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Random; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Logs the use of the LatinIME keyboard. |
| * |
| * This class logs operations on the IME keyboard, including what the user has typed. |
| * Data is stored locally in a file in app-specific storage. |
| * |
| * This functionality is off by default. See |
| * {@link ProductionFlag#USES_DEVELOPMENT_ONLY_DIAGNOSTICS}. |
| */ |
| public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener { |
| // TODO: This class has grown quite large and combines several concerns that should be |
| // separated. The following refactorings will be applied as soon as possible after adding |
| // support for replaying historical events, fixing some replay bugs, adding some ui constraints |
| // on the feedback dialog, and adding the survey dialog. |
| // TODO: Refactor. Move splash screen code into separate class. |
| // TODO: Refactor. Move feedback screen code into separate class. |
| // TODO: Refactor. Move logging invocations into their own class. |
| // TODO: Refactor. Move currentLogUnit management into separate class. |
| private static final String TAG = ResearchLogger.class.getSimpleName(); |
| private static final boolean DEBUG = false |
| && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; |
| private static final boolean DEBUG_REPLAY_AFTER_FEEDBACK = false |
| && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; |
| // Whether the TextView contents are logged at the end of the session. true will disclose |
| // private info. |
| private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false |
| && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; |
| // Whether the feedback dialog preserves the editable text across invocations. Should be false |
| // for normal research builds so users do not have to delete the same feedback string they |
| // entered earlier. Should be true for builds internal to a development team so when the text |
| // field holds a channel name, the developer does not have to re-enter it when using the |
| // feedback mechanism to generate multiple tests. |
| private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false; |
| /* package */ static boolean sIsLogging = false; |
| private static final int OUTPUT_FORMAT_VERSION = 5; |
| private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; |
| // Whether all words should be recorded, leaving unsampled word between bigrams. Useful for |
| // testing. |
| /* package for test */ static final boolean IS_LOGGING_EVERYTHING = false |
| && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; |
| // The number of words between n-grams to omit from the log. |
| private static final int NUMBER_OF_WORDS_BETWEEN_SAMPLES = |
| IS_LOGGING_EVERYTHING ? 0 : (DEBUG ? 2 : 18); |
| |
| // Whether to show an indicator on the screen that logging is on. Currently a very small red |
| // dot in the lower right hand corner. Most users should not notice it. |
| private static final boolean IS_SHOWING_INDICATOR = true; |
| // Change the default indicator to something very visible. Currently two red vertical bars on |
| // either side of they keyboard. |
| private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false || |
| (IS_LOGGING_EVERYTHING && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG); |
| // FEEDBACK_WORD_BUFFER_SIZE should add 1 because it must also hold the feedback LogUnit itself. |
| public static final int FEEDBACK_WORD_BUFFER_SIZE = (Integer.MAX_VALUE - 1) + 1; |
| |
| // constants related to specific log points |
| private static final String WHITESPACE_SEPARATORS = " \t\n\r"; |
| private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 |
| private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel"; |
| |
| private static final long RESEARCHLOG_CLOSE_TIMEOUT_IN_MS = 5 * 1000; |
| private static final long RESEARCHLOG_ABORT_TIMEOUT_IN_MS = 5 * 1000; |
| private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS; |
| private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS; |
| |
| private static final ResearchLogger sInstance = new ResearchLogger(); |
| private static String sAccountType = null; |
| private static String sAllowedAccountDomain = null; |
| /* package */ ResearchLog mMainResearchLog; |
| // mFeedbackLog records all events for the session, private or not (excepting |
| // passwords). It is written to permanent storage only if the user explicitly commands |
| // the system to do so. |
| // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are |
| // complete. |
| /* package */ MainLogBuffer mMainLogBuffer; |
| // TODO: Remove the feedback log. The feedback log continuously captured user data in case the |
| // user wanted to submit it. We now use the mUserRecordingLogBuffer to allow the user to |
| // explicitly reproduce a problem. |
| /* package */ ResearchLog mFeedbackLog; |
| /* package */ LogBuffer mFeedbackLogBuffer; |
| /* package */ ResearchLog mUserRecordingLog; |
| /* package */ LogBuffer mUserRecordingLogBuffer; |
| private File mUserRecordingFile = null; |
| |
| private boolean mIsPasswordView = false; |
| private boolean mIsLoggingSuspended = false; |
| private SharedPreferences mPrefs; |
| |
| // digits entered by the user are replaced with this codepoint. |
| /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT = |
| Character.codePointAt("\uE000", 0); // U+E000 is in the "private-use area" |
| // U+E001 is in the "private-use area" |
| /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001"; |
| protected static final int SUSPEND_DURATION_IN_MINUTES = 1; |
| // set when LatinIME should ignore an onUpdateSelection() callback that |
| // arises from operations in this class |
| private static boolean sLatinIMEExpectingUpdateSelection = false; |
| |
| // used to check whether words are not unique |
| private Suggest mSuggest; |
| private MainKeyboardView mMainKeyboardView; |
| // TODO: Check whether a superclass can be used instead of LatinIME. |
| /* package for test */ LatinIME mLatinIME; |
| private final Statistics mStatistics; |
| private final MotionEventReader mMotionEventReader = new MotionEventReader(); |
| private final Replayer mReplayer = Replayer.getInstance(); |
| private ResearchLogDirectory mResearchLogDirectory; |
| |
| private Intent mUploadIntent; |
| private Intent mUploadNowIntent; |
| |
| /* package for test */ LogUnit mCurrentLogUnit = new LogUnit(); |
| |
| // Gestured or tapped words may be committed after the gesture of the next word has started. |
| // To ensure that the gesture data of the next word is not associated with the previous word, |
| // thereby leaking private data, we store the time of the down event that started the second |
| // gesture, and when committing the earlier word, split the LogUnit. |
| private long mSavedDownEventTime; |
| private Bundle mFeedbackDialogBundle = null; |
| private boolean mInFeedbackDialog = false; |
| // The feedback dialog causes stop() to be called for the keyboard connected to the original |
| // window. This is because the feedback dialog must present its own EditText box that displays |
| // a keyboard. stop() normally causes mFeedbackLogBuffer, which contains the user's data, to be |
| // cleared, and causes mFeedbackLog, which is ready to collect information in case the user |
| // wants to upload, to be closed. This is good because we don't need to log information about |
| // what the user is typing in the feedback dialog, but bad because this data must be uploaded. |
| // Here we save the LogBuffer and Log so the feedback dialog can later access their data. |
| private LogBuffer mSavedFeedbackLogBuffer; |
| private ResearchLog mSavedFeedbackLog; |
| private Handler mUserRecordingTimeoutHandler; |
| private static final long USER_RECORDING_TIMEOUT_MS = 30L * DateUtils.SECOND_IN_MILLIS; |
| |
| private ResearchLogger() { |
| mStatistics = Statistics.getInstance(); |
| } |
| |
| public static ResearchLogger getInstance() { |
| return sInstance; |
| } |
| |
| public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher, |
| final Suggest suggest) { |
| assert latinIME != null; |
| mLatinIME = latinIME; |
| mPrefs = PreferenceManager.getDefaultSharedPreferences(latinIME); |
| mPrefs.registerOnSharedPreferenceChangeListener(this); |
| |
| // Initialize fields from preferences |
| sIsLogging = ResearchSettings.readResearchLoggerEnabledFlag(mPrefs); |
| |
| // Initialize fields from resources |
| final Resources res = latinIME.getResources(); |
| sAccountType = res.getString(R.string.research_account_type); |
| sAllowedAccountDomain = res.getString(R.string.research_allowed_account_domain); |
| |
| // Initialize directory manager |
| mResearchLogDirectory = new ResearchLogDirectory(mLatinIME); |
| cleanLogDirectoryIfNeeded(mResearchLogDirectory, System.currentTimeMillis()); |
| |
| // Initialize external services |
| mUploadIntent = new Intent(mLatinIME, UploaderService.class); |
| mUploadNowIntent = new Intent(mLatinIME, UploaderService.class); |
| mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true); |
| if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { |
| UploaderService.cancelAndRescheduleUploadingService(mLatinIME, |
| true /* needsRescheduling */); |
| } |
| mReplayer.setKeyboardSwitcher(keyboardSwitcher); |
| } |
| |
| private void cleanLogDirectoryIfNeeded(final ResearchLogDirectory researchLogDirectory, |
| final long now) { |
| final long lastCleanupTime = ResearchSettings.readResearchLastDirCleanupTime(mPrefs); |
| if (now - lastCleanupTime < DURATION_BETWEEN_DIR_CLEANUP_IN_MS) return; |
| final long oldestAllowedFileTime = now - MAX_LOGFILE_AGE_IN_MS; |
| mResearchLogDirectory.cleanupLogFilesOlderThan(oldestAllowedFileTime); |
| ResearchSettings.writeResearchLastDirCleanupTime(mPrefs, now); |
| } |
| |
| public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) { |
| mMainKeyboardView = mainKeyboardView; |
| maybeShowSplashScreen(); |
| } |
| |
| public void mainKeyboardView_onDetachedFromWindow() { |
| mMainKeyboardView = null; |
| } |
| |
| public void onDestroy() { |
| if (mPrefs != null) { |
| mPrefs.unregisterOnSharedPreferenceChangeListener(this); |
| } |
| } |
| |
| private Dialog mSplashDialog = null; |
| |
| private void maybeShowSplashScreen() { |
| if (ResearchSettings.readHasSeenSplash(mPrefs)) { |
| return; |
| } |
| if (mSplashDialog != null && mSplashDialog.isShowing()) { |
| return; |
| } |
| final IBinder windowToken = mMainKeyboardView != null |
| ? mMainKeyboardView.getWindowToken() : null; |
| if (windowToken == null) { |
| return; |
| } |
| final AlertDialog.Builder builder = new AlertDialog.Builder(mLatinIME) |
| .setTitle(R.string.research_splash_title) |
| .setMessage(R.string.research_splash_content) |
| .setPositiveButton(android.R.string.yes, |
| new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| onUserLoggingConsent(); |
| mSplashDialog.dismiss(); |
| } |
| }) |
| .setNegativeButton(android.R.string.no, |
| new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| final String packageName = mLatinIME.getPackageName(); |
| final Uri packageUri = Uri.parse("package:" + packageName); |
| final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, |
| packageUri); |
| intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| mLatinIME.startActivity(intent); |
| } |
| }) |
| .setCancelable(true) |
| .setOnCancelListener( |
| new OnCancelListener() { |
| @Override |
| public void onCancel(DialogInterface dialog) { |
| mLatinIME.requestHideSelf(0); |
| } |
| }); |
| mSplashDialog = builder.create(); |
| final Window w = mSplashDialog.getWindow(); |
| final WindowManager.LayoutParams lp = w.getAttributes(); |
| lp.token = windowToken; |
| lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; |
| w.setAttributes(lp); |
| w.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); |
| mSplashDialog.show(); |
| } |
| |
| public void onUserLoggingConsent() { |
| if (mPrefs == null) { |
| mPrefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); |
| if (mPrefs == null) return; |
| } |
| sIsLogging = true; |
| ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, true); |
| ResearchSettings.writeHasSeenSplash(mPrefs, true); |
| restart(); |
| } |
| |
| private void setLoggingAllowed(final boolean enableLogging) { |
| if (mPrefs == null) return; |
| sIsLogging = enableLogging; |
| ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, enableLogging); |
| } |
| |
| private void checkForEmptyEditor() { |
| if (mLatinIME == null) { |
| return; |
| } |
| final InputConnection ic = mLatinIME.getCurrentInputConnection(); |
| if (ic == null) { |
| return; |
| } |
| final CharSequence textBefore = ic.getTextBeforeCursor(1, 0); |
| if (!TextUtils.isEmpty(textBefore)) { |
| mStatistics.setIsEmptyUponStarting(false); |
| return; |
| } |
| final CharSequence textAfter = ic.getTextAfterCursor(1, 0); |
| if (!TextUtils.isEmpty(textAfter)) { |
| mStatistics.setIsEmptyUponStarting(false); |
| return; |
| } |
| if (textBefore != null && textAfter != null) { |
| mStatistics.setIsEmptyUponStarting(true); |
| } |
| } |
| |
| private void start() { |
| if (DEBUG) { |
| Log.d(TAG, "start called"); |
| } |
| maybeShowSplashScreen(); |
| updateSuspendedState(); |
| requestIndicatorRedraw(); |
| mStatistics.reset(); |
| checkForEmptyEditor(); |
| if (mFeedbackLogBuffer == null) { |
| resetFeedbackLogging(); |
| } |
| if (!isAllowedToLog()) { |
| // Log.w(TAG, "not in usability mode; not logging"); |
| return; |
| } |
| if (mMainLogBuffer == null) { |
| mMainResearchLog = new ResearchLog(mResearchLogDirectory.getLogFilePath( |
| System.currentTimeMillis(), System.nanoTime()), mLatinIME); |
| final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1); |
| mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore, |
| mSuggest) { |
| @Override |
| protected void publish(final ArrayList<LogUnit> logUnits, |
| boolean canIncludePrivateData) { |
| canIncludePrivateData |= IS_LOGGING_EVERYTHING; |
| for (final LogUnit logUnit : logUnits) { |
| if (DEBUG) { |
| final String wordsString = logUnit.getWordsAsString(); |
| Log.d(TAG, "onPublish: '" + wordsString |
| + "', hc: " + logUnit.containsCorrection() |
| + ", cipd: " + canIncludePrivateData); |
| } |
| for (final String word : logUnit.getWordsAsStringArray()) { |
| final Dictionary dictionary = getDictionary(); |
| mStatistics.recordWordEntered( |
| dictionary != null && dictionary.isValidWord(word), |
| logUnit.containsCorrection()); |
| } |
| } |
| if (mMainResearchLog != null) { |
| publishLogUnits(logUnits, mMainResearchLog, canIncludePrivateData); |
| } |
| } |
| }; |
| } |
| } |
| |
| private void resetFeedbackLogging() { |
| mFeedbackLog = new ResearchLog(mResearchLogDirectory.getLogFilePath( |
| System.currentTimeMillis(), System.nanoTime()), mLatinIME); |
| mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE); |
| } |
| |
| /* package */ void stop() { |
| if (DEBUG) { |
| Log.d(TAG, "stop called"); |
| } |
| // Commit mCurrentLogUnit before closing. |
| commitCurrentLogUnit(); |
| |
| if (mMainLogBuffer != null) { |
| mMainLogBuffer.shiftAndPublishAll(); |
| logStatistics(); |
| commitCurrentLogUnit(); |
| mMainLogBuffer.setIsStopping(); |
| mMainLogBuffer.shiftAndPublishAll(); |
| mMainResearchLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS); |
| mMainLogBuffer = null; |
| } |
| if (mFeedbackLogBuffer != null) { |
| mFeedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS); |
| mFeedbackLogBuffer = null; |
| } |
| } |
| |
| public void abort() { |
| if (DEBUG) { |
| Log.d(TAG, "abort called"); |
| } |
| if (mMainLogBuffer != null) { |
| mMainLogBuffer.clear(); |
| mMainResearchLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS); |
| mMainLogBuffer = null; |
| } |
| if (mFeedbackLogBuffer != null) { |
| mFeedbackLogBuffer.clear(); |
| mFeedbackLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS); |
| mFeedbackLogBuffer = null; |
| } |
| } |
| |
| private void restart() { |
| stop(); |
| start(); |
| } |
| |
| private long mResumeTime = 0L; |
| private void updateSuspendedState() { |
| final long time = System.currentTimeMillis(); |
| if (time > mResumeTime) { |
| mIsLoggingSuspended = false; |
| } |
| } |
| |
| @Override |
| public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { |
| if (key == null || prefs == null) { |
| return; |
| } |
| sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); |
| if (sIsLogging == false) { |
| abort(); |
| } |
| requestIndicatorRedraw(); |
| mPrefs = prefs; |
| prefsChanged(prefs); |
| } |
| |
| public void onResearchKeySelected(final LatinIME latinIME) { |
| if (mInFeedbackDialog) { |
| Toast.makeText(latinIME, R.string.research_please_exit_feedback_form, |
| Toast.LENGTH_LONG).show(); |
| return; |
| } |
| presentFeedbackDialog(latinIME); |
| } |
| |
| public void presentFeedbackDialog(final LatinIME latinIME) { |
| if (isMakingUserRecording()) { |
| saveRecording(); |
| } |
| mInFeedbackDialog = true; |
| mSavedFeedbackLogBuffer = mFeedbackLogBuffer; |
| mSavedFeedbackLog = mFeedbackLog; |
| // Set the non-saved versions to null so that the stop() caused by switching to the |
| // Feedback dialog will not close them. |
| mFeedbackLogBuffer = null; |
| mFeedbackLog = null; |
| |
| final Intent intent = new Intent(); |
| intent.setClass(mLatinIME, FeedbackActivity.class); |
| if (mFeedbackDialogBundle == null) { |
| // Restore feedback field with channel name |
| final Bundle bundle = new Bundle(); |
| bundle.putBoolean(FeedbackFragment.KEY_INCLUDE_ACCOUNT_NAME, true); |
| bundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, false); |
| if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) { |
| final String savedChannelName = mPrefs.getString(PREF_RESEARCH_SAVED_CHANNEL, ""); |
| bundle.putString(FeedbackFragment.KEY_FEEDBACK_STRING, savedChannelName); |
| } |
| mFeedbackDialogBundle = bundle; |
| } |
| intent.putExtras(mFeedbackDialogBundle); |
| intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| latinIME.startActivity(intent); |
| } |
| |
| public void setFeedbackDialogBundle(final Bundle bundle) { |
| mFeedbackDialogBundle = bundle; |
| } |
| |
| public void startRecording() { |
| final Resources res = mLatinIME.getResources(); |
| Toast.makeText(mLatinIME, |
| res.getString(R.string.research_feedback_demonstration_instructions), |
| Toast.LENGTH_LONG).show(); |
| startRecordingInternal(); |
| } |
| |
| private void startRecordingInternal() { |
| if (mUserRecordingLog != null) { |
| mUserRecordingLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS); |
| } |
| mUserRecordingFile = mResearchLogDirectory.getUserRecordingFilePath( |
| System.currentTimeMillis(), System.nanoTime()); |
| mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME); |
| mUserRecordingLogBuffer = new LogBuffer(); |
| resetRecordingTimer(); |
| } |
| |
| private boolean isMakingUserRecording() { |
| return mUserRecordingLog != null; |
| } |
| |
| private void resetRecordingTimer() { |
| if (mUserRecordingTimeoutHandler == null) { |
| mUserRecordingTimeoutHandler = new Handler(); |
| } |
| clearRecordingTimer(); |
| mUserRecordingTimeoutHandler.postDelayed(mRecordingHandlerTimeoutRunnable, |
| USER_RECORDING_TIMEOUT_MS); |
| } |
| |
| private void clearRecordingTimer() { |
| mUserRecordingTimeoutHandler.removeCallbacks(mRecordingHandlerTimeoutRunnable); |
| } |
| |
| private Runnable mRecordingHandlerTimeoutRunnable = new Runnable() { |
| @Override |
| public void run() { |
| cancelRecording(); |
| requestIndicatorRedraw(); |
| final Resources res = mLatinIME.getResources(); |
| Toast.makeText(mLatinIME, res.getString(R.string.research_feedback_recording_failure), |
| Toast.LENGTH_LONG).show(); |
| } |
| }; |
| |
| private void cancelRecording() { |
| if (mUserRecordingLog != null) { |
| mUserRecordingLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS); |
| } |
| mUserRecordingLog = null; |
| mUserRecordingLogBuffer = null; |
| if (mFeedbackDialogBundle != null) { |
| mFeedbackDialogBundle.putBoolean("HasRecording", false); |
| } |
| } |
| |
| private void saveRecording() { |
| commitCurrentLogUnit(); |
| publishLogBuffer(mUserRecordingLogBuffer, mUserRecordingLog, true); |
| mUserRecordingLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS); |
| mUserRecordingLog = null; |
| mUserRecordingLogBuffer = null; |
| |
| if (mFeedbackDialogBundle != null) { |
| mFeedbackDialogBundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, true); |
| } |
| clearRecordingTimer(); |
| } |
| |
| // TODO: currently unreachable. Remove after being sure enable/disable is |
| // not needed. |
| /* |
| public void enableOrDisable(final boolean showEnable, final LatinIME latinIME) { |
| if (showEnable) { |
| if (!sIsLogging) { |
| setLoggingAllowed(true); |
| } |
| resumeLogging(); |
| Toast.makeText(latinIME, |
| R.string.research_notify_session_logging_enabled, |
| Toast.LENGTH_LONG).show(); |
| } else { |
| Toast toast = Toast.makeText(latinIME, |
| R.string.research_notify_session_log_deleting, |
| Toast.LENGTH_LONG); |
| toast.show(); |
| boolean isLogDeleted = abort(); |
| final long currentTime = System.currentTimeMillis(); |
| final long resumeTime = currentTime + 1000 * 60 * |
| SUSPEND_DURATION_IN_MINUTES; |
| suspendLoggingUntil(resumeTime); |
| toast.cancel(); |
| Toast.makeText(latinIME, R.string.research_notify_logging_suspended, |
| Toast.LENGTH_LONG).show(); |
| } |
| } |
| */ |
| |
| /** |
| * Get the name of the first allowed account on the device. |
| * |
| * Allowed accounts must be in the domain given by ALLOWED_ACCOUNT_DOMAIN. |
| * |
| * @return The user's account name. |
| */ |
| public String getAccountName() { |
| if (sAccountType == null || sAccountType.isEmpty()) { |
| return null; |
| } |
| if (sAllowedAccountDomain == null || sAllowedAccountDomain.isEmpty()) { |
| return null; |
| } |
| final AccountManager manager = AccountManager.get(mLatinIME); |
| // Filter first by account type. |
| final Account[] accounts = manager.getAccountsByType(sAccountType); |
| |
| for (final Account account : accounts) { |
| if (DEBUG) { |
| Log.d(TAG, account.name); |
| } |
| final String[] parts = account.name.split("@"); |
| if (parts.length > 1 && parts[1].equals(sAllowedAccountDomain)) { |
| return parts[0]; |
| } |
| } |
| return null; |
| } |
| |
| private static final LogStatement LOGSTATEMENT_FEEDBACK = |
| new LogStatement("UserFeedback", false, false, "contents", "accountName", "recording"); |
| public void sendFeedback(final String feedbackContents, final boolean includeHistory, |
| final boolean isIncludingAccountName, final boolean isIncludingRecording) { |
| if (mSavedFeedbackLogBuffer == null) { |
| return; |
| } |
| if (!includeHistory) { |
| mSavedFeedbackLogBuffer.clear(); |
| } |
| String recording = ""; |
| if (isIncludingRecording) { |
| // Try to read recording from recently written json file |
| if (mUserRecordingFile != null) { |
| FileChannel channel = null; |
| try { |
| channel = new FileInputStream(mUserRecordingFile).getChannel(); |
| final MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, |
| channel.size()); |
| // Android's openFileOutput() creates the file, so we use Android's default |
| // Charset (UTF-8) here to read it. |
| recording = Charset.defaultCharset().decode(buffer).toString(); |
| } catch (FileNotFoundException e) { |
| Log.e(TAG, "Could not find recording file", e); |
| } catch (IOException e) { |
| Log.e(TAG, "Error reading recording file", e); |
| } finally { |
| if (channel != null) { |
| try { |
| channel.close(); |
| } catch (IOException e) { |
| Log.e(TAG, "Error closing recording file", e); |
| } |
| } |
| } |
| } |
| } |
| final LogUnit feedbackLogUnit = new LogUnit(); |
| final String accountName = isIncludingAccountName ? getAccountName() : ""; |
| feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(), |
| feedbackContents, accountName, recording); |
| mFeedbackLogBuffer.shiftIn(feedbackLogUnit); |
| publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */); |
| mSavedFeedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS); |
| uploadNow(); |
| |
| if (isIncludingRecording && DEBUG_REPLAY_AFTER_FEEDBACK) { |
| final Handler handler = new Handler(); |
| handler.postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| final ReplayData replayData = |
| mMotionEventReader.readMotionEventData(mUserRecordingFile); |
| mReplayer.replay(replayData, null); |
| } |
| }, 1000); |
| } |
| |
| if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) { |
| // Use feedback string as a channel name to label feedback strings. Here we record the |
| // string for prepopulating the field next time. |
| final String channelName = feedbackContents; |
| if (mPrefs == null) { |
| return; |
| } |
| mPrefs.edit().putString(PREF_RESEARCH_SAVED_CHANNEL, channelName).apply(); |
| } |
| } |
| |
| public void uploadNow() { |
| if (DEBUG) { |
| Log.d(TAG, "calling uploadNow()"); |
| } |
| mLatinIME.startService(mUploadNowIntent); |
| } |
| |
| public void onLeavingSendFeedbackDialog() { |
| mInFeedbackDialog = false; |
| } |
| |
| public void initSuggest(final Suggest suggest) { |
| mSuggest = suggest; |
| // MainLogBuffer has out-of-date Suggest object. Need to close it down and create a new |
| // one. |
| if (mMainLogBuffer != null) { |
| stop(); |
| start(); |
| } |
| } |
| |
| private Dictionary getDictionary() { |
| if (mSuggest == null) { |
| return null; |
| } |
| return mSuggest.getMainDictionary(); |
| } |
| |
| private void setIsPasswordView(boolean isPasswordView) { |
| mIsPasswordView = isPasswordView; |
| } |
| |
| private boolean isAllowedToLog() { |
| return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog; |
| } |
| |
| public void requestIndicatorRedraw() { |
| if (!IS_SHOWING_INDICATOR) { |
| return; |
| } |
| if (mMainKeyboardView == null) { |
| return; |
| } |
| mMainKeyboardView.invalidateAllKeys(); |
| } |
| |
| private boolean isReplaying() { |
| return mReplayer.isReplaying(); |
| } |
| |
| private int getIndicatorColor() { |
| if (isMakingUserRecording()) { |
| return Color.YELLOW; |
| } |
| if (isReplaying()) { |
| return Color.GREEN; |
| } |
| return Color.RED; |
| } |
| |
| public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width, |
| int height) { |
| // TODO: Reimplement using a keyboard background image specific to the ResearchLogger |
| // and remove this method. |
| // The check for MainKeyboardView ensures that the indicator only decorates the main |
| // keyboard, not every keyboard. |
| if (IS_SHOWING_INDICATOR && (isAllowedToLog() || isReplaying()) |
| && view instanceof MainKeyboardView) { |
| final int savedColor = paint.getColor(); |
| paint.setColor(getIndicatorColor()); |
| final Style savedStyle = paint.getStyle(); |
| paint.setStyle(Style.STROKE); |
| final float savedStrokeWidth = paint.getStrokeWidth(); |
| if (IS_SHOWING_INDICATOR_CLEARLY) { |
| paint.setStrokeWidth(5); |
| canvas.drawLine(0, 0, 0, height, paint); |
| canvas.drawLine(width, 0, width, height, paint); |
| } else { |
| // Put a tiny dot on the screen so a knowledgeable user can check whether it is |
| // enabled. The dot is actually a zero-width, zero-height rectangle, placed at the |
| // lower-right corner of the canvas, painted with a non-zero border width. |
| paint.setStrokeWidth(3); |
| canvas.drawRect(width - 1, height - 1, width, height, paint); |
| } |
| paint.setColor(savedColor); |
| paint.setStyle(savedStyle); |
| paint.setStrokeWidth(savedStrokeWidth); |
| } |
| } |
| |
| /** |
| * Buffer a research log event, flagging it as privacy-sensitive. |
| */ |
| private synchronized void enqueueEvent(final LogStatement logStatement, |
| final Object... values) { |
| enqueueEvent(mCurrentLogUnit, logStatement, values); |
| } |
| |
| private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement, |
| final Object... values) { |
| assert values.length == logStatement.getKeys().length; |
| if (isAllowedToLog() && logUnit != null) { |
| final long time = SystemClock.uptimeMillis(); |
| logUnit.addLogStatement(logStatement, time, values); |
| } |
| } |
| |
| private void setCurrentLogUnitContainsDigitFlag() { |
| mCurrentLogUnit.setMayContainDigit(); |
| } |
| |
| private void setCurrentLogUnitContainsCorrection() { |
| mCurrentLogUnit.setContainsCorrection(); |
| } |
| |
| private void setCurrentLogUnitCorrectionType(final int correctionType) { |
| mCurrentLogUnit.setCorrectionType(correctionType); |
| } |
| |
| /* package for test */ void commitCurrentLogUnit() { |
| if (DEBUG) { |
| Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasOneOrMoreWords() ? |
| ": " + mCurrentLogUnit.getWordsAsString() : "")); |
| } |
| if (!mCurrentLogUnit.isEmpty()) { |
| if (mMainLogBuffer != null) { |
| mMainLogBuffer.shiftIn(mCurrentLogUnit); |
| } |
| if (mFeedbackLogBuffer != null) { |
| mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); |
| } |
| if (mUserRecordingLogBuffer != null) { |
| mUserRecordingLogBuffer.shiftIn(mCurrentLogUnit); |
| } |
| mCurrentLogUnit = new LogUnit(); |
| } else { |
| if (DEBUG) { |
| Log.d(TAG, "Warning: tried to commit empty log unit."); |
| } |
| } |
| } |
| |
| private static final LogStatement LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT = |
| new LogStatement("UncommitCurrentLogUnit", false, false); |
| public void uncommitCurrentLogUnit(final String expectedWord, |
| final boolean dumpCurrentLogUnit) { |
| // The user has deleted this word and returned to the previous. Check that the word in the |
| // logUnit matches the expected word. If so, restore the last log unit committed to be the |
| // current logUnit. I.e., pull out the last LogUnit from all the LogBuffers, and make |
| // restore it to mCurrentLogUnit so the new edits are captured with the word. Optionally |
| // dump the contents of mCurrentLogUnit (useful if they contain deletions of the next word |
| // that should not be reported to protect user privacy) |
| // |
| // Note that we don't use mLastLogUnit here, because it only goes one word back and is only |
| // needed for reverts, which only happen one back. |
| if (mMainLogBuffer == null) { |
| return; |
| } |
| final LogUnit oldLogUnit = mMainLogBuffer.peekLastLogUnit(); |
| |
| // Check that expected word matches. |
| if (oldLogUnit != null) { |
| final String oldLogUnitWords = oldLogUnit.getWordsAsString(); |
| if (oldLogUnitWords != null && !oldLogUnitWords.equals(expectedWord)) { |
| return; |
| } |
| } |
| |
| // Uncommit, merging if necessary. |
| mMainLogBuffer.unshiftIn(); |
| if (oldLogUnit != null && !dumpCurrentLogUnit) { |
| oldLogUnit.append(mCurrentLogUnit); |
| mSavedDownEventTime = Long.MAX_VALUE; |
| } |
| if (oldLogUnit == null) { |
| mCurrentLogUnit = new LogUnit(); |
| } else { |
| mCurrentLogUnit = oldLogUnit; |
| } |
| if (mFeedbackLogBuffer != null) { |
| mFeedbackLogBuffer.unshiftIn(); |
| } |
| enqueueEvent(LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT); |
| if (DEBUG) { |
| Log.d(TAG, "uncommitCurrentLogUnit (dump=" + dumpCurrentLogUnit + ") back to " |
| + (mCurrentLogUnit.hasOneOrMoreWords() ? ": '" |
| + mCurrentLogUnit.getWordsAsString() + "'" : "")); |
| } |
| } |
| |
| /** |
| * Publish all the logUnits in the logBuffer, without doing any privacy filtering. |
| */ |
| /* package for test */ void publishLogBuffer(final LogBuffer logBuffer, |
| final ResearchLog researchLog, final boolean canIncludePrivateData) { |
| publishLogUnits(logBuffer.getLogUnits(), researchLog, canIncludePrivateData); |
| } |
| |
| private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_OPENING = |
| new LogStatement("logSegmentStart", false, false, "isIncludingPrivateData"); |
| private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_CLOSING = |
| new LogStatement("logSegmentEnd", false, false); |
| /** |
| * Publish all LogUnits in a list. |
| * |
| * Any privacy checks should be performed before calling this method. |
| */ |
| /* package for test */ void publishLogUnits(final List<LogUnit> logUnits, |
| final ResearchLog researchLog, final boolean canIncludePrivateData) { |
| final LogUnit openingLogUnit = new LogUnit(); |
| if (logUnits.isEmpty()) return; |
| // LogUnits not containing private data, such as contextual data for the log, do not require |
| // logSegment boundary statements. |
| if (canIncludePrivateData) { |
| openingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_OPENING, |
| SystemClock.uptimeMillis(), canIncludePrivateData); |
| researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */); |
| } |
| for (LogUnit logUnit : logUnits) { |
| if (DEBUG) { |
| Log.d(TAG, "publishLogBuffer: " + (logUnit.hasOneOrMoreWords() |
| ? logUnit.getWordsAsString() : "<wordless>") |
| + ", correction?: " + logUnit.containsCorrection()); |
| } |
| researchLog.publish(logUnit, canIncludePrivateData); |
| } |
| if (canIncludePrivateData) { |
| final LogUnit closingLogUnit = new LogUnit(); |
| closingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_CLOSING, |
| SystemClock.uptimeMillis()); |
| researchLog.publish(closingLogUnit, true /* isIncludingPrivateData */); |
| } |
| } |
| |
| public static boolean hasLetters(final String word) { |
| final int length = word.length(); |
| for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { |
| final int codePoint = word.codePointAt(i); |
| if (Character.isLetter(codePoint)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Commit the portion of mCurrentLogUnit before maxTime as a worded logUnit. |
| * |
| * After this operation completes, mCurrentLogUnit will hold any logStatements that happened |
| * after maxTime. |
| */ |
| /* package for test */ void commitCurrentLogUnitAsWord(final String word, final long maxTime, |
| final boolean isBatchMode) { |
| if (word == null) { |
| return; |
| } |
| if (word.length() > 0 && hasLetters(word)) { |
| mCurrentLogUnit.setWords(word); |
| } |
| final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime); |
| enqueueCommitText(word, isBatchMode); |
| commitCurrentLogUnit(); |
| mCurrentLogUnit = newLogUnit; |
| } |
| |
| /** |
| * Record the time of a MotionEvent.ACTION_DOWN. |
| * |
| * Warning: Not thread safe. Only call from the main thread. |
| */ |
| private void setSavedDownEventTime(final long time) { |
| mSavedDownEventTime = time; |
| } |
| |
| public void onWordFinished(final String word, final boolean isBatchMode) { |
| commitCurrentLogUnitAsWord(word, mSavedDownEventTime, isBatchMode); |
| mSavedDownEventTime = Long.MAX_VALUE; |
| } |
| |
| private static int scrubDigitFromCodePoint(int codePoint) { |
| return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint; |
| } |
| |
| /* package for test */ static String scrubDigitsFromString(String s) { |
| StringBuilder sb = null; |
| final int length = s.length(); |
| for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) { |
| final int codePoint = Character.codePointAt(s, i); |
| if (Character.isDigit(codePoint)) { |
| if (sb == null) { |
| sb = new StringBuilder(length); |
| sb.append(s.substring(0, i)); |
| } |
| sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT); |
| } else { |
| if (sb != null) { |
| sb.appendCodePoint(codePoint); |
| } |
| } |
| } |
| if (sb == null) { |
| return s; |
| } else { |
| return sb.toString(); |
| } |
| } |
| |
| private String scrubWord(String word) { |
| final Dictionary dictionary = getDictionary(); |
| if (dictionary == null) { |
| return WORD_REPLACEMENT_STRING; |
| } |
| if (dictionary.isValidWord(word)) { |
| return word; |
| } |
| return WORD_REPLACEMENT_STRING; |
| } |
| |
| // Specific logging methods follow below. The comments for each logging method should |
| // indicate what specific method is logged, and how to trigger it from the user interface. |
| // |
| // Logging methods can be generally classified into two flavors, "UserAction", which should |
| // correspond closely to an event that is sensed by the IME, and is usually generated |
| // directly by the user, and "SystemResponse" which corresponds to an event that the IME |
| // generates, often after much processing of user input. SystemResponses should correspond |
| // closely to user-visible events. |
| // TODO: Consider exposing the UserAction classification in the log output. |
| |
| /** |
| * Log a call to LatinIME.onStartInputViewInternal(). |
| * |
| * UserAction: called each time the keyboard is opened up. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL = |
| new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid", |
| "packageName", "inputType", "imeOptions", "fieldId", "display", "model", |
| "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything", |
| "isDevTeamBuild"); |
| public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, |
| final SharedPreferences prefs) { |
| final ResearchLogger researchLogger = getInstance(); |
| if (editorInfo != null) { |
| final boolean isPassword = InputTypeUtils.isPasswordInputType(editorInfo.inputType) |
| || InputTypeUtils.isVisiblePasswordInputType(editorInfo.inputType); |
| getInstance().setIsPasswordView(isPassword); |
| researchLogger.start(); |
| final Context context = researchLogger.mLatinIME; |
| try { |
| final PackageInfo packageInfo; |
| packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), |
| 0); |
| final Integer versionCode = packageInfo.versionCode; |
| final String versionName = packageInfo.versionName; |
| final String uuid = ResearchSettings.readResearchLoggerUuid(researchLogger.mPrefs); |
| researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL, |
| uuid, editorInfo.packageName, Integer.toHexString(editorInfo.inputType), |
| Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, |
| Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, |
| OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING, |
| researchLogger.isDevTeamBuild()); |
| // Commit the logUnit so the LatinImeOnStartInputViewInternal event is in its own |
| // logUnit at the beginning of the log. |
| researchLogger.commitCurrentLogUnit(); |
| } catch (final NameNotFoundException e) { |
| Log.e(TAG, "NameNotFound", e); |
| } |
| } |
| } |
| |
| // TODO: Update this heuristic pattern to something more reliable. Developer builds tend to |
| // have the developer name and year embedded. |
| private static final Pattern developerBuildRegex = Pattern.compile("[A-Za-z]\\.20[1-9]"); |
| private boolean isDevTeamBuild() { |
| try { |
| final PackageInfo packageInfo; |
| packageInfo = mLatinIME.getPackageManager().getPackageInfo(mLatinIME.getPackageName(), |
| 0); |
| final String versionName = packageInfo.versionName; |
| return developerBuildRegex.matcher(versionName).find(); |
| } catch (final NameNotFoundException e) { |
| Log.e(TAG, "Could not determine package name", e); |
| return false; |
| } |
| } |
| |
| /** |
| * Log a change in preferences. |
| * |
| * UserAction: called when the user changes the settings. |
| */ |
| private static final LogStatement LOGSTATEMENT_PREFS_CHANGED = |
| new LogStatement("PrefsChanged", false, false, "prefs"); |
| public static void prefsChanged(final SharedPreferences prefs) { |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.enqueueEvent(LOGSTATEMENT_PREFS_CHANGED, prefs); |
| } |
| |
| /** |
| * Log a call to MainKeyboardView.processMotionEvent(). |
| * |
| * UserAction: called when the user puts their finger onto the screen (ACTION_DOWN). |
| * |
| */ |
| private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT = |
| new LogStatement("MotionEvent", true, false, "action", |
| LogStatement.KEY_IS_LOGGING_RELATED, "motionEvent"); |
| public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action, |
| final long eventTime, final int index, final int id, final int x, final int y) { |
| if (me != null) { |
| final String actionString = LoggingUtils.getMotionEventActionTypeString(action); |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT, |
| actionString, false /* IS_LOGGING_RELATED */, MotionEvent.obtain(me)); |
| if (action == MotionEvent.ACTION_DOWN) { |
| // Subtract 1 from eventTime so the down event is included in the later |
| // LogUnit, not the earlier (the test is for inequality). |
| researchLogger.setSavedDownEventTime(eventTime - 1); |
| } |
| // Refresh the timer in case we are capturing user feedback. |
| if (researchLogger.isMakingUserRecording()) { |
| researchLogger.resetRecordingTimer(); |
| } |
| } |
| } |
| |
| /** |
| * Log a call to LatinIME.onCodeInput(). |
| * |
| * SystemResponse: The main processing step for entering text. Called when the user performs a |
| * tap, a flick, a long press, releases a gesture, or taps a punctuation suggestion. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT = |
| new LogStatement("LatinImeOnCodeInput", true, false, "code", "x", "y"); |
| public static void latinIME_onCodeInput(final int code, final int x, final int y) { |
| final long time = SystemClock.uptimeMillis(); |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT, |
| Constants.printableCode(scrubDigitFromCodePoint(code)), x, y); |
| if (Character.isDigit(code)) { |
| researchLogger.setCurrentLogUnitContainsDigitFlag(); |
| } |
| researchLogger.mStatistics.recordChar(code, time); |
| } |
| /** |
| * Log a call to LatinIME.onDisplayCompletions(). |
| * |
| * SystemResponse: The IME has displayed application-specific completions. They may show up |
| * in the suggestion strip, such as a landscape phone. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS = |
| new LogStatement("LatinIMEOnDisplayCompletions", true, true, |
| "applicationSpecifiedCompletions"); |
| public static void latinIME_onDisplayCompletions( |
| final CompletionInfo[] applicationSpecifiedCompletions) { |
| // Note; passing an array as a single element in a vararg list. Must create a new |
| // dummy array around it or it will get expanded. |
| getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS, |
| new Object[] { applicationSpecifiedCompletions }); |
| } |
| |
| public static boolean getAndClearLatinIMEExpectingUpdateSelection() { |
| boolean returnValue = sLatinIMEExpectingUpdateSelection; |
| sLatinIMEExpectingUpdateSelection = false; |
| return returnValue; |
| } |
| |
| /** |
| * The IME is finishing; it is either being destroyed, or is about to be hidden. |
| * |
| * UserAction: The user has performed an action that has caused the IME to be closed. They may |
| * have focused on something other than a text field, or explicitly closed it. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_ONFINISHINPUTVIEWINTERNAL = |
| new LogStatement("LatinIMEOnFinishInputViewInternal", false, false, "isTextTruncated", |
| "text"); |
| public static void latinIME_onFinishInputViewInternal(final boolean finishingInput, |
| final int savedSelectionStart, final int savedSelectionEnd, final InputConnection ic) { |
| // The finishingInput flag is set in InputMethodService. It is true if called from |
| // doFinishInput(), which can be called as part of doStartInput(). This can happen at times |
| // when the IME is not closing, such as when powering up. The finishinInput flag is false |
| // if called from finishViews(), which is called from hideWindow() and onDestroy(). These |
| // are the situations in which we want to finish up the researchLog. |
| if (ic != null && !finishingInput) { |
| final boolean isTextTruncated; |
| final String text; |
| if (LOG_FULL_TEXTVIEW_CONTENTS) { |
| // Capture the TextView contents. This will trigger onUpdateSelection(), so we |
| // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called, |
| // it can tell that it was generated by the logging code, and not by the user, and |
| // therefore keep user-visible state as is. |
| ic.beginBatchEdit(); |
| ic.performContextMenuAction(android.R.id.selectAll); |
| CharSequence charSequence = ic.getSelectedText(0); |
| if (savedSelectionStart != -1 && savedSelectionEnd != -1) { |
| ic.setSelection(savedSelectionStart, savedSelectionEnd); |
| } |
| ic.endBatchEdit(); |
| sLatinIMEExpectingUpdateSelection = true; |
| if (TextUtils.isEmpty(charSequence)) { |
| isTextTruncated = false; |
| text = ""; |
| } else { |
| if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) { |
| int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE; |
| // do not cut in the middle of a supplementary character |
| final char c = charSequence.charAt(length - 1); |
| if (Character.isHighSurrogate(c)) { |
| length--; |
| } |
| final CharSequence truncatedCharSequence = charSequence.subSequence(0, |
| length); |
| isTextTruncated = true; |
| text = truncatedCharSequence.toString(); |
| } else { |
| isTextTruncated = false; |
| text = charSequence.toString(); |
| } |
| } |
| } else { |
| isTextTruncated = true; |
| text = ""; |
| } |
| final ResearchLogger researchLogger = getInstance(); |
| // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g. |
| // during a live user test), so the normal isPotentiallyPrivate and |
| // isPotentiallyRevealing flags do not apply |
| researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONFINISHINPUTVIEWINTERNAL, |
| isTextTruncated, text); |
| researchLogger.commitCurrentLogUnit(); |
| getInstance().stop(); |
| } |
| } |
| |
| /** |
| * Log a call to LatinIME.onUpdateSelection(). |
| * |
| * UserAction/SystemResponse: The user has moved the cursor or selection. This function may |
| * be called, however, when the system has moved the cursor, say by inserting a character. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_ONUPDATESELECTION = |
| new LogStatement("LatinIMEOnUpdateSelection", true, false, "lastSelectionStart", |
| "lastSelectionEnd", "oldSelStart", "oldSelEnd", "newSelStart", "newSelEnd", |
| "composingSpanStart", "composingSpanEnd", "expectingUpdateSelection", |
| "expectingUpdateSelectionFromLogger", "context"); |
| public static void latinIME_onUpdateSelection(final int lastSelectionStart, |
| final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd, |
| final int newSelStart, final int newSelEnd, final int composingSpanStart, |
| final int composingSpanEnd, final boolean expectingUpdateSelection, |
| final boolean expectingUpdateSelectionFromLogger, |
| final RichInputConnection connection) { |
| String word = ""; |
| if (connection != null) { |
| Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1); |
| if (range != null) { |
| word = range.mWord.toString(); |
| } |
| } |
| final ResearchLogger researchLogger = getInstance(); |
| final String scrubbedWord = researchLogger.scrubWord(word); |
| researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONUPDATESELECTION, lastSelectionStart, |
| lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd, |
| composingSpanStart, composingSpanEnd, expectingUpdateSelection, |
| expectingUpdateSelectionFromLogger, scrubbedWord); |
| } |
| |
| /** |
| * Log a call to LatinIME.onTextInput(). |
| * |
| * SystemResponse: Raw text is added to the TextView. |
| */ |
| public static void latinIME_onTextInput(final String text, final boolean isBatchMode) { |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode); |
| } |
| |
| /** |
| * Log a call to LatinIME.pickSuggestionManually(). |
| * |
| * UserAction: The user has chosen a specific word from the suggestion strip. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY = |
| new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index", |
| "suggestion", "x", "y", "isBatchMode"); |
| public static void latinIME_pickSuggestionManually(final String replacedWord, |
| final int index, final String suggestion, final boolean isBatchMode) { |
| final ResearchLogger researchLogger = getInstance(); |
| if (!replacedWord.equals(suggestion.toString())) { |
| // The user chose something other than what was already there. |
| researchLogger.setCurrentLogUnitContainsCorrection(); |
| researchLogger.setCurrentLogUnitCorrectionType(LogUnit.CORRECTIONTYPE_TYPO); |
| } |
| final String scrubbedWord = scrubDigitsFromString(suggestion); |
| researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY, |
| scrubDigitsFromString(replacedWord), index, |
| suggestion == null ? null : scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE, |
| Constants.SUGGESTION_STRIP_COORDINATE, isBatchMode); |
| researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode); |
| researchLogger.mStatistics.recordManualSuggestion(SystemClock.uptimeMillis()); |
| } |
| |
| /** |
| * Log a call to LatinIME.punctuationSuggestion(). |
| * |
| * UserAction: The user has chosen punctuation from the punctuation suggestion strip. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION = |
| new LogStatement("LatinIMEPunctuationSuggestion", false, false, "index", "suggestion", |
| "x", "y", "isPrediction"); |
| public static void latinIME_punctuationSuggestion(final int index, final String suggestion, |
| final boolean isBatchMode, final boolean isPrediction) { |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion, |
| Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, |
| isPrediction); |
| researchLogger.commitCurrentLogUnitAsWord(suggestion, Long.MAX_VALUE, isBatchMode); |
| } |
| |
| /** |
| * Log a call to LatinIME.sendKeyCodePoint(). |
| * |
| * SystemResponse: The IME is inserting text into the TextView for numbers, fixed strings, or |
| * some other unusual mechanism. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT = |
| new LogStatement("LatinIMESendKeyCodePoint", true, false, "code"); |
| public static void latinIME_sendKeyCodePoint(final int code) { |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT, |
| Constants.printableCode(scrubDigitFromCodePoint(code))); |
| if (Character.isDigit(code)) { |
| researchLogger.setCurrentLogUnitContainsDigitFlag(); |
| } |
| } |
| |
| /** |
| * Log a call to LatinIME.promotePhantomSpace(). |
| * |
| * SystemResponse: The IME is inserting a real space in place of a phantom space. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE = |
| new LogStatement("LatinIMEPromotPhantomSpace", false, false); |
| public static void latinIME_promotePhantomSpace() { |
| final ResearchLogger researchLogger = getInstance(); |
| final LogUnit logUnit; |
| if (researchLogger.mMainLogBuffer == null) { |
| logUnit = researchLogger.mCurrentLogUnit; |
| } else { |
| logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); |
| } |
| researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE); |
| } |
| |
| /** |
| * Log a call to LatinIME.swapSwapperAndSpace(). |
| * |
| * SystemResponse: A symbol has been swapped with a space character. E.g. punctuation may swap |
| * if a soft space is inserted after a word. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE = |
| new LogStatement("LatinIMESwapSwapperAndSpace", false, false, "originalCharacters", |
| "charactersAfterSwap"); |
| public static void latinIME_swapSwapperAndSpace(final CharSequence originalCharacters, |
| final String charactersAfterSwap) { |
| final ResearchLogger researchLogger = getInstance(); |
| final LogUnit logUnit; |
| if (researchLogger.mMainLogBuffer == null) { |
| logUnit = null; |
| } else { |
| logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); |
| } |
| if (logUnit != null) { |
| researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE, |
| originalCharacters, charactersAfterSwap); |
| } |
| } |
| |
| /** |
| * Log a call to LatinIME.maybeDoubleSpacePeriod(). |
| * |
| * SystemResponse: Two spaces have been replaced by period space. |
| */ |
| public static void latinIME_maybeDoubleSpacePeriod(final String text, |
| final boolean isBatchMode) { |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.commitCurrentLogUnitAsWord(text, Long.MAX_VALUE, isBatchMode); |
| } |
| |
| /** |
| * Log a call to MainKeyboardView.onLongPress(). |
| * |
| * UserAction: The user has performed a long-press on a key. |
| */ |
| private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS = |
| new LogStatement("MainKeyboardViewOnLongPress", false, false); |
| public static void mainKeyboardView_onLongPress() { |
| getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS); |
| } |
| |
| /** |
| * Log a call to MainKeyboardView.setKeyboard(). |
| * |
| * SystemResponse: The IME has switched to a new keyboard (e.g. French, English). |
| * This is typically called right after LatinIME.onStartInputViewInternal (when starting a new |
| * IME), but may happen at other times if the user explicitly requests a keyboard change. |
| */ |
| private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD = |
| new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale", |
| "orientation", "width", "modeName", "action", "navigateNext", |
| "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled", |
| "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th", |
| "keys"); |
| public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) { |
| final KeyboardId kid = keyboard.mId; |
| final boolean isPasswordView = kid.passwordInput(); |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.setIsPasswordView(isPasswordView); |
| researchLogger.enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD, |
| KeyboardId.elementIdToName(kid.mElementId), |
| kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), |
| kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(), |
| kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey, |
| isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey, |
| kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth, |
| keyboard.mOccupiedHeight, keyboard.mKeys); |
| } |
| |
| /** |
| * Log a call to LatinIME.revertCommit(). |
| * |
| * SystemResponse: The IME has reverted commited text. This happens when the user enters |
| * a word, commits it by pressing space or punctuation, and then reverts the commit by hitting |
| * backspace. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT = |
| new LogStatement("LatinIMERevertCommit", true, false, "committedWord", |
| "originallyTypedWord", "separatorString"); |
| public static void latinIME_revertCommit(final String committedWord, |
| final String originallyTypedWord, final boolean isBatchMode, |
| final String separatorString) { |
| final ResearchLogger researchLogger = getInstance(); |
| // TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word. |
| final LogUnit logUnit; |
| if (researchLogger.mMainLogBuffer == null) { |
| logUnit = null; |
| } else { |
| logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); |
| } |
| if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) { |
| if (logUnit != null) { |
| logUnit.setWords(originallyTypedWord); |
| } |
| } |
| researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit, |
| LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, originallyTypedWord, |
| separatorString); |
| if (logUnit != null) { |
| logUnit.setContainsCorrection(); |
| } |
| researchLogger.mStatistics.recordRevertCommit(SystemClock.uptimeMillis()); |
| researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE, isBatchMode); |
| } |
| |
| /** |
| * Log a call to PointerTracker.callListenerOnCancelInput(). |
| * |
| * UserAction: The user has canceled the input, e.g., by pressing down, but then removing |
| * outside the keyboard area. |
| * TODO: Verify |
| */ |
| private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT = |
| new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false); |
| public static void pointerTracker_callListenerOnCancelInput() { |
| getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT); |
| } |
| |
| /** |
| * Log a call to PointerTracker.callListenerOnCodeInput(). |
| * |
| * SystemResponse: The user has entered a key through the normal tapping mechanism. |
| * LatinIME.onCodeInput will also be called. |
| */ |
| private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT = |
| new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "code", |
| "outputText", "x", "y", "ignoreModifierKey", "altersCode", "isEnabled"); |
| public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x, |
| final int y, final boolean ignoreModifierKey, final boolean altersCode, |
| final int code) { |
| if (key != null) { |
| String outputText = key.getOutputText(); |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT, |
| Constants.printableCode(scrubDigitFromCodePoint(code)), |
| outputText == null ? null : scrubDigitsFromString(outputText.toString()), |
| x, y, ignoreModifierKey, altersCode, key.isEnabled()); |
| if (code == Constants.CODE_RESEARCH) { |
| researchLogger.suppressResearchKeyMotionData(); |
| } |
| } |
| } |
| |
| private void suppressResearchKeyMotionData() { |
| mCurrentLogUnit.removeResearchButtonInvocation(); |
| } |
| |
| /** |
| * Log a call to PointerTracker.callListenerCallListenerOnRelease(). |
| * |
| * UserAction: The user has released their finger or thumb from the screen. |
| */ |
| private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE = |
| new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code", |
| "withSliding", "ignoreModifierKey", "isEnabled"); |
| public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode, |
| final boolean withSliding, final boolean ignoreModifierKey) { |
| if (key != null) { |
| getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE, |
| Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding, |
| ignoreModifierKey, key.isEnabled()); |
| } |
| } |
| |
| /** |
| * Log a call to PointerTracker.onDownEvent(). |
| * |
| * UserAction: The user has pressed down on a key. |
| * TODO: Differentiate with LatinIME.processMotionEvent. |
| */ |
| private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT = |
| new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared"); |
| public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) { |
| getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT, deltaT, |
| distanceSquared); |
| } |
| |
| /** |
| * Log a call to PointerTracker.onMoveEvent(). |
| * |
| * UserAction: The user has moved their finger while pressing on the screen. |
| * TODO: Differentiate with LatinIME.processMotionEvent(). |
| */ |
| private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT = |
| new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY"); |
| public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX, |
| final int lastY) { |
| getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY); |
| } |
| |
| /** |
| * Log a call to RichInputConnection.commitCompletion(). |
| * |
| * SystemResponse: The IME has committed a completion. A completion is an application- |
| * specific suggestion that is presented in a pop-up menu in the TextView. |
| */ |
| private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION = |
| new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo"); |
| public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) { |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION, |
| completionInfo); |
| } |
| |
| /** |
| * Log a call to RichInputConnection.revertDoubleSpacePeriod(). |
| * |
| * SystemResponse: The IME has reverted ". ", which had previously replaced two typed spaces. |
| */ |
| private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD = |
| new LogStatement("RichInputConnectionRevertDoubleSpacePeriod", false, false); |
| public static void richInputConnection_revertDoubleSpacePeriod() { |
| getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTDOUBLESPACEPERIOD); |
| } |
| |
| /** |
| * Log a call to RichInputConnection.revertSwapPunctuation(). |
| * |
| * SystemResponse: The IME has reverted a punctuation swap. |
| */ |
| private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION = |
| new LogStatement("RichInputConnectionRevertSwapPunctuation", false, false); |
| public static void richInputConnection_revertSwapPunctuation() { |
| getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_REVERTSWAPPUNCTUATION); |
| } |
| |
| /** |
| * Log a call to LatinIME.commitCurrentAutoCorrection(). |
| * |
| * SystemResponse: The IME has committed an auto-correction. An auto-correction changes the raw |
| * text input to another word (or words) that the user more likely desired to type. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION = |
| new LogStatement("LatinIMECommitCurrentAutoCorrection", true, true, "typedWord", |
| "autoCorrection", "separatorString"); |
| public static void latinIme_commitCurrentAutoCorrection(final String typedWord, |
| final String autoCorrection, final String separatorString, final boolean isBatchMode, |
| final SuggestedWords suggestedWords) { |
| final String scrubbedTypedWord = scrubDigitsFromString(typedWord); |
| final String scrubbedAutoCorrection = scrubDigitsFromString(autoCorrection); |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords); |
| researchLogger.onWordFinished(scrubbedAutoCorrection, isBatchMode); |
| |
| // Add the autocorrection logStatement at the end of the logUnit for the committed word. |
| // We have to do this after calling commitCurrentLogUnitAsWord, because it may split the |
| // current logUnit, and then we have to peek to get the logUnit reference back. |
| final LogUnit logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit(); |
| // TODO: Add test to confirm that the commitCurrentAutoCorrection log statement should |
| // always be added to logUnit (if non-null) and not mCurrentLogUnit. |
| researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_COMMITCURRENTAUTOCORRECTION, |
| scrubbedTypedWord, scrubbedAutoCorrection, separatorString); |
| } |
| |
| private boolean isExpectingCommitText = false; |
| /** |
| * Log a call to (UnknownClass).commitPartialText |
| * |
| * SystemResponse: The IME is committing part of a word. This happens if a space is |
| * automatically inserted to split a single typed string into two or more words. |
| */ |
| // TODO: This method is currently unused. Find where it should be called from in the IME and |
| // add invocations. |
| private static final LogStatement LOGSTATEMENT_COMMIT_PARTIAL_TEXT = |
| new LogStatement("CommitPartialText", true, false, "newCursorPosition"); |
| public static void commitPartialText(final String committedWord, |
| final long lastTimestampOfWordData, final boolean isBatchMode) { |
| final ResearchLogger researchLogger = getInstance(); |
| final String scrubbedWord = scrubDigitsFromString(committedWord); |
| researchLogger.enqueueEvent(LOGSTATEMENT_COMMIT_PARTIAL_TEXT); |
| researchLogger.mStatistics.recordAutoCorrection(SystemClock.uptimeMillis()); |
| researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData, |
| isBatchMode); |
| } |
| |
| /** |
| * Log a call to RichInputConnection.commitText(). |
| * |
| * SystemResponse: The IME is committing text. This happens after the user has typed a word |
| * and then a space or punctuation key. |
| */ |
| private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT = |
| new LogStatement("RichInputConnectionCommitText", true, false, "newCursorPosition"); |
| public static void richInputConnection_commitText(final String committedWord, |
| final int newCursorPosition, final boolean isBatchMode) { |
| final ResearchLogger researchLogger = getInstance(); |
| // Only include opening and closing logSegments if private data is included |
| final String scrubbedWord = scrubDigitsFromString(committedWord); |
| if (!researchLogger.isExpectingCommitText) { |
| researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTIONCOMMITTEXT, |
| newCursorPosition); |
| researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode); |
| } |
| researchLogger.isExpectingCommitText = false; |
| } |
| |
| /** |
| * Shared event for logging committed text. |
| */ |
| private static final LogStatement LOGSTATEMENT_COMMITTEXT = |
| new LogStatement("CommitText", true, false, "committedText", "isBatchMode"); |
| private void enqueueCommitText(final String word, final boolean isBatchMode) { |
| enqueueEvent(LOGSTATEMENT_COMMITTEXT, word, isBatchMode); |
| } |
| |
| /** |
| * Log a call to RichInputConnection.deleteSurroundingText(). |
| * |
| * SystemResponse: The IME has deleted text. |
| */ |
| private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = |
| new LogStatement("RichInputConnectionDeleteSurroundingText", true, false, |
| "beforeLength", "afterLength"); |
| public static void richInputConnection_deleteSurroundingText(final int beforeLength, |
| final int afterLength) { |
| getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, |
| beforeLength, afterLength); |
| } |
| |
| /** |
| * Log a call to RichInputConnection.finishComposingText(). |
| * |
| * SystemResponse: The IME has left the composing text as-is. |
| */ |
| private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = |
| new LogStatement("RichInputConnectionFinishComposingText", false, false); |
| public static void richInputConnection_finishComposingText() { |
| getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT); |
| } |
| |
| /** |
| * Log a call to RichInputConnection.performEditorAction(). |
| * |
| * SystemResponse: The IME is invoking an action specific to the editor. |
| */ |
| private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION = |
| new LogStatement("RichInputConnectionPerformEditorAction", false, false, |
| "imeActionId"); |
| public static void richInputConnection_performEditorAction(final int imeActionId) { |
| getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION, |
| imeActionId); |
| } |
| |
| /** |
| * Log a call to RichInputConnection.sendKeyEvent(). |
| * |
| * SystemResponse: The IME is telling the TextView that a key is being pressed through an |
| * alternate channel. |
| * TODO: only for hardware keys? |
| */ |
| private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT = |
| new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action", |
| "code"); |
| public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) { |
| getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT, |
| keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode()); |
| } |
| |
| /** |
| * Log a call to RichInputConnection.setComposingText(). |
| * |
| * SystemResponse: The IME is setting the composing text. Happens each time a character is |
| * entered. |
| */ |
| private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = |
| new LogStatement("RichInputConnectionSetComposingText", true, true, "text", |
| "newCursorPosition"); |
| public static void richInputConnection_setComposingText(final CharSequence text, |
| final int newCursorPosition) { |
| if (text == null) { |
| throw new RuntimeException("setComposingText is null"); |
| } |
| getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, text, |
| newCursorPosition); |
| } |
| |
| /** |
| * Log a call to RichInputConnection.setSelection(). |
| * |
| * SystemResponse: The IME is requesting that the selection change. User-initiated selection- |
| * change requests do not go through this method -- it's only when the system wants to change |
| * the selection. |
| */ |
| private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION = |
| new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to"); |
| public static void richInputConnection_setSelection(final int from, final int to) { |
| getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to); |
| } |
| |
| /** |
| * Log a call to SuddenJumpingTouchEventHandler.onTouchEvent(). |
| * |
| * SystemResponse: The IME has filtered input events in case of an erroneous sensor reading. |
| */ |
| private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = |
| new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false, |
| "motionEvent"); |
| public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) { |
| if (me != null) { |
| getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, |
| me.toString()); |
| } |
| } |
| |
| /** |
| * Log a call to SuggestionsView.setSuggestions(). |
| * |
| * SystemResponse: The IME is setting the suggestions in the suggestion strip. |
| */ |
| private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS = |
| new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords"); |
| public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) { |
| if (suggestedWords != null) { |
| getInstance().enqueueEvent(LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS, |
| suggestedWords); |
| } |
| } |
| |
| /** |
| * The user has indicated a particular point in the log that is of interest. |
| * |
| * UserAction: From direct menu invocation. |
| */ |
| private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP = |
| new LogStatement("UserTimestamp", false, false); |
| public void userTimestamp() { |
| getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP); |
| } |
| |
| /** |
| * Log a call to LatinIME.onEndBatchInput(). |
| * |
| * SystemResponse: The system has completed a gesture. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT = |
| new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText", |
| "enteredWordPos"); |
| public static void latinIME_onEndBatchInput(final CharSequence enteredText, |
| final int enteredWordPos, final SuggestedWords suggestedWords) { |
| final ResearchLogger researchLogger = getInstance(); |
| if (!TextUtils.isEmpty(enteredText) && hasLetters(enteredText.toString())) { |
| researchLogger.mCurrentLogUnit.setWords(enteredText.toString()); |
| } |
| researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText, |
| enteredWordPos); |
| researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords); |
| researchLogger.mStatistics.recordGestureInput(enteredText.length(), |
| SystemClock.uptimeMillis()); |
| } |
| |
| /** |
| * Log a call to LatinIME.handleBackspace() that is not a batch delete. |
| * |
| * UserInput: The user is deleting one or more characters by hitting the backspace key once. |
| * The covers single character deletes as well as deleting selections. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE = |
| new LogStatement("LatinIMEHandleBackspace", true, false, "numCharacters"); |
| public static void latinIME_handleBackspace(final int numCharacters) { |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE, numCharacters); |
| } |
| |
| /** |
| * Log a call to LatinIME.handleBackspace() that is a batch delete. |
| * |
| * UserInput: The user is deleting a gestured word by hitting the backspace key once. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH = |
| new LogStatement("LatinIMEHandleBackspaceBatch", true, false, "deletedText", |
| "numCharacters"); |
| public static void latinIME_handleBackspace_batch(final CharSequence deletedText, |
| final int numCharacters) { |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLEBACKSPACE_BATCH, deletedText, |
| numCharacters); |
| researchLogger.mStatistics.recordGestureDelete(deletedText.length(), |
| SystemClock.uptimeMillis()); |
| } |
| |
| /** |
| * Log a long interval between user operation. |
| * |
| * UserInput: The user has not done anything for a while. |
| */ |
| private static final LogStatement LOGSTATEMENT_ONUSERPAUSE = new LogStatement("OnUserPause", |
| false, false, "intervalInMs"); |
| public static void onUserPause(final long interval) { |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.enqueueEvent(LOGSTATEMENT_ONUSERPAUSE, interval); |
| } |
| |
| /** |
| * Record the current time in case the LogUnit is later split. |
| * |
| * If the current logUnit is split, then tapping, motion events, etc. before this time should |
| * be assigned to one LogUnit, and events after this time should go into the following LogUnit. |
| */ |
| public static void recordTimeForLogUnitSplit() { |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.setSavedDownEventTime(SystemClock.uptimeMillis()); |
| researchLogger.mSavedDownEventTime = Long.MAX_VALUE; |
| } |
| |
| /** |
| * Log a call to LatinIME.handleSeparator() |
| * |
| * SystemResponse: The system is inserting a separator character, possibly performing auto- |
| * correction or other actions appropriate at the end of a word. |
| */ |
| private static final LogStatement LOGSTATEMENT_LATINIME_HANDLESEPARATOR = |
| new LogStatement("LatinIMEHandleSeparator", false, false, "primaryCode", |
| "isComposingWord"); |
| public static void latinIME_handleSeparator(final int primaryCode, |
| final boolean isComposingWord) { |
| final ResearchLogger researchLogger = getInstance(); |
| researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_HANDLESEPARATOR, primaryCode, |
| isComposingWord); |
| } |
| |
| /** |
| * Log statistics. |
| * |
| * ContextualData, recorded at the end of a session. |
| */ |
| private static final LogStatement LOGSTATEMENT_STATISTICS = |
| new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount", |
| "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting", |
| "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete", |
| "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete", |
| "dictionaryWordCount", "splitWordsCount", "gestureInputCount", |
| "gestureCharsCount", "gesturesDeletedCount", "manualSuggestionsCount", |
| "revertCommitsCount", "correctedWordsCount", "autoCorrectionsCount"); |
| private static void logStatistics() { |
| final ResearchLogger researchLogger = getInstance(); |
| final Statistics statistics = researchLogger.mStatistics; |
| researchLogger.enqueueEvent(LOGSTATEMENT_STATISTICS, statistics.mCharCount, |
| statistics.mLetterCount, statistics.mNumberCount, statistics.mSpaceCount, |
| statistics.mDeleteKeyCount, statistics.mWordCount, statistics.mIsEmptyUponStarting, |
| statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(), |
| statistics.mBeforeDeleteKeyCounter.getAverageTime(), |
| statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(), |
| statistics.mAfterDeleteKeyCounter.getAverageTime(), |
| statistics.mDictionaryWordCount, statistics.mSplitWordsCount, |
| statistics.mGesturesInputCount, statistics.mGesturesCharsCount, |
| statistics.mGesturesDeletedCount, statistics.mManualSuggestionsCount, |
| statistics.mRevertCommitsCount, statistics.mCorrectedWordsCount, |
| statistics.mAutoCorrectionsCount); |
| } |
| } |