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