diff options
Diffstat (limited to 'java/src/com/android/inputmethod/research/ResearchLogger.java')
-rw-r--r-- | java/src/com/android/inputmethod/research/ResearchLogger.java | 321 |
1 files changed, 219 insertions, 102 deletions
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index dbf2d2982..c4d53e10a 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -39,6 +39,8 @@ 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; @@ -57,6 +59,7 @@ 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; @@ -69,8 +72,17 @@ 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.BufferedReader; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -87,8 +99,18 @@ import java.util.UUID; * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}. */ 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.IS_EXPERIMENTAL_DEBUG; + private static final boolean DEBUG_REPLAY_AFTER_FEEDBACK = false + && ProductionFlag.IS_EXPERIMENTAL_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 @@ -98,8 +120,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private static final int OUTPUT_FORMAT_VERSION = 5; private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash"; - /* package */ static final String FILENAME_PREFIX = "researchLog"; - private static final String FILENAME_SUFFIX = ".txt"; + /* package */ static final String LOG_FILENAME_PREFIX = "researchLog"; + private static final String LOG_FILENAME_SUFFIX = ".txt"; + /* package */ static final String USER_RECORDING_FILENAME_PREFIX = "recording"; + private static final String USER_RECORDING_FILENAME_SUFFIX = ".txt"; private static final SimpleDateFormat TIMESTAMP_DATEFORMAT = new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); // Whether to show an indicator on the screen that logging is on. Currently a very small red @@ -129,9 +153,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // the system to do so. // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are // complete. - /* package */ ResearchLog mFeedbackLog; /* 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; @@ -144,7 +174,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001"; private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time"; private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS; - private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS; + private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS; protected static final int SUSPEND_DURATION_IN_MINUTES = 1; // set when LatinIME should ignore an onUpdateSelection() callback that // arises from operations in this class @@ -153,10 +183,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // 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. private LatinIME mLatinIME; private final Statistics mStatistics; + private final MotionEventReader mMotionEventReader = new MotionEventReader(); + private final Replayer mReplayer = new Replayer(); private Intent mUploadIntent; + private Intent mUploadNowIntent; private LogUnit mCurrentLogUnit = new LogUnit(); @@ -165,6 +199,20 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // 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(); } @@ -173,7 +221,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return sInstance; } - public void init(final LatinIME latinIME) { + public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher) { assert latinIME != null; if (latinIME == null) { Log.w(TAG, "IMS is null; logging is off"); @@ -210,6 +258,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mLatinIME = latinIME; mPrefs = prefs; mUploadIntent = new Intent(mLatinIME, UploaderService.class); + mUploadNowIntent = new Intent(mLatinIME, UploaderService.class); + mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true); + mReplayer.setKeyboardSwitcher(keyboardSwitcher); if (ProductionFlag.IS_EXPERIMENTAL) { scheduleUploadingService(mLatinIME); @@ -237,8 +288,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private void cleanupLoggingDir(final File dir, final long time) { for (File file : dir.listFiles()) { - if (file.getName().startsWith(ResearchLogger.FILENAME_PREFIX) && - file.lastModified() < time) { + final String filename = file.getName(); + if ((filename.startsWith(ResearchLogger.LOG_FILENAME_PREFIX) + || filename.startsWith(ResearchLogger.USER_RECORDING_FILENAME_PREFIX)) + && file.lastModified() < time) { file.delete(); } } @@ -335,9 +388,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private static int sLogFileCounter = 0; - private File createLogFile(File filesDir) { + private File createLogFile(final File filesDir) { final StringBuilder sb = new StringBuilder(); - sb.append(FILENAME_PREFIX).append('-'); + sb.append(LOG_FILENAME_PREFIX).append('-'); sb.append(mUUIDString).append('-'); sb.append(TIMESTAMP_DATEFORMAT.format(new Date())).append('-'); // Sometimes logFiles are created within milliseconds of each other. Append a counter to @@ -349,7 +402,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang sLogFileCounter = 0; } sb.append(sLogFileCounter); - sb.append(FILENAME_SUFFIX); + sb.append(LOG_FILENAME_SUFFIX); + return new File(filesDir, sb.toString()); + } + + private File createUserRecordingFile(final File filesDir) { + final StringBuilder sb = new StringBuilder(); + sb.append(USER_RECORDING_FILENAME_PREFIX).append('-'); + sb.append(mUUIDString).append('-'); + sb.append(TIMESTAMP_DATEFORMAT.format(new Date())); + sb.append(USER_RECORDING_FILENAME_SUFFIX); return new File(filesDir, sb.toString()); } @@ -517,51 +579,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang presentFeedbackDialog(latinIME); } - // TODO: currently unreachable. Remove after being sure no menu is needed. - /* - public void presentResearchDialog(final LatinIME latinIME) { - final CharSequence title = latinIME.getString(R.string.english_ime_research_log); - final boolean showEnable = mIsLoggingSuspended || !sIsLogging; - final CharSequence[] items = new CharSequence[] { - latinIME.getString(R.string.research_feedback_menu_option), - showEnable ? latinIME.getString(R.string.research_enable_session_logging) : - latinIME.getString(R.string.research_do_not_log_this_session) - }; - final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface di, int position) { - di.dismiss(); - switch (position) { - case 0: - presentFeedbackDialog(latinIME); - break; - case 1: - enableOrDisable(showEnable, latinIME); - break; - } - } - - }; - final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME) - .setItems(items, listener) - .setTitle(title); - latinIME.showOptionDialog(builder.create()); - } - */ - - 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; - public void presentFeedbackDialog(LatinIME latinIME) { + if (isMakingUserRecording()) { + saveRecording(); + } mInFeedbackDialog = true; mSavedFeedbackLogBuffer = mFeedbackLogBuffer; mSavedFeedbackLog = mFeedbackLog; @@ -569,7 +590,90 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // Feedback dialog will not close them. mFeedbackLogBuffer = null; mFeedbackLog = null; - latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class); + + Intent intent = new Intent(); + intent.setClass(mLatinIME, FeedbackActivity.class); + if (mFeedbackDialogBundle != null) { + Log.d(TAG, "putting extra in feedbackdialogbundle"); + 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() { + commitCurrentLogUnit(); + if (mUserRecordingLog != null) { + mUserRecordingLog.abort(); + } + mUserRecordingFile = createUserRecordingFile(mFilesDir); + 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.abort(); + } + mUserRecordingLog = null; + mUserRecordingLogBuffer = null; + if (mFeedbackDialogBundle != null) { + mFeedbackDialogBundle.putBoolean("HasRecording", false); + } + } + + private void saveRecording() { + commitCurrentLogUnit(); + publishLogBuffer(mUserRecordingLogBuffer, mUserRecordingLog, true); + mUserRecordingLog.close(null); + 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 @@ -631,52 +735,39 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return null; } - static class LogStatement { - final String mName; - - // mIsPotentiallyPrivate indicates that event contains potentially private information. If - // the word that this event is a part of is determined to be privacy-sensitive, then this - // event should not be included in the output log. The system waits to output until the - // containing word is known. - final boolean mIsPotentiallyPrivate; - - // mIsPotentiallyRevealing indicates that this statement may disclose details about other - // words typed in other LogUnits. This can happen if the user is not inserting spaces, and - // data from Suggestions and/or Composing text reveals the entire "megaword". For example, - // say the user is typing "for the win", and the system wants to record the bigram "the - // win". If the user types "forthe", omitting the space, the system will give "for the" as - // a suggestion. If the user accepts the autocorrection, the suggestion for "for the" is - // included in the log for the word "the", disclosing that the previous word had been "for". - // For now, we simply do not include this data when logging part of a "megaword". - final boolean mIsPotentiallyRevealing; - - // mKeys stores the names that are the attributes in the output json objects - final String[] mKeys; - private static final String[] NULL_KEYS = new String[0]; - - LogStatement(final String name, final boolean isPotentiallyPrivate, - final boolean isPotentiallyRevealing, final String... keys) { - mName = name; - mIsPotentiallyPrivate = isPotentiallyPrivate; - mIsPotentiallyRevealing = isPotentiallyRevealing; - mKeys = (keys == null) ? NULL_KEYS : keys; - } - } - private static final LogStatement LOGSTATEMENT_FEEDBACK = - new LogStatement("UserFeedback", false, false, "contents", "accountName"); + new LogStatement("UserFeedback", false, false, "contents", "accountName", "recording"); public void sendFeedback(final String feedbackContents, final boolean includeHistory, - final boolean isIncludingAccountName) { + 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) { + try { + final FileChannel 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) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } final LogUnit feedbackLogUnit = new LogUnit(); final String accountName = isIncludingAccountName ? getAccountName() : ""; feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(), - feedbackContents, accountName); + feedbackContents, accountName, recording); mFeedbackLogBuffer.shiftIn(feedbackLogUnit); publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */); mSavedFeedbackLog.close(new Runnable() { @@ -685,13 +776,25 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang 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); + } + }, 1000); + } } public void uploadNow() { if (DEBUG) { Log.d(TAG, "calling uploadNow()"); } - mLatinIME.startService(mUploadIntent); + mLatinIME.startService(mUploadNowIntent); } public void onLeavingSendFeedbackDialog() { @@ -734,11 +837,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang int height) { // TODO: Reimplement using a keyboard background image specific to the ResearchLogger // and remove this method. - // The check for MainKeyboardView ensures that a red border is only placed around - // the main keyboard, not every keyboard. + // The check for MainKeyboardView ensures that the indicator only decorates the main + // keyboard, not every keyboard. if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) { final int savedColor = paint.getColor(); - paint.setColor(Color.RED); + paint.setColor(isMakingUserRecording() ? Color.YELLOW : Color.RED); final Style savedStyle = paint.getStyle(); paint.setStyle(Style.STROKE); final float savedStrokeWidth = paint.getStrokeWidth(); @@ -747,10 +850,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang canvas.drawLine(0, 0, 0, height, paint); canvas.drawLine(width, 0, width, height, paint); } else { - // Put a tiny red 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. + // 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, height, width, height, paint); } @@ -770,7 +872,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement, final Object... values) { - assert values.length == logStatement.mKeys.length; + assert values.length == logStatement.getKeys().length; if (isAllowedToLog() && logUnit != null) { final long time = SystemClock.uptimeMillis(); logUnit.addLogStatement(logStatement, time, values); @@ -801,6 +903,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (mFeedbackLogBuffer != null) { mFeedbackLogBuffer.shiftIn(mCurrentLogUnit); } + if (mUserRecordingLogBuffer != null) { + mUserRecordingLogBuffer.shiftIn(mCurrentLogUnit); + } mCurrentLogUnit = new LogUnit(); } else { if (DEBUG) { @@ -1058,7 +1163,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang * */ private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT = - new LogStatement("MotionEvent", true, false, "action", "MotionEvent"); + new LogStatement("MotionEvent", true, false, "action", "MotionEvent", "loggingRelated"); 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) { @@ -1075,12 +1180,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } final ResearchLogger researchLogger = getInstance(); researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT, - actionString, MotionEvent.obtain(me)); + actionString, MotionEvent.obtain(me), false); 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(); + } } } @@ -1442,13 +1551,21 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final int code) { if (key != null) { String outputText = key.getOutputText(); - getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT, + 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(). * |