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