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         // Since BugreportProvider and BugreportProgressService aren't tightly coupled,
790         // we need to make sure they are explicitly tied to a single unique notification URI
791         // so that the service can alert the provider of changes it has done (ie. new bug
792         // reports)
793         // See { @link Cursor#setNotificationUri } and {@link ContentResolver#notifyChanges }
794         final Uri notificationUri = BugreportStorageProvider.getNotificationUri();
795         mContext.getContentResolver().notifyChange(notificationUri, null, false);
796 
797         if (bugreportFile == null) {
798             // Should never happen, dumpstate always set the file.
799             Log.wtf(TAG, "Missing " + EXTRA_BUGREPORT + " on intent " + intent);
800             return;
801         }
802         mInfoDialog.onBugreportFinished();
803         BugreportInfo info = getInfo(id);
804         if (info == null) {
805             // Happens when BUGREPORT_FINISHED was received without a BUGREPORT_STARTED first.
806             Log.v(TAG, "Creating info for untracked ID " + id);
807             info = new BugreportInfo(mContext, id);
808             mProcesses.put(id, new DumpstateListener(info));
809         }
810         info.renameScreenshots(mScreenshotsDir);
811         info.bugreportFile = bugreportFile;
812 
813         final int max = intent.getIntExtra(EXTRA_MAX, -1);
814         if (max != -1) {
815             MetricsLogger.histogram(this, "dumpstate_duration", max);
816             info.max = max;
817         }
818 
819         final File screenshot = getFileExtra(intent, EXTRA_SCREENSHOT);
820         if (screenshot != null) {
821             info.addScreenshot(screenshot);
822         }
823 
824         final String shareTitle = intent.getStringExtra(EXTRA_TITLE);
825         if (!TextUtils.isEmpty(shareTitle)) {
826             info.title = shareTitle;
827             final String shareDescription = intent.getStringExtra(EXTRA_DESCRIPTION);
828             if (!TextUtils.isEmpty(shareDescription)) {
829                 info.shareDescription= shareDescription;
830             }
831             Log.d(TAG, "Bugreport title is " + info.title + ","
832                     + " shareDescription is " + info.shareDescription);
833         }
834         info.finished = true;
835 
836         // Stop running on foreground, otherwise share notification cannot be dismissed.
837         stopForegroundWhenDone(id);
838 
839         triggerLocalNotification(mContext, info);
840     }
841 
842     /**
843      * Responsible for triggering a notification that allows the user to start a "share" intent with
844      * the bugreport. On watches we have other methods to allow the user to start this intent
845      * (usually by triggering it on another connected device); we don't need to display the
846      * notification in this case.
847      */
triggerLocalNotification(final Context context, final BugreportInfo info)848     private void triggerLocalNotification(final Context context, final BugreportInfo info) {
849         if (!info.bugreportFile.exists() || !info.bugreportFile.canRead()) {
850             Log.e(TAG, "Could not read bugreport file " + info.bugreportFile);
851             Toast.makeText(context, R.string.bugreport_unreadable_text, Toast.LENGTH_LONG).show();
852             stopProgress(info.id);
853             return;
854         }
855 
856         boolean isPlainText = info.bugreportFile.getName().toLowerCase().endsWith(".txt");
857         if (!isPlainText) {
858             // Already zipped, send it right away.
859             sendBugreportNotification(info, mTakingScreenshot);
860         } else {
861             // Asynchronously zip the file first, then send it.
862             sendZippedBugreportNotification(info, mTakingScreenshot);
863         }
864     }
865 
buildWarningIntent(Context context, Intent sendIntent)866     private static Intent buildWarningIntent(Context context, Intent sendIntent) {
867         final Intent intent = new Intent(context, BugreportWarningActivity.class);
868         intent.putExtra(Intent.EXTRA_INTENT, sendIntent);
869         return intent;
870     }
871 
872     /**
873      * Build {@link Intent} that can be used to share the given bugreport.
874      */
buildSendIntent(Context context, BugreportInfo info)875     private static Intent buildSendIntent(Context context, BugreportInfo info) {
876         // Files are kept on private storage, so turn into Uris that we can
877         // grant temporary permissions for.
878         final Uri bugreportUri;
879         try {
880             bugreportUri = getUri(context, info.bugreportFile);
881         } catch (IllegalArgumentException e) {
882             // Should not happen on production, but happens when a Shell is sideloaded and
883             // FileProvider cannot find a configured root for it.
884             Log.wtf(TAG, "Could not get URI for " + info.bugreportFile, e);
885             return null;
886         }
887 
888         final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
889         final String mimeType = "application/vnd.android.bugreport";
890         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
891         intent.addCategory(Intent.CATEGORY_DEFAULT);
892         intent.setType(mimeType);
893 
894         final String subject = !TextUtils.isEmpty(info.title) ?
895                 info.title : bugreportUri.getLastPathSegment();
896         intent.putExtra(Intent.EXTRA_SUBJECT, subject);
897 
898         // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String.
899         // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually
900         // create the ClipData object with the attachments URIs.
901         final StringBuilder messageBody = new StringBuilder("Build info: ")
902             .append(SystemProperties.get("ro.build.description"))
903             .append("\nSerial number: ")
904             .append(SystemProperties.get("ro.serialno"));
905         int descriptionLength = 0;
906         if (!TextUtils.isEmpty(info.description)) {
907             messageBody.append("\nDescription: ").append(info.description);
908             descriptionLength = info.description.length();
909         }
910         intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString());
911         final ClipData clipData = new ClipData(null, new String[] { mimeType },
912                 new ClipData.Item(null, null, null, bugreportUri));
913         Log.d(TAG, "share intent: bureportUri=" + bugreportUri);
914         final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri);
915         for (File screenshot : info.screenshotFiles) {
916             final Uri screenshotUri = getUri(context, screenshot);
917             Log.d(TAG, "share intent: screenshotUri=" + screenshotUri);
918             clipData.addItem(new ClipData.Item(null, null, null, screenshotUri));
919             attachments.add(screenshotUri);
920         }
921         intent.setClipData(clipData);
922         intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
923 
924         final Pair<UserHandle, Account> sendToAccount = findSendToAccount(context,
925                 SystemProperties.get("sendbug.preferred.domain"));
926         if (sendToAccount != null) {
927             intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.second.name });
928 
929             // TODO Open the chooser activity on work profile by default.
930             // If we just use startActivityAsUser(), then the launched app couldn't read
931             // attachments.
932             // We probably need to change ChooserActivity to take an extra argument for the
933             // default profile.
934         }
935 
936         // Log what was sent to the intent
937         Log.d(TAG, "share intent: EXTRA_SUBJECT=" + subject + ", EXTRA_TEXT=" + messageBody.length()
938                 + " chars, description=" + descriptionLength + " chars");
939 
940         return intent;
941     }
942 
943     /**
944      * Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE}
945      * intent, but issuing a warning dialog the first time.
946      */
shareBugreport(int id, BugreportInfo sharedInfo)947     private void shareBugreport(int id, BugreportInfo sharedInfo) {
948         MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SHARE);
949         BugreportInfo info = getInfo(id);
950         if (info == null) {
951             // Service was terminated but notification persisted
952             info = sharedInfo;
953             Log.d(TAG, "shareBugreport(): no info for ID " + id + " on managed processes ("
954                     + mProcesses + "), using info from intent instead (" + info + ")");
955         } else {
956             Log.v(TAG, "shareBugReport(): id " + id + " info = " + info);
957         }
958 
959         addDetailsToZipFile(info);
960 
961         final Intent sendIntent = buildSendIntent(mContext, info);
962         if (sendIntent == null) {
963             Log.w(TAG, "Stopping progres on ID " + id + " because share intent could not be built");
964             stopProgress(id);
965             return;
966         }
967 
968         final Intent notifIntent;
969         boolean useChooser = true;
970 
971         // Send through warning dialog by default
972         if (getWarningState(mContext, STATE_UNKNOWN) != STATE_HIDE) {
973             notifIntent = buildWarningIntent(mContext, sendIntent);
974             // No need to show a chooser in this case.
975             useChooser = false;
976         } else {
977             notifIntent = sendIntent;
978         }
979         notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
980 
981         // Send the share intent...
982         if (useChooser) {
983             sendShareIntent(mContext, notifIntent);
984         } else {
985             mContext.startActivity(notifIntent);
986         }
987 
988         // ... and stop watching this process.
989         stopProgress(id);
990     }
991 
sendShareIntent(Context context, Intent intent)992     static void sendShareIntent(Context context, Intent intent) {
993         final Intent chooserIntent = Intent.createChooser(intent,
994                 context.getResources().getText(R.string.bugreport_intent_chooser_title));
995 
996         // Since we may be launched behind lockscreen, make sure that ChooserActivity doesn't finish
997         // itself in onStop.
998         chooserIntent.putExtra(ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP, true);
999         // Starting the activity from a service.
1000         chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1001         context.startActivity(chooserIntent);
1002     }
1003 
1004     /**
1005      * Sends a notification indicating the bugreport has finished so use can share it.
1006      */
sendBugreportNotification(BugreportInfo info, boolean takingScreenshot)1007     private void sendBugreportNotification(BugreportInfo info, boolean takingScreenshot) {
1008 
1009         // Since adding the details can take a while, do it before notifying user.
1010         addDetailsToZipFile(info);
1011 
1012         final Intent shareIntent = new Intent(INTENT_BUGREPORT_SHARE);
1013         shareIntent.setClass(mContext, BugreportProgressService.class);
1014         shareIntent.setAction(INTENT_BUGREPORT_SHARE);
1015         shareIntent.putExtra(EXTRA_ID, info.id);
1016         shareIntent.putExtra(EXTRA_INFO, info);
1017 
1018         String content;
1019         content = takingScreenshot ?
1020                 mContext.getString(R.string.bugreport_finished_pending_screenshot_text)
1021                 : mContext.getString(R.string.bugreport_finished_text);
1022         final String title;
1023         if (TextUtils.isEmpty(info.title)) {
1024             title = mContext.getString(R.string.bugreport_finished_title, info.id);
1025         } else {
1026             title = info.title;
1027             if (!TextUtils.isEmpty(info.shareDescription)) {
1028                 if(!takingScreenshot) content = info.shareDescription;
1029             }
1030         }
1031 
1032         final Notification.Builder builder = newBaseNotification(mContext)
1033                 .setContentTitle(title)
1034                 .setTicker(title)
1035                 .setContentText(content)
1036                 .setContentIntent(PendingIntent.getService(mContext, info.id, shareIntent,
1037                         PendingIntent.FLAG_UPDATE_CURRENT))
1038                 .setDeleteIntent(newCancelIntent(mContext, info));
1039 
1040         if (!TextUtils.isEmpty(info.name)) {
1041             builder.setSubText(info.name);
1042         }
1043 
1044         Log.v(TAG, "Sending 'Share' notification for ID " + info.id + ": " + title);
1045         NotificationManager.from(mContext).notify(info.id, builder.build());
1046     }
1047 
1048     /**
1049      * Sends a notification indicating the bugreport is being updated so the user can wait until it
1050      * finishes - at this point there is nothing to be done other than waiting, hence it has no
1051      * pending action.
1052      */
sendBugreportBeingUpdatedNotification(Context context, int id)1053     private void sendBugreportBeingUpdatedNotification(Context context, int id) {
1054         final String title = context.getString(R.string.bugreport_updating_title);
1055         final Notification.Builder builder = newBaseNotification(context)
1056                 .setContentTitle(title)
1057                 .setTicker(title)
1058                 .setContentText(context.getString(R.string.bugreport_updating_wait));
1059         Log.v(TAG, "Sending 'Updating zip' notification for ID " + id + ": " + title);
1060         sendForegroundabledNotification(id, builder.build());
1061     }
1062 
newBaseNotification(Context context)1063     private static Notification.Builder newBaseNotification(Context context) {
1064         if (sNotificationBundle.isEmpty()) {
1065             // Rename notifcations from "Shell" to "Android System"
1066             sNotificationBundle.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
1067                     context.getString(com.android.internal.R.string.android_system_label));
1068         }
1069         return new Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
1070                 .addExtras(sNotificationBundle)
1071                 .setCategory(Notification.CATEGORY_SYSTEM)
1072                 .setSmallIcon(
1073                         isTv(context) ? R.drawable.ic_bug_report_black_24dp
1074                                 : com.android.internal.R.drawable.stat_sys_adb)
1075                 .setLocalOnly(true)
1076                 .setColor(context.getColor(
1077                         com.android.internal.R.color.system_notification_accent_color))
1078                 .extend(new Notification.TvExtender());
1079     }
1080 
1081     /**
1082      * Sends a zipped bugreport notification.
1083      */
sendZippedBugreportNotification( final BugreportInfo info, final boolean takingScreenshot)1084     private void sendZippedBugreportNotification( final BugreportInfo info,
1085             final boolean takingScreenshot) {
1086         new AsyncTask<Void, Void, Void>() {
1087             @Override
1088             protected Void doInBackground(Void... params) {
1089                 zipBugreport(info);
1090                 sendBugreportNotification(info, takingScreenshot);
1091                 return null;
1092             }
1093         }.execute();
1094     }
1095 
1096     /**
1097      * Zips a bugreport file, returning the path to the new file (or to the
1098      * original in case of failure).
1099      */
zipBugreport(BugreportInfo info)1100     private static void zipBugreport(BugreportInfo info) {
1101         final String bugreportPath = info.bugreportFile.getAbsolutePath();
1102         final String zippedPath = bugreportPath.replace(".txt", ".zip");
1103         Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath);
1104         final File bugreportZippedFile = new File(zippedPath);
1105         try (InputStream is = new FileInputStream(info.bugreportFile);
1106                 ZipOutputStream zos = new ZipOutputStream(
1107                         new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) {
1108             addEntry(zos, info.bugreportFile.getName(), is);
1109             // Delete old file
1110             final boolean deleted = info.bugreportFile.delete();
1111             if (deleted) {
1112                 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")");
1113             } else {
1114                 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")");
1115             }
1116             info.bugreportFile = bugreportZippedFile;
1117         } catch (IOException e) {
1118             Log.e(TAG, "exception zipping file " + zippedPath, e);
1119         }
1120     }
1121 
1122     /**
1123      * Adds the user-provided info into the bugreport zip file.
1124      * <p>
1125      * If user provided a title, it will be saved into a {@code title.txt} entry; similarly, the
1126      * description will be saved on {@code description.txt}.
1127      */
addDetailsToZipFile(BugreportInfo info)1128     private void addDetailsToZipFile(BugreportInfo info) {
1129         synchronized (mLock) {
1130             addDetailsToZipFileLocked(info);
1131         }
1132     }
1133 
addDetailsToZipFileLocked(BugreportInfo info)1134     private void addDetailsToZipFileLocked(BugreportInfo info) {
1135         if (info.bugreportFile == null) {
1136             // One possible reason is a bug in the Parcelization code.
1137             Log.wtf(TAG, "addDetailsToZipFile(): no bugreportFile on " + info);
1138             return;
1139         }
1140         if (TextUtils.isEmpty(info.title) && TextUtils.isEmpty(info.description)) {
1141             Log.d(TAG, "Not touching zip file since neither title nor description are set");
1142             return;
1143         }
1144         if (info.addedDetailsToZip || info.addingDetailsToZip) {
1145             Log.d(TAG, "Already added details to zip file for " + info);
1146             return;
1147         }
1148         info.addingDetailsToZip = true;
1149 
1150         // It's not possible to add a new entry into an existing file, so we need to create a new
1151         // zip, copy all entries, then rename it.
1152         sendBugreportBeingUpdatedNotification(mContext, info.id); // ...and that takes time
1153 
1154         final File dir = info.bugreportFile.getParentFile();
1155         final File tmpZip = new File(dir, "tmp-" + info.bugreportFile.getName());
1156         Log.d(TAG, "Writing temporary zip file (" + tmpZip + ") with title and/or description");
1157         try (ZipFile oldZip = new ZipFile(info.bugreportFile);
1158                 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip))) {
1159 
1160             // First copy contents from original zip.
1161             Enumeration<? extends ZipEntry> entries = oldZip.entries();
1162             while (entries.hasMoreElements()) {
1163                 final ZipEntry entry = entries.nextElement();
1164                 final String entryName = entry.getName();
1165                 if (!entry.isDirectory()) {
1166                     addEntry(zos, entryName, entry.getTime(), oldZip.getInputStream(entry));
1167                 } else {
1168                     Log.w(TAG, "skipping directory entry: " + entryName);
1169                 }
1170             }
1171 
1172             // Then add the user-provided info.
1173             addEntry(zos, "title.txt", info.title);
1174             addEntry(zos, "description.txt", info.description);
1175         } catch (IOException e) {
1176             Log.e(TAG, "exception zipping file " + tmpZip, e);
1177             Toast.makeText(mContext, R.string.bugreport_add_details_to_zip_failed,
1178                     Toast.LENGTH_LONG).show();
1179             return;
1180         } finally {
1181             // Make sure it only tries to add details once, even it fails the first time.
1182             info.addedDetailsToZip = true;
1183             info.addingDetailsToZip = false;
1184             stopForegroundWhenDone(info.id);
1185         }
1186 
1187         if (!tmpZip.renameTo(info.bugreportFile)) {
1188             Log.e(TAG, "Could not rename " + tmpZip + " to " + info.bugreportFile);
1189         }
1190     }
1191 
addEntry(ZipOutputStream zos, String entry, String text)1192     private static void addEntry(ZipOutputStream zos, String entry, String text)
1193             throws IOException {
1194         if (DEBUG) Log.v(TAG, "adding entry '" + entry + "': " + text);
1195         if (!TextUtils.isEmpty(text)) {
1196             addEntry(zos, entry, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)));
1197         }
1198     }
1199 
addEntry(ZipOutputStream zos, String entryName, InputStream is)1200     private static void addEntry(ZipOutputStream zos, String entryName, InputStream is)
1201             throws IOException {
1202         addEntry(zos, entryName, System.currentTimeMillis(), is);
1203     }
1204 
addEntry(ZipOutputStream zos, String entryName, long timestamp, InputStream is)1205     private static void addEntry(ZipOutputStream zos, String entryName, long timestamp,
1206             InputStream is) throws IOException {
1207         final ZipEntry entry = new ZipEntry(entryName);
1208         entry.setTime(timestamp);
1209         zos.putNextEntry(entry);
1210         final int totalBytes = Streams.copy(is, zos);
1211         if (DEBUG) Log.v(TAG, "size of '" + entryName + "' entry: " + totalBytes + " bytes");
1212         zos.closeEntry();
1213     }
1214 
1215     /**
1216      * Find the best matching {@link Account} based on build properties.  If none found, returns
1217      * the first account that looks like an email address.
1218      */
1219     @VisibleForTesting
findSendToAccount(Context context, String preferredDomain)1220     static Pair<UserHandle, Account> findSendToAccount(Context context, String preferredDomain) {
1221         final UserManager um = context.getSystemService(UserManager.class);
1222         final AccountManager am = context.getSystemService(AccountManager.class);
1223 
1224         if (preferredDomain != null && !preferredDomain.startsWith("@")) {
1225             preferredDomain = "@" + preferredDomain;
1226         }
1227 
1228         Pair<UserHandle, Account> first = null;
1229 
1230         for (UserHandle user : um.getUserProfiles()) {
1231             final Account[] accounts;
1232             try {
1233                 accounts = am.getAccountsAsUser(user.getIdentifier());
1234             } catch (RuntimeException e) {
1235                 Log.e(TAG, "Could not get accounts for preferred domain " + preferredDomain
1236                         + " for user " + user, e);
1237                 continue;
1238             }
1239             if (DEBUG) Log.d(TAG, "User: " + user + "  Number of accounts: " + accounts.length);
1240             for (Account account : accounts) {
1241                 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
1242                     final Pair<UserHandle, Account> candidate = Pair.create(user, account);
1243 
1244                     if (!TextUtils.isEmpty(preferredDomain)) {
1245                         // if we have a preferred domain and it matches, return; otherwise keep
1246                         // looking
1247                         if (account.name.endsWith(preferredDomain)) {
1248                             return candidate;
1249                         }
1250                         // if we don't have a preferred domain, just return since it looks like
1251                         // an email address
1252                     } else {
1253                         return candidate;
1254                     }
1255                     if (first == null) {
1256                         first = candidate;
1257                     }
1258                 }
1259             }
1260         }
1261         return first;
1262     }
1263 
getUri(Context context, File file)1264     static Uri getUri(Context context, File file) {
1265         return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null;
1266     }
1267 
getFileExtra(Intent intent, String key)1268     static File getFileExtra(Intent intent, String key) {
1269         final String path = intent.getStringExtra(key);
1270         if (path != null) {
1271             return new File(path);
1272         } else {
1273             return null;
1274         }
1275     }
1276 
1277     /**
1278      * Dumps an intent, extracting the relevant extras.
1279      */
dumpIntent(Intent intent)1280     static String dumpIntent(Intent intent) {
1281         if (intent == null) {
1282             return "NO INTENT";
1283         }
1284         String action = intent.getAction();
1285         if (action == null) {
1286             // Happens when BugreportReceiver calls startService...
1287             action = "no action";
1288         }
1289         final StringBuilder buffer = new StringBuilder(action).append(" extras: ");
1290         addExtra(buffer, intent, EXTRA_ID);
1291         addExtra(buffer, intent, EXTRA_PID);
1292         addExtra(buffer, intent, EXTRA_MAX);
1293         addExtra(buffer, intent, EXTRA_NAME);
1294         addExtra(buffer, intent, EXTRA_DESCRIPTION);
1295         addExtra(buffer, intent, EXTRA_BUGREPORT);
1296         addExtra(buffer, intent, EXTRA_SCREENSHOT);
1297         addExtra(buffer, intent, EXTRA_INFO);
1298         addExtra(buffer, intent, EXTRA_TITLE);
1299 
1300         if (intent.hasExtra(EXTRA_ORIGINAL_INTENT)) {
1301             buffer.append(SHORT_EXTRA_ORIGINAL_INTENT).append(": ");
1302             final Intent originalIntent = intent.getParcelableExtra(EXTRA_ORIGINAL_INTENT);
1303             buffer.append(dumpIntent(originalIntent));
1304         } else {
1305             buffer.append("no ").append(SHORT_EXTRA_ORIGINAL_INTENT);
1306         }
1307 
1308         return buffer.toString();
1309     }
1310 
1311     private static final String SHORT_EXTRA_ORIGINAL_INTENT =
1312             EXTRA_ORIGINAL_INTENT.substring(EXTRA_ORIGINAL_INTENT.lastIndexOf('.') + 1);
1313 
addExtra(StringBuilder buffer, Intent intent, String name)1314     private static void addExtra(StringBuilder buffer, Intent intent, String name) {
1315         final String shortName = name.substring(name.lastIndexOf('.') + 1);
1316         if (intent.hasExtra(name)) {
1317             buffer.append(shortName).append('=').append(intent.getExtra(name));
1318         } else {
1319             buffer.append("no ").append(shortName);
1320         }
1321         buffer.append(", ");
1322     }
1323 
setSystemProperty(String key, String value)1324     private static boolean setSystemProperty(String key, String value) {
1325         try {
1326             if (DEBUG) Log.v(TAG, "Setting system property " + key + " to " + value);
1327             SystemProperties.set(key, value);
1328         } catch (IllegalArgumentException e) {
1329             Log.e(TAG, "Could not set property " + key + " to " + value, e);
1330             return false;
1331         }
1332         return true;
1333     }
1334 
1335     /**
1336      * Updates the system property used by {@code dumpstate} to rename the final bugreport files.
1337      */
setBugreportNameProperty(int pid, String name)1338     private boolean setBugreportNameProperty(int pid, String name) {
1339         Log.d(TAG, "Updating bugreport name to " + name);
1340         final String key = DUMPSTATE_PREFIX + pid + NAME_SUFFIX;
1341         return setSystemProperty(key, name);
1342     }
1343 
1344     /**
1345      * Updates the user-provided details of a bugreport.
1346      */
updateBugreportInfo(int id, String name, String title, String description)1347     private void updateBugreportInfo(int id, String name, String title, String description) {
1348         final BugreportInfo info = getInfo(id);
1349         if (info == null) {
1350             return;
1351         }
1352         if (title != null && !title.equals(info.title)) {
1353             Log.d(TAG, "updating bugreport title: " + title);
1354             MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_TITLE_CHANGED);
1355         }
1356         info.title = title;
1357         if (description != null && !description.equals(info.description)) {
1358             Log.d(TAG, "updating bugreport description: " + description.length() + " chars");
1359             MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_DESCRIPTION_CHANGED);
1360         }
1361         info.description = description;
1362         if (name != null && !name.equals(info.name)) {
1363             Log.d(TAG, "updating bugreport name: " + name);
1364             MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_NAME_CHANGED);
1365             info.name = name;
1366             updateProgress(info);
1367         }
1368     }
1369 
collapseNotificationBar()1370     private void collapseNotificationBar() {
1371         sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
1372     }
1373 
newLooper(String name)1374     private static Looper newLooper(String name) {
1375         final HandlerThread thread = new HandlerThread(name, THREAD_PRIORITY_BACKGROUND);
1376         thread.start();
1377         return thread.getLooper();
1378     }
1379 
1380     /**
1381      * Takes a screenshot and save it to the given location.
1382      */
takeScreenshot(Context context, String path)1383     private static boolean takeScreenshot(Context context, String path) {
1384         final Bitmap bitmap = Screenshooter.takeScreenshot();
1385         if (bitmap == null) {
1386             return false;
1387         }
1388         try (final FileOutputStream fos = new FileOutputStream(path)) {
1389             if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)) {
1390                 ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(150);
1391                 return true;
1392             } else {
1393                 Log.e(TAG, "Failed to save screenshot on " + path);
1394             }
1395         } catch (IOException e ) {
1396             Log.e(TAG, "Failed to save screenshot on " + path, e);
1397             return false;
1398         } finally {
1399             bitmap.recycle();
1400         }
1401         return false;
1402     }
1403 
isTv(Context context)1404     private static boolean isTv(Context context) {
1405         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
1406     }
1407 
1408     /**
1409      * Checks whether a character is valid on bugreport names.
1410      */
1411     @VisibleForTesting
isValid(char c)1412     static boolean isValid(char c) {
1413         return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
1414                 || c == '_' || c == '-';
1415     }
1416 
1417     /**
1418      * Helper class encapsulating the UI elements and logic used to display a dialog where user
1419      * can change the details of a bugreport.
1420      */
1421     private final class BugreportInfoDialog {
1422         private EditText mInfoName;
1423         private EditText mInfoTitle;
1424         private EditText mInfoDescription;
1425         private AlertDialog mDialog;
1426         private Button mOkButton;
1427         private int mId;
1428         private int mPid;
1429 
1430         /**
1431          * Last "committed" value of the bugreport name.
1432          * <p>
1433          * Once initially set, it's only updated when user clicks the OK button.
1434          */
1435         private String mSavedName;
1436 
1437         /**
1438          * Last value of the bugreport name as entered by the user.
1439          * <p>
1440          * Every time it's changed the equivalent system property is changed as well, but if the
1441          * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored.
1442          * <p>
1443          * This logic handles the corner-case scenario where {@code dumpstate} finishes after the
1444          * user changed the name but didn't clicked OK yet (for example, because the user is typing
1445          * the description). The only drawback is that if the user changes the name while
1446          * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name
1447          * will be the one that has been canceled. But when {@code dumpstate} finishes the {code
1448          * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of
1449          * such drawback.
1450          */
1451         private String mTempName;
1452 
1453         /**
1454          * Sets its internal state and displays the dialog.
1455          */
1456         @MainThread
initialize(final Context context, BugreportInfo info)1457         void initialize(final Context context, BugreportInfo info) {
1458             final String dialogTitle =
1459                     context.getString(R.string.bugreport_info_dialog_title, info.id);
1460             // First initializes singleton.
1461             if (mDialog == null) {
1462                 @SuppressLint("InflateParams")
1463                 // It's ok pass null ViewRoot on AlertDialogs.
1464                 final View view = View.inflate(context, R.layout.dialog_bugreport_info, null);
1465 
1466                 mInfoName = (EditText) view.findViewById(R.id.name);
1467                 mInfoTitle = (EditText) view.findViewById(R.id.title);
1468                 mInfoDescription = (EditText) view.findViewById(R.id.description);
1469 
1470                 mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() {
1471 
1472                     @Override
1473                     public void onFocusChange(View v, boolean hasFocus) {
1474                         if (hasFocus) {
1475                             return;
1476                         }
1477                         sanitizeName();
1478                     }
1479                 });
1480 
1481                 mDialog = new AlertDialog.Builder(context)
1482                         .setView(view)
1483                         .setTitle(dialogTitle)
1484                         .setCancelable(true)
1485                         .setPositiveButton(context.getString(R.string.save),
1486                                 null)
1487                         .setNegativeButton(context.getString(com.android.internal.R.string.cancel),
1488                                 new DialogInterface.OnClickListener()
1489                                 {
1490                                     @Override
1491                                     public void onClick(DialogInterface dialog, int id)
1492                                     {
1493                                         MetricsLogger.action(context,
1494                                                 MetricsEvent.ACTION_BUGREPORT_DETAILS_CANCELED);
1495                                         if (!mTempName.equals(mSavedName)) {
1496                                             // Must restore dumpstate's name since it was changed
1497                                             // before user clicked OK.
1498                                             setBugreportNameProperty(mPid, mSavedName);
1499                                         }
1500                                     }
1501                                 })
1502                         .create();
1503 
1504                 mDialog.getWindow().setAttributes(
1505                         new WindowManager.LayoutParams(
1506                                 WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG));
1507 
1508             } else {
1509                 // Re-use view, but reset fields first.
1510                 mDialog.setTitle(dialogTitle);
1511                 mInfoName.setText(null);
1512                 mInfoName.setEnabled(true);
1513                 mInfoTitle.setText(null);
1514                 mInfoDescription.setText(null);
1515             }
1516 
1517             // Then set fields.
1518             mSavedName = mTempName = info.name;
1519             mId = info.id;
1520             mPid = info.pid;
1521             if (!TextUtils.isEmpty(info.name)) {
1522                 mInfoName.setText(info.name);
1523             }
1524             if (!TextUtils.isEmpty(info.title)) {
1525                 mInfoTitle.setText(info.title);
1526             }
1527             if (!TextUtils.isEmpty(info.description)) {
1528                 mInfoDescription.setText(info.description);
1529             }
1530 
1531             // And finally display it.
1532             mDialog.show();
1533 
1534             // TODO: in a traditional AlertDialog, when the positive button is clicked the
1535             // dialog is always closed, but we need to validate the name first, so we need to
1536             // get a reference to it, which is only available after it's displayed.
1537             // It would be cleaner to use a regular dialog instead, but let's keep this
1538             // workaround for now and change it later, when we add another button to take
1539             // extra screenshots.
1540             if (mOkButton == null) {
1541                 mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
1542                 mOkButton.setOnClickListener(new View.OnClickListener() {
1543 
1544                     @Override
1545                     public void onClick(View view) {
1546                         MetricsLogger.action(context, MetricsEvent.ACTION_BUGREPORT_DETAILS_SAVED);
1547                         sanitizeName();
1548                         final String name = mInfoName.getText().toString();
1549                         final String title = mInfoTitle.getText().toString();
1550                         final String description = mInfoDescription.getText().toString();
1551 
1552                         updateBugreportInfo(mId, name, title, description);
1553                         mDialog.dismiss();
1554                     }
1555                 });
1556             }
1557         }
1558 
1559         /**
1560          * Sanitizes the user-provided value for the {@code name} field, automatically replacing
1561          * invalid characters if necessary.
1562          */
sanitizeName()1563         private void sanitizeName() {
1564             String name = mInfoName.getText().toString();
1565             if (name.equals(mTempName)) {
1566                 if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name);
1567                 return;
1568             }
1569             final StringBuilder safeName = new StringBuilder(name.length());
1570             boolean changed = false;
1571             for (int i = 0; i < name.length(); i++) {
1572                 final char c = name.charAt(i);
1573                 if (isValid(c)) {
1574                     safeName.append(c);
1575                 } else {
1576                     changed = true;
1577                     safeName.append('_');
1578                 }
1579             }
1580             if (changed) {
1581                 Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'");
1582                 name = safeName.toString();
1583                 mInfoName.setText(name);
1584             }
1585             mTempName = name;
1586 
1587             // Must update system property for the cases where dumpstate finishes
1588             // while the user is still entering other fields (like title or
1589             // description)
1590             setBugreportNameProperty(mPid, name);
1591         }
1592 
1593        /**
1594          * Notifies the dialog that the bugreport has finished so it disables the {@code name}
1595          * field.
1596          * <p>Once the bugreport is finished dumpstate has already generated the final files, so
1597          * changing the name would have no effect.
1598          */
onBugreportFinished()1599         void onBugreportFinished() {
1600             if (mInfoName != null) {
1601                 mInfoName.setEnabled(false);
1602                 mInfoName.setText(mSavedName);
1603             }
1604         }
1605 
cancel()1606         void cancel() {
1607             if (mDialog != null) {
1608                 mDialog.cancel();
1609             }
1610         }
1611     }
1612 
1613     /**
1614      * Information about a bugreport process while its in progress.
1615      */
1616     private static final class BugreportInfo implements Parcelable {
1617         private final Context context;
1618 
1619         /**
1620          * Sequential, user-friendly id used to identify the bugreport.
1621          */
1622         final int id;
1623 
1624         /**
1625          * {@code pid} of the {@code dumpstate} process generating the bugreport.
1626          */
1627         final int pid;
1628 
1629         /**
1630          * Name of the bugreport, will be used to rename the final files.
1631          * <p>
1632          * Initial value is the bugreport filename reported by {@code dumpstate}, but user can
1633          * change it later to a more meaningful name.
1634          */
1635         String name;
1636 
1637         /**
1638          * User-provided, one-line summary of the bug; when set, will be used as the subject
1639          * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1640          */
1641         String title;
1642 
1643         /**
1644          * User-provided, detailed description of the bugreport; when set, will be added to the body
1645          * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1646          */
1647         String description;
1648 
1649         /**
1650          * Maximum progress of the bugreport generation as displayed by the UI.
1651          */
1652         int max;
1653 
1654         /**
1655          * Current progress of the bugreport generation as displayed by the UI.
1656          */
1657         int progress;
1658 
1659         /**
1660          * Maximum progress of the bugreport generation as reported by dumpstate.
1661          */
1662         int realMax;
1663 
1664         /**
1665          * Current progress of the bugreport generation as reported by dumpstate.
1666          */
1667         int realProgress;
1668 
1669         /**
1670          * Time of the last progress update.
1671          */
1672         long lastUpdate = System.currentTimeMillis();
1673 
1674         /**
1675          * Time of the last progress update when Parcel was created.
1676          */
1677         String formattedLastUpdate;
1678 
1679         /**
1680          * Path of the main bugreport file.
1681          */
1682         File bugreportFile;
1683 
1684         /**
1685          * Path of the screenshot files.
1686          */
1687         List<File> screenshotFiles = new ArrayList<>(1);
1688 
1689         /**
1690          * Whether dumpstate sent an intent informing it has finished.
1691          */
1692         boolean finished;
1693 
1694         /**
1695          * Whether the details entries have been added to the bugreport yet.
1696          */
1697         boolean addingDetailsToZip;
1698         boolean addedDetailsToZip;
1699 
1700         /**
1701          * Internal counter used to name screenshot files.
1702          */
1703         int screenshotCounter;
1704 
1705         /**
1706          * Descriptive text that will be shown to the user in the notification message.
1707          */
1708         String shareDescription;
1709 
1710         /**
1711          * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED.
1712          */
BugreportInfo(Context context, int id, int pid, String name, int max)1713         BugreportInfo(Context context, int id, int pid, String name, int max) {
1714             this.context = context;
1715             this.id = id;
1716             this.pid = pid;
1717             this.name = name;
1718             this.max = this.realMax = max;
1719         }
1720 
1721         /**
1722          * Constructor for untracked bugreports - typically called upon receiving BUGREPORT_FINISHED
1723          * without a previous call to BUGREPORT_STARTED.
1724          */
BugreportInfo(Context context, int id)1725         BugreportInfo(Context context, int id) {
1726             this(context, id, id, null, 0);
1727             this.finished = true;
1728         }
1729 
1730         /**
1731          * Gets the name for next screenshot file.
1732          */
getPathNextScreenshot()1733         String getPathNextScreenshot() {
1734             screenshotCounter ++;
1735             return "screenshot-" + pid + "-" + screenshotCounter + ".png";
1736         }
1737 
1738         /**
1739          * Saves the location of a taken screenshot so it can be sent out at the end.
1740          */
addScreenshot(File screenshot)1741         void addScreenshot(File screenshot) {
1742             screenshotFiles.add(screenshot);
1743         }
1744 
1745         /**
1746          * Rename all screenshots files so that they contain the user-generated name instead of pid.
1747          */
renameScreenshots(File screenshotDir)1748         void renameScreenshots(File screenshotDir) {
1749             if (TextUtils.isEmpty(name)) {
1750                 return;
1751             }
1752             final List<File> renamedFiles = new ArrayList<>(screenshotFiles.size());
1753             for (File oldFile : screenshotFiles) {
1754                 final String oldName = oldFile.getName();
1755                 final String newName = oldName.replaceFirst(Integer.toString(pid), name);
1756                 final File newFile;
1757                 if (!newName.equals(oldName)) {
1758                     final File renamedFile = new File(screenshotDir, newName);
1759                     Log.d(TAG, "Renaming screenshot file " + oldFile + " to " + renamedFile);
1760                     newFile = oldFile.renameTo(renamedFile) ? renamedFile : oldFile;
1761                 } else {
1762                     Log.w(TAG, "Name didn't change: " + oldName); // Shouldn't happen.
1763                     newFile = oldFile;
1764                 }
1765                 renamedFiles.add(newFile);
1766             }
1767             screenshotFiles = renamedFiles;
1768         }
1769 
getFormattedLastUpdate()1770         String getFormattedLastUpdate() {
1771             if (context == null) {
1772                 // Restored from Parcel
1773                 return formattedLastUpdate == null ?
1774                         Long.toString(lastUpdate) : formattedLastUpdate;
1775             }
1776             return DateUtils.formatDateTime(context, lastUpdate,
1777                     DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
1778         }
1779 
1780         @Override
toString()1781         public String toString() {
1782             final float percent = ((float) progress * 100 / max);
1783             final float realPercent = ((float) realProgress * 100 / realMax);
1784 
1785             final StringBuilder builder = new StringBuilder()
1786                     .append("\tid: ").append(id)
1787                     .append(", pid: ").append(pid)
1788                     .append(", name: ").append(name)
1789                     .append(", finished: ").append(finished)
1790                     .append("\n\ttitle: ").append(title)
1791                     .append("\n\tdescription: ");
1792             if (description == null) {
1793                 builder.append("null");
1794             } else {
1795                 if (TextUtils.getTrimmedLength(description) == 0) {
1796                     builder.append("empty ");
1797                 }
1798                 builder.append("(").append(description.length()).append(" chars)");
1799             }
1800 
1801             return builder
1802                 .append("\n\tfile: ").append(bugreportFile)
1803                 .append("\n\tscreenshots: ").append(screenshotFiles)
1804                 .append("\n\tprogress: ").append(progress).append("/").append(max)
1805                 .append(" (").append(percent).append(")")
1806                 .append("\n\treal progress: ").append(realProgress).append("/").append(realMax)
1807                 .append(" (").append(realPercent).append(")")
1808                 .append("\n\tlast_update: ").append(getFormattedLastUpdate())
1809                 .append("\n\taddingDetailsToZip: ").append(addingDetailsToZip)
1810                 .append(" addedDetailsToZip: ").append(addedDetailsToZip)
1811                 .append("\n\tshareDescription: ").append(shareDescription)
1812                 .toString();
1813         }
1814 
1815         // Parcelable contract
BugreportInfo(Parcel in)1816         protected BugreportInfo(Parcel in) {
1817             context = null;
1818             id = in.readInt();
1819             pid = in.readInt();
1820             name = in.readString();
1821             title = in.readString();
1822             description = in.readString();
1823             max = in.readInt();
1824             progress = in.readInt();
1825             realMax = in.readInt();
1826             realProgress = in.readInt();
1827             lastUpdate = in.readLong();
1828             formattedLastUpdate = in.readString();
1829             bugreportFile = readFile(in);
1830 
1831             int screenshotSize = in.readInt();
1832             for (int i = 1; i <= screenshotSize; i++) {
1833                   screenshotFiles.add(readFile(in));
1834             }
1835 
1836             finished = in.readInt() == 1;
1837             screenshotCounter = in.readInt();
1838             shareDescription = in.readString();
1839         }
1840 
1841         @Override
writeToParcel(Parcel dest, int flags)1842         public void writeToParcel(Parcel dest, int flags) {
1843             dest.writeInt(id);
1844             dest.writeInt(pid);
1845             dest.writeString(name);
1846             dest.writeString(title);
1847             dest.writeString(description);
1848             dest.writeInt(max);
1849             dest.writeInt(progress);
1850             dest.writeInt(realMax);
1851             dest.writeInt(realProgress);
1852             dest.writeLong(lastUpdate);
1853             dest.writeString(getFormattedLastUpdate());
1854             writeFile(dest, bugreportFile);
1855 
1856             dest.writeInt(screenshotFiles.size());
1857             for (File screenshotFile : screenshotFiles) {
1858                 writeFile(dest, screenshotFile);
1859             }
1860 
1861             dest.writeInt(finished ? 1 : 0);
1862             dest.writeInt(screenshotCounter);
1863             dest.writeString(shareDescription);
1864         }
1865 
1866         @Override
describeContents()1867         public int describeContents() {
1868             return 0;
1869         }
1870 
writeFile(Parcel dest, File file)1871         private void writeFile(Parcel dest, File file) {
1872             dest.writeString(file == null ? null : file.getPath());
1873         }
1874 
readFile(Parcel in)1875         private File readFile(Parcel in) {
1876             final String path = in.readString();
1877             return path == null ? null : new File(path);
1878         }
1879 
1880         @SuppressWarnings("unused")
1881         public static final Parcelable.Creator<BugreportInfo> CREATOR =
1882                 new Parcelable.Creator<BugreportInfo>() {
1883             @Override
1884             public BugreportInfo createFromParcel(Parcel source) {
1885                 return new BugreportInfo(source);
1886             }
1887 
1888             @Override
1889             public BugreportInfo[] newArray(int size) {
1890                 return new BugreportInfo[size];
1891             }
1892         };
1893 
1894     }
1895 
1896     private final class DumpstateListener extends IDumpstateListener.Stub
1897         implements DeathRecipient {
1898 
1899         private final BugreportInfo info;
1900         private IDumpstateToken token;
1901 
DumpstateListener(BugreportInfo info)1902         DumpstateListener(BugreportInfo info) {
1903             this.info = info;
1904         }
1905 
1906         /**
1907          * Connects to the {@code dumpstate} binder to receive updates.
1908          */
connect()1909         boolean connect() {
1910             if (token != null) {
1911                 Log.d(TAG, "connect(): " + info.id + " already connected");
1912                 return true;
1913             }
1914             final IBinder service = ServiceManager.getService("dumpstate");
1915             if (service == null) {
1916                 Log.d(TAG, "dumpstate service not bound yet");
1917                 return true;
1918             }
1919             final IDumpstate dumpstate = IDumpstate.Stub.asInterface(service);
1920             try {
1921                 token = dumpstate.setListener("Shell", this);
1922                 if (token != null) {
1923                     token.asBinder().linkToDeath(this, 0);
1924                 }
1925             } catch (Exception e) {
1926                 Log.e(TAG, "Could not set dumpstate listener: " + e);
1927             }
1928             return token != null;
1929         }
1930 
1931         @Override
binderDied()1932         public void binderDied() {
1933             if (!info.finished) {
1934                 // TODO: linkToDeath() might be called BEFORE Shell received the
1935                 // BUGREPORT_FINISHED broadcast, in which case the statements below
1936                 // spam logcat (but are harmless).
1937                 // The right, long-term solution is to provide an onFinished() callback
1938                 // on IDumpstateListener and call it instead of using a broadcast.
1939                 Log.w(TAG, "Dumpstate process died:\n" + info);
1940                 stopProgress(info.id);
1941             }
1942             token.asBinder().unlinkToDeath(this, 0);
1943         }
1944 
1945         @Override
onProgressUpdated(int progress)1946         public void onProgressUpdated(int progress) throws RemoteException {
1947             /*
1948              * Checks whether the progress changed in a way that should be displayed to the user:
1949              * - info.progress / info.max represents the displayed progress
1950              * - info.realProgress / info.realMax represents the real progress
1951              * - since the real progress can decrease, the displayed progress is only updated if it
1952              *   increases
1953              * - the displayed progress is capped at a maximum (like 99%)
1954              */
1955             info.realProgress = progress;
1956             final int oldPercentage = (CAPPED_MAX * info.progress) / info.max;
1957             int newPercentage = (CAPPED_MAX * info.realProgress) / info.realMax;
1958             int max = info.realMax;
1959 
1960             if (newPercentage > CAPPED_PROGRESS) {
1961                 progress = newPercentage = CAPPED_PROGRESS;
1962                 max = CAPPED_MAX;
1963             }
1964 
1965             if (newPercentage > oldPercentage) {
1966                 if (DEBUG) {
1967                     if (progress != info.progress) {
1968                         Log.v(TAG, "Updating progress for PID " + info.pid + "(id: " + info.id
1969                                 + ") from " + info.progress + " to " + progress);
1970                     }
1971                     if (max != info.max) {
1972                         Log.v(TAG, "Updating max progress for PID " + info.pid + "(id: " + info.id
1973                                 + ") from " + info.max + " to " + max);
1974                     }
1975                 }
1976                 info.progress = progress;
1977                 info.max = max;
1978                 info.lastUpdate = System.currentTimeMillis();
1979 
1980                 updateProgress(info);
1981             }
1982         }
1983 
1984         @Override
onMaxProgressUpdated(int maxProgress)1985         public void onMaxProgressUpdated(int maxProgress) throws RemoteException {
1986             Log.d(TAG, "onMaxProgressUpdated: " + maxProgress);
1987             info.realMax = maxProgress;
1988         }
1989 
dump(String prefix, PrintWriter pw)1990         public void dump(String prefix, PrintWriter pw) {
1991             pw.print(prefix); pw.print("token: "); pw.println(token);
1992         }
1993     }
1994 }
1995