+ * Once initially set, it's only updated when user clicks the OK button. + */ + private String mSavedName; + + /** + * Last value of the bugreport name as entered by the user. + *
+ * Every time it's changed the equivalent system property is changed as well, but if the + * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored. + *
+ * This logic handles the corner-case scenario where {@code dumpstate} finishes after the + * user changed the name but didn't clicked OK yet (for example, because the user is typing + * the description). The only drawback is that if the user changes the name while + * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name + * will be the one that has been canceled. But when {@code dumpstate} finishes the {code + * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of + * such drawback. + */ + private String mTempName; + + /** + * Sets its internal state and displays the dialog. + */ + private synchronized void initialize(Context context, int pid, String name, String title, + String description) { + // First initializes singleton. + if (mDialog == null) { + @SuppressLint("InflateParams") + // It's ok pass null ViewRoot on AlertDialogs. + final View view = View.inflate(context, R.layout.dialog_bugreport_info, null); + + mInfoName = (EditText) view.findViewById(R.id.name); + mInfoTitle = (EditText) view.findViewById(R.id.title); + mInfoDescription = (EditText) view.findViewById(R.id.description); + + mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() { + + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + return; + } + sanitizeName(); + } + }); + + mDialog = new AlertDialog.Builder(context) + .setView(view) + .setTitle(context.getString(R.string.bugreport_info_dialog_title)) + .setCancelable(false) + .setPositiveButton(context.getString(com.android.internal.R.string.ok), + null) + .setNegativeButton(context.getString(com.android.internal.R.string.cancel), + new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int id) + { + if (!mTempName.equals(mSavedName)) { + // Must restore dumpstate's name since it was changed + // before user clicked OK. + setBugreportNameProperty(mPid, mSavedName); + } + } + }) + .create(); + + mDialog.getWindow().setAttributes( + new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG)); + + } + + // Then set fields. + mSavedName = mTempName = name; + mPid = pid; + if (!TextUtils.isEmpty(name)) { + mInfoName.setText(name); + } + if (!TextUtils.isEmpty(title)) { + mInfoTitle.setText(title); + } + if (!TextUtils.isEmpty(description)) { + mInfoDescription.setText(description); + } + + // And finally display it. + mDialog.show(); + + // TODO: in a traditional AlertDialog, when the positive button is clicked the + // dialog is always closed, but we need to validate the name first, so we need to + // get a reference to it, which is only available after it's displayed. + // It would be cleaner to use a regular dialog instead, but let's keep this + // workaround for now and change it later, when we add another button to take + // extra screenshots. + if (mOkButton == null) { + mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); + mOkButton.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View view) { + sanitizeName(); + final String name = mInfoName.getText().toString(); + final String title = mInfoTitle.getText().toString(); + final String description = mInfoDescription.getText().toString(); + + updateBugreportInfo(mPid, name, title, description); + mDialog.dismiss(); + } + }); + } + } + + /** + * Sanitizes the user-provided value for the {@code name} field, automatically replacing + * invalid characters if necessary. + */ + private synchronized void sanitizeName() { + String name = mInfoName.getText().toString(); + if (name.equals(mTempName)) { + if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name); + return; + } + final StringBuilder safeName = new StringBuilder(name.length()); + boolean changed = false; + for (int i = 0; i < name.length(); i++) { + final char c = name.charAt(i); + if (isValid(c)) { + safeName.append(c); + } else { + changed = true; + safeName.append('_'); + } + } + if (changed) { + Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'"); + name = safeName.toString(); + mInfoName.setText(name); + } + mTempName = name; + + // Must update system property for the cases where dumpstate finishes + // while the user is still entering other fields (like title or + // description) + setBugreportNameProperty(mPid, name); + } + + /** + * Notifies the dialog that the bugreport has finished so it disables the {@code name} + * field. + *
Once the bugreport is finished dumpstate has already generated the final files, so
+ * changing the name would have no effect.
+ */
+ private synchronized void onBugreportFinished(int pid) {
+ if (mInfoName != null) {
+ mInfoName.setEnabled(false);
+ mInfoName.setText(mSavedName);
+ }
+ }
+
+ }
+
/**
* Information about a bugreport process while its in progress.
*/
@@ -703,6 +1001,18 @@ public class BugreportProgressService extends Service {
*/
String name;
+ /**
+ * User-provided, one-line summary of the bug; when set, will be used as the subject
+ * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
+ */
+ String title;
+
+ /**
+ * User-provided, detailed description of the bugreport; when set, will be added to the body
+ * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
+ */
+ String description;
+
/**
* Maximum progress of the bugreport generation.
*/
@@ -761,6 +1071,7 @@ public class BugreportProgressService extends Service {
public String toString() {
final float percent = ((float) progress * 100 / max);
return "pid: " + pid + ", name: " + name + ", finished: " + finished
+ + "\n\ttitle: " + title + "\n\tdescription: " + description
+ "\n\tfile: " + bugreportFile + "\n\tscreenshot: " + screenshotFile
+ "\n\tprogress: " + progress + "/" + max + "(" + percent + ")"
+ "\n\tlast_update: " + getFormattedLastUpdate();
diff --git a/packages/Shell/tests/Android.mk b/packages/Shell/tests/Android.mk
index 62a37bc70f4ee..1e0eaace35dac 100644
--- a/packages/Shell/tests/Android.mk
+++ b/packages/Shell/tests/Android.mk
@@ -8,9 +8,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_JAVA_LIBRARIES := android.test.runner
-# TODO: update and/or remove
LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator
-#LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 mockito-target ub-uiautomator
LOCAL_PACKAGE_NAME := ShellTests
LOCAL_INSTRUMENTATION_FOR := Shell
diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
index 1f4d749f39928..7f609faac83d7 100644
--- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
+++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
@@ -94,7 +94,11 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
private static final int PID = 42;
private static final String PROGRESS_PROPERTY = "dumpstate.42.progress";
private static final String MAX_PROPERTY = "dumpstate.42.max";
+ private static final String NAME_PROPERTY = "dumpstate.42.name";
private static final String NAME = "BUG, Y U NO REPORT?";
+ private static final String NEW_NAME = "Bug_Forrest_Bug";
+ private static final String TITLE = "Wimbugdom Champion 2015";
+ private String mDescription;
private String mPlainTextPath;
private String mZipPath;
@@ -120,10 +124,17 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
createTextFile(mScreenshotPath, SCREENSHOT_CONTENT);
createZipFile(mZipPath, BUGREPORT_FILE, BUGREPORT_CONTENT);
+ // Creates a multi-line description.
+ StringBuilder sb = new StringBuilder();
+ for (int i = 1; i <= 20; i++) {
+ sb.append("All work and no play makes Shell a dull app!\n");
+ }
+ mDescription = sb.toString();
+
BugreportPrefs.setWarningState(mContext, BugreportPrefs.STATE_HIDE);
}
- public void testFullWorkflow() throws Exception {
+ public void testProgress() throws Exception {
resetProperties();
sendBugreportStarted(1000);
@@ -145,6 +156,81 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
assertServiceNotRunning();
}
+ public void testProgress_changeDetails() throws Exception {
+ resetProperties();
+ sendBugreportStarted(1000);
+
+ DetailsUi detailsUi = new DetailsUi(mUiBot);
+
+ // Check initial name.
+ String actualName = detailsUi.nameField.getText().toString();
+ assertEquals("Wrong value on field 'name'", NAME, actualName);
+
+ // Change name - it should have changed system property once focus is changed.
+ detailsUi.nameField.setText(NEW_NAME);
+ detailsUi.focusAwayFromName();
+ assertPropertyValue(NAME_PROPERTY, NEW_NAME);
+
+ // Cancel the dialog to make sure property was restored.
+ detailsUi.clickCancel();
+ assertPropertyValue(NAME_PROPERTY, NAME);
+
+ // Now try to set an invalid name.
+ detailsUi.reOpen();
+ detailsUi.nameField.setText("/etc/passwd");
+ detailsUi.clickOk();
+ assertPropertyValue(NAME_PROPERTY, "_etc_passwd");
+
+ // Finally, make the real changes.
+ detailsUi.reOpen();
+ detailsUi.nameField.setText(NEW_NAME);
+ detailsUi.titleField.setText(TITLE);
+ detailsUi.descField.setText(mDescription);
+
+ detailsUi.clickOk();
+
+ assertPropertyValue(NAME_PROPERTY, NEW_NAME);
+ assertProgressNotification(NEW_NAME, "0.00%");
+
+ Bundle extras = sendBugreportFinishedAndGetSharedIntent(PID, mPlainTextPath,
+ mScreenshotPath);
+ assertActionSendMultiple(extras, TITLE, mDescription, BUGREPORT_CONTENT, SCREENSHOT_CONTENT);
+
+ assertServiceNotRunning();
+ }
+
+ public void testProgress_bugreportFinishedWhileChangingDetails() throws Exception {
+ resetProperties();
+ sendBugreportStarted(1000);
+
+ DetailsUi detailsUi = new DetailsUi(mUiBot);
+
+ // Finish the bugreport while user's still typing the name.
+ detailsUi.nameField.setText(NEW_NAME);
+ sendBugreportFinished(PID, mPlainTextPath, mScreenshotPath);
+
+ // Wait until the share notifcation is received...
+ mUiBot.getNotification(mContext.getString(R.string.bugreport_finished_title));
+ // ...then close notification bar.
+ mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+
+ // Make sure UI was updated properly.
+ assertFalse("didn't disable name on UI", detailsUi.nameField.isEnabled());
+ assertEquals("didn't revert name on UI", NAME, detailsUi.nameField.getText().toString());
+
+ // Finish changing other fields.
+ detailsUi.titleField.setText(TITLE);
+ detailsUi.descField.setText(mDescription);
+ detailsUi.clickOk();
+
+ // Finally, share bugreport.
+ Bundle extras = acceptBugreportAndGetSharedIntent();
+ assertActionSendMultiple(extras, TITLE, mDescription, BUGREPORT_CONTENT,
+ SCREENSHOT_CONTENT);
+
+ assertServiceNotRunning();
+ }
+
public void testBugreportFinished_withWarning() throws Exception {
// Explicitly shows the warning.
BugreportPrefs.setWarningState(mContext, BugreportPrefs.STATE_SHOW);
@@ -204,14 +290,18 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
private void assertProgressNotification(String name, String percent) {
// TODO: it current looks for 3 distinct objects, without taking advantage of their
// relationship.
- String title = mContext.getString(R.string.bugreport_in_progress_title);
- Log.v(TAG, "Looking for progress notification title: '" + title+ "'");
- mUiBot.getNotification(title);
+ openProgressNotification();
Log.v(TAG, "Looking for progress notification details: '" + name + "-" + percent + "'");
mUiBot.getObject(name);
mUiBot.getObject(percent);
}
+ private void openProgressNotification() {
+ String title = mContext.getString(R.string.bugreport_in_progress_title);
+ Log.v(TAG, "Looking for progress notification title: '" + title + "'");
+ mUiBot.getNotification(title);
+ }
+
void resetProperties() {
// TODO: call method to remove property instead
SystemProperties.set(PROGRESS_PROPERTY, "0");
@@ -270,7 +360,6 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
/**
* Sends a "bugreport finished" intent.
- *
*/
private void sendBugreportFinished(Integer pid, String bugreportPath, String screenshotPath) {
Intent intent = new Intent(INTENT_BUGREPORT_FINISHED);
@@ -292,13 +381,21 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
*/
private void assertActionSendMultiple(Bundle extras, String bugreportContent,
String screenshotContent) throws IOException {
+ assertActionSendMultiple(extras, ZIP_FILE, null, bugreportContent, screenshotContent);
+ }
+
+ private void assertActionSendMultiple(Bundle extras, String subject, String description,
+ String bugreportContent, String screenshotContent) throws IOException {
String body = extras.getString(Intent.EXTRA_TEXT);
assertContainsRegex("missing build info",
SystemProperties.get("ro.build.description"), body);
assertContainsRegex("missing serial number",
SystemProperties.get("ro.serialno"), body);
+ if (description != null) {
+ assertContainsRegex("missing description", description, body);
+ }
- assertEquals("wrong subject", ZIP_FILE, extras.getString(Intent.EXTRA_SUBJECT));
+ assertEquals("wrong subject", subject, extras.getString(Intent.EXTRA_SUBJECT));
List