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