blob: 1f4d749f399286590c02dfc18bc717377edccae4 [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.BugreportProgressService.EXTRA_BUGREPORT;
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 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.util.List;
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.ContextWrapper;
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.support.test.uiautomator.UiObject;
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.LargeTest;
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) BugreportProgressService.POLLING_FREQUENCY * 4;
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 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 String PROGRESS_PROPERTY = "dumpstate.42.progress";
private static final String MAX_PROPERTY = "dumpstate.42.max";
private static final String NAME = "BUG, Y U NO REPORT?";
private String mPlainTextPath;
private String mZipPath;
private String mScreenshotPath;
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();
mPlainTextPath = getPath(BUGREPORT_FILE);
mZipPath = getPath(ZIP_FILE);
mScreenshotPath = getPath(SCREENSHOT_FILE);
createTextFile(mPlainTextPath, BUGREPORT_CONTENT);
createTextFile(mScreenshotPath, SCREENSHOT_CONTENT);
createZipFile(mZipPath, BUGREPORT_FILE, BUGREPORT_CONTENT);
BugreportPrefs.setWarningState(mContext, BugreportPrefs.STATE_HIDE);
}
public void testFullWorkflow() throws Exception {
resetProperties();
sendBugreportStarted(1000);
assertProgressNotification(NAME, "0.00%");
SystemProperties.set(PROGRESS_PROPERTY, "108");
assertProgressNotification(NAME, "10.80%");
SystemProperties.set(PROGRESS_PROPERTY, "500");
assertProgressNotification(NAME, "50.00%");
SystemProperties.set(MAX_PROPERTY, "2000");
assertProgressNotification(NAME, "25.00%");
Bundle extras =
sendBugreportFinishedAndGetSharedIntent(PID, mPlainTextPath, mScreenshotPath);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT);
assertServiceNotRunning();
}
public void testBugreportFinished_withWarning() throws Exception {
// Explicitly shows the warning.
BugreportPrefs.setWarningState(mContext, BugreportPrefs.STATE_SHOW);
// Send notification and click on share.
sendBugreportFinished(null, mPlainTextPath, null);
acceptBugreport();
// Handle the warning
mUiBot.getVisibleObject(mContext.getString(R.string.bugreport_confirm));
// TODO: get ok and showMessageAgain from the dialog reference above
UiObject showMessageAgain =
mUiBot.getVisibleObject(mContext.getString(R.string.bugreport_confirm_repeat));
mUiBot.click(showMessageAgain, "show-message-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, null);
// Make sure it's hidden now.
int newState = BugreportPrefs.getWarningState(mContext, BugreportPrefs.STATE_UNKNOWN);
assertEquals("Didn't change state", BugreportPrefs.STATE_HIDE, newState);
}
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, null);
assertActionSendMultiple(extras, BUGREPORT_CONTENT, null);
}
public void testBugreportFinished_zippedBugreportAndNoScreenshot() throws Exception {
Bundle extras = sendBugreportFinishedAndGetSharedIntent(mZipPath, 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);
}
}
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);
Log.v(TAG, "Looking for progress notification details: '" + name + "-" + percent + "'");
mUiBot.getObject(name);
mUiBot.getObject(percent);
}
void resetProperties() {
// TODO: call method to remove property instead
SystemProperties.set(PROGRESS_PROPERTY, "0");
SystemProperties.set(MAX_PROPERTY, "0");
}
/**
* Sends a "bugreport started" intent with the default values.
*/
private void sendBugreportStarted(int max) {
Intent intent = new Intent(INTENT_BUGREPORT_STARTED);
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(null, bugreportPath, screenshotPath);
}
/**
* Sends a "bugreport finished" intent and waits for the result.
*
* @return extras sent in the shared intent.
*/
private Bundle sendBugreportFinishedAndGetSharedIntent(Integer pid, String bugreportPath,
String screenshotPath) {
sendBugreportFinished(pid, bugreportPath, screenshotPath);
return acceptBugreportAndGetSharedIntent();
}
/**
* Accepts the notification to share the finished bugreport and waits for the result.
*
* @return extras sent in the shared intent.
*/
private Bundle acceptBugreportAndGetSharedIntent() {
acceptBugreport();
mUiBot.chooseActivity(UI_NAME);
return mListener.getExtras();
}
/**
* Accepts the notification to share the finished bugreport.
*/
private void acceptBugreport() {
mUiBot.clickOnNotification(mContext.getString(R.string.bugreport_finished_title));
}
/**
* Sends a "bugreport finished" intent.
*
*/
private void sendBugreportFinished(Integer pid, String bugreportPath, String screenshotPath) {
Intent intent = new Intent(INTENT_BUGREPORT_FINISHED);
if (pid != null) {
intent.putExtra(EXTRA_PID, pid);
}
if (bugreportPath != null) {
intent.putExtra(EXTRA_BUGREPORT, bugreportPath);
}
if (screenshotPath != null) {
intent.putExtra(EXTRA_SCREENSHOT, screenshotPath);
}
mContext.sendBroadcast(intent);
}
/**
* 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<Uri> 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 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 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();
}
}
private String getPath(String file) {
File rootDir = new ContextWrapper(mContext).getFilesDir();
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;
}
}