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