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