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