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