• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 com.android.shell;
18 
19 import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
20 import static com.android.shell.BugreportPrefs.STATE_HIDE;
21 import static com.android.shell.BugreportPrefs.STATE_UNKNOWN;
22 import static com.android.shell.BugreportPrefs.getWarningState;
23 
24 import java.io.BufferedOutputStream;
25 import java.io.ByteArrayInputStream;
26 import java.io.File;
27 import java.io.FileDescriptor;
28 import java.io.FileInputStream;
29 import java.io.FileOutputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.PrintWriter;
33 import java.nio.charset.StandardCharsets;
34 import java.text.NumberFormat;
35 import java.util.ArrayList;
36 import java.util.Enumeration;
37 import java.util.List;
38 import java.util.zip.ZipEntry;
39 import java.util.zip.ZipFile;
40 import java.util.zip.ZipOutputStream;
41 
42 import libcore.io.Streams;
43 
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.logging.MetricsLogger;
46 import com.android.internal.logging.MetricsProto.MetricsEvent;
47 import com.google.android.collect.Lists;
48 
49 import android.accounts.Account;
50 import android.accounts.AccountManager;
51 import android.annotation.SuppressLint;
52 import android.app.AlertDialog;
53 import android.app.Notification;
54 import android.app.Notification.Action;
55 import android.app.NotificationManager;
56 import android.app.PendingIntent;
57 import android.app.Service;
58 import android.content.ClipData;
59 import android.content.Context;
60 import android.content.DialogInterface;
61 import android.content.Intent;
62 import android.content.res.Configuration;
63 import android.graphics.Bitmap;
64 import android.hardware.display.DisplayManagerGlobal;
65 import android.net.Uri;
66 import android.os.AsyncTask;
67 import android.os.Bundle;
68 import android.os.Handler;
69 import android.os.HandlerThread;
70 import android.os.IBinder;
71 import android.os.Looper;
72 import android.os.Message;
73 import android.os.Parcel;
74 import android.os.Parcelable;
75 import android.os.SystemProperties;
76 import android.os.Vibrator;
77 import android.support.v4.content.FileProvider;
78 import android.text.TextUtils;
79 import android.text.format.DateUtils;
80 import android.util.Log;
81 import android.util.Patterns;
82 import android.util.SparseArray;
83 import android.view.Display;
84 import android.view.KeyEvent;
85 import android.view.View;
86 import android.view.WindowManager;
87 import android.view.View.OnFocusChangeListener;
88 import android.view.inputmethod.EditorInfo;
89 import android.widget.Button;
90 import android.widget.EditText;
91 import android.widget.Toast;
92 
93 /**
94  * Service used to keep progress of bugreport processes ({@code dumpstate}).
95  * <p>
96  * The workflow is:
97  * <ol>
98  * <li>When {@code dumpstate} starts, it sends a {@code BUGREPORT_STARTED} with a sequential id,
99  * its pid, and the estimated total effort.
100  * <li>{@link BugreportReceiver} receives the intent and delegates it to this service.
101  * <li>Upon start, this service:
102  * <ol>
103  * <li>Issues a system notification so user can watch the progresss (which is 0% initially).
104  * <li>Polls the {@link SystemProperties} for updates on the {@code dumpstate} progress.
105  * <li>If the progress changed, it updates the system notification.
106  * </ol>
107  * <li>As {@code dumpstate} progresses, it updates the system property.
108  * <li>When {@code dumpstate} finishes, it sends a {@code BUGREPORT_FINISHED} intent.
109  * <li>{@link BugreportReceiver} receives the intent and delegates it to this service, which in
110  * turn:
111  * <ol>
112  * <li>Updates the system notification so user can share the bugreport.
113  * <li>Stops monitoring that {@code dumpstate} process.
114  * <li>Stops itself if it doesn't have any process left to monitor.
115  * </ol>
116  * </ol>
117  */
118 public class BugreportProgressService extends Service {
119     private static final String TAG = "BugreportProgressService";
120     private static final boolean DEBUG = false;
121 
122     private static final String AUTHORITY = "com.android.shell";
123 
124     // External intents sent by dumpstate.
125     static final String INTENT_BUGREPORT_STARTED = "android.intent.action.BUGREPORT_STARTED";
126     static final String INTENT_BUGREPORT_FINISHED = "android.intent.action.BUGREPORT_FINISHED";
127     static final String INTENT_REMOTE_BUGREPORT_FINISHED =
128             "android.intent.action.REMOTE_BUGREPORT_FINISHED";
129 
130     // Internal intents used on notification actions.
131     static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL";
132     static final String INTENT_BUGREPORT_SHARE = "android.intent.action.BUGREPORT_SHARE";
133     static final String INTENT_BUGREPORT_INFO_LAUNCH =
134             "android.intent.action.BUGREPORT_INFO_LAUNCH";
135     static final String INTENT_BUGREPORT_SCREENSHOT =
136             "android.intent.action.BUGREPORT_SCREENSHOT";
137 
138     static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
139     static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
140     static final String EXTRA_ID = "android.intent.extra.ID";
141     static final String EXTRA_PID = "android.intent.extra.PID";
142     static final String EXTRA_MAX = "android.intent.extra.MAX";
143     static final String EXTRA_NAME = "android.intent.extra.NAME";
144     static final String EXTRA_TITLE = "android.intent.extra.TITLE";
145     static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION";
146     static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT";
147     static final String EXTRA_INFO = "android.intent.extra.INFO";
148 
149     private static final int MSG_SERVICE_COMMAND = 1;
150     private static final int MSG_POLL = 2;
151     private static final int MSG_DELAYED_SCREENSHOT = 3;
152     private static final int MSG_SCREENSHOT_REQUEST = 4;
153     private static final int MSG_SCREENSHOT_RESPONSE = 5;
154 
155     // Passed to Message.obtain() when msg.arg2 is not used.
156     private static final int UNUSED_ARG2 = -2;
157 
158     // Maximum progress displayed (like 99.00%).
159     private static final int CAPPED_PROGRESS = 9900;
160     private static final int CAPPED_MAX = 10000;
161 
162     /**
163      * Delay before a screenshot is taken.
164      * <p>
165      * Should be at least 3 seconds, otherwise its toast might show up in the screenshot.
166      */
167     static final int SCREENSHOT_DELAY_SECONDS = 3;
168 
169     /** Polling frequency, in milliseconds. */
170     static final long POLLING_FREQUENCY = 2 * DateUtils.SECOND_IN_MILLIS;
171 
172     /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */
173     private static final long INACTIVITY_TIMEOUT = 10 * DateUtils.MINUTE_IN_MILLIS;
174 
175     /** System properties used for monitoring progress. */
176     private static final String DUMPSTATE_PREFIX = "dumpstate.";
177     private static final String PROGRESS_SUFFIX = ".progress";
178     private static final String MAX_SUFFIX = ".max";
179     private static final String NAME_SUFFIX = ".name";
180 
181     /** System property (and value) used to stop dumpstate. */
182     // TODO: should call ActiveManager API instead
183     private static final String CTL_STOP = "ctl.stop";
184     private static final String BUGREPORT_SERVICE = "bugreportplus";
185 
186     /**
187      * Directory on Shell's data storage where screenshots will be stored.
188      * <p>
189      * Must be a path supported by its FileProvider.
190      */
191     private static final String SCREENSHOT_DIR = "bugreports";
192 
193     /** Managed dumpstate processes (keyed by id) */
194     private final SparseArray<BugreportInfo> mProcesses = new SparseArray<>();
195 
196     private Context mContext;
197     private ServiceHandler mMainHandler;
198     private ScreenshotHandler mScreenshotHandler;
199 
200     private final BugreportInfoDialog mInfoDialog = new BugreportInfoDialog();
201 
202     private File mScreenshotsDir;
203 
204     /**
205      * id of the notification used to set service on foreground.
206      */
207     private int mForegroundId = -1;
208 
209     /**
210      * Flag indicating whether a screenshot is being taken.
211      * <p>
212      * This is the only state that is shared between the 2 handlers and hence must have synchronized
213      * access.
214      */
215     private boolean mTakingScreenshot;
216 
217     private static final Bundle sNotificationBundle = new Bundle();
218 
219     private boolean mIsWatch;
220 
221     @Override
onCreate()222     public void onCreate() {
223         mContext = getApplicationContext();
224         mMainHandler = new ServiceHandler("BugreportProgressServiceMainThread");
225         mScreenshotHandler = new ScreenshotHandler("BugreportProgressServiceScreenshotThread");
226 
227         mScreenshotsDir = new File(getFilesDir(), SCREENSHOT_DIR);
228         if (!mScreenshotsDir.exists()) {
229             Log.i(TAG, "Creating directory " + mScreenshotsDir + " to store temporary screenshots");
230             if (!mScreenshotsDir.mkdir()) {
231                 Log.w(TAG, "Could not create directory " + mScreenshotsDir);
232             }
233         }
234         final Configuration conf = mContext.getResources().getConfiguration();
235         mIsWatch = (conf.uiMode & Configuration.UI_MODE_TYPE_MASK) ==
236                 Configuration.UI_MODE_TYPE_WATCH;
237     }
238 
239     @Override
onStartCommand(Intent intent, int flags, int startId)240     public int onStartCommand(Intent intent, int flags, int startId) {
241         Log.v(TAG, "onStartCommand(): " + dumpIntent(intent));
242         if (intent != null) {
243             // Handle it in a separate thread.
244             final Message msg = mMainHandler.obtainMessage();
245             msg.what = MSG_SERVICE_COMMAND;
246             msg.obj = intent;
247             mMainHandler.sendMessage(msg);
248         }
249 
250         // If service is killed it cannot be recreated because it would not know which
251         // dumpstate IDs it would have to watch.
252         return START_NOT_STICKY;
253     }
254 
255     @Override
onBind(Intent intent)256     public IBinder onBind(Intent intent) {
257         return null;
258     }
259 
260     @Override
onDestroy()261     public void onDestroy() {
262         mMainHandler.getLooper().quit();
263         mScreenshotHandler.getLooper().quit();
264         super.onDestroy();
265     }
266 
267     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)268     protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
269         final int size = mProcesses.size();
270         if (size == 0) {
271             writer.printf("No monitored processes");
272             return;
273         }
274         writer.printf("Foreground id: %d\n\n", mForegroundId);
275         writer.printf("Monitored dumpstate processes\n");
276         writer.printf("-----------------------------\n");
277         for (int i = 0; i < size; i++) {
278             writer.printf("%s\n", mProcesses.valueAt(i));
279         }
280     }
281 
282     /**
283      * Main thread used to handle all requests but taking screenshots.
284      */
285     private final class ServiceHandler extends Handler {
ServiceHandler(String name)286         public ServiceHandler(String name) {
287             super(newLooper(name));
288         }
289 
290         @Override
handleMessage(Message msg)291         public void handleMessage(Message msg) {
292             if (msg.what == MSG_POLL) {
293                 poll();
294                 return;
295             }
296 
297             if (msg.what == MSG_DELAYED_SCREENSHOT) {
298                 takeScreenshot(msg.arg1, msg.arg2);
299                 return;
300             }
301 
302             if (msg.what == MSG_SCREENSHOT_RESPONSE) {
303                 handleScreenshotResponse(msg);
304                 return;
305             }
306 
307             if (msg.what != MSG_SERVICE_COMMAND) {
308                 // Sanity check.
309                 Log.e(TAG, "Invalid message type: " + msg.what);
310                 return;
311             }
312 
313             // At this point it's handling onStartCommand(), with the intent passed as an Extra.
314             if (!(msg.obj instanceof Intent)) {
315                 // Sanity check.
316                 Log.wtf(TAG, "handleMessage(): invalid msg.obj type: " + msg.obj);
317                 return;
318             }
319             final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT);
320             Log.v(TAG, "handleMessage(): " + dumpIntent((Intent) parcel));
321             final Intent intent;
322             if (parcel instanceof Intent) {
323                 // The real intent was passed to BugreportReceiver, which delegated to the service.
324                 intent = (Intent) parcel;
325             } else {
326                 intent = (Intent) msg.obj;
327             }
328             final String action = intent.getAction();
329             final int pid = intent.getIntExtra(EXTRA_PID, 0);
330             final int id = intent.getIntExtra(EXTRA_ID, 0);
331             final int max = intent.getIntExtra(EXTRA_MAX, -1);
332             final String name = intent.getStringExtra(EXTRA_NAME);
333 
334             if (DEBUG)
335                 Log.v(TAG, "action: " + action + ", name: " + name + ", id: " + id + ", pid: "
336                         + pid + ", max: " + max);
337             switch (action) {
338                 case INTENT_BUGREPORT_STARTED:
339                     if (!startProgress(name, id, pid, max)) {
340                         stopSelfWhenDone();
341                         return;
342                     }
343                     poll();
344                     break;
345                 case INTENT_BUGREPORT_FINISHED:
346                     if (id == 0) {
347                         // Shouldn't happen, unless BUGREPORT_FINISHED is received from a legacy,
348                         // out-of-sync dumpstate process.
349                         Log.w(TAG, "Missing " + EXTRA_ID + " on intent " + intent);
350                     }
351                     onBugreportFinished(id, intent);
352                     break;
353                 case INTENT_BUGREPORT_INFO_LAUNCH:
354                     launchBugreportInfoDialog(id);
355                     break;
356                 case INTENT_BUGREPORT_SCREENSHOT:
357                     takeScreenshot(id);
358                     break;
359                 case INTENT_BUGREPORT_SHARE:
360                     shareBugreport(id, (BugreportInfo) intent.getParcelableExtra(EXTRA_INFO));
361                     break;
362                 case INTENT_BUGREPORT_CANCEL:
363                     cancel(id);
364                     break;
365                 default:
366                     Log.w(TAG, "Unsupported intent: " + action);
367             }
368             return;
369 
370         }
371 
poll()372         private void poll() {
373             if (pollProgress()) {
374                 // Keep polling...
375                 sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY);
376             } else {
377                 Log.i(TAG, "Stopped polling");
378             }
379         }
380     }
381 
382     /**
383      * Separate thread used only to take screenshots so it doesn't block the main thread.
384      */
385     private final class ScreenshotHandler extends Handler {
ScreenshotHandler(String name)386         public ScreenshotHandler(String name) {
387             super(newLooper(name));
388         }
389 
390         @Override
handleMessage(Message msg)391         public void handleMessage(Message msg) {
392             if (msg.what != MSG_SCREENSHOT_REQUEST) {
393                 Log.e(TAG, "Invalid message type: " + msg.what);
394                 return;
395             }
396             handleScreenshotRequest(msg);
397         }
398     }
399 
getInfo(int id)400     private BugreportInfo getInfo(int id) {
401         final BugreportInfo info = mProcesses.get(id);
402         if (info == null) {
403             Log.w(TAG, "Not monitoring process with ID " + id);
404         }
405         return info;
406     }
407 
408     /**
409      * Creates the {@link BugreportInfo} for a process and issue a system notification to
410      * indicate its progress.
411      *
412      * @return whether it succeeded or not.
413      */
startProgress(String name, int id, int pid, int max)414     private boolean startProgress(String name, int id, int pid, int max) {
415         if (name == null) {
416             Log.w(TAG, "Missing " + EXTRA_NAME + " on start intent");
417         }
418         if (id == -1) {
419             Log.e(TAG, "Missing " + EXTRA_ID + " on start intent");
420             return false;
421         }
422         if (pid == -1) {
423             Log.e(TAG, "Missing " + EXTRA_PID + " on start intent");
424             return false;
425         }
426         if (max <= 0) {
427             Log.e(TAG, "Invalid value for extra " + EXTRA_MAX + ": " + max);
428             return false;
429         }
430 
431         final BugreportInfo info = new BugreportInfo(mContext, id, pid, name, max);
432         if (mProcesses.indexOfKey(id) >= 0) {
433             // BUGREPORT_STARTED intent was already received; ignore it.
434             Log.w(TAG, "ID " + id + " already watched");
435             return true;
436         }
437         mProcesses.put(info.id, info);
438         updateProgress(info);
439         return true;
440     }
441 
442     /**
443      * Updates the system notification for a given bugreport.
444      */
updateProgress(BugreportInfo info)445     private void updateProgress(BugreportInfo info) {
446         if (info.max <= 0 || info.progress < 0) {
447             Log.e(TAG, "Invalid progress values for " + info);
448             return;
449         }
450 
451         if (info.finished) {
452             Log.w(TAG, "Not sending progress notification because bugreport has finished already ("
453                     + info + ")");
454             return;
455         }
456 
457         final NumberFormat nf = NumberFormat.getPercentInstance();
458         nf.setMinimumFractionDigits(2);
459         nf.setMaximumFractionDigits(2);
460         final String percentageText = nf.format((double) info.progress / info.max);
461 
462         String title = mContext.getString(R.string.bugreport_in_progress_title, info.id);
463 
464         // TODO: Remove this workaround when notification progress is implemented on Wear.
465         if (mIsWatch) {
466             nf.setMinimumFractionDigits(0);
467             nf.setMaximumFractionDigits(0);
468             final String watchPercentageText = nf.format((double) info.progress / info.max);
469             title = title + "\n" + watchPercentageText;
470         }
471 
472         final String name =
473                 info.name != null ? info.name : mContext.getString(R.string.bugreport_unnamed);
474 
475         final Notification.Builder builder = newBaseNotification(mContext)
476                 .setContentTitle(title)
477                 .setTicker(title)
478                 .setContentText(name)
479                 .setProgress(info.max, info.progress, false)
480                 .setOngoing(true);
481 
482         // Wear bugreport doesn't need the bug info dialog, screenshot and cancel action.
483         if (!mIsWatch) {
484             final Action cancelAction = new Action.Builder(null, mContext.getString(
485                     com.android.internal.R.string.cancel), newCancelIntent(mContext, info)).build();
486             final Intent infoIntent = new Intent(mContext, BugreportProgressService.class);
487             infoIntent.setAction(INTENT_BUGREPORT_INFO_LAUNCH);
488             infoIntent.putExtra(EXTRA_ID, info.id);
489             final PendingIntent infoPendingIntent =
490                     PendingIntent.getService(mContext, info.id, infoIntent,
491                     PendingIntent.FLAG_UPDATE_CURRENT);
492             final Action infoAction = new Action.Builder(null,
493                     mContext.getString(R.string.bugreport_info_action),
494                     infoPendingIntent).build();
495             final Intent screenshotIntent = new Intent(mContext, BugreportProgressService.class);
496             screenshotIntent.setAction(INTENT_BUGREPORT_SCREENSHOT);
497             screenshotIntent.putExtra(EXTRA_ID, info.id);
498             PendingIntent screenshotPendingIntent = mTakingScreenshot ? null : PendingIntent
499                     .getService(mContext, info.id, screenshotIntent,
500                             PendingIntent.FLAG_UPDATE_CURRENT);
501             final Action screenshotAction = new Action.Builder(null,
502                     mContext.getString(R.string.bugreport_screenshot_action),
503                     screenshotPendingIntent).build();
504             builder.setContentIntent(infoPendingIntent)
505                 .setActions(infoAction, screenshotAction, cancelAction);
506         }
507 
508         if (DEBUG) {
509             Log.d(TAG, "Sending 'Progress' notification for id " + info.id + " (pid " + info.pid
510                     + "): " + percentageText);
511         }
512         sendForegroundabledNotification(info.id, builder.build());
513     }
514 
sendForegroundabledNotification(int id, Notification notification)515     private void sendForegroundabledNotification(int id, Notification notification) {
516         if (mForegroundId >= 0) {
517             if (DEBUG) Log.d(TAG, "Already running as foreground service");
518             NotificationManager.from(mContext).notify(id, notification);
519         } else {
520             mForegroundId = id;
521             Log.d(TAG, "Start running as foreground service on id " + mForegroundId);
522             startForeground(mForegroundId, notification);
523         }
524     }
525 
526     /**
527      * Creates a {@link PendingIntent} for a notification action used to cancel a bugreport.
528      */
newCancelIntent(Context context, BugreportInfo info)529     private static PendingIntent newCancelIntent(Context context, BugreportInfo info) {
530         final Intent intent = new Intent(INTENT_BUGREPORT_CANCEL);
531         intent.setClass(context, BugreportProgressService.class);
532         intent.putExtra(EXTRA_ID, info.id);
533         return PendingIntent.getService(context, info.id, intent,
534                 PendingIntent.FLAG_UPDATE_CURRENT);
535     }
536 
537     /**
538      * Finalizes the progress on a given bugreport and cancel its notification.
539      */
stopProgress(int id)540     private void stopProgress(int id) {
541         if (mProcesses.indexOfKey(id) < 0) {
542             Log.w(TAG, "ID not watched: " + id);
543         } else {
544             Log.d(TAG, "Removing ID " + id);
545             mProcesses.remove(id);
546         }
547         // Must stop foreground service first, otherwise notif.cancel() will fail below.
548         stopForegroundWhenDone(id);
549         Log.d(TAG, "stopProgress(" + id + "): cancel notification");
550         NotificationManager.from(mContext).cancel(id);
551         stopSelfWhenDone();
552     }
553 
554     /**
555      * Cancels a bugreport upon user's request.
556      */
cancel(int id)557     private void cancel(int id) {
558         MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_CANCEL);
559         Log.v(TAG, "cancel: ID=" + id);
560         final BugreportInfo info = getInfo(id);
561         if (info != null && !info.finished) {
562             Log.i(TAG, "Cancelling bugreport service (ID=" + id + ") on user's request");
563             setSystemProperty(CTL_STOP, BUGREPORT_SERVICE);
564             deleteScreenshots(info);
565         }
566         stopProgress(id);
567     }
568 
569     /**
570      * Poll {@link SystemProperties} to get the progress on each monitored process.
571      *
572      * @return whether it should keep polling.
573      */
pollProgress()574     private boolean pollProgress() {
575         final int total = mProcesses.size();
576         if (total == 0) {
577             Log.d(TAG, "No process to poll progress.");
578         }
579         int activeProcesses = 0;
580         for (int i = 0; i < total; i++) {
581             final BugreportInfo info = mProcesses.valueAt(i);
582             if (info == null) {
583                 Log.wtf(TAG, "pollProgress(): null info at index " + i + "(ID = "
584                         + mProcesses.keyAt(i) + ")");
585                 continue;
586             }
587 
588             final int pid = info.pid;
589             final int id = info.id;
590             if (info.finished) {
591                 if (DEBUG) Log.v(TAG, "Skipping finished process " + pid + " (id: " + id + ")");
592                 continue;
593             }
594             activeProcesses++;
595             final String progressKey = DUMPSTATE_PREFIX + pid + PROGRESS_SUFFIX;
596             info.realProgress = SystemProperties.getInt(progressKey, 0);
597             if (info.realProgress == 0) {
598                 Log.v(TAG, "System property " + progressKey + " is not set yet");
599             }
600             final String maxKey = DUMPSTATE_PREFIX + pid + MAX_SUFFIX;
601             info.realMax = SystemProperties.getInt(maxKey, info.max);
602             if (info.realMax <= 0 ) {
603                 Log.w(TAG, "Property " + maxKey + " is not positive: " + info.max);
604                 continue;
605             }
606             /*
607              * Checks whether the progress changed in a way that should be displayed to the user:
608              * - info.progress / info.max represents the displayed progress
609              * - info.realProgress / info.realMax represents the real progress
610              * - since the real progress can decrease, the displayed progress is only updated if it
611              *   increases
612              * - the displayed progress is capped at a maximum (like 99%)
613              */
614             final int oldPercentage = (CAPPED_MAX * info.progress) / info.max;
615             int newPercentage = (CAPPED_MAX * info.realProgress) / info.realMax;
616             int max = info.realMax;
617             int progress = info.realProgress;
618 
619             if (newPercentage > CAPPED_PROGRESS) {
620                 progress = newPercentage = CAPPED_PROGRESS;
621                 max = CAPPED_MAX;
622             }
623 
624             if (newPercentage > oldPercentage) {
625                 if (DEBUG) {
626                     if (progress != info.progress) {
627                         Log.v(TAG, "Updating progress for PID " + pid + "(id: " + id + ") from "
628                                 + info.progress + " to " + progress);
629                     }
630                     if (max != info.max) {
631                         Log.v(TAG, "Updating max progress for PID " + pid + "(id: " + id + ") from "
632                                 + info.max + " to " + max);
633                     }
634                 }
635                 info.progress = progress;
636                 info.max = max;
637                 info.lastUpdate = System.currentTimeMillis();
638                 updateProgress(info);
639             } else {
640                 long inactiveTime = System.currentTimeMillis() - info.lastUpdate;
641                 if (inactiveTime >= INACTIVITY_TIMEOUT) {
642                     Log.w(TAG, "No progress update for PID " + pid + " since "
643                             + info.getFormattedLastUpdate());
644                     stopProgress(info.id);
645                 }
646             }
647         }
648         if (DEBUG) Log.v(TAG, "pollProgress() total=" + total + ", actives=" + activeProcesses);
649         return activeProcesses > 0;
650     }
651 
652     /**
653      * Fetches a {@link BugreportInfo} for a given process and launches a dialog where the user can
654      * change its values.
655      */
launchBugreportInfoDialog(int id)656     private void launchBugreportInfoDialog(int id) {
657         MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_DETAILS);
658         // Copy values so it doesn't lock mProcesses while UI is being updated
659         final String name, title, description;
660         final BugreportInfo info = getInfo(id);
661         if (info == null) {
662             // Most likely am killed Shell before user tapped the notification. Since system might
663             // be too busy anwyays, it's better to ignore the notification and switch back to the
664             // non-interactive mode (where the bugerport will be shared upon completion).
665             Log.w(TAG, "launchBugreportInfoDialog(): canceling notification because id " + id
666                     + " was not found");
667             // TODO: add test case to make sure notification is canceled.
668             NotificationManager.from(mContext).cancel(id);
669             return;
670         }
671 
672         collapseNotificationBar();
673         mInfoDialog.initialize(mContext, info);
674     }
675 
676     /**
677      * Starting point for taking a screenshot.
678      * <p>
679      * It first display a toast message and waits {@link #SCREENSHOT_DELAY_SECONDS} seconds before
680      * taking the screenshot.
681      */
takeScreenshot(int id)682     private void takeScreenshot(int id) {
683         MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SCREENSHOT);
684         if (getInfo(id) == null) {
685             // Most likely am killed Shell before user tapped the notification. Since system might
686             // be too busy anwyays, it's better to ignore the notification and switch back to the
687             // non-interactive mode (where the bugerport will be shared upon completion).
688             Log.w(TAG, "takeScreenshot(): canceling notification because id " + id
689                     + " was not found");
690             // TODO: add test case to make sure notification is canceled.
691             NotificationManager.from(mContext).cancel(id);
692             return;
693         }
694         setTakingScreenshot(true);
695         collapseNotificationBar();
696         final String msg = mContext.getResources()
697                 .getQuantityString(com.android.internal.R.plurals.bugreport_countdown,
698                         SCREENSHOT_DELAY_SECONDS, SCREENSHOT_DELAY_SECONDS);
699         Log.i(TAG, msg);
700         // Show a toast just once, otherwise it might be captured in the screenshot.
701         Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
702 
703         takeScreenshot(id, SCREENSHOT_DELAY_SECONDS);
704     }
705 
706     /**
707      * Takes a screenshot after {@code delay} seconds.
708      */
takeScreenshot(int id, int delay)709     private void takeScreenshot(int id, int delay) {
710         if (delay > 0) {
711             Log.d(TAG, "Taking screenshot for " + id + " in " + delay + " seconds");
712             final Message msg = mMainHandler.obtainMessage();
713             msg.what = MSG_DELAYED_SCREENSHOT;
714             msg.arg1 = id;
715             msg.arg2 = delay - 1;
716             mMainHandler.sendMessageDelayed(msg, DateUtils.SECOND_IN_MILLIS);
717             return;
718         }
719 
720         // It's time to take the screenshot: let the proper thread handle it
721         final BugreportInfo info = getInfo(id);
722         if (info == null) {
723             return;
724         }
725         final String screenshotPath =
726                 new File(mScreenshotsDir, info.getPathNextScreenshot()).getAbsolutePath();
727 
728         Message.obtain(mScreenshotHandler, MSG_SCREENSHOT_REQUEST, id, UNUSED_ARG2, screenshotPath)
729                 .sendToTarget();
730     }
731 
732     /**
733      * Sets the internal {@code mTakingScreenshot} state and updates all notifications so their
734      * SCREENSHOT button is enabled or disabled accordingly.
735      */
setTakingScreenshot(boolean flag)736     private void setTakingScreenshot(boolean flag) {
737         synchronized (BugreportProgressService.this) {
738             mTakingScreenshot = flag;
739             for (int i = 0; i < mProcesses.size(); i++) {
740                 final BugreportInfo info = mProcesses.valueAt(i);
741                 if (info.finished) {
742                     Log.d(TAG, "Not updating progress for " + info.id + " while taking screenshot"
743                             + " because share notification was already sent");
744                     continue;
745                 }
746                 updateProgress(info);
747             }
748         }
749     }
750 
handleScreenshotRequest(Message requestMsg)751     private void handleScreenshotRequest(Message requestMsg) {
752         String screenshotFile = (String) requestMsg.obj;
753         boolean taken = takeScreenshot(mContext, screenshotFile);
754         setTakingScreenshot(false);
755 
756         Message.obtain(mMainHandler, MSG_SCREENSHOT_RESPONSE, requestMsg.arg1, taken ? 1 : 0,
757                 screenshotFile).sendToTarget();
758     }
759 
handleScreenshotResponse(Message resultMsg)760     private void handleScreenshotResponse(Message resultMsg) {
761         final boolean taken = resultMsg.arg2 != 0;
762         final BugreportInfo info = getInfo(resultMsg.arg1);
763         if (info == null) {
764             return;
765         }
766         final File screenshotFile = new File((String) resultMsg.obj);
767 
768         final String msg;
769         if (taken) {
770             info.addScreenshot(screenshotFile);
771             if (info.finished) {
772                 Log.d(TAG, "Screenshot finished after bugreport; updating share notification");
773                 info.renameScreenshots(mScreenshotsDir);
774                 sendBugreportNotification(info, mTakingScreenshot);
775             }
776             msg = mContext.getString(R.string.bugreport_screenshot_taken);
777         } else {
778             msg = mContext.getString(R.string.bugreport_screenshot_failed);
779             Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
780         }
781         Log.d(TAG, msg);
782     }
783 
784     /**
785      * Deletes all screenshots taken for a given bugreport.
786      */
deleteScreenshots(BugreportInfo info)787     private void deleteScreenshots(BugreportInfo info) {
788         for (File file : info.screenshotFiles) {
789             Log.i(TAG, "Deleting screenshot file " + file);
790             file.delete();
791         }
792     }
793 
794     /**
795      * Stop running on foreground once there is no more active bugreports being watched.
796      */
stopForegroundWhenDone(int id)797     private void stopForegroundWhenDone(int id) {
798         if (id != mForegroundId) {
799             Log.d(TAG, "stopForegroundWhenDone(" + id + "): ignoring since foreground id is "
800                     + mForegroundId);
801             return;
802         }
803 
804         Log.d(TAG, "detaching foreground from id " + mForegroundId);
805         stopForeground(Service.STOP_FOREGROUND_DETACH);
806         mForegroundId = -1;
807 
808         // Might need to restart foreground using a new notification id.
809         final int total = mProcesses.size();
810         if (total > 0) {
811             for (int i = 0; i < total; i++) {
812                 final BugreportInfo info = mProcesses.valueAt(i);
813                 if (!info.finished) {
814                     updateProgress(info);
815                     break;
816                 }
817             }
818         }
819     }
820 
821     /**
822      * Finishes the service when it's not monitoring any more processes.
823      */
stopSelfWhenDone()824     private void stopSelfWhenDone() {
825         if (mProcesses.size() > 0) {
826             if (DEBUG) Log.d(TAG, "Staying alive, waiting for IDs " + mProcesses);
827             return;
828         }
829         Log.v(TAG, "No more processes to handle, shutting down");
830         stopSelf();
831     }
832 
833     /**
834      * Handles the BUGREPORT_FINISHED intent sent by {@code dumpstate}.
835      */
onBugreportFinished(int id, Intent intent)836     private void onBugreportFinished(int id, Intent intent) {
837         final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT);
838         // Since BugreportProvider and BugreportProgressService aren't tightly coupled,
839         // we need to make sure they are explicitly tied to a single unique notification URI
840         // so that the service can alert the provider of changes it has done (ie. new bug
841         // reports)
842         // See { @link Cursor#setNotificationUri } and {@link ContentResolver#notifyChanges }
843         final Uri notificationUri = BugreportStorageProvider.getNotificationUri();
844         mContext.getContentResolver().notifyChange(notificationUri, null, false);
845 
846         if (bugreportFile == null) {
847             // Should never happen, dumpstate always set the file.
848             Log.wtf(TAG, "Missing " + EXTRA_BUGREPORT + " on intent " + intent);
849             return;
850         }
851         mInfoDialog.onBugreportFinished(id);
852         BugreportInfo info = getInfo(id);
853         if (info == null) {
854             // Happens when BUGREPORT_FINISHED was received without a BUGREPORT_STARTED first.
855             Log.v(TAG, "Creating info for untracked ID " + id);
856             info = new BugreportInfo(mContext, id);
857             mProcesses.put(id, info);
858         }
859         info.renameScreenshots(mScreenshotsDir);
860         info.bugreportFile = bugreportFile;
861 
862         final int max = intent.getIntExtra(EXTRA_MAX, -1);
863         if (max != -1) {
864             MetricsLogger.histogram(this, "dumpstate_duration", max);
865             info.max = max;
866         }
867 
868         final File screenshot = getFileExtra(intent, EXTRA_SCREENSHOT);
869         if (screenshot != null) {
870             info.addScreenshot(screenshot);
871         }
872         info.finished = true;
873 
874         // Stop running on foreground, otherwise share notification cannot be dismissed.
875         stopForegroundWhenDone(id);
876 
877         triggerLocalNotification(mContext, info);
878     }
879 
880     /**
881      * Responsible for triggering a notification that allows the user to start a "share" intent with
882      * the bugreport. On watches we have other methods to allow the user to start this intent
883      * (usually by triggering it on another connected device); we don't need to display the
884      * notification in this case.
885      */
triggerLocalNotification(final Context context, final BugreportInfo info)886     private void triggerLocalNotification(final Context context, final BugreportInfo info) {
887         if (!info.bugreportFile.exists() || !info.bugreportFile.canRead()) {
888             Log.e(TAG, "Could not read bugreport file " + info.bugreportFile);
889             Toast.makeText(context, R.string.bugreport_unreadable_text, Toast.LENGTH_LONG).show();
890             stopProgress(info.id);
891             return;
892         }
893 
894         boolean isPlainText = info.bugreportFile.getName().toLowerCase().endsWith(".txt");
895         if (!isPlainText) {
896             // Already zipped, send it right away.
897             sendBugreportNotification(info, mTakingScreenshot);
898         } else {
899             // Asynchronously zip the file first, then send it.
900             sendZippedBugreportNotification(info, mTakingScreenshot);
901         }
902     }
903 
buildWarningIntent(Context context, Intent sendIntent)904     private static Intent buildWarningIntent(Context context, Intent sendIntent) {
905         final Intent intent = new Intent(context, BugreportWarningActivity.class);
906         intent.putExtra(Intent.EXTRA_INTENT, sendIntent);
907         return intent;
908     }
909 
910     /**
911      * Build {@link Intent} that can be used to share the given bugreport.
912      */
buildSendIntent(Context context, BugreportInfo info)913     private static Intent buildSendIntent(Context context, BugreportInfo info) {
914         // Files are kept on private storage, so turn into Uris that we can
915         // grant temporary permissions for.
916         final Uri bugreportUri;
917         try {
918             bugreportUri = getUri(context, info.bugreportFile);
919         } catch (IllegalArgumentException e) {
920             // Should not happen on production, but happens when a Shell is sideloaded and
921             // FileProvider cannot find a configured root for it.
922             Log.wtf(TAG, "Could not get URI for " + info.bugreportFile, e);
923             return null;
924         }
925 
926         final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
927         final String mimeType = "application/vnd.android.bugreport";
928         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
929         intent.addCategory(Intent.CATEGORY_DEFAULT);
930         intent.setType(mimeType);
931 
932         final String subject = !TextUtils.isEmpty(info.title) ?
933                 info.title : bugreportUri.getLastPathSegment();
934         intent.putExtra(Intent.EXTRA_SUBJECT, subject);
935 
936         // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String.
937         // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually
938         // create the ClipData object with the attachments URIs.
939         final StringBuilder messageBody = new StringBuilder("Build info: ")
940             .append(SystemProperties.get("ro.build.description"))
941             .append("\nSerial number: ")
942             .append(SystemProperties.get("ro.serialno"));
943         if (!TextUtils.isEmpty(info.description)) {
944             messageBody.append("\nDescription: ").append(info.description);
945         }
946         intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString());
947         final ClipData clipData = new ClipData(null, new String[] { mimeType },
948                 new ClipData.Item(null, null, null, bugreportUri));
949         final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri);
950         for (File screenshot : info.screenshotFiles) {
951             final Uri screenshotUri = getUri(context, screenshot);
952             clipData.addItem(new ClipData.Item(null, null, null, screenshotUri));
953             attachments.add(screenshotUri);
954         }
955         intent.setClipData(clipData);
956         intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
957 
958         final Account sendToAccount = findSendToAccount(context);
959         if (sendToAccount != null) {
960             intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name });
961         }
962 
963         return intent;
964     }
965 
966     /**
967      * Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE}
968      * intent, but issuing a warning dialog the first time.
969      */
shareBugreport(int id, BugreportInfo sharedInfo)970     private void shareBugreport(int id, BugreportInfo sharedInfo) {
971         MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SHARE);
972         BugreportInfo info = getInfo(id);
973         if (info == null) {
974             // Service was terminated but notification persisted
975             info = sharedInfo;
976             Log.d(TAG, "shareBugreport(): no info for ID " + id + " on managed processes ("
977                     + mProcesses + "), using info from intent instead (" + info + ")");
978         } else {
979             Log.v(TAG, "shareBugReport(): id " + id + " info = " + info);
980         }
981 
982         addDetailsToZipFile(info);
983 
984         final Intent sendIntent = buildSendIntent(mContext, info);
985         if (sendIntent == null) {
986             Log.w(TAG, "Stopping progres on ID " + id + " because share intent could not be built");
987             stopProgress(id);
988             return;
989         }
990 
991         final Intent notifIntent;
992 
993         // Send through warning dialog by default
994         if (getWarningState(mContext, STATE_UNKNOWN) != STATE_HIDE) {
995             notifIntent = buildWarningIntent(mContext, sendIntent);
996         } else {
997             notifIntent = sendIntent;
998         }
999         notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1000 
1001         // Send the share intent...
1002         mContext.startActivity(notifIntent);
1003 
1004         // ... and stop watching this process.
1005         stopProgress(id);
1006     }
1007 
1008     /**
1009      * Sends a notification indicating the bugreport has finished so use can share it.
1010      */
sendBugreportNotification(BugreportInfo info, boolean takingScreenshot)1011     private void sendBugreportNotification(BugreportInfo info, boolean takingScreenshot) {
1012 
1013         // Since adding the details can take a while, do it before notifying user.
1014         addDetailsToZipFile(info);
1015 
1016         final Intent shareIntent = new Intent(INTENT_BUGREPORT_SHARE);
1017         shareIntent.setClass(mContext, BugreportProgressService.class);
1018         shareIntent.setAction(INTENT_BUGREPORT_SHARE);
1019         shareIntent.putExtra(EXTRA_ID, info.id);
1020         shareIntent.putExtra(EXTRA_INFO, info);
1021 
1022         final String title = mContext.getString(R.string.bugreport_finished_title, info.id);
1023         final String content = takingScreenshot ?
1024                 mContext.getString(R.string.bugreport_finished_pending_screenshot_text)
1025                 : mContext.getString(R.string.bugreport_finished_text);
1026         final Notification.Builder builder = newBaseNotification(mContext)
1027                 .setContentTitle(title)
1028                 .setTicker(title)
1029                 .setContentText(content)
1030                 .setContentIntent(PendingIntent.getService(mContext, info.id, shareIntent,
1031                         PendingIntent.FLAG_UPDATE_CURRENT))
1032                 .setDeleteIntent(newCancelIntent(mContext, info));
1033 
1034         if (!TextUtils.isEmpty(info.name)) {
1035             builder.setSubText(info.name);
1036         }
1037 
1038         Log.v(TAG, "Sending 'Share' notification for ID " + info.id + ": " + title);
1039         NotificationManager.from(mContext).notify(info.id, builder.build());
1040     }
1041 
1042     /**
1043      * Sends a notification indicating the bugreport is being updated so the user can wait until it
1044      * finishes - at this point there is nothing to be done other than waiting, hence it has no
1045      * pending action.
1046      */
sendBugreportBeingUpdatedNotification(Context context, int id)1047     private void sendBugreportBeingUpdatedNotification(Context context, int id) {
1048         final String title = context.getString(R.string.bugreport_updating_title);
1049         final Notification.Builder builder = newBaseNotification(context)
1050                 .setContentTitle(title)
1051                 .setTicker(title)
1052                 .setContentText(context.getString(R.string.bugreport_updating_wait));
1053         Log.v(TAG, "Sending 'Updating zip' notification for ID " + id + ": " + title);
1054         sendForegroundabledNotification(id, builder.build());
1055     }
1056 
newBaseNotification(Context context)1057     private static Notification.Builder newBaseNotification(Context context) {
1058         if (sNotificationBundle.isEmpty()) {
1059             // Rename notifcations from "Shell" to "Android System"
1060             sNotificationBundle.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
1061                     context.getString(com.android.internal.R.string.android_system_label));
1062         }
1063         return new Notification.Builder(context)
1064                 .addExtras(sNotificationBundle)
1065                 .setCategory(Notification.CATEGORY_SYSTEM)
1066                 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
1067                 .setLocalOnly(true)
1068                 .setColor(context.getColor(
1069                         com.android.internal.R.color.system_notification_accent_color));
1070     }
1071 
1072     /**
1073      * Sends a zipped bugreport notification.
1074      */
sendZippedBugreportNotification( final BugreportInfo info, final boolean takingScreenshot)1075     private void sendZippedBugreportNotification( final BugreportInfo info,
1076             final boolean takingScreenshot) {
1077         new AsyncTask<Void, Void, Void>() {
1078             @Override
1079             protected Void doInBackground(Void... params) {
1080                 zipBugreport(info);
1081                 sendBugreportNotification(info, takingScreenshot);
1082                 return null;
1083             }
1084         }.execute();
1085     }
1086 
1087     /**
1088      * Zips a bugreport file, returning the path to the new file (or to the
1089      * original in case of failure).
1090      */
zipBugreport(BugreportInfo info)1091     private static void zipBugreport(BugreportInfo info) {
1092         final String bugreportPath = info.bugreportFile.getAbsolutePath();
1093         final String zippedPath = bugreportPath.replace(".txt", ".zip");
1094         Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath);
1095         final File bugreportZippedFile = new File(zippedPath);
1096         try (InputStream is = new FileInputStream(info.bugreportFile);
1097                 ZipOutputStream zos = new ZipOutputStream(
1098                         new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) {
1099             addEntry(zos, info.bugreportFile.getName(), is);
1100             // Delete old file
1101             final boolean deleted = info.bugreportFile.delete();
1102             if (deleted) {
1103                 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")");
1104             } else {
1105                 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")");
1106             }
1107             info.bugreportFile = bugreportZippedFile;
1108         } catch (IOException e) {
1109             Log.e(TAG, "exception zipping file " + zippedPath, e);
1110         }
1111     }
1112 
1113     /**
1114      * Adds the user-provided info into the bugreport zip file.
1115      * <p>
1116      * If user provided a title, it will be saved into a {@code title.txt} entry; similarly, the
1117      * description will be saved on {@code description.txt}.
1118      */
addDetailsToZipFile(BugreportInfo info)1119     private void addDetailsToZipFile(BugreportInfo info) {
1120         if (info.bugreportFile == null) {
1121             // One possible reason is a bug in the Parcelization code.
1122             Log.wtf(TAG, "addDetailsToZipFile(): no bugreportFile on " + info);
1123             return;
1124         }
1125         if (TextUtils.isEmpty(info.title) && TextUtils.isEmpty(info.description)) {
1126             Log.d(TAG, "Not touching zip file since neither title nor description are set");
1127             return;
1128         }
1129         if (info.addedDetailsToZip || info.addingDetailsToZip) {
1130             Log.d(TAG, "Already added details to zip file for " + info);
1131             return;
1132         }
1133         info.addingDetailsToZip = true;
1134 
1135         // It's not possible to add a new entry into an existing file, so we need to create a new
1136         // zip, copy all entries, then rename it.
1137         sendBugreportBeingUpdatedNotification(mContext, info.id); // ...and that takes time
1138 
1139         final File dir = info.bugreportFile.getParentFile();
1140         final File tmpZip = new File(dir, "tmp-" + info.bugreportFile.getName());
1141         Log.d(TAG, "Writing temporary zip file (" + tmpZip + ") with title and/or description");
1142         try (ZipFile oldZip = new ZipFile(info.bugreportFile);
1143                 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip))) {
1144 
1145             // First copy contents from original zip.
1146             Enumeration<? extends ZipEntry> entries = oldZip.entries();
1147             while (entries.hasMoreElements()) {
1148                 final ZipEntry entry = entries.nextElement();
1149                 final String entryName = entry.getName();
1150                 if (!entry.isDirectory()) {
1151                     addEntry(zos, entryName, entry.getTime(), oldZip.getInputStream(entry));
1152                 } else {
1153                     Log.w(TAG, "skipping directory entry: " + entryName);
1154                 }
1155             }
1156 
1157             // Then add the user-provided info.
1158             addEntry(zos, "title.txt", info.title);
1159             addEntry(zos, "description.txt", info.description);
1160         } catch (IOException e) {
1161             Log.e(TAG, "exception zipping file " + tmpZip, e);
1162             Toast.makeText(mContext, R.string.bugreport_add_details_to_zip_failed,
1163                     Toast.LENGTH_LONG).show();
1164             return;
1165         } finally {
1166             // Make sure it only tries to add details once, even it fails the first time.
1167             info.addedDetailsToZip = true;
1168             info.addingDetailsToZip = false;
1169             stopForegroundWhenDone(info.id);
1170         }
1171 
1172         if (!tmpZip.renameTo(info.bugreportFile)) {
1173             Log.e(TAG, "Could not rename " + tmpZip + " to " + info.bugreportFile);
1174         }
1175     }
1176 
addEntry(ZipOutputStream zos, String entry, String text)1177     private static void addEntry(ZipOutputStream zos, String entry, String text)
1178             throws IOException {
1179         if (DEBUG) Log.v(TAG, "adding entry '" + entry + "': " + text);
1180         if (!TextUtils.isEmpty(text)) {
1181             addEntry(zos, entry, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)));
1182         }
1183     }
1184 
addEntry(ZipOutputStream zos, String entryName, InputStream is)1185     private static void addEntry(ZipOutputStream zos, String entryName, InputStream is)
1186             throws IOException {
1187         addEntry(zos, entryName, System.currentTimeMillis(), is);
1188     }
1189 
addEntry(ZipOutputStream zos, String entryName, long timestamp, InputStream is)1190     private static void addEntry(ZipOutputStream zos, String entryName, long timestamp,
1191             InputStream is) throws IOException {
1192         final ZipEntry entry = new ZipEntry(entryName);
1193         entry.setTime(timestamp);
1194         zos.putNextEntry(entry);
1195         final int totalBytes = Streams.copy(is, zos);
1196         if (DEBUG) Log.v(TAG, "size of '" + entryName + "' entry: " + totalBytes + " bytes");
1197         zos.closeEntry();
1198     }
1199 
1200     /**
1201      * Find the best matching {@link Account} based on build properties.
1202      */
findSendToAccount(Context context)1203     private static Account findSendToAccount(Context context) {
1204         final AccountManager am = (AccountManager) context.getSystemService(
1205                 Context.ACCOUNT_SERVICE);
1206 
1207         String preferredDomain = SystemProperties.get("sendbug.preferred.domain");
1208         if (!preferredDomain.startsWith("@")) {
1209             preferredDomain = "@" + preferredDomain;
1210         }
1211 
1212         final Account[] accounts;
1213         try {
1214             accounts = am.getAccounts();
1215         } catch (RuntimeException e) {
1216             Log.e(TAG, "Could not get accounts for preferred domain " + preferredDomain, e);
1217             return null;
1218         }
1219         if (DEBUG) Log.d(TAG, "Number of accounts: " + accounts.length);
1220         Account foundAccount = null;
1221         for (Account account : accounts) {
1222             if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
1223                 if (!preferredDomain.isEmpty()) {
1224                     // if we have a preferred domain and it matches, return; otherwise keep
1225                     // looking
1226                     if (account.name.endsWith(preferredDomain)) {
1227                         return account;
1228                     } else {
1229                         foundAccount = account;
1230                     }
1231                     // if we don't have a preferred domain, just return since it looks like
1232                     // an email address
1233                 } else {
1234                     return account;
1235                 }
1236             }
1237         }
1238         return foundAccount;
1239     }
1240 
getUri(Context context, File file)1241     static Uri getUri(Context context, File file) {
1242         return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null;
1243     }
1244 
getFileExtra(Intent intent, String key)1245     static File getFileExtra(Intent intent, String key) {
1246         final String path = intent.getStringExtra(key);
1247         if (path != null) {
1248             return new File(path);
1249         } else {
1250             return null;
1251         }
1252     }
1253 
1254     /**
1255      * Dumps an intent, extracting the relevant extras.
1256      */
dumpIntent(Intent intent)1257     static String dumpIntent(Intent intent) {
1258         if (intent == null) {
1259             return "NO INTENT";
1260         }
1261         String action = intent.getAction();
1262         if (action == null) {
1263             // Happens when BugreportReceiver calls startService...
1264             action = "no action";
1265         }
1266         final StringBuilder buffer = new StringBuilder(action).append(" extras: ");
1267         addExtra(buffer, intent, EXTRA_ID);
1268         addExtra(buffer, intent, EXTRA_PID);
1269         addExtra(buffer, intent, EXTRA_MAX);
1270         addExtra(buffer, intent, EXTRA_NAME);
1271         addExtra(buffer, intent, EXTRA_DESCRIPTION);
1272         addExtra(buffer, intent, EXTRA_BUGREPORT);
1273         addExtra(buffer, intent, EXTRA_SCREENSHOT);
1274         addExtra(buffer, intent, EXTRA_INFO);
1275 
1276         if (intent.hasExtra(EXTRA_ORIGINAL_INTENT)) {
1277             buffer.append(SHORT_EXTRA_ORIGINAL_INTENT).append(": ");
1278             final Intent originalIntent = intent.getParcelableExtra(EXTRA_ORIGINAL_INTENT);
1279             buffer.append(dumpIntent(originalIntent));
1280         } else {
1281             buffer.append("no ").append(SHORT_EXTRA_ORIGINAL_INTENT);
1282         }
1283 
1284         return buffer.toString();
1285     }
1286 
1287     private static final String SHORT_EXTRA_ORIGINAL_INTENT =
1288             EXTRA_ORIGINAL_INTENT.substring(EXTRA_ORIGINAL_INTENT.lastIndexOf('.') + 1);
1289 
addExtra(StringBuilder buffer, Intent intent, String name)1290     private static void addExtra(StringBuilder buffer, Intent intent, String name) {
1291         final String shortName = name.substring(name.lastIndexOf('.') + 1);
1292         if (intent.hasExtra(name)) {
1293             buffer.append(shortName).append('=').append(intent.getExtra(name));
1294         } else {
1295             buffer.append("no ").append(shortName);
1296         }
1297         buffer.append(", ");
1298     }
1299 
setSystemProperty(String key, String value)1300     private static boolean setSystemProperty(String key, String value) {
1301         try {
1302             if (DEBUG) Log.v(TAG, "Setting system property " + key + " to " + value);
1303             SystemProperties.set(key, value);
1304         } catch (IllegalArgumentException e) {
1305             Log.e(TAG, "Could not set property " + key + " to " + value, e);
1306             return false;
1307         }
1308         return true;
1309     }
1310 
1311     /**
1312      * Updates the system property used by {@code dumpstate} to rename the final bugreport files.
1313      */
setBugreportNameProperty(int pid, String name)1314     private boolean setBugreportNameProperty(int pid, String name) {
1315         Log.d(TAG, "Updating bugreport name to " + name);
1316         final String key = DUMPSTATE_PREFIX + pid + NAME_SUFFIX;
1317         return setSystemProperty(key, name);
1318     }
1319 
1320     /**
1321      * Updates the user-provided details of a bugreport.
1322      */
updateBugreportInfo(int id, String name, String title, String description)1323     private void updateBugreportInfo(int id, String name, String title, String description) {
1324         final BugreportInfo info = getInfo(id);
1325         if (info == null) {
1326             return;
1327         }
1328         if (title != null && !title.equals(info.title)) {
1329             MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_TITLE_CHANGED);
1330         }
1331         info.title = title;
1332         if (description != null && !description.equals(info.description)) {
1333             MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_DESCRIPTION_CHANGED);
1334         }
1335         info.description = description;
1336         if (name != null && !name.equals(info.name)) {
1337             MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_NAME_CHANGED);
1338             info.name = name;
1339             updateProgress(info);
1340         }
1341     }
1342 
collapseNotificationBar()1343     private void collapseNotificationBar() {
1344         sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
1345     }
1346 
newLooper(String name)1347     private static Looper newLooper(String name) {
1348         final HandlerThread thread = new HandlerThread(name, THREAD_PRIORITY_BACKGROUND);
1349         thread.start();
1350         return thread.getLooper();
1351     }
1352 
1353     /**
1354      * Takes a screenshot and save it to the given location.
1355      */
takeScreenshot(Context context, String path)1356     private static boolean takeScreenshot(Context context, String path) {
1357         final Bitmap bitmap = Screenshooter.takeScreenshot();
1358         if (bitmap == null) {
1359             return false;
1360         }
1361         boolean status;
1362         try (final FileOutputStream fos = new FileOutputStream(path)) {
1363             if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)) {
1364                 ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(150);
1365                 return true;
1366             } else {
1367                 Log.e(TAG, "Failed to save screenshot on " + path);
1368             }
1369         } catch (IOException e ) {
1370             Log.e(TAG, "Failed to save screenshot on " + path, e);
1371             return false;
1372         } finally {
1373             bitmap.recycle();
1374         }
1375         return false;
1376     }
1377 
1378     /**
1379      * Checks whether a character is valid on bugreport names.
1380      */
1381     @VisibleForTesting
isValid(char c)1382     static boolean isValid(char c) {
1383         return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
1384                 || c == '_' || c == '-';
1385     }
1386 
1387     /**
1388      * Helper class encapsulating the UI elements and logic used to display a dialog where user
1389      * can change the details of a bugreport.
1390      */
1391     private final class BugreportInfoDialog {
1392         private EditText mInfoName;
1393         private EditText mInfoTitle;
1394         private EditText mInfoDescription;
1395         private AlertDialog mDialog;
1396         private Button mOkButton;
1397         private int mId;
1398         private int mPid;
1399 
1400         /**
1401          * Last "committed" value of the bugreport name.
1402          * <p>
1403          * Once initially set, it's only updated when user clicks the OK button.
1404          */
1405         private String mSavedName;
1406 
1407         /**
1408          * Last value of the bugreport name as entered by the user.
1409          * <p>
1410          * Every time it's changed the equivalent system property is changed as well, but if the
1411          * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored.
1412          * <p>
1413          * This logic handles the corner-case scenario where {@code dumpstate} finishes after the
1414          * user changed the name but didn't clicked OK yet (for example, because the user is typing
1415          * the description). The only drawback is that if the user changes the name while
1416          * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name
1417          * will be the one that has been canceled. But when {@code dumpstate} finishes the {code
1418          * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of
1419          * such drawback.
1420          */
1421         private String mTempName;
1422 
1423         /**
1424          * Sets its internal state and displays the dialog.
1425          */
initialize(final Context context, BugreportInfo info)1426         private void initialize(final Context context, BugreportInfo info) {
1427             final String dialogTitle =
1428                     context.getString(R.string.bugreport_info_dialog_title, info.id);
1429             // First initializes singleton.
1430             if (mDialog == null) {
1431                 @SuppressLint("InflateParams")
1432                 // It's ok pass null ViewRoot on AlertDialogs.
1433                 final View view = View.inflate(context, R.layout.dialog_bugreport_info, null);
1434 
1435                 mInfoName = (EditText) view.findViewById(R.id.name);
1436                 mInfoTitle = (EditText) view.findViewById(R.id.title);
1437                 mInfoDescription = (EditText) view.findViewById(R.id.description);
1438 
1439                 mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() {
1440 
1441                     @Override
1442                     public void onFocusChange(View v, boolean hasFocus) {
1443                         if (hasFocus) {
1444                             return;
1445                         }
1446                         sanitizeName();
1447                     }
1448                 });
1449 
1450                 mDialog = new AlertDialog.Builder(context)
1451                         .setView(view)
1452                         .setTitle(dialogTitle)
1453                         .setCancelable(false)
1454                         .setPositiveButton(context.getString(R.string.save),
1455                                 null)
1456                         .setNegativeButton(context.getString(com.android.internal.R.string.cancel),
1457                                 new DialogInterface.OnClickListener()
1458                                 {
1459                                     @Override
1460                                     public void onClick(DialogInterface dialog, int id)
1461                                     {
1462                                         MetricsLogger.action(context,
1463                                                 MetricsEvent.ACTION_BUGREPORT_DETAILS_CANCELED);
1464                                         if (!mTempName.equals(mSavedName)) {
1465                                             // Must restore dumpstate's name since it was changed
1466                                             // before user clicked OK.
1467                                             setBugreportNameProperty(mPid, mSavedName);
1468                                         }
1469                                     }
1470                                 })
1471                         .create();
1472 
1473                 mDialog.getWindow().setAttributes(
1474                         new WindowManager.LayoutParams(
1475                                 WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG));
1476 
1477             } else {
1478                 // Re-use view, but reset fields first.
1479                 mDialog.setTitle(dialogTitle);
1480                 mInfoName.setText(null);
1481                 mInfoTitle.setText(null);
1482                 mInfoDescription.setText(null);
1483             }
1484 
1485             // Then set fields.
1486             mSavedName = mTempName = info.name;
1487             mId = info.id;
1488             mPid = info.pid;
1489             if (!TextUtils.isEmpty(info.name)) {
1490                 mInfoName.setText(info.name);
1491             }
1492             if (!TextUtils.isEmpty(info.title)) {
1493                 mInfoTitle.setText(info.title);
1494             }
1495             if (!TextUtils.isEmpty(info.description)) {
1496                 mInfoDescription.setText(info.description);
1497             }
1498 
1499             // And finally display it.
1500             mDialog.show();
1501 
1502             // TODO: in a traditional AlertDialog, when the positive button is clicked the
1503             // dialog is always closed, but we need to validate the name first, so we need to
1504             // get a reference to it, which is only available after it's displayed.
1505             // It would be cleaner to use a regular dialog instead, but let's keep this
1506             // workaround for now and change it later, when we add another button to take
1507             // extra screenshots.
1508             if (mOkButton == null) {
1509                 mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
1510                 mOkButton.setOnClickListener(new View.OnClickListener() {
1511 
1512                     @Override
1513                     public void onClick(View view) {
1514                         MetricsLogger.action(context, MetricsEvent.ACTION_BUGREPORT_DETAILS_SAVED);
1515                         sanitizeName();
1516                         final String name = mInfoName.getText().toString();
1517                         final String title = mInfoTitle.getText().toString();
1518                         final String description = mInfoDescription.getText().toString();
1519 
1520                         updateBugreportInfo(mId, name, title, description);
1521                         mDialog.dismiss();
1522                     }
1523                 });
1524             }
1525         }
1526 
1527         /**
1528          * Sanitizes the user-provided value for the {@code name} field, automatically replacing
1529          * invalid characters if necessary.
1530          */
sanitizeName()1531         private void sanitizeName() {
1532             String name = mInfoName.getText().toString();
1533             if (name.equals(mTempName)) {
1534                 if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name);
1535                 return;
1536             }
1537             final StringBuilder safeName = new StringBuilder(name.length());
1538             boolean changed = false;
1539             for (int i = 0; i < name.length(); i++) {
1540                 final char c = name.charAt(i);
1541                 if (isValid(c)) {
1542                     safeName.append(c);
1543                 } else {
1544                     changed = true;
1545                     safeName.append('_');
1546                 }
1547             }
1548             if (changed) {
1549                 Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'");
1550                 name = safeName.toString();
1551                 mInfoName.setText(name);
1552             }
1553             mTempName = name;
1554 
1555             // Must update system property for the cases where dumpstate finishes
1556             // while the user is still entering other fields (like title or
1557             // description)
1558             setBugreportNameProperty(mPid, name);
1559         }
1560 
1561        /**
1562          * Notifies the dialog that the bugreport has finished so it disables the {@code name}
1563          * field.
1564          * <p>Once the bugreport is finished dumpstate has already generated the final files, so
1565          * changing the name would have no effect.
1566          */
onBugreportFinished(int id)1567         private void onBugreportFinished(int id) {
1568             if (mInfoName != null) {
1569                 mInfoName.setEnabled(false);
1570                 mInfoName.setText(mSavedName);
1571             }
1572         }
1573 
1574     }
1575 
1576     /**
1577      * Information about a bugreport process while its in progress.
1578      */
1579     private static final class BugreportInfo implements Parcelable {
1580         private final Context context;
1581 
1582         /**
1583          * Sequential, user-friendly id used to identify the bugreport.
1584          */
1585         final int id;
1586 
1587         /**
1588          * {@code pid} of the {@code dumpstate} process generating the bugreport.
1589          */
1590         final int pid;
1591 
1592         /**
1593          * Name of the bugreport, will be used to rename the final files.
1594          * <p>
1595          * Initial value is the bugreport filename reported by {@code dumpstate}, but user can
1596          * change it later to a more meaningful name.
1597          */
1598         String name;
1599 
1600         /**
1601          * User-provided, one-line summary of the bug; when set, will be used as the subject
1602          * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1603          */
1604         String title;
1605 
1606         /**
1607          * User-provided, detailed description of the bugreport; when set, will be added to the body
1608          * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1609          */
1610         String description;
1611 
1612         /**
1613          * Maximum progress of the bugreport generation as displayed by the UI.
1614          */
1615         int max;
1616 
1617         /**
1618          * Current progress of the bugreport generation as displayed by the UI.
1619          */
1620         int progress;
1621 
1622         /**
1623          * Maximum progress of the bugreport generation as reported by dumpstate.
1624          */
1625         int realMax;
1626 
1627         /**
1628          * Current progress of the bugreport generation as reported by dumpstate.
1629          */
1630         int realProgress;
1631 
1632         /**
1633          * Time of the last progress update.
1634          */
1635         long lastUpdate = System.currentTimeMillis();
1636 
1637         /**
1638          * Time of the last progress update when Parcel was created.
1639          */
1640         String formattedLastUpdate;
1641 
1642         /**
1643          * Path of the main bugreport file.
1644          */
1645         File bugreportFile;
1646 
1647         /**
1648          * Path of the screenshot files.
1649          */
1650         List<File> screenshotFiles = new ArrayList<>(1);
1651 
1652         /**
1653          * Whether dumpstate sent an intent informing it has finished.
1654          */
1655         boolean finished;
1656 
1657         /**
1658          * Whether the details entries have been added to the bugreport yet.
1659          */
1660         boolean addingDetailsToZip;
1661         boolean addedDetailsToZip;
1662 
1663         /**
1664          * Internal counter used to name screenshot files.
1665          */
1666         int screenshotCounter;
1667 
1668         /**
1669          * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED.
1670          */
BugreportInfo(Context context, int id, int pid, String name, int max)1671         BugreportInfo(Context context, int id, int pid, String name, int max) {
1672             this.context = context;
1673             this.id = id;
1674             this.pid = pid;
1675             this.name = name;
1676             this.max = max;
1677         }
1678 
1679         /**
1680          * Constructor for untracked bugreports - typically called upon receiving BUGREPORT_FINISHED
1681          * without a previous call to BUGREPORT_STARTED.
1682          */
BugreportInfo(Context context, int id)1683         BugreportInfo(Context context, int id) {
1684             this(context, id, id, null, 0);
1685             this.finished = true;
1686         }
1687 
1688         /**
1689          * Gets the name for next screenshot file.
1690          */
getPathNextScreenshot()1691         String getPathNextScreenshot() {
1692             screenshotCounter ++;
1693             return "screenshot-" + pid + "-" + screenshotCounter + ".png";
1694         }
1695 
1696         /**
1697          * Saves the location of a taken screenshot so it can be sent out at the end.
1698          */
addScreenshot(File screenshot)1699         void addScreenshot(File screenshot) {
1700             screenshotFiles.add(screenshot);
1701         }
1702 
1703         /**
1704          * Rename all screenshots files so that they contain the user-generated name instead of pid.
1705          */
renameScreenshots(File screenshotDir)1706         void renameScreenshots(File screenshotDir) {
1707             if (TextUtils.isEmpty(name)) {
1708                 return;
1709             }
1710             final List<File> renamedFiles = new ArrayList<>(screenshotFiles.size());
1711             for (File oldFile : screenshotFiles) {
1712                 final String oldName = oldFile.getName();
1713                 final String newName = oldName.replaceFirst(Integer.toString(pid), name);
1714                 final File newFile;
1715                 if (!newName.equals(oldName)) {
1716                     final File renamedFile = new File(screenshotDir, newName);
1717                     Log.d(TAG, "Renaming screenshot file " + oldFile + " to " + renamedFile);
1718                     newFile = oldFile.renameTo(renamedFile) ? renamedFile : oldFile;
1719                 } else {
1720                     Log.w(TAG, "Name didn't change: " + oldName); // Shouldn't happen.
1721                     newFile = oldFile;
1722                 }
1723                 renamedFiles.add(newFile);
1724             }
1725             screenshotFiles = renamedFiles;
1726         }
1727 
getFormattedLastUpdate()1728         String getFormattedLastUpdate() {
1729             if (context == null) {
1730                 // Restored from Parcel
1731                 return formattedLastUpdate == null ?
1732                         Long.toString(lastUpdate) : formattedLastUpdate;
1733             }
1734             return DateUtils.formatDateTime(context, lastUpdate,
1735                     DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
1736         }
1737 
1738         @Override
toString()1739         public String toString() {
1740             final float percent = ((float) progress * 100 / max);
1741             final float realPercent = ((float) realProgress * 100 / realMax);
1742             return "id: " + id + ", pid: " + pid + ", name: " + name + ", finished: " + finished
1743                     + "\n\ttitle: " + title + "\n\tdescription: " + description
1744                     + "\n\tfile: " + bugreportFile + "\n\tscreenshots: " + screenshotFiles
1745                     + "\n\tprogress: " + progress + "/" + max + " (" + percent + ")"
1746                     + "\n\treal progress: " + realProgress + "/" + realMax + " (" + realPercent + ")"
1747                     + "\n\tlast_update: " + getFormattedLastUpdate()
1748                     + "\naddingDetailsToZip: " + addingDetailsToZip
1749                     + " addedDetailsToZip: " + addedDetailsToZip;
1750         }
1751 
1752         // Parcelable contract
BugreportInfo(Parcel in)1753         protected BugreportInfo(Parcel in) {
1754             context = null;
1755             id = in.readInt();
1756             pid = in.readInt();
1757             name = in.readString();
1758             title = in.readString();
1759             description = in.readString();
1760             max = in.readInt();
1761             progress = in.readInt();
1762             realMax = in.readInt();
1763             realProgress = in.readInt();
1764             lastUpdate = in.readLong();
1765             formattedLastUpdate = in.readString();
1766             bugreportFile = readFile(in);
1767 
1768             int screenshotSize = in.readInt();
1769             for (int i = 1; i <= screenshotSize; i++) {
1770                   screenshotFiles.add(readFile(in));
1771             }
1772 
1773             finished = in.readInt() == 1;
1774             screenshotCounter = in.readInt();
1775         }
1776 
1777         @Override
writeToParcel(Parcel dest, int flags)1778         public void writeToParcel(Parcel dest, int flags) {
1779             dest.writeInt(id);
1780             dest.writeInt(pid);
1781             dest.writeString(name);
1782             dest.writeString(title);
1783             dest.writeString(description);
1784             dest.writeInt(max);
1785             dest.writeInt(progress);
1786             dest.writeInt(realMax);
1787             dest.writeInt(realProgress);
1788             dest.writeLong(lastUpdate);
1789             dest.writeString(getFormattedLastUpdate());
1790             writeFile(dest, bugreportFile);
1791 
1792             dest.writeInt(screenshotFiles.size());
1793             for (File screenshotFile : screenshotFiles) {
1794                 writeFile(dest, screenshotFile);
1795             }
1796 
1797             dest.writeInt(finished ? 1 : 0);
1798             dest.writeInt(screenshotCounter);
1799         }
1800 
1801         @Override
describeContents()1802         public int describeContents() {
1803             return 0;
1804         }
1805 
writeFile(Parcel dest, File file)1806         private void writeFile(Parcel dest, File file) {
1807             dest.writeString(file == null ? null : file.getPath());
1808         }
1809 
readFile(Parcel in)1810         private File readFile(Parcel in) {
1811             final String path = in.readString();
1812             return path == null ? null : new File(path);
1813         }
1814 
1815         public static final Parcelable.Creator<BugreportInfo> CREATOR =
1816                 new Parcelable.Creator<BugreportInfo>() {
1817             public BugreportInfo createFromParcel(Parcel source) {
1818                 return new BugreportInfo(source);
1819             }
1820 
1821             public BugreportInfo[] newArray(int size) {
1822                 return new BugreportInfo[size];
1823             }
1824         };
1825 
1826     }
1827 }
1828