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