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