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