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.SuppressAutoDoc;
26 import android.annotation.SystemApi;
27 import android.annotation.SystemService;
28 import android.annotation.WorkerThread;
29 import android.app.ActivityManager;
30 import android.content.Context;
31 import android.util.Log;
32 import android.widget.Toast;
33 
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  * <p>This class may only be used by apps that currently have carrier privileges (see {@link
49  * android.telephony.TelephonyManager#hasCarrierPrivileges}) on an active SIM or priv-apps
50  * explicitly allowed by the device manufacturer.
51  *
52  * <p>Only one bugreport can be generated by the system at a time.
53  */
54 @SystemService(Context.BUGREPORT_SERVICE)
55 public final class BugreportManager {
56 
57     private static final String TAG = "BugreportManager";
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      * <p>Callers will receive {@link #onProgress} calls as the bugreport progresses, followed by a
72      * terminal call to either {@link #onFinished} or {@link #onError}.
73      *
74      * <p>If an issue is encountered while starting the bugreport asynchronously, callers will
75      * receive an {@link #onError} call without any {@link #onProgress} callbacks.
76      */
77     public abstract static class BugreportCallback {
78         /**
79          * Possible error codes taking a bugreport can encounter.
80          *
81          * @hide
82          */
83         @Retention(RetentionPolicy.SOURCE)
84         @IntDef(
85                 prefix = {"BUGREPORT_ERROR_"},
86                 value = {
87                     BUGREPORT_ERROR_INVALID_INPUT,
88                     BUGREPORT_ERROR_RUNTIME,
89                     BUGREPORT_ERROR_USER_DENIED_CONSENT,
90                     BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT,
91                     BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS
92                 })
93         public @interface BugreportErrorCode {}
94 
95         /**
96          * The input options were invalid. For example, the destination file the app provided could
97          * not be written by the system.
98          */
99         public static final int BUGREPORT_ERROR_INVALID_INPUT =
100                 IDumpstateListener.BUGREPORT_ERROR_INVALID_INPUT;
101 
102         /** A runtime error occurred. */
103         public static final int BUGREPORT_ERROR_RUNTIME =
104                 IDumpstateListener.BUGREPORT_ERROR_RUNTIME_ERROR;
105 
106         /** User denied consent to share the bugreport. */
107         public static final int BUGREPORT_ERROR_USER_DENIED_CONSENT =
108                 IDumpstateListener.BUGREPORT_ERROR_USER_DENIED_CONSENT;
109 
110         /** The request to get user consent timed out. */
111         public static final int BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT =
112                 IDumpstateListener.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT;
113 
114         /** There is currently a bugreport running. The caller should try again later. */
115         public static final int BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS =
116                 IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS;
117 
118         /**
119          * Called when there is a progress update.
120          *
121          * @param progress the progress in [0.0, 100.0]
122          */
onProgress(@loatRangefrom = 0f, to = 100f) float progress)123         public void onProgress(@FloatRange(from = 0f, to = 100f) float progress) {}
124 
125         /**
126          * Called when taking bugreport resulted in an error.
127          *
128          * <p>If {@code BUGREPORT_ERROR_USER_DENIED_CONSENT} is passed, then the user did not
129          * consent to sharing the bugreport with the calling app.
130          *
131          * <p>If {@code BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT} is passed, then the consent timed
132          * out, but the bugreport could be available in the internal directory of dumpstate for
133          * manual retrieval.
134          *
135          * <p>If {@code BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS} is passed, then the caller
136          * should try later, as only one bugreport can be in progress at a time.
137          */
onError(@ugreportErrorCode int errorCode)138         public void onError(@BugreportErrorCode int errorCode) {}
139 
140         /** Called when taking bugreport finishes successfully. */
onFinished()141         public void onFinished() {}
142 
143         /**
144          * Called when it is ready for calling app to show UI, showing any extra UI before this
145          * callback can interfere with bugreport generation.
146          */
onEarlyReportFinished()147         public void onEarlyReportFinished() {}
148     }
149 
150     /**
151      * Starts a bugreport.
152      *
153      * <p>This starts a bugreport in the background. However the call itself can take several
154      * seconds to return in the worst case. {@code callback} will receive progress and status
155      * updates.
156      *
157      * <p>The bugreport artifacts will be copied over to the given file descriptors only if the user
158      * consents to sharing with the calling app.
159      *
160      * <p>{@link BugreportManager} takes ownership of {@code bugreportFd} and {@code screenshotFd}.
161      *
162      * @param bugreportFd file to write the bugreport. This should be opened in write-only, append
163      *     mode.
164      * @param screenshotFd file to write the screenshot, if necessary. This should be opened in
165      *     write-only, append mode.
166      * @param params options that specify what kind of a bugreport should be taken
167      * @param callback callback for progress and status updates
168      * @hide
169      */
170     @SystemApi
171     @RequiresPermission(android.Manifest.permission.DUMP)
172     @WorkerThread
startBugreport( @onNull ParcelFileDescriptor bugreportFd, @Nullable ParcelFileDescriptor screenshotFd, @NonNull BugreportParams params, @NonNull @CallbackExecutor Executor executor, @NonNull BugreportCallback callback)173     public void startBugreport(
174             @NonNull ParcelFileDescriptor bugreportFd,
175             @Nullable ParcelFileDescriptor screenshotFd,
176             @NonNull BugreportParams params,
177             @NonNull @CallbackExecutor Executor executor,
178             @NonNull BugreportCallback callback) {
179         try {
180             Preconditions.checkNotNull(bugreportFd);
181             Preconditions.checkNotNull(params);
182             Preconditions.checkNotNull(executor);
183             Preconditions.checkNotNull(callback);
184 
185             boolean isScreenshotRequested = screenshotFd != null;
186             if (screenshotFd == null) {
187                 // Binder needs a valid File Descriptor to be passed
188                 screenshotFd =
189                         ParcelFileDescriptor.open(
190                                 new File("/dev/null"), ParcelFileDescriptor.MODE_READ_ONLY);
191             }
192             DumpstateListener dsListener =
193                     new DumpstateListener(executor, callback, isScreenshotRequested);
194             // Note: mBinder can get callingUid from the binder transaction.
195             mBinder.startBugreport(
196                     -1 /* callingUid */,
197                     mContext.getOpPackageName(),
198                     bugreportFd.getFileDescriptor(),
199                     screenshotFd.getFileDescriptor(),
200                     params.getMode(),
201                     dsListener,
202                     isScreenshotRequested);
203         } catch (RemoteException e) {
204             throw e.rethrowFromSystemServer();
205         } catch (FileNotFoundException e) {
206             Log.wtf(TAG, "Not able to find /dev/null file: ", e);
207         } finally {
208             // We can close the file descriptors here because binder would have duped them.
209             IoUtils.closeQuietly(bugreportFd);
210             if (screenshotFd != null) {
211                 IoUtils.closeQuietly(screenshotFd);
212             }
213         }
214     }
215 
216     /**
217      * Starts a connectivity bugreport.
218      *
219      * <p>The connectivity bugreport is a specialized version of bugreport that only includes
220      * information specifically for debugging connectivity-related issues (e.g. telephony, wi-fi,
221      * and IP networking issues). It is intended primarily for use by OEMs and network providers
222      * such as mobile network operators. In addition to generally excluding information that isn't
223      * targeted to connectivity debugging, this type of bugreport excludes PII and sensitive
224      * information that isn't strictly necessary for connectivity debugging.
225      *
226      * <p>The calling app MUST have a context-specific reason for requesting a connectivity
227      * bugreport, such as detecting a connectivity-related issue. This API SHALL NOT be used to
228      * perform random sampling from a fleet of public end-user devices.
229      *
230      * <p>Calling this API will cause the system to ask the user for consent every single time. The
231      * bugreport artifacts will be copied over to the given file descriptors only if the user
232      * consents to sharing with the calling app.
233      *
234      * <p>This starts a bugreport in the background. However the call itself can take several
235      * seconds to return in the worst case. {@code callback} will receive progress and status
236      * updates.
237      *
238      * <p>Requires that the calling app has carrier privileges (see {@link
239      * android.telephony.TelephonyManager#hasCarrierPrivileges}) on any active subscription.
240      *
241      * @param bugreportFd file to write the bugreport. This should be opened in write-only, append
242      *     mode.
243      * @param callback callback for progress and status updates.
244      */
245     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
246     @WorkerThread
startConnectivityBugreport( @onNull ParcelFileDescriptor bugreportFd, @NonNull @CallbackExecutor Executor executor, @NonNull BugreportCallback callback)247     public void startConnectivityBugreport(
248             @NonNull ParcelFileDescriptor bugreportFd,
249             @NonNull @CallbackExecutor Executor executor,
250             @NonNull BugreportCallback callback) {
251         startBugreport(
252                 bugreportFd,
253                 null /* screenshotFd */,
254                 new BugreportParams(BugreportParams.BUGREPORT_MODE_TELEPHONY),
255                 executor,
256                 callback);
257     }
258 
259     /**
260      * Cancels the currently running bugreport.
261      *
262      * <p>Apps are only able to cancel their own bugreports. App A cannot cancel a bugreport started
263      * by app B.
264      *
265      * <p>Requires permission: {@link android.Manifest.permission#DUMP} or that the calling app has
266      * carrier privileges (see {@link android.telephony.TelephonyManager#hasCarrierPrivileges}) on
267      * any active subscription.
268      *
269      * @throws SecurityException if trying to cancel another app's bugreport in progress
270      */
271     @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
272     @WorkerThread
cancelBugreport()273     public void cancelBugreport() {
274         try {
275             mBinder.cancelBugreport(-1 /* callingUid */, mContext.getOpPackageName());
276         } catch (RemoteException e) {
277             throw e.rethrowFromSystemServer();
278         }
279     }
280 
281     /**
282      * Requests a bugreport.
283      *
284      * <p>This requests the platform/system to take a bugreport and makes the final bugreport
285      * available to the user. The user may choose to share it with another app, but the bugreport is
286      * never given back directly to the app that requested it.
287      *
288      * @param params {@link BugreportParams} that specify what kind of a bugreport should be taken,
289      *     please note that not all kinds of bugreport allow for a progress notification
290      * @param shareTitle title on the final share notification
291      * @param shareDescription description on the final share notification
292      * @hide
293      */
294     @SystemApi
295     @RequiresPermission(android.Manifest.permission.DUMP)
requestBugreport( @onNull BugreportParams params, @Nullable CharSequence shareTitle, @Nullable CharSequence shareDescription)296     public void requestBugreport(
297             @NonNull BugreportParams params,
298             @Nullable CharSequence shareTitle,
299             @Nullable CharSequence shareDescription) {
300         try {
301             String title = shareTitle == null ? null : shareTitle.toString();
302             String description = shareDescription == null ? null : shareDescription.toString();
303             ActivityManager.getService()
304                     .requestBugReportWithDescription(title, description, params.getMode());
305         } catch (RemoteException e) {
306             throw e.rethrowFromSystemServer();
307         }
308     }
309 
310     private final class DumpstateListener extends IDumpstateListener.Stub {
311         private final Executor mExecutor;
312         private final BugreportCallback mCallback;
313         private final boolean mIsScreenshotRequested;
314 
DumpstateListener( Executor executor, BugreportCallback callback, boolean isScreenshotRequested)315         DumpstateListener(
316                 Executor executor, BugreportCallback callback, boolean isScreenshotRequested) {
317             mExecutor = executor;
318             mCallback = callback;
319             mIsScreenshotRequested = isScreenshotRequested;
320         }
321 
322         @Override
onProgress(int progress)323         public void onProgress(int progress) throws RemoteException {
324             final long identity = Binder.clearCallingIdentity();
325             try {
326                 mExecutor.execute(() -> mCallback.onProgress(progress));
327             } finally {
328                 Binder.restoreCallingIdentity(identity);
329             }
330         }
331 
332         @Override
onError(int errorCode)333         public void onError(int errorCode) throws RemoteException {
334             final long identity = Binder.clearCallingIdentity();
335             try {
336                 mExecutor.execute(() -> mCallback.onError(errorCode));
337             } finally {
338                 Binder.restoreCallingIdentity(identity);
339             }
340         }
341 
342         @Override
onFinished()343         public void onFinished() throws RemoteException {
344             final long identity = Binder.clearCallingIdentity();
345             try {
346                 mExecutor.execute(() -> mCallback.onFinished());
347             } finally {
348                 Binder.restoreCallingIdentity(identity);
349             }
350         }
351 
352         @Override
onScreenshotTaken(boolean success)353         public void onScreenshotTaken(boolean success) throws RemoteException {
354             if (!mIsScreenshotRequested) {
355                 return;
356             }
357 
358             Handler mainThreadHandler = new Handler(Looper.getMainLooper());
359             mainThreadHandler.post(
360                     () -> {
361                         int message =
362                                 success
363                                         ? R.string.bugreport_screenshot_success_toast
364                                         : R.string.bugreport_screenshot_failure_toast;
365                         Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
366                     });
367         }
368 
369         @Override
onUiIntensiveBugreportDumpsFinished()370         public void onUiIntensiveBugreportDumpsFinished() throws RemoteException {
371             final long identity = Binder.clearCallingIdentity();
372             try {
373                 mExecutor.execute(() -> mCallback.onEarlyReportFinished());
374             } finally {
375                 Binder.restoreCallingIdentity(identity);
376             }
377         }
378     }
379 }
380