am 9c18a471: Merge "[Rlog29] User interface for recording"

# Via Android (Google) Code Review (1) and Kurt Partridge (1)
* commit '9c18a47162cb88242632e3a37bfae99d21d8f85a':
  [Rlog29] User interface for recording
main
Kurt Partridge 2013-01-30 08:43:24 -08:00 committed by Android Git Automerger
commit fcc161b53d
6 changed files with 365 additions and 180 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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;