1 /* 2 * Copyright (C) 2019 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 com.android.os.bugreports.tests; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.junit.Assert.fail; 22 23 import android.Manifest; 24 import android.content.Context; 25 import android.os.BugreportManager; 26 import android.os.BugreportManager.BugreportCallback; 27 import android.os.BugreportParams; 28 import android.os.Handler; 29 import android.os.HandlerThread; 30 import android.os.ParcelFileDescriptor; 31 import android.util.Log; 32 33 import androidx.test.InstrumentationRegistry; 34 35 import org.junit.After; 36 import org.junit.Before; 37 import org.junit.Rule; 38 import org.junit.Test; 39 import org.junit.rules.TestName; 40 import org.junit.runner.RunWith; 41 import org.junit.runners.JUnit4; 42 43 import java.io.File; 44 import java.util.concurrent.Executor; 45 import java.util.concurrent.TimeUnit; 46 47 48 /** 49 * Tests for BugreportManager API. 50 */ 51 @RunWith(JUnit4.class) 52 public class BugreportManagerTest { 53 @Rule public TestName name = new TestName(); 54 55 private static final String TAG = "BugreportManagerTest"; 56 private static final long BUGREPORT_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(10); 57 58 private Handler mHandler; 59 private Executor mExecutor; 60 private BugreportManager mBrm; 61 private File mBugreportFile; 62 private File mScreenshotFile; 63 private ParcelFileDescriptor mBugreportFd; 64 private ParcelFileDescriptor mScreenshotFd; 65 66 @Before setup()67 public void setup() throws Exception { 68 mHandler = createHandler(); 69 mExecutor = (runnable) -> { 70 if (mHandler != null) { 71 mHandler.post(() -> { 72 runnable.run(); 73 }); 74 } 75 }; 76 77 mBrm = getBugreportManager(); 78 mBugreportFile = createTempFile("bugreport_" + name.getMethodName(), ".zip"); 79 mScreenshotFile = createTempFile("screenshot_" + name.getMethodName(), ".png"); 80 mBugreportFd = parcelFd(mBugreportFile); 81 mScreenshotFd = parcelFd(mScreenshotFile); 82 83 getPermissions(); 84 } 85 86 @After teardown()87 public void teardown() throws Exception { 88 dropPermissions(); 89 } 90 91 92 @Test normalFlow_wifi()93 public void normalFlow_wifi() throws Exception { 94 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 95 // wifi bugreport does not take screenshot 96 mBrm.startBugreport(mBugreportFd, null /*screenshotFd = null*/, wifi(), 97 mExecutor, callback); 98 waitTillDoneOrTimeout(callback); 99 100 assertThat(callback.isDone()).isTrue(); 101 // Wifi bugreports should not receive any progress. 102 assertThat(callback.hasReceivedProgress()).isFalse(); 103 // TODO: Because of b/130234145, consent dialog is not shown; so we get a timeout error. 104 // When the bug is fixed, accept consent via UIAutomator and verify contents 105 // of mBugreportFd. 106 assertThat(callback.getErrorCode()).isEqualTo( 107 BugreportCallback.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT); 108 assertFdsAreClosed(mBugreportFd); 109 } 110 111 @Test normalFlow_interactive()112 public void normalFlow_interactive() throws Exception { 113 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 114 // interactive bugreport does not take screenshot 115 mBrm.startBugreport(mBugreportFd, null /*screenshotFd = null*/, interactive(), 116 mExecutor, callback); 117 118 waitTillDoneOrTimeout(callback); 119 assertThat(callback.isDone()).isTrue(); 120 // Interactive bugreports show progress updates. 121 assertThat(callback.hasReceivedProgress()).isTrue(); 122 assertThat(callback.getErrorCode()).isEqualTo( 123 BugreportCallback.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT); 124 assertFdsAreClosed(mBugreportFd); 125 } 126 127 @Test normalFlow_full()128 public void normalFlow_full() throws Exception { 129 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 130 mBrm.startBugreport(mBugreportFd, mScreenshotFd, full(), mExecutor, callback); 131 132 waitTillDoneOrTimeout(callback); 133 assertThat(callback.isDone()).isTrue(); 134 assertThat(callback.getErrorCode()).isEqualTo( 135 BugreportCallback.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT); 136 // bugreport and screenshot files should be empty when user consent timed out. 137 assertThat(mBugreportFile.length()).isEqualTo(0); 138 assertThat(mScreenshotFile.length()).isEqualTo(0); 139 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 140 } 141 142 @Test simultaneousBugreportsNotAllowed()143 public void simultaneousBugreportsNotAllowed() throws Exception { 144 // Start bugreport #1 145 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 146 mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); 147 148 // Before #1 is done, try to start #2. 149 assertThat(callback.isDone()).isFalse(); 150 BugreportCallbackImpl callback2 = new BugreportCallbackImpl(); 151 File bugreportFile2 = createTempFile("bugreport_2_" + name.getMethodName(), ".zip"); 152 File screenshotFile2 = createTempFile("screenshot_2_" + name.getMethodName(), ".png"); 153 ParcelFileDescriptor bugreportFd2 = parcelFd(bugreportFile2); 154 ParcelFileDescriptor screenshotFd2 = parcelFd(screenshotFile2); 155 mBrm.startBugreport(bugreportFd2, screenshotFd2, wifi(), mExecutor, callback2); 156 Thread.sleep(500 /* .5s */); 157 158 // Verify #2 encounters an error. 159 assertThat(callback2.getErrorCode()).isEqualTo( 160 BugreportCallback.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS); 161 assertFdsAreClosed(bugreportFd2, screenshotFd2); 162 163 // Cancel #1 so we can move on to the next test. 164 mBrm.cancelBugreport(); 165 Thread.sleep(500 /* .5s */); 166 assertThat(callback.isDone()).isTrue(); 167 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 168 } 169 170 @Test cancelBugreport()171 public void cancelBugreport() throws Exception { 172 // Start a bugreport. 173 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 174 mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); 175 176 // Verify it's not finished yet. 177 assertThat(callback.isDone()).isFalse(); 178 179 // Try to cancel it, but first without DUMP permission. 180 dropPermissions(); 181 try { 182 mBrm.cancelBugreport(); 183 fail("Expected cancelBugreport to throw SecurityException without DUMP permission"); 184 } catch (SecurityException expected) { 185 } 186 assertThat(callback.isDone()).isFalse(); 187 188 // Try again, with DUMP permission. 189 getPermissions(); 190 mBrm.cancelBugreport(); 191 Thread.sleep(500 /* .5s */); 192 assertThat(callback.isDone()).isTrue(); 193 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 194 } 195 196 @Test insufficientPermissions_throwsException()197 public void insufficientPermissions_throwsException() throws Exception { 198 dropPermissions(); 199 200 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 201 try { 202 mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); 203 fail("Expected startBugreport to throw SecurityException without DUMP permission"); 204 } catch (SecurityException expected) { 205 } 206 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 207 } 208 209 @Test invalidBugreportMode_throwsException()210 public void invalidBugreportMode_throwsException() throws Exception { 211 BugreportCallbackImpl callback = new BugreportCallbackImpl(); 212 213 try { 214 mBrm.startBugreport(mBugreportFd, mScreenshotFd, 215 new BugreportParams(25) /* unknown bugreport mode */, mExecutor, callback); 216 fail("Expected to throw IllegalArgumentException with unknown bugreport mode"); 217 } catch (IllegalArgumentException expected) { 218 } 219 assertFdsAreClosed(mBugreportFd, mScreenshotFd); 220 } 221 createHandler()222 private Handler createHandler() { 223 HandlerThread handlerThread = new HandlerThread("BugreportManagerTest"); 224 handlerThread.start(); 225 return new Handler(handlerThread.getLooper()); 226 } 227 228 /* Implementatiion of {@link BugreportCallback} that offers wrappers around execution result */ 229 private static final class BugreportCallbackImpl extends BugreportCallback { 230 private int mErrorCode = -1; 231 private boolean mSuccess = false; 232 private boolean mReceivedProgress = false; 233 private final Object mLock = new Object(); 234 235 @Override onProgress(float progress)236 public void onProgress(float progress) { 237 synchronized (mLock) { 238 mReceivedProgress = true; 239 } 240 } 241 242 @Override onError(int errorCode)243 public void onError(int errorCode) { 244 synchronized (mLock) { 245 mErrorCode = errorCode; 246 Log.d(TAG, "bugreport errored."); 247 } 248 } 249 250 @Override onFinished()251 public void onFinished() { 252 synchronized (mLock) { 253 Log.d(TAG, "bugreport finished."); 254 mSuccess = true; 255 } 256 } 257 258 /* Indicates completion; and ended up with a success or error. */ isDone()259 public boolean isDone() { 260 synchronized (mLock) { 261 return (mErrorCode != -1) || mSuccess; 262 } 263 } 264 getErrorCode()265 public int getErrorCode() { 266 synchronized (mLock) { 267 return mErrorCode; 268 } 269 } 270 isSuccess()271 public boolean isSuccess() { 272 synchronized (mLock) { 273 return mSuccess; 274 } 275 } 276 hasReceivedProgress()277 public boolean hasReceivedProgress() { 278 synchronized (mLock) { 279 return mReceivedProgress; 280 } 281 } 282 } 283 getBugreportManager()284 public static BugreportManager getBugreportManager() { 285 Context context = InstrumentationRegistry.getContext(); 286 BugreportManager bm = 287 (BugreportManager) context.getSystemService(Context.BUGREPORT_SERVICE); 288 if (bm == null) { 289 throw new AssertionError("Failed to get BugreportManager"); 290 } 291 return bm; 292 } 293 createTempFile(String prefix, String extension)294 private static File createTempFile(String prefix, String extension) throws Exception { 295 final File f = File.createTempFile(prefix, extension); 296 f.setReadable(true, true); 297 f.setWritable(true, true); 298 f.deleteOnExit(); 299 return f; 300 } 301 parcelFd(File file)302 private static ParcelFileDescriptor parcelFd(File file) throws Exception { 303 return ParcelFileDescriptor.open(file, 304 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND); 305 } 306 dropPermissions()307 private static void dropPermissions() { 308 InstrumentationRegistry.getInstrumentation().getUiAutomation() 309 .dropShellPermissionIdentity(); 310 } 311 getPermissions()312 private static void getPermissions() { 313 InstrumentationRegistry.getInstrumentation().getUiAutomation() 314 .adoptShellPermissionIdentity(Manifest.permission.DUMP); 315 } 316 assertFdIsClosed(ParcelFileDescriptor pfd)317 private static void assertFdIsClosed(ParcelFileDescriptor pfd) { 318 try { 319 int fd = pfd.getFd(); 320 fail("Expected ParcelFileDescriptor argument to be closed, but got: " + fd); 321 } catch (IllegalStateException expected) { 322 } 323 } 324 assertFdsAreClosed(ParcelFileDescriptor... pfds)325 private static void assertFdsAreClosed(ParcelFileDescriptor... pfds) { 326 for (int i = 0; i < pfds.length; i++) { 327 assertFdIsClosed(pfds[i]); 328 } 329 } 330 now()331 private static long now() { 332 return System.currentTimeMillis(); 333 } 334 shouldTimeout(long startTimeMs)335 private static boolean shouldTimeout(long startTimeMs) { 336 return now() - startTimeMs >= BUGREPORT_TIMEOUT_MS; 337 } 338 waitTillDoneOrTimeout(BugreportCallbackImpl callback)339 private static void waitTillDoneOrTimeout(BugreportCallbackImpl callback) throws Exception { 340 long startTimeMs = now(); 341 while (!callback.isDone()) { 342 Thread.sleep(1000 /* 1s */); 343 if (shouldTimeout(startTimeMs)) { 344 break; 345 } 346 Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms"); 347 } 348 } 349 350 /* 351 * Returns a {@link BugreportParams} for wifi only bugreport. 352 * 353 * <p>Wifi bugreports have minimal content and are fast to run. They also suppress progress 354 * updates. 355 */ wifi()356 private static BugreportParams wifi() { 357 return new BugreportParams(BugreportParams.BUGREPORT_MODE_WIFI); 358 } 359 360 /* 361 * Returns a {@link BugreportParams} for interactive bugreport that offers progress updates. 362 * 363 * <p>This is the typical bugreport taken by users. This can take on the order of minutes to 364 * finish. 365 */ interactive()366 private static BugreportParams interactive() { 367 return new BugreportParams(BugreportParams.BUGREPORT_MODE_INTERACTIVE); 368 } 369 370 /* 371 * Returns a {@link BugreportParams} for full bugreport that includes a screenshot. 372 * 373 * <p> This can take on the order of minutes to finish 374 */ full()375 private static BugreportParams full() { 376 return new BugreportParams(BugreportParams.BUGREPORT_MODE_FULL); 377 } 378 } 379