blob: 44e956abeb02473c1ff7e8cb8304a404b9e727ba [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.shell;
import static android.test.MoreAsserts.assertContainsRegex;
import static com.android.shell.ActionSendMultipleConsumerActivity.UI_NAME;
import static com.android.shell.BugreportPrefs.PREFS_BUGREPORT;
import static com.android.shell.BugreportPrefs.STATE_HIDE;
import static com.android.shell.BugreportPrefs.STATE_SHOW;
import static com.android.shell.BugreportPrefs.STATE_UNKNOWN;
import static com.android.shell.BugreportPrefs.getWarningState;
import static com.android.shell.BugreportPrefs.setWarningState;
import static com.android.shell.BugreportProgressService.EXTRA_BUGREPORT;
import static com.android.shell.BugreportProgressService.EXTRA_ID;
import static com.android.shell.BugreportProgressService.EXTRA_MAX;
import static com.android.shell.BugreportProgressService.EXTRA_NAME;
import static com.android.shell.BugreportProgressService.EXTRA_PID;
import static com.android.shell.BugreportProgressService.EXTRA_SCREENSHOT;
import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_FINISHED;
import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_STARTED;
import static com.android.shell.BugreportProgressService.POLLING_FREQUENCY;
import static com.android.shell.BugreportProgressService.SCREENSHOT_DELAY_SECONDS;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import libcore.io.Streams;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.app.Instrumentation;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemProperties;
import android.service.notification.StatusBarNotification;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
import android.support.test.uiautomator.UiObjectNotFoundException;
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.LargeTest;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import com.android.shell.ActionSendMultipleConsumerActivity.CustomActionSendMultipleListener;
/**
* Integration tests for {@link BugreportReceiver}.
* <p>
* These tests don't mock any component and rely on external UI components (like the notification
* bar and activity chooser), which can make them unreliable and slow.
* <p>
* The general workflow is:
* <ul>
* <li>creates the bug report files
* <li>generates the BUGREPORT_FINISHED intent
* <li>emulate user actions to share the intent with a custom activity
* <li>asserts the extras received by the custom activity
* </ul>
* <p>
* <strong>NOTE</strong>: these tests only work if the device is unlocked.
*/
@LargeTest
public class BugreportReceiverTest extends InstrumentationTestCase {
private static final String TAG = "BugreportReceiverTest";
// Timeout for UI operations, in milliseconds.
private static final int TIMEOUT = (int) 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";
private static final String ZIP_FILE2 = "test_bugreport2.zip";
private static final String SCREENSHOT_FILE = "test_screenshot.png";
private static final String BUGREPORT_CONTENT = "Dump, might as well dump!\n";
private static final String SCREENSHOT_CONTENT = "A picture is worth a thousand words!\n";
private static final int PID = 42;
private static final int PID2 = 24;
private static final int ID = 108;
private static final int ID2 = 801;
private static final String PROGRESS_PROPERTY = "dumpstate." + PID + ".progress";
private static final String MAX_PROPERTY = "dumpstate." + PID + ".max";
private static final String NAME_PROPERTY = "dumpstate." + PID + ".name";
private static final String NAME = "BUG, Y U NO REPORT?";
private static final String NAME2 = "A bugreport's life";
private static final String NEW_NAME = "Bug_Forrest_Bug";
private static final String NEW_NAME2 = "BugsyReportsy";
private static final String TITLE = "Wimbugdom Champion 2015";
private static final String TITLE2 = "Master of the Universe";
private static final String DESCRIPTION = "One's description...";
private static final String DESCRIPTION2 = "...is another's treasure.";
private static final String NO_DESCRIPTION = null;
private static final String NO_NAME = null;
private static final String NO_SCREENSHOT = null;
private static final String NO_TITLE = null;
private static final int NO_ID = 0;
private static final boolean RENAMED_SCREENSHOTS = true;
private static final boolean DIDNT_RENAME_SCREENSHOTS = false;
private String mDescription;
private String mPlainTextPath;
private String mZipPath;
private String mZipPath2;
private String mScreenshotPath;
private Context mContext;
private UiBot mUiBot;
private CustomActionSendMultipleListener mListener;
@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);
mListener = ActionSendMultipleConsumerActivity.getListener(mContext);
cancelExistingNotifications();
mPlainTextPath = getPath(BUGREPORT_FILE);
mZipPath = getPath(ZIP_FILE);
mZipPath2 = getPath(ZIP_FILE2);
mScreenshotPath = getPath(SCREENSHOT_FILE);
createTextFile(mPlainTextPath, BUGREPORT_CONTENT);
createTextFile(mScreenshotPath, SCREENSHOT_CONTENT);
createZipFile(mZipPath, BUGREPORT_FILE, BUGREPORT_CONTENT);
createZipFile(mZipPath2, 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();
setWarningState(mContext, STATE_HIDE);
}
public void testProgress() throws Exception {
resetProperties();
sendBugreportStarted(1000);
waitForScreenshotButtonEnabled(true);
assertProgressNotification(NAME, 0f);
SystemProperties.set(PROGRESS_PROPERTY, "108");
assertProgressNotification(NAME, 10.80f);
assertProgressNotification(NAME, 50.00f);
SystemProperties.set(PROGRESS_PROPERTY, "950");
assertProgressNotification(NAME, 95.00f);
// Make sure progress never goes back...
SystemProperties.set(MAX_PROPERTY, "2000");
Thread.sleep(POLLING_FREQUENCY + DateUtils.SECOND_IN_MILLIS);
assertProgressNotification(NAME, 95.00f);
SystemProperties.set(PROGRESS_PROPERTY, "1000");
assertProgressNotification(NAME, 95.00f);
// ...only forward...
SystemProperties.set(PROGRESS_PROPERTY, "1902");
assertProgressNotification(NAME, 95.10f);
SystemProperties.set(PROGRESS_PROPERTY, "1960");
assertProgressNotification(NAME, 98.00f);
// ...but never more than the capped value.
SystemProperties.set(PROGRESS_PROPERTY, "2000");
assertProgressNotification(NAME, 99.00f);
SystemProperties.set(PROGRESS_PROPERTY, "3000");
assertProgressNotification(NAME, 99.00f);
Bundle extras =
sendBugreportFinishedAndGetSharedIntent(ID, mPlainTextPath, mScreenshotPath);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, ZIP_FILE,
NAME, NO_TITLE, NO_DESCRIPTION, 0, RENAMED_SCREENSHOTS);
assertServiceNotRunning();
}
public void testProgress_cancel() throws Exception {
resetProperties();
sendBugreportStarted(1000);
waitForScreenshotButtonEnabled(true);
final NumberFormat nf = NumberFormat.getPercentInstance();
nf.setMinimumFractionDigits(2);
nf.setMaximumFractionDigits(2);
assertProgressNotification(NAME, 00.00f);
openProgressNotification(ID);
UiObject cancelButton = mUiBot.getVisibleObject(mContext.getString(
com.android.internal.R.string.cancel).toUpperCase());
mUiBot.click(cancelButton, "cancel_button");
waitForService(false);
}
public void testProgress_takeExtraScreenshot() throws Exception {
resetProperties();
sendBugreportStarted(1000);
waitForScreenshotButtonEnabled(true);
takeScreenshot();
assertScreenshotButtonEnabled(false);
waitForScreenshotButtonEnabled(true);
sendBugreportFinished(ID, mPlainTextPath, mScreenshotPath);
Bundle extras = acceptBugreportAndGetSharedIntent(ID);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, ZIP_FILE,
NAME, NO_TITLE, NO_DESCRIPTION, 1, RENAMED_SCREENSHOTS);
assertServiceNotRunning();
}
public void testScreenshotFinishesAfterBugreport() throws Exception {
resetProperties();
sendBugreportStarted(1000);
waitForScreenshotButtonEnabled(true);
takeScreenshot();
sendBugreportFinished(ID, mPlainTextPath, NO_SCREENSHOT);
waitShareNotification(ID);
// 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);
Bundle extras = acceptBugreportAndGetSharedIntent(ID);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT, ID, PID, ZIP_FILE,
NAME, NO_TITLE, NO_DESCRIPTION, 1, RENAMED_SCREENSHOTS);
assertServiceNotRunning();
}
public void testProgress_changeDetailsInvalidInput() throws Exception {
resetProperties();
sendBugreportStarted(1000);
waitForScreenshotButtonEnabled(true);
DetailsUi detailsUi = new DetailsUi(mUiBot, ID);
// Check initial name.
detailsUi.assertName(NAME);
// Change name - it should have changed system property once focus is changed.
detailsUi.focusOnName();
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, 00.00f);
Bundle extras = sendBugreportFinishedAndGetSharedIntent(ID, mPlainTextPath,
mScreenshotPath);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, TITLE,
NEW_NAME, TITLE, mDescription, 0, RENAMED_SCREENSHOTS);
assertServiceNotRunning();
}
public void testProgress_changeDetailsPlainBugreport() throws Exception {
changeDetailsTest(true);
}
public void testProgress_changeDetailsZippedBugreport() throws Exception {
changeDetailsTest(false);
}
public void changeDetailsTest(boolean plainText) throws Exception {
resetProperties();
sendBugreportStarted(1000);
waitForScreenshotButtonEnabled(true);
DetailsUi detailsUi = new DetailsUi(mUiBot, ID);
// Check initial name.
detailsUi.assertName(NAME);
// Change fields.
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, 00.00f);
Bundle extras = sendBugreportFinishedAndGetSharedIntent(ID,
plainText? mPlainTextPath : mZipPath, mScreenshotPath);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, TITLE,
NEW_NAME, TITLE, mDescription, 0, RENAMED_SCREENSHOTS);
assertServiceNotRunning();
}
public void testProgress_changeJustDetailsTouchingDetails() throws Exception {
changeJustDetailsTest(true);
}
public void testProgress_changeJustDetailsTouchingNotification() throws Exception {
changeJustDetailsTest(false);
}
private void changeJustDetailsTest(boolean touchDetails) throws Exception {
resetProperties();
sendBugreportStarted(1000);
waitForScreenshotButtonEnabled(true);
DetailsUi detailsUi = new DetailsUi(mUiBot, ID, touchDetails);
detailsUi.nameField.setText("");
detailsUi.titleField.setText("");
detailsUi.descField.setText(mDescription);
detailsUi.clickOk();
Bundle extras = sendBugreportFinishedAndGetSharedIntent(ID, mZipPath, mScreenshotPath);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, ZIP_FILE,
NO_NAME, NO_TITLE, mDescription, 0, DIDNT_RENAME_SCREENSHOTS);
assertServiceNotRunning();
}
/*
* TODO: this test can be flanky because it relies in the order the notifications are displayed,
* since mUiBot gets the first notification.
* Ideally, openProgressNotification() should return the whole notification, so DetailsUi
* could use it and find children instead, but unfortunately the notification object hierarchy
* is too complex and getting it from the notification text object would be to fragile
* (for instance, it could require navigating many parents up in the hierarchy).
*/
public void testProgress_changeJustDetailsIsClearedOnSecondBugreport() throws Exception {
resetProperties();
sendBugreportStarted(ID, PID, NAME, 1000);
waitForScreenshotButtonEnabled(true);
DetailsUi detailsUi = new DetailsUi(mUiBot, ID);
detailsUi.assertName(NAME);
detailsUi.assertTitle("");
detailsUi.assertDescription("");
detailsUi.nameField.setText(NEW_NAME);
detailsUi.titleField.setText(TITLE);
detailsUi.descField.setText(DESCRIPTION);
detailsUi.clickOk();
sendBugreportStarted(ID2, PID2, NAME2, 1000);
sendBugreportFinished(ID, mZipPath, mScreenshotPath);
Bundle extras = acceptBugreportAndGetSharedIntent(ID);
detailsUi = new DetailsUi(mUiBot, ID2);
detailsUi.assertName(NAME2);
detailsUi.assertTitle("");
detailsUi.assertDescription("");
detailsUi.nameField.setText(NEW_NAME2);
detailsUi.titleField.setText(TITLE2);
detailsUi.descField.setText(DESCRIPTION2);
detailsUi.clickOk();
// Must use a different zip file otherwise it will fail because zip already contains
// title.txt and description.txt entries.
extras = sendBugreportFinishedAndGetSharedIntent(ID2, mZipPath2, NO_SCREENSHOT);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT, ID2, PID2, TITLE2,
NEW_NAME2, TITLE2, DESCRIPTION2, 0, RENAMED_SCREENSHOTS);
assertServiceNotRunning();
}
/**
* Tests the scenario where the initial screenshot and dumpstate are finished while the user
* is changing the info in the details screen.
*/
public void testProgress_bugreportAndScreenshotFinishedWhileChangingDetails() throws Exception {
bugreportFinishedWhileChangingDetailsTest(false);
}
/**
* Tests the scenario where dumpstate is finished while the user is changing the info in the
* details screen, but the initial screenshot finishes afterwards.
*/
public void testProgress_bugreportFinishedWhileChangingDetails() throws Exception {
bugreportFinishedWhileChangingDetailsTest(true);
}
private void bugreportFinishedWhileChangingDetailsTest(boolean waitScreenshot) throws Exception {
resetProperties();
sendBugreportStarted(1000);
if (waitScreenshot) {
waitForScreenshotButtonEnabled(true);
}
DetailsUi detailsUi = new DetailsUi(mUiBot, ID);
// Finish the bugreport while user's still typing the name.
detailsUi.nameField.setText(NEW_NAME);
sendBugreportFinished(ID, mPlainTextPath, mScreenshotPath);
// Wait until the share notification is received...
waitShareNotification(ID);
// ...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(ID);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, TITLE,
NAME, TITLE, mDescription, 0, RENAMED_SCREENSHOTS);
assertServiceNotRunning();
}
public void testBugreportFinished_withWarningFirstTime() throws Exception {
bugreportFinishedWithWarningTest(null);
}
public void testBugreportFinished_withWarningUnknownState() throws Exception {
bugreportFinishedWithWarningTest(STATE_UNKNOWN);
}
public void testBugreportFinished_withWarningShowAgain() throws Exception {
bugreportFinishedWithWarningTest(STATE_SHOW);
}
private void bugreportFinishedWithWarningTest(Integer propertyState) throws Exception {
if (propertyState == null) {
// Clear properties
mContext.getSharedPreferences(PREFS_BUGREPORT, Context.MODE_PRIVATE)
.edit().clear().commit();
// Sanity check...
assertEquals("Did not reset properties", STATE_UNKNOWN,
getWarningState(mContext, STATE_UNKNOWN));
} else {
setWarningState(mContext, propertyState);
}
// Send notification and click on share.
sendBugreportFinished(NO_ID, mPlainTextPath, null);
acceptBugreport(NO_ID);
// Handle the warning
mUiBot.getVisibleObject(mContext.getString(R.string.bugreport_confirm));
// TODO: get ok and dontShowAgain from the dialog reference above
UiObject dontShowAgain =
mUiBot.getVisibleObject(mContext.getString(R.string.bugreport_confirm_dont_repeat));
final boolean firstTime = propertyState == null || propertyState == STATE_UNKNOWN;
if (firstTime) {
if (Build.TYPE.equals("user")) {
assertFalse("Checkbox should NOT be checked by default on user builds",
dontShowAgain.isChecked());
mUiBot.click(dontShowAgain, "dont-show-again");
} else {
assertTrue("Checkbox should be checked by default on build type " + Build.TYPE,
dontShowAgain.isChecked());
}
} else {
assertFalse("Checkbox should not be checked", dontShowAgain.isChecked());
mUiBot.click(dontShowAgain, "dont-show-again");
}
UiObject ok = mUiBot.getVisibleObject(mContext.getString(com.android.internal.R.string.ok));
mUiBot.click(ok, "ok");
// Share the bugreport.
mUiBot.chooseActivity(UI_NAME);
Bundle extras = mListener.getExtras();
assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT);
// Make sure it's hidden now.
int newState = getWarningState(mContext, STATE_UNKNOWN);
assertEquals("Didn't change state", STATE_HIDE, newState);
}
public void testShareBugreportAfterServiceDies() throws Exception {
sendBugreportFinished(NO_ID, mPlainTextPath, NO_SCREENSHOT);
waitForService(false);
Bundle extras = acceptBugreportAndGetSharedIntent(NO_ID);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT);
}
public void testBugreportFinished_plainBugreportAndScreenshot() throws Exception {
Bundle extras = sendBugreportFinishedAndGetSharedIntent(mPlainTextPath, mScreenshotPath);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT);
}
public void testBugreportFinished_zippedBugreportAndScreenshot() throws Exception {
Bundle extras = sendBugreportFinishedAndGetSharedIntent(mZipPath, mScreenshotPath);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT);
}
public void testBugreportFinished_plainBugreportAndNoScreenshot() throws Exception {
Bundle extras = sendBugreportFinishedAndGetSharedIntent(mPlainTextPath, NO_SCREENSHOT);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT);
}
public void testBugreportFinished_zippedBugreportAndNoScreenshot() throws Exception {
Bundle extras = sendBugreportFinishedAndGetSharedIntent(mZipPath, NO_SCREENSHOT);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT);
}
private void cancelExistingNotifications() {
NotificationManager nm = NotificationManager.from(mContext);
for (StatusBarNotification notification : nm.getActiveNotifications()) {
int id = notification.getId();
Log.i(TAG, "Canceling existing notification (id=" + id + ")");
nm.cancel(id);
}
}
private void assertProgressNotification(String name, float percent) {
// TODO: it currently looks for 3 distinct objects, without taking advantage of their
// relationship.
openProgressNotification(ID);
Log.v(TAG, "Looking for progress notification details: '" + name + "-" + percent + "'");
mUiBot.getObject(name);
// TODO: need a way to get the ProgresBar from the "android:id/progress" UIObject...
}
private UiObject openProgressNotification(int id) {
String title = mContext.getString(R.string.bugreport_in_progress_title, id);
Log.v(TAG, "Looking for progress notification title: '" + title + "'");
return mUiBot.getNotification(title);
}
void resetProperties() {
// TODO: call method to remove property instead
SystemProperties.set(PROGRESS_PROPERTY, "Reset");
SystemProperties.set(MAX_PROPERTY, "Reset");
SystemProperties.set(NAME_PROPERTY, "Reset");
}
/**
* Sends a "bugreport started" intent with the default values.
*/
private void sendBugreportStarted(int max) throws Exception {
sendBugreportStarted(ID, PID, NAME, max);
}
private void sendBugreportStarted(int id, int pid, String name, int max) throws Exception {
Intent intent = new Intent(INTENT_BUGREPORT_STARTED);
intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.putExtra(EXTRA_ID, id);
intent.putExtra(EXTRA_PID, pid);
intent.putExtra(EXTRA_NAME, name);
intent.putExtra(EXTRA_MAX, max);
mContext.sendBroadcast(intent);
}
/**
* Sends a "bugreport finished" intent and waits for the result.
*
* @return extras sent in the shared intent.
*/
private Bundle sendBugreportFinishedAndGetSharedIntent(String bugreportPath,
String screenshotPath) {
return sendBugreportFinishedAndGetSharedIntent(NO_ID, bugreportPath, screenshotPath);
}
/**
* Sends a "bugreport finished" intent and waits for the result.
*
* @return extras sent in the shared intent.
*/
private Bundle sendBugreportFinishedAndGetSharedIntent(int id, String bugreportPath,
String screenshotPath) {
sendBugreportFinished(id, bugreportPath, screenshotPath);
return acceptBugreportAndGetSharedIntent(id);
}
/**
* Accepts the notification to share the finished bugreport and waits for the result.
*
* @return extras sent in the shared intent.
*/
private Bundle acceptBugreportAndGetSharedIntent(int id) {
acceptBugreport(id);
mUiBot.chooseActivity(UI_NAME);
return mListener.getExtras();
}
/**
* Waits for the notification to share the finished bugreport.
*/
private void waitShareNotification(int id) {
mUiBot.getNotification(mContext.getString(R.string.bugreport_finished_title, id));
}
/**
* Accepts the notification to share the finished bugreport.
*/
private void acceptBugreport(int id) {
mUiBot.clickOnNotification(mContext.getString(R.string.bugreport_finished_title, id));
}
/**
* Sends a "bugreport finished" intent.
*/
private void sendBugreportFinished(int id, String bugreportPath, String screenshotPath) {
Intent intent = new Intent(INTENT_BUGREPORT_FINISHED);
intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
if (id != NO_ID) {
intent.putExtra(EXTRA_ID, id);
}
if (bugreportPath != null) {
intent.putExtra(EXTRA_BUGREPORT, bugreportPath);
}
if (screenshotPath != null) {
intent.putExtra(EXTRA_SCREENSHOT, screenshotPath);
}
mContext.sendBroadcast(intent);
}
/**
* Asserts the proper {@link Intent#ACTION_SEND_MULTIPLE} intent was sent.
*/
private void assertActionSendMultiple(Bundle extras, String bugreportContent,
String screenshotContent) throws IOException {
assertActionSendMultiple(extras, bugreportContent, screenshotContent, ID, PID, ZIP_FILE,
NO_NAME, NO_TITLE, NO_DESCRIPTION, 0, DIDNT_RENAME_SCREENSHOTS);
}
/**
* Asserts the proper {@link Intent#ACTION_SEND_MULTIPLE} intent was sent.
*
* @param extras extras received in the intent
* @param bugreportContent expected content in the bugreport file
* @param screenshotContent expected content in the screenshot file (sent by dumpstate), if any
* @param id emulated dumpstate id
* @param pid emulated dumpstate pid
* @param name expected subject
* @param name bugreport name as provided by the user (or received by dumpstate)
* @param title bugreport name as provided by the user
* @param description bugreport description as provided by the user
* @param numberScreenshots expected number of screenshots taken by Shell.
* @param renamedScreenshots whether the screenshots are expected to be renamed
*/
private void assertActionSendMultiple(Bundle extras, String bugreportContent,
String screenshotContent, int id, int pid, String subject,
String name, String title, String description,
int numberScreenshots, boolean renamedScreenshots) 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", subject, extras.getString(Intent.EXTRA_SUBJECT));
List<Uri> attachments = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
int expectedNumberScreenshots = numberScreenshots;
if (screenshotContent != null) {
expectedNumberScreenshots ++; // Add screenshot received by dumpstate
}
int expectedSize = expectedNumberScreenshots + 1; // All screenshots plus the bugreport file
assertEquals("wrong number of attachments (" + attachments + ")",
expectedSize, attachments.size());
// Need to interact through all attachments, since order is not guaranteed.
Uri zipUri = null;
List<Uri> screenshotUris = new ArrayList<>(expectedNumberScreenshots);
for (Uri attachment : attachments) {
if (attachment.getPath().endsWith(".zip")) {
zipUri = attachment;
}
if (attachment.getPath().endsWith(".png")) {
screenshotUris.add(attachment);
}
}
assertNotNull("did not get .zip attachment", zipUri);
assertZipContent(zipUri, BUGREPORT_FILE, BUGREPORT_CONTENT);
if (!TextUtils.isEmpty(title)) {
assertZipContent(zipUri, "title.txt", title);
}
if (!TextUtils.isEmpty(description)) {
assertZipContent(zipUri, "description.txt", description);
}
// URI of the screenshot taken by dumpstate.
Uri externalScreenshotUri = null;
SortedSet<String> internalScreenshotNames = new TreeSet<>();
for (Uri screenshotUri : screenshotUris) {
String screenshotName = screenshotUri.getLastPathSegment();
if (screenshotName.endsWith(SCREENSHOT_FILE)) {
externalScreenshotUri = screenshotUri;
} else {
internalScreenshotNames.add(screenshotName);
}
}
// Check external screenshot
if (screenshotContent != null) {
assertNotNull("did not get .png attachment for external screenshot",
externalScreenshotUri);
assertContent(externalScreenshotUri, SCREENSHOT_CONTENT);
} else {
assertNull("should not have .png attachment for external screenshot",
externalScreenshotUri);
}
// Check internal screenshots.
SortedSet<String> expectedNames = new TreeSet<>();
for (int i = 1 ; i <= numberScreenshots; i++) {
String prefix = renamedScreenshots ? name : Integer.toString(pid);
String expectedName = "screenshot-" + prefix + "-" + i + ".png";
expectedNames.add(expectedName);
}
// Ideally we should use MoreAsserts, but the error message in case of failure is not
// really useful.
assertEquals("wrong names for internal screenshots",
expectedNames, internalScreenshotNames);
}
private void assertContent(Uri uri, String expectedContent) throws IOException {
Log.v(TAG, "assertContents(uri=" + uri);
try (InputStream is = mContext.getContentResolver().openInputStream(uri)) {
String actualContent = new String(Streams.readFully(is));
assertEquals("wrong content for '" + uri + "'", expectedContent, actualContent);
}
}
private void assertZipContent(Uri uri, String entryName, String expectedContent)
throws IOException, IOException {
Log.v(TAG, "assertZipEntry(uri=" + uri + ", entryName=" + entryName);
try (ZipInputStream zis = new ZipInputStream(mContext.getContentResolver().openInputStream(
uri))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Log.v(TAG, "Zip entry: " + entry.getName());
if (entry.getName().equals(entryName)) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Streams.copy(zis, bos);
String actualContent = new String(bos.toByteArray(), "UTF-8");
bos.close();
assertEquals("wrong content for zip entry'" + entryName + "' on '" + uri + "'",
expectedContent, actualContent);
return;
}
}
}
fail("Did not find entry '" + entryName + "' on file '" + uri + "'");
}
private void assertPropertyValue(String key, String expectedValue) {
String actualValue = SystemProperties.get(key);
assertEquals("Wrong value for property '" + key + "'", expectedValue, actualValue);
}
private void assertServiceNotRunning() {
String service = BugreportProgressService.class.getName();
assertFalse("Service '" + service + "' is still running", isServiceRunning(service));
}
private boolean isServiceRunning(String name) {
ActivityManager manager = (ActivityManager) mContext
.getSystemService(Context.ACTIVITY_SERVICE);
for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
if (service.service.getClassName().equals(name)) {
return true;
}
}
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();
}
}
fail("Service status didn't change to " + expectRunning);
}
private void createTextFile(String path, String content) throws IOException {
Log.v(TAG, "createFile(" + path + ")");
try (Writer writer = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(path)))) {
writer.write(content);
}
}
private void createZipFile(String path, String entryName, String content) throws IOException {
Log.v(TAG, "createZipFile(" + path + ", " + entryName + ")");
try (ZipOutputStream zos = new ZipOutputStream(
new BufferedOutputStream(new FileOutputStream(path)))) {
ZipEntry entry = new ZipEntry(entryName);
zos.putNextEntry(entry);
byte[] data = content.getBytes();
zos.write(data, 0, data.length);
zos.closeEntry();
}
}
private String getPath(String file) {
final File rootDir = mContext.getFilesDir();
final File dir = new File(rootDir, BUGREPORTS_DIR);
if (!dir.exists()) {
Log.i(TAG, "Creating directory " + dir);
assertTrue("Could not create directory " + dir, dir.mkdir());
}
String path = new File(dir, file).getAbsolutePath();
Log.v(TAG, "Path for '" + file + "': " + path);
return path;
}
/**
* Gets the notification button used to take a screenshot.
*/
private UiObject getScreenshotButton() {
openProgressNotification(ID);
return mUiBot.getVisibleObject(
mContext.getString(R.string.bugreport_screenshot_action).toUpperCase());
}
/**
* Takes a screenshot using the system notification.
*/
private void takeScreenshot() throws Exception {
UiObject screenshotButton = getScreenshotButton();
mUiBot.click(screenshotButton, "screenshot_button");
}
private UiObject waitForScreenshotButtonEnabled(boolean expectedEnabled) throws Exception {
UiObject screenshotButton = getScreenshotButton();
int maxAttempts = SAFE_SCREENSHOT_DELAY;
int i = 0;
do {
boolean enabled = screenshotButton.isEnabled();
if (enabled == expectedEnabled) {
return screenshotButton;
}
i++;
Log.v(TAG, "Sleeping for 1 second while waiting for screenshot.enable to be "
+ expectedEnabled + " (attempt " + i + ")");
Thread.sleep(DateUtils.SECOND_IN_MILLIS);
} while (i <= maxAttempts);
fail("screenshot.enable didn't change to " + expectedEnabled + " in " + maxAttempts + "s");
return screenshotButton;
}
private void assertScreenshotButtonEnabled(boolean expectedEnabled) throws Exception {
UiObject screenshotButton = getScreenshotButton();
assertEquals("wrong state for screenshot button ", expectedEnabled,
screenshotButton.isEnabled());
}
/**
* Helper class containing the UiObjects present in the bugreport info dialog.
*/
private final class DetailsUi {
final UiObject detailsButton;
final UiObject nameField;
final UiObject titleField;
final UiObject descField;
final UiObject okButton;
final UiObject cancelButton;
/**
* Gets the UI objects by opening the progress notification and clicking DETAILS.
*/
DetailsUi(UiBot uiBot, int id) throws UiObjectNotFoundException {
this(uiBot, id, true);
}
/**
* Gets the UI objects by opening the progress notification and clicking on DETAILS or in
* the notification itself.
*/
DetailsUi(UiBot uiBot, int id, boolean clickDetails) throws UiObjectNotFoundException {
UiObject notification = openProgressNotification(id);
detailsButton = mUiBot.getVisibleObject(mContext.getString(
R.string.bugreport_info_action).toUpperCase());
if (clickDetails) {
mUiBot.click(detailsButton, "details_button");
} else {
mUiBot.click(notification, "notification");
}
// TODO: unhardcode resource ids
UiObject dialogTitle = mUiBot.getVisibleObjectById("android:id/alertTitle");
assertEquals("Wrong title", mContext.getString(R.string.bugreport_info_dialog_title,
id), dialogTitle.getText().toString());
nameField = mUiBot.getVisibleObjectById("com.android.shell:id/name");
titleField = mUiBot.getVisibleObjectById("com.android.shell:id/title");
descField = mUiBot.getVisibleObjectById("com.android.shell:id/description");
okButton = mUiBot.getObjectById("android:id/button1");
cancelButton = mUiBot.getObjectById("android:id/button2");
}
private void assertField(String name, UiObject field, String expected)
throws UiObjectNotFoundException {
String actual = field.getText().toString();
assertEquals("Wrong value on field '" + name + "'", expected, actual);
}
void assertName(String expected) throws UiObjectNotFoundException {
assertField("name", nameField, expected);
}
void assertTitle(String expected) throws UiObjectNotFoundException {
assertField("title", titleField, expected);
}
void assertDescription(String expected) throws UiObjectNotFoundException {
assertField("description", descField, expected);
}
/**
* Set focus on the name field so it can be validated once focus is lost.
*/
void focusOnName() throws UiObjectNotFoundException {
mUiBot.click(nameField, "name_field");
assertTrue("name_field not focused", nameField.isFocused());
}
/**
* Takes focus away from the name field so it can be validated.
*/
void focusAwayFromName() throws UiObjectNotFoundException {
mUiBot.click(titleField, "title_field"); // Change focus.
mUiBot.pressBack(); // Dismiss keyboard.
assertFalse("name_field is focused", nameField.isFocused());
}
void reOpen() {
openProgressNotification(ID);
mUiBot.click(detailsButton, "details_button");
}
void clickOk() {
mUiBot.click(okButton, "details_ok_button");
}
void clickCancel() {
mUiBot.click(cancelButton, "details_cancel_button");
}
}
}