[go: nahoru, domu]

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