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