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