diff options
author | 2013-01-21 17:12:44 -0800 | |
---|---|---|
committer | 2013-01-29 15:46:16 -0800 | |
commit | faf35c323b8f41e780c7379932d0985bd3b40a52 (patch) | |
tree | 4d74840a170d3d909d0163218dfea9844910dd1c /java/src | |
parent | f3731188e5ee46c0bee7e9366528c826289a91bb (diff) | |
download | latinime-faf35c323b8f41e780c7379932d0985bd3b40a52.tar.gz latinime-faf35c323b8f41e780c7379932d0985bd3b40a52.tar.xz latinime-faf35c323b8f41e780c7379932d0985bd3b40a52.zip |
[Rlog29] User interface for recording
- Also, internal flag for automatically replaying after a recording is made (off by default)
- RLog key to "Bug?"
multi-project commit with I0c2fababd73eed5a341af487bca04ddd650d4cc2
Change-Id: I162c96a715de7180f276e08b4686a20f29dabafb
Diffstat (limited to 'java/src')
4 files changed, 258 insertions, 87 deletions
diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java index f66d55bdd..b985fda21 100644 --- a/java/src/com/android/inputmethod/research/FeedbackActivity.java +++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java @@ -28,25 +28,10 @@ public class FeedbackActivity extends Activity { super.onCreate(savedInstanceState); setContentView(R.layout.research_feedback_activity); final FeedbackLayout layout = (FeedbackLayout) findViewById(R.id.research_feedback_layout); - final CheckBox checkbox = (CheckBox) findViewById(R.id.research_feedback_include_history); - final CharSequence cs = checkbox.getText(); - final String actualString = String.format(cs.toString(), - ResearchLogger.FEEDBACK_WORD_BUFFER_SIZE); - checkbox.setText(actualString); layout.setActivity(this); } @Override - protected void onResume() { - super.onResume(); - } - - @Override - protected void onPause() { - super.onPause(); - } - - @Override public void onBackPressed() { ResearchLogger.getInstance().onLeavingSendFeedbackDialog(); super.onBackPressed(); diff --git a/java/src/com/android/inputmethod/research/FeedbackFragment.java b/java/src/com/android/inputmethod/research/FeedbackFragment.java index fee61a923..11a833a85 100644 --- a/java/src/com/android/inputmethod/research/FeedbackFragment.java +++ b/java/src/com/android/inputmethod/research/FeedbackFragment.java @@ -20,6 +20,7 @@ import android.app.Activity; import android.app.Fragment; import android.os.Bundle; import android.text.Editable; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; @@ -30,10 +31,18 @@ import android.widget.EditText; import com.android.inputmethod.latin.R; -public class FeedbackFragment extends Fragment { +public class FeedbackFragment extends Fragment implements OnClickListener { + private static final String TAG = FeedbackFragment.class.getSimpleName(); + + private static final String KEY_FEEDBACK_STRING = "FeedbackString"; + private static final String KEY_INCLUDE_ACCOUNT_NAME = "IncludeAccountName"; + public static final String KEY_HAS_USER_RECORDING = "HasRecording"; + private EditText mEditText; - private CheckBox mIncludingHistoryCheckBox; private CheckBox mIncludingAccountNameCheckBox; + private CheckBox mIncludingUserRecordingCheckBox; + private Button mSendButton; + private Button mCancelButton; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -41,39 +50,96 @@ public class FeedbackFragment extends Fragment { final View view = inflater.inflate(R.layout.research_feedback_fragment_layout, container, false); mEditText = (EditText) view.findViewById(R.id.research_feedback_contents); - mIncludingHistoryCheckBox = (CheckBox) view.findViewById( - R.id.research_feedback_include_history); + mEditText.requestFocus(); mIncludingAccountNameCheckBox = (CheckBox) view.findViewById( R.id.research_feedback_include_account_name); + mIncludingUserRecordingCheckBox = (CheckBox) view.findViewById( + R.id.research_feedback_include_recording_checkbox); + mIncludingUserRecordingCheckBox.setOnClickListener(this); + + mSendButton = (Button) view.findViewById(R.id.research_feedback_send_button); + mSendButton.setOnClickListener(this); + mCancelButton = (Button) view.findViewById(R.id.research_feedback_cancel_button); + mCancelButton.setOnClickListener(this); - final Button sendButton = (Button) view.findViewById( - R.id.research_feedback_send_button); - sendButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - final Editable editable = mEditText.getText(); - final String feedbackContents = editable.toString(); - final boolean isIncludingHistory = mIncludingHistoryCheckBox.isChecked(); - final boolean isIncludingAccountName = mIncludingAccountNameCheckBox.isChecked(); - ResearchLogger.getInstance().sendFeedback(feedbackContents, isIncludingHistory, - isIncludingAccountName); - final Activity activity = FeedbackFragment.this.getActivity(); - activity.finish(); - ResearchLogger.getInstance().onLeavingSendFeedbackDialog(); + if (savedInstanceState != null) { + Log.d(TAG, "restoring from savedInstanceState"); + restoreState(savedInstanceState); + } else { + final Bundle bundle = getActivity().getIntent().getExtras(); + if (bundle != null) { + Log.d(TAG, "restoring from getArguments()"); + restoreState(bundle); } - }); - - final Button cancelButton = (Button) view.findViewById( - R.id.research_feedback_cancel_button); - cancelButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - final Activity activity = FeedbackFragment.this.getActivity(); - activity.finish(); - ResearchLogger.getInstance().onLeavingSendFeedbackDialog(); + } + return view; + } + + @Override + public void onClick(final View view) { + final ResearchLogger researchLogger = ResearchLogger.getInstance(); + if (view == mIncludingUserRecordingCheckBox) { + if (hasUserRecording()) { + // Remove the recording + setHasUserRecording(false); + } else { + final Bundle bundle = new Bundle(); + onSaveInstanceState(bundle); + + // Let the user make a recording + getActivity().finish(); + + researchLogger.setFeedbackDialogBundle(bundle); + researchLogger.onLeavingSendFeedbackDialog(); + researchLogger.startRecording(); } - }); + } else if (view == mSendButton) { + final Editable editable = mEditText.getText(); + final String feedbackContents = editable.toString(); + final boolean isIncludingAccountName = isIncludingAccountName(); + researchLogger.sendFeedback(feedbackContents, + false /* isIncludingHistory */, isIncludingAccountName, hasUserRecording()); + getActivity().finish(); + researchLogger.setFeedbackDialogBundle(null); + researchLogger.onLeavingSendFeedbackDialog(); + } else if (view == mCancelButton) { + Log.d(TAG, "Finishing"); + getActivity().finish(); + researchLogger.setFeedbackDialogBundle(null); + researchLogger.onLeavingSendFeedbackDialog(); + } else { + Log.e(TAG, "Unknown view passed to FeedbackFragment.onClick()"); + } + } - return view; + @Override + public void onSaveInstanceState(final Bundle bundle) { + final String savedFeedbackString = mEditText.getText().toString(); + + bundle.putString(KEY_FEEDBACK_STRING, savedFeedbackString); + bundle.putBoolean(KEY_INCLUDE_ACCOUNT_NAME, isIncludingAccountName()); + bundle.putBoolean(KEY_HAS_USER_RECORDING, hasUserRecording()); + } + + public void restoreState(final Bundle bundle) { + mEditText.setText(bundle.getString(KEY_FEEDBACK_STRING)); + setIsIncludingAccountName(bundle.getBoolean(KEY_INCLUDE_ACCOUNT_NAME)); + setHasUserRecording(bundle.getBoolean(KEY_HAS_USER_RECORDING)); + } + + private boolean hasUserRecording() { + return mIncludingUserRecordingCheckBox.isChecked(); + } + + private void setHasUserRecording(final boolean hasRecording) { + mIncludingUserRecordingCheckBox.setChecked(hasRecording); + } + + private boolean isIncludingAccountName() { + return mIncludingAccountNameCheckBox.isChecked(); + } + + private void setIsIncludingAccountName(final boolean isIncludingAccountName) { + mIncludingAccountNameCheckBox.setChecked(isIncludingAccountName); } } diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index 925a72e45..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; @@ -70,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; @@ -88,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 @@ -153,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 @@ -162,12 +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(); @@ -176,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(); } @@ -221,6 +258,8 @@ 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) { @@ -540,16 +579,41 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang presentFeedbackDialog(latinIME); } - private void cancelRecording() { - if (mUserRecordingLog != null) { - mUserRecordingLog.abort(); + public void presentFeedbackDialog(LatinIME latinIME) { + if (isMakingUserRecording()) { + saveRecording(); } - mUserRecordingLog = null; - mUserRecordingLogBuffer = null; + 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; + + 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 startRecording() { - // Don't record the "start recording" motion. + private void startRecordingInternal() { commitCurrentLogUnit(); if (mUserRecordingLog != null) { mUserRecordingLog.abort(); @@ -557,6 +621,46 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang 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() { @@ -565,29 +669,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mUserRecordingLog.close(null); mUserRecordingLog = null; mUserRecordingLogBuffer = 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; - public void presentFeedbackDialog(LatinIME latinIME) { - 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; - latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class); + if (mFeedbackDialogBundle != null) { + mFeedbackDialogBundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, true); + } + clearRecordingTimer(); } // TODO: currently unreachable. Remove after being sure enable/disable is @@ -650,19 +736,38 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } 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() { @@ -671,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() { @@ -720,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(); @@ -733,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); } @@ -1070,6 +1186,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // 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(); + } } } diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java index 69fb36d9c..89c67fbb2 100644 --- a/java/src/com/android/inputmethod/research/UploaderService.java +++ b/java/src/com/android/inputmethod/research/UploaderService.java @@ -51,7 +51,7 @@ public final class UploaderService extends IntentService { private static final boolean IS_INHIBITING_AUTO_UPLOAD = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; // Force false in production public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR; - private static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName() + public static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName() + ".extra.UPLOAD_UNCONDITIONALLY"; private static final int BUF_SIZE = 1024 * 8; protected static final int TIMEOUT_IN_MS = 1000 * 4; |