Merge "Save bugreport info on share intent."

This commit is contained in:
Felipe Leme
2016-01-13 21:23:09 +00:00
committed by Android (Google) Code Review
3 changed files with 219 additions and 21 deletions

View File

@@ -65,6 +65,7 @@ import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemProperties;
import android.os.Vibrator;
@@ -108,7 +109,7 @@ import android.widget.Toast;
* </ol>
*/
public class BugreportProgressService extends Service {
static final String TAG = "Shell";
private static final String TAG = "BugreportProgressService";
private static final boolean DEBUG = false;
private static final String AUTHORITY = "com.android.shell";
@@ -137,6 +138,7 @@ public class BugreportProgressService extends Service {
static final String EXTRA_TITLE = "android.intent.extra.TITLE";
static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION";
static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT";
static final String EXTRA_INFO = "android.intent.extra.INFO";
private static final int MSG_SERVICE_COMMAND = 1;
private static final int MSG_POLL = 2;
@@ -325,7 +327,7 @@ public class BugreportProgressService extends Service {
takeScreenshot(pid, true);
break;
case INTENT_BUGREPORT_SHARE:
shareBugreport(pid);
shareBugreport(pid, (BugreportInfo) intent.getParcelableExtra(EXTRA_INFO));
break;
case INTENT_BUGREPORT_CANCEL:
cancel(pid);
@@ -483,6 +485,7 @@ public class BugreportProgressService extends Service {
if (mProcesses.indexOfKey(pid) < 0) {
Log.w(TAG, "PID not watched: " + pid);
} else {
Log.d(TAG, "Removing PID " + pid);
mProcesses.remove(pid);
}
stopSelfWhenDone();
@@ -675,6 +678,11 @@ public class BugreportProgressService extends Service {
final int msgId;
if (taken) {
info.addScreenshot(screenshotFile);
if (info.finished) {
Log.d(TAG, "Screenshot finished after bugreport; updating share notification");
info.renameScreenshots(mScreenshotsDir);
sendBugreportNotification(mContext, info);
}
msgId = R.string.bugreport_screenshot_taken;
} else {
// TODO: try again using Framework APIs instead of relying on screencap.
@@ -814,12 +822,13 @@ public class BugreportProgressService extends Service {
* Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE}
* intent, but issuing a warning dialog the first time.
*/
private void shareBugreport(int pid) {
final BugreportInfo info = getInfo(pid);
private void shareBugreport(int pid, BugreportInfo sharedInfo) {
BugreportInfo info = getInfo(pid);
if (info == null) {
// Should not happen, so log if it does...
Log.e(TAG, "INTERNAL ERROR: no info for PID " + pid + ": " + mProcesses);
return;
// Service was terminated but notification persisted
info = sharedInfo;
Log.d(TAG, "shareBugreport(): no info for PID " + pid + " on managed processes ("
+ mProcesses + "), using info from intent instead (" + info + ")");
}
addDetailsToZipFile(info);
@@ -850,6 +859,7 @@ public class BugreportProgressService extends Service {
shareIntent.setClass(context, BugreportProgressService.class);
shareIntent.setAction(INTENT_BUGREPORT_SHARE);
shareIntent.putExtra(EXTRA_PID, info.pid);
shareIntent.putExtra(EXTRA_INFO, info);
final String title = context.getString(R.string.bugreport_finished_title);
final Notification.Builder builder = new Notification.Builder(context)
@@ -919,6 +929,11 @@ public class BugreportProgressService extends Service {
* description will be saved on {@code description.txt}.
*/
private void addDetailsToZipFile(BugreportInfo info) {
if (info.bugreportFile == null) {
// One possible reason is a bug in the Parcelization code.
Log.e(TAG, "INTERNAL ERROR: no bugreportFile on " + info);
return;
}
// It's not possible to add a new entry into an existing file, so we need to create a new
// zip, copy all entries, then rename it.
final File dir = info.bugreportFile.getParentFile();
@@ -1281,7 +1296,7 @@ public class BugreportProgressService extends Service {
/**
* Information about a bugreport process while its in progress.
*/
private static final class BugreportInfo {
private static final class BugreportInfo implements Parcelable {
private final Context context;
/**
@@ -1324,6 +1339,11 @@ public class BugreportProgressService extends Service {
*/
long lastUpdate = System.currentTimeMillis();
/**
* Time of the last progress update when Parcel was created.
*/
String formattedLastUpdate;
/**
* Path of the main bugreport file.
*/
@@ -1403,6 +1423,11 @@ public class BugreportProgressService extends Service {
}
String getFormattedLastUpdate() {
if (context == null) {
// Restored from Parcel
return formattedLastUpdate == null ?
Long.toString(lastUpdate) : formattedLastUpdate;
}
return DateUtils.formatDateTime(context, lastUpdate,
DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
}
@@ -1416,5 +1441,74 @@ public class BugreportProgressService extends Service {
+ "\n\tprogress: " + progress + "/" + max + "(" + percent + ")"
+ "\n\tlast_update: " + getFormattedLastUpdate();
}
// Parcelable contract
protected BugreportInfo(Parcel in) {
context = null;
pid = in.readInt();
name = in.readString();
title = in.readString();
description = in.readString();
max = in.readInt();
progress = in.readInt();
lastUpdate = in.readLong();
formattedLastUpdate = in.readString();
bugreportFile = readFile(in);
int screenshotSize = in.readInt();
for (int i = 1; i <= screenshotSize; i++) {
screenshotFiles.add(readFile(in));
}
finished = in.readInt() == 1;
screenshotCounter = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(pid);
dest.writeString(name);
dest.writeString(title);
dest.writeString(description);
dest.writeInt(max);
dest.writeInt(progress);
dest.writeLong(lastUpdate);
dest.writeString(getFormattedLastUpdate());
writeFile(dest, bugreportFile);
dest.writeInt(screenshotFiles.size());
for (File screenshotFile : screenshotFiles) {
writeFile(dest, screenshotFile);
}
dest.writeInt(finished ? 1 : 0);
dest.writeInt(screenshotCounter);
}
@Override
public int describeContents() {
return 0;
}
private void writeFile(Parcel dest, File file) {
dest.writeString(file == null ? null : file.getPath());
}
private File readFile(Parcel in) {
final String path = in.readString();
return path == null ? null : new File(path);
}
public static final Parcelable.Creator<BugreportInfo> CREATOR =
new Parcelable.Creator<BugreportInfo>() {
public BugreportInfo createFromParcel(Parcel source) {
return new BugreportInfo(source);
}
public BugreportInfo[] newArray(int size) {
return new BugreportInfo[size];
}
};
}
}

View File

@@ -19,7 +19,6 @@ package com.android.shell;
import static com.android.shell.BugreportProgressService.EXTRA_BUGREPORT;
import static com.android.shell.BugreportProgressService.EXTRA_ORIGINAL_INTENT;
import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_FINISHED;
import static com.android.shell.BugreportProgressService.TAG;
import static com.android.shell.BugreportProgressService.getFileExtra;
import java.io.File;
@@ -37,6 +36,7 @@ import android.util.Log;
* {@link Intent#ACTION_SEND_MULTIPLE}.
*/
public class BugreportReceiver extends BroadcastReceiver {
private static final String TAG = "BugreportReceiver";
/**
* Always keep the newest 8 bugreport files; 4 reports and 4 screenshots are
@@ -74,7 +74,11 @@ public class BugreportReceiver extends BroadcastReceiver {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
FileUtils.deleteOlderFiles(bugreportFile.getParentFile(), minCount, minAge);
try {
FileUtils.deleteOlderFiles(bugreportFile.getParentFile(), minCount, minAge);
} catch (RuntimeException e) {
Log.e(TAG, "RuntimeException deleting old files", e);
}
result.finish();
return null;
}

View File

@@ -65,6 +65,7 @@ import android.text.format.DateUtils;
import android.util.Log;
import com.android.shell.ActionSendMultipleConsumerActivity.CustomActionSendMultipleListener;
import com.android.shell.BugreportProgressService;
/**
* Integration tests for {@link BugreportReceiver}.
@@ -89,6 +90,9 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
// Timeout for UI operations, in milliseconds.
private static final int TIMEOUT = (int) BugreportProgressService.POLLING_FREQUENCY * 4;
// Timeout for when waiting for a screenshot to finish.
private static final int SAFE_SCREENSHOT_DELAY = SCREENSHOT_DELAY_SECONDS + 10;
private static final String BUGREPORTS_DIR = "bugreports";
private static final String BUGREPORT_FILE = "test_bugreport.txt";
private static final String ZIP_FILE = "test_bugreport.zip";
@@ -109,6 +113,7 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
private static final String NO_NAME = null;
private static final String NO_SCREENSHOT = null;
private static final String NO_TITLE = null;
private static final Integer NO_PID = null;
private String mDescription;
@@ -122,6 +127,7 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
Log.i(TAG, "#### setup() on " + getName());
Instrumentation instrumentation = getInstrumentation();
mContext = instrumentation.getTargetContext();
mUiBot = new UiBot(UiDevice.getInstance(instrumentation), TIMEOUT);
@@ -164,13 +170,21 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
Bundle extras =
sendBugreportFinishedAndGetSharedIntent(PID, mPlainTextPath, mScreenshotPath);
assertActionSendMultiple(extras, BUGREPORT_FILE, BUGREPORT_CONTENT, PID, ZIP_FILE,
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, PID, ZIP_FILE,
NAME, NO_TITLE, NO_DESCRIPTION, 1, true);
assertServiceNotRunning();
}
public void testProgress_takeExtraScreenshot() throws Exception {
takeExtraScreenshotTest(false);
}
public void testProgress_takeExtraScreenshotServiceDiesAfterScreenshotTaken() throws Exception {
takeExtraScreenshotTest(true);
}
private void takeExtraScreenshotTest(boolean serviceDies) throws Exception {
resetProperties();
sendBugreportStarted(1000);
@@ -179,14 +193,49 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
assertScreenshotButtonEnabled(false);
waitForScreenshotButtonEnabled(true);
Bundle extras =
sendBugreportFinishedAndGetSharedIntent(PID, mPlainTextPath, mScreenshotPath);
assertActionSendMultiple(extras, BUGREPORT_FILE, BUGREPORT_CONTENT, PID, ZIP_FILE,
sendBugreportFinished(PID, mPlainTextPath, mScreenshotPath);
if (serviceDies) {
waitShareNotification();
killService();
}
Bundle extras = acceptBugreportAndGetSharedIntent();
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, PID, ZIP_FILE,
NAME, NO_TITLE, NO_DESCRIPTION, 2, true);
assertServiceNotRunning();
}
public void testScreenshotFinishesAfterBugreport() throws Exception {
screenshotFinishesAfterBugreportTest(false);
}
public void testScreenshotFinishesAfterBugreportAndServiceDiesBeforeSharing() throws Exception {
screenshotFinishesAfterBugreportTest(true);
}
private void screenshotFinishesAfterBugreportTest(boolean serviceDies) throws Exception {
resetProperties();
sendBugreportStarted(1000);
sendBugreportFinished(PID, mPlainTextPath, NO_SCREENSHOT);
waitShareNotification();
// There's no indication in the UI about the screenshot finish, so just sleep like a baby...
Thread.sleep(SAFE_SCREENSHOT_DELAY * DateUtils.SECOND_IN_MILLIS);
if (serviceDies) {
killService();
}
Bundle extras = acceptBugreportAndGetSharedIntent();
assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT, PID, ZIP_FILE,
NAME, NO_TITLE, NO_DESCRIPTION, 1, true);
assertServiceNotRunning();
}
public void testProgress_changeDetailsInvalidInput() throws Exception {
resetProperties();
@@ -227,7 +276,7 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
Bundle extras = sendBugreportFinishedAndGetSharedIntent(PID, mPlainTextPath,
mScreenshotPath);
assertActionSendMultiple(extras, BUGREPORT_FILE, BUGREPORT_CONTENT, PID, TITLE,
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, PID, TITLE,
NEW_NAME, TITLE, mDescription, 1, true);
assertServiceNotRunning();
@@ -266,7 +315,7 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
Bundle extras = sendBugreportFinishedAndGetSharedIntent(PID,
plainText? mPlainTextPath : mZipPath, mScreenshotPath);
assertActionSendMultiple(extras, BUGREPORT_FILE, BUGREPORT_CONTENT, PID, TITLE,
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, PID, TITLE,
NEW_NAME, TITLE, mDescription, 1, true);
assertServiceNotRunning();
@@ -317,8 +366,8 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
// Finally, share bugreport.
Bundle extras = acceptBugreportAndGetSharedIntent();
assertActionSendMultiple(extras, BUGREPORT_FILE, BUGREPORT_CONTENT, PID, TITLE,
NAME, TITLE, mDescription, 1, waitScreenshot);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, PID, TITLE,
NAME, TITLE, mDescription, 1, true);
assertServiceNotRunning();
}
@@ -328,7 +377,7 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
BugreportPrefs.setWarningState(mContext, BugreportPrefs.STATE_SHOW);
// Send notification and click on share.
sendBugreportFinished(null, mPlainTextPath, null);
sendBugreportFinished(NO_PID, mPlainTextPath, null);
acceptBugreport();
// Handle the warning
@@ -350,6 +399,13 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
assertEquals("Didn't change state", BugreportPrefs.STATE_HIDE, newState);
}
public void testShareBugreportAfterServiceDies() throws Exception {
sendBugreportFinished(NO_PID, mPlainTextPath, NO_SCREENSHOT);
killService();
Bundle extras = acceptBugreportAndGetSharedIntent();
assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT);
}
public void testBugreportFinished_plainBugreportAndScreenshot() throws Exception {
Bundle extras = sendBugreportFinishedAndGetSharedIntent(mPlainTextPath, mScreenshotPath);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT);
@@ -443,6 +499,13 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
return mListener.getExtras();
}
/**
* Waits for the notification to share the finished bugreport.
*/
private void waitShareNotification() {
mUiBot.getNotification(mContext.getString(R.string.bugreport_finished_title));
}
/**
* Accepts the notification to share the finished bugreport.
*/
@@ -512,7 +575,8 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
expectedNumberScreenshots ++; // Add screenshot received by dumpstate
}
int expectedSize = expectedNumberScreenshots + 1; // All screenshots plus the bugreport file
assertEquals("wrong number of attachments", expectedSize, attachments.size());
assertEquals("wrong number of attachments (" + attachments + ")",
expectedSize, attachments.size());
// Need to interact through all attachments, since order is not guaranteed.
Uri zipUri = null;
@@ -607,6 +671,15 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
assertFalse("Service '" + service + "' is still running", isServiceRunning(service));
}
private void killService() {
waitForService(true);
Log.v(TAG, "Stopping service");
boolean stopped = mContext.stopService(new Intent(mContext, BugreportProgressService.class));
Log.d(TAG, "stopService returned " + stopped);
waitForService(false);
assertServiceNotRunning(); // Sanity check.
}
private boolean isServiceRunning(String name) {
ActivityManager manager = (ActivityManager) mContext
.getSystemService(Context.ACTIVITY_SERVICE);
@@ -618,6 +691,33 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
return false;
}
private void waitForService(boolean expectRunning) {
String service = BugreportProgressService.class.getName();
boolean actualRunning;
for (int i = 1; i <= 5; i++) {
actualRunning = isServiceRunning(service);
Log.d(TAG, "Attempt " + i + " to check status of service '"
+ service + "': expected=" + expectRunning + ", actual= " + actualRunning);
if (actualRunning == expectRunning) {
return;
}
try {
Thread.sleep(DateUtils.SECOND_IN_MILLIS);
} catch (InterruptedException e) {
Log.w(TAG, "thread interrupted");
Thread.currentThread().interrupt();
}
}
if (!expectRunning) {
// Typically happens when service is waiting for a screenshot to finish.
Log.w(TAG, "Service didn't stop; try to kill it again");
killService();
return;
}
fail("Service status didn't change to " + expectRunning);
}
private static void createTextFile(String path, String content) throws IOException {
Log.v(TAG, "createFile(" + path + ")");
try (Writer writer = new BufferedWriter(new OutputStreamWriter(
@@ -669,7 +769,7 @@ public class BugreportReceiverTest extends InstrumentationTestCase {
private UiObject waitForScreenshotButtonEnabled(boolean expectedEnabled) throws Exception {
UiObject screenshotButton = getScreenshotButton();
int maxAttempts = SCREENSHOT_DELAY_SECONDS + 5;
int maxAttempts = SAFE_SCREENSHOT_DELAY;
int i = 0;
do {
boolean enabled = screenshotButton.isEnabled();