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