• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 android.carrierapi.cts;
18 
19 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 import static com.google.common.truth.Truth.assertWithMessage;
23 
24 import static org.junit.Assert.fail;
25 
26 import android.os.BugreportManager;
27 import android.os.BugreportManager.BugreportCallback;
28 import android.os.BugreportParams;
29 import android.os.FileUtils;
30 import android.os.ParcelFileDescriptor;
31 import android.platform.test.annotations.SystemUserOnly;
32 import android.util.Log;
33 
34 import androidx.test.InstrumentationRegistry;
35 import androidx.test.runner.AndroidJUnit4;
36 import androidx.test.uiautomator.By;
37 import androidx.test.uiautomator.BySelector;
38 import androidx.test.uiautomator.UiDevice;
39 import androidx.test.uiautomator.UiObject2;
40 import androidx.test.uiautomator.Until;
41 
42 import com.android.compatibility.common.util.CddTest;
43 import com.android.compatibility.common.util.PollingCheck;
44 
45 import org.junit.After;
46 import org.junit.Before;
47 import org.junit.Rule;
48 import org.junit.Test;
49 import org.junit.rules.TestName;
50 import org.junit.runner.RunWith;
51 
52 import java.io.File;
53 import java.util.concurrent.TimeUnit;
54 
55 /**
56  * Unit tests for {@link BugreportManager}'s carrier functionality, specifically "connectivity"
57  * bugreports.
58  *
59  * <p>Structure is largely adapted from
60  * frameworks/base/core/tests/bugreports/.../BugreportManagerTest.java.
61  *
62  * <p>Test using `atest CtsCarrierApiTestCases:BugreportManagerTest` or `make cts -j64 &&
63  * cts-tradefed run cts -m CtsCarrierApiTestCases --test
64  * android.carrierapi.cts.BugreportManagerTest`
65  *
66  * <p>TODO(b/211774553) consider enforcing BR content. Will likely have to be a host-side test for
67  * performance reasons.
68  */
69 @SystemUserOnly(reason = "BugreportManager requires calls to originate from the primary user")
70 @RunWith(AndroidJUnit4.class)
71 public class BugreportManagerTest extends BaseCarrierApiTest {
72     private static final String TAG = "BugreportManagerTest";
73 
74     // See BugreportManagerServiceImpl#BUGREPORT_SERVICE.
75     private static final String BUGREPORT_SERVICE = "bugreportd";
76 
77     private static final long BUGREPORT_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(10);
78     private static final long UIAUTOMATOR_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
79     private static final long ONEWAY_CALLBACK_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
80     // This value is defined in dumpstate.cpp:TELEPHONY_REPORT_USER_CONSENT_TIMEOUT_MS. Because the
81     // consent dialog is so large and important, the user *must* be given at least 2 minutes to read
82     // it before it times out.
83     private static final long MINIMUM_CONSENT_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(2);
84 
85     private static final BySelector CONSENT_DIALOG_TITLE_SELECTOR = By.res("android", "alertTitle");
86 
87     @Rule public TestName name = new TestName();
88 
89     private BugreportManager mBugreportManager;
90     private File mBugreportFile;
91     private ParcelFileDescriptor mBugreportFd;
92     private File mScreenshotFile;
93     private ParcelFileDescriptor mScreenshotFd;
94 
95     @Before
setUp()96     public void setUp() throws Exception {
97         mBugreportManager = getContext().getSystemService(BugreportManager.class);
98 
99         killCurrentBugreportIfRunning();
100         mBugreportFile = createTempFile("bugreport_" + name.getMethodName(), ".zip");
101         mBugreportFd = parcelFd(mBugreportFile);
102         // Should never be written for anything a carrier app can trigger; several tests assert that
103         // this file has no content.
104         mScreenshotFile = createTempFile("screenshot_" + name.getMethodName(), ".png");
105         mScreenshotFd = parcelFd(mScreenshotFile);
106     }
107 
108     @After
tearDown()109     public void tearDown() throws Exception {
110         if (!werePreconditionsSatisfied()) return;
111 
112         FileUtils.closeQuietly(mBugreportFd);
113         FileUtils.closeQuietly(mScreenshotFd);
114         killCurrentBugreportIfRunning();
115     }
116 
117     @Test
118     @CddTest(requirement = "9.8.10/C-1-1")
startConnectivityBugreport()119     public void startConnectivityBugreport() throws Exception {
120         BugreportCallbackImpl callback = new BugreportCallbackImpl();
121 
122         assertThat(callback.hasEarlyReportFinished()).isFalse();
123         mBugreportManager.startConnectivityBugreport(mBugreportFd, Runnable::run, callback);
124         setConsentDialogReply(ConsentReply.ALLOW);
125         waitUntilDoneOrTimeout(callback);
126 
127         assertThat(callback.isSuccess()).isTrue();
128         assertThat(callback.hasEarlyReportFinished()).isTrue();
129         assertThat(callback.hasReceivedProgress()).isTrue();
130         assertThat(mBugreportFile.length()).isGreaterThan(0L);
131         assertFdIsClosed(mBugreportFd);
132     }
133 
134     @Test
135     @CddTest(requirement = "9.8.10/C-1-3")
startConnectivityBugreport_consentDenied()136     public void startConnectivityBugreport_consentDenied() throws Exception {
137         BugreportCallbackImpl callback = new BugreportCallbackImpl();
138 
139         mBugreportManager.startConnectivityBugreport(mBugreportFd, Runnable::run, callback);
140         setConsentDialogReply(ConsentReply.DENY);
141         waitUntilDoneOrTimeout(callback);
142 
143         assertThat(callback.getErrorCode())
144                 .isEqualTo(BugreportCallback.BUGREPORT_ERROR_USER_DENIED_CONSENT);
145         assertThat(callback.hasReceivedProgress()).isTrue();
146         assertThat(mBugreportFile.length()).isEqualTo(0L);
147         assertFdIsClosed(mBugreportFd);
148     }
149 
150     @Test
151     @CddTest(requirement = "9.8.10/C-1-3")
startConnectivityBugreport_consentTimeout()152     public void startConnectivityBugreport_consentTimeout() throws Exception {
153         BugreportCallbackImpl callback = new BugreportCallbackImpl();
154         long startTimeMillis = System.currentTimeMillis();
155 
156         mBugreportManager.startConnectivityBugreport(mBugreportFd, Runnable::run, callback);
157         setConsentDialogReply(ConsentReply.NONE_TIMEOUT);
158         waitUntilDoneOrTimeout(callback);
159 
160         assertThat(callback.getErrorCode())
161                 .isEqualTo(BugreportCallback.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT);
162         assertThat(callback.hasReceivedProgress()).isTrue();
163         assertThat(mBugreportFile.length()).isEqualTo(0L);
164         assertFdIsClosed(mBugreportFd);
165         // Ensure the dialog was displaying long enough.
166         assertThat(System.currentTimeMillis() - startTimeMillis)
167                 .isAtLeast(MINIMUM_CONSENT_TIMEOUT_MILLIS);
168         // The dialog may still be displaying, dismiss it if so.
169         dismissConsentDialogIfPresent();
170     }
171 
172     @Test
simultaneousBugreportsNotAllowed()173     public void simultaneousBugreportsNotAllowed() throws Exception {
174         BugreportCallbackImpl callback1 = new BugreportCallbackImpl();
175         BugreportCallbackImpl callback2 = new BugreportCallbackImpl();
176         File bugreportFile2 = createTempFile("bugreport_2_" + name.getMethodName(), ".zip");
177         ParcelFileDescriptor bugreportFd2 = parcelFd(bugreportFile2);
178 
179         assertThat(callback1.hasEarlyReportFinished()).isFalse();
180         // Start the first report, but don't accept the consent dialog or wait for the callback to
181         // complete yet.
182         mBugreportManager.startConnectivityBugreport(mBugreportFd, Runnable::run, callback1);
183 
184         // Attempting to start a second report immediately gets us a concurrency error.
185         mBugreportManager.startConnectivityBugreport(bugreportFd2, Runnable::run, callback2);
186         // Since IDumpstateListener#onError is oneway, it's not guaranteed that binder has delivered
187         // the callback to us yet, even though BugreportManagerServiceImpl sends it before returning
188         // from #startBugreport.
189         PollingCheck.check(
190                 "No terminal callback received for the second bugreport",
191                 ONEWAY_CALLBACK_TIMEOUT_MILLIS,
192                 callback2::isDone);
193         assertThat(callback2.getErrorCode())
194                 .isEqualTo(BugreportCallback.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS);
195 
196         // Now wait for the first report to complete normally.
197         setConsentDialogReply(ConsentReply.ALLOW);
198         waitUntilDoneOrTimeout(callback1);
199 
200         assertThat(callback1.isSuccess()).isTrue();
201         assertThat(callback1.hasEarlyReportFinished()).isTrue();
202         assertThat(callback1.hasReceivedProgress()).isTrue();
203         assertThat(mBugreportFile.length()).isGreaterThan(0L);
204         assertFdIsClosed(mBugreportFd);
205         // The second report never got any details filled in.
206         assertThat(callback2.hasReceivedProgress()).isFalse();
207         assertThat(bugreportFile2.length()).isEqualTo(0L);
208         assertFdIsClosed(bugreportFd2);
209     }
210 
211     @Test
212     @CddTest(requirement = "9.8.10/C-1-3")
cancelBugreport()213     public void cancelBugreport() throws Exception {
214         BugreportCallbackImpl callback = new BugreportCallbackImpl();
215 
216         // Start the report, but don't accept the consent dialog or wait for the callback to
217         // complete yet.
218         mBugreportManager.startConnectivityBugreport(mBugreportFd, Runnable::run, callback);
219 
220         assertThat(callback.isDone()).isFalse();
221 
222         // Cancel and wait for the final result.
223         mBugreportManager.cancelBugreport();
224         waitUntilDoneOrTimeout(callback);
225 
226         assertThat(callback.getErrorCode()).isEqualTo(BugreportCallback.BUGREPORT_ERROR_RUNTIME);
227         assertThat(mBugreportFile.length()).isEqualTo(0L);
228         assertFdIsClosed(mBugreportFd);
229     }
230 
231     @Test
232     @CddTest(requirement = "9.8.10/C-1-1")
startBugreport_connectivityBugreport()233     public void startBugreport_connectivityBugreport() throws Exception {
234         BugreportCallbackImpl callback = new BugreportCallbackImpl();
235 
236         assertThat(callback.hasEarlyReportFinished()).isFalse();
237         // Carrier apps that compile with the system SDK have visibility to use this API, so we need
238         // to enforce that the additional parameters can't be abused to e.g. surreptitiously capture
239         // screenshots.
240         mBugreportManager.startBugreport(
241                 mBugreportFd,
242                 mScreenshotFd,
243                 new BugreportParams(BugreportParams.BUGREPORT_MODE_TELEPHONY),
244                 Runnable::run,
245                 callback);
246         setConsentDialogReply(ConsentReply.ALLOW);
247         waitUntilDoneOrTimeout(callback);
248 
249         assertThat(callback.isSuccess()).isTrue();
250         assertThat(callback.hasEarlyReportFinished()).isTrue();
251         assertThat(callback.hasReceivedProgress()).isTrue();
252         assertThat(mBugreportFile.length()).isGreaterThan(0L);
253         assertFdIsClosed(mBugreportFd);
254         // Screenshots are never captured for connectivity bugreports, even if an FD is passed in.
255         assertThat(mScreenshotFile.length()).isEqualTo(0L);
256         assertFdIsClosed(mScreenshotFd);
257     }
258 
259     @Test
startBugreport_fullBugreport()260     public void startBugreport_fullBugreport() throws Exception {
261         assertSecurityExceptionThrownForMode(BugreportParams.BUGREPORT_MODE_FULL);
262     }
263 
264     @Test
startBugreport_interactiveBugreport()265     public void startBugreport_interactiveBugreport() throws Exception {
266         assertSecurityExceptionThrownForMode(BugreportParams.BUGREPORT_MODE_INTERACTIVE);
267     }
268 
269     @Test
startBugreport_remoteBugreport()270     public void startBugreport_remoteBugreport() throws Exception {
271         assertSecurityExceptionThrownForMode(BugreportParams.BUGREPORT_MODE_REMOTE);
272     }
273 
274     @Test
startBugreport_wearBugreport()275     public void startBugreport_wearBugreport() throws Exception {
276         assertSecurityExceptionThrownForMode(BugreportParams.BUGREPORT_MODE_WEAR);
277     }
278 
279     @Test
startBugreport_wifiBugreport()280     public void startBugreport_wifiBugreport() throws Exception {
281         assertSecurityExceptionThrownForMode(BugreportParams.BUGREPORT_MODE_WIFI);
282     }
283 
284     @Test
startBugreport_defaultBugreport()285     public void startBugreport_defaultBugreport() throws Exception {
286         // BUGREPORT_MODE_DEFAULT (6) is defined by the AIDL, but isn't accepted by
287         // BugreportManagerServiceImpl or exposed in BugreportParams.
288         assertExceptionThrownForMode(6, IllegalArgumentException.class);
289     }
290 
291     @Test
startBugreport_negativeMode()292     public void startBugreport_negativeMode() throws Exception {
293         assertExceptionThrownForMode(-1, IllegalArgumentException.class);
294     }
295 
296     @Test
startBugreport_invalidMode()297     public void startBugreport_invalidMode() throws Exception {
298         // Current max is BUGREPORT_MODE_DEFAULT (6) as defined by the AIDL.
299         assertExceptionThrownForMode(7, IllegalArgumentException.class);
300     }
301 
302     /* Implementatiion of {@link BugreportCallback} that offers wrappers around execution result */
303     private static final class BugreportCallbackImpl extends BugreportCallback {
304         private int mErrorCode = -1;
305         private boolean mSuccess = false;
306         private boolean mReceivedProgress = false;
307         private boolean mEarlyReportFinished = false;
308         private final Object mLock = new Object();
309 
310         @Override
onProgress(float progress)311         public synchronized void onProgress(float progress) {
312             mReceivedProgress = true;
313         }
314 
315         @Override
onError(int errorCode)316         public synchronized void onError(int errorCode) {
317             Log.d(TAG, "Bugreport errored");
318             mErrorCode = errorCode;
319         }
320 
321         @Override
onFinished()322         public synchronized void onFinished() {
323             Log.d(TAG, "Bugreport finished");
324             mSuccess = true;
325         }
326 
327         @Override
onEarlyReportFinished()328         public synchronized void onEarlyReportFinished() {
329             mEarlyReportFinished = true;
330         }
331 
332         /* Indicates completion; and ended up with a success or error. */
isDone()333         public synchronized boolean isDone() {
334             return (mErrorCode != -1) || mSuccess;
335         }
336 
getErrorCode()337         public synchronized int getErrorCode() {
338             return mErrorCode;
339         }
340 
isSuccess()341         public synchronized boolean isSuccess() {
342             return mSuccess;
343         }
344 
hasReceivedProgress()345         public synchronized boolean hasReceivedProgress() {
346             return mReceivedProgress;
347         }
348 
hasEarlyReportFinished()349         public synchronized boolean hasEarlyReportFinished() {
350             return mEarlyReportFinished;
351         }
352     }
353 
354     /**
355      * Kills the current bugreport if one is in progress to prevent failing test cases from
356      * cascading into other cases and causing flakes.
357      */
killCurrentBugreportIfRunning()358     private static void killCurrentBugreportIfRunning() throws Exception {
359         runShellCommand("setprop ctl.stop " + BUGREPORT_SERVICE);
360     }
361 
362     /** Allow/deny the consent dialog to sharing bugreport data, or just check existence. */
363     private enum ConsentReply {
364         // Touch the positive button.
365         ALLOW,
366         // Touch the negative button.
367         DENY,
368         // Just verify that the dialog has appeared, but make no touches.
369         NONE_TIMEOUT,
370     }
371 
setConsentDialogReply(ConsentReply consentReply)372     private void setConsentDialogReply(ConsentReply consentReply) throws Exception {
373         UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
374 
375         // No need to wake + dismiss keyguard here; CTS respects our DISABLE_KEYGUARD permission.
376         if (!device.wait(
377                 Until.hasObject(CONSENT_DIALOG_TITLE_SELECTOR), UIAUTOMATOR_TIMEOUT_MILLIS)) {
378             fail("The consent dialog can't be found");
379         }
380 
381         final BySelector replySelector;
382         switch (consentReply) {
383             case ALLOW:
384                 Log.d(TAG, "Allow the consent dialog");
385                 replySelector = By.res("android", "button1");
386                 break;
387             case DENY:
388                 Log.d(TAG, "Deny the consent dialog");
389                 replySelector = By.res("android", "button2");
390                 break;
391             case NONE_TIMEOUT:
392             default:
393                 // Not making a choice, just leave the dialog up now that we know it exists. It will
394                 // eventually time out, but we don't wait for that here.
395                 return;
396         }
397         UiObject2 replyButton = device.findObject(replySelector);
398         assertWithMessage("The button of consent dialog is not found")
399                 .that(replyButton)
400                 .isNotNull();
401         replyButton.click();
402 
403         assertThat(
404                         device.wait(
405                                 Until.gone(CONSENT_DIALOG_TITLE_SELECTOR),
406                                 UIAUTOMATOR_TIMEOUT_MILLIS))
407                 .isTrue();
408     }
409 
dismissConsentDialogIfPresent()410     private void dismissConsentDialogIfPresent() throws Exception {
411         UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
412 
413         if (!device.hasObject(CONSENT_DIALOG_TITLE_SELECTOR)) {
414             return;
415         }
416 
417         Log.d(
418                 TAG,
419                 "Consent dialog still present on the screen even though report finished,"
420                         + " dismissing it");
421         device.pressBack();
422         assertThat(
423                         device.wait(
424                                 Until.gone(CONSENT_DIALOG_TITLE_SELECTOR),
425                                 UIAUTOMATOR_TIMEOUT_MILLIS))
426                 .isTrue();
427     }
428 
waitUntilDoneOrTimeout(BugreportCallbackImpl callback)429     private static void waitUntilDoneOrTimeout(BugreportCallbackImpl callback) throws Exception {
430         long startTimeMillis = System.currentTimeMillis();
431         while (!callback.isDone()) {
432             Thread.sleep(1000);
433             if (System.currentTimeMillis() - startTimeMillis >= BUGREPORT_TIMEOUT_MILLIS) {
434                 Log.w(TAG, "Timed out waiting for bugreport completion");
435                 break;
436             }
437             Log.d(TAG, "Waited " + (System.currentTimeMillis() - startTimeMillis + "ms"));
438         }
439     }
440 
assertSecurityExceptionThrownForMode(int mode)441     private void assertSecurityExceptionThrownForMode(int mode) {
442         assertExceptionThrownForMode(mode, SecurityException.class);
443     }
444 
assertExceptionThrownForMode( int mode, Class<T> exceptionType)445     private <T extends Throwable> void assertExceptionThrownForMode(
446             int mode, Class<T> exceptionType) {
447         BugreportCallbackImpl callback = new BugreportCallbackImpl();
448         try {
449             mBugreportManager.startBugreport(
450                     mBugreportFd,
451                     mScreenshotFd,
452                     new BugreportParams(mode),
453                     Runnable::run,
454                     callback);
455             fail("BugreportMode " + mode + " should cause " + exceptionType.getSimpleName());
456         } catch (Throwable thrown) {
457             if (!exceptionType.isInstance(thrown)) {
458                 throw thrown;
459             }
460         }
461 
462         assertThat(callback.isDone()).isFalse();
463         assertThat(callback.hasReceivedProgress()).isFalse();
464         assertThat(mBugreportFile.length()).isEqualTo(0L);
465         assertFdIsClosed(mBugreportFd);
466         assertThat(mScreenshotFile.length()).isEqualTo(0L);
467         assertFdIsClosed(mScreenshotFd);
468     }
469 
createTempFile(String prefix, String extension)470     private static File createTempFile(String prefix, String extension) throws Exception {
471         File f = File.createTempFile(prefix, extension);
472         f.setReadable(true, true);
473         f.setWritable(true, true);
474         f.deleteOnExit();
475         return f;
476     }
477 
parcelFd(File file)478     private static ParcelFileDescriptor parcelFd(File file) throws Exception {
479         return ParcelFileDescriptor.open(
480                 file, ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND);
481     }
482 
assertFdIsClosed(ParcelFileDescriptor pfd)483     private static void assertFdIsClosed(ParcelFileDescriptor pfd) {
484         try {
485             int fd = pfd.getFd();
486             fail("Expected ParcelFileDescriptor argument to be closed, but got: " + fd);
487         } catch (IllegalStateException expected) {
488         }
489     }
490 }
491