BugreportProgressService.java revision bc73ffc06fd2b5b30802cc7e8874a986626b897d
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.zip.ZipEntry;
33import java.util.zip.ZipOutputStream;
34
35import libcore.io.Streams;
36
37import com.android.internal.annotations.VisibleForTesting;
38import com.google.android.collect.Lists;
39
40import android.accounts.Account;
41import android.accounts.AccountManager;
42import android.annotation.SuppressLint;
43import android.app.AlertDialog;
44import android.app.Notification;
45import android.app.Notification.Action;
46import android.app.NotificationManager;
47import android.app.PendingIntent;
48import android.app.Service;
49import android.content.ClipData;
50import android.content.Context;
51import android.content.DialogInterface;
52import android.content.Intent;
53import android.content.res.Configuration;
54import android.net.Uri;
55import android.os.AsyncTask;
56import android.os.Handler;
57import android.os.HandlerThread;
58import android.os.IBinder;
59import android.os.Looper;
60import android.os.Message;
61import android.os.Parcelable;
62import android.os.Process;
63import android.os.SystemProperties;
64import android.support.v4.content.FileProvider;
65import android.text.TextUtils;
66import android.text.format.DateUtils;
67import android.util.Log;
68import android.util.Patterns;
69import android.util.SparseArray;
70import android.view.View;
71import android.view.WindowManager;
72import android.view.View.OnFocusChangeListener;
73import android.view.inputmethod.EditorInfo;
74import android.widget.Button;
75import android.widget.EditText;
76import android.widget.Toast;
77
78/**
79 * Service used to keep progress of bugreport processes ({@code dumpstate}).
80 * <p>
81 * The workflow is:
82 * <ol>
83 * <li>When {@code dumpstate} starts, it sends a {@code BUGREPORT_STARTED} with its pid and the
84 * estimated total effort.
85 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service.
86 * <li>Upon start, this service:
87 * <ol>
88 * <li>Issues a system notification so user can watch the progresss (which is 0% initially).
89 * <li>Polls the {@link SystemProperties} for updates on the {@code dumpstate} progress.
90 * <li>If the progress changed, it updates the system notification.
91 * </ol>
92 * <li>As {@code dumpstate} progresses, it updates the system property.
93 * <li>When {@code dumpstate} finishes, it sends a {@code BUGREPORT_FINISHED} intent.
94 * <li>{@link BugreportReceiver} receives the intent and delegates it to this service, which in
95 * turn:
96 * <ol>
97 * <li>Updates the system notification so user can share the bugreport.
98 * <li>Stops monitoring that {@code dumpstate} process.
99 * <li>Stops itself if it doesn't have any process left to monitor.
100 * </ol>
101 * </ol>
102 */
103public class BugreportProgressService extends Service {
104    static final String TAG = "Shell";
105    private static final boolean DEBUG = false;
106
107    private static final String AUTHORITY = "com.android.shell";
108
109    // External intents sent by dumpstate.
110    static final String INTENT_BUGREPORT_STARTED = "android.intent.action.BUGREPORT_STARTED";
111    static final String INTENT_BUGREPORT_FINISHED = "android.intent.action.BUGREPORT_FINISHED";
112
113    // Internal intents used on notification actions.
114    static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL";
115    static final String INTENT_BUGREPORT_SHARE = "android.intent.action.BUGREPORT_SHARE";
116    static final String INTENT_BUGREPORT_INFO_LAUNCH =
117            "android.intent.action.BUGREPORT_INFO_LAUNCH";
118
119    static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
120    static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
121    static final String EXTRA_PID = "android.intent.extra.PID";
122    static final String EXTRA_MAX = "android.intent.extra.MAX";
123    static final String EXTRA_NAME = "android.intent.extra.NAME";
124    static final String EXTRA_TITLE = "android.intent.extra.TITLE";
125    static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION";
126    static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT";
127
128    private static final int MSG_SERVICE_COMMAND = 1;
129    private static final int MSG_POLL = 2;
130
131    /** Polling frequency, in milliseconds. */
132    static final long POLLING_FREQUENCY = 2 * DateUtils.SECOND_IN_MILLIS;
133
134    /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */
135    private static final long INACTIVITY_TIMEOUT = 3 * DateUtils.MINUTE_IN_MILLIS;
136
137    /** System properties used for monitoring progress. */
138    private static final String DUMPSTATE_PREFIX = "dumpstate.";
139    private static final String PROGRESS_SUFFIX = ".progress";
140    private static final String MAX_SUFFIX = ".max";
141    private static final String NAME_SUFFIX = ".name";
142
143    /** System property (and value) used to stop dumpstate. */
144    private static final String CTL_STOP = "ctl.stop";
145    private static final String BUGREPORT_SERVICE = "bugreportplus";
146
147    /** Managed dumpstate processes (keyed by pid) */
148    private final SparseArray<BugreportInfo> mProcesses = new SparseArray<>();
149
150    private Looper mServiceLooper;
151    private ServiceHandler mServiceHandler;
152
153    private final BugreportInfoDialog mInfoDialog = new BugreportInfoDialog();
154
155    @Override
156    public void onCreate() {
157        HandlerThread thread = new HandlerThread("BugreportProgressServiceThread",
158                Process.THREAD_PRIORITY_BACKGROUND);
159        thread.start();
160
161        mServiceLooper = thread.getLooper();
162        mServiceHandler = new ServiceHandler(mServiceLooper);
163    }
164
165    @Override
166    public int onStartCommand(Intent intent, int flags, int startId) {
167        if (intent != null) {
168            // Handle it in a separate thread.
169            Message msg = mServiceHandler.obtainMessage();
170            msg.what = MSG_SERVICE_COMMAND;
171            msg.obj = intent;
172            mServiceHandler.sendMessage(msg);
173        }
174
175        // If service is killed it cannot be recreated because it would not know which
176        // dumpstate PIDs it would have to watch.
177        return START_NOT_STICKY;
178    }
179
180    @Override
181    public IBinder onBind(Intent intent) {
182        return null;
183    }
184
185    @Override
186    public void onDestroy() {
187        mServiceLooper.quit();
188        super.onDestroy();
189    }
190
191    @Override
192    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
193        synchronized (mProcesses) {
194            final int size = mProcesses.size();
195            if (size == 0) {
196                writer.printf("No monitored processes");
197                return;
198            }
199            writer.printf("Monitored dumpstate processes\n");
200            writer.printf("-----------------------------\n");
201            for (int i = 0; i < size; i++) {
202              writer.printf("%s\n", mProcesses.valueAt(i));
203            }
204        }
205    }
206
207    private final class ServiceHandler extends Handler {
208        public ServiceHandler(Looper looper) {
209            super(looper);
210        }
211
212        @Override
213        public void handleMessage(Message msg) {
214            if (msg.what == MSG_POLL) {
215                poll();
216                return;
217            }
218
219            if (msg.what != MSG_SERVICE_COMMAND) {
220                // Sanity check.
221                Log.e(TAG, "Invalid message type: " + msg.what);
222                return;
223            }
224
225            // At this point it's handling onStartCommand(), with the intent passed as an Extra.
226            if (!(msg.obj instanceof Intent)) {
227                // Sanity check.
228                Log.e(TAG, "Internal error: invalid msg.obj: " + msg.obj);
229                return;
230            }
231            final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT);
232            final Intent intent;
233            if (parcel instanceof Intent) {
234                // The real intent was passed to BugreportReceiver, which delegated to the service.
235                intent = (Intent) parcel;
236            } else {
237                intent = (Intent) msg.obj;
238            }
239            final String action = intent.getAction();
240            final int pid = intent.getIntExtra(EXTRA_PID, 0);
241            final int max = intent.getIntExtra(EXTRA_MAX, -1);
242            final String name = intent.getStringExtra(EXTRA_NAME);
243
244            if (DEBUG) Log.v(TAG, "action: " + action + ", name: " + name + ", pid: " + pid
245                    + ", max: "+ max);
246            switch (action) {
247                case INTENT_BUGREPORT_STARTED:
248                    if (!startProgress(name, pid, max)) {
249                        stopSelfWhenDone();
250                        return;
251                    }
252                    poll();
253                    break;
254                case INTENT_BUGREPORT_FINISHED:
255                    if (pid == 0) {
256                        // Shouldn't happen, unless BUGREPORT_FINISHED is received from a legacy,
257                        // out-of-sync dumpstate process.
258                        Log.w(TAG, "Missing " + EXTRA_PID + " on intent " + intent);
259                    }
260                    onBugreportFinished(pid, intent);
261                    break;
262                case INTENT_BUGREPORT_INFO_LAUNCH:
263                    launchBugreportInfoDialog(pid);
264                    break;
265                case INTENT_BUGREPORT_SHARE:
266                    shareBugreport(pid);
267                    break;
268                case INTENT_BUGREPORT_CANCEL:
269                    cancel(pid);
270                    break;
271                default:
272                    Log.w(TAG, "Unsupported intent: " + action);
273            }
274            return;
275
276        }
277
278        private void poll() {
279            if (pollProgress()) {
280                // Keep polling...
281                sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY);
282            } else {
283                Log.i(TAG, "Stopped polling");
284            }
285        }
286    }
287
288    /**
289     * Creates the {@link BugreportInfo} for a process and issue a system notification to
290     * indicate its progress.
291     *
292     * @return whether it succeeded or not.
293     */
294    private boolean startProgress(String name, int pid, int max) {
295        if (name == null) {
296            Log.w(TAG, "Missing " + EXTRA_NAME + " on start intent");
297        }
298        if (pid == -1) {
299            Log.e(TAG, "Missing " + EXTRA_PID + " on start intent");
300            return false;
301        }
302        if (max <= 0) {
303            Log.e(TAG, "Invalid value for extra " + EXTRA_MAX + ": " + max);
304            return false;
305        }
306
307        final BugreportInfo info = new BugreportInfo(getApplicationContext(), pid, name, max);
308        synchronized (mProcesses) {
309            if (mProcesses.indexOfKey(pid) >= 0) {
310                Log.w(TAG, "PID " + pid + " already watched");
311            } else {
312                mProcesses.put(info.pid, info);
313            }
314        }
315        updateProgress(info);
316        return true;
317    }
318
319    /**
320     * Updates the system notification for a given bugreport.
321     */
322    private void updateProgress(BugreportInfo info) {
323        if (info.max <= 0 || info.progress < 0) {
324            Log.e(TAG, "Invalid progress values for " + info);
325            return;
326        }
327
328        final Context context = getApplicationContext();
329        final NumberFormat nf = NumberFormat.getPercentInstance();
330        nf.setMinimumFractionDigits(2);
331        nf.setMaximumFractionDigits(2);
332        final String percentText = nf.format((double) info.progress / info.max);
333        final Action cancelAction = new Action.Builder(null, context.getString(
334                com.android.internal.R.string.cancel), newCancelIntent(context, info)).build();
335        final Intent infoIntent = new Intent(context, BugreportProgressService.class);
336        infoIntent.setAction(INTENT_BUGREPORT_INFO_LAUNCH);
337        infoIntent.putExtra(EXTRA_PID, info.pid);
338        final Action infoAction = new Action.Builder(null,
339                context.getString(R.string.bugreport_info_action),
340                PendingIntent.getService(context, info.pid, infoIntent,
341                        PendingIntent.FLAG_UPDATE_CURRENT)).build();
342
343        final String title = context.getString(R.string.bugreport_in_progress_title);
344        final String name =
345                info.name != null ? info.name : context.getString(R.string.bugreport_unnamed);
346
347        final Notification notification = new Notification.Builder(context)
348                .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
349                .setContentTitle(title)
350                .setTicker(title)
351                .setContentText(name)
352                .setContentInfo(percentText)
353                .setProgress(info.max, info.progress, false)
354                .setOngoing(true)
355                .setLocalOnly(true)
356                .setColor(context.getColor(
357                        com.android.internal.R.color.system_notification_accent_color))
358                .addAction(infoAction)
359                .addAction(cancelAction)
360                .build();
361
362        NotificationManager.from(context).notify(TAG, info.pid, notification);
363    }
364
365    /**
366     * Creates a {@link PendingIntent} for a notification action used to cancel a bugreport.
367     */
368    private static PendingIntent newCancelIntent(Context context, BugreportInfo info) {
369        final Intent intent = new Intent(INTENT_BUGREPORT_CANCEL);
370        intent.setClass(context, BugreportProgressService.class);
371        intent.putExtra(EXTRA_PID, info.pid);
372        return PendingIntent.getService(context, info.pid, intent,
373                PendingIntent.FLAG_UPDATE_CURRENT);
374    }
375
376    /**
377     * Finalizes the progress on a given bugreport and cancel its notification.
378     */
379    private void stopProgress(int pid) {
380        synchronized (mProcesses) {
381            if (mProcesses.indexOfKey(pid) < 0) {
382                Log.w(TAG, "PID not watched: " + pid);
383            } else {
384                mProcesses.remove(pid);
385            }
386            stopSelfWhenDone();
387        }
388        Log.v(TAG, "stopProgress(" + pid + "): cancel notification");
389        NotificationManager.from(getApplicationContext()).cancel(TAG, pid);
390    }
391
392    /**
393     * Cancels a bugreport upon user's request.
394     */
395    private void cancel(int pid) {
396        Log.v(TAG, "cancel: pid=" + pid);
397        synchronized (mProcesses) {
398            BugreportInfo info = mProcesses.get(pid);
399            if (info != null && !info.finished) {
400                Log.i(TAG, "Cancelling bugreport service (pid=" + pid + ") on user's request");
401                setSystemProperty(CTL_STOP, BUGREPORT_SERVICE);
402            }
403        }
404        stopProgress(pid);
405    }
406
407    /**
408     * Poll {@link SystemProperties} to get the progress on each monitored process.
409     *
410     * @return whether it should keep polling.
411     */
412    private boolean pollProgress() {
413        synchronized (mProcesses) {
414            final int total = mProcesses.size();
415            if (total == 0) {
416                Log.d(TAG, "No process to poll progress.");
417            }
418            int activeProcesses = 0;
419            for (int i = 0; i < total; i++) {
420                final int pid = mProcesses.keyAt(i);
421                final BugreportInfo info = mProcesses.valueAt(i);
422                if (info.finished) {
423                    if (DEBUG) Log.v(TAG, "Skipping finished process " + pid);
424                    continue;
425                }
426                activeProcesses++;
427                final String progressKey = DUMPSTATE_PREFIX + pid + PROGRESS_SUFFIX;
428                final int progress = SystemProperties.getInt(progressKey, 0);
429                if (progress == 0) {
430                    Log.v(TAG, "System property " + progressKey + " is not set yet");
431                }
432                final int max = SystemProperties.getInt(DUMPSTATE_PREFIX + pid + MAX_SUFFIX, 0);
433                final boolean maxChanged = max > 0 && max != info.max;
434                final boolean progressChanged = progress > 0 && progress != info.progress;
435
436                if (progressChanged || maxChanged) {
437                    if (progressChanged) {
438                        if (DEBUG) Log.v(TAG, "Updating progress for PID " + pid + " from "
439                                + info.progress + " to " + progress);
440                        info.progress = progress;
441                    }
442                    if (maxChanged) {
443                        Log.i(TAG, "Updating max progress for PID " + pid + " from " + info.max
444                                + " to " + max);
445                        info.max = max;
446                    }
447                    info.lastUpdate = System.currentTimeMillis();
448                    updateProgress(info);
449                } else {
450                    long inactiveTime = System.currentTimeMillis() - info.lastUpdate;
451                    if (inactiveTime >= INACTIVITY_TIMEOUT) {
452                        Log.w(TAG, "No progress update for process " + pid + " since "
453                                + info.getFormattedLastUpdate());
454                        stopProgress(info.pid);
455                    }
456                }
457            }
458            if (DEBUG) Log.v(TAG, "pollProgress() total=" + total + ", actives=" + activeProcesses);
459            return activeProcesses > 0;
460        }
461    }
462
463    /**
464     * Fetches a {@link BugreportInfo} for a given process and launches a dialog where the user can
465     * change its values.
466     */
467    private void launchBugreportInfoDialog(int pid) {
468        // Copy values so it doesn't lock mProcesses while UI is being updated
469        final String name, title, description;
470        synchronized (mProcesses) {
471            final BugreportInfo info = mProcesses.get(pid);
472            if (info == null) {
473                Log.w(TAG, "No bugreport info for PID " + pid);
474                return;
475            }
476            name = info.name;
477            title = info.title;
478            description = info.description;
479        }
480
481        // Closes the notification bar first.
482        sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
483
484        mInfoDialog.initialize(getApplicationContext(), pid, name, title, description);
485    }
486
487    /**
488     * Finishes the service when it's not monitoring any more processes.
489     */
490    private void stopSelfWhenDone() {
491        synchronized (mProcesses) {
492            if (mProcesses.size() > 0) {
493                if (DEBUG) Log.v(TAG, "Staying alive, waiting for pids " + mProcesses);
494                return;
495            }
496            Log.v(TAG, "No more pids to handle, shutting down");
497            stopSelf();
498        }
499    }
500
501    /**
502     * Handles the BUGREPORT_FINISHED intent sent by {@code dumpstate}.
503     */
504    private void onBugreportFinished(int pid, Intent intent) {
505        mInfoDialog.onBugreportFinished(pid);
506        final Context context = getApplicationContext();
507        BugreportInfo info;
508        synchronized (mProcesses) {
509            info = mProcesses.get(pid);
510            if (info == null) {
511                // Happens when BUGREPORT_FINISHED was received without a BUGREPORT_STARTED
512                Log.v(TAG, "Creating info for untracked pid " + pid);
513                info = new BugreportInfo(context, pid);
514                mProcesses.put(pid, info);
515            }
516            info.bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT);
517            info.screenshotFile = getFileExtra(intent, EXTRA_SCREENSHOT);
518            info.finished = true;
519        }
520
521        final Configuration conf = context.getResources().getConfiguration();
522        if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) {
523            triggerLocalNotification(context, info);
524        }
525    }
526
527    /**
528     * Responsible for triggering a notification that allows the user to start a "share" intent with
529     * the bugreport. On watches we have other methods to allow the user to start this intent
530     * (usually by triggering it on another connected device); we don't need to display the
531     * notification in this case.
532     */
533    private static void triggerLocalNotification(final Context context, final BugreportInfo info) {
534        if (!info.bugreportFile.exists() || !info.bugreportFile.canRead()) {
535            Log.e(TAG, "Could not read bugreport file " + info.bugreportFile);
536            Toast.makeText(context, context.getString(R.string.bugreport_unreadable_text),
537                    Toast.LENGTH_LONG).show();
538            return;
539        }
540
541        boolean isPlainText = info.bugreportFile.getName().toLowerCase().endsWith(".txt");
542        if (!isPlainText) {
543            // Already zipped, send it right away.
544            sendBugreportNotification(context, info);
545        } else {
546            // Asynchronously zip the file first, then send it.
547            sendZippedBugreportNotification(context, info);
548        }
549    }
550
551    private static Intent buildWarningIntent(Context context, Intent sendIntent) {
552        final Intent intent = new Intent(context, BugreportWarningActivity.class);
553        intent.putExtra(Intent.EXTRA_INTENT, sendIntent);
554        return intent;
555    }
556
557    /**
558     * Build {@link Intent} that can be used to share the given bugreport.
559     */
560    private static Intent buildSendIntent(Context context, BugreportInfo info) {
561        // Files are kept on private storage, so turn into Uris that we can
562        // grant temporary permissions for.
563        final Uri bugreportUri = getUri(context, info.bugreportFile);
564        final Uri screenshotUri = getUri(context, info.screenshotFile);
565
566        final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
567        final String mimeType = "application/vnd.android.bugreport";
568        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
569        intent.addCategory(Intent.CATEGORY_DEFAULT);
570        intent.setType(mimeType);
571
572        final String subject = info.title != null ? info.title : bugreportUri.getLastPathSegment();
573        intent.putExtra(Intent.EXTRA_SUBJECT, subject);
574
575        // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String.
576        // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually
577        // create the ClipData object with the attachments URIs.
578        StringBuilder messageBody = new StringBuilder("Build info: ")
579            .append(SystemProperties.get("ro.build.description"))
580            .append("\nSerial number: ")
581            .append(SystemProperties.get("ro.serialno"));
582        if (!TextUtils.isEmpty(info.description)) {
583            messageBody.append("\nDescription: ").append(info.description);
584        }
585        intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString());
586        final ClipData clipData = new ClipData(null, new String[] { mimeType },
587                new ClipData.Item(null, null, null, bugreportUri));
588        final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri);
589        if (screenshotUri != null) {
590            clipData.addItem(new ClipData.Item(null, null, null, screenshotUri));
591            attachments.add(screenshotUri);
592        }
593        intent.setClipData(clipData);
594        intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
595
596        final Account sendToAccount = findSendToAccount(context);
597        if (sendToAccount != null) {
598            intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name });
599        }
600
601        return intent;
602    }
603
604    /**
605     * Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE}
606     * intent, but issuing a warning dialog the first time.
607     */
608    private void shareBugreport(int pid) {
609        final Context context = getApplicationContext();
610        final BugreportInfo info;
611        synchronized (mProcesses) {
612            info = mProcesses.get(pid);
613            if (info == null) {
614                // Should not happen, so log if it does...
615                Log.e(TAG, "INTERNAL ERROR: no info for PID " + pid + ": " + mProcesses);
616                return;
617            }
618        }
619        final Intent sendIntent = buildSendIntent(context, info);
620        final Intent notifIntent;
621
622        // Send through warning dialog by default
623        if (getWarningState(context, STATE_SHOW) == STATE_SHOW) {
624            notifIntent = buildWarningIntent(context, sendIntent);
625        } else {
626            notifIntent = sendIntent;
627        }
628        notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
629
630        // Send the share intent...
631        context.startActivity(notifIntent);
632
633        // ... and stop watching this process.
634        stopProgress(pid);
635    }
636
637    /**
638     * Sends a notitication indicating the bugreport has finished so use can share it.
639     */
640    private static void sendBugreportNotification(Context context, BugreportInfo info) {
641        final Intent shareIntent = new Intent(INTENT_BUGREPORT_SHARE);
642        shareIntent.setClass(context, BugreportProgressService.class);
643        shareIntent.setAction(INTENT_BUGREPORT_SHARE);
644        shareIntent.putExtra(EXTRA_PID, info.pid);
645
646        final String title = context.getString(R.string.bugreport_finished_title);
647        final Notification.Builder builder = new Notification.Builder(context)
648                .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
649                .setContentTitle(title)
650                .setTicker(title)
651                .setContentText(context.getString(R.string.bugreport_finished_text))
652                .setContentIntent(PendingIntent.getService(context, info.pid, shareIntent,
653                        PendingIntent.FLAG_UPDATE_CURRENT))
654                .setDeleteIntent(newCancelIntent(context, info))
655                .setLocalOnly(true)
656                .setColor(context.getColor(
657                        com.android.internal.R.color.system_notification_accent_color));
658
659        if (!TextUtils.isEmpty(info.name)) {
660            builder.setContentInfo(info.name);
661        }
662
663        NotificationManager.from(context).notify(TAG, info.pid, builder.build());
664    }
665
666    /**
667     * Sends a zipped bugreport notification.
668     */
669    private static void sendZippedBugreportNotification(final Context context,
670            final BugreportInfo info) {
671        new AsyncTask<Void, Void, Void>() {
672            @Override
673            protected Void doInBackground(Void... params) {
674                info.bugreportFile = zipBugreport(info.bugreportFile);
675                sendBugreportNotification(context, info);
676                return null;
677            }
678        }.execute();
679    }
680
681    /**
682     * Zips a bugreport file, returning the path to the new file (or to the
683     * original in case of failure).
684     */
685    private static File zipBugreport(File bugreportFile) {
686        String bugreportPath = bugreportFile.getAbsolutePath();
687        String zippedPath = bugreportPath.replace(".txt", ".zip");
688        Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath);
689        File bugreportZippedFile = new File(zippedPath);
690        try (InputStream is = new FileInputStream(bugreportFile);
691                ZipOutputStream zos = new ZipOutputStream(
692                        new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) {
693            ZipEntry entry = new ZipEntry(bugreportFile.getName());
694            entry.setTime(bugreportFile.lastModified());
695            zos.putNextEntry(entry);
696            int totalBytes = Streams.copy(is, zos);
697            Log.v(TAG, "size of original bugreport: " + totalBytes + " bytes");
698            zos.closeEntry();
699            // Delete old file;
700            boolean deleted = bugreportFile.delete();
701            if (deleted) {
702                Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")");
703            } else {
704                Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")");
705            }
706            return bugreportZippedFile;
707        } catch (IOException e) {
708            Log.e(TAG, "exception zipping file " + zippedPath, e);
709            return bugreportFile; // Return original.
710        }
711    }
712
713    /**
714     * Find the best matching {@link Account} based on build properties.
715     */
716    private static Account findSendToAccount(Context context) {
717        final AccountManager am = (AccountManager) context.getSystemService(
718                Context.ACCOUNT_SERVICE);
719
720        String preferredDomain = SystemProperties.get("sendbug.preferred.domain");
721        if (!preferredDomain.startsWith("@")) {
722            preferredDomain = "@" + preferredDomain;
723        }
724
725        final Account[] accounts = am.getAccounts();
726        Account foundAccount = null;
727        for (Account account : accounts) {
728            if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
729                if (!preferredDomain.isEmpty()) {
730                    // if we have a preferred domain and it matches, return; otherwise keep
731                    // looking
732                    if (account.name.endsWith(preferredDomain)) {
733                        return account;
734                    } else {
735                        foundAccount = account;
736                    }
737                    // if we don't have a preferred domain, just return since it looks like
738                    // an email address
739                } else {
740                    return account;
741                }
742            }
743        }
744        return foundAccount;
745    }
746
747    private static Uri getUri(Context context, File file) {
748        return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null;
749    }
750
751    static File getFileExtra(Intent intent, String key) {
752        final String path = intent.getStringExtra(key);
753        if (path != null) {
754            return new File(path);
755        } else {
756            return null;
757        }
758    }
759
760    private static boolean setSystemProperty(String key, String value) {
761        try {
762            if (DEBUG) Log.v(TAG, "Setting system property" + key + " to " + value);
763            SystemProperties.set(key, value);
764        } catch (IllegalArgumentException e) {
765            Log.e(TAG, "Could not set property " + key + " to " + value, e);
766            return false;
767        }
768        return true;
769    }
770
771    /**
772     * Updates the system property used by {@code dumpstate} to rename the final bugreport files.
773     */
774    private boolean setBugreportNameProperty(int pid, String name) {
775        Log.d(TAG, "Updating bugreport name to " + name);
776        final String key = DUMPSTATE_PREFIX + pid + NAME_SUFFIX;
777        return setSystemProperty(key, name);
778    }
779
780    /**
781     * Updates the user-provided details of a bugreport.
782     */
783    private void updateBugreportInfo(int pid, String name, String title, String description) {
784        synchronized (mProcesses) {
785            final BugreportInfo info = mProcesses.get(pid);
786            if (info == null) {
787                Log.w(TAG, "No bugreport info for PID " + pid);
788                return;
789            }
790            info.title = title;
791            info.description = description;
792            if (name != null && !info.name.equals(name)) {
793                info.name = name;
794                updateProgress(info);
795            }
796        }
797    }
798
799    /**
800     * Checks whether a character is valid on bugreport names.
801     */
802    @VisibleForTesting
803    static boolean isValid(char c) {
804        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
805                || c == '_' || c == '-';
806    }
807
808    /**
809     * Helper class encapsulating the UI elements and logic used to display a dialog where user
810     * can change the details of a bugreport.
811     */
812    private final class BugreportInfoDialog {
813        private EditText mInfoName;
814        private EditText mInfoTitle;
815        private EditText mInfoDescription;
816        private AlertDialog mDialog;
817        private Button mOkButton;
818        private int mPid;
819
820        /**
821         * Last "committed" value of the bugreport name.
822         * <p>
823         * Once initially set, it's only updated when user clicks the OK button.
824         */
825        private String mSavedName;
826
827        /**
828         * Last value of the bugreport name as entered by the user.
829         * <p>
830         * Every time it's changed the equivalent system property is changed as well, but if the
831         * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored.
832         * <p>
833         * This logic handles the corner-case scenario where {@code dumpstate} finishes after the
834         * user changed the name but didn't clicked OK yet (for example, because the user is typing
835         * the description). The only drawback is that if the user changes the name while
836         * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name
837         * will be the one that has been canceled. But when {@code dumpstate} finishes the {code
838         * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of
839         * such drawback.
840         */
841        private String mTempName;
842
843        /**
844         * Sets its internal state and displays the dialog.
845         */
846        private synchronized void initialize(Context context, int pid, String name, String title,
847                String description) {
848            // First initializes singleton.
849            if (mDialog == null) {
850                @SuppressLint("InflateParams")
851                // It's ok pass null ViewRoot on AlertDialogs.
852                final View view = View.inflate(context, R.layout.dialog_bugreport_info, null);
853
854                mInfoName = (EditText) view.findViewById(R.id.name);
855                mInfoTitle = (EditText) view.findViewById(R.id.title);
856                mInfoDescription = (EditText) view.findViewById(R.id.description);
857
858                mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() {
859
860                    @Override
861                    public void onFocusChange(View v, boolean hasFocus) {
862                        if (hasFocus) {
863                            return;
864                        }
865                        sanitizeName();
866                    }
867                });
868
869                mDialog = new AlertDialog.Builder(context)
870                        .setView(view)
871                        .setTitle(context.getString(R.string.bugreport_info_dialog_title))
872                        .setCancelable(false)
873                        .setPositiveButton(context.getString(com.android.internal.R.string.ok),
874                                null)
875                        .setNegativeButton(context.getString(com.android.internal.R.string.cancel),
876                                new DialogInterface.OnClickListener()
877                                {
878                                    @Override
879                                    public void onClick(DialogInterface dialog, int id)
880                                    {
881                                        if (!mTempName.equals(mSavedName)) {
882                                            // Must restore dumpstate's name since it was changed
883                                            // before user clicked OK.
884                                            setBugreportNameProperty(mPid, mSavedName);
885                                        }
886                                    }
887                                })
888                        .create();
889
890                mDialog.getWindow().setAttributes(
891                        new WindowManager.LayoutParams(
892                                WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG));
893
894            }
895
896            // Then set fields.
897            mSavedName = mTempName = name;
898            mPid = pid;
899            if (!TextUtils.isEmpty(name)) {
900                mInfoName.setText(name);
901            }
902            if (!TextUtils.isEmpty(title)) {
903                mInfoTitle.setText(title);
904            }
905            if (!TextUtils.isEmpty(description)) {
906                mInfoDescription.setText(description);
907            }
908
909            // And finally display it.
910            mDialog.show();
911
912            // TODO: in a traditional AlertDialog, when the positive button is clicked the
913            // dialog is always closed, but we need to validate the name first, so we need to
914            // get a reference to it, which is only available after it's displayed.
915            // It would be cleaner to use a regular dialog instead, but let's keep this
916            // workaround for now and change it later, when we add another button to take
917            // extra screenshots.
918            if (mOkButton == null) {
919                mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
920                mOkButton.setOnClickListener(new View.OnClickListener() {
921
922                    @Override
923                    public void onClick(View view) {
924                        sanitizeName();
925                        final String name = mInfoName.getText().toString();
926                        final String title = mInfoTitle.getText().toString();
927                        final String description = mInfoDescription.getText().toString();
928
929                        updateBugreportInfo(mPid, name, title, description);
930                        mDialog.dismiss();
931                    }
932                });
933            }
934        }
935
936        /**
937         * Sanitizes the user-provided value for the {@code name} field, automatically replacing
938         * invalid characters if necessary.
939         */
940        private synchronized void sanitizeName() {
941            String name = mInfoName.getText().toString();
942            if (name.equals(mTempName)) {
943                if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name);
944                return;
945            }
946            final StringBuilder safeName = new StringBuilder(name.length());
947            boolean changed = false;
948            for (int i = 0; i < name.length(); i++) {
949                final char c = name.charAt(i);
950                if (isValid(c)) {
951                    safeName.append(c);
952                } else {
953                    changed = true;
954                    safeName.append('_');
955                }
956            }
957            if (changed) {
958                Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'");
959                name = safeName.toString();
960                mInfoName.setText(name);
961            }
962            mTempName = name;
963
964            // Must update system property for the cases where dumpstate finishes
965            // while the user is still entering other fields (like title or
966            // description)
967            setBugreportNameProperty(mPid, name);
968        }
969
970       /**
971         * Notifies the dialog that the bugreport has finished so it disables the {@code name}
972         * field.
973         * <p>Once the bugreport is finished dumpstate has already generated the final files, so
974         * changing the name would have no effect.
975         */
976        private synchronized void onBugreportFinished(int pid) {
977            if (mInfoName != null) {
978                mInfoName.setEnabled(false);
979                mInfoName.setText(mSavedName);
980            }
981        }
982
983    }
984
985    /**
986     * Information about a bugreport process while its in progress.
987     */
988    private static final class BugreportInfo {
989        private final Context context;
990
991        /**
992         * {@code pid} of the {@code dumpstate} process generating the bugreport.
993         */
994        final int pid;
995
996        /**
997         * Name of the bugreport, will be used to rename the final files.
998         * <p>
999         * Initial value is the bugreport filename reported by {@code dumpstate}, but user can
1000         * change it later to a more meaningful name.
1001         */
1002        String name;
1003
1004        /**
1005         * User-provided, one-line summary of the bug; when set, will be used as the subject
1006         * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1007         */
1008        String title;
1009
1010        /**
1011         * User-provided, detailed description of the bugreport; when set, will be added to the body
1012         * of the {@link Intent#ACTION_SEND_MULTIPLE} intent.
1013         */
1014        String description;
1015
1016        /**
1017         * Maximum progress of the bugreport generation.
1018         */
1019        int max;
1020
1021        /**
1022         * Current progress of the bugreport generation.
1023         */
1024        int progress;
1025
1026        /**
1027         * Time of the last progress update.
1028         */
1029        long lastUpdate = System.currentTimeMillis();
1030
1031        /**
1032         * Path of the main bugreport file.
1033         */
1034        File bugreportFile;
1035
1036        /**
1037         * Path of the screenshot file.
1038         */
1039        File screenshotFile;
1040
1041        /**
1042         * Whether dumpstate sent an intent informing it has finished.
1043         */
1044        boolean finished;
1045
1046        /**
1047         * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED.
1048         */
1049        BugreportInfo(Context context, int pid, String name, int max) {
1050            this.context = context;
1051            this.pid = pid;
1052            this.name = name;
1053            this.max = max;
1054        }
1055
1056        /**
1057         * Constructor for untracked bugreports - typically called upon receiving BUGREPORT_FINISHED
1058         * without a previous call to BUGREPORT_STARTED.
1059         */
1060        BugreportInfo(Context context, int pid) {
1061            this(context, pid, null, 0);
1062            this.finished = true;
1063        }
1064
1065        String getFormattedLastUpdate() {
1066            return DateUtils.formatDateTime(context, lastUpdate,
1067                    DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
1068        }
1069
1070        @Override
1071        public String toString() {
1072            final float percent = ((float) progress * 100 / max);
1073            return "pid: " + pid + ", name: " + name + ", finished: " + finished
1074                    + "\n\ttitle: " + title + "\n\tdescription: " + description
1075                    + "\n\tfile: " + bugreportFile + "\n\tscreenshot: " + screenshotFile
1076                    + "\n\tprogress: " + progress + "/" + max + "(" + percent + ")"
1077                    + "\n\tlast_update: " + getFormattedLastUpdate();
1078        }
1079    }
1080}
1081