/* * 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.INTENT_BUGREPORT_REQUESTED; import static com.android.shell.BugreportProgressService.PROPERTY_LAST_ID; import static com.android.shell.BugreportProgressService.SCREENSHOT_DELAY_SECONDS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; 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.BugreportManager; import android.os.Build; import android.os.Bundle; import android.os.IDumpstate; import android.os.IDumpstateListener; import android.os.ParcelFileDescriptor; import android.os.SystemClock; 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.UiObject2; import android.support.test.uiautomator.UiObjectNotFoundException; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.Log; import androidx.test.InstrumentationRegistry; import androidx.test.filters.LargeTest; import androidx.test.rule.ServiceTestRule; import androidx.test.runner.AndroidJUnit4; import com.android.shell.ActionSendMultipleConsumerActivity.CustomActionSendMultipleListener; import libcore.io.IoUtils; import libcore.io.Streams; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; 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.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; /** * Integration tests for {@link BugreportProgressService}. *
* These tests rely on external UI components (like the notificatio bar and activity chooser), * which can make them unreliable and slow. *
* The general workflow is: *
* NOTE: these tests only work if the device is unlocked.
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class BugreportReceiverTest {
private static final String TAG = "BugreportReceiverTest";
// Timeout for UI operations, in milliseconds.
private static final int TIMEOUT = (int) (5 * DateUtils.SECOND_IN_MILLIS);
// The default timeout is too short to verify the notification button state. Using a longer
// timeout in the tests.
private static final int SCREENSHOT_DELAY_SECONDS = 5;
// Timeout for when waiting for a screenshot to finish.
private static final int SAFE_SCREENSHOT_DELAY = SCREENSHOT_DELAY_SECONDS + 10;
private static final String BUGREPORT_FILE = "test_bugreport.txt";
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 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 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 String mDescription;
private String mProgressTitle;
private int mBugreportId;
private Context mContext;
private UiBot mUiBot;
private CustomActionSendMultipleListener mListener;
private BugreportProgressService mService;
private IDumpstateListener mIDumpstateListener;
private ParcelFileDescriptor mBugreportFd;
private ParcelFileDescriptor mScreenshotFd;
@Mock private IDumpstate mMockIDumpstate;
@Rule public TestName mName = new TestName();
@Rule public ServiceTestRule mServiceRule = new ServiceTestRule();
@Before
public void setUp() throws Exception {
Log.i(TAG, getName() + ".setup()");
MockitoAnnotations.initMocks(this);
Instrumentation instrumentation = getInstrumentation();
mContext = instrumentation.getTargetContext();
mUiBot = new UiBot(instrumentation, TIMEOUT);
mListener = ActionSendMultipleConsumerActivity.getListener(mContext);
cancelExistingNotifications();
mBugreportId = getBugreportId();
mProgressTitle = getBugreportInProgress(mBugreportId);
// 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();
// Mocks BugreportManager and updates tests value to the service
mService = ((BugreportProgressService.LocalBinder) mServiceRule.bindService(
new Intent(mContext, BugreportProgressService.class))).getService();
mService.mBugreportManager = new BugreportManager(mContext, mMockIDumpstate);
mService.mScreenshotDelaySec = SCREENSHOT_DELAY_SECONDS;
// Dup the fds which are passing to startBugreport function.
Mockito.doAnswer(invocation -> {
final boolean isScreenshotRequested = invocation.getArgument(6);
if (isScreenshotRequested) {
mScreenshotFd = ParcelFileDescriptor.dup(invocation.getArgument(3));
}
mBugreportFd = ParcelFileDescriptor.dup(invocation.getArgument(2));
return null;
}).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), anyInt(),
any(), anyBoolean());
setWarningState(mContext, STATE_HIDE);
mUiBot.turnScreenOn();
}
@After
public void tearDown() throws Exception {
Log.i(TAG, getName() + ".tearDown()");
if (mBugreportFd != null) {
IoUtils.closeQuietly(mBugreportFd);
}
if (mScreenshotFd != null) {
IoUtils.closeQuietly(mScreenshotFd);
}
mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
try {
cancelExistingNotifications();
} finally {
// Collapses just in case, so a failure here does not compromise tests on other classes.
mUiBot.collapseStatusBar();
}
}
/*
* TODO: this test is incomplete because:
* - the assertProgressNotification() is not really asserting the progress because the
* UI automation API doesn't provide a way to check the notification progress bar value
* - it should use the binder object instead of SystemProperties to update progress
*/
@Test
public void testProgress() throws Exception {
sendBugreportStarted();
waitForScreenshotButtonEnabled(true);
assertProgressNotification(mProgressTitle, 0f);
mIDumpstateListener.onProgress(10);
assertProgressNotification(mProgressTitle, 10);
mIDumpstateListener.onProgress(95);
assertProgressNotification(mProgressTitle, 95.00f);
// ...but never more than the capped value.
mIDumpstateListener.onProgress(200);
assertProgressNotification(mProgressTitle, 99);
mIDumpstateListener.onProgress(300);
assertProgressNotification(mProgressTitle, 99);
Bundle extras = sendBugreportFinishedAndGetSharedIntent(mBugreportId);
assertActionSendMultiple(extras);
assertServiceNotRunning();
}
@Test
public void testProgress_cancel() throws Exception {
sendBugreportStarted();
waitForScreenshotButtonEnabled(true);
assertProgressNotification(mProgressTitle, 00.00f);
cancelFromNotification(mProgressTitle);
assertServiceNotRunning();
}
@Test
public void testProgress_takeExtraScreenshot() throws Exception {
sendBugreportStarted();
waitForScreenshotButtonEnabled(true);
takeScreenshot();
assertScreenshotButtonEnabled(false);
waitForScreenshotButtonEnabled(true);
Bundle extras = sendBugreportFinishedAndGetSharedIntent(mBugreportId);
assertActionSendMultiple(extras, NO_NAME, NO_TITLE, NO_DESCRIPTION, 1);
assertServiceNotRunning();
}
@Test
public void testScreenshotFinishesAfterBugreport() throws Exception {
sendBugreportStarted();
waitForScreenshotButtonEnabled(true);
takeScreenshot();
sendBugreportFinished();
waitShareNotification(mBugreportId);
// There's no indication in the UI about the screenshot finish, so just sleep like a baby...
sleep(SAFE_SCREENSHOT_DELAY * DateUtils.SECOND_IN_MILLIS);
Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId);
assertActionSendMultiple(extras, NO_NAME, NO_TITLE, NO_DESCRIPTION, 1);
assertServiceNotRunning();
}
@Test
public void testProgress_changeDetailsInvalidInput() throws Exception {
sendBugreportStarted();
waitForScreenshotButtonEnabled(true);
DetailsUi detailsUi = new DetailsUi(mBugreportId);
// Change name
detailsUi.focusOnName();
detailsUi.nameField.setText(NEW_NAME);
detailsUi.focusAwayFromName();
detailsUi.clickOk();
// Now try to set an invalid name.
detailsUi.reOpen(NEW_NAME);
detailsUi.nameField.setText("/etc/passwd");
detailsUi.clickOk();
// Finally, make the real changes.
detailsUi.reOpen("_etc_passwd");
detailsUi.nameField.setText(NEW_NAME);
detailsUi.titleField.setText(TITLE);
detailsUi.descField.setText(mDescription);
detailsUi.clickOk();
assertProgressNotification(NEW_NAME, 00.00f);
Bundle extras = sendBugreportFinishedAndGetSharedIntent(TITLE);
assertActionSendMultiple(extras, NEW_NAME, TITLE, mDescription, 0);
assertServiceNotRunning();
}
@Test
public void testProgress_cancelBugClosesDetailsDialog() throws Exception {
sendBugreportStarted();
waitForScreenshotButtonEnabled(true);
cancelFromNotification(mProgressTitle);
mUiBot.collapseStatusBar();
assertDetailsUiClosed();
assertServiceNotRunning();
}
@Test
public void testProgress_changeDetailsTest() throws Exception {
sendBugreportStarted();
waitForScreenshotButtonEnabled(true);
DetailsUi detailsUi = new DetailsUi(mBugreportId);
// Change fields.
detailsUi.reOpen(mProgressTitle);
detailsUi.nameField.setText(NEW_NAME);
detailsUi.titleField.setText(TITLE);
detailsUi.descField.setText(mDescription);
detailsUi.clickOk();
assertProgressNotification(NEW_NAME, 00.00f);
Bundle extras = sendBugreportFinishedAndGetSharedIntent(TITLE);
assertActionSendMultiple(extras, NEW_NAME, TITLE, mDescription, 0);
assertServiceNotRunning();
}
@Test
public void testProgress_changeJustDetailsTouchingDetails() throws Exception {
changeJustDetailsTest(true);
}
@Test
public void testProgress_changeJustDetailsTouchingNotification() throws Exception {
changeJustDetailsTest(false);
}
private void changeJustDetailsTest(boolean touchDetails) throws Exception {
sendBugreportStarted();
waitForScreenshotButtonEnabled(true);
DetailsUi detailsUi = new DetailsUi(mBugreportId, touchDetails);
detailsUi.nameField.setText("");
detailsUi.titleField.setText("");
detailsUi.descField.setText(mDescription);
detailsUi.clickOk();
Bundle extras = sendBugreportFinishedAndGetSharedIntent(mBugreportId);
assertActionSendMultiple(extras, NO_NAME, NO_TITLE, mDescription, 0);
assertServiceNotRunning();
}
/**
* Tests the scenario where the initial screenshot and dumpstate are finished while the user
* is changing the info in the details screen.
*/
@Test
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.
*/
@Test
public void testProgress_bugreportFinishedWhileChangingDetails() throws Exception {
bugreportFinishedWhileChangingDetailsTest(true);
}
private void bugreportFinishedWhileChangingDetailsTest(boolean waitScreenshot) throws Exception {
sendBugreportStarted();
if (waitScreenshot) {
waitForScreenshotButtonEnabled(true);
}
DetailsUi detailsUi = new DetailsUi(mBugreportId);
// Finish the bugreport while user's still typing the name.
detailsUi.nameField.setText(NEW_NAME);
sendBugreportFinished();
// Wait until the share notification is received...
waitShareNotification(mBugreportId);
// ...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());
assertNotEquals("didn't revert name on UI", NAME, detailsUi.nameField.getText());
// Finish changing other fields.
detailsUi.titleField.setText(TITLE);
detailsUi.descField.setText(mDescription);
detailsUi.clickOk();
// Finally, share bugreport.
Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId);
assertActionSendMultiple(extras, NO_NAME, TITLE, mDescription, 0);
assertServiceNotRunning();
}
@Test
public void testBugreportFinished_withWarningFirstTime() throws Exception {
bugreportFinishedWithWarningTest(null);
}
@Test
public void testBugreportFinished_withWarningUnknownState() throws Exception {
bugreportFinishedWithWarningTest(STATE_UNKNOWN);
}
@Test
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();
// Confidence check...
assertEquals("Did not reset properties", STATE_UNKNOWN,
getWarningState(mContext, STATE_UNKNOWN));
} else {
setWarningState(mContext, propertyState);
}
// Send notification and click on share.
sendBugreportStarted();
waitForScreenshotButtonEnabled(true);
sendBugreportFinished();
mUiBot.clickOnNotification(mContext.getString(
R.string.bugreport_finished_title, mBugreportId));
// Handle the warning
mUiBot.getObject(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.IS_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);
// Make sure it's hidden now.
int newState = getWarningState(mContext, STATE_UNKNOWN);
assertEquals("Didn't change state", STATE_HIDE, newState);
}
@Test
public void testBugreportFinished_withEmptyBugreportFile() throws Exception {
sendBugreportStarted();
IoUtils.closeQuietly(mBugreportFd);
mBugreportFd = null;
sendBugreportFinished();
assertServiceNotRunning();
}
@Test
public void testShareBugreportAfterServiceDies() throws Exception {
sendBugreportStarted();
waitForScreenshotButtonEnabled(true);
sendBugreportFinished();
killService();
assertServiceNotRunning();
Bundle extras = acceptBugreportAndGetSharedIntent(mBugreportId);
assertActionSendMultiple(extras);
}
@Test
public void testBugreportRequestTwice_oneStartBugreportInvoked() throws Exception {
sendBugreportStarted();
new BugreportRequestedReceiver().onReceive(mContext,
new Intent(INTENT_BUGREPORT_REQUESTED));
getInstrumentation().waitForIdleSync();
verify(mMockIDumpstate, times(1)).startBugreport(anyInt(), any(), any(), any(),
anyInt(), anyInt(), any(), anyBoolean());
sendBugreportFinished();
}
private void cancelExistingNotifications() {
// Must kill service first, because notifications from a foreground service cannot be
// canceled.
killService();
NotificationManager nm = NotificationManager.from(mContext);
StatusBarNotification[] activeNotifications = nm.getActiveNotifications();
if (activeNotifications.length == 0) {
return;
}
Log.w(TAG, getName() + ": " + activeNotifications.length + " active notifications");
nm.cancelAll();
// Wait a little bit...
for (int i = 1; i < 5; i++) {
int total = nm.getActiveNotifications().length;
if (total == 0) {
return;
}
Log.d(TAG, total + "notifications are still active; sleeping ");
nm.cancelAll();
sleep(1000);
}
assertEquals("old notifications were not cancelled", 0, nm.getActiveNotifications().length);
}
private void cancelFromNotification(String name) {
openProgressNotification(name);
UiObject cancelButton = mUiBot.getObject(mContext.getString(
com.android.internal.R.string.cancel));
mUiBot.click(cancelButton, "cancel_button");
}
private void assertProgressNotification(String name, float percent) {
openProgressNotification(name);
// TODO: need a way to get the ProgresBar from the "android:id/progress" UIObject...
}
private void openProgressNotification(String title) {
Log.v(TAG, "Looking for progress notification for '" + title + "'");
UiObject2 notification = mUiBot.getNotification2(title);
if (notification != null) {
mUiBot.expandNotification(notification);
}
}
/**
* Sends a "bugreport requested" intent with the default values.
*/
private void sendBugreportStarted() throws Exception {
Intent intent = new Intent(INTENT_BUGREPORT_REQUESTED);
// Ideally, we should invoke BugreportRequestedReceiver by sending
// INTENT_BUGREPORT_REQUESTED. But the intent has been protected broadcast by the system
// starting from S.
new BugreportRequestedReceiver().onReceive(mContext, intent);
ArgumentCaptor