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