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