1 /* 2 * Copyright (C) 2020 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 package android.car.hiddenapitest; 17 18 import static com.google.common.truth.Truth.assertThat; 19 20 import static org.junit.Assert.assertThrows; 21 22 import android.Manifest; 23 import android.annotation.FloatRange; 24 import android.car.Car; 25 import android.car.CarBugreportManager; 26 import android.car.CarBugreportManager.CarBugreportManagerCallback; 27 import android.car.extendedapitest.testbase.CarApiTestBase; 28 import android.os.FileUtils; 29 import android.os.ParcelFileDescriptor; 30 31 import androidx.test.filters.LargeTest; 32 import androidx.test.platform.app.InstrumentationRegistry; 33 import androidx.test.runner.AndroidJUnit4; 34 35 import org.junit.After; 36 import org.junit.Before; 37 import org.junit.Test; 38 import org.junit.runner.RunWith; 39 40 import java.io.ByteArrayOutputStream; 41 import java.io.Closeable; 42 import java.io.File; 43 import java.io.FileOutputStream; 44 import java.io.IOException; 45 import java.io.InputStream; 46 import java.nio.charset.StandardCharsets; 47 import java.util.ArrayList; 48 import java.util.Collections; 49 import java.util.List; 50 import java.util.concurrent.CountDownLatch; 51 import java.util.concurrent.TimeUnit; 52 import java.util.zip.ZipEntry; 53 import java.util.zip.ZipFile; 54 55 @RunWith(AndroidJUnit4.class) 56 @LargeTest 57 public final class CarBugreportManagerTest extends CarApiTestBase { 58 private static final String TAG = CarBugreportManagerTest.class.getSimpleName(); 59 60 // Note that most of the test environments have 600s time limit, and in some cases the time 61 // limit is shared between all the tests. 62 // Dumpstate with dry_run flag should finish within one minute, but it might work slower on 63 // busy devices. 64 private static final int BUGREPORT_TIMEOUT_MILLIS = 90_000; 65 private static final int NO_ERROR = -1; 66 67 // These items will be closed during tearDown(). 68 private final ArrayList<Closeable> mAllCloseables = new ArrayList<>(); 69 70 private CarBugreportManager mManager; 71 private FakeCarBugreportCallback mFakeCallback; 72 private ParcelFileDescriptor mOutput; 73 private ParcelFileDescriptor mExtraOutput; 74 75 @Before setUp()76 public void setUp() throws Exception { 77 mManager = (CarBugreportManager) getCar().getCarManager(Car.CAR_BUGREPORT_SERVICE); 78 mFakeCallback = new FakeCarBugreportCallback(); 79 mOutput = openDevNullParcelFd(); 80 mExtraOutput = openDevNullParcelFd(); 81 mAllCloseables.addAll(List.of(mOutput, mExtraOutput)); 82 } 83 84 @After tearDown()85 public void tearDown() throws Exception { 86 getPermissions(); // For cancelBugreport() 87 try { 88 mManager.cancelBugreport(); 89 } finally { 90 dropPermissions(); 91 } 92 for (Closeable closeable : mAllCloseables) { 93 try { 94 closeable.close(); 95 } catch (IOException e) { 96 // No need to handle it 97 } 98 } 99 } 100 101 @Test test_requestBugreport_failsWhenNoPermission()102 public void test_requestBugreport_failsWhenNoPermission() { 103 dropPermissions(); 104 105 SecurityException expected = 106 assertThrows(SecurityException.class, 107 () -> mManager.requestBugreportForTesting( 108 mOutput, mExtraOutput, mFakeCallback)); 109 assertThat(expected).hasMessageThat().contains( 110 "nor current process has android.permission.DUMP."); 111 } 112 113 @Test test_requestBugreport_works()114 public void test_requestBugreport_works() throws Exception { 115 getPermissions(); 116 PipedTempFile output = PipedTempFile.create("bugreport-" + getTestName(), ".zip"); 117 PipedTempFile extraOutput = PipedTempFile.create("screenshot-" + getTestName(), ".png"); 118 mAllCloseables.addAll(List.of(output, extraOutput)); 119 120 mManager.requestBugreportForTesting( 121 output.getWriteFd(), extraOutput.getWriteFd(), mFakeCallback); 122 123 // The FDs must be duped and closed in requestBugreport() immediately. 124 assertFdIsClosed(output.getWriteFd()); 125 assertFdIsClosed(extraOutput.getWriteFd()); 126 127 // Blocks the thread until bugreport is finished. 128 PipedTempFile.copyAllToPersistentFiles(output, extraOutput); 129 130 mFakeCallback.waitTillDoneOrTimeout(BUGREPORT_TIMEOUT_MILLIS); 131 assertThat(mFakeCallback.isFinishedSuccessfully()).isEqualTo(true); 132 assertThat(mFakeCallback.getReceivedProgress()).isTrue(); 133 assertContainsValidBugreport(output.getPersistentFile()); 134 } 135 136 @Test test_requestBugreport_cannotRunMultipleBugreports()137 public void test_requestBugreport_cannotRunMultipleBugreports() throws Exception { 138 getPermissions(); 139 FakeCarBugreportCallback callback2 = new FakeCarBugreportCallback(); 140 ParcelFileDescriptor output2 = openDevNullParcelFd(); 141 ParcelFileDescriptor extraOutput2 = openDevNullParcelFd(); 142 143 // 1st bugreport. 144 mManager.requestBugreportForTesting(mOutput, mExtraOutput, mFakeCallback); 145 146 // 2nd bugreport. 147 mManager.requestBugreportForTesting(output2, extraOutput2, callback2); 148 149 callback2.waitTillDoneOrTimeout(BUGREPORT_TIMEOUT_MILLIS); 150 assertThat(callback2.getErrorCode()).isEqualTo( 151 CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS); 152 assertThat(mFakeCallback.isFinished()).isFalse(); 153 } 154 155 @Test test_cancelBugreport_works()156 public void test_cancelBugreport_works() throws Exception { 157 getPermissions(); 158 FakeCarBugreportCallback callback2 = new FakeCarBugreportCallback(); 159 ParcelFileDescriptor output2 = openDevNullParcelFd(); 160 ParcelFileDescriptor extraOutput2 = openDevNullParcelFd(); 161 162 // 1st bugreport. 163 mManager.requestBugreportForTesting(mOutput, mExtraOutput, mFakeCallback); 164 mManager.cancelBugreport(); 165 166 // Allow the system to finish the bugreport cancellation, 0.5 seconds is enough. 167 Thread.sleep(500); 168 169 // 2nd bugreport must work, because 1st bugreport was cancelled. 170 mManager.requestBugreportForTesting(output2, extraOutput2, callback2); 171 172 callback2.waitTillProgressOrTimeout(BUGREPORT_TIMEOUT_MILLIS); 173 assertThat(callback2.getErrorCode()).isEqualTo(NO_ERROR); 174 assertThat(callback2.getReceivedProgress()).isEqualTo(true); 175 } 176 getPermissions()177 private static void getPermissions() { 178 InstrumentationRegistry.getInstrumentation().getUiAutomation() 179 .adoptShellPermissionIdentity(Manifest.permission.DUMP); 180 } 181 dropPermissions()182 private static void dropPermissions() { 183 InstrumentationRegistry.getInstrumentation().getUiAutomation() 184 .dropShellPermissionIdentity(); 185 } 186 assertFdIsClosed(ParcelFileDescriptor pfd)187 private static void assertFdIsClosed(ParcelFileDescriptor pfd) { 188 try { 189 int fd = pfd.getFd(); 190 fail("Expected ParcelFileDescriptor argument to be closed, but got: " + fd); 191 } catch (IllegalStateException expected) { 192 } 193 } 194 assertContainsValidBugreport(File file)195 private static void assertContainsValidBugreport(File file) throws IOException { 196 try (ZipFile zipFile = new ZipFile(file)) { 197 for (ZipEntry entry : Collections.list(zipFile.entries())) { 198 if (entry.isDirectory()) { 199 continue; 200 } 201 // Find "bugreport-TIMESTAMP.txt" file. 202 if (!entry.getName().startsWith("bugreport-") || !entry.getName().endsWith( 203 ".txt")) { 204 continue; 205 } 206 try (InputStream entryStream = zipFile.getInputStream(entry)) { 207 String data = streamToText(entryStream, /* maxSizeBytes= */ 51200); 208 assertThat(data).contains("== dumpstate: "); 209 assertThat(data).contains("dry_run=1"); 210 assertThat(data).contains("Build fingerprint: "); 211 } 212 return; 213 } 214 } 215 fail("bugreport-TIMESTAMP.txt not found in the final zip file."); 216 } 217 streamToText(InputStream in, int maxSizeBytes)218 private static String streamToText(InputStream in, int maxSizeBytes) throws IOException { 219 assertThat(maxSizeBytes).isGreaterThan(0); 220 221 ByteArrayOutputStream result = new ByteArrayOutputStream(); 222 byte[] data = new byte[maxSizeBytes]; 223 int nRead; 224 int totalRead = 0; 225 226 while ((nRead = in.read(data, 0, data.length)) != -1 && totalRead <= maxSizeBytes) { 227 result.write(data, 0, nRead); 228 totalRead += maxSizeBytes; 229 } 230 231 return result.toString(StandardCharsets.UTF_8.name()); 232 } 233 openDevNullParcelFd()234 private static ParcelFileDescriptor openDevNullParcelFd() throws IOException { 235 return ParcelFileDescriptor.open( 236 new File("/dev/null"), 237 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND); 238 } 239 240 /** 241 * Creates a piped ParcelFileDescriptor that anyone can write. Clients must call 242 * {@link copyToPersistentFile}, otherwise writers will be blocked when writing to the pipe. 243 * 244 * <p>It was created because {@code CarService} is denied to write to a test cache file 245 * by SELinux. 246 */ 247 private static class PipedTempFile implements Closeable { 248 private final File mPersistentFile; 249 private final ParcelFileDescriptor mReadFd; 250 private final ParcelFileDescriptor mWriteFd; 251 create(String prefix, String extension)252 static PipedTempFile create(String prefix, String extension) throws IOException { 253 ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); 254 File f = File.createTempFile(prefix, extension); 255 f.setReadable(/* readable= */ true, /* ownerOnly= */ true); 256 f.setWritable(/* readable= */ true, /* ownerOnly= */ true); 257 f.deleteOnExit(); 258 return new PipedTempFile(pipe[0], pipe[1], f); 259 } 260 copyAllToPersistentFiles(PipedTempFile... files)261 static void copyAllToPersistentFiles(PipedTempFile... files) throws IOException { 262 for (PipedTempFile f : files) { 263 f.copyToPersistentFile(); 264 } 265 } 266 PipedTempFile( ParcelFileDescriptor readFd, ParcelFileDescriptor writeFd, File persistentFile)267 private PipedTempFile( 268 ParcelFileDescriptor readFd, ParcelFileDescriptor writeFd, File persistentFile) { 269 mReadFd = readFd; 270 mWriteFd = writeFd; 271 mPersistentFile = persistentFile; 272 } 273 getWriteFd()274 ParcelFileDescriptor getWriteFd() { 275 return mWriteFd; 276 } 277 getPersistentFile()278 File getPersistentFile() { 279 return mPersistentFile; 280 } 281 282 @Override close()283 public void close() throws IOException { 284 try { 285 mReadFd.close(); 286 } finally { 287 mWriteFd.close(); 288 } 289 } 290 291 /** Copies data from the pipe to the persistent file. Blocks the thread. */ copyToPersistentFile()292 void copyToPersistentFile() throws IOException { 293 try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(mReadFd); 294 FileOutputStream out = new FileOutputStream(mPersistentFile)) { 295 FileUtils.copy(in, out); 296 } 297 } 298 } 299 300 private static class FakeCarBugreportCallback extends CarBugreportManagerCallback { 301 private final Object mLock = new Object(); 302 private final CountDownLatch mEndedLatch = new CountDownLatch(1); 303 private final CountDownLatch mProgressLatch = new CountDownLatch(1); 304 private boolean mReceivedProgress = false; 305 private int mErrorCode = NO_ERROR; 306 307 @Override onProgress(@loatRangefrom = 0f, to = 100f) float progress)308 public void onProgress(@FloatRange(from = 0f, to = 100f) float progress) { 309 synchronized (mLock) { 310 mReceivedProgress = true; 311 } 312 mProgressLatch.countDown(); 313 } 314 315 @Override onError( @arBugreportManagerCallback.CarBugreportErrorCode int errorCode)316 public void onError( 317 @CarBugreportManagerCallback.CarBugreportErrorCode int errorCode) { 318 synchronized (mLock) { 319 mErrorCode = errorCode; 320 } 321 mEndedLatch.countDown(); 322 mProgressLatch.countDown(); 323 } 324 325 @Override onFinished()326 public void onFinished() { 327 mEndedLatch.countDown(); 328 mProgressLatch.countDown(); 329 } 330 getErrorCode()331 int getErrorCode() { 332 synchronized (mLock) { 333 return mErrorCode; 334 } 335 } 336 getReceivedProgress()337 boolean getReceivedProgress() { 338 synchronized (mLock) { 339 return mReceivedProgress; 340 } 341 } 342 isFinishedSuccessfully()343 boolean isFinishedSuccessfully() { 344 return mEndedLatch.getCount() == 0 && getErrorCode() == NO_ERROR; 345 } 346 isFinished()347 boolean isFinished() { 348 return mEndedLatch.getCount() == 0; 349 } 350 waitTillDoneOrTimeout(long timeoutMillis)351 void waitTillDoneOrTimeout(long timeoutMillis) throws InterruptedException { 352 mEndedLatch.await(timeoutMillis, TimeUnit.MILLISECONDS); 353 if (mEndedLatch.getCount() > 0) { 354 fail("Time out. CarBugreportManager didn't finish."); 355 } 356 } 357 waitTillProgressOrTimeout(long timeoutMillis)358 void waitTillProgressOrTimeout(long timeoutMillis) throws InterruptedException { 359 mProgressLatch.await(timeoutMillis, TimeUnit.MILLISECONDS); 360 if (mProgressLatch.getCount() > 0) { 361 fail("Time out. CarBugreportManager didn't send progress or finish."); 362 } 363 } 364 } 365 } 366