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