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