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