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.content.Context;
29 import android.util.Log;
30 
31 import com.android.internal.util.Preconditions;
32 
33 import libcore.io.IoUtils;
34 
35 import java.io.File;
36 import java.io.FileNotFoundException;
37 import java.lang.annotation.Retention;
38 import java.lang.annotation.RetentionPolicy;
39 import java.util.concurrent.Executor;
40 
41 /**
42  * Class that provides a privileged API to capture and consume bugreports.
43  *
44  * @hide
45  */
46 @SystemApi
47 @TestApi
48 @SystemService(Context.BUGREPORT_SERVICE)
49 public final class BugreportManager {
50 
51     private static final String TAG = "BugreportManager";
52 
53     private final Context mContext;
54     private final IDumpstate mBinder;
55 
56     /** @hide */
BugreportManager(@onNull Context context, IDumpstate binder)57     public BugreportManager(@NonNull Context context, IDumpstate binder) {
58         mContext = context;
59         mBinder = binder;
60     }
61 
62     /**
63      * An interface describing the callback for bugreport progress and status.
64      */
65     public abstract static class BugreportCallback {
66         /** @hide */
67         @Retention(RetentionPolicy.SOURCE)
68         @IntDef(prefix = { "BUGREPORT_ERROR_" }, value = {
69                 BUGREPORT_ERROR_INVALID_INPUT,
70                 BUGREPORT_ERROR_RUNTIME,
71                 BUGREPORT_ERROR_USER_DENIED_CONSENT,
72                 BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT,
73                 BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS
74         })
75 
76         /** Possible error codes taking a bugreport can encounter */
77         public @interface BugreportErrorCode {}
78 
79         /** The input options were invalid */
80         public static final int BUGREPORT_ERROR_INVALID_INPUT =
81                 IDumpstateListener.BUGREPORT_ERROR_INVALID_INPUT;
82 
83         /** A runtime error occured */
84         public static final int BUGREPORT_ERROR_RUNTIME =
85                 IDumpstateListener.BUGREPORT_ERROR_RUNTIME_ERROR;
86 
87         /** User denied consent to share the bugreport */
88         public static final int BUGREPORT_ERROR_USER_DENIED_CONSENT =
89                 IDumpstateListener.BUGREPORT_ERROR_USER_DENIED_CONSENT;
90 
91         /** The request to get user consent timed out. */
92         public static final int BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT =
93                 IDumpstateListener.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT;
94 
95         /** There is currently a bugreport running. The caller should try again later. */
96         public static final int BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS =
97                 IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS;
98 
99         /**
100          * Called when there is a progress update.
101          * @param progress the progress in [0.0, 100.0]
102          */
onProgress(@loatRangefrom = 0f, to = 100f) float progress)103         public void onProgress(@FloatRange(from = 0f, to = 100f) float progress) {}
104 
105         /**
106          * Called when taking bugreport resulted in an error.
107          *
108          * <p>If {@code BUGREPORT_ERROR_USER_DENIED_CONSENT} is passed, then the user did not
109          * consent to sharing the bugreport with the calling app.
110          *
111          * <p>If {@code BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT} is passed, then the consent timed
112          * out, but the bugreport could be available in the internal directory of dumpstate for
113          * manual retrieval.
114          *
115          * <p> If {@code BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS} is passed, then the
116          * caller should try later, as only one bugreport can be in progress at a time.
117          */
onError(@ugreportErrorCode int errorCode)118         public void onError(@BugreportErrorCode int errorCode) {}
119 
120         /**
121          * Called when taking bugreport finishes successfully.
122          */
onFinished()123         public void onFinished() {}
124     }
125 
126     /**
127      * Starts a bugreport.
128      *
129      * <p>This starts a bugreport in the background. However the call itself can take several
130      * seconds to return in the worst case. {@code callback} will receive progress and status
131      * updates.
132      *
133      * <p>The bugreport artifacts will be copied over to the given file descriptors only if the
134      * user consents to sharing with the calling app.
135      *
136      * <p>{@link BugreportManager} takes ownership of {@code bugreportFd} and {@code screenshotFd}.
137      *
138      * @param bugreportFd file to write the bugreport. This should be opened in write-only,
139      *     append mode.
140      * @param screenshotFd file to write the screenshot, if necessary. This should be opened
141      *     in write-only, append mode.
142      * @param params options that specify what kind of a bugreport should be taken
143      * @param callback callback for progress and status updates
144      */
145     @RequiresPermission(android.Manifest.permission.DUMP)
startBugreport(@onNull ParcelFileDescriptor bugreportFd, @Nullable ParcelFileDescriptor screenshotFd, @NonNull BugreportParams params, @NonNull @CallbackExecutor Executor executor, @NonNull BugreportCallback callback)146     public void startBugreport(@NonNull ParcelFileDescriptor bugreportFd,
147             @Nullable ParcelFileDescriptor screenshotFd,
148             @NonNull BugreportParams params,
149             @NonNull @CallbackExecutor Executor executor,
150             @NonNull BugreportCallback callback) {
151         try {
152             Preconditions.checkNotNull(bugreportFd);
153             Preconditions.checkNotNull(params);
154             Preconditions.checkNotNull(executor);
155             Preconditions.checkNotNull(callback);
156 
157             if (screenshotFd == null) {
158                 // Binder needs a valid File Descriptor to be passed
159                 screenshotFd = ParcelFileDescriptor.open(new File("/dev/null"),
160                         ParcelFileDescriptor.MODE_READ_ONLY);
161             }
162             DumpstateListener dsListener = new DumpstateListener(executor, callback);
163             // Note: mBinder can get callingUid from the binder transaction.
164             mBinder.startBugreport(-1 /* callingUid */,
165                     mContext.getOpPackageName(),
166                     bugreportFd.getFileDescriptor(),
167                     screenshotFd.getFileDescriptor(),
168                     params.getMode(), dsListener);
169         } catch (RemoteException e) {
170             throw e.rethrowFromSystemServer();
171         } catch (FileNotFoundException e) {
172             Log.wtf(TAG, "Not able to find /dev/null file: ", e);
173         } finally {
174             // We can close the file descriptors here because binder would have duped them.
175             IoUtils.closeQuietly(bugreportFd);
176             if (screenshotFd != null) {
177                 IoUtils.closeQuietly(screenshotFd);
178             }
179         }
180     }
181 
182     /*
183      * Cancels a currently running bugreport.
184      */
185     @RequiresPermission(android.Manifest.permission.DUMP)
cancelBugreport()186     public void cancelBugreport() {
187         try {
188             mBinder.cancelBugreport();
189         } catch (RemoteException e) {
190             throw e.rethrowFromSystemServer();
191         }
192     }
193 
194     private final class DumpstateListener extends IDumpstateListener.Stub {
195         private final Executor mExecutor;
196         private final BugreportCallback mCallback;
197 
DumpstateListener(Executor executor, BugreportCallback callback)198         DumpstateListener(Executor executor, BugreportCallback callback) {
199             mExecutor = executor;
200             mCallback = callback;
201         }
202 
203         @Override
onProgress(int progress)204         public void onProgress(int progress) throws RemoteException {
205             final long identity = Binder.clearCallingIdentity();
206             try {
207                 mExecutor.execute(() -> {
208                     mCallback.onProgress(progress);
209                 });
210             } finally {
211                 Binder.restoreCallingIdentity(identity);
212             }
213         }
214 
215         @Override
onError(int errorCode)216         public void onError(int errorCode) throws RemoteException {
217             final long identity = Binder.clearCallingIdentity();
218             try {
219                 mExecutor.execute(() -> {
220                     mCallback.onError(errorCode);
221                 });
222             } finally {
223                 Binder.restoreCallingIdentity(identity);
224             }
225         }
226 
227         @Override
onFinished()228         public void onFinished() throws RemoteException {
229             final long identity = Binder.clearCallingIdentity();
230             try {
231                 mExecutor.execute(() -> {
232                     mCallback.onFinished();
233                 });
234             } finally {
235                 Binder.restoreCallingIdentity(identity);
236             }
237         }
238     }
239 }
240