mQueue = new SynchronousQueue<>();
+
+ public CustomActionSendMultipleListener(Context context) {
+ BroadcastReceiver receiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ try {
+ mQueue.put(intent.getExtras());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ };
+
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(CUSTOM_ACTION_SEND_MULTIPLE_INTENT);
+ context.registerReceiver(receiver, filter);
+ }
+
+ /**
+ * Gets the extras from the custom intent, blocking until it's received.
+ */
+ Bundle getExtras() {
+ Bundle bundle = null;
+ try {
+ bundle = mQueue.poll(TIMEOUT, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ if (bundle == null) {
+ throw new IllegalStateException("Intent not received after " + TIMEOUT + "s");
+ }
+ return bundle;
+ }
+ }
+}
diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
new file mode 100644
index 0000000000000..06565c0b4ae69
--- /dev/null
+++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
@@ -0,0 +1,242 @@
+/*
+ * 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.BugreportReceiver.EXTRA_BUGREPORT;
+import static com.android.shell.BugreportReceiver.EXTRA_SCREENSHOT;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+import libcore.io.Streams;
+import android.app.Instrumentation;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.service.notification.StatusBarNotification;
+import android.support.test.uiautomator.UiDevice;
+import android.test.InstrumentationTestCase;
+import android.util.Log;
+
+import com.android.shell.ActionSendMultipleConsumerActivity.CustomActionSendMultipleListener;
+
+/**
+ * Integration tests for {@link BugreportReceiver}.
+ *
+ * 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.
+ *
+ * The general workflow is:
+ *
+ * - creates the bug report files
+ *
- generates the BUGREPORT_FINISHED intent
+ *
- emulate user actions to share the intent with a custom activity
+ *
- asserts the extras received by the custom activity
+ *
+ *
+ * TODO: currently, these tests only work if the bug report sharing warning is disabled and the
+ * device screen is unlocked.
+ */
+public class BugreportReceiverTest extends InstrumentationTestCase {
+
+ private static final String TAG = "BugreportReceiverTest";
+
+ // Timeout for UI operations, in milliseconds.
+ private static final int TIMEOUT = 1000;
+
+ private static final String ROOT_DIR = "/data/data/com.android.shell/files/bugreports";
+ private static final String BUGREPORT_FILE = "test_bugreport.txt";
+ private static final String ZIP_FILE = "test_bugreport.zip";
+ private static final String PLAIN_TEXT_PATH = ROOT_DIR + "/" + BUGREPORT_FILE;
+ private static final String ZIP_PATH = ROOT_DIR + "/" + ZIP_FILE;
+ private static final String SCREENSHOT_PATH = ROOT_DIR + "/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 Context mContext;
+ private UiBot mUiBot;
+ private CustomActionSendMultipleListener mListener;
+
+ @Override
+ protected void setUp() throws Exception {
+ Instrumentation instrumentation = getInstrumentation();
+ mContext = instrumentation.getTargetContext();
+ mUiBot = new UiBot(UiDevice.getInstance(instrumentation), TIMEOUT);
+ mListener = ActionSendMultipleConsumerActivity.getListener(mContext);
+ cancelExistingNotifications();
+ }
+
+ public void testBugreportFinished_plainBugreportAndScreenshot() throws Exception {
+ createTextFile(PLAIN_TEXT_PATH, BUGREPORT_CONTENT);
+ createTextFile(SCREENSHOT_PATH, SCREENSHOT_CONTENT);
+ Bundle extras = sendBugreportFinishedIntent(PLAIN_TEXT_PATH, SCREENSHOT_PATH);
+ assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT);
+ }
+
+ public void testBugreportFinished_zippedBugreportAndScreenshot() throws Exception {
+ createZipFile(ZIP_PATH, BUGREPORT_FILE, BUGREPORT_CONTENT);
+ createTextFile(SCREENSHOT_PATH, SCREENSHOT_CONTENT);
+ Bundle extras = sendBugreportFinishedIntent(ZIP_PATH, SCREENSHOT_PATH);
+ assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT);
+ }
+
+ public void testBugreportFinished_plainBugreportAndNoScreenshot() throws Exception {
+ createTextFile(PLAIN_TEXT_PATH, BUGREPORT_CONTENT);
+ Bundle extras = sendBugreportFinishedIntent(PLAIN_TEXT_PATH, null);
+ assertActionSendMultiple(extras, BUGREPORT_CONTENT, null);
+ }
+
+ public void testBugreportFinished_zippedBugreportAndNoScreenshot() throws Exception {
+ createZipFile(ZIP_PATH, BUGREPORT_FILE, BUGREPORT_CONTENT);
+ Bundle extras = sendBugreportFinishedIntent(ZIP_PATH, null);
+ assertActionSendMultiple(extras, BUGREPORT_CONTENT, null);
+ }
+
+ 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);
+ }
+ }
+
+ /**
+ * Sends a "bugreport finished" intent and waits for the result.
+ *
+ * @return extras sent to the bugreport finished consumer.
+ */
+ private Bundle sendBugreportFinishedIntent(String bugreportPath, String screenshotPath) {
+ Intent intent = new Intent("android.intent.action.BUGREPORT_FINISHED");
+ if (bugreportPath != null) {
+ intent.putExtra(EXTRA_BUGREPORT, bugreportPath);
+ }
+ if (screenshotPath != null) {
+ intent.putExtra(EXTRA_SCREENSHOT, screenshotPath);
+ }
+
+ mContext.sendBroadcast(intent);
+
+ mUiBot.clickOnNotification(mContext.getString(R.string.bugreport_finished_title));
+ mUiBot.chooseActivity(UI_NAME);
+ return mListener.getExtras();
+ }
+
+ /**
+ * Asserts the proper ACTION_SEND_MULTIPLE intent was sent.
+ */
+ private void assertActionSendMultiple(Bundle extras, 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);
+
+ assertEquals("wrong subject", ZIP_FILE, extras.getString(Intent.EXTRA_SUBJECT));
+
+ List attachments = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
+ int expectedSize = screenshotContent != null ? 2 : 1;
+ assertEquals("wrong number of attachments", expectedSize, attachments.size());
+
+ // Need to interact through all attachments, since order is not guaranteed.
+ Uri zipUri = null, screenshotUri = null;
+ for (Uri attachment : attachments) {
+ if (attachment.getPath().endsWith(".zip")) {
+ zipUri = attachment;
+ }
+ if (attachment.getPath().endsWith(".png")) {
+ screenshotUri = attachment;
+ }
+ }
+ assertNotNull("did not get .zip attachment", zipUri);
+ assertZipContent(zipUri, BUGREPORT_FILE, BUGREPORT_CONTENT);
+
+ if (screenshotContent != null) {
+ assertNotNull("did not get .png attachment", screenshotUri);
+ assertContent(screenshotUri, SCREENSHOT_CONTENT);
+ } else {
+ assertNull("should not have .png attachment", screenshotUri);
+ }
+ }
+
+ 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 static 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();
+ }
+ }
+}
diff --git a/packages/Shell/tests/src/com/android/shell/UiBot.java b/packages/Shell/tests/src/com/android/shell/UiBot.java
new file mode 100644
index 0000000000000..f5dd31c6fdc02
--- /dev/null
+++ b/packages/Shell/tests/src/com/android/shell/UiBot.java
@@ -0,0 +1,130 @@
+/*
+ * 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 android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import static junit.framework.Assert.assertTrue;
+
+/**
+ * A helper class for UI-related testing tasks.
+ */
+final class UiBot {
+
+ private static final String TAG = "UiBot";
+ private static final String SYSTEMUI_PACKAGED = "com.android.systemui";
+
+ private final UiDevice mDevice;
+ private final int mTimeout;
+
+ public UiBot(UiDevice device, int timeout) {
+ mDevice = device;
+ mTimeout = timeout;
+ }
+
+ /**
+ * Opens the system notification and clicks a given notification.
+ *
+ * @param text Notificaton's text as displayed by the UI.
+ */
+ public void clickOnNotification(String text) {
+ boolean opened = mDevice.openNotification();
+ Log.v(TAG, "openNotification(): " + opened);
+ boolean gotIt = mDevice.wait(Until.hasObject(By.pkg(SYSTEMUI_PACKAGED)), mTimeout);
+ assertTrue("could not get system ui (" + SYSTEMUI_PACKAGED + ")", gotIt);
+
+ gotIt = mDevice.wait(Until.hasObject(By.text(text)), mTimeout);
+ assertTrue("object with text '(" + text + "') not visible yet", gotIt);
+
+ UiObject notification = getVisibleObject(text);
+
+ click(notification, "bug report notification");
+ }
+
+ /**
+ * Gets an object which is guaranteed to be present in the current UI.\
+ *
+ * @param text Object's text as displayed by the UI.
+ */
+ public UiObject getVisibleObject(String text) {
+ UiObject uiObject = mDevice.findObject(new UiSelector().text(text));
+ assertTrue("could not find object with text '(" + text + "')", uiObject.exists());
+ return uiObject;
+ }
+
+ /**
+ * Clicks on a UI element.
+ *
+ * @param uiObject UI element to be clicked.
+ * @param description Elements's description used on logging statements.
+ */
+ public void click(UiObject uiObject, String description) {
+ try {
+ boolean clicked = uiObject.click();
+ // TODO: assertion below fails sometimes, even though the click succeeded,
+ // (specially when clicking the "Just Once" button), so it's currently just logged.
+ // assertTrue("could not click on object '" + description + "'", clicked);
+
+ Log.v(TAG, "onClick for " + description + ": " + clicked);
+ } catch (UiObjectNotFoundException e) {
+ throw new IllegalStateException("exception when clicking on object '" + description
+ + "'", e);
+ }
+ }
+
+ /**
+ * Chooses a given activity to handle an Intent, using the "Just Once" button.
+ *
+ * @param name name of the activity as displayed in the UI (typically the value set by
+ * {@code android:label} in the manifest).
+ */
+ // TODO: UI Automator should provide such logic.
+ public void chooseActivity(String name) {
+ // First select activity if it's not the default option.
+ boolean gotIt = mDevice.wait(Until.hasObject(By.text(name)), mTimeout);
+ // TODO: if the activity is indeed the default option, call above will timeout, which will
+ // make the tests run slower. It might be better to change the logic to assume the default
+ // first.
+ if (gotIt) {
+ Log.v(TAG, "Found activity " + name + ", it's not default action");
+ UiObject activityChooser = getVisibleObject(name);
+ click(activityChooser, "activity chooser");
+ } else {
+ String text = String.format("Share with %s", name);
+ Log.v(TAG, "Didn't find activity " + name
+ + ", assuming it's the default action and search for '" + text + "'");
+ gotIt = mDevice.wait(Until.hasObject(By.text(text)), mTimeout);
+ assertTrue("did not find text '" + text + "'", gotIt);
+ }
+
+ // Then clicks the "Just Once" button.
+ gotIt = mDevice
+ .wait(Until.hasObject(By.res("android", "button_once")), mTimeout);
+ assertTrue("'Just Once' button not visible yet", gotIt);
+
+ UiObject justOnce = mDevice
+ .findObject(new UiSelector().resourceId("android:id/button_once"));
+ assertTrue("'Just Once' button not found", justOnce.exists());
+
+ click(justOnce, "Just Once");
+ }
+}