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