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