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 android.os; 18 19 import android.annotation.CallbackExecutor; 20 import android.annotation.FloatRange; 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.RequiresPermission; 25 import android.annotation.SuppressAutoDoc; 26 import android.annotation.SystemApi; 27 import android.annotation.SystemService; 28 import android.annotation.WorkerThread; 29 import android.app.ActivityManager; 30 import android.content.Context; 31 import android.util.Log; 32 import android.widget.Toast; 33 34 import com.android.internal.R; 35 import com.android.internal.util.Preconditions; 36 37 import libcore.io.IoUtils; 38 39 import java.io.File; 40 import java.io.FileNotFoundException; 41 import java.lang.annotation.Retention; 42 import java.lang.annotation.RetentionPolicy; 43 import java.util.concurrent.Executor; 44 45 /** 46 * Class that provides a privileged API to capture and consume bugreports. 47 * 48 * <p>This class may only be used by apps that currently have carrier privileges (see {@link 49 * android.telephony.TelephonyManager#hasCarrierPrivileges}) on an active SIM or priv-apps 50 * explicitly allowed by the device manufacturer. 51 * 52 * <p>Only one bugreport can be generated by the system at a time. 53 */ 54 @SystemService(Context.BUGREPORT_SERVICE) 55 public final class BugreportManager { 56 57 private static final String TAG = "BugreportManager"; 58 59 private final Context mContext; 60 private final IDumpstate mBinder; 61 62 /** @hide */ BugreportManager(@onNull Context context, IDumpstate binder)63 public BugreportManager(@NonNull Context context, IDumpstate binder) { 64 mContext = context; 65 mBinder = binder; 66 } 67 68 /** 69 * An interface describing the callback for bugreport progress and status. 70 * 71 * <p>Callers will receive {@link #onProgress} calls as the bugreport progresses, followed by a 72 * terminal call to either {@link #onFinished} or {@link #onError}. 73 * 74 * <p>If an issue is encountered while starting the bugreport asynchronously, callers will 75 * receive an {@link #onError} call without any {@link #onProgress} callbacks. 76 */ 77 public abstract static class BugreportCallback { 78 /** 79 * Possible error codes taking a bugreport can encounter. 80 * 81 * @hide 82 */ 83 @Retention(RetentionPolicy.SOURCE) 84 @IntDef( 85 prefix = {"BUGREPORT_ERROR_"}, 86 value = { 87 BUGREPORT_ERROR_INVALID_INPUT, 88 BUGREPORT_ERROR_RUNTIME, 89 BUGREPORT_ERROR_USER_DENIED_CONSENT, 90 BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT, 91 BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS 92 }) 93 public @interface BugreportErrorCode {} 94 95 /** 96 * The input options were invalid. For example, the destination file the app provided could 97 * not be written by the system. 98 */ 99 public static final int BUGREPORT_ERROR_INVALID_INPUT = 100 IDumpstateListener.BUGREPORT_ERROR_INVALID_INPUT; 101 102 /** A runtime error occurred. */ 103 public static final int BUGREPORT_ERROR_RUNTIME = 104 IDumpstateListener.BUGREPORT_ERROR_RUNTIME_ERROR; 105 106 /** User denied consent to share the bugreport. */ 107 public static final int BUGREPORT_ERROR_USER_DENIED_CONSENT = 108 IDumpstateListener.BUGREPORT_ERROR_USER_DENIED_CONSENT; 109 110 /** The request to get user consent timed out. */ 111 public static final int BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT = 112 IDumpstateListener.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT; 113 114 /** There is currently a bugreport running. The caller should try again later. */ 115 public static final int BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS = 116 IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS; 117 118 /** 119 * Called when there is a progress update. 120 * 121 * @param progress the progress in [0.0, 100.0] 122 */ onProgress(@loatRangefrom = 0f, to = 100f) float progress)123 public void onProgress(@FloatRange(from = 0f, to = 100f) float progress) {} 124 125 /** 126 * Called when taking bugreport resulted in an error. 127 * 128 * <p>If {@code BUGREPORT_ERROR_USER_DENIED_CONSENT} is passed, then the user did not 129 * consent to sharing the bugreport with the calling app. 130 * 131 * <p>If {@code BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT} is passed, then the consent timed 132 * out, but the bugreport could be available in the internal directory of dumpstate for 133 * manual retrieval. 134 * 135 * <p>If {@code BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS} is passed, then the caller 136 * should try later, as only one bugreport can be in progress at a time. 137 */ onError(@ugreportErrorCode int errorCode)138 public void onError(@BugreportErrorCode int errorCode) {} 139 140 /** Called when taking bugreport finishes successfully. */ onFinished()141 public void onFinished() {} 142 143 /** 144 * Called when it is ready for calling app to show UI, showing any extra UI before this 145 * callback can interfere with bugreport generation. 146 */ onEarlyReportFinished()147 public void onEarlyReportFinished() {} 148 } 149 150 /** 151 * Starts a bugreport. 152 * 153 * <p>This starts a bugreport in the background. However the call itself can take several 154 * seconds to return in the worst case. {@code callback} will receive progress and status 155 * updates. 156 * 157 * <p>The bugreport artifacts will be copied over to the given file descriptors only if the user 158 * consents to sharing with the calling app. 159 * 160 * <p>{@link BugreportManager} takes ownership of {@code bugreportFd} and {@code screenshotFd}. 161 * 162 * @param bugreportFd file to write the bugreport. This should be opened in write-only, append 163 * mode. 164 * @param screenshotFd file to write the screenshot, if necessary. This should be opened in 165 * write-only, append mode. 166 * @param params options that specify what kind of a bugreport should be taken 167 * @param callback callback for progress and status updates 168 * @hide 169 */ 170 @SystemApi 171 @RequiresPermission(android.Manifest.permission.DUMP) 172 @WorkerThread startBugreport( @onNull ParcelFileDescriptor bugreportFd, @Nullable ParcelFileDescriptor screenshotFd, @NonNull BugreportParams params, @NonNull @CallbackExecutor Executor executor, @NonNull BugreportCallback callback)173 public void startBugreport( 174 @NonNull ParcelFileDescriptor bugreportFd, 175 @Nullable ParcelFileDescriptor screenshotFd, 176 @NonNull BugreportParams params, 177 @NonNull @CallbackExecutor Executor executor, 178 @NonNull BugreportCallback callback) { 179 try { 180 Preconditions.checkNotNull(bugreportFd); 181 Preconditions.checkNotNull(params); 182 Preconditions.checkNotNull(executor); 183 Preconditions.checkNotNull(callback); 184 185 boolean isScreenshotRequested = screenshotFd != null; 186 if (screenshotFd == null) { 187 // Binder needs a valid File Descriptor to be passed 188 screenshotFd = 189 ParcelFileDescriptor.open( 190 new File("/dev/null"), ParcelFileDescriptor.MODE_READ_ONLY); 191 } 192 DumpstateListener dsListener = 193 new DumpstateListener(executor, callback, isScreenshotRequested); 194 // Note: mBinder can get callingUid from the binder transaction. 195 mBinder.startBugreport( 196 -1 /* callingUid */, 197 mContext.getOpPackageName(), 198 bugreportFd.getFileDescriptor(), 199 screenshotFd.getFileDescriptor(), 200 params.getMode(), 201 dsListener, 202 isScreenshotRequested); 203 } catch (RemoteException e) { 204 throw e.rethrowFromSystemServer(); 205 } catch (FileNotFoundException e) { 206 Log.wtf(TAG, "Not able to find /dev/null file: ", e); 207 } finally { 208 // We can close the file descriptors here because binder would have duped them. 209 IoUtils.closeQuietly(bugreportFd); 210 if (screenshotFd != null) { 211 IoUtils.closeQuietly(screenshotFd); 212 } 213 } 214 } 215 216 /** 217 * Starts a connectivity bugreport. 218 * 219 * <p>The connectivity bugreport is a specialized version of bugreport that only includes 220 * information specifically for debugging connectivity-related issues (e.g. telephony, wi-fi, 221 * and IP networking issues). It is intended primarily for use by OEMs and network providers 222 * such as mobile network operators. In addition to generally excluding information that isn't 223 * targeted to connectivity debugging, this type of bugreport excludes PII and sensitive 224 * information that isn't strictly necessary for connectivity debugging. 225 * 226 * <p>The calling app MUST have a context-specific reason for requesting a connectivity 227 * bugreport, such as detecting a connectivity-related issue. This API SHALL NOT be used to 228 * perform random sampling from a fleet of public end-user devices. 229 * 230 * <p>Calling this API will cause the system to ask the user for consent every single time. The 231 * bugreport artifacts will be copied over to the given file descriptors only if the user 232 * consents to sharing with the calling app. 233 * 234 * <p>This starts a bugreport in the background. However the call itself can take several 235 * seconds to return in the worst case. {@code callback} will receive progress and status 236 * updates. 237 * 238 * <p>Requires that the calling app has carrier privileges (see {@link 239 * android.telephony.TelephonyManager#hasCarrierPrivileges}) on any active subscription. 240 * 241 * @param bugreportFd file to write the bugreport. This should be opened in write-only, append 242 * mode. 243 * @param callback callback for progress and status updates. 244 */ 245 @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges 246 @WorkerThread startConnectivityBugreport( @onNull ParcelFileDescriptor bugreportFd, @NonNull @CallbackExecutor Executor executor, @NonNull BugreportCallback callback)247 public void startConnectivityBugreport( 248 @NonNull ParcelFileDescriptor bugreportFd, 249 @NonNull @CallbackExecutor Executor executor, 250 @NonNull BugreportCallback callback) { 251 startBugreport( 252 bugreportFd, 253 null /* screenshotFd */, 254 new BugreportParams(BugreportParams.BUGREPORT_MODE_TELEPHONY), 255 executor, 256 callback); 257 } 258 259 /** 260 * Cancels the currently running bugreport. 261 * 262 * <p>Apps are only able to cancel their own bugreports. App A cannot cancel a bugreport started 263 * by app B. 264 * 265 * <p>Requires permission: {@link android.Manifest.permission#DUMP} or that the calling app has 266 * carrier privileges (see {@link android.telephony.TelephonyManager#hasCarrierPrivileges}) on 267 * any active subscription. 268 * 269 * @throws SecurityException if trying to cancel another app's bugreport in progress 270 */ 271 @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges 272 @WorkerThread cancelBugreport()273 public void cancelBugreport() { 274 try { 275 mBinder.cancelBugreport(-1 /* callingUid */, mContext.getOpPackageName()); 276 } catch (RemoteException e) { 277 throw e.rethrowFromSystemServer(); 278 } 279 } 280 281 /** 282 * Requests a bugreport. 283 * 284 * <p>This requests the platform/system to take a bugreport and makes the final bugreport 285 * available to the user. The user may choose to share it with another app, but the bugreport is 286 * never given back directly to the app that requested it. 287 * 288 * @param params {@link BugreportParams} that specify what kind of a bugreport should be taken, 289 * please note that not all kinds of bugreport allow for a progress notification 290 * @param shareTitle title on the final share notification 291 * @param shareDescription description on the final share notification 292 * @hide 293 */ 294 @SystemApi 295 @RequiresPermission(android.Manifest.permission.DUMP) requestBugreport( @onNull BugreportParams params, @Nullable CharSequence shareTitle, @Nullable CharSequence shareDescription)296 public void requestBugreport( 297 @NonNull BugreportParams params, 298 @Nullable CharSequence shareTitle, 299 @Nullable CharSequence shareDescription) { 300 try { 301 String title = shareTitle == null ? null : shareTitle.toString(); 302 String description = shareDescription == null ? null : shareDescription.toString(); 303 ActivityManager.getService() 304 .requestBugReportWithDescription(title, description, params.getMode()); 305 } catch (RemoteException e) { 306 throw e.rethrowFromSystemServer(); 307 } 308 } 309 310 private final class DumpstateListener extends IDumpstateListener.Stub { 311 private final Executor mExecutor; 312 private final BugreportCallback mCallback; 313 private final boolean mIsScreenshotRequested; 314 DumpstateListener( Executor executor, BugreportCallback callback, boolean isScreenshotRequested)315 DumpstateListener( 316 Executor executor, BugreportCallback callback, boolean isScreenshotRequested) { 317 mExecutor = executor; 318 mCallback = callback; 319 mIsScreenshotRequested = isScreenshotRequested; 320 } 321 322 @Override onProgress(int progress)323 public void onProgress(int progress) throws RemoteException { 324 final long identity = Binder.clearCallingIdentity(); 325 try { 326 mExecutor.execute(() -> mCallback.onProgress(progress)); 327 } finally { 328 Binder.restoreCallingIdentity(identity); 329 } 330 } 331 332 @Override onError(int errorCode)333 public void onError(int errorCode) throws RemoteException { 334 final long identity = Binder.clearCallingIdentity(); 335 try { 336 mExecutor.execute(() -> mCallback.onError(errorCode)); 337 } finally { 338 Binder.restoreCallingIdentity(identity); 339 } 340 } 341 342 @Override onFinished()343 public void onFinished() throws RemoteException { 344 final long identity = Binder.clearCallingIdentity(); 345 try { 346 mExecutor.execute(() -> mCallback.onFinished()); 347 } finally { 348 Binder.restoreCallingIdentity(identity); 349 } 350 } 351 352 @Override onScreenshotTaken(boolean success)353 public void onScreenshotTaken(boolean success) throws RemoteException { 354 if (!mIsScreenshotRequested) { 355 return; 356 } 357 358 Handler mainThreadHandler = new Handler(Looper.getMainLooper()); 359 mainThreadHandler.post( 360 () -> { 361 int message = 362 success 363 ? R.string.bugreport_screenshot_success_toast 364 : R.string.bugreport_screenshot_failure_toast; 365 Toast.makeText(mContext, message, Toast.LENGTH_LONG).show(); 366 }); 367 } 368 369 @Override onUiIntensiveBugreportDumpsFinished()370 public void onUiIntensiveBugreportDumpsFinished() throws RemoteException { 371 final long identity = Binder.clearCallingIdentity(); 372 try { 373 mExecutor.execute(() -> mCallback.onEarlyReportFinished()); 374 } finally { 375 Binder.restoreCallingIdentity(identity); 376 } 377 } 378 } 379 } 380