BugreportProgressService.java revision 719aaae3c167c2b15525dbe5c7db514a2c0c8269
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 private 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 = "bugreport"; 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 pollProgress(); 184 } 185 186 @Override 187 public void handleMessage(Message msg) { 188 if (msg.what == MSG_POLL) { 189 pollProgress(); 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 /** 247 * Creates the {@link BugreportInfo} for a process and issue a system notification to 248 * indicate its progress. 249 * 250 * @return whether it succeeded or not. 251 */ 252 private boolean startProgress(String name, int pid, int max) { 253 if (name == null) { 254 Log.w(TAG, "Missing " + EXTRA_NAME + " on start intent"); 255 name = "N/A"; 256 } 257 if (pid == -1) { 258 Log.e(TAG, "Missing " + EXTRA_PID + " on start intent"); 259 return false; 260 } 261 if (max <= 0) { 262 Log.e(TAG, "Invalid value for extra " + EXTRA_MAX + ": " + max); 263 return false; 264 } 265 266 final BugreportInfo info = new BugreportInfo(getApplicationContext(), pid, name, max); 267 synchronized (mProcesses) { 268 if (mProcesses.indexOfKey(pid) >= 0) { 269 Log.w(TAG, "PID " + pid + " already watched"); 270 } else { 271 mProcesses.put(info.pid, info); 272 } 273 } 274 updateProgress(info); 275 return true; 276 } 277 278 /** 279 * Updates the system notification for a given bug report. 280 */ 281 private void updateProgress(BugreportInfo info) { 282 if (info.max <= 0 || info.progress < 0 || info.name == null) { 283 Log.e(TAG, "Invalid progress values for " + info); 284 return; 285 } 286 287 final Context context = getApplicationContext(); 288 final NumberFormat nf = NumberFormat.getPercentInstance(); 289 nf.setMinimumFractionDigits(2); 290 nf.setMaximumFractionDigits(2); 291 final String percentText = nf.format((double) info.progress / info.max); 292 293 final Intent cancelIntent = new Intent(context, BugreportReceiver.class); 294 cancelIntent.setAction(INTENT_BUGREPORT_CANCEL); 295 cancelIntent.putExtra(EXTRA_PID, info.pid); 296 final Action cancelAction = new Action.Builder(null, 297 context.getString(com.android.internal.R.string.cancel), 298 PendingIntent.getBroadcast(context, info.pid, cancelIntent, 299 PendingIntent.FLAG_CANCEL_CURRENT)).build(); 300 301 final String title = context.getString(R.string.bugreport_in_progress_title); 302 final Notification notification = new Notification.Builder(context) 303 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) 304 .setContentTitle(title) 305 .setTicker(title) 306 .setContentText(info.name) 307 .setContentInfo(percentText) 308 .setProgress(info.max, info.progress, false) 309 .setOngoing(true) 310 .setLocalOnly(true) 311 .setColor(context.getColor( 312 com.android.internal.R.color.system_notification_accent_color)) 313 .addAction(cancelAction) 314 .build(); 315 316 NotificationManager.from(context).notify(TAG, info.pid, notification); 317 } 318 319 /** 320 * Finalizes the progress on a given process and sends the finished intent. 321 */ 322 private void stopProgress(int pid, Intent intent) { 323 synchronized (mProcesses) { 324 if (mProcesses.indexOfKey(pid) < 0) { 325 Log.w(TAG, "PID not watched: " + pid); 326 } else { 327 mProcesses.remove(pid); 328 } 329 stopSelfWhenDone(); 330 } 331 if (DEBUG) Log.v(TAG, "stopProgress(" + pid + "): cancel notification"); 332 NotificationManager.from(getApplicationContext()).cancel(TAG, pid); 333 if (intent != null) { 334 // Bug report finished fine: send a new, different notification. 335 if (DEBUG) Log.v(TAG, "stopProgress(" + pid + "): finish bug report"); 336 onBugreportFinished(pid, intent); 337 } 338 } 339 340 /** 341 * Cancels a bugreport upon user's request. 342 */ 343 private void cancel(int pid) { 344 Log.i(TAG, "Cancelling PID " + pid + " on user's request"); 345 SystemProperties.set(CTL_STOP, BUGREPORT_SERVICE); 346 stopProgress(pid, null); 347 } 348 349 /** 350 * Poll {@link SystemProperties} to get the progress on each monitored process. 351 */ 352 private void pollProgress() { 353 synchronized (mProcesses) { 354 if (mProcesses.size() == 0) { 355 Log.d(TAG, "No process to poll progress."); 356 } 357 for (int i = 0; i < mProcesses.size(); i++) { 358 final int pid = mProcesses.keyAt(i); 359 final String progressKey = DUMPSTATE_PREFIX + pid + PROGRESS_SUFFIX; 360 final int progress = SystemProperties.getInt(progressKey, 0); 361 if (progress == 0) { 362 Log.v(TAG, "System property " + progressKey + " is not set yet"); 363 continue; 364 } 365 final int max = SystemProperties.getInt(DUMPSTATE_PREFIX + pid + MAX_SUFFIX, 0); 366 final BugreportInfo info = mProcesses.valueAt(i); 367 final boolean maxChanged = max > 0 && max != info.max; 368 final boolean progressChanged = progress > 0 && progress != info.progress; 369 370 if (progressChanged || maxChanged) { 371 if (progressChanged) { 372 if (DEBUG) Log.v(TAG, "Updating progress for PID " + pid + " from " 373 + info.progress + " to " + progress); 374 info.progress = progress; 375 } 376 if (maxChanged) { 377 Log.i(TAG, "Updating max progress for PID " + pid + " from " + info.max 378 + " to " + max); 379 info.max = max; 380 } 381 info.lastUpdate = System.currentTimeMillis(); 382 updateProgress(info); 383 } else { 384 long inactiveTime = System.currentTimeMillis() - info.lastUpdate; 385 if (inactiveTime >= INACTIVITY_TIMEOUT) { 386 Log.w(TAG, "No progress update for process " + pid + " since " 387 + info.getFormattedLastUpdate()); 388 stopProgress(info.pid, null); 389 } 390 } 391 } 392 // Keep polling... 393 sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY); 394 } 395 } 396 397 /** 398 * Finishes the service when it's not monitoring any more processes. 399 */ 400 private void stopSelfWhenDone() { 401 synchronized (mProcesses) { 402 if (mProcesses.size() > 0) { 403 if (DEBUG) Log.v(TAG, "Staying alive, waiting for pids " + mProcesses); 404 return; 405 } 406 Log.v(TAG, "No more pids to handle, shutting down"); 407 stopSelf(); 408 } 409 } 410 411 private void onBugreportFinished(int pid, Intent intent) { 412 final Context context = getApplicationContext(); 413 final Configuration conf = context.getResources().getConfiguration(); 414 final File bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT); 415 final File screenshotFile = getFileExtra(intent, EXTRA_SCREENSHOT); 416 417 if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) { 418 triggerLocalNotification(context, pid, bugreportFile, screenshotFile); 419 } 420 } 421 } 422 423 /** 424 * Responsible for triggering a notification that allows the user to start a "share" intent with 425 * the bug report. On watches we have other methods to allow the user to start this intent 426 * (usually by triggering it on another connected device); we don't need to display the 427 * notification in this case. 428 */ 429 private static void triggerLocalNotification(final Context context, final int pid, 430 final File bugreportFile, final File screenshotFile) { 431 if (!bugreportFile.exists() || !bugreportFile.canRead()) { 432 Log.e(TAG, "Could not read bugreport file " + bugreportFile); 433 Toast.makeText(context, context.getString(R.string.bugreport_unreadable_text), 434 Toast.LENGTH_LONG).show(); 435 return; 436 } 437 438 boolean isPlainText = bugreportFile.getName().toLowerCase().endsWith(".txt"); 439 if (!isPlainText) { 440 // Already zipped, send it right away. 441 sendBugreportNotification(context, pid, bugreportFile, screenshotFile); 442 } else { 443 // Asynchronously zip the file first, then send it. 444 sendZippedBugreportNotification(context, pid, bugreportFile, screenshotFile); 445 } 446 } 447 448 private static Intent buildWarningIntent(Context context, Intent sendIntent) { 449 final Intent intent = new Intent(context, BugreportWarningActivity.class); 450 intent.putExtra(Intent.EXTRA_INTENT, sendIntent); 451 return intent; 452 } 453 454 /** 455 * Build {@link Intent} that can be used to share the given bugreport. 456 */ 457 private static Intent buildSendIntent(Context context, Uri bugreportUri, Uri screenshotUri) { 458 final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); 459 final String mimeType = "application/vnd.android.bugreport"; 460 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 461 intent.addCategory(Intent.CATEGORY_DEFAULT); 462 intent.setType(mimeType); 463 464 intent.putExtra(Intent.EXTRA_SUBJECT, bugreportUri.getLastPathSegment()); 465 466 // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String. 467 // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually 468 // create the ClipData object with the attachments URIs. 469 String messageBody = String.format("Build info: %s\nSerial number:%s", 470 SystemProperties.get("ro.build.description"), SystemProperties.get("ro.serialno")); 471 intent.putExtra(Intent.EXTRA_TEXT, messageBody); 472 final ClipData clipData = new ClipData(null, new String[] { mimeType }, 473 new ClipData.Item(null, null, null, bugreportUri)); 474 final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri); 475 if (screenshotUri != null) { 476 clipData.addItem(new ClipData.Item(null, null, null, screenshotUri)); 477 attachments.add(screenshotUri); 478 } 479 intent.setClipData(clipData); 480 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments); 481 482 final Account sendToAccount = findSendToAccount(context); 483 if (sendToAccount != null) { 484 intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name }); 485 } 486 487 return intent; 488 } 489 490 /** 491 * Sends a bugreport notitication. 492 */ 493 private static void sendBugreportNotification(Context context, int pid, File bugreportFile, 494 File screenshotFile) { 495 // Files are kept on private storage, so turn into Uris that we can 496 // grant temporary permissions for. 497 final Uri bugreportUri = getUri(context, bugreportFile); 498 final Uri screenshotUri = getUri(context, screenshotFile); 499 500 Intent sendIntent = buildSendIntent(context, bugreportUri, screenshotUri); 501 Intent notifIntent; 502 503 // Send through warning dialog by default 504 if (getWarningState(context, STATE_SHOW) == STATE_SHOW) { 505 notifIntent = buildWarningIntent(context, sendIntent); 506 } else { 507 notifIntent = sendIntent; 508 } 509 notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 510 511 final String title = context.getString(R.string.bugreport_finished_title); 512 final Notification.Builder builder = new Notification.Builder(context) 513 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) 514 .setContentTitle(title) 515 .setTicker(title) 516 .setContentText(context.getString(R.string.bugreport_finished_text)) 517 .setContentIntent(PendingIntent.getActivity( 518 context, 0, notifIntent, PendingIntent.FLAG_CANCEL_CURRENT)) 519 .setAutoCancel(true) 520 .setLocalOnly(true) 521 .setColor(context.getColor( 522 com.android.internal.R.color.system_notification_accent_color)); 523 524 NotificationManager.from(context).notify(TAG, pid, builder.build()); 525 } 526 527 /** 528 * Sends a zipped bugreport notification. 529 */ 530 private static void sendZippedBugreportNotification(final Context context, 531 final int pid, final File bugreportFile, final File screenshotFile) { 532 new AsyncTask<Void, Void, Void>() { 533 @Override 534 protected Void doInBackground(Void... params) { 535 File zippedFile = zipBugreport(bugreportFile); 536 sendBugreportNotification(context, pid, zippedFile, screenshotFile); 537 return null; 538 } 539 }.execute(); 540 } 541 542 /** 543 * Zips a bugreport file, returning the path to the new file (or to the 544 * original in case of failure). 545 */ 546 private static File zipBugreport(File bugreportFile) { 547 String bugreportPath = bugreportFile.getAbsolutePath(); 548 String zippedPath = bugreportPath.replace(".txt", ".zip"); 549 Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath); 550 File bugreportZippedFile = new File(zippedPath); 551 try (InputStream is = new FileInputStream(bugreportFile); 552 ZipOutputStream zos = new ZipOutputStream( 553 new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) { 554 ZipEntry entry = new ZipEntry(bugreportFile.getName()); 555 entry.setTime(bugreportFile.lastModified()); 556 zos.putNextEntry(entry); 557 int totalBytes = Streams.copy(is, zos); 558 Log.v(TAG, "size of original bugreport: " + totalBytes + " bytes"); 559 zos.closeEntry(); 560 // Delete old file; 561 boolean deleted = bugreportFile.delete(); 562 if (deleted) { 563 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")"); 564 } else { 565 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")"); 566 } 567 return bugreportZippedFile; 568 } catch (IOException e) { 569 Log.e(TAG, "exception zipping file " + zippedPath, e); 570 return bugreportFile; // Return original. 571 } 572 } 573 574 /** 575 * Find the best matching {@link Account} based on build properties. 576 */ 577 private static Account findSendToAccount(Context context) { 578 final AccountManager am = (AccountManager) context.getSystemService( 579 Context.ACCOUNT_SERVICE); 580 581 String preferredDomain = SystemProperties.get("sendbug.preferred.domain"); 582 if (!preferredDomain.startsWith("@")) { 583 preferredDomain = "@" + preferredDomain; 584 } 585 586 final Account[] accounts = am.getAccounts(); 587 Account foundAccount = null; 588 for (Account account : accounts) { 589 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { 590 if (!preferredDomain.isEmpty()) { 591 // if we have a preferred domain and it matches, return; otherwise keep 592 // looking 593 if (account.name.endsWith(preferredDomain)) { 594 return account; 595 } else { 596 foundAccount = account; 597 } 598 // if we don't have a preferred domain, just return since it looks like 599 // an email address 600 } else { 601 return account; 602 } 603 } 604 } 605 return foundAccount; 606 } 607 608 private static Uri getUri(Context context, File file) { 609 return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null; 610 } 611 612 static File getFileExtra(Intent intent, String key) { 613 final String path = intent.getStringExtra(key); 614 if (path != null) { 615 return new File(path); 616 } else { 617 return null; 618 } 619 } 620 621 /** 622 * Information about a bug report process while its in progress. 623 */ 624 private static final class BugreportInfo { 625 private final Context context; 626 627 /** 628 * {@code pid} of the {@code dumpstate} process generating the bug report. 629 */ 630 final int pid; 631 632 /** 633 * Name of the bug report, will be used to rename the final files. 634 * <p> 635 * Initial value is the bug report filename reported by {@code dumpstate}, but user can 636 * change it later to a more meaningful name. 637 */ 638 String name; 639 640 /** 641 * Maximum progress of the bug report generation. 642 */ 643 int max; 644 645 /** 646 * Current progress of the bug report generation. 647 */ 648 int progress; 649 650 /** 651 * Time of the last progress update. 652 */ 653 long lastUpdate = System.currentTimeMillis(); 654 655 BugreportInfo(Context context, int pid, String name, int max) { 656 this.context = context; 657 this.pid = pid; 658 this.name = name; 659 this.max = max; 660 } 661 662 String getFormattedLastUpdate() { 663 return DateUtils.formatDateTime(context, lastUpdate, 664 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 665 } 666 667 @Override 668 public String toString() { 669 final float percent = ((float) progress * 100 / max); 670 return "Progress for " + name + " (pid=" + pid + "): " + progress + "/" + max 671 + " (" + percent + "%) Last update: " + getFormattedLastUpdate(); 672 } 673 } 674} 675