BugreportProgressService.java revision a0bf0336f0b6ff39cd90aabe0eb48b022d008ed6
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 com.android.shell.BugreportPrefs.STATE_SHOW;
20import static com.android.shell.BugreportPrefs.getWarningState;
21
22import java.io.BufferedOutputStream;
23import java.io.File;
24import java.io.FileDescriptor;
25import java.io.FileInputStream;
26import java.io.FileOutputStream;
27import java.io.IOException;
28import java.io.InputStream;
29import java.io.PrintWriter;
30import java.text.NumberFormat;
31import java.util.ArrayList;
32import java.util.Date;
33import java.util.zip.ZipEntry;
34import java.util.zip.ZipOutputStream;
35
36import libcore.io.Streams;
37
38import com.google.android.collect.Lists;
39
40import android.accounts.Account;
41import android.accounts.AccountManager;
42import android.app.Notification;
43import android.app.Notification.Action;
44import android.app.NotificationManager;
45import android.app.PendingIntent;
46import android.app.Service;
47import android.content.ClipData;
48import android.content.Context;
49import android.content.Intent;
50import android.content.res.Configuration;
51import android.net.Uri;
52import android.os.AsyncTask;
53import android.os.Handler;
54import android.os.HandlerThread;
55import android.os.IBinder;
56import android.os.Looper;
57import android.os.Message;
58import android.os.Parcelable;
59import android.os.Process;
60import android.os.SystemProperties;
61import android.support.v4.content.FileProvider;
62import android.text.format.DateUtils;
63import android.util.Log;
64import android.util.Patterns;
65import android.util.SparseArray;
66import android.widget.Toast;
67
68/**
69 * Service used to keep progress of bug reports processes ({@code dumpstate}).
70 * <p>
71 * The workflow is:
72 * <ol>
73 * <li>When {@code dumpstate} starts, it sends a {@code BUGREPORT_STARTED} with its pid and the
74 * estimated total effort.
75 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service.
76 * <li>Upon start, this service:
77 * <ol>
78 * <li>Issues a system notification so user can watch the progresss (which is 0% initially).
79 * <li>Polls the {@link SystemProperties} for updates on the {@code dumpstate} progress.
80 * <li>If the progress changed, it updates the system notification.
81 * </ol>
82 * <li>As {@code dumpstate} progresses, it updates the system property.
83 * <li>When {@code dumpstate} finishes, it sends a {@code BUGREPORT_FINISHED} intent.
84 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service, which in
85 * turn:
86 * <ol>
87 * <li>Updates the system notification so user can share the bug report.
88 * <li>Stops monitoring that {@code dumpstate} process.
89 * <li>Stops itself if it doesn't have any process left to monitor.
90 * </ol>
91 * </ol>
92 */
93public class BugreportProgressService extends Service {
94    static final String TAG = "Shell";
95    private static final boolean DEBUG = false;
96
97    private static final String AUTHORITY = "com.android.shell";
98
99    static final String INTENT_BUGREPORT_STARTED = "android.intent.action.BUGREPORT_STARTED";
100    static final String INTENT_BUGREPORT_FINISHED = "android.intent.action.BUGREPORT_FINISHED";
101    static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL";
102
103    static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
104    static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
105    static final String EXTRA_PID = "android.intent.extra.PID";
106    static final String EXTRA_MAX = "android.intent.extra.MAX";
107    static final String EXTRA_NAME = "android.intent.extra.NAME";
108    static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT";
109
110    private static final int MSG_SERVICE_COMMAND = 1;
111    private static final int MSG_POLL = 2;
112
113    /** Polling frequency, in milliseconds. */
114    private static final long POLLING_FREQUENCY = 500;
115
116    /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */
117    private static final long INACTIVITY_TIMEOUT = 3 * DateUtils.MINUTE_IN_MILLIS;
118
119    /** System properties used for monitoring progress. */
120    private static final String DUMPSTATE_PREFIX = "dumpstate.";
121    private static final String PROGRESS_SUFFIX = ".progress";
122    private static final String MAX_SUFFIX = ".max";
123
124    /** System property (and value) used for stop dumpstate. */
125    private static final String CTL_STOP = "ctl.stop";
126    private static final String BUGREPORT_SERVICE = "bugreportplus";
127
128    /** Managed dumpstate processes (keyed by pid) */
129    private final SparseArray<BugreportInfo> mProcesses = new SparseArray<>();
130
131    private Looper mServiceLooper;
132    private ServiceHandler mServiceHandler;
133
134    @Override
135    public void onCreate() {
136        HandlerThread thread = new HandlerThread("BugreportProgressServiceThread",
137                Process.THREAD_PRIORITY_BACKGROUND);
138        thread.start();
139
140        mServiceLooper = thread.getLooper();
141        mServiceHandler = new ServiceHandler(mServiceLooper);
142    }
143
144    @Override
145    public int onStartCommand(Intent intent, int flags, int startId) {
146        if (intent != null) {
147            // Handle it in a separate thread.
148            Message msg = mServiceHandler.obtainMessage();
149            msg.what = MSG_SERVICE_COMMAND;
150            msg.obj = intent;
151            mServiceHandler.sendMessage(msg);
152        }
153
154        // If service is killed it cannot be recreated because it would not know which
155        // dumpstate PIDs it would have to watch.
156        return START_NOT_STICKY;
157    }
158
159    @Override
160    public IBinder onBind(Intent intent) {
161        return null;
162    }
163
164    @Override
165    public void onDestroy() {
166        mServiceLooper.quit();
167        super.onDestroy();
168    }
169
170    @Override
171    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
172        writer.printf("Monitored dumpstate processes: \n");
173        synchronized (mProcesses) {
174            for (int i = 0; i < mProcesses.size(); i++) {
175              writer.printf("\t%s\n", mProcesses.valueAt(i));
176            }
177        }
178    }
179
180    private final class ServiceHandler extends Handler {
181        public ServiceHandler(Looper looper) {
182            super(looper);
183            poll();
184        }
185
186        @Override
187        public void handleMessage(Message msg) {
188            if (msg.what == MSG_POLL) {
189                poll();
190                return;
191            }
192
193            if (msg.what != MSG_SERVICE_COMMAND) {
194                // Sanity check.
195                Log.e(TAG, "Invalid message type: " + msg.what);
196                return;
197            }
198
199            // At this point it's handling onStartCommand(), whose intent contains the extras
200            // originally received by BugreportReceiver.
201            if (!(msg.obj instanceof Intent)) {
202                // Sanity check.
203                Log.e(TAG, "Internal error: invalid msg.obj: " + msg.obj);
204                return;
205            }
206            final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT);
207            if (!(parcel instanceof Intent)) {
208                // Sanity check.
209                Log.e(TAG, "Internal error: msg.obj is missing extra " + EXTRA_ORIGINAL_INTENT);
210                return;
211            }
212
213            final Intent intent = (Intent) parcel;
214            final String action = intent.getAction();
215            int pid = intent.getIntExtra(EXTRA_PID, 0);
216            int max = intent.getIntExtra(EXTRA_MAX, -1);
217            String name = intent.getStringExtra(EXTRA_NAME);
218
219            if (DEBUG) Log.v(TAG, "action: " + action + ", name: " + name + ", pid: " + pid
220                    + ", max: "+ max);
221            switch (action) {
222                case INTENT_BUGREPORT_STARTED:
223                    if (!startProgress(name, pid, max)) {
224                        stopSelfWhenDone();
225                        return;
226                    }
227                    break;
228                case INTENT_BUGREPORT_FINISHED:
229                    if (pid == -1) {
230                        // Shouldn't happen, unless BUGREPORT_FINISHED is received from a legacy,
231                        // out-of-sync dumpstate process.
232                        Log.w(TAG, "Missing " + EXTRA_PID + " on intent " + intent);
233                    }
234                    stopProgress(pid, intent);
235                    break;
236                case INTENT_BUGREPORT_CANCEL:
237                    cancel(pid);
238                    break;
239                default:
240                    Log.w(TAG, "Unsupported intent: " + action);
241            }
242            return;
243
244        }
245
246        private void poll() {
247            if (pollProgress()) {
248                // Keep polling...
249                sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY);
250            }
251        }
252    }
253
254    /**
255     * Creates the {@link BugreportInfo} for a process and issue a system notification to
256     * indicate its progress.
257     *
258     * @return whether it succeeded or not.
259     */
260    private boolean startProgress(String name, int pid, int max) {
261        if (name == null) {
262            Log.w(TAG, "Missing " + EXTRA_NAME + " on start intent");
263        }
264        if (pid == -1) {
265            Log.e(TAG, "Missing " + EXTRA_PID + " on start intent");
266            return false;
267        }
268        if (max <= 0) {
269            Log.e(TAG, "Invalid value for extra " + EXTRA_MAX + ": " + max);
270            return false;
271        }
272
273        final BugreportInfo info = new BugreportInfo(getApplicationContext(), pid, name, max);
274        synchronized (mProcesses) {
275            if (mProcesses.indexOfKey(pid) >= 0) {
276                Log.w(TAG, "PID " + pid + " already watched");
277            } else {
278                mProcesses.put(info.pid, info);
279            }
280        }
281        updateProgress(info);
282        return true;
283    }
284
285    /**
286     * Updates the system notification for a given bug report.
287     */
288    private void updateProgress(BugreportInfo info) {
289        if (info.max <= 0 || info.progress < 0) {
290            Log.e(TAG, "Invalid progress values for " + info);
291            return;
292        }
293
294        final Context context = getApplicationContext();
295        final NumberFormat nf = NumberFormat.getPercentInstance();
296        nf.setMinimumFractionDigits(2);
297        nf.setMaximumFractionDigits(2);
298        final String percentText = nf.format((double) info.progress / info.max);
299
300        final Intent cancelIntent = new Intent(context, BugreportReceiver.class);
301        cancelIntent.setAction(INTENT_BUGREPORT_CANCEL);
302        cancelIntent.putExtra(EXTRA_PID, info.pid);
303        final Action cancelAction = new Action.Builder(null,
304                context.getString(com.android.internal.R.string.cancel),
305                PendingIntent.getBroadcast(context, info.pid, cancelIntent,
306                        PendingIntent.FLAG_CANCEL_CURRENT)).build();
307
308        final String title = context.getString(R.string.bugreport_in_progress_title);
309        final String name =
310                info.name != null ? info.name : context.getString(R.string.bugreport_unnamed);
311
312        final Notification notification = new Notification.Builder(context)
313                .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
314                .setContentTitle(title)
315                .setTicker(title)
316                .setContentText(name)
317                .setContentInfo(percentText)
318                .setProgress(info.max, info.progress, false)
319                .setOngoing(true)
320                .setLocalOnly(true)
321                .setColor(context.getColor(
322                        com.android.internal.R.color.system_notification_accent_color))
323                .addAction(cancelAction)
324                .build();
325
326        NotificationManager.from(context).notify(TAG, info.pid, notification);
327    }
328
329    /**
330     * Finalizes the progress on a given process and sends the finished intent.
331     */
332    private void stopProgress(int pid, Intent intent) {
333        synchronized (mProcesses) {
334            if (mProcesses.indexOfKey(pid) < 0) {
335                Log.w(TAG, "PID not watched: " + pid);
336            } else {
337                mProcesses.remove(pid);
338            }
339            stopSelfWhenDone();
340        }
341        if (DEBUG) Log.v(TAG, "stopProgress(" + pid + "): cancel notification");
342        NotificationManager.from(getApplicationContext()).cancel(TAG, pid);
343        if (intent != null) {
344            // Bug report finished fine: send a new, different notification.
345            if (DEBUG) Log.v(TAG, "stopProgress(" + pid + "): finish bug report");
346            onBugreportFinished(pid, intent);
347        }
348    }
349
350    /**
351     * Cancels a bugreport upon user's request.
352     */
353    private void cancel(int pid) {
354        Log.i(TAG, "Cancelling PID " + pid + " on user's request");
355        SystemProperties.set(CTL_STOP, BUGREPORT_SERVICE);
356        stopProgress(pid, null);
357    }
358
359    /**
360     * Poll {@link SystemProperties} to get the progress on each monitored process.
361     *
362     * @return whether it should keep polling.
363     */
364    private boolean pollProgress() {
365        synchronized (mProcesses) {
366            if (mProcesses.size() == 0) {
367                Log.d(TAG, "No process to poll progress.");
368            }
369            for (int i = 0; i < mProcesses.size(); i++) {
370                final int pid = mProcesses.keyAt(i);
371                final String progressKey = DUMPSTATE_PREFIX + pid + PROGRESS_SUFFIX;
372                final int progress = SystemProperties.getInt(progressKey, 0);
373                if (progress == 0) {
374                    Log.v(TAG, "System property " + progressKey + " is not set yet");
375                    continue;
376                }
377                final int max = SystemProperties.getInt(DUMPSTATE_PREFIX + pid + MAX_SUFFIX, 0);
378                final BugreportInfo info = mProcesses.valueAt(i);
379                final boolean maxChanged = max > 0 && max != info.max;
380                final boolean progressChanged = progress > 0 && progress != info.progress;
381
382                if (progressChanged || maxChanged) {
383                    if (progressChanged) {
384                        if (DEBUG) Log.v(TAG, "Updating progress for PID " + pid + " from "
385                                + info.progress + " to " + progress);
386                        info.progress = progress;
387                    }
388                    if (maxChanged) {
389                        Log.i(TAG, "Updating max progress for PID " + pid + " from " + info.max
390                                + " to " + max);
391                        info.max = max;
392                    }
393                    info.lastUpdate = System.currentTimeMillis();
394                    updateProgress(info);
395                } else {
396                    long inactiveTime = System.currentTimeMillis() - info.lastUpdate;
397                    if (inactiveTime >= INACTIVITY_TIMEOUT) {
398                        Log.w(TAG, "No progress update for process " + pid + " since "
399                                + info.getFormattedLastUpdate());
400                        stopProgress(info.pid, null);
401                    }
402                }
403            }
404            return true;
405        }
406    }
407
408    /**
409     * Finishes the service when it's not monitoring any more processes.
410     */
411    private void stopSelfWhenDone() {
412        synchronized (mProcesses) {
413            if (mProcesses.size() > 0) {
414                if (DEBUG) Log.v(TAG, "Staying alive, waiting for pids " + mProcesses);
415                return;
416            }
417            Log.v(TAG, "No more pids to handle, shutting down");
418            stopSelf();
419        }
420    }
421
422    private void onBugreportFinished(int pid, Intent intent) {
423        final Context context = getApplicationContext();
424        final Configuration conf = context.getResources().getConfiguration();
425        final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT);
426        final File screenshotFile = getFileExtra(intent, EXTRA_SCREENSHOT);
427
428        if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) {
429            triggerLocalNotification(context, pid, bugreportFile, screenshotFile);
430        }
431    }
432
433    /**
434     * Responsible for triggering a notification that allows the user to start a "share" intent with
435     * the bug report. On watches we have other methods to allow the user to start this intent
436     * (usually by triggering it on another connected device); we don't need to display the
437     * notification in this case.
438     */
439    private static void triggerLocalNotification(final Context context, final int pid,
440            final File bugreportFile, final File screenshotFile) {
441        if (!bugreportFile.exists() || !bugreportFile.canRead()) {
442            Log.e(TAG, "Could not read bugreport file " + bugreportFile);
443            Toast.makeText(context, context.getString(R.string.bugreport_unreadable_text),
444                    Toast.LENGTH_LONG).show();
445            return;
446        }
447
448        boolean isPlainText = bugreportFile.getName().toLowerCase().endsWith(".txt");
449        if (!isPlainText) {
450            // Already zipped, send it right away.
451            sendBugreportNotification(context, pid, bugreportFile, screenshotFile);
452        } else {
453            // Asynchronously zip the file first, then send it.
454            sendZippedBugreportNotification(context, pid, bugreportFile, screenshotFile);
455        }
456    }
457
458    private static Intent buildWarningIntent(Context context, Intent sendIntent) {
459        final Intent intent = new Intent(context, BugreportWarningActivity.class);
460        intent.putExtra(Intent.EXTRA_INTENT, sendIntent);
461        return intent;
462    }
463
464    /**
465     * Build {@link Intent} that can be used to share the given bugreport.
466     */
467    private static Intent buildSendIntent(Context context, Uri bugreportUri, Uri screenshotUri) {
468        final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
469        final String mimeType = "application/vnd.android.bugreport";
470        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
471        intent.addCategory(Intent.CATEGORY_DEFAULT);
472        intent.setType(mimeType);
473
474        intent.putExtra(Intent.EXTRA_SUBJECT, bugreportUri.getLastPathSegment());
475
476        // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String.
477        // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually
478        // create the ClipData object with the attachments URIs.
479        String messageBody = String.format("Build info: %s\nSerial number:%s",
480                SystemProperties.get("ro.build.description"), SystemProperties.get("ro.serialno"));
481        intent.putExtra(Intent.EXTRA_TEXT, messageBody);
482        final ClipData clipData = new ClipData(null, new String[] { mimeType },
483                new ClipData.Item(null, null, null, bugreportUri));
484        final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri);
485        if (screenshotUri != null) {
486            clipData.addItem(new ClipData.Item(null, null, null, screenshotUri));
487            attachments.add(screenshotUri);
488        }
489        intent.setClipData(clipData);
490        intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
491
492        final Account sendToAccount = findSendToAccount(context);
493        if (sendToAccount != null) {
494            intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name });
495        }
496
497        return intent;
498    }
499
500    /**
501     * Sends a bugreport notitication.
502     */
503    private static void sendBugreportNotification(Context context, int pid, File bugreportFile,
504            File screenshotFile) {
505        // Files are kept on private storage, so turn into Uris that we can
506        // grant temporary permissions for.
507        final Uri bugreportUri = getUri(context, bugreportFile);
508        final Uri screenshotUri = getUri(context, screenshotFile);
509
510        Intent sendIntent = buildSendIntent(context, bugreportUri, screenshotUri);
511        Intent notifIntent;
512
513        // Send through warning dialog by default
514        if (getWarningState(context, STATE_SHOW) == STATE_SHOW) {
515            notifIntent = buildWarningIntent(context, sendIntent);
516        } else {
517            notifIntent = sendIntent;
518        }
519        notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
520
521        final String title = context.getString(R.string.bugreport_finished_title);
522        final Notification.Builder builder = new Notification.Builder(context)
523                .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
524                .setContentTitle(title)
525                .setTicker(title)
526                .setContentText(context.getString(R.string.bugreport_finished_text))
527                .setContentIntent(PendingIntent.getActivity(
528                        context, 0, notifIntent, PendingIntent.FLAG_CANCEL_CURRENT))
529                .setAutoCancel(true)
530                .setLocalOnly(true)
531                .setColor(context.getColor(
532                        com.android.internal.R.color.system_notification_accent_color));
533
534        NotificationManager.from(context).notify(TAG, pid, builder.build());
535    }
536
537    /**
538     * Sends a zipped bugreport notification.
539     */
540    private static void sendZippedBugreportNotification(final Context context,
541            final int pid, final File bugreportFile, final File screenshotFile) {
542        new AsyncTask<Void, Void, Void>() {
543            @Override
544            protected Void doInBackground(Void... params) {
545                File zippedFile = zipBugreport(bugreportFile);
546                sendBugreportNotification(context, pid, zippedFile, screenshotFile);
547                return null;
548            }
549        }.execute();
550    }
551
552    /**
553     * Zips a bugreport file, returning the path to the new file (or to the
554     * original in case of failure).
555     */
556    private static File zipBugreport(File bugreportFile) {
557        String bugreportPath = bugreportFile.getAbsolutePath();
558        String zippedPath = bugreportPath.replace(".txt", ".zip");
559        Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath);
560        File bugreportZippedFile = new File(zippedPath);
561        try (InputStream is = new FileInputStream(bugreportFile);
562                ZipOutputStream zos = new ZipOutputStream(
563                        new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) {
564            ZipEntry entry = new ZipEntry(bugreportFile.getName());
565            entry.setTime(bugreportFile.lastModified());
566            zos.putNextEntry(entry);
567            int totalBytes = Streams.copy(is, zos);
568            Log.v(TAG, "size of original bugreport: " + totalBytes + " bytes");
569            zos.closeEntry();
570            // Delete old file;
571            boolean deleted = bugreportFile.delete();
572            if (deleted) {
573                Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")");
574            } else {
575                Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")");
576            }
577            return bugreportZippedFile;
578        } catch (IOException e) {
579            Log.e(TAG, "exception zipping file " + zippedPath, e);
580            return bugreportFile; // Return original.
581        }
582    }
583
584    /**
585     * Find the best matching {@link Account} based on build properties.
586     */
587    private static Account findSendToAccount(Context context) {
588        final AccountManager am = (AccountManager) context.getSystemService(
589                Context.ACCOUNT_SERVICE);
590
591        String preferredDomain = SystemProperties.get("sendbug.preferred.domain");
592        if (!preferredDomain.startsWith("@")) {
593            preferredDomain = "@" + preferredDomain;
594        }
595
596        final Account[] accounts = am.getAccounts();
597        Account foundAccount = null;
598        for (Account account : accounts) {
599            if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
600                if (!preferredDomain.isEmpty()) {
601                    // if we have a preferred domain and it matches, return; otherwise keep
602                    // looking
603                    if (account.name.endsWith(preferredDomain)) {
604                        return account;
605                    } else {
606                        foundAccount = account;
607                    }
608                    // if we don't have a preferred domain, just return since it looks like
609                    // an email address
610                } else {
611                    return account;
612                }
613            }
614        }
615        return foundAccount;
616    }
617
618    private static Uri getUri(Context context, File file) {
619        return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null;
620    }
621
622    static File getFileExtra(Intent intent, String key) {
623        final String path = intent.getStringExtra(key);
624        if (path != null) {
625            return new File(path);
626        } else {
627            return null;
628        }
629    }
630
631    /**
632     * Information about a bug report process while its in progress.
633     */
634    private static final class BugreportInfo {
635        private final Context context;
636
637        /**
638         * {@code pid} of the {@code dumpstate} process generating the bug report.
639         */
640        final int pid;
641
642        /**
643         * Name of the bug report, will be used to rename the final files.
644         * <p>
645         * Initial value is the bug report filename reported by {@code dumpstate}, but user can
646         * change it later to a more meaningful name.
647         */
648        String name;
649
650        /**
651         * Maximum progress of the bug report generation.
652         */
653        int max;
654
655        /**
656         * Current progress of the bug report generation.
657         */
658        int progress;
659
660        /**
661         * Time of the last progress update.
662         */
663        long lastUpdate = System.currentTimeMillis();
664
665        BugreportInfo(Context context, int pid, String name, int max) {
666            this.context = context;
667            this.pid = pid;
668            this.name = name;
669            this.max = max;
670        }
671
672        String getFormattedLastUpdate() {
673            return DateUtils.formatDateTime(context, lastUpdate,
674                    DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
675        }
676
677        @Override
678        public String toString() {
679            final float percent = ((float) progress * 100 / max);
680            return "Progress for " + name + " (pid=" + pid + "): " + progress + "/" + max
681                    + " (" + percent + "%) Last update: " + getFormattedLastUpdate();
682        }
683    }
684}
685