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