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