am 9c18a471: Merge "[Rlog29] User interface for recording"
# Via Android (Google) Code Review (1) and Kurt Partridge (1) * commit '9c18a47162cb88242632e3a37bfae99d21d8f85a': [Rlog29] User interface for recordingmain
commit
fcc161b53d
|
@ -14,26 +14,33 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<!-- 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
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="fill_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:layout_marginStart="8dip"
|
||||||
>
|
android:layout_marginEnd="8dip"
|
||||||
|
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. -->
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical">
|
||||||
>
|
<View android:layout_width="match_parent"
|
||||||
<com.android.internal.widget.DialogTitle
|
android:layout_height="2dip"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:background="@android:color/holo_blue_light" />
|
||||||
|
<TextView
|
||||||
style="?android:attr/windowTitleStyle"
|
style="?android:attr/windowTitleStyle"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="64dip"
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="64dip"
|
||||||
android:layout_marginLeft="16dip"
|
android:layout_marginLeft="16dip"
|
||||||
android:layout_marginRight="16dip"
|
android:layout_marginRight="16dip"
|
||||||
android:gravity="center_vertical|left"
|
android:gravity="center_vertical|left"
|
||||||
|
@ -53,68 +60,65 @@
|
||||||
android:layout_marginRight="8dip"
|
android:layout_marginRight="8dip"
|
||||||
android:layout_marginBottom="8dip"
|
android:layout_marginBottom="8dip"
|
||||||
android:layout_marginTop="8dip"
|
android:layout_marginTop="8dip"
|
||||||
android:lines="2"
|
android:minLines="2"
|
||||||
|
android:scrollbars="vertical"
|
||||||
android:hint="@string/research_feedback_hint"
|
android:hint="@string/research_feedback_hint"
|
||||||
android:inputType="textMultiLine"
|
android:inputType="textMultiLine">
|
||||||
android:imeOptions="flagNoFullscreen"
|
|
||||||
>
|
|
||||||
<requestFocus />
|
<requestFocus />
|
||||||
</EditText>
|
</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
|
<CheckBox
|
||||||
android:id="@+id/research_feedback_include_account_name"
|
android:id="@+id/research_feedback_include_account_name"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
android:layout_marginLeft="16dip"
|
||||||
|
android:layout_marginRight="16dip"
|
||||||
android:layout_marginBottom="8dip"
|
android:layout_marginBottom="8dip"
|
||||||
android:checked="false"
|
android:checked="false"
|
||||||
android:text="@string/research_feedback_include_account_name_label"
|
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_marginLeft="16dip"
|
||||||
|
android:layout_marginRight="16dip"
|
||||||
|
android:layout_marginBottom="8dip"
|
||||||
|
android:checked="false"
|
||||||
|
android:text="@string/research_feedback_include_recording_label" />
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:divider="?android:attr/dividerHorizontal"
|
android:divider="?android:attr/dividerHorizontal"
|
||||||
android:showDividers="beginning"
|
android:showDividers="beginning"
|
||||||
android:dividerPadding="0dip"
|
android:dividerPadding="0dip">
|
||||||
>
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
style="?android:attr/buttonBarStyle"
|
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="horizontal"
|
||||||
android:measureWithLargestChild="true"
|
android:layoutDirection="locale"
|
||||||
>
|
android:measureWithLargestChild="true">
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/research_feedback_cancel_button"
|
android:id="@+id/research_feedback_cancel_button"
|
||||||
android:layout_width="0dip"
|
android:layout_width="wrap_content"
|
||||||
android:layout_gravity="left"
|
android:layout_gravity="left"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:maxLines="2"
|
android:maxLines="2"
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:text="@string/research_feedback_cancel"
|
android:text="@string/research_feedback_cancel"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content" />
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/research_feedback_send_button"
|
android:id="@+id/research_feedback_send_button"
|
||||||
android:layout_width="0dip"
|
android:layout_width="wrap_content"
|
||||||
android:layout_gravity="right"
|
android:layout_gravity="right"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:maxLines="2"
|
android:maxLines="2"
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:text="@string/research_feedback_send"
|
android:text="@string/research_feedback_send"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content" />
|
||||||
/>
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</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] -->
|
<!-- 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>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
if (mFeedbackDialogBundle != null) {
|
||||||
|
mFeedbackDialogBundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, true);
|
||||||
}
|
}
|
||||||
|
clearRecordingTimer();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue