BugreportReceiverTest.java revision 9ca5e5639048375100acd98a40ad82d8b699c575
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.shell;
18
19import static android.test.MoreAsserts.assertContainsRegex;
20import static com.android.shell.ActionSendMultipleConsumerActivity.UI_NAME;
21import static com.android.shell.BugreportProgressService.EXTRA_BUGREPORT;
22import static com.android.shell.BugreportProgressService.EXTRA_ID;
23import static com.android.shell.BugreportProgressService.EXTRA_MAX;
24import static com.android.shell.BugreportProgressService.EXTRA_NAME;
25import static com.android.shell.BugreportProgressService.EXTRA_PID;
26import static com.android.shell.BugreportProgressService.EXTRA_SCREENSHOT;
27import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_FINISHED;
28import static com.android.shell.BugreportProgressService.INTENT_BUGREPORT_STARTED;
29import static com.android.shell.BugreportProgressService.POLLING_FREQUENCY;
30import static com.android.shell.BugreportProgressService.SCREENSHOT_DELAY_SECONDS;
31
32import java.io.BufferedOutputStream;
33import java.io.BufferedWriter;
34import java.io.ByteArrayOutputStream;
35import java.io.File;
36import java.io.FileOutputStream;
37import java.io.IOException;
38import java.io.InputStream;
39import java.io.OutputStreamWriter;
40import java.io.Writer;
41import java.text.NumberFormat;
42import java.util.ArrayList;
43import java.util.List;
44import java.util.SortedSet;
45import java.util.TreeSet;
46import java.util.zip.ZipEntry;
47import java.util.zip.ZipInputStream;
48import java.util.zip.ZipOutputStream;
49
50import libcore.io.Streams;
51import android.app.ActivityManager;
52import android.app.ActivityManager.RunningServiceInfo;
53import android.app.Instrumentation;
54import android.app.NotificationManager;
55import android.content.Context;
56import android.content.ContextWrapper;
57import android.content.Intent;
58import android.net.Uri;
59import android.os.Bundle;
60import android.os.SystemProperties;
61import android.service.notification.StatusBarNotification;
62import android.support.test.uiautomator.UiDevice;
63import android.support.test.uiautomator.UiObject;
64import android.support.test.uiautomator.UiObjectNotFoundException;
65import android.support.test.uiautomator.UiSelector;
66import android.test.InstrumentationTestCase;
67import android.test.suitebuilder.annotation.LargeTest;
68import android.text.TextUtils;
69import android.text.format.DateUtils;
70import android.util.Log;
71
72import com.android.shell.ActionSendMultipleConsumerActivity.CustomActionSendMultipleListener;
73import com.android.shell.BugreportProgressService;
74
75/**
76 * Integration tests for {@link BugreportReceiver}.
77 * <p>
78 * These tests don't mock any component and rely on external UI components (like the notification
79 * bar and activity chooser), which can make them unreliable and slow.
80 * <p>
81 * The general workflow is:
82 * <ul>
83 * <li>creates the bug report files
84 * <li>generates the BUGREPORT_FINISHED intent
85 * <li>emulate user actions to share the intent with a custom activity
86 * <li>asserts the extras received by the custom activity
87 * </ul>
88 * <p>
89 * <strong>NOTE</strong>: these tests only work if the device is unlocked.
90 */
91@LargeTest
92public class BugreportReceiverTest extends InstrumentationTestCase {
93    private static final String TAG = "BugreportReceiverTest";
94
95    // Timeout for UI operations, in milliseconds.
96    private static final int TIMEOUT = (int) POLLING_FREQUENCY * 4;
97
98    // Timeout for when waiting for a screenshot to finish.
99    private static final int SAFE_SCREENSHOT_DELAY = SCREENSHOT_DELAY_SECONDS + 10;
100
101    private static final String BUGREPORTS_DIR = "bugreports";
102    private static final String BUGREPORT_FILE = "test_bugreport.txt";
103    private static final String ZIP_FILE = "test_bugreport.zip";
104    private static final String ZIP_FILE2 = "test_bugreport2.zip";
105    private static final String SCREENSHOT_FILE = "test_screenshot.png";
106
107    private static final String BUGREPORT_CONTENT = "Dump, might as well dump!\n";
108    private static final String SCREENSHOT_CONTENT = "A picture is worth a thousand words!\n";
109
110    private static final int PID = 42;
111    private static final int PID2 = 24;
112    private static final int ID = 108;
113    private static final int ID2 = 801;
114    private static final String PROGRESS_PROPERTY = "dumpstate." + PID + ".progress";
115    private static final String MAX_PROPERTY = "dumpstate." + PID + ".max";
116    private static final String NAME_PROPERTY = "dumpstate." + PID + ".name";
117    private static final String NAME = "BUG, Y U NO REPORT?";
118    private static final String NAME2 = "A bugreport's life";
119    private static final String NEW_NAME = "Bug_Forrest_Bug";
120    private static final String NEW_NAME2 = "BugsyReportsy";
121    private static final String TITLE = "Wimbugdom Champion 2015";
122    private static final String TITLE2 = "Master of the Universe";
123    private static final String DESCRIPTION = "One's description...";
124    private static final String DESCRIPTION2 = "...is another's treasure.";
125
126    private static final String NO_DESCRIPTION = null;
127    private static final String NO_NAME = null;
128    private static final String NO_SCREENSHOT = null;
129    private static final String NO_TITLE = null;
130    private static final int NO_ID = 0;
131    private static final boolean RENAMED_SCREENSHOTS = true;
132    private static final boolean DIDNT_RENAME_SCREENSHOTS = false;
133
134    private static final boolean PENDING_SCREENSHOT = true;
135    private static final boolean NOT_PENDING_SCREENSHOT = false;
136
137    private String mDescription;
138
139    private String mPlainTextPath;
140    private String mZipPath;
141    private String mZipPath2;
142    private String mScreenshotPath;
143
144    private Context mContext;
145    private UiBot mUiBot;
146    private CustomActionSendMultipleListener mListener;
147
148    @Override
149    protected void setUp() throws Exception {
150        Log.i(TAG, "#### setup() on " + getName());
151        Instrumentation instrumentation = getInstrumentation();
152        mContext = instrumentation.getTargetContext();
153        mUiBot = new UiBot(UiDevice.getInstance(instrumentation), TIMEOUT);
154        mListener = ActionSendMultipleConsumerActivity.getListener(mContext);
155
156        cancelExistingNotifications();
157
158        mPlainTextPath = getPath(BUGREPORT_FILE);
159        mZipPath = getPath(ZIP_FILE);
160        mZipPath2 = getPath(ZIP_FILE2);
161        mScreenshotPath = getPath(SCREENSHOT_FILE);
162        createTextFile(mPlainTextPath, BUGREPORT_CONTENT);
163        createTextFile(mScreenshotPath, SCREENSHOT_CONTENT);
164        createZipFile(mZipPath, BUGREPORT_FILE, BUGREPORT_CONTENT);
165        createZipFile(mZipPath2, BUGREPORT_FILE, BUGREPORT_CONTENT);
166
167        // Creates a multi-line description.
168        StringBuilder sb = new StringBuilder();
169        for (int i = 1; i <= 20; i++) {
170            sb.append("All work and no play makes Shell a dull app!\n");
171        }
172        mDescription = sb.toString();
173
174        BugreportPrefs.setWarningState(mContext, BugreportPrefs.STATE_HIDE);
175    }
176
177    public void testProgress() throws Exception {
178        resetProperties();
179        sendBugreportStarted(1000);
180        waitForScreenshotButtonEnabled(true);
181
182        assertProgressNotification(NAME, 0f);
183
184        SystemProperties.set(PROGRESS_PROPERTY, "108");
185        assertProgressNotification(NAME, 10.80f);
186
187        assertProgressNotification(NAME, 50.00f);
188
189        SystemProperties.set(PROGRESS_PROPERTY, "950");
190        assertProgressNotification(NAME, 95.00f);
191
192        // Make sure progress never goes back...
193        SystemProperties.set(MAX_PROPERTY, "2000");
194        Thread.sleep(POLLING_FREQUENCY + DateUtils.SECOND_IN_MILLIS);
195        assertProgressNotification(NAME, 95.00f);
196
197        SystemProperties.set(PROGRESS_PROPERTY, "1000");
198        assertProgressNotification(NAME, 95.00f);
199
200        // ...only forward...
201        SystemProperties.set(PROGRESS_PROPERTY, "1902");
202        assertProgressNotification(NAME, 95.10f);
203
204        SystemProperties.set(PROGRESS_PROPERTY, "1960");
205        assertProgressNotification(NAME, 98.00f);
206
207        // ...but never more than the capped value.
208        SystemProperties.set(PROGRESS_PROPERTY, "2000");
209        assertProgressNotification(NAME, 99.00f);
210
211        SystemProperties.set(PROGRESS_PROPERTY, "3000");
212        assertProgressNotification(NAME, 99.00f);
213
214        Bundle extras =
215                sendBugreportFinishedAndGetSharedIntent(ID, mPlainTextPath, mScreenshotPath);
216        assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, ZIP_FILE,
217                NAME, NO_TITLE, NO_DESCRIPTION, 1, RENAMED_SCREENSHOTS);
218
219        assertServiceNotRunning();
220    }
221
222    public void testProgress_cancel() throws Exception {
223        resetProperties();
224        sendBugreportStarted(1000);
225        waitForScreenshotButtonEnabled(true);
226
227        final NumberFormat nf = NumberFormat.getPercentInstance();
228        nf.setMinimumFractionDigits(2);
229        nf.setMaximumFractionDigits(2);
230
231        assertProgressNotification(NAME, 00.00f);
232
233        openProgressNotification(ID);
234        UiObject cancelButton = mUiBot.getVisibleObject(mContext.getString(
235                com.android.internal.R.string.cancel).toUpperCase());
236        mUiBot.click(cancelButton, "cancel_button");
237
238        waitForService(false);
239    }
240
241    public void testProgress_takeExtraScreenshot() throws Exception {
242        takeExtraScreenshotTest(false);
243    }
244
245    public void testProgress_takeExtraScreenshotServiceDiesAfterScreenshotTaken() throws Exception {
246        takeExtraScreenshotTest(true);
247    }
248
249    private void takeExtraScreenshotTest(boolean serviceDies) throws Exception {
250        resetProperties();
251        sendBugreportStarted(1000);
252
253        waitForScreenshotButtonEnabled(true);
254        takeScreenshot();
255        assertScreenshotButtonEnabled(false);
256        waitForScreenshotButtonEnabled(true);
257
258        sendBugreportFinished(ID, mPlainTextPath, mScreenshotPath);
259
260        if (serviceDies) {
261            waitShareNotification(ID);
262            killService();
263        }
264
265        Bundle extras = acceptBugreportAndGetSharedIntent(ID);
266        assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, ZIP_FILE,
267                NAME, NO_TITLE, NO_DESCRIPTION, 2, RENAMED_SCREENSHOTS);
268
269        assertServiceNotRunning();
270    }
271
272    public void testScreenshotFinishesAfterBugreport() throws Exception {
273        screenshotFinishesAfterBugreportTest(false);
274    }
275
276    public void testScreenshotFinishesAfterBugreportAndServiceDiesBeforeSharing() throws Exception {
277        screenshotFinishesAfterBugreportTest(true);
278    }
279
280    private void screenshotFinishesAfterBugreportTest(boolean serviceDies) throws Exception {
281        resetProperties();
282
283        sendBugreportStarted(1000);
284        sendBugreportFinished(ID, mPlainTextPath, NO_SCREENSHOT);
285        waitShareNotification(ID);
286
287        // There's no indication in the UI about the screenshot finish, so just sleep like a baby...
288        Thread.sleep(SAFE_SCREENSHOT_DELAY * DateUtils.SECOND_IN_MILLIS);
289
290        if (serviceDies) {
291            killService();
292        }
293
294        Bundle extras = acceptBugreportAndGetSharedIntent(ID);
295        assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT, ID, PID, ZIP_FILE,
296                NAME, NO_TITLE, NO_DESCRIPTION, 1, RENAMED_SCREENSHOTS);
297
298        assertServiceNotRunning();
299    }
300
301    public void testProgress_changeDetailsInvalidInput() throws Exception {
302        resetProperties();
303        sendBugreportStarted(1000);
304        waitForScreenshotButtonEnabled(true);
305
306        DetailsUi detailsUi = new DetailsUi(mUiBot, ID);
307
308        // Check initial name.
309        detailsUi.assertName(NAME);
310
311        // Change name - it should have changed system property once focus is changed.
312        detailsUi.focusOnName();
313        detailsUi.nameField.setText(NEW_NAME);
314        detailsUi.focusAwayFromName();
315        assertPropertyValue(NAME_PROPERTY, NEW_NAME);
316
317        // Cancel the dialog to make sure property was restored.
318        detailsUi.clickCancel();
319        assertPropertyValue(NAME_PROPERTY, NAME);
320
321        // Now try to set an invalid name.
322        detailsUi.reOpen();
323        detailsUi.nameField.setText("/etc/passwd");
324        detailsUi.clickOk();
325        assertPropertyValue(NAME_PROPERTY, "_etc_passwd");
326
327        // Finally, make the real changes.
328        detailsUi.reOpen();
329        detailsUi.nameField.setText(NEW_NAME);
330        detailsUi.titleField.setText(TITLE);
331        detailsUi.descField.setText(mDescription);
332
333        detailsUi.clickOk();
334
335        assertPropertyValue(NAME_PROPERTY, NEW_NAME);
336        assertProgressNotification(NEW_NAME, 00.00f);
337
338        Bundle extras = sendBugreportFinishedAndGetSharedIntent(ID, mPlainTextPath,
339                mScreenshotPath);
340        assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, TITLE,
341                NEW_NAME, TITLE, mDescription, 1, RENAMED_SCREENSHOTS);
342
343        assertServiceNotRunning();
344    }
345
346    public void testProgress_changeDetailsPlainBugreport() throws Exception {
347        changeDetailsTest(true);
348    }
349
350    public void testProgress_changeDetailsZippedBugreport() throws Exception {
351        changeDetailsTest(false);
352    }
353
354    public void changeDetailsTest(boolean plainText) throws Exception {
355        resetProperties();
356        sendBugreportStarted(1000);
357        waitForScreenshotButtonEnabled(true);
358
359        DetailsUi detailsUi = new DetailsUi(mUiBot, ID);
360
361        // Check initial name.
362        detailsUi.assertName(NAME);
363
364        // Change fields.
365        detailsUi.reOpen();
366        detailsUi.nameField.setText(NEW_NAME);
367        detailsUi.titleField.setText(TITLE);
368        detailsUi.descField.setText(mDescription);
369
370        detailsUi.clickOk();
371
372        assertPropertyValue(NAME_PROPERTY, NEW_NAME);
373        assertProgressNotification(NEW_NAME, 00.00f);
374
375        Bundle extras = sendBugreportFinishedAndGetSharedIntent(ID,
376                plainText? mPlainTextPath : mZipPath, mScreenshotPath);
377        assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, TITLE,
378                NEW_NAME, TITLE, mDescription, 1, RENAMED_SCREENSHOTS);
379
380        assertServiceNotRunning();
381    }
382
383    public void testProgress_changeJustDetailsTouchingDetails() throws Exception {
384        changeJustDetailsTest(true);
385    }
386
387    public void testProgress_changeJustDetailsTouchingNotification() throws Exception {
388        changeJustDetailsTest(false);
389    }
390
391    private void changeJustDetailsTest(boolean touchDetails) throws Exception {
392        resetProperties();
393        sendBugreportStarted(1000);
394        waitForScreenshotButtonEnabled(true);
395
396        DetailsUi detailsUi = new DetailsUi(mUiBot, ID, touchDetails);
397
398        detailsUi.nameField.setText("");
399        detailsUi.titleField.setText("");
400        detailsUi.descField.setText(mDescription);
401        detailsUi.clickOk();
402
403        Bundle extras = sendBugreportFinishedAndGetSharedIntent(ID, mZipPath, mScreenshotPath);
404        assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, ZIP_FILE,
405                NO_NAME, NO_TITLE, mDescription, 1, DIDNT_RENAME_SCREENSHOTS);
406
407        assertServiceNotRunning();
408    }
409
410    /*
411     * TODO: this test can be flanky because it relies in the order the notifications are displayed,
412     * since mUiBot gets the first notification.
413     * Ideally, openProgressNotification() should return the whole notification, so DetailsUi
414     * could use it and find children instead, but unfortunately the notification object hierarchy
415     * is too complex and getting it from the notification text object would be to fragile
416     * (for instance, it could require navigating many parents up in the hierarchy).
417     */
418    public void testProgress_changeJustDetailsIsClearedOnSecondBugreport() throws Exception {
419        resetProperties();
420        sendBugreportStarted(ID, PID, NAME, 1000);
421        waitForScreenshotButtonEnabled(true);
422
423        DetailsUi detailsUi = new DetailsUi(mUiBot, ID);
424        detailsUi.assertName(NAME);
425        detailsUi.assertTitle("");
426        detailsUi.assertDescription("");
427        detailsUi.nameField.setText(NEW_NAME);
428        detailsUi.titleField.setText(TITLE);
429        detailsUi.descField.setText(DESCRIPTION);
430        detailsUi.clickOk();
431
432        sendBugreportStarted(ID2, PID2, NAME2, 1000);
433
434        sendBugreportFinished(ID, mZipPath, mScreenshotPath);
435        Bundle extras = acceptBugreportAndGetSharedIntent(ID, PENDING_SCREENSHOT);
436
437        detailsUi = new DetailsUi(mUiBot, ID2);
438        detailsUi.assertName(NAME2);
439        detailsUi.assertTitle("");
440        detailsUi.assertDescription("");
441        detailsUi.nameField.setText(NEW_NAME2);
442        detailsUi.titleField.setText(TITLE2);
443        detailsUi.descField.setText(DESCRIPTION2);
444        detailsUi.clickOk();
445
446        // Must use a different zip file otherwise it will fail because zip already contains
447        // title.txt and description.txt entries.
448        extras = sendBugreportFinishedAndGetSharedIntent(ID2, mZipPath2, NO_SCREENSHOT);
449        assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT, ID2, PID2, TITLE2,
450                NEW_NAME2, TITLE2, DESCRIPTION2, 1, RENAMED_SCREENSHOTS);
451
452        assertServiceNotRunning();
453    }
454
455    /**
456     * Tests the scenario where the initial screenshot and dumpstate are finished while the user
457     * is changing the info in the details screen.
458     */
459    public void testProgress_bugreportAndScreenshotFinishedWhileChangingDetails() throws Exception {
460        bugreportFinishedWhileChangingDetailsTest(false);
461    }
462
463    /**
464     * Tests the scenario where dumpstate is finished while the user is changing the info in the
465     * details screen, but the initial screenshot finishes afterwards.
466     */
467    public void testProgress_bugreportFinishedWhileChangingDetails() throws Exception {
468        bugreportFinishedWhileChangingDetailsTest(true);
469    }
470
471    private void bugreportFinishedWhileChangingDetailsTest(boolean waitScreenshot) throws Exception {
472        resetProperties();
473        sendBugreportStarted(1000);
474        if (waitScreenshot) {
475            waitForScreenshotButtonEnabled(true);
476        }
477
478        DetailsUi detailsUi = new DetailsUi(mUiBot, ID);
479
480        // Finish the bugreport while user's still typing the name.
481        detailsUi.nameField.setText(NEW_NAME);
482        sendBugreportFinished(ID, mPlainTextPath, mScreenshotPath);
483
484        // Wait until the share notification is received...
485        waitShareNotification(ID);
486        // ...then close notification bar.
487        mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
488
489        // Make sure UI was updated properly.
490        assertFalse("didn't disable name on UI", detailsUi.nameField.isEnabled());
491        assertEquals("didn't revert name on UI", NAME, detailsUi.nameField.getText().toString());
492
493        // Finish changing other fields.
494        detailsUi.titleField.setText(TITLE);
495        detailsUi.descField.setText(mDescription);
496        detailsUi.clickOk();
497
498        // Finally, share bugreport.
499        Bundle extras = acceptBugreportAndGetSharedIntent(ID);
500        assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT, ID, PID, TITLE,
501                NAME, TITLE, mDescription, 1, RENAMED_SCREENSHOTS);
502
503        assertServiceNotRunning();
504    }
505
506    public void testBugreportFinished_withWarning() throws Exception {
507        // Explicitly shows the warning.
508        BugreportPrefs.setWarningState(mContext, BugreportPrefs.STATE_SHOW);
509
510        // Send notification and click on share.
511        sendBugreportFinished(NO_ID, mPlainTextPath, null);
512        acceptBugreport(NO_ID);
513
514        // Handle the warning
515        mUiBot.getVisibleObject(mContext.getString(R.string.bugreport_confirm));
516        // TODO: get ok and showMessageAgain from the dialog reference above
517        UiObject showMessageAgain =
518                mUiBot.getVisibleObject(mContext.getString(R.string.bugreport_confirm_repeat));
519        mUiBot.click(showMessageAgain, "show-message-again");
520        UiObject ok = mUiBot.getVisibleObject(mContext.getString(com.android.internal.R.string.ok));
521        mUiBot.click(ok, "ok");
522
523        // Share the bugreport.
524        mUiBot.chooseActivity(UI_NAME);
525        Bundle extras = mListener.getExtras();
526        assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT);
527
528        // Make sure it's hidden now.
529        int newState = BugreportPrefs.getWarningState(mContext, BugreportPrefs.STATE_UNKNOWN);
530        assertEquals("Didn't change state", BugreportPrefs.STATE_HIDE, newState);
531    }
532
533    public void testShareBugreportAfterServiceDies() throws Exception {
534        sendBugreportFinished(NO_ID, mPlainTextPath, NO_SCREENSHOT);
535        killService();
536        Bundle extras = acceptBugreportAndGetSharedIntent(NO_ID);
537        assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT);
538    }
539
540    public void testBugreportFinished_plainBugreportAndScreenshot() throws Exception {
541        Bundle extras = sendBugreportFinishedAndGetSharedIntent(mPlainTextPath, mScreenshotPath);
542        assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT);
543    }
544
545    public void testBugreportFinished_zippedBugreportAndScreenshot() throws Exception {
546        Bundle extras = sendBugreportFinishedAndGetSharedIntent(mZipPath, mScreenshotPath);
547        assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT);
548    }
549
550    public void testBugreportFinished_plainBugreportAndNoScreenshot() throws Exception {
551        Bundle extras = sendBugreportFinishedAndGetSharedIntent(mPlainTextPath, NO_SCREENSHOT);
552        assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT);
553    }
554
555    public void testBugreportFinished_zippedBugreportAndNoScreenshot() throws Exception {
556        Bundle extras = sendBugreportFinishedAndGetSharedIntent(mZipPath, NO_SCREENSHOT);
557        assertActionSendMultiple(extras, BUGREPORT_CONTENT, NO_SCREENSHOT);
558    }
559
560    private void cancelExistingNotifications() {
561        NotificationManager nm = NotificationManager.from(mContext);
562        for (StatusBarNotification notification : nm.getActiveNotifications()) {
563            int id = notification.getId();
564            Log.i(TAG, "Canceling existing notification (id=" + id + ")");
565            nm.cancel(id);
566        }
567    }
568
569    private void assertProgressNotification(String name, float percent) {
570        // TODO: it currently looks for 3 distinct objects, without taking advantage of their
571        // relationship.
572        openProgressNotification(ID);
573        Log.v(TAG, "Looking for progress notification details: '" + name + "-" + percent + "'");
574        mUiBot.getObject(name);
575        // TODO: need a way to get the ProgresBar from the "android:id/progress" UIObject...
576    }
577
578    private UiObject openProgressNotification(int id) {
579        String title = mContext.getString(R.string.bugreport_in_progress_title, id);
580        Log.v(TAG, "Looking for progress notification title: '" + title + "'");
581        return mUiBot.getNotification(title);
582    }
583
584    void resetProperties() {
585        // TODO: call method to remove property instead
586        SystemProperties.set(PROGRESS_PROPERTY, "Reset");
587        SystemProperties.set(MAX_PROPERTY, "Reset");
588        SystemProperties.set(NAME_PROPERTY, "Reset");
589    }
590
591    /**
592     * Sends a "bugreport started" intent with the default values.
593     */
594    private void sendBugreportStarted(int max) throws Exception {
595        sendBugreportStarted(ID, PID, NAME, max);
596    }
597
598    private void sendBugreportStarted(int id, int pid, String name, int max) throws Exception {
599        Intent intent = new Intent(INTENT_BUGREPORT_STARTED);
600        intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
601        intent.putExtra(EXTRA_ID, id);
602        intent.putExtra(EXTRA_PID, pid);
603        intent.putExtra(EXTRA_NAME, name);
604        intent.putExtra(EXTRA_MAX, max);
605        mContext.sendBroadcast(intent);
606    }
607
608    /**
609     * Sends a "bugreport finished" intent and waits for the result.
610     *
611     * @return extras sent in the shared intent.
612     */
613    private Bundle sendBugreportFinishedAndGetSharedIntent(String bugreportPath,
614            String screenshotPath) {
615        return sendBugreportFinishedAndGetSharedIntent(NO_ID, bugreportPath, screenshotPath);
616    }
617
618    /**
619     * Sends a "bugreport finished" intent and waits for the result.
620     *
621     * @return extras sent in the shared intent.
622     */
623    private Bundle sendBugreportFinishedAndGetSharedIntent(int id, String bugreportPath,
624            String screenshotPath) {
625        sendBugreportFinished(id, bugreportPath, screenshotPath);
626        return acceptBugreportAndGetSharedIntent(id, NOT_PENDING_SCREENSHOT);
627    }
628
629    /**
630     * Accepts the notification to share the finished bugreport and waits for the result.
631     *
632     * @return extras sent in the shared intent.
633     */
634    private Bundle acceptBugreportAndGetSharedIntent(int id) {
635        return acceptBugreportAndGetSharedIntent(id, NOT_PENDING_SCREENSHOT);
636    }
637
638    private Bundle acceptBugreportAndGetSharedIntent(int id, boolean pendingScreenshot) {
639        acceptBugreport(id, pendingScreenshot);
640        mUiBot.chooseActivity(UI_NAME);
641        return mListener.getExtras();
642    }
643
644    /**
645     * Waits for the notification to share the finished bugreport.
646     */
647    private void waitShareNotification(int id) {
648        mUiBot.getNotification(mContext.getString(R.string.bugreport_finished_title, id));
649    }
650
651    /**
652     * Accepts the notification to share the finished bugreport.
653     */
654    private void acceptBugreport(int id) {
655        acceptBugreport(id, NOT_PENDING_SCREENSHOT);
656    }
657
658    private void acceptBugreport(int id, boolean pendingScreenshot) {
659        final int res = pendingScreenshot ? R.string.bugreport_finished_pending_screenshot_title
660                : R.string.bugreport_finished_title;
661        mUiBot.clickOnNotification(mContext.getString(res, id));
662    }
663
664    /**
665     * Sends a "bugreport finished" intent.
666     */
667    private void sendBugreportFinished(int id, String bugreportPath, String screenshotPath) {
668        Intent intent = new Intent(INTENT_BUGREPORT_FINISHED);
669        intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
670        if (id != NO_ID) {
671            intent.putExtra(EXTRA_ID, id);
672        }
673        if (bugreportPath != null) {
674            intent.putExtra(EXTRA_BUGREPORT, bugreportPath);
675        }
676        if (screenshotPath != null) {
677            intent.putExtra(EXTRA_SCREENSHOT, screenshotPath);
678        }
679
680        mContext.sendBroadcast(intent);
681    }
682
683    /**
684     * Asserts the proper {@link Intent#ACTION_SEND_MULTIPLE} intent was sent.
685     */
686    private void assertActionSendMultiple(Bundle extras, String bugreportContent,
687            String screenshotContent) throws IOException {
688        assertActionSendMultiple(extras, bugreportContent, screenshotContent, ID, PID, ZIP_FILE,
689                NO_NAME, NO_TITLE, NO_DESCRIPTION, 0, DIDNT_RENAME_SCREENSHOTS);
690    }
691
692    /**
693     * Asserts the proper {@link Intent#ACTION_SEND_MULTIPLE} intent was sent.
694     *
695     * @param extras extras received in the intent
696     * @param bugreportContent expected content in the bugreport file
697     * @param screenshotContent expected content in the screenshot file (sent by dumpstate), if any
698     * @param id emulated dumpstate id
699     * @param pid emulated dumpstate pid
700     * @param name expected subject
701     * @param name bugreport name as provided by the user (or received by dumpstate)
702     * @param title bugreport name as provided by the user
703     * @param description bugreport description as provided by the user
704     * @param numberScreenshots expected number of screenshots taken by Shell.
705     * @param renamedScreenshots whether the screenshots are expected to be renamed
706     */
707    private void assertActionSendMultiple(Bundle extras, String bugreportContent,
708            String screenshotContent, int id, int pid, String subject,
709            String name, String title, String description,
710            int numberScreenshots, boolean renamedScreenshots) throws IOException {
711        String body = extras.getString(Intent.EXTRA_TEXT);
712        assertContainsRegex("missing build info",
713                SystemProperties.get("ro.build.description"), body);
714        assertContainsRegex("missing serial number",
715                SystemProperties.get("ro.serialno"), body);
716        if (description != null) {
717            assertContainsRegex("missing description", description, body);
718        }
719
720        assertEquals("wrong subject", subject, extras.getString(Intent.EXTRA_SUBJECT));
721
722        List<Uri> attachments = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
723        int expectedNumberScreenshots = numberScreenshots;
724        if (screenshotContent != null) {
725            expectedNumberScreenshots ++; // Add screenshot received by dumpstate
726        }
727        int expectedSize = expectedNumberScreenshots + 1; // All screenshots plus the bugreport file
728        assertEquals("wrong number of attachments (" + attachments + ")",
729                expectedSize, attachments.size());
730
731        // Need to interact through all attachments, since order is not guaranteed.
732        Uri zipUri = null;
733        List<Uri> screenshotUris = new ArrayList<>(expectedNumberScreenshots);
734        for (Uri attachment : attachments) {
735            if (attachment.getPath().endsWith(".zip")) {
736                zipUri = attachment;
737            }
738            if (attachment.getPath().endsWith(".png")) {
739                screenshotUris.add(attachment);
740            }
741        }
742        assertNotNull("did not get .zip attachment", zipUri);
743        assertZipContent(zipUri, BUGREPORT_FILE, BUGREPORT_CONTENT);
744        if (!TextUtils.isEmpty(title)) {
745            assertZipContent(zipUri, "title.txt", title);
746        }
747        if (!TextUtils.isEmpty(description)) {
748            assertZipContent(zipUri, "description.txt", description);
749        }
750
751        // URI of the screenshot taken by dumpstate.
752        Uri externalScreenshotUri = null;
753        SortedSet<String> internalScreenshotNames = new TreeSet<>();
754        for (Uri screenshotUri : screenshotUris) {
755            String screenshotName = screenshotUri.getLastPathSegment();
756            if (screenshotName.endsWith(SCREENSHOT_FILE)) {
757                externalScreenshotUri = screenshotUri;
758            } else {
759                internalScreenshotNames.add(screenshotName);
760            }
761        }
762        // Check external screenshot
763        if (screenshotContent != null) {
764            assertNotNull("did not get .png attachment for external screenshot",
765                    externalScreenshotUri);
766            assertContent(externalScreenshotUri, SCREENSHOT_CONTENT);
767        } else {
768            assertNull("should not have .png attachment for external screenshot",
769                    externalScreenshotUri);
770        }
771        // Check internal screenshots.
772        SortedSet<String> expectedNames = new TreeSet<>();
773        for (int i = 1 ; i <= numberScreenshots; i++) {
774            String prefix = renamedScreenshots  ? name : Integer.toString(pid);
775            String expectedName = "screenshot-" + prefix + "-" + i + ".png";
776            expectedNames.add(expectedName);
777        }
778        // Ideally we should use MoreAsserts, but the error message in case of failure is not
779        // really useful.
780        assertEquals("wrong names for internal screenshots",
781                expectedNames, internalScreenshotNames);
782    }
783
784    private void assertContent(Uri uri, String expectedContent) throws IOException {
785        Log.v(TAG, "assertContents(uri=" + uri);
786        try (InputStream is = mContext.getContentResolver().openInputStream(uri)) {
787            String actualContent = new String(Streams.readFully(is));
788            assertEquals("wrong content for '" + uri + "'", expectedContent, actualContent);
789        }
790    }
791
792    private void assertZipContent(Uri uri, String entryName, String expectedContent)
793            throws IOException, IOException {
794        Log.v(TAG, "assertZipEntry(uri=" + uri + ", entryName=" + entryName);
795        try (ZipInputStream zis = new ZipInputStream(mContext.getContentResolver().openInputStream(
796                uri))) {
797            ZipEntry entry;
798            while ((entry = zis.getNextEntry()) != null) {
799                Log.v(TAG, "Zip entry: " + entry.getName());
800                if (entry.getName().equals(entryName)) {
801                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
802                    Streams.copy(zis, bos);
803                    String actualContent = new String(bos.toByteArray(), "UTF-8");
804                    bos.close();
805                    assertEquals("wrong content for zip entry'" + entryName + "' on '" + uri + "'",
806                            expectedContent, actualContent);
807                    return;
808                }
809            }
810        }
811        fail("Did not find entry '" + entryName + "' on file '" + uri + "'");
812    }
813
814    private void assertPropertyValue(String key, String expectedValue) {
815        String actualValue = SystemProperties.get(key);
816        assertEquals("Wrong value for property '" + key + "'", expectedValue, actualValue);
817    }
818
819    private void assertServiceNotRunning() {
820        String service = BugreportProgressService.class.getName();
821        assertFalse("Service '" + service + "' is still running", isServiceRunning(service));
822    }
823
824    private void killService() {
825        waitForService(true);
826        Log.v(TAG, "Stopping service");
827        boolean stopped = mContext.stopService(new Intent(mContext, BugreportProgressService.class));
828        Log.d(TAG, "stopService returned " + stopped);
829        waitForService(false);
830        assertServiceNotRunning();  // Sanity check.
831    }
832
833    private boolean isServiceRunning(String name) {
834        ActivityManager manager = (ActivityManager) mContext
835                .getSystemService(Context.ACTIVITY_SERVICE);
836        for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
837            if (service.service.getClassName().equals(name)) {
838                return true;
839            }
840        }
841        return false;
842    }
843
844    private void waitForService(boolean expectRunning) {
845        String service = BugreportProgressService.class.getName();
846        boolean actualRunning;
847        for (int i = 1; i <= 5; i++) {
848            actualRunning = isServiceRunning(service);
849            Log.d(TAG, "Attempt " + i + " to check status of service '"
850                    + service + "': expected=" + expectRunning + ", actual= " + actualRunning);
851            if (actualRunning == expectRunning) {
852                return;
853            }
854            try {
855                Thread.sleep(DateUtils.SECOND_IN_MILLIS);
856            } catch (InterruptedException e) {
857                Log.w(TAG, "thread interrupted");
858                Thread.currentThread().interrupt();
859            }
860        }
861        if (!expectRunning) {
862            // Typically happens when service is waiting for a screenshot to finish.
863            Log.w(TAG, "Service didn't stop; try to kill it again");
864            killService();
865            return;
866        }
867
868        fail("Service status didn't change to " + expectRunning);
869    }
870
871    private void createTextFile(String path, String content) throws IOException {
872        Log.v(TAG, "createFile(" + path + ")");
873        try (Writer writer = new BufferedWriter(new OutputStreamWriter(
874                new FileOutputStream(path)))) {
875            writer.write(content);
876        }
877    }
878
879    private void createZipFile(String path, String entryName, String content) throws IOException {
880        Log.v(TAG, "createZipFile(" + path + ", " + entryName + ")");
881        try (ZipOutputStream zos = new ZipOutputStream(
882                new BufferedOutputStream(new FileOutputStream(path)))) {
883            ZipEntry entry = new ZipEntry(entryName);
884            zos.putNextEntry(entry);
885            byte[] data = content.getBytes();
886            zos.write(data, 0, data.length);
887            zos.closeEntry();
888        }
889    }
890
891    private String getPath(String file) {
892        File rootDir = new ContextWrapper(mContext).getFilesDir();
893        File dir = new File(rootDir, BUGREPORTS_DIR);
894        if (!dir.exists()) {
895            Log.i(TAG, "Creating directory " + dir);
896            assertTrue("Could not create directory " + dir, dir.mkdir());
897        }
898        String path = new File(dir, file).getAbsolutePath();
899        Log.v(TAG, "Path for '" + file + "': " + path);
900        return path;
901    }
902
903    /**
904     * Gets the notification button used to take a screenshot.
905     */
906    private UiObject getScreenshotButton() {
907        openProgressNotification(ID);
908        return mUiBot.getVisibleObject(
909                mContext.getString(R.string.bugreport_screenshot_action).toUpperCase());
910    }
911
912    /**
913     * Takes a screenshot using the system notification.
914     */
915    private void takeScreenshot() throws Exception {
916        UiObject screenshotButton = getScreenshotButton();
917        mUiBot.click(screenshotButton, "screenshot_button");
918    }
919
920    private UiObject waitForScreenshotButtonEnabled(boolean expectedEnabled) throws Exception {
921        UiObject screenshotButton = getScreenshotButton();
922        int maxAttempts = SAFE_SCREENSHOT_DELAY;
923        int i = 0;
924        do {
925            boolean enabled = screenshotButton.isEnabled();
926            if (enabled == expectedEnabled) {
927                return screenshotButton;
928            }
929            i++;
930            Log.v(TAG, "Sleeping for 1 second while waiting for screenshot.enable to be "
931                    + expectedEnabled + " (attempt " + i + ")");
932            Thread.sleep(DateUtils.SECOND_IN_MILLIS);
933        } while (i <= maxAttempts);
934        fail("screenshot.enable didn't change to " + expectedEnabled + " in " + maxAttempts + "s");
935        return screenshotButton;
936    }
937
938    private void assertScreenshotButtonEnabled(boolean expectedEnabled) throws Exception {
939        UiObject screenshotButton = getScreenshotButton();
940        assertEquals("wrong state for screenshot button ", expectedEnabled,
941                screenshotButton.isEnabled());
942    }
943
944    /**
945     * Helper class containing the UiObjects present in the bugreport info dialog.
946     */
947    private final class DetailsUi {
948
949        final UiObject detailsButton;
950        final UiObject nameField;
951        final UiObject titleField;
952        final UiObject descField;
953        final UiObject okButton;
954        final UiObject cancelButton;
955
956        /**
957         * Gets the UI objects by opening the progress notification and clicking DETAILS.
958         */
959        DetailsUi(UiBot uiBot, int id) throws UiObjectNotFoundException {
960            this(uiBot, id, true);
961        }
962
963        /**
964         * Gets the UI objects by opening the progress notification and clicking on DETAILS or in
965         * the notification itself.
966         */
967        DetailsUi(UiBot uiBot, int id, boolean clickDetails) throws UiObjectNotFoundException {
968            UiObject notification = openProgressNotification(id);
969            detailsButton = mUiBot.getVisibleObject(mContext.getString(
970                    R.string.bugreport_info_action).toUpperCase());
971
972            if (clickDetails) {
973                mUiBot.click(detailsButton, "details_button");
974            } else {
975                mUiBot.click(notification, "notification");
976            }
977            // TODO: unhardcode resource ids
978            UiObject dialogTitle = mUiBot.getVisibleObjectById("android:id/alertTitle");
979            assertEquals("Wrong title", mContext.getString(R.string.bugreport_info_dialog_title,
980                    id), dialogTitle.getText().toString());
981            nameField = mUiBot.getVisibleObjectById("com.android.shell:id/name");
982            titleField = mUiBot.getVisibleObjectById("com.android.shell:id/title");
983            descField = mUiBot.getVisibleObjectById("com.android.shell:id/description");
984            okButton = mUiBot.getObjectById("android:id/button1");
985            cancelButton = mUiBot.getObjectById("android:id/button2");
986        }
987
988        private void assertField(String name, UiObject field, String expected)
989                throws UiObjectNotFoundException {
990            String actual = field.getText().toString();
991            assertEquals("Wrong value on field '" + name + "'", expected, actual);
992        }
993
994        void assertName(String expected) throws UiObjectNotFoundException {
995            assertField("name", nameField, expected);
996        }
997
998        void assertTitle(String expected) throws UiObjectNotFoundException {
999            assertField("title", titleField, expected);
1000        }
1001
1002        void assertDescription(String expected) throws UiObjectNotFoundException {
1003            assertField("description", descField, expected);
1004        }
1005
1006        /**
1007         * Set focus on the name field so it can be validated once focus is lost.
1008         */
1009        void focusOnName() throws UiObjectNotFoundException {
1010            mUiBot.click(nameField, "name_field");
1011            assertTrue("name_field not focused", nameField.isFocused());
1012        }
1013
1014        /**
1015         * Takes focus away from the name field so it can be validated.
1016         */
1017        void focusAwayFromName() throws UiObjectNotFoundException {
1018            mUiBot.click(titleField, "title_field"); // Change focus.
1019            mUiBot.pressBack(); // Dismiss keyboard.
1020            assertFalse("name_field is focused", nameField.isFocused());
1021        }
1022
1023        void reOpen() {
1024            openProgressNotification(ID);
1025            mUiBot.click(detailsButton, "details_button");
1026        }
1027
1028        void clickOk() {
1029            mUiBot.click(okButton, "details_ok_button");
1030        }
1031
1032        void clickCancel() {
1033            mUiBot.click(cancelButton, "details_cancel_button");
1034        }
1035    }
1036}
1037