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