/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.os; import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressAutoDoc; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.UserHandleAware; import android.annotation.WorkerThread; import android.app.ActivityManager; import android.content.Context; import android.util.Log; import android.widget.Toast; import com.android.internal.R; import com.android.internal.util.Preconditions; import libcore.io.IoUtils; import java.io.File; import java.io.FileNotFoundException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.concurrent.Executor; /** * Class that provides a privileged API to capture and consume bugreports. * *

This class may only be used by apps that currently have carrier privileges (see {@link * android.telephony.TelephonyManager#hasCarrierPrivileges}) on an active SIM or priv-apps * explicitly allowed by the device manufacturer. * *

Only one bugreport can be generated by the system at a time. */ @SystemService(Context.BUGREPORT_SERVICE) public final class BugreportManager { private static final String TAG = "BugreportManager"; private final Context mContext; private final IDumpstate mBinder; /** @hide */ public BugreportManager(@NonNull Context context, IDumpstate binder) { mContext = context; mBinder = binder; } /** * An interface describing the callback for bugreport progress and status. * *

Callers will receive {@link #onProgress} calls as the bugreport progresses, followed by a * terminal call to either {@link #onFinished} or {@link #onError}. * *

If an issue is encountered while starting the bugreport asynchronously, callers will * receive an {@link #onError} call without any {@link #onProgress} callbacks. */ public abstract static class BugreportCallback { /** * Possible error codes taking a bugreport can encounter. * * @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = {"BUGREPORT_ERROR_"}, value = { BUGREPORT_ERROR_INVALID_INPUT, BUGREPORT_ERROR_RUNTIME, BUGREPORT_ERROR_USER_DENIED_CONSENT, BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT, BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS, BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE }) public @interface BugreportErrorCode {} /** * The input options were invalid. For example, the destination file the app provided could * not be written by the system. */ public static final int BUGREPORT_ERROR_INVALID_INPUT = IDumpstateListener.BUGREPORT_ERROR_INVALID_INPUT; /** A runtime error occurred. */ public static final int BUGREPORT_ERROR_RUNTIME = IDumpstateListener.BUGREPORT_ERROR_RUNTIME_ERROR; /** User denied consent to share the bugreport. */ public static final int BUGREPORT_ERROR_USER_DENIED_CONSENT = IDumpstateListener.BUGREPORT_ERROR_USER_DENIED_CONSENT; /** The request to get user consent timed out. */ public static final int BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT = IDumpstateListener.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT; /** There is currently a bugreport running. The caller should try again later. */ public static final int BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS = IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS; /** There is no bugreport to retrieve for the caller. */ public static final int BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE = IDumpstateListener.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE; /** * Called when there is a progress update. * * @param progress the progress in [0.0, 100.0] */ public void onProgress(@FloatRange(from = 0f, to = 100f) float progress) {} /** * Called when taking bugreport resulted in an error. * *

If {@code BUGREPORT_ERROR_USER_DENIED_CONSENT} is passed, then the user did not * consent to sharing the bugreport with the calling app. * *

If {@code BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT} is passed, then the consent timed * out, but the bugreport could be available in the internal directory of dumpstate for * manual retrieval. * *

If {@code BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS} is passed, then the caller * should try later, as only one bugreport can be in progress at a time. */ public void onError(@BugreportErrorCode int errorCode) {} /** * Called when taking bugreport finishes successfully. * *

This callback will be invoked if the * {@code BugreportParams#BUGREPORT_FLAG_DEFER_CONSENT} flag is not set. */ public void onFinished() {} /** Called when taking bugreport finishes successfully. * *

This callback will only be invoked if the * {@link BugreportParams#BUGREPORT_FLAG_DEFER_CONSENT} flag is set. Otherwise, the * {@link #onFinished()} callback will be invoked. * * @param bugreportFile the absolute path of the generated bugreport file. * @hide */ @SystemApi public void onFinished(@NonNull String bugreportFile) {} /** * Called when it is ready for calling app to show UI, showing any extra UI before this * callback can interfere with bugreport generation. */ public void onEarlyReportFinished() {} } /** * Speculatively pre-dumps UI data for a bugreport request that might come later. * *

Triggers the dump of certain critical UI data, e.g. traces stored in short * ring buffers that might get lost by the time the actual bugreport is requested. * *

{@link #startBugreport} will then pick the pre-dumped data if both of the following * conditions are met: * - {@link android.os.BugreportParams#BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA} is specified. * - {@link #preDumpUiData} and {@link #startBugreport} were called by the same UID. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.DUMP) @WorkerThread public void preDumpUiData() { try { mBinder.preDumpUiData(mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Starts a bugreport. * *

This starts a bugreport in the background. However the call itself can take several * seconds to return in the worst case. {@code callback} will receive progress and status * updates. * *

The bugreport artifacts will be copied over to the given file descriptors only if the user * consents to sharing with the calling app. If * {@link BugreportParams#BUGREPORT_FLAG_DEFER_CONSENT} is set, user consent will be deferred * and no files will be copied to the given file descriptors. * *

{@link BugreportManager} takes ownership of {@code bugreportFd} and {@code screenshotFd}. * * @param bugreportFd file to write the bugreport. This should be opened in write-only, append * mode. * @param screenshotFd file to write the screenshot, if necessary. This should be opened in * write-only, append mode. * @param params options that specify what kind of a bugreport should be taken * @param callback callback for progress and status updates * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.DUMP) @WorkerThread public void startBugreport( @NonNull ParcelFileDescriptor bugreportFd, @Nullable ParcelFileDescriptor screenshotFd, @NonNull BugreportParams params, @NonNull @CallbackExecutor Executor executor, @NonNull BugreportCallback callback) { try { Preconditions.checkNotNull(bugreportFd); Preconditions.checkNotNull(params); Preconditions.checkNotNull(executor); Preconditions.checkNotNull(callback); boolean deferConsent = (params.getFlags() & BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT) != 0; boolean isScreenshotRequested = screenshotFd != null || deferConsent; if (screenshotFd == null) { // Binder needs a valid File Descriptor to be passed screenshotFd = ParcelFileDescriptor.open( new File("/dev/null"), ParcelFileDescriptor.MODE_READ_ONLY); } DumpstateListener dsListener = new DumpstateListener(executor, callback, isScreenshotRequested, deferConsent); // Note: mBinder can get callingUid from the binder transaction. mBinder.startBugreport( -1 /* callingUid */, mContext.getOpPackageName(), bugreportFd.getFileDescriptor(), screenshotFd.getFileDescriptor(), params.getMode(), params.getFlags(), dsListener, isScreenshotRequested); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } catch (FileNotFoundException e) { Log.wtf(TAG, "Not able to find /dev/null file: ", e); } finally { // We can close the file descriptors here because binder would have duped them. IoUtils.closeQuietly(bugreportFd); if (screenshotFd != null) { IoUtils.closeQuietly(screenshotFd); } } } /** * Retrieves a previously generated bugreport. * *

The previously generated bugreport must have been generated by calling {@link * #startBugreport(ParcelFileDescriptor, ParcelFileDescriptor, BugreportParams, * Executor, BugreportCallback)} with the {@link BugreportParams#BUGREPORT_FLAG_DEFER_CONSENT} * flag set. The bugreport file returned by the {@link BugreportCallback#onFinished(String)} * callback for a previously generated bugreport must be passed to this method. A caller may * only retrieve bugreports that they have previously requested. * *

The bugreport artifacts will be copied over to the given file descriptor only if the user * consents to sharing with the calling app. * *

{@link BugreportManager} takes ownership of {@code bugreportFd}. * *

The caller can reattempt to retrieve the bugreport multiple times if the user has not * consented on previous attempts. * * @param bugreportFile the identifier for a bugreport that was previously generated for this * caller using {@code startBugreport}. * @param bugreportFd file to copy over the previous bugreport. This should be opened in * write-only, append mode. * @param executor the executor to execute callback methods. * @param callback callback for progress and status updates. * @hide */ @SystemApi @RequiresPermission(Manifest.permission.DUMP) @WorkerThread @UserHandleAware public void retrieveBugreport( @NonNull String bugreportFile, @NonNull ParcelFileDescriptor bugreportFd, @NonNull @CallbackExecutor Executor executor, @NonNull BugreportCallback callback ) { try { Preconditions.checkNotNull(bugreportFile); Preconditions.checkNotNull(bugreportFd); Preconditions.checkNotNull(executor); Preconditions.checkNotNull(callback); DumpstateListener dsListener = new DumpstateListener(executor, callback, false, false); mBinder.retrieveBugreport(Binder.getCallingUid(), mContext.getOpPackageName(), mContext.getUserId(), bugreportFd.getFileDescriptor(), bugreportFile, /* keepBugreportOnRetrieval = */ false, dsListener); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } finally { IoUtils.closeQuietly(bugreportFd); } } /** * Starts a connectivity bugreport. * *

The connectivity bugreport is a specialized version of bugreport that only includes * information specifically for debugging connectivity-related issues (e.g. telephony, wi-fi, * and IP networking issues). It is intended primarily for use by OEMs and network providers * such as mobile network operators. In addition to generally excluding information that isn't * targeted to connectivity debugging, this type of bugreport excludes PII and sensitive * information that isn't strictly necessary for connectivity debugging. * *

The calling app MUST have a context-specific reason for requesting a connectivity * bugreport, such as detecting a connectivity-related issue. This API SHALL NOT be used to * perform random sampling from a fleet of public end-user devices. * *

Calling this API will cause the system to ask the user for consent every single time. The * bugreport artifacts will be copied over to the given file descriptors only if the user * consents to sharing with the calling app. * *

This starts a bugreport in the background. However the call itself can take several * seconds to return in the worst case. {@code callback} will receive progress and status * updates. * *

Requires that the calling app has carrier privileges (see {@link * android.telephony.TelephonyManager#hasCarrierPrivileges}) on any active subscription. * * @param bugreportFd file to write the bugreport. This should be opened in write-only, append * mode. * @param callback callback for progress and status updates. */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @WorkerThread public void startConnectivityBugreport( @NonNull ParcelFileDescriptor bugreportFd, @NonNull @CallbackExecutor Executor executor, @NonNull BugreportCallback callback) { startBugreport( bugreportFd, null /* screenshotFd */, new BugreportParams(BugreportParams.BUGREPORT_MODE_TELEPHONY), executor, callback); } /** * Cancels the currently running bugreport. * *

Apps are only able to cancel their own bugreports. App A cannot cancel a bugreport started * by app B. * *

Requires permission: {@link android.Manifest.permission#DUMP} or that the calling app has * carrier privileges (see {@link android.telephony.TelephonyManager#hasCarrierPrivileges}) on * any active subscription. * * @throws SecurityException if trying to cancel another app's bugreport in progress */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @WorkerThread public void cancelBugreport() { try { mBinder.cancelBugreport(-1 /* callingUid */, mContext.getOpPackageName()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Requests a bugreport. * *

This requests the platform/system to take a bugreport and makes the final bugreport * available to the user. The user may choose to share it with another app, but the bugreport is * never given back directly to the app that requested it. * * @param params {@link BugreportParams} that specify what kind of a bugreport should be taken, * please note that not all kinds of bugreport allow for a progress notification * @param shareTitle title on the final share notification * @param shareDescription description on the final share notification * @hide */ @SystemApi @RequiresPermission(Manifest.permission.DUMP) public void requestBugreport( @NonNull BugreportParams params, @Nullable CharSequence shareTitle, @Nullable CharSequence shareDescription) { try { String title = shareTitle == null ? null : shareTitle.toString(); String description = shareDescription == null ? null : shareDescription.toString(); ActivityManager.getService() .requestBugReportWithDescription(title, description, params.getMode()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } private final class DumpstateListener extends IDumpstateListener.Stub { private final Executor mExecutor; private final BugreportCallback mCallback; private final boolean mIsScreenshotRequested; private final boolean mIsConsentDeferred; DumpstateListener( Executor executor, BugreportCallback callback, boolean isScreenshotRequested, boolean isConsentDeferred) { mExecutor = executor; mCallback = callback; mIsScreenshotRequested = isScreenshotRequested; mIsConsentDeferred = isConsentDeferred; } @Override public void onProgress(int progress) throws RemoteException { final long identity = Binder.clearCallingIdentity(); try { mExecutor.execute(() -> mCallback.onProgress(progress)); } finally { Binder.restoreCallingIdentity(identity); } } @Override public void onError(int errorCode) throws RemoteException { final long identity = Binder.clearCallingIdentity(); try { mExecutor.execute(() -> mCallback.onError(errorCode)); } finally { Binder.restoreCallingIdentity(identity); } } @Override public void onFinished(String bugreportFile) throws RemoteException { final long identity = Binder.clearCallingIdentity(); try { if (mIsConsentDeferred) { mExecutor.execute(() -> mCallback.onFinished(bugreportFile)); } else { mExecutor.execute(() -> mCallback.onFinished()); } } finally { Binder.restoreCallingIdentity(identity); } } @Override public void onScreenshotTaken(boolean success) throws RemoteException { if (!mIsScreenshotRequested) { return; } Handler mainThreadHandler = new Handler(Looper.getMainLooper()); mainThreadHandler.post( () -> { int message = success ? R.string.bugreport_screenshot_success_toast : R.string.bugreport_screenshot_failure_toast; Toast.makeText(mContext, message, Toast.LENGTH_LONG).show(); }); } @Override public void onUiIntensiveBugreportDumpsFinished() throws RemoteException { final long identity = Binder.clearCallingIdentity(); try { mExecutor.execute(() -> mCallback.onEarlyReportFinished()); } finally { Binder.restoreCallingIdentity(identity); } } } }