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