Merge "[Rlog29] User interface for recording"
commit
9c18a47162
|
@ -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: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"
|
||||
/>
|
||||
<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: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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
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();
|
||||
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);
|
||||
|
||||
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()");
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
private void startRecording() {
|
||||
// Don't record the "start recording" motion.
|
||||
public void setFeedbackDialogBundle(final Bundle bundle) {
|
||||
mFeedbackDialogBundle = bundle;
|
||||
}
|
||||
|
||||
public void startRecording() {
|
||||
final Resources res = mLatinIME.getResources();
|
||||
Toast.makeText(mLatinIME,
|
||||
res.getString(R.string.research_feedback_demonstration_instructions),
|
||||
Toast.LENGTH_LONG).show();
|
||||
startRecordingInternal();
|
||||
}
|
||||
|
||||
private void startRecordingInternal() {
|
||||
commitCurrentLogUnit();
|
||||
if (mUserRecordingLog != null) {
|
||||
mUserRecordingLog.abort();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue