xref: /aosp_15_r20/frameworks/base/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
1 /*
2  * Copyright (C) 2019 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.os.bugreports.tests;
18 
19 import static android.content.Context.RECEIVER_EXPORTED;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.assertTrue;
25 import static org.junit.Assert.fail;
26 
27 import android.Manifest;
28 import android.content.BroadcastReceiver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.os.BugreportManager;
33 import android.os.BugreportManager.BugreportCallback;
34 import android.os.BugreportParams;
35 import android.os.FileUtils;
36 import android.os.Handler;
37 import android.os.HandlerThread;
38 import android.os.ParcelFileDescriptor;
39 import android.os.Process;
40 import android.os.StrictMode;
41 import android.util.Log;
42 
43 import androidx.annotation.NonNull;
44 import androidx.test.InstrumentationRegistry;
45 import androidx.test.filters.LargeTest;
46 import androidx.test.uiautomator.By;
47 import androidx.test.uiautomator.BySelector;
48 import androidx.test.uiautomator.UiDevice;
49 import androidx.test.uiautomator.UiObject2;
50 import androidx.test.uiautomator.Until;
51 
52 import com.google.common.io.ByteStreams;
53 import com.google.common.io.Files;
54 
55 import org.junit.After;
56 import org.junit.Before;
57 import org.junit.Rule;
58 import org.junit.Test;
59 import org.junit.rules.ExternalResource;
60 import org.junit.rules.TestName;
61 import org.junit.runner.RunWith;
62 import org.junit.runners.JUnit4;
63 
64 import java.io.BufferedInputStream;
65 import java.io.BufferedOutputStream;
66 import java.io.File;
67 import java.io.FileInputStream;
68 import java.io.FileOutputStream;
69 import java.io.IOException;
70 import java.nio.charset.StandardCharsets;
71 import java.nio.file.Path;
72 import java.nio.file.Paths;
73 import java.util.ArrayList;
74 import java.util.Arrays;
75 import java.util.List;
76 import java.util.concurrent.CountDownLatch;
77 import java.util.concurrent.Executor;
78 import java.util.concurrent.TimeUnit;
79 import java.util.zip.ZipEntry;
80 import java.util.zip.ZipInputStream;
81 
82 /**
83  * Tests for BugreportManager API.
84  */
85 @RunWith(JUnit4.class)
86 public class BugreportManagerTest {
87     @Rule public TestName name = new TestName();
88     @Rule public ExtendedStrictModeVmPolicy mTemporaryVmPolicy = new ExtendedStrictModeVmPolicy();
89 
90     private static final String TAG = "BugreportManagerTest";
91     private static final long BUGREPORT_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(10);
92     private static final long DUMPSTATE_STARTUP_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
93     private static final long DUMPSTATE_TEARDOWN_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
94     private static final long UIAUTOMATOR_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
95 
96 
97     // A small timeout used when waiting for the result of a BugreportCallback to be received.
98     // This value must be at least 1000ms since there is an intentional delay in
99     // BugreportManagerServiceImpl in the error case.
100     private static final long CALLBACK_RESULT_TIMEOUT_MS = 1500;
101 
102     // Sent by Shell when its bugreport finishes (contains final bugreport/screenshot file name
103     // associated with the bugreport).
104     private static final String INTENT_BUGREPORT_FINISHED =
105             "com.android.internal.intent.action.BUGREPORT_FINISHED";
106 
107     private ArrayList<Path> mUiTracesPreDumped = new ArrayList<>(Arrays.asList(
108             Paths.get("/data/misc/perfetto-traces/bugreport/systrace.pftrace"),
109             Paths.get("/data/misc/wmtrace/wm_trace.winscope")
110     ));
111 
112     private Handler mHandler;
113     private Executor mExecutor;
114     private BugreportManager mBrm;
115     private File mBugreportFile;
116     private File mScreenshotFile;
117     private ParcelFileDescriptor mBugreportFd;
118     private ParcelFileDescriptor mScreenshotFd;
119 
120     @Before
setup()121     public void setup() throws Exception {
122         if (!android.tracing.Flags.perfettoIme()) {
123             mUiTracesPreDumped.add(Paths.get("/data/misc/wmtrace/ime_trace_clients.winscope"));
124             mUiTracesPreDumped.add(
125                     Paths.get("/data/misc/wmtrace/ime_trace_managerservice.winscope"));
126             mUiTracesPreDumped.add(Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"));
127         }
128 
129         if (!android.tracing.Flags.perfettoProtologTracing()) {
130             mUiTracesPreDumped.add(Paths.get("/data/misc/wmtrace/wm_log.winscope"));
131         }
132 
133         mHandler = createHandler();
134         mExecutor = (runnable) -> {
135             if (mHandler != null) {
136                 mHandler.post(() -> {
137                     runnable.run();
138                 });
139             }
140         };
141 
142         mBrm = getBugreportManager();
143         mBugreportFile = createTempFile("bugreport_" + name.getMethodName(), ".zip");
144         mScreenshotFile = createTempFile("screenshot_" + name.getMethodName(), ".png");
145         mBugreportFd = parcelFd(mBugreportFile);
146         mScreenshotFd = parcelFd(mScreenshotFile);
147 
148         getPermissions();
149     }
150 
151     @After
teardown()152     public void teardown() throws Exception {
153         dropPermissions();
154         FileUtils.closeQuietly(mBugreportFd);
155         FileUtils.closeQuietly(mScreenshotFd);
156     }
157 
158     @Test
normalFlow_wifi()159     public void normalFlow_wifi() throws Exception {
160         BugreportCallbackImpl callback = new BugreportCallbackImpl();
161         // wifi bugreport does not take screenshot
162         mBrm.startBugreport(mBugreportFd, null /*screenshotFd = null*/, wifi(),
163                 mExecutor, callback);
164         shareConsentDialog(ConsentReply.ALLOW);
165         waitTillDoneOrTimeout(callback);
166 
167         assertThat(callback.isDone()).isTrue();
168         // Wifi bugreports should not receive any progress.
169         assertThat(callback.hasReceivedProgress()).isFalse();
170         assertThat(mBugreportFile.length()).isGreaterThan(0L);
171         assertThat(callback.hasEarlyReportFinished()).isTrue();
172         assertFdsAreClosed(mBugreportFd);
173     }
174 
175     @LargeTest
176     @Test
normalFlow_interactive()177     public void normalFlow_interactive() throws Exception {
178         BugreportCallbackImpl callback = new BugreportCallbackImpl();
179         // interactive bugreport does not take screenshot
180         mBrm.startBugreport(mBugreportFd, null /*screenshotFd = null*/, interactive(),
181                 mExecutor, callback);
182         shareConsentDialog(ConsentReply.ALLOW);
183         waitTillDoneOrTimeout(callback);
184 
185         assertThat(callback.isDone()).isTrue();
186         // Interactive bugreports show progress updates.
187         assertThat(callback.hasReceivedProgress()).isTrue();
188         assertThat(mBugreportFile.length()).isGreaterThan(0L);
189         assertThat(callback.hasEarlyReportFinished()).isTrue();
190         assertFdsAreClosed(mBugreportFd);
191     }
192 
193     @LargeTest
194     @Test
normalFlow_full()195     public void normalFlow_full() throws Exception {
196         BugreportCallbackImpl callback = new BugreportCallbackImpl();
197         mBrm.startBugreport(mBugreportFd, mScreenshotFd, full(), mExecutor, callback);
198         shareConsentDialog(ConsentReply.ALLOW);
199         waitTillDoneOrTimeout(callback);
200 
201         assertThat(callback.isDone()).isTrue();
202         // bugreport and screenshot files shouldn't be empty when user consents.
203         assertThat(mBugreportFile.length()).isGreaterThan(0L);
204         assertThat(mScreenshotFile.length()).isGreaterThan(0L);
205         assertFdsAreClosed(mBugreportFd, mScreenshotFd);
206     }
207 
208     @LargeTest
209     @Test
preDumpUiData_then_fullWithUsePreDumpFlag()210     public void preDumpUiData_then_fullWithUsePreDumpFlag() throws Exception {
211         startPreDumpedUiTraces();
212 
213         mBrm.preDumpUiData();
214         waitTillDumpstateExitedOrTimeout();
215         List<File> expectedPreDumpedTraceFiles = copyFiles(mUiTracesPreDumped);
216 
217         BugreportCallbackImpl callback = new BugreportCallbackImpl();
218         mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor,
219                 callback);
220         shareConsentDialog(ConsentReply.ALLOW);
221         waitTillDoneOrTimeout(callback);
222 
223         stopPreDumpedUiTraces();
224 
225         assertThat(callback.isDone()).isTrue();
226         assertThat(mBugreportFile.length()).isGreaterThan(0L);
227         assertFdsAreClosed(mBugreportFd);
228 
229         assertThatBugreportContainsFiles(mUiTracesPreDumped);
230 
231         List<File> actualPreDumpedTraceFiles = extractFilesFromBugreport(mUiTracesPreDumped);
232         assertThatAllFileContentsAreEqual(actualPreDumpedTraceFiles, expectedPreDumpedTraceFiles);
233     }
234 
235     @LargeTest
236     @Test
preDumpData_then_fullWithoutUsePreDumpFlag_ignoresPreDump()237     public void preDumpData_then_fullWithoutUsePreDumpFlag_ignoresPreDump() throws Exception {
238         startPreDumpedUiTraces();
239 
240         // Simulate pre-dump, instead of taking a real one.
241         // In some corner cases, data dumped as part of the full bugreport could be the same as the
242         // pre-dumped data and this test would fail. Hence, here we create fake/artificial
243         // pre-dumped data that we know it won't match with the full bugreport data.
244         createFakeTraceFiles(mUiTracesPreDumped);
245 
246         List<File> preDumpedTraceFiles = copyFiles(mUiTracesPreDumped);
247 
248         BugreportCallbackImpl callback = new BugreportCallbackImpl();
249         mBrm.startBugreport(mBugreportFd, null, full(), mExecutor,
250                 callback);
251         shareConsentDialog(ConsentReply.ALLOW);
252         waitTillDoneOrTimeout(callback);
253 
254         stopPreDumpedUiTraces();
255 
256         assertThat(callback.isDone()).isTrue();
257         assertThat(mBugreportFile.length()).isGreaterThan(0L);
258         assertFdsAreClosed(mBugreportFd);
259 
260         assertThatBugreportContainsFiles(mUiTracesPreDumped);
261 
262         List<File> actualTraceFiles = extractFilesFromBugreport(mUiTracesPreDumped);
263         assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles);
264     }
265 
266     @LargeTest
267     @Test
noPreDumpData_then_fullWithUsePreDumpFlag_ignoresFlag()268     public void noPreDumpData_then_fullWithUsePreDumpFlag_ignoresFlag() throws Exception {
269         startPreDumpedUiTraces();
270 
271         mBrm.preDumpUiData();
272         waitTillDumpstateExitedOrTimeout();
273 
274         // Simulate lost of pre-dumped data.
275         // For example it can happen in this scenario:
276         // 1. Pre-dump data
277         // 2. Start bugreport + "use pre-dump" flag (USE AND REMOVE THE PRE-DUMP FROM DISK)
278         // 3. Start bugreport + "use pre-dump" flag (NO PRE-DUMP AVAILABLE ON DISK)
279         removeFilesIfNeeded(mUiTracesPreDumped);
280 
281         // Start bugreport with "use predump" flag. Because the pre-dumped data is not available
282         // the flag will be ignored and data will be dumped as in normal flow.
283         BugreportCallbackImpl callback = new BugreportCallbackImpl();
284         mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor,
285                 callback);
286         shareConsentDialog(ConsentReply.ALLOW);
287         waitTillDoneOrTimeout(callback);
288 
289         stopPreDumpedUiTraces();
290 
291         assertThat(callback.isDone()).isTrue();
292         assertThat(mBugreportFile.length()).isGreaterThan(0L);
293         assertFdsAreClosed(mBugreportFd);
294 
295         assertThatBugreportContainsFiles(mUiTracesPreDumped);
296     }
297 
298     @Test
simultaneousBugreportsNotAllowed()299     public void simultaneousBugreportsNotAllowed() throws Exception {
300         // Start bugreport #1
301         BugreportCallbackImpl callback = new BugreportCallbackImpl();
302         mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback);
303         // TODO(b/162389762) Make sure the wait time is reasonable
304         shareConsentDialog(ConsentReply.ALLOW);
305 
306         // Before #1 is done, try to start #2.
307         assertThat(callback.isDone()).isFalse();
308         BugreportCallbackImpl callback2 = new BugreportCallbackImpl();
309         File bugreportFile2 = createTempFile("bugreport_2_" + name.getMethodName(), ".zip");
310         File screenshotFile2 = createTempFile("screenshot_2_" + name.getMethodName(), ".png");
311         ParcelFileDescriptor bugreportFd2 = parcelFd(bugreportFile2);
312         ParcelFileDescriptor screenshotFd2 = parcelFd(screenshotFile2);
313         mBrm.startBugreport(bugreportFd2, screenshotFd2, wifi(), mExecutor, callback2);
314         Thread.sleep(CALLBACK_RESULT_TIMEOUT_MS);
315 
316         // Verify #2 encounters an error.
317         assertThat(callback2.getErrorCode()).isEqualTo(
318                 BugreportCallback.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS);
319         assertFdsAreClosed(bugreportFd2, screenshotFd2);
320 
321         // Cancel #1 so we can move on to the next test.
322         mBrm.cancelBugreport();
323         waitTillDoneOrTimeout(callback);
324         assertThat(callback.isDone()).isTrue();
325         assertFdsAreClosed(mBugreportFd, mScreenshotFd);
326     }
327 
328     @Test
cancelBugreport()329     public void cancelBugreport() throws Exception {
330         // Start a bugreport.
331         BugreportCallbackImpl callback = new BugreportCallbackImpl();
332         mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback);
333 
334         // Verify it's not finished yet.
335         assertThat(callback.isDone()).isFalse();
336 
337         // Try to cancel it, but first without DUMP permission.
338         dropPermissions();
339         try {
340             mBrm.cancelBugreport();
341             fail("Expected cancelBugreport to throw SecurityException without DUMP permission");
342         } catch (SecurityException expected) {
343         }
344         assertThat(callback.isDone()).isFalse();
345 
346         // Try again, with DUMP permission.
347         getPermissions();
348         mBrm.cancelBugreport();
349         waitTillDoneOrTimeout(callback);
350         assertThat(callback.isDone()).isTrue();
351         assertFdsAreClosed(mBugreportFd, mScreenshotFd);
352     }
353 
354     @Test
cancelBugreport_noReportStarted()355     public void cancelBugreport_noReportStarted() throws Exception {
356         // Without the native DumpstateService running, we don't get a SecurityException.
357         mBrm.cancelBugreport();
358     }
359 
360     @LargeTest
361     @Test
cancelBugreport_fromDifferentUid()362     public void cancelBugreport_fromDifferentUid() throws Exception {
363         assertThat(Process.myUid()).isNotEqualTo(Process.SHELL_UID);
364 
365         // Start a bugreport through ActivityManager's shell command - this starts a BR from the
366         // shell UID rather than our own.
367         BugreportBroadcastReceiver br = new BugreportBroadcastReceiver();
368         InstrumentationRegistry.getContext()
369                 .registerReceiver(
370                         br,
371                         new IntentFilter(INTENT_BUGREPORT_FINISHED),
372                         RECEIVER_EXPORTED);
373         UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
374                 .executeShellCommand("am bug-report");
375 
376         // The command triggers the report through a broadcast, so wait until dumpstate actually
377         // starts up, which may take a bit.
378         waitTillDumpstateRunningOrTimeout();
379 
380         try {
381             mBrm.cancelBugreport();
382             fail("Expected cancelBugreport to throw SecurityException when report started by "
383                     + "different UID");
384         } catch (SecurityException expected) {
385         } finally {
386             // Do this in the finally block so that even if this test case fails, we don't break
387             // other test cases unexpectedly due to the still-running shell report.
388             try {
389                 // The shell's BR is still running and should complete successfully.
390                 br.waitForBugreportFinished();
391             } finally {
392                 // The latch may fail for a number of reasons but we still need to unregister the
393                 // BroadcastReceiver.
394                 InstrumentationRegistry.getContext().unregisterReceiver(br);
395             }
396         }
397     }
398 
399     @Test
insufficientPermissions_throwsException()400     public void insufficientPermissions_throwsException() throws Exception {
401         dropPermissions();
402 
403         BugreportCallbackImpl callback = new BugreportCallbackImpl();
404         try {
405             mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback);
406             fail("Expected startBugreport to throw SecurityException without DUMP permission");
407         } catch (SecurityException expected) {
408         }
409         assertFdsAreClosed(mBugreportFd, mScreenshotFd);
410     }
411 
412     @Test
invalidBugreportMode_throwsException()413     public void invalidBugreportMode_throwsException() throws Exception {
414         BugreportCallbackImpl callback = new BugreportCallbackImpl();
415 
416         try {
417             mBrm.startBugreport(mBugreportFd, mScreenshotFd,
418                     new BugreportParams(25) /* unknown bugreport mode */, mExecutor, callback);
419             fail("Expected to throw IllegalArgumentException with unknown bugreport mode");
420         } catch (IllegalArgumentException expected) {
421         }
422         assertFdsAreClosed(mBugreportFd, mScreenshotFd);
423     }
424 
createHandler()425     private Handler createHandler() {
426         HandlerThread handlerThread = new HandlerThread("BugreportManagerTest");
427         handlerThread.start();
428         return new Handler(handlerThread.getLooper());
429     }
430 
431     /* Implementatiion of {@link BugreportCallback} that offers wrappers around execution result */
432     private static final class BugreportCallbackImpl extends BugreportCallback {
433         private int mErrorCode = -1;
434         private boolean mSuccess = false;
435         private boolean mReceivedProgress = false;
436         private boolean mEarlyReportFinished = false;
437         private final Object mLock = new Object();
438 
439         @Override
onProgress(float progress)440         public void onProgress(float progress) {
441             synchronized (mLock) {
442                 mReceivedProgress = true;
443             }
444         }
445 
446         @Override
onError(int errorCode)447         public void onError(int errorCode) {
448             synchronized (mLock) {
449                 mErrorCode = errorCode;
450                 Log.d(TAG, "bugreport errored.");
451             }
452         }
453 
454         @Override
onFinished()455         public void onFinished() {
456             synchronized (mLock) {
457                 Log.d(TAG, "bugreport finished.");
458                 mSuccess =  true;
459             }
460         }
461 
462         @Override
onEarlyReportFinished()463         public void onEarlyReportFinished() {
464             synchronized (mLock) {
465                 mEarlyReportFinished = true;
466             }
467         }
468 
469         /* Indicates completion; and ended up with a success or error. */
isDone()470         public boolean isDone() {
471             synchronized (mLock) {
472                 return (mErrorCode != -1) || mSuccess;
473             }
474         }
475 
getErrorCode()476         public int getErrorCode() {
477             synchronized (mLock) {
478                 return mErrorCode;
479             }
480         }
481 
isSuccess()482         public boolean isSuccess() {
483             synchronized (mLock) {
484                 return mSuccess;
485             }
486         }
487 
hasReceivedProgress()488         public boolean hasReceivedProgress() {
489             synchronized (mLock) {
490                 return mReceivedProgress;
491             }
492         }
493 
hasEarlyReportFinished()494         public boolean hasEarlyReportFinished() {
495             synchronized (mLock) {
496                 return mEarlyReportFinished;
497             }
498         }
499     }
500 
getBugreportManager()501     public static BugreportManager getBugreportManager() {
502         Context context = InstrumentationRegistry.getContext();
503         BugreportManager bm =
504                 (BugreportManager) context.getSystemService(Context.BUGREPORT_SERVICE);
505         if (bm == null) {
506             throw new AssertionError("Failed to get BugreportManager");
507         }
508         return bm;
509     }
createTempFile(String prefix, String extension)510     private static File createTempFile(String prefix, String extension) throws Exception {
511         final File f = File.createTempFile(prefix, extension);
512         f.setReadable(true, true);
513         f.setWritable(true, true);
514         f.deleteOnExit();
515         return f;
516     }
517 
startPreDumpedUiTraces()518     private static void startPreDumpedUiTraces() throws Exception {
519         // Perfetto traces
520         String perfettoConfig =
521                 "buffers: {\n"
522                 + "    size_kb: 2048\n"
523                 + "    fill_policy: RING_BUFFER\n"
524                 + "}\n"
525                 + "data_sources: {\n"
526                 + "    config {\n"
527                 + "        name: \"android.surfaceflinger.transactions\"\n"
528                 + "    }\n"
529                 + "}\n"
530                 + "bugreport_score: 10\n";
531         File tmp = createTempFile("tmp", ".cfg");
532         Files.write(perfettoConfig.getBytes(StandardCharsets.UTF_8), tmp);
533         InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
534                 "install -m 644 -o root -g root "
535                 + tmp.getAbsolutePath() + " /data/misc/perfetto-configs/bugreport-manager-test.cfg"
536         );
537         InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
538                 "perfetto --background-wait"
539                 + " --config /data/misc/perfetto-configs/bugreport-manager-test.cfg --txt"
540                 + " --out /data/misc/perfetto-traces/not-used.perfetto-trace"
541         );
542 
543         // Legacy traces
544         InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
545                 "cmd input_method tracing start"
546         );
547         InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
548                 "cmd window tracing start"
549         );
550     }
551 
stopPreDumpedUiTraces()552     private static void stopPreDumpedUiTraces() {
553         InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
554                 "cmd input_method tracing stop"
555         );
556         InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
557                 "cmd window tracing stop"
558         );
559         InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
560                 "service call SurfaceFlinger 1025 i32 0"
561         );
562     }
563 
assertThatBugreportContainsFiles(List<Path> paths)564     private void assertThatBugreportContainsFiles(List<Path> paths)
565             throws IOException {
566         List<Path> entries = listZipArchiveEntries(mBugreportFile);
567         for (Path pathInDevice : paths) {
568             Path pathInArchive = Paths.get("FS" + pathInDevice.toString());
569             assertThat(entries).contains(pathInArchive);
570         }
571     }
572 
extractFilesFromBugreport(List<Path> paths)573     private List<File> extractFilesFromBugreport(List<Path> paths) throws Exception {
574         List<File> files = new ArrayList<File>();
575         for (Path pathInDevice : paths) {
576             Path pathInArchive = Paths.get("FS" + pathInDevice.toString());
577             files.add(extractZipArchiveEntry(mBugreportFile, pathInArchive));
578         }
579         return files;
580     }
581 
listZipArchiveEntries(File archive)582     private static List<Path> listZipArchiveEntries(File archive) throws IOException {
583         ArrayList<Path> entries = new ArrayList<>();
584 
585         ZipInputStream stream = new ZipInputStream(
586                 new BufferedInputStream(new FileInputStream(archive)));
587 
588         for (ZipEntry entry = stream.getNextEntry(); entry != null; entry = stream.getNextEntry()) {
589             entries.add(Paths.get(entry.toString()));
590         }
591 
592         return entries;
593     }
594 
extractZipArchiveEntry(File archive, Path entryToExtract)595     private static File extractZipArchiveEntry(File archive, Path entryToExtract)
596             throws Exception {
597         File extractedFile = createTempFile(entryToExtract.getFileName().toString(), ".extracted");
598 
599         ZipInputStream is = new ZipInputStream(new FileInputStream(archive));
600         boolean hasFoundEntry = false;
601 
602         for (ZipEntry entry = is.getNextEntry(); entry != null; entry = is.getNextEntry()) {
603             if (entry.toString().equals(entryToExtract.toString())) {
604                 BufferedOutputStream os =
605                         new BufferedOutputStream(new FileOutputStream(extractedFile));
606                 ByteStreams.copy(is, os);
607                 os.close();
608                 hasFoundEntry = true;
609                 break;
610             }
611 
612             ByteStreams.exhaust(is); // skip entry
613         }
614 
615         is.closeEntry();
616         is.close();
617 
618         assertThat(hasFoundEntry).isTrue();
619 
620         return extractedFile;
621     }
622 
createFakeTraceFiles(List<Path> paths)623     private static void createFakeTraceFiles(List<Path> paths) throws Exception {
624         File src = createTempFile("fake", ".data");
625         Files.write("fake data".getBytes(StandardCharsets.UTF_8), src);
626 
627         for (Path path : paths) {
628             InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
629                     "install -m 644 -o system -g system "
630                     + src.getAbsolutePath() + " " + path.toString()
631             );
632         }
633 
634         // Dumpstate executes "perfetto --save-for-bugreport" as shell
635         InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
636                 "chown shell:shell /data/misc/perfetto-traces/bugreport/systrace.pftrace"
637         );
638     }
639 
copyFiles(List<Path> paths)640     private static List<File> copyFiles(List<Path> paths) throws Exception {
641         ArrayList<File> files = new ArrayList<File>();
642         for (Path src : paths) {
643             File dst = createTempFile(src.getFileName().toString(), ".copy");
644             InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
645                     "cp " + src.toString() + " " + dst.getAbsolutePath()
646             );
647             files.add(dst);
648         }
649         return files;
650     }
651 
removeFilesIfNeeded(List<Path> paths)652     private static void removeFilesIfNeeded(List<Path> paths) throws Exception {
653         for (Path path : paths) {
654             InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
655                     "rm -f " + path.toString()
656             );
657         }
658     }
659 
parcelFd(File file)660     private static ParcelFileDescriptor parcelFd(File file) throws Exception {
661         return ParcelFileDescriptor.open(file,
662                 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND);
663     }
664 
assertThatAllFileContentsAreEqual(List<File> actual, List<File> expected)665     private static void assertThatAllFileContentsAreEqual(List<File> actual, List<File> expected)
666             throws IOException {
667         if (actual.size() != expected.size()) {
668             fail("File lists have different size");
669         }
670         for (int i = 0; i < actual.size(); ++i) {
671             if (!Files.equal(actual.get(i), expected.get(i))) {
672                 fail("Contents of " + actual.get(i).toString()
673                         + " != " + expected.get(i).toString());
674             }
675         }
676     }
677 
assertThatAllFileContentsAreDifferent(List<File> a, List<File> b)678     private static void assertThatAllFileContentsAreDifferent(List<File> a, List<File> b)
679             throws IOException {
680         if (a.size() != b.size()) {
681             fail("File lists have different size");
682         }
683         for (int i = 0; i < a.size(); ++i) {
684             if (Files.equal(a.get(i), b.get(i))) {
685                 fail("Contents of " + a.get(i).toString() + " == " + b.get(i).toString());
686             }
687         }
688     }
689 
dropPermissions()690     private static void dropPermissions() {
691         InstrumentationRegistry.getInstrumentation().getUiAutomation()
692                 .dropShellPermissionIdentity();
693     }
694 
getPermissions()695     private static void getPermissions() {
696         InstrumentationRegistry.getInstrumentation().getUiAutomation()
697                 .adoptShellPermissionIdentity(Manifest.permission.DUMP);
698     }
699 
isDumpstateRunning()700     private static boolean isDumpstateRunning() {
701         String output;
702         try {
703             output = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
704                     .executeShellCommand("service list | grep dumpstate");
705         } catch (IOException e) {
706             Log.w(TAG, "Failed to check if dumpstate is running", e);
707             return false;
708         }
709         for (String line : output.trim().split("\n")) {
710             if (line.matches("^.*\\s+dumpstate:\\s+\\[.*\\]$")) {
711                 return true;
712             }
713         }
714         return false;
715     }
716 
assertFdIsClosed(ParcelFileDescriptor pfd)717     private static void assertFdIsClosed(ParcelFileDescriptor pfd) {
718         try {
719             int fd = pfd.getFd();
720             fail("Expected ParcelFileDescriptor argument to be closed, but got: " + fd);
721         } catch (IllegalStateException expected) {
722         }
723     }
724 
assertFdsAreClosed(ParcelFileDescriptor... pfds)725     private static void assertFdsAreClosed(ParcelFileDescriptor... pfds) {
726         for (int i = 0; i <  pfds.length; i++) {
727             assertFdIsClosed(pfds[i]);
728         }
729     }
730 
now()731     private static long now() {
732         return System.currentTimeMillis();
733     }
734 
waitTillDumpstateExitedOrTimeout()735     private static void waitTillDumpstateExitedOrTimeout() throws Exception {
736         long startTimeMs = now();
737         while (isDumpstateRunning()) {
738             Thread.sleep(500 /* .5s */);
739             if (now() - startTimeMs >= DUMPSTATE_TEARDOWN_TIMEOUT_MS) {
740                 break;
741             }
742             Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for dumpstate to exit");
743         }
744     }
745 
waitTillDumpstateRunningOrTimeout()746     private static void waitTillDumpstateRunningOrTimeout() throws Exception {
747         long startTimeMs = now();
748         while (!isDumpstateRunning()) {
749             Thread.sleep(500 /* .5s */);
750             if (now() - startTimeMs >= DUMPSTATE_STARTUP_TIMEOUT_MS) {
751                 break;
752             }
753             Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for dumpstate to start");
754         }
755     }
756 
waitTillDoneOrTimeout(BugreportCallbackImpl callback)757     private static void waitTillDoneOrTimeout(BugreportCallbackImpl callback) throws Exception {
758         long startTimeMs = now();
759         while (!callback.isDone()) {
760             Thread.sleep(1000 /* 1s */);
761             if (now() - startTimeMs >= BUGREPORT_TIMEOUT_MS) {
762                 break;
763             }
764             Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms for bugreport to finish");
765         }
766     }
767 
768     /*
769      * Returns a {@link BugreportParams} for wifi only bugreport.
770      *
771      * <p>Wifi bugreports have minimal content and are fast to run. They also suppress progress
772      * updates.
773      */
wifi()774     private static BugreportParams wifi() {
775         return new BugreportParams(BugreportParams.BUGREPORT_MODE_WIFI);
776     }
777 
778     /*
779      * Returns a {@link BugreportParams} for interactive bugreport that offers progress updates.
780      *
781      * <p>This is the typical bugreport taken by users. This can take on the order of minutes to
782      * finish.
783      */
interactive()784     private static BugreportParams interactive() {
785         return new BugreportParams(BugreportParams.BUGREPORT_MODE_INTERACTIVE);
786     }
787 
788     /*
789      * Returns a {@link BugreportParams} for full bugreport that includes a screenshot.
790      *
791      * <p> This can take on the order of minutes to finish
792      */
full()793     private static BugreportParams full() {
794         return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL);
795     }
796 
797     /*
798      * Returns a {@link BugreportParams} for full bugreport that reuses pre-dumped data.
799      *
800      * <p> This can take on the order of minutes to finish
801      */
fullWithUsePreDumpFlag()802     private static BugreportParams fullWithUsePreDumpFlag() {
803         return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL,
804                 BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA);
805     }
806 
807     /* Allow/deny the consent dialog to sharing bugreport data or check existence only. */
808     private enum ConsentReply {
809         ALLOW,
810         DENY,
811         TIMEOUT
812     }
813 
814     /*
815      * Ensure the consent dialog is shown and take action according to <code>consentReply<code/>.
816      * It will fail if the dialog is not shown when <code>ignoreNotFound<code/> is false.
817      */
shareConsentDialog(@onNull ConsentReply consentReply)818     private void shareConsentDialog(@NonNull ConsentReply consentReply) throws Exception {
819         mTemporaryVmPolicy.permitIncorrectContextUse();
820         final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
821 
822         // Unlock before finding/clicking an object.
823         device.wakeUp();
824         device.executeShellCommand("wm dismiss-keyguard");
825 
826         final BySelector consentTitleObj = By.res("android", "alertTitle");
827         if (!device.wait(Until.hasObject(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS)) {
828             fail("The consent dialog is not found");
829         }
830         if (consentReply.equals(ConsentReply.TIMEOUT)) {
831             return;
832         }
833         final BySelector selector;
834         if (consentReply.equals(ConsentReply.ALLOW)) {
835             selector = By.res("android", "button1");
836             Log.d(TAG, "Allow the consent dialog");
837         } else { // ConsentReply.DENY
838             selector = By.res("android", "button2");
839             Log.d(TAG, "Deny the consent dialog");
840         }
841         final UiObject2 btnObj = device.findObject(selector);
842         assertNotNull("The button of consent dialog is not found", btnObj);
843         btnObj.click();
844 
845         Log.d(TAG, "Wait for the dialog to be dismissed");
846         assertTrue(device.wait(Until.gone(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS));
847     }
848 
849     private class BugreportBroadcastReceiver extends BroadcastReceiver {
850         Intent mBugreportFinishedIntent = null;
851         final CountDownLatch mLatch;
852 
BugreportBroadcastReceiver()853         BugreportBroadcastReceiver() {
854             mLatch = new CountDownLatch(1);
855         }
856 
857         @Override
onReceive(Context context, Intent intent)858         public void onReceive(Context context, Intent intent) {
859             setBugreportFinishedIntent(intent);
860             mLatch.countDown();
861         }
862 
setBugreportFinishedIntent(Intent intent)863         private void setBugreportFinishedIntent(Intent intent) {
864             mBugreportFinishedIntent = intent;
865         }
866 
getBugreportFinishedIntent()867         public Intent getBugreportFinishedIntent() {
868             return mBugreportFinishedIntent;
869         }
870 
waitForBugreportFinished()871         public void waitForBugreportFinished() throws Exception {
872             if (!mLatch.await(BUGREPORT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
873                 throw new Exception("Failed to receive BUGREPORT_FINISHED in "
874                         + BUGREPORT_TIMEOUT_MS + " ms.");
875             }
876         }
877     }
878 
879     /**
880      * A rule to change strict mode vm policy temporarily till test method finished.
881      *
882      * To permit the non-visual context usage in tests while taking bugreports need user consent,
883      * or UiAutomator/BugreportManager.DumpstateListener would run into error.
884      * UiDevice#findObject creates UiObject2, its Gesture object and ViewConfiguration and
885      * UiObject2#click need to know bounds. Both of them access to WindowManager internally without
886      * visual context comes from InstrumentationRegistry and violate the policy.
887      * Also <code>DumpstateListener<code/> violate the policy when onScreenshotTaken is called.
888      *
889      * TODO(b/161201609) Remove this class once violations fixed.
890      */
891     static class ExtendedStrictModeVmPolicy extends ExternalResource {
892         private boolean mWasVmPolicyChanged = false;
893         private StrictMode.VmPolicy mOldVmPolicy;
894 
895         @Override
after()896         protected void after() {
897             restoreVmPolicyIfNeeded();
898         }
899 
permitIncorrectContextUse()900         public void permitIncorrectContextUse() {
901             // Allow to call multiple times without losing old policy.
902             if (mOldVmPolicy == null) {
903                 mOldVmPolicy = StrictMode.getVmPolicy();
904             }
905             StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
906                     .detectAll()
907                     .permitIncorrectContextUse()
908                     .penaltyLog()
909                     .build());
910             mWasVmPolicyChanged = true;
911         }
912 
restoreVmPolicyIfNeeded()913         private void restoreVmPolicyIfNeeded() {
914             if (mWasVmPolicyChanged && mOldVmPolicy != null) {
915                 StrictMode.setVmPolicy(mOldVmPolicy);
916                 mOldVmPolicy = null;
917             }
918         }
919     }
920 }
921