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 in %.
180     private static final int CAPPED_PROGRESS = 99;
181     private static final int CAPPED_MAX = 100;
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(R.drawable.ic_bug_report_black_24dp)
1083                 .setLocalOnly(true)
1084                 .setColor(context.getColor(
1085                         com.android.internal.R.color.system_notification_accent_color))
1086                 .extend(new Notification.TvExtender());
1087     }
1088 
1089     /**
1090      * Sends a zipped bugreport notification.
1091      */
sendZippedBugreportNotification( final BugreportInfo info, final boolean takingScreenshot)1092     private void sendZippedBugreportNotification( final BugreportInfo info,
1093             final boolean takingScreenshot) {
1094         new AsyncTask<Void, Void, Void>() {
1095             @Override
1096             protected Void doInBackground(Void... params) {
1097                 Looper.prepare();
1098                 zipBugreport(info);
1099                 sendBugreportNotification(info, takingScreenshot);
1100                 return null;
1101             }
1102         }.execute();
1103     }
1104 
1105     /**
1106      * Zips a bugreport file, returning the path to the new file (or to the
1107      * original in case of failure).
1108      */
zipBugreport(BugreportInfo info)1109     private static void zipBugreport(BugreportInfo info) {
1110         final String bugreportPath = info.bugreportFile.getAbsolutePath();
1111         final String zippedPath = bugreportPath.replace(".txt", ".zip");
1112         Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath);
1113         final File bugreportZippedFile = new File(zippedPath);
1114         try (InputStream is = new FileInputStream(info.bugreportFile);
1115                 ZipOutputStream zos = new ZipOutputStream(
1116                         new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) {
1117             addEntry(zos, info.bugreportFile.getName(), is);
1118             // Delete old file
1119             final boolean deleted = info.bugreportFile.delete();
1120             if (deleted) {
1121                 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")");
1122             } else {
1123                 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")");
1124             }
1125             info.bugreportFile = bugreportZippedFile;
1126         } catch (IOException e) {
1127             Log.e(TAG, "exception zipping file " + zippedPath, e);
1128         }
1129     }
1130 
1131     /**
1132      * Adds the user-provided info into the bugreport zip file.
1133      * <p>
1134      * If user provided a title, it will be saved into a {@code title.txt} entry; similarly, the
1135      * description will be saved on {@code description.txt}.
1136      */
addDetailsToZipFile(BugreportInfo info)1137     private void addDetailsToZipFile(BugreportInfo info) {
1138         synchronized (mLock) {
1139             addDetailsToZipFileLocked(info);
1140         }
1141     }
1142 
addDetailsToZipFileLocked(BugreportInfo info)1143     private void addDetailsToZipFileLocked(BugreportInfo info) {
1144         if (info.bugreportFile == null) {
1145             // One possible reason is a bug in the Parcelization code.
1146             Log.wtf(TAG, "addDetailsToZipFile(): no bugreportFile on " + info);
1147             return;
1148         }
1149         if (TextUtils.isEmpty(info.title) && TextUtils.isEmpty(info.description)) {
1150             Log.d(TAG, "Not touching zip file since neither title nor description are set");
1151             return;
1152         }
1153         if (info.addedDetailsToZip || info.addingDetailsToZip) {
1154             Log.d(TAG, "Already added details to zip file for " + info);
1155             return;
1156         }
1157         info.addingDetailsToZip = true;
1158 
1159         // It's not possible to add a new entry into an existing file, so we need to create a new
1160         // zip, copy all entries, then rename it.
1161         sendBugreportBeingUpdatedNotification(mContext, info.id); // ...and that takes time
1162 
1163         final File dir = info.bugreportFile.getParentFile();
1164         final File tmpZip = new File(dir, "tmp-" + info.bugreportFile.getName());
1165         Log.d(TAG, "Writing temporary zip file (" + tmpZip + ") with title and/or description");
1166         try (ZipFile oldZip = new ZipFile(info.bugreportFile);
1167                 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip))) {
1168 
1169             // First copy contents from original zip.
1170             Enumeration<? extends ZipEntry> entries = oldZip.entries();
1171             while (entries.hasMoreElements()) {
1172                 final ZipEntry entry = entries.nextElement();
1173                 final String entryName = entry.getName();
1174                 if (!entry.isDirectory()) {
1175                     addEntry(zos, entryName, entry.getTime(), oldZip.getInputStream(entry));
1176                 } else {
1177                     Log.w(TAG, "skipping directory entry: " + entryName);
1178                 }
1179             }
1180 
1181             // Then add the user-provided info.
1182             addEntry(zos, "title.txt", info.title);
1183             addEntry(zos, "description.txt", info.description);
1184         } catch (IOException e) {
1185             Log.e(TAG, "exception zipping file " + tmpZip, e);
1186             Toast.makeText(mContext, R.string.bugreport_add_details_to_zip_failed,
1187                     Toast.LENGTH_LONG).show();
1188             return;
1189         } finally {
1190             // Make sure it only tries to add details once, even it fails the first time.
1191             info.addedDetailsToZip = true;
1192             info.addingDetailsToZip = false;
1193             stopForegroundWhenDone(info.id);
1194         }
1195 
1196         if (!tmpZip.renameTo(info.bugreportFile)) {
1197             Log.e(TAG, "Could not rename " + tmpZip + " to " + info.bugreportFile);
1198         }
1199     }
1200 
addEntry(ZipOutputStream zos, String entry, String text)1201     private static void addEntry(ZipOutputStream zos, String entry, String text)
1202             throws IOException {
1203         if (DEBUG) Log.v(TAG, "adding entry '" + entry + "': " + text);
1204         if (!TextUtils.isEmpty(text)) {
1205             addEntry(zos, entry, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)));
1206         }
1207     }
1208 
addEntry(ZipOutputStream zos, String entryName, InputStream is)1209     private static void addEntry(ZipOutputStream zos, String entryName, InputStream is)
1210             throws IOException {
1211         addEntry(zos, entryName, System.currentTimeMillis(), is);
1212     }
1213 
addEntry(ZipOutputStream zos, String entryName, long timestamp, InputStream is)1214     private static void addEntry(ZipOutputStream zos, String entryName, long timestamp,
1215             InputStream is) throws IOException {
1216         final ZipEntry entry = new ZipEntry(entryName);
1217         entry.setTime(timestamp);
1218         zos.putNextEntry(entry);
1219         final int totalBytes = Streams.copy(is, zos);
1220         if (DEBUG) Log.v(TAG, "size of '" + entryName + "' entry: " + totalBytes + " bytes");
1221         zos.closeEntry();
1222     }
1223 
1224     /**
1225      * Find the best matching {@link Account} based on build properties.  If none found, returns
1226      * the first account that looks like an email address.
1227      */
1228     @VisibleForTesting
findSendToAccount(Context context, String preferredDomain)1229     static Pair<UserHandle, Account> findSendToAccount(Context context, String preferredDomain) {
1230         final UserManager um = context.getSystemService(UserManager.class);
1231         final AccountManager am = context.getSystemService(AccountManager.class);
1232 
1233         if (preferredDomain != null && !preferredDomain.startsWith("@")) {
1234             preferredDomain = "@" + preferredDomain;
1235         }
1236 
1237         Pair<UserHandle, Account> first = null;
1238 
1239         for (UserHandle user : um.getUserProfiles()) {
1240             final Account[] accounts;
1241             try {
1242                 accounts = am.getAccountsAsUser(user.getIdentifier());
1243             } catch (RuntimeException e) {
1244                 Log.e(TAG, "Could not get accounts for preferred domain " + preferredDomain
1245                         + " for user " + user, e);
1246                 continue;
1247             }
1248             if (DEBUG) Log.d(TAG, "User: " + user + "  Number of accounts: " + accounts.length);
1249             for (Account account : accounts) {
1250                 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
1251                     final Pair<UserHandle, Account> candidate = Pair.create(user, account);
1252 
1253                     if (!TextUtils.isEmpty(preferredDomain)) {
1254                         // if we have a preferred domain and it matches, return; otherwise keep
1255                         // looking
1256                         if (account.name.endsWith(preferredDomain)) {
1257                             return candidate;
1258                         }
1259                         // if we don't have a preferred domain, just return since it looks like
1260                         // an email address
1261                     } else {
1262                         return candidate;
1263                     }
1264                     if (first == null) {
1265                         first = candidate;
1266                     }
1267                 }
1268             }
1269         }
1270         return first;
1271     }
1272 
getUri(Context context, File file)1273     static Uri getUri(Context context, File file) {
1274         return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null;
1275     }
1276 
getFileExtra(Intent intent, String key)1277     static File getFileExtra(Intent intent, String key) {
1278         final String path = intent.getStringExtra(key);
1279         if (path != null) {
1280             return new File(path);
1281         } else {
1282             return null;
1283         }
1284     }
1285 
1286     /**
1287      * Dumps an intent, extracting the relevant extras.
1288      */
dumpIntent(Intent intent)1289     static String dumpIntent(Intent intent) {
1290         if (intent == null) {
1291             return "NO INTENT";
1292         }
1293         String action = intent.getAction();
1294         if (action == null) {
1295             // Happens when BugreportReceiver calls startService...
1296             action = "no action";
1297         }
1298         final StringBuilder buffer = new StringBuilder(action).append(" extras: ");
1299         addExtra(buffer, intent, EXTRA_ID);
1300         addExtra(buffer, intent, EXTRA_PID);
1301         addExtra(buffer, intent, EXTRA_MAX);
1302         addExtra(buffer, intent, EXTRA_NAME);
1303         addExtra(buffer, intent, EXTRA_DESCRIPTION);
1304         addExtra(buffer, intent, EXTRA_BUGREPORT);
1305         addExtra(buffer, intent, EXTRA_SCREENSHOT);
1306         addExtra(buffer, intent, EXTRA_INFO);
1307         addExtra(buffer, intent, EXTRA_TITLE);
1308 
1309         if (intent.hasExtra(EXTRA_ORIGINAL_INTENT)) {
1310             buffer.append(SHORT_EXTRA_ORIGINAL_INTENT).append(": ");
1311             final Intent originalIntent = intent.getParcelableExtra(EXTRA_ORIGINAL_INTENT);
1312             buffer.append(dumpIntent(originalIntent));
1313         } else {
1314             buffer.append("no ").append(SHORT_EXTRA_ORIGINAL_INTENT);
1315         }
1316 
1317         return buffer.toString();
1318     }
1319 
1320     private static final String SHORT_EXTRA_ORIGINAL_INTENT =
1321             EXTRA_ORIGINAL_INTENT.substring(EXTRA_ORIGINAL_INTENT.lastIndexOf('.') + 1);
1322 
addExtra(StringBuilder buffer, Intent intent, String name)1323     private static void addExtra(StringBuilder buffer, Intent intent, String name) {
1324         final String shortName = name.substring(name.lastIndexOf('.') + 1);
1325         if (intent.hasExtra(name)) {
1326             buffer.append(shortName).append('=').append(intent.getExtra(name));
1327         } else {
1328             buffer.append("no ").append(shortName);
1329         }
1330         buffer.append(", ");
1331     }
1332 
setSystemProperty(String key, String value)1333     private static boolean setSystemProperty(String key, String value) {
1334         try {
1335             if (DEBUG) Log.v(TAG, "Setting system property " + key + " to " + value);
1336             SystemProperties.set(key, value);
1337         } catch (IllegalArgumentException e) {
1338             Log.e(TAG, "Could not set property " + key + " to " + value, e);
1339             return false;
1340         }
1341         return true;
1342     }
1343 
1344     /**
1345      * Updates the system property used by {@code dumpstate} to rename the final bugreport files.
1346      */
setBugreportNameProperty(int pid, String name)1347     private boolean setBugreportNameProperty(int pid, String name) {
1348         Log.d(TAG, "Updating bugreport name to " + name);
1349         final String key = DUMPSTATE_PREFIX + pid + NAME_SUFFIX;
1350         return setSystemProperty(key, name);
1351     }
1352 
1353     /**
1354      * Updates the user-provided details of a bugreport.
1355      */
updateBugreportInfo(int id, String name, String title, String description)1356     private void updateBugreportInfo(int id, String name, String title, String description) {
1357         final BugreportInfo info = getInfo(id);
1358         if (info == null) {
1359             return;
1360         }
1361         if (title != null && !title.equals(info.title)) {
1362             Log.d(TAG, "updating bugreport title: " + title);
1363             MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_TITLE_CHANGED);
1364         }
1365         info.title = title;
1366         if (description != null && !description.equals(info.description)) {
1367             Log.d(TAG, "updating bugreport description: " + description.length() + " chars");
1368             MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_DESCRIPTION_CHANGED);
1369         }
1370         info.description = description;
1371         if (name != null && !name.equals(info.name)) {
1372             Log.d(TAG, "updating bugreport name: " + name);
1373             MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_NAME_CHANGED);
1374             info.name = name;
1375             updateProgress(info);
1376         }
1377     }
1378 
collapseNotificationBar()1379     private void collapseNotificationBar() {
1380         sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
1381     }
1382 
newLooper(String name)1383     private static Looper newLooper(String name) {
1384         final HandlerThread thread = new HandlerThread(name, THREAD_PRIORITY_BACKGROUND);
1385         thread.start();
1386         return thread.getLooper();
1387     }
1388 
1389     /**
1390      * Takes a screenshot and save it to the given location.
1391      */
takeScreenshot(Context context, String path)1392     private static boolean takeScreenshot(Context context, String path) {
1393         final Bitmap bitmap = Screenshooter.takeScreenshot();
1394         if (bitmap == null) {
1395             return false;
1396         }
1397         try (final FileOutputStream fos = new FileOutputStream(path)) {
1398             if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)) {
1399                 ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(150);
1400                 return true;
1401             } else {
1402                 Log.e(TAG, "Failed to save screenshot on " + path);
1403             }
1404         } catch (IOException e ) {
1405             Log.e(TAG, "Failed to save screenshot on " + path, e);
1406             return false;
1407         } finally {
1408             bitmap.recycle();
1409         }
1410         return false;
1411     }
1412 
isTv(Context context)1413     static boolean isTv(Context context) {
1414         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
1415     }
1416 
1417     /**
1418      * Checks whether a character is valid on bugreport names.
1419      */
1420     @VisibleForTesting
isValid(char c)1421     static boolean isValid(char c) {
1422         return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
1423                 || c == '_' || c == '-';
1424     }
1425 
1426     /**
1427      * Helper class encapsulating the UI elements and logic used to display a dialog where user
1428      * can change the details of a bugreport.
1429      */
1430     private final class BugreportInfoDialog {
1431         private EditText mInfoName;
1432         private EditText mInfoTitle;
1433         private EditText mInfoDescription;
1434         private AlertDialog mDialog;
1435         private Button mOkButton;
1436         private int mId;
1437         private int mPid;
1438 
1439         /**
1440          * Last "committed" value of the bugreport name.
1441          * <p>
1442          * Once initially set, it's only updated when user clicks the OK button.
1443          */
1444         private String mSavedName;
1445 
1446         /**
1447          * Last value of the bugreport name as entered by the user.
1448          * <p>
1449          * Every time it's changed the equivalent system property is changed as well, but if the
1450          * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored.
1451          * <p>
1452          * This logic handles the corner-case scenario where {@code dumpstate} finishes after the
1453          * user changed the name but didn't clicked OK yet (for example, because the user is typing
1454          * the description). The only drawback is that if the user changes the name while
1455          * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name
1456          * will be the one that has been canceled. But when {@code dumpstate} finishes the {code
1457          * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of
1458          * such drawback.
1459          */
1460         private String mTempName;
1461 
1462         /**
1463          * Sets its internal state and displays the dialog.
1464          */
1465         @MainThread
initialize(final Context context, BugreportInfo info)1466         void initialize(final Context context, BugreportInfo info) {
1467             final String dialogTitle =
1468                     context.getString(R.string.bugreport_info_dialog_title, info.id);
1469             final Context themedContext = new ContextThemeWrapper(
1470                     context, com.android.internal.R.style.Theme_DeviceDefault_DayNight);
1471             // First initializes singleton.
1472             if (mDialog == null) {
1473                 @SuppressLint("InflateParams")
1474                 // It's ok pass null ViewRoot on AlertDialogs.
1475                 final View view = View.inflate(themedContext, R.layout.dialog_bugreport_info, null);
1476 
1477                 mInfoName = (EditText) view.findViewById(R.id.name);
1478                 mInfoTitle = (EditText) view.findViewById(R.id.title);
1479                 mInfoDescription = (EditText) view.findViewById(R.id.description);
1480 
1481                 mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() {
1482 
1483                     @Override
1484                     public void onFocusChange(View v, boolean hasFocus) {
1485                         if (hasFocus) {
1486                             return;
1487                         }
1488                         sanitizeName();
1489                     }
1490                 });
1491 
1492                 mDialog = new AlertDialog.Builder(themedContext)
1493                         .setView(view)
1494                         .setTitle(dialogTitle)
1495                         .setCancelable(true)
1496                         .setPositiveButton(context.getString(R.string.save),
1497                                 null)
1498                         .setNegativeButton(context.getString(com.android.internal.R.string.cancel),
1499                                 new DialogInterface.OnClickListener()
1500                                 {
1501                                     @Override
1502                                     public void onClick(DialogInterface dialog, int id)
1503                                     {
1504                                         MetricsLogger.action(context,
1505                                                 MetricsEvent.ACTION_BUGREPORT_DETAILS_CANCELED);
1506                                         if (!mTempName.equals(mSavedName)) {
1507                                             // Must restore dumpstate's name since it was changed
1508                                             // before user clicked OK.
1509                                             setBugreportNameProperty(mPid, mSavedName);
1510                                         }
1511                                     }
1512                                 })
1513                         .create();
1514 
1515                 mDialog.getWindow().setAttributes(
1516                         new WindowManager.LayoutParams(
1517                                 WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG));
1518 
1519             } else {
1520                 // Re-use view, but reset fields first.
1521                 mDialog.setTitle(dialogTitle);
1522                 mInfoName.setText(null);
1523                 mInfoName.setEnabled(true);
1524                 mInfoTitle.setText(null);
1525                 mInfoDescription.setText(null);
1526             }
1527 
1528             // Then set fields.
1529             mSavedName = mTempName = info.name;
1530             mId = info.id;
1531             mPid = info.pid;
1532             if (!TextUtils.isEmpty(info.name)) {
1533                 mInfoName.setText(info.name);
1534             }
1535             if (!TextUtils.isEmpty(info.title)) {
1536                 mInfoTitle.setText(info.title);
1537             }
1538             if (!TextUtils.isEmpty(info.description)) {
1539                 mInfoDescription.setText(info.description);
1540             }
1541 
1542             // And finally display it.
1543             mDialog.show();
1544 
1545             // TODO: in a traditional AlertDialog, when the positive button is clicked the
1546             // dialog is always closed, but we need to validate the name first, so we need to
1547             // get a reference to it, which is only available after it's displayed.
1548             // It would be cleaner to use a regular dialog instead, but let's keep this
1549             // workaround for now and change it later, when we add another button to take
1550             // extra screenshots.
1551             if (mOkButton == null) {
1552                 mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
1553                 mOkButton.setOnClickListener(new View.OnClickListener() {
1554 
1555                     @Override
1556                     public void onClick(View view) {
1557                         MetricsLogger.action(context, MetricsEvent.ACTION_BUGREPORT_DETAILS_SAVED);
1558                         sanitizeName();
1559                         final String name = mInfoName.getText().toString();
1560                         final String title = mInfoTitle.getText().toString();
1561                         final String description = mInfoDescription.getText().toString();
1562 
1563                         updateBugreportInfo(mId, name, title, description);
1564                         mDialog.dismiss();
1565                     }
1566                 });
1567             }
1568         }
1569 
1570         /**
1571          * Sanitizes the user-provided value for the {@code name} field, automatically replacing
1572          * invalid characters if necessary.
1573          */
sanitizeName()1574         private void sanitizeName() {
1575             String name = mInfoName.getText().toString();
1576             if (name.equals(mTempName)) {
1577                 if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name);
1578                 return;
1579             }
1580             final StringBuilder safeName = new StringBuilder(name.length());
1581             boolean changed = false;
1582             for (int i = 0; i < name.length(); i++) {
1583                 final char c = name.charAt(i);
1584                 if (isValid(c)) {
1585                     safeName.append(c);
1586                 } else {
1587                     changed = true;
1588                     safeName.append('_');
1589                 }
1590             }
1591             if (changed) {
1592                 Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'");
1593                 name = safeName.toString();
1594                 mInfoName.setText(name);
1595             }
1596             mTempName = name;
1597 
1598             // Must update system property for the cases where dumpstate finishes
1599             // while the user is still entering other fields (like title or
1600             // description)
1601             setBugreportNameProperty(mPid, name);
1602         }
1603 
1604        /**
1605          * Notifies the dialog that the bugreport has finished so it disables the {@code name}
1606          * field.
1607          * <p>Once the bugreport is finished dumpstate has already generated the final files, so
1608          * changing the name would have no effect.
1609          */
onBugreportFinished()1610         void onBugreportFinished() {
1611             if (mInfoName != null) {
1612                 mInfoName.setEnabled(false);
1613                 mInfoName.setText(mSavedName);
1614             }
1615         }
1616 
cancel()1617         void cancel() {
1618             if (mDialog != null) {
1619                 mDialog.cancel();
1620             }
1621         }
1622     }
1623 
1624     /**
1625      * Information about a bugreport process while its in progress.
1626      */
1627     private static final class BugreportInfo implements Parcelable {
1628         private final Context context;
1629 
1630         /**
1631          * Sequential, user-friendly id used to identify the bugreport.
1632          */
1633         final int id;
1634 
1635         /**
1636          * {@code pid} of the {@code dumpstate} process generating the bugreport.
1637          */
1638         final int pid;
1639 
1640         /**
1641          * Name of the bugreport, will be used to rename the final files.
1642          * <p>
1643          * Initial value is the bugreport filename reported by {@code dumpstate}, but user can
1644          * change it later to a more meaningful name.
1645          */
1646         String name;
1647 
1648         /**
1649          * User-provided, one-line summary of the bug; when set, will be used as the subject
1650          * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1651          */
1652         String title;
1653 
1654         /**
1655          * User-provided, detailed description of the bugreport; when set, will be added to the body
1656          * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1657          */
1658         String description;
1659 
1660         /**
1661          * Maximum progress of the bugreport generation as displayed by the UI.
1662          */
1663         int max;
1664 
1665         /**
1666          * Current progress of the bugreport generation as displayed by the UI.
1667          */
1668         int progress;
1669 
1670         /**
1671          * Maximum progress of the bugreport generation as reported by dumpstate.
1672          */
1673         int realMax;
1674 
1675         /**
1676          * Current progress of the bugreport generation as reported by dumpstate.
1677          */
1678         int realProgress;
1679 
1680         /**
1681          * Time of the last progress update.
1682          */
1683         long lastUpdate = System.currentTimeMillis();
1684 
1685         /**
1686          * Time of the last progress update when Parcel was created.
1687          */
1688         String formattedLastUpdate;
1689 
1690         /**
1691          * Path of the main bugreport file.
1692          */
1693         File bugreportFile;
1694 
1695         /**
1696          * Path of the screenshot files.
1697          */
1698         List<File> screenshotFiles = new ArrayList<>(1);
1699 
1700         /**
1701          * Whether dumpstate sent an intent informing it has finished.
1702          */
1703         boolean finished;
1704 
1705         /**
1706          * Whether the details entries have been added to the bugreport yet.
1707          */
1708         boolean addingDetailsToZip;
1709         boolean addedDetailsToZip;
1710 
1711         /**
1712          * Internal counter used to name screenshot files.
1713          */
1714         int screenshotCounter;
1715 
1716         /**
1717          * Descriptive text that will be shown to the user in the notification message.
1718          */
1719         String shareDescription;
1720 
1721         /**
1722          * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED.
1723          */
BugreportInfo(Context context, int id, int pid, String name, int max)1724         BugreportInfo(Context context, int id, int pid, String name, int max) {
1725             this.context = context;
1726             this.id = id;
1727             this.pid = pid;
1728             this.name = name;
1729             this.max = this.realMax = max;
1730         }
1731 
1732         /**
1733          * Constructor for untracked bugreports - typically called upon receiving BUGREPORT_FINISHED
1734          * without a previous call to BUGREPORT_STARTED.
1735          */
BugreportInfo(Context context, int id)1736         BugreportInfo(Context context, int id) {
1737             this(context, id, id, null, 0);
1738             this.finished = true;
1739         }
1740 
1741         /**
1742          * Gets the name for next screenshot file.
1743          */
getPathNextScreenshot()1744         String getPathNextScreenshot() {
1745             screenshotCounter ++;
1746             return "screenshot-" + pid + "-" + screenshotCounter + ".png";
1747         }
1748 
1749         /**
1750          * Saves the location of a taken screenshot so it can be sent out at the end.
1751          */
addScreenshot(File screenshot)1752         void addScreenshot(File screenshot) {
1753             screenshotFiles.add(screenshot);
1754         }
1755 
1756         /**
1757          * Rename all screenshots files so that they contain the user-generated name instead of pid.
1758          */
renameScreenshots(File screenshotDir)1759         void renameScreenshots(File screenshotDir) {
1760             if (TextUtils.isEmpty(name)) {
1761                 return;
1762             }
1763             final List<File> renamedFiles = new ArrayList<>(screenshotFiles.size());
1764             for (File oldFile : screenshotFiles) {
1765                 final String oldName = oldFile.getName();
1766                 final String newName = oldName.replaceFirst(Integer.toString(pid), name);
1767                 final File newFile;
1768                 if (!newName.equals(oldName)) {
1769                     final File renamedFile = new File(screenshotDir, newName);
1770                     Log.d(TAG, "Renaming screenshot file " + oldFile + " to " + renamedFile);
1771                     newFile = oldFile.renameTo(renamedFile) ? renamedFile : oldFile;
1772                 } else {
1773                     Log.w(TAG, "Name didn't change: " + oldName); // Shouldn't happen.
1774                     newFile = oldFile;
1775                 }
1776                 renamedFiles.add(newFile);
1777             }
1778             screenshotFiles = renamedFiles;
1779         }
1780 
getFormattedLastUpdate()1781         String getFormattedLastUpdate() {
1782             if (context == null) {
1783                 // Restored from Parcel
1784                 return formattedLastUpdate == null ?
1785                         Long.toString(lastUpdate) : formattedLastUpdate;
1786             }
1787             return DateUtils.formatDateTime(context, lastUpdate,
1788                     DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
1789         }
1790 
1791         @Override
toString()1792         public String toString() {
1793             final float percent = ((float) progress * 100 / max);
1794             final float realPercent = ((float) realProgress * 100 / realMax);
1795 
1796             final StringBuilder builder = new StringBuilder()
1797                     .append("\tid: ").append(id)
1798                     .append(", pid: ").append(pid)
1799                     .append(", name: ").append(name)
1800                     .append(", finished: ").append(finished)
1801                     .append("\n\ttitle: ").append(title)
1802                     .append("\n\tdescription: ");
1803             if (description == null) {
1804                 builder.append("null");
1805             } else {
1806                 if (TextUtils.getTrimmedLength(description) == 0) {
1807                     builder.append("empty ");
1808                 }
1809                 builder.append("(").append(description.length()).append(" chars)");
1810             }
1811 
1812             return builder
1813                 .append("\n\tfile: ").append(bugreportFile)
1814                 .append("\n\tscreenshots: ").append(screenshotFiles)
1815                 .append("\n\tprogress: ").append(progress).append("/").append(max)
1816                 .append(" (").append(percent).append(")")
1817                 .append("\n\treal progress: ").append(realProgress).append("/").append(realMax)
1818                 .append(" (").append(realPercent).append(")")
1819                 .append("\n\tlast_update: ").append(getFormattedLastUpdate())
1820                 .append("\n\taddingDetailsToZip: ").append(addingDetailsToZip)
1821                 .append(" addedDetailsToZip: ").append(addedDetailsToZip)
1822                 .append("\n\tshareDescription: ").append(shareDescription)
1823                 .toString();
1824         }
1825 
1826         // Parcelable contract
BugreportInfo(Parcel in)1827         protected BugreportInfo(Parcel in) {
1828             context = null;
1829             id = in.readInt();
1830             pid = in.readInt();
1831             name = in.readString();
1832             title = in.readString();
1833             description = in.readString();
1834             max = in.readInt();
1835             progress = in.readInt();
1836             realMax = in.readInt();
1837             realProgress = in.readInt();
1838             lastUpdate = in.readLong();
1839             formattedLastUpdate = in.readString();
1840             bugreportFile = readFile(in);
1841 
1842             int screenshotSize = in.readInt();
1843             for (int i = 1; i <= screenshotSize; i++) {
1844                   screenshotFiles.add(readFile(in));
1845             }
1846 
1847             finished = in.readInt() == 1;
1848             screenshotCounter = in.readInt();
1849             shareDescription = in.readString();
1850         }
1851 
1852         @Override
writeToParcel(Parcel dest, int flags)1853         public void writeToParcel(Parcel dest, int flags) {
1854             dest.writeInt(id);
1855             dest.writeInt(pid);
1856             dest.writeString(name);
1857             dest.writeString(title);
1858             dest.writeString(description);
1859             dest.writeInt(max);
1860             dest.writeInt(progress);
1861             dest.writeInt(realMax);
1862             dest.writeInt(realProgress);
1863             dest.writeLong(lastUpdate);
1864             dest.writeString(getFormattedLastUpdate());
1865             writeFile(dest, bugreportFile);
1866 
1867             dest.writeInt(screenshotFiles.size());
1868             for (File screenshotFile : screenshotFiles) {
1869                 writeFile(dest, screenshotFile);
1870             }
1871 
1872             dest.writeInt(finished ? 1 : 0);
1873             dest.writeInt(screenshotCounter);
1874             dest.writeString(shareDescription);
1875         }
1876 
1877         @Override
describeContents()1878         public int describeContents() {
1879             return 0;
1880         }
1881 
writeFile(Parcel dest, File file)1882         private void writeFile(Parcel dest, File file) {
1883             dest.writeString(file == null ? null : file.getPath());
1884         }
1885 
readFile(Parcel in)1886         private File readFile(Parcel in) {
1887             final String path = in.readString();
1888             return path == null ? null : new File(path);
1889         }
1890 
1891         @SuppressWarnings("unused")
1892         public static final Parcelable.Creator<BugreportInfo> CREATOR =
1893                 new Parcelable.Creator<BugreportInfo>() {
1894             @Override
1895             public BugreportInfo createFromParcel(Parcel source) {
1896                 return new BugreportInfo(source);
1897             }
1898 
1899             @Override
1900             public BugreportInfo[] newArray(int size) {
1901                 return new BugreportInfo[size];
1902             }
1903         };
1904 
1905     }
1906 
1907     private final class DumpstateListener extends IDumpstateListener.Stub
1908         implements DeathRecipient {
1909 
1910         private final BugreportInfo info;
1911         private IDumpstateToken token;
1912 
DumpstateListener(BugreportInfo info)1913         DumpstateListener(BugreportInfo info) {
1914             this.info = info;
1915         }
1916 
1917         /**
1918          * Connects to the {@code dumpstate} binder to receive updates.
1919          */
connect()1920         boolean connect() {
1921             if (token != null) {
1922                 Log.d(TAG, "connect(): " + info.id + " already connected");
1923                 return true;
1924             }
1925             final IBinder service = ServiceManager.getService("dumpstate");
1926             if (service == null) {
1927                 Log.d(TAG, "dumpstate service not bound yet");
1928                 return true;
1929             }
1930             final IDumpstate dumpstate = IDumpstate.Stub.asInterface(service);
1931             try {
1932                 token = dumpstate.setListener("Shell", this, /* perSectionDetails= */ false);
1933                 if (token != null) {
1934                     token.asBinder().linkToDeath(this, 0);
1935                 }
1936             } catch (Exception e) {
1937                 Log.e(TAG, "Could not set dumpstate listener: " + e);
1938             }
1939             return token != null;
1940         }
1941 
1942         @Override
binderDied()1943         public void binderDied() {
1944             if (!info.finished) {
1945                 // TODO: linkToDeath() might be called BEFORE Shell received the
1946                 // BUGREPORT_FINISHED broadcast, in which case the statements below
1947                 // spam logcat (but are harmless).
1948                 // The right, long-term solution is to provide an onFinished() callback
1949                 // on IDumpstateListener and call it instead of using a broadcast.
1950                 Log.w(TAG, "Dumpstate process died:\n" + info);
1951                 stopProgress(info.id);
1952             }
1953             token.asBinder().unlinkToDeath(this, 0);
1954         }
1955 
1956         @Override
onProgress(int progress)1957         public void onProgress(int progress) throws RemoteException {
1958             if (progress > CAPPED_PROGRESS) {
1959                 progress = CAPPED_PROGRESS;
1960             }
1961             updateProgressInfo(progress, CAPPED_MAX);
1962         }
1963 
1964         @Override
onError(int errorCode)1965         public void onError(int errorCode) throws RemoteException {
1966             // TODO(b/111441001): implement
1967         }
1968 
1969         @Override
onFinished()1970         public void onFinished() throws RemoteException {
1971             // TODO(b/111441001): implement
1972         }
1973 
dump(String prefix, PrintWriter pw)1974         public void dump(String prefix, PrintWriter pw) {
1975             pw.print(prefix); pw.print("token: "); pw.println(token);
1976         }
1977 
updateProgressInfo(int progress, int max)1978         private void updateProgressInfo(int progress, int max) {
1979             if (DEBUG) {
1980                 if (progress != info.progress) {
1981                     Log.v(TAG, "Updating progress for PID " + info.pid + "(id: " + info.id
1982                             + ") from " + info.progress + " to " + progress);
1983                 }
1984                 if (max != info.max) {
1985                     Log.v(TAG, "Updating max progress for PID " + info.pid + "(id: " + info.id
1986                             + ") from " + info.max + " to " + max);
1987                 }
1988             }
1989             info.progress = progress;
1990             info.max = max;
1991             info.lastUpdate = System.currentTimeMillis();
1992 
1993             updateProgress(info);
1994         }
1995     }
1996 }
1997