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