From faf35c323b8f41e780c7379932d0985bd3b40a52 Mon Sep 17 00:00:00 2001 From: Kurt Partridge <kep@google.com> Date: Mon, 21 Jan 2013 17:12:44 -0800 Subject: [PATCH] [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 --- .../research_feedback_fragment_layout.xml | 188 ++++++++-------- java/res/values/strings.xml | 10 + .../research/FeedbackActivity.java | 15 -- .../research/FeedbackFragment.java | 126 ++++++++--- .../inputmethod/research/ResearchLogger.java | 202 ++++++++++++++---- .../inputmethod/research/UploaderService.java | 2 +- 6 files changed, 364 insertions(+), 179 deletions(-) diff --git a/java/res/layout/research_feedback_fragment_layout.xml b/java/res/layout/research_feedback_fragment_layout.xml index 2a90ba2a02..2725e7f49a 100644 --- a/java/res/layout/research_feedback_fragment_layout.xml +++ b/java/res/layout/research_feedback_fragment_layout.xml @@ -14,107 +14,111 @@ limitations under the License. --> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:orientation="vertical" -> - - <!-- Mimic a dialog title. Necessary since the dialog is actually an activity, so the normal - dialog title construction code is not available. --> +<!-- Adapted from frameworks/base/core/res/res/layout/alert_dialog_holo.xml. We + want a dialog, but it must be its own activity so we can launch the soft + keyboard on it. A regular dialog will not work since it would be launched from + the IME. --> +<ScrollView> <LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - > - <com.android.internal.widget.DialogTitle - style="?android:attr/windowTitleStyle" - android:singleLine="true" - android:ellipsize="end" + android:layout_height="match_parent" + android:layout_marginStart="8dip" + android:layout_marginEnd="8dip" + android:orientation="vertical"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + <View android:layout_width="match_parent" + android:layout_height="2dip" + android:visibility="gone" + android:background="@android:color/holo_blue_light" /> + <TextView + style="?android:attr/windowTitleStyle" + android:singleLine="true" + android:ellipsize="end" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="64dip" + android:layout_marginLeft="16dip" + android:layout_marginRight="16dip" + android:gravity="center_vertical|left" + android:text="@string/research_feedback_dialog_title" /> + <View + android:layout_width="match_parent" + android:layout_height="2dip" + android:background="@android:color/holo_blue_light" /> + </LinearLayout> + + <EditText + android:id="@+id/research_feedback_contents" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:layout_gravity="fill_horizontal|center_vertical" + android:layout_marginLeft="8dip" + android:layout_marginRight="8dip" + android:layout_marginBottom="8dip" + android:layout_marginTop="8dip" + android:minLines="2" + android:scrollbars="vertical" + android:hint="@string/research_feedback_hint" + android:inputType="textMultiLine"> + <requestFocus /> + </EditText> + <CheckBox + android:id="@+id/research_feedback_include_account_name" + android:layout_height="wrap_content" android:layout_width="match_parent" - android:layout_height="64dip" android:layout_marginLeft="16dip" android:layout_marginRight="16dip" - android:gravity="center_vertical|left" - android:text="@string/research_feedback_dialog_title" /> - <View + android:layout_marginBottom="8dip" + android:checked="false" + android:text="@string/research_feedback_include_account_name_label" /> + <CheckBox + android:id="@+id/research_feedback_include_recording_checkbox" + android:layout_height="wrap_content" android:layout_width="match_parent" - android:layout_height="2dip" - android:background="@android:color/holo_blue_light" /> - </LinearLayout> - - <EditText - android:id="@+id/research_feedback_contents" - android:layout_height="wrap_content" - android:layout_width="match_parent" - android:layout_gravity="fill_horizontal|center_vertical" - android:layout_marginLeft="8dip" - android:layout_marginRight="8dip" - android:layout_marginBottom="8dip" - android:layout_marginTop="8dip" - android:lines="2" - android:hint="@string/research_feedback_hint" - android:inputType="textMultiLine" - android:imeOptions="flagNoFullscreen" - > - <requestFocus /> - </EditText> - - <CheckBox - android:id="@+id/research_feedback_include_history" - android:layout_height="wrap_content" - android:layout_width="match_parent" - android:layout_marginBottom="8dip" - android:checked="true" - android:text="@string/research_feedback_include_history_label" - /> - - <CheckBox - android:id="@+id/research_feedback_include_account_name" - android:layout_height="wrap_content" - android:layout_width="match_parent" - android:layout_marginBottom="8dip" - android:checked="false" - android:text="@string/research_feedback_include_account_name_label" - /> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - android:divider="?android:attr/dividerHorizontal" - android:showDividers="beginning" - android:dividerPadding="0dip" - > + android:layout_marginLeft="16dip" + android:layout_marginRight="16dip" + android:layout_marginBottom="8dip" + android:checked="false" + android:text="@string/research_feedback_include_recording_label" /> <LinearLayout - style="?android:attr/buttonBarStyle" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="horizontal" - android:measureWithLargestChild="true" - > - <Button - android:id="@+id/research_feedback_cancel_button" - android:layout_width="0dip" - android:layout_gravity="left" - android:layout_weight="1" - android:maxLines="2" - style="?android:attr/buttonBarButtonStyle" - android:textSize="14sp" - android:text="@string/research_feedback_cancel" - android:layout_height="wrap_content" - /> - <Button - android:id="@+id/research_feedback_send_button" - android:layout_width="0dip" - android:layout_gravity="right" - android:layout_weight="1" - android:maxLines="2" - style="?android:attr/buttonBarButtonStyle" - android:textSize="14sp" - android:text="@string/research_feedback_send" + android:orientation="vertical" + android:divider="?android:attr/dividerHorizontal" + android:showDividers="beginning" + android:dividerPadding="0dip"> + <LinearLayout + style="?android:attr/buttonBarStyle" + android:layout_width="match_parent" android:layout_height="wrap_content" - /> + android:orientation="horizontal" + android:layoutDirection="locale" + android:measureWithLargestChild="true"> + <Button + android:id="@+id/research_feedback_cancel_button" + android:layout_width="wrap_content" + android:layout_gravity="left" + android:layout_weight="1" + android:maxLines="2" + style="?android:attr/buttonBarButtonStyle" + android:textSize="14sp" + android:text="@string/research_feedback_cancel" + android:layout_height="wrap_content" /> + <Button + android:id="@+id/research_feedback_send_button" + android:layout_width="wrap_content" + android:layout_gravity="right" + android:layout_weight="1" + android:maxLines="2" + style="?android:attr/buttonBarButtonStyle" + android:textSize="14sp" + android:text="@string/research_feedback_send" + android:layout_height="wrap_content" /> + </LinearLayout> </LinearLayout> </LinearLayout> -</LinearLayout> +</ScrollView> diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index e3cd84c9d8..8822e8d18a 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -278,6 +278,9 @@ <!-- Text for checkbox option to include user account name in feedback for research purposes [CHAR LIMIT=50] --> <!-- TODO: remove translatable=false attribute once text is stable --> <string name="research_feedback_include_account_name_label" translatable="false">Include account name</string> + <!-- Text for checkbox option to include a recording in feedback for research purposes [CHAR LIMIT=50] --> + <!-- TODO: remove translatable=false attribute once text is stable --> + <string name="research_feedback_include_recording_label" translatable="false">Include recorded demonstration</string> <!-- Hint to user about the text entry field where they should enter research feedback [CHAR LIMIT=40] --> <!-- TODO: remove translatable=false attribute once text is stable --> <string name="research_feedback_hint" translatable="false">Enter your feedback here.</string> @@ -287,6 +290,13 @@ <!-- Dialog button choice to cancel sending research feedback [CHAR LIMIT=35] --> <!-- TODO: remove translatable=false attribute once text is stable --> <string name="research_feedback_cancel" translatable="false">Cancel</string> + <!-- Temporary notification to provide user with instructions about stopping a recording + - operation[CHAR LIMIT=100] --> + <!-- TODO: remove translatable=false attribute once text is stable --> + <string name="research_feedback_demonstration_instructions" translatable="false">Please demonstrate the issue you are writing about.\n\nWhen finished, select the \"Bug?\" button again."</string> + <!-- Temporary notification of recording failure [CHAR LIMIT=100] --> + <!-- TODO: remove translatable=false attribute once text is stable --> + <string name="research_feedback_recording_failure" translatable="false">Recording cancelled due to timeout</string> <!-- Toast notification to ask user to quit the research feedback dialog to perform this operation [CHAR LIMIT=100] --> <!-- TODO: remove translatable=false attribute once text is stable --> <string name="research_please_exit_feedback_form" translatable="false">Please exit the feedback dialog to access the research log menu</string> diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java index f66d55bdd6..b985fda217 100644 --- a/java/src/com/android/inputmethod/research/FeedbackActivity.java +++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java @@ -28,24 +28,9 @@ 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(); diff --git a/java/src/com/android/inputmethod/research/FeedbackFragment.java b/java/src/com/android/inputmethod/research/FeedbackFragment.java index fee61a9238..11a833a85e 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 925a72e45d..c4d53e10aa 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 69fb36d9c2..89c67fbb23 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; -- GitLab