| /* |
| * 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_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.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.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.support.test.uiautomator.UiObjectNotFoundException; |
| import android.support.test.uiautomator.UiSelector; |
| 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; |
| import com.android.shell.BugreportProgressService; |
| |
| /** |
| * 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; |
| |
| // 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 static final boolean PENDING_SCREENSHOT = true; |
| private static final boolean NOT_PENDING_SCREENSHOT = 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(); |
| |
| BugreportPrefs.setWarningState(mContext, BugreportPrefs.STATE_HIDE); |
| } |
| |
| public void testProgress() throws Exception { |
| resetProperties(); |
| sendBugreportStarted(1000); |
| waitForScreenshotButtonEnabled(true); |
| |
| final NumberFormat nf = NumberFormat.getPercentInstance(); |
| nf.setMinimumFractionDigits(2); |
| nf.setMaximumFractionDigits(2); |
| |
| assertProgressNotification(NAME, nf.format(0)); |
| |
| SystemProperties.set(PROGRESS_PROPERTY, "108"); |
| assertProgressNotification(NAME, nf.format(0.108)); |
| |
| SystemProperties.set(PROGRESS_PROPERTY, "500"); |
| assertProgressNotification(NAME, nf.format(0.50)); |
| |
| SystemProperties.set(MAX_PROPERTY, "2000"); |
| assertProgressNotification(NAME, nf.format(0.25)); |
| |
| Bundle extras = |
| sendBugreportFinishedAndGetSharedIntent(ID, mPlainTextPath, mScreenshotPath); |
| assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, ZIP_FILE, |
| NAME, NO_TITLE, NO_DESCRIPTION, 1, 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, nf.format(0)); |
| |
| 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 { |
| takeExtraScreenshotTest(false); |
| } |
| |
| public void testProgress_takeExtraScreenshotServiceDiesAfterScreenshotTaken() throws Exception { |
| takeExtraScreenshotTest(true); |
| } |
| |
| private void takeExtraScreenshotTest(boolean serviceDies) throws Exception { |
| resetProperties(); |
| sendBugreportStarted(1000); |
| |
| waitForScreenshotButtonEnabled(true); |
| takeScreenshot(); |
| assertScreenshotButtonEnabled(false); |
| waitForScreenshotButtonEnabled(true); |
| |
| sendBugreportFinished(ID, mPlainTextPath, mScreenshotPath); |
| |
| if (serviceDies) { |
| waitShareNotification(ID); |
| killService(); |
| } |
| |
| Bundle extras = acceptBugreportAndGetSharedIntent(ID); |
| assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, ZIP_FILE, |
| NAME, NO_TITLE, NO_DESCRIPTION, 2, RENAMED_SCREENSHOTS); |
| |
| 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(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); |
| |
| if (serviceDies) { |
| killService(); |
| } |
| |
| 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, "0.00%"); |
| |
| Bundle extras = sendBugreportFinishedAndGetSharedIntent(ID, mPlainTextPath, |
| mScreenshotPath); |
| assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, TITLE, |
| NEW_NAME, TITLE, mDescription, 1, 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, "0.00%"); |
| |
| Bundle extras = sendBugreportFinishedAndGetSharedIntent(ID, |
| plainText? mPlainTextPath : mZipPath, mScreenshotPath); |
| assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, TITLE, |
| NEW_NAME, TITLE, mDescription, 1, 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, 1, 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, PENDING_SCREENSHOT); |
| |
| 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, 1, 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, 1, RENAMED_SCREENSHOTS); |
| |
| assertServiceNotRunning(); |
| } |
| |
| public void testBugreportFinished_withWarning() throws Exception { |
| // Explicitly shows the warning. |
| BugreportPrefs.setWarningState(mContext, BugreportPrefs.STATE_SHOW); |
| |
| // 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 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, NO_SCREENSHOT); |
| |
| // 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 testShareBugreportAfterServiceDies() throws Exception { |
| sendBugreportFinished(NO_ID, mPlainTextPath, NO_SCREENSHOT); |
| killService(); |
| 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, String 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); |
| mUiBot.getObject(percent); |
| } |
| |
| 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, NOT_PENDING_SCREENSHOT); |
| } |
| |
| /** |
| * 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) { |
| return acceptBugreportAndGetSharedIntent(id, NOT_PENDING_SCREENSHOT); |
| } |
| |
| private Bundle acceptBugreportAndGetSharedIntent(int id, boolean pendingScreenshot) { |
| acceptBugreport(id, pendingScreenshot); |
| 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) { |
| acceptBugreport(id, NOT_PENDING_SCREENSHOT); |
| } |
| |
| private void acceptBugreport(int id, boolean pendingScreenshot) { |
| final int res = pendingScreenshot ? R.string.bugreport_finished_pending_screenshot_title |
| : R.string.bugreport_finished_title; |
| mUiBot.clickOnNotification(mContext.getString(res, 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 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); |
| 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(); |
| } |
| } |
| 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 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; |
| } |
| |
| /** |
| * 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"); |
| } |
| } |
| } |