1 /* 2 * Copyright (C) 2023 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.bugreport.cts_root; 18 19 import static com.android.compatibility.common.util.SystemUtil.runShellCommand; 20 21 import static com.google.common.truth.Truth.assertThat; 22 23 import static org.junit.Assert.fail; 24 25 import android.content.Context; 26 import android.os.BugreportManager; 27 import android.os.BugreportManager.BugreportCallback; 28 import android.os.BugreportParams; 29 import android.os.ParcelFileDescriptor; 30 31 import androidx.annotation.NonNull; 32 import androidx.test.InstrumentationRegistry; 33 import androidx.test.filters.LargeTest; 34 import androidx.test.runner.AndroidJUnit4; 35 import androidx.test.uiautomator.By; 36 import androidx.test.uiautomator.BySelector; 37 import androidx.test.uiautomator.UiDevice; 38 import androidx.test.uiautomator.UiObject2; 39 import androidx.test.uiautomator.Until; 40 41 import org.junit.After; 42 import org.junit.Before; 43 import org.junit.Rule; 44 import org.junit.Test; 45 import org.junit.rules.TestName; 46 import org.junit.runner.RunWith; 47 48 import java.io.File; 49 import java.lang.reflect.Method; 50 import java.util.concurrent.CountDownLatch; 51 import java.util.concurrent.TimeUnit; 52 53 /** 54 * Device-side tests for Bugreport Manager API. 55 * 56 * <p>These tests require root to allowlist the test package to use the BugreportManager APIs. 57 */ 58 @RunWith(AndroidJUnit4.class) 59 public class BugreportManagerTest { 60 61 private Context mContext; 62 private BugreportManager mBugreportManager; 63 64 @Rule 65 public TestName name = new TestName(); 66 67 private static final long UIAUTOMATOR_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10); 68 69 @Before setup()70 public void setup() { 71 mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 72 mBugreportManager = mContext.getSystemService(BugreportManager.class); 73 // Kill current bugreport, so that it does not interfere with future bugreports. 74 runShellCommand("setprop ctl.stop bugreportd"); 75 } 76 77 @After tearDown()78 public void tearDown() { 79 // Kill current bugreport, so that it does not interfere with future bugreports. 80 runShellCommand("setprop ctl.stop bugreportd"); 81 } 82 83 @LargeTest 84 @Test testRetrieveBugreportConsentGranted()85 public void testRetrieveBugreportConsentGranted() throws Exception { 86 File bugreportFile = createTempFile("bugreport_" + name.getMethodName(), ".zip"); 87 File startBugreportFile = createTempFile("startbugreport", ".zip"); 88 CountDownLatch latch = new CountDownLatch(1); 89 BugreportCallbackImpl callback = new BugreportCallbackImpl(latch); 90 mBugreportManager.startBugreport(parcelFd(startBugreportFile), null, 91 new BugreportParams( 92 BugreportParams.BUGREPORT_MODE_INTERACTIVE, 93 BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT), 94 mContext.getMainExecutor(), callback); 95 latch.await(4, TimeUnit.MINUTES); 96 assertThat(callback.isSuccess()).isTrue(); 97 // No data should be passed to the FD used to call startBugreport. 98 assertThat(startBugreportFile.length()).isEqualTo(0); 99 String bugreportFileLocation = callback.getBugreportFile(); 100 waitForDumpstateServiceToStop(); 101 102 103 104 // Trying to retrieve an unknown bugreport should fail 105 latch = new CountDownLatch(1); 106 callback = new BugreportCallbackImpl(latch); 107 File bugreportFile2 = createTempFile("bugreport2_" + name.getMethodName(), ".zip"); 108 mBugreportManager.retrieveBugreport( 109 "unknown/file.zip", parcelFd(bugreportFile2), 110 mContext.getMainExecutor(), callback); 111 assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); 112 assertThat(callback.getErrorCode()).isEqualTo( 113 BugreportCallback.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE); 114 115 // A bugreport was previously generated for this caller. When the consent dialog is invoked 116 // and accepted, the bugreport files should be passed to the calling package. 117 ParcelFileDescriptor bugreportFd = parcelFd(bugreportFile); 118 assertThat(bugreportFd).isNotNull(); 119 latch = new CountDownLatch(1); 120 mBugreportManager.retrieveBugreport(bugreportFileLocation, bugreportFd, 121 mContext.getMainExecutor(), new BugreportCallbackImpl(latch)); 122 shareConsentDialog(ConsentReply.ALLOW); 123 assertThat(latch.await(1, TimeUnit.MINUTES)).isTrue(); 124 assertThat(bugreportFile.length()).isGreaterThan(0); 125 } 126 127 128 @LargeTest 129 @Test testRetrieveBugreportConsentDenied()130 public void testRetrieveBugreportConsentDenied() throws Exception { 131 File bugreportFile = createTempFile("bugreport_" + name.getMethodName(), ".zip"); 132 133 // User denies consent, therefore no data should be passed back to the bugreport file. 134 CountDownLatch latch = new CountDownLatch(1); 135 BugreportCallbackImpl callback = new BugreportCallbackImpl(latch); 136 mBugreportManager.startBugreport(parcelFd(new File("/dev/null")), 137 null, new BugreportParams(BugreportParams.BUGREPORT_MODE_INTERACTIVE, 138 BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT), 139 mContext.getMainExecutor(), callback); 140 latch.await(4, TimeUnit.MINUTES); 141 assertThat(callback.isSuccess()).isTrue(); 142 String bugreportFileLocation = callback.getBugreportFile(); 143 waitForDumpstateServiceToStop(); 144 145 latch = new CountDownLatch(1); 146 ParcelFileDescriptor bugreportFd = parcelFd(bugreportFile); 147 assertThat(bugreportFd).isNotNull(); 148 mBugreportManager.retrieveBugreport( 149 bugreportFileLocation, 150 bugreportFd, 151 mContext.getMainExecutor(), 152 callback); 153 shareConsentDialog(ConsentReply.DENY); 154 latch.await(1, TimeUnit.MINUTES); 155 assertThat(callback.getErrorCode()).isEqualTo( 156 BugreportCallback.BUGREPORT_ERROR_USER_DENIED_CONSENT); 157 assertThat(bugreportFile.length()).isEqualTo(0); 158 159 // Since consent has already been denied, this call should fail because consent cannot 160 // be requested twice for the same bugreport. 161 latch = new CountDownLatch(1); 162 callback = new BugreportCallbackImpl(latch); 163 mBugreportManager.retrieveBugreport(bugreportFileLocation, parcelFd(bugreportFile), 164 mContext.getMainExecutor(), callback); 165 latch.await(1, TimeUnit.MINUTES); 166 assertThat(callback.getErrorCode()).isEqualTo( 167 BugreportCallback.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE); 168 } 169 parcelFd(File file)170 private ParcelFileDescriptor parcelFd(File file) throws Exception { 171 return ParcelFileDescriptor.open(file, 172 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND); 173 } 174 createTempFile(String prefix, String extension)175 private static File createTempFile(String prefix, String extension) throws Exception { 176 final File f = File.createTempFile(prefix, extension); 177 f.setReadable(true, true); 178 f.setWritable(true, true); 179 180 f.deleteOnExit(); 181 return f; 182 } 183 184 private static final class BugreportCallbackImpl extends BugreportCallback { 185 private int mErrorCode = -1; 186 private boolean mSuccess = false; 187 private String mBugreportFile; 188 private final Object mLock = new Object(); 189 190 private final CountDownLatch mLatch; 191 BugreportCallbackImpl(CountDownLatch latch)192 BugreportCallbackImpl(CountDownLatch latch) { 193 mLatch = latch; 194 } 195 196 @Override onError(int errorCode)197 public void onError(int errorCode) { 198 synchronized (mLock) { 199 mErrorCode = errorCode; 200 mLatch.countDown(); 201 } 202 } 203 204 @Override onFinished(String bugreportFile)205 public void onFinished(String bugreportFile) { 206 synchronized (mLock) { 207 mBugreportFile = bugreportFile; 208 mLatch.countDown(); 209 mSuccess = true; 210 } 211 } 212 213 @Override onFinished()214 public void onFinished() { 215 synchronized (mLock) { 216 mLatch.countDown(); 217 mSuccess = true; 218 } 219 } 220 getErrorCode()221 public int getErrorCode() { 222 synchronized (mLock) { 223 return mErrorCode; 224 } 225 } 226 isSuccess()227 public boolean isSuccess() { 228 synchronized (mLock) { 229 return mSuccess; 230 } 231 } 232 getBugreportFile()233 public String getBugreportFile() { 234 synchronized (mLock) { 235 return mBugreportFile; 236 } 237 } 238 } 239 240 private enum ConsentReply { 241 ALLOW, 242 DENY, 243 TIMEOUT 244 } 245 246 /* 247 * Ensure the consent dialog is shown and take action according to <code>consentReply<code/>. 248 * It will fail if the dialog is not shown when <code>ignoreNotFound<code/> is false. 249 */ shareConsentDialog(@onNull ConsentReply consentReply)250 private void shareConsentDialog(@NonNull ConsentReply consentReply) throws Exception { 251 final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 252 253 // Unlock before finding/clicking an object. 254 device.wakeUp(); 255 device.executeShellCommand("wm dismiss-keyguard"); 256 257 final BySelector consentTitleObj = By.res("android", "alertTitle"); 258 if (!device.wait(Until.hasObject(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS)) { 259 fail("The consent dialog is not found"); 260 } 261 if (consentReply.equals(ConsentReply.TIMEOUT)) { 262 return; 263 } 264 final BySelector selector; 265 if (consentReply.equals(ConsentReply.ALLOW)) { 266 selector = By.res("android", "button1"); 267 } else { // ConsentReply.DENY 268 selector = By.res("android", "button2"); 269 } 270 final UiObject2 btnObj = device.findObject(selector); 271 assertThat(btnObj).isNotNull(); 272 btnObj.click(); 273 274 assertThat(device.wait(Until.gone(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS)).isTrue(); 275 } 276 277 278 /** Waits for the dumpstate service to stop, for up to 5 seconds. */ waitForDumpstateServiceToStop()279 private void waitForDumpstateServiceToStop() throws Exception { 280 int pollingIntervalMillis = 100; 281 int numPolls = 50; 282 Method method = Class.forName("android.os.ServiceManager").getMethod( 283 "getService", String.class); 284 while (numPolls-- > 0) { 285 // If getService() returns null, the service has stopped. 286 if (method.invoke(null, "dumpstate") == null) { 287 return; 288 } 289 Thread.sleep(pollingIntervalMillis); 290 } 291 fail("Dumpstate did not stop within 5 seconds"); 292 } 293 } 294