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