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.SystemApi; 26 import android.annotation.SystemService; 27 import android.annotation.TestApi; 28 import android.app.ActivityManager; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.os.Handler; 32 import android.util.Log; 33 import android.widget.Toast; 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 * @hide 49 */ 50 @SystemApi 51 @TestApi 52 @SystemService(Context.BUGREPORT_SERVICE) 53 public final class BugreportManager { 54 55 private static final String TAG = "BugreportManager"; 56 private static final String INTENT_UI_INTENSIVE_BUGREPORT_DUMPS_FINISHED = 57 "com.android.internal.intent.action.UI_INTENSIVE_BUGREPORT_DUMPS_FINISHED"; 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 public abstract static class BugreportCallback { 72 /** @hide */ 73 @Retention(RetentionPolicy.SOURCE) 74 @IntDef(prefix = { "BUGREPORT_ERROR_" }, value = { 75 BUGREPORT_ERROR_INVALID_INPUT, 76 BUGREPORT_ERROR_RUNTIME, 77 BUGREPORT_ERROR_USER_DENIED_CONSENT, 78 BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT, 79 BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS 80 }) 81 82 /** Possible error codes taking a bugreport can encounter */ 83 public @interface BugreportErrorCode {} 84 85 /** The input options were invalid */ 86 public static final int BUGREPORT_ERROR_INVALID_INPUT = 87 IDumpstateListener.BUGREPORT_ERROR_INVALID_INPUT; 88 89 /** A runtime error occured */ 90 public static final int BUGREPORT_ERROR_RUNTIME = 91 IDumpstateListener.BUGREPORT_ERROR_RUNTIME_ERROR; 92 93 /** User denied consent to share the bugreport */ 94 public static final int BUGREPORT_ERROR_USER_DENIED_CONSENT = 95 IDumpstateListener.BUGREPORT_ERROR_USER_DENIED_CONSENT; 96 97 /** The request to get user consent timed out. */ 98 public static final int BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT = 99 IDumpstateListener.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT; 100 101 /** There is currently a bugreport running. The caller should try again later. */ 102 public static final int BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS = 103 IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS; 104 105 /** 106 * Called when there is a progress update. 107 * @param progress the progress in [0.0, 100.0] 108 */ onProgress(@loatRangefrom = 0f, to = 100f) float progress)109 public void onProgress(@FloatRange(from = 0f, to = 100f) float progress) {} 110 111 /** 112 * Called when taking bugreport resulted in an error. 113 * 114 * <p>If {@code BUGREPORT_ERROR_USER_DENIED_CONSENT} is passed, then the user did not 115 * consent to sharing the bugreport with the calling app. 116 * 117 * <p>If {@code BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT} is passed, then the consent timed 118 * out, but the bugreport could be available in the internal directory of dumpstate for 119 * manual retrieval. 120 * 121 * <p> If {@code BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS} is passed, then the 122 * caller should try later, as only one bugreport can be in progress at a time. 123 */ onError(@ugreportErrorCode int errorCode)124 public void onError(@BugreportErrorCode int errorCode) {} 125 126 /** 127 * Called when taking bugreport finishes successfully. 128 */ onFinished()129 public void onFinished() {} 130 } 131 132 /** 133 * Starts a bugreport. 134 * 135 * <p>This starts a bugreport in the background. However the call itself can take several 136 * seconds to return in the worst case. {@code callback} will receive progress and status 137 * updates. 138 * 139 * <p>The bugreport artifacts will be copied over to the given file descriptors only if the 140 * user consents to sharing with the calling app. 141 * 142 * <p>{@link BugreportManager} takes ownership of {@code bugreportFd} and {@code screenshotFd}. 143 * 144 * @param bugreportFd file to write the bugreport. This should be opened in write-only, 145 * append mode. 146 * @param screenshotFd file to write the screenshot, if necessary. This should be opened 147 * in write-only, append mode. 148 * @param params options that specify what kind of a bugreport should be taken 149 * @param callback callback for progress and status updates 150 */ 151 @RequiresPermission(android.Manifest.permission.DUMP) startBugreport(@onNull ParcelFileDescriptor bugreportFd, @Nullable ParcelFileDescriptor screenshotFd, @NonNull BugreportParams params, @NonNull @CallbackExecutor Executor executor, @NonNull BugreportCallback callback)152 public void startBugreport(@NonNull ParcelFileDescriptor bugreportFd, 153 @Nullable ParcelFileDescriptor screenshotFd, 154 @NonNull BugreportParams params, 155 @NonNull @CallbackExecutor Executor executor, 156 @NonNull BugreportCallback callback) { 157 try { 158 Preconditions.checkNotNull(bugreportFd); 159 Preconditions.checkNotNull(params); 160 Preconditions.checkNotNull(executor); 161 Preconditions.checkNotNull(callback); 162 163 boolean isScreenshotRequested = screenshotFd != null; 164 if (screenshotFd == null) { 165 // Binder needs a valid File Descriptor to be passed 166 screenshotFd = ParcelFileDescriptor.open(new File("/dev/null"), 167 ParcelFileDescriptor.MODE_READ_ONLY); 168 } 169 DumpstateListener dsListener = new DumpstateListener(executor, callback, 170 isScreenshotRequested); 171 // Note: mBinder can get callingUid from the binder transaction. 172 mBinder.startBugreport(-1 /* callingUid */, 173 mContext.getOpPackageName(), 174 bugreportFd.getFileDescriptor(), 175 screenshotFd.getFileDescriptor(), 176 params.getMode(), dsListener, isScreenshotRequested); 177 } catch (RemoteException e) { 178 throw e.rethrowFromSystemServer(); 179 } catch (FileNotFoundException e) { 180 Log.wtf(TAG, "Not able to find /dev/null file: ", e); 181 } finally { 182 // We can close the file descriptors here because binder would have duped them. 183 IoUtils.closeQuietly(bugreportFd); 184 if (screenshotFd != null) { 185 IoUtils.closeQuietly(screenshotFd); 186 } 187 } 188 } 189 190 /* 191 * Cancels a currently running bugreport. 192 */ 193 @RequiresPermission(android.Manifest.permission.DUMP) cancelBugreport()194 public void cancelBugreport() { 195 try { 196 mBinder.cancelBugreport(); 197 } catch (RemoteException e) { 198 throw e.rethrowFromSystemServer(); 199 } 200 } 201 202 /** 203 * Requests a bugreport. 204 * 205 * <p>This requests the platform/system to take a bugreport and makes the final bugreport 206 * available to the user. The user may choose to share it with another app, but the bugreport 207 * is never given back directly to the app that requested it. 208 * 209 * @param params {@link BugreportParams} that specify what kind of a bugreport should 210 * be taken, please note that not all kinds of bugreport allow for a 211 * progress notification 212 * @param shareTitle title on the final share notification 213 * @param shareDescription description on the final share notification 214 */ 215 @RequiresPermission(android.Manifest.permission.DUMP) requestBugreport(@onNull BugreportParams params, @Nullable CharSequence shareTitle, @Nullable CharSequence shareDescription)216 public void requestBugreport(@NonNull BugreportParams params, @Nullable CharSequence shareTitle, 217 @Nullable CharSequence shareDescription) { 218 try { 219 String title = shareTitle == null ? null : shareTitle.toString(); 220 String description = shareDescription == null ? null : shareDescription.toString(); 221 ActivityManager.getService().requestBugReportWithDescription(title, description, 222 params.getMode()); 223 } catch (RemoteException e) { 224 throw e.rethrowFromSystemServer(); 225 } 226 } 227 228 private final class DumpstateListener extends IDumpstateListener.Stub { 229 private final Executor mExecutor; 230 private final BugreportCallback mCallback; 231 private final boolean mIsScreenshotRequested; 232 DumpstateListener(Executor executor, BugreportCallback callback, boolean isScreenshotRequested)233 DumpstateListener(Executor executor, BugreportCallback callback, 234 boolean isScreenshotRequested) { 235 mExecutor = executor; 236 mCallback = callback; 237 mIsScreenshotRequested = isScreenshotRequested; 238 } 239 240 @Override onProgress(int progress)241 public void onProgress(int progress) throws RemoteException { 242 final long identity = Binder.clearCallingIdentity(); 243 try { 244 mExecutor.execute(() -> { 245 mCallback.onProgress(progress); 246 }); 247 } finally { 248 Binder.restoreCallingIdentity(identity); 249 } 250 } 251 252 @Override onError(int errorCode)253 public void onError(int errorCode) throws RemoteException { 254 final long identity = Binder.clearCallingIdentity(); 255 try { 256 mExecutor.execute(() -> { 257 mCallback.onError(errorCode); 258 }); 259 } finally { 260 Binder.restoreCallingIdentity(identity); 261 } 262 } 263 264 @Override onFinished()265 public void onFinished() throws RemoteException { 266 final long identity = Binder.clearCallingIdentity(); 267 try { 268 mExecutor.execute(() -> { 269 mCallback.onFinished(); 270 }); 271 } finally { 272 Binder.restoreCallingIdentity(identity); 273 } 274 } 275 276 @Override onScreenshotTaken(boolean success)277 public void onScreenshotTaken(boolean success) throws RemoteException { 278 if (!mIsScreenshotRequested) { 279 return; 280 } 281 282 Handler mainThreadHandler = new Handler(Looper.getMainLooper()); 283 mainThreadHandler.post( 284 () -> { 285 int message = success ? R.string.bugreport_screenshot_success_toast 286 : R.string.bugreport_screenshot_failure_toast; 287 Toast.makeText(mContext, message, Toast.LENGTH_LONG).show(); 288 }); 289 } 290 291 @Override onUiIntensiveBugreportDumpsFinished(String callingPackage)292 public void onUiIntensiveBugreportDumpsFinished(String callingPackage) 293 throws RemoteException { 294 final long identity = Binder.clearCallingIdentity(); 295 try { 296 mExecutor.execute(() -> { 297 // Send intent to let calling app to show UI safely without interfering with 298 // the bugreport/screenshot generation. 299 // TODO(b/154298410): When S is ready for API change, add a method in 300 // BugreportCallback so we can just call the callback instead of using 301 // broadcast. 302 Intent intent = new Intent(INTENT_UI_INTENSIVE_BUGREPORT_DUMPS_FINISHED); 303 intent.setPackage(callingPackage); 304 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 305 intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); 306 mContext.sendBroadcast(intent, android.Manifest.permission.DUMP); 307 }); 308 } finally { 309 Binder.restoreCallingIdentity(identity); 310 } 311 } 312 } 313 } 314