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