[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
main
Kurt Partridge 2013-01-21 17:12:44 -08:00
parent f3731188e5
commit faf35c323b
6 changed files with 365 additions and 180 deletions

View File

@ -14,107 +14,111 @@
limitations under the License. limitations under the License.
--> -->
<LinearLayout <!-- Adapted from frameworks/base/core/res/res/layout/alert_dialog_holo.xml. We
xmlns:android="http://schemas.android.com/apk/res/android" want a dialog, but it must be its own activity so we can launch the soft
android:layout_width="fill_parent" keyboard on it. A regular dialog will not work since it would be launched from
android:layout_height="fill_parent" the IME. -->
android:orientation="vertical" <ScrollView>
>
<!-- Mimic a dialog title. Necessary since the dialog is actually an activity, so the normal
dialog title construction code is not available. -->
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="vertical" android:layout_marginStart="8dip"
> android:layout_marginEnd="8dip"
<com.android.internal.widget.DialogTitle android:orientation="vertical">
style="?android:attr/windowTitleStyle" <LinearLayout
android:singleLine="true" android:layout_width="match_parent"
android:ellipsize="end" 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_width="match_parent"
android:layout_height="64dip"
android:layout_marginLeft="16dip" android:layout_marginLeft="16dip"
android:layout_marginRight="16dip" android:layout_marginRight="16dip"
android:gravity="center_vertical|left" android:layout_marginBottom="8dip"
android:text="@string/research_feedback_dialog_title" /> android:checked="false"
<View 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_width="match_parent"
android:layout_height="2dip" android:layout_marginLeft="16dip"
android:background="@android:color/holo_blue_light" /> android:layout_marginRight="16dip"
</LinearLayout> android:layout_marginBottom="8dip"
android:checked="false"
<EditText android:text="@string/research_feedback_include_recording_label" />
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"
>
<LinearLayout <LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="vertical"
android:measureWithLargestChild="true" android:divider="?android:attr/dividerHorizontal"
> android:showDividers="beginning"
<Button android:dividerPadding="0dip">
android:id="@+id/research_feedback_cancel_button" <LinearLayout
android:layout_width="0dip" style="?android:attr/buttonBarStyle"
android:layout_gravity="left" android:layout_width="match_parent"
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" android:layout_height="wrap_content"
/> android:orientation="horizontal"
<Button android:layoutDirection="locale"
android:id="@+id/research_feedback_send_button" android:measureWithLargestChild="true">
android:layout_width="0dip" <Button
android:layout_gravity="right" android:id="@+id/research_feedback_cancel_button"
android:layout_weight="1" android:layout_width="wrap_content"
android:maxLines="2" android:layout_gravity="left"
style="?android:attr/buttonBarButtonStyle" android:layout_weight="1"
android:textSize="14sp" android:maxLines="2"
android:text="@string/research_feedback_send" style="?android:attr/buttonBarButtonStyle"
android:layout_height="wrap_content" 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> </LinearLayout>
</LinearLayout> </ScrollView>

View File

@ -278,6 +278,9 @@
<!-- Text for checkbox option to include user account name in feedback for research purposes [CHAR LIMIT=50] --> <!-- 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 --> <!-- TODO: remove translatable=false attribute once text is stable -->
<string name="research_feedback_include_account_name_label" translatable="false">Include account name</string> <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] --> <!-- 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 --> <!-- TODO: remove translatable=false attribute once text is stable -->
<string name="research_feedback_hint" translatable="false">Enter your feedback here.</string> <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] --> <!-- Dialog button choice to cancel sending research feedback [CHAR LIMIT=35] -->
<!-- TODO: remove translatable=false attribute once text is stable --> <!-- TODO: remove translatable=false attribute once text is stable -->
<string name="research_feedback_cancel" translatable="false">Cancel</string> <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] --> <!-- 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 --> <!-- 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> <string name="research_please_exit_feedback_form" translatable="false">Please exit the feedback dialog to access the research log menu</string>

View File

@ -28,24 +28,9 @@ public class FeedbackActivity extends Activity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.research_feedback_activity); setContentView(R.layout.research_feedback_activity);
final FeedbackLayout layout = (FeedbackLayout) findViewById(R.id.research_feedback_layout); 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); layout.setActivity(this);
} }
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onPause() {
super.onPause();
}
@Override @Override
public void onBackPressed() { public void onBackPressed() {
ResearchLogger.getInstance().onLeavingSendFeedbackDialog(); ResearchLogger.getInstance().onLeavingSendFeedbackDialog();

View File

@ -20,6 +20,7 @@ import android.app.Activity;
import android.app.Fragment; import android.app.Fragment;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
@ -30,10 +31,18 @@ import android.widget.EditText;
import com.android.inputmethod.latin.R; 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 EditText mEditText;
private CheckBox mIncludingHistoryCheckBox;
private CheckBox mIncludingAccountNameCheckBox; private CheckBox mIncludingAccountNameCheckBox;
private CheckBox mIncludingUserRecordingCheckBox;
private Button mSendButton;
private Button mCancelButton;
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, 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, final View view = inflater.inflate(R.layout.research_feedback_fragment_layout, container,
false); false);
mEditText = (EditText) view.findViewById(R.id.research_feedback_contents); mEditText = (EditText) view.findViewById(R.id.research_feedback_contents);
mIncludingHistoryCheckBox = (CheckBox) view.findViewById( mEditText.requestFocus();
R.id.research_feedback_include_history);
mIncludingAccountNameCheckBox = (CheckBox) view.findViewById( mIncludingAccountNameCheckBox = (CheckBox) view.findViewById(
R.id.research_feedback_include_account_name); R.id.research_feedback_include_account_name);
mIncludingUserRecordingCheckBox = (CheckBox) view.findViewById(
R.id.research_feedback_include_recording_checkbox);
mIncludingUserRecordingCheckBox.setOnClickListener(this);
final Button sendButton = (Button) view.findViewById( mSendButton = (Button) view.findViewById(R.id.research_feedback_send_button);
R.id.research_feedback_send_button); mSendButton.setOnClickListener(this);
sendButton.setOnClickListener(new OnClickListener() { mCancelButton = (Button) view.findViewById(R.id.research_feedback_cancel_button);
@Override mCancelButton.setOnClickListener(this);
public void onClick(View v) {
final Editable editable = mEditText.getText(); if (savedInstanceState != null) {
final String feedbackContents = editable.toString(); Log.d(TAG, "restoring from savedInstanceState");
final boolean isIncludingHistory = mIncludingHistoryCheckBox.isChecked(); restoreState(savedInstanceState);
final boolean isIncludingAccountName = mIncludingAccountNameCheckBox.isChecked(); } else {
ResearchLogger.getInstance().sendFeedback(feedbackContents, isIncludingHistory, final Bundle bundle = getActivity().getIntent().getExtras();
isIncludingAccountName); if (bundle != null) {
final Activity activity = FeedbackFragment.this.getActivity(); Log.d(TAG, "restoring from getArguments()");
activity.finish(); restoreState(bundle);
ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
} }
}); }
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; 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()");
}
}
@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);
}
} }

View File

@ -39,6 +39,8 @@ import android.graphics.Paint;
import android.graphics.Paint.Style; import android.graphics.Paint.Style;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock; import android.os.SystemClock;
import android.preference.PreferenceManager; 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.Suggest;
import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.define.ProductionFlag; 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.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.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
@ -88,8 +99,18 @@ import java.util.UUID;
* This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}. * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
*/ */
public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener { 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 String TAG = ResearchLogger.class.getSimpleName();
private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; 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 // Whether the TextView contents are logged at the end of the session. true will disclose
// private info. // private info.
private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false 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"; /* 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 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 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; protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
// set when LatinIME should ignore an onUpdateSelection() callback that // set when LatinIME should ignore an onUpdateSelection() callback that
// arises from operations in this class // arises from operations in this class
@ -162,12 +183,14 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// used to check whether words are not unique // used to check whether words are not unique
private Suggest mSuggest; private Suggest mSuggest;
private MainKeyboardView mMainKeyboardView; private MainKeyboardView mMainKeyboardView;
// TODO: Check whether a superclass can be used instead of LatinIME.
private LatinIME mLatinIME; private LatinIME mLatinIME;
private final Statistics mStatistics; private final Statistics mStatistics;
private final MotionEventReader mMotionEventReader = new MotionEventReader(); private final MotionEventReader mMotionEventReader = new MotionEventReader();
private final Replayer mReplayer = new Replayer(); private final Replayer mReplayer = new Replayer();
private Intent mUploadIntent; private Intent mUploadIntent;
private Intent mUploadNowIntent;
private LogUnit mCurrentLogUnit = new LogUnit(); 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 // 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. // gesture, and when committing the earlier word, split the LogUnit.
private long mSavedDownEventTime; 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() { private ResearchLogger() {
mStatistics = Statistics.getInstance(); mStatistics = Statistics.getInstance();
} }
@ -221,6 +258,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mLatinIME = latinIME; mLatinIME = latinIME;
mPrefs = prefs; mPrefs = prefs;
mUploadIntent = new Intent(mLatinIME, UploaderService.class); mUploadIntent = new Intent(mLatinIME, UploaderService.class);
mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true);
mReplayer.setKeyboardSwitcher(keyboardSwitcher); mReplayer.setKeyboardSwitcher(keyboardSwitcher);
if (ProductionFlag.IS_EXPERIMENTAL) { if (ProductionFlag.IS_EXPERIMENTAL) {
@ -540,16 +579,41 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
presentFeedbackDialog(latinIME); presentFeedbackDialog(latinIME);
} }
private void cancelRecording() { public void presentFeedbackDialog(LatinIME latinIME) {
if (mUserRecordingLog != null) { if (isMakingUserRecording()) {
mUserRecordingLog.abort(); saveRecording();
} }
mUserRecordingLog = null; mInFeedbackDialog = true;
mUserRecordingLogBuffer = null; 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);
} }
private void startRecording() { public void setFeedbackDialogBundle(final Bundle bundle) {
// Don't record the "start recording" motion. mFeedbackDialogBundle = bundle;
}
public void startRecording() {
final Resources res = mLatinIME.getResources();
Toast.makeText(mLatinIME,
res.getString(R.string.research_feedback_demonstration_instructions),
Toast.LENGTH_LONG).show();
startRecordingInternal();
}
private void startRecordingInternal() {
commitCurrentLogUnit(); commitCurrentLogUnit();
if (mUserRecordingLog != null) { if (mUserRecordingLog != null) {
mUserRecordingLog.abort(); mUserRecordingLog.abort();
@ -557,6 +621,46 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mUserRecordingFile = createUserRecordingFile(mFilesDir); mUserRecordingFile = createUserRecordingFile(mFilesDir);
mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME); mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME);
mUserRecordingLogBuffer = new LogBuffer(); 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() { private void saveRecording() {
@ -565,29 +669,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mUserRecordingLog.close(null); mUserRecordingLog.close(null);
mUserRecordingLog = null; mUserRecordingLog = null;
mUserRecordingLogBuffer = null; mUserRecordingLogBuffer = null;
}
private boolean mInFeedbackDialog = false; if (mFeedbackDialogBundle != null) {
mFeedbackDialogBundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, true);
// 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 clearRecordingTimer();
// 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);
} }
// TODO: currently unreachable. Remove after being sure enable/disable is // 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 = 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, public void sendFeedback(final String feedbackContents, final boolean includeHistory,
final boolean isIncludingAccountName) { final boolean isIncludingAccountName, final boolean isIncludingRecording) {
if (mSavedFeedbackLogBuffer == null) { if (mSavedFeedbackLogBuffer == null) {
return; return;
} }
if (!includeHistory) { if (!includeHistory) {
mSavedFeedbackLogBuffer.clear(); 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 LogUnit feedbackLogUnit = new LogUnit();
final String accountName = isIncludingAccountName ? getAccountName() : ""; final String accountName = isIncludingAccountName ? getAccountName() : "";
feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(), feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
feedbackContents, accountName); feedbackContents, accountName, recording);
mFeedbackLogBuffer.shiftIn(feedbackLogUnit); mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */); publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */);
mSavedFeedbackLog.close(new Runnable() { mSavedFeedbackLog.close(new Runnable() {
@ -671,13 +776,25 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
uploadNow(); 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() { public void uploadNow() {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "calling uploadNow()"); Log.d(TAG, "calling uploadNow()");
} }
mLatinIME.startService(mUploadIntent); mLatinIME.startService(mUploadNowIntent);
} }
public void onLeavingSendFeedbackDialog() { public void onLeavingSendFeedbackDialog() {
@ -720,11 +837,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
int height) { int height) {
// TODO: Reimplement using a keyboard background image specific to the ResearchLogger // TODO: Reimplement using a keyboard background image specific to the ResearchLogger
// and remove this method. // and remove this method.
// The check for MainKeyboardView ensures that a red border is only placed around // The check for MainKeyboardView ensures that the indicator only decorates the main
// the main keyboard, not every keyboard. // keyboard, not every keyboard.
if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) { if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) {
final int savedColor = paint.getColor(); final int savedColor = paint.getColor();
paint.setColor(Color.RED); paint.setColor(isMakingUserRecording() ? Color.YELLOW : Color.RED);
final Style savedStyle = paint.getStyle(); final Style savedStyle = paint.getStyle();
paint.setStyle(Style.STROKE); paint.setStyle(Style.STROKE);
final float savedStrokeWidth = paint.getStrokeWidth(); final float savedStrokeWidth = paint.getStrokeWidth();
@ -733,10 +850,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
canvas.drawLine(0, 0, 0, height, paint); canvas.drawLine(0, 0, 0, height, paint);
canvas.drawLine(width, 0, width, height, paint); canvas.drawLine(width, 0, width, height, paint);
} else { } else {
// Put a tiny red dot on the screen so a knowledgeable user can check whether // Put a tiny dot on the screen so a knowledgeable user can check whether it is
// it is enabled. The dot is actually a zero-width, zero-height rectangle, // enabled. The dot is actually a zero-width, zero-height rectangle, placed at the
// placed at the lower-right corner of the canvas, painted with a non-zero border // lower-right corner of the canvas, painted with a non-zero border width.
// width.
paint.setStrokeWidth(3); paint.setStrokeWidth(3);
canvas.drawRect(width, height, width, height, paint); 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). // LogUnit, not the earlier (the test is for inequality).
researchLogger.setSavedDownEventTime(eventTime - 1); researchLogger.setSavedDownEventTime(eventTime - 1);
} }
// Refresh the timer in case we are capturing user feedback.
if (researchLogger.isMakingUserRecording()) {
researchLogger.resetRecordingTimer();
}
} }
} }

View File

@ -51,7 +51,7 @@ public final class UploaderService extends IntentService {
private static final boolean IS_INHIBITING_AUTO_UPLOAD = false private static final boolean IS_INHIBITING_AUTO_UPLOAD = false
&& ProductionFlag.IS_EXPERIMENTAL_DEBUG; // Force false in production && ProductionFlag.IS_EXPERIMENTAL_DEBUG; // Force false in production
public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR; 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"; + ".extra.UPLOAD_UNCONDITIONALLY";
private static final int BUF_SIZE = 1024 * 8; private static final int BUF_SIZE = 1024 * 8;
protected static final int TIMEOUT_IN_MS = 1000 * 4; protected static final int TIMEOUT_IN_MS = 1000 * 4;