BugreportProgressService.java revision a5d3cfa9837e63486ed63ef7ad76b5bcac565f1e
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 bugreport 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 bugreport. 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 // External intents sent by dumpstate. 100 static final String INTENT_BUGREPORT_STARTED = "android.intent.action.BUGREPORT_STARTED"; 101 static final String INTENT_BUGREPORT_FINISHED = "android.intent.action.BUGREPORT_FINISHED"; 102 103 // Internal intents used on notification actions. 104 static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL"; 105 static final String INTENT_BUGREPORT_SHARE = "android.intent.action.BUGREPORT_SHARE"; 106 107 static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT"; 108 static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT"; 109 static final String EXTRA_PID = "android.intent.extra.PID"; 110 static final String EXTRA_MAX = "android.intent.extra.MAX"; 111 static final String EXTRA_NAME = "android.intent.extra.NAME"; 112 static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT"; 113 114 private static final int MSG_SERVICE_COMMAND = 1; 115 private static final int MSG_POLL = 2; 116 117 /** Polling frequency, in milliseconds. */ 118 static final long POLLING_FREQUENCY = 2000; 119 120 /** How long (in ms) a dumpstate process will be monitored if it didn't show progress. */ 121 private static final long INACTIVITY_TIMEOUT = 3 * DateUtils.MINUTE_IN_MILLIS; 122 123 /** System properties used for monitoring progress. */ 124 private static final String DUMPSTATE_PREFIX = "dumpstate."; 125 private static final String PROGRESS_SUFFIX = ".progress"; 126 private static final String MAX_SUFFIX = ".max"; 127 128 /** System property (and value) used for stop dumpstate. */ 129 private static final String CTL_STOP = "ctl.stop"; 130 private static final String BUGREPORT_SERVICE = "bugreportplus"; 131 132 /** Managed dumpstate processes (keyed by pid) */ 133 private final SparseArray<BugreportInfo> mProcesses = new SparseArray<>(); 134 135 private Looper mServiceLooper; 136 private ServiceHandler mServiceHandler; 137 138 @Override 139 public void onCreate() { 140 HandlerThread thread = new HandlerThread("BugreportProgressServiceThread", 141 Process.THREAD_PRIORITY_BACKGROUND); 142 thread.start(); 143 144 mServiceLooper = thread.getLooper(); 145 mServiceHandler = new ServiceHandler(mServiceLooper); 146 } 147 148 @Override 149 public int onStartCommand(Intent intent, int flags, int startId) { 150 if (intent != null) { 151 // Handle it in a separate thread. 152 Message msg = mServiceHandler.obtainMessage(); 153 msg.what = MSG_SERVICE_COMMAND; 154 msg.obj = intent; 155 mServiceHandler.sendMessage(msg); 156 } 157 158 // If service is killed it cannot be recreated because it would not know which 159 // dumpstate PIDs it would have to watch. 160 return START_NOT_STICKY; 161 } 162 163 @Override 164 public IBinder onBind(Intent intent) { 165 return null; 166 } 167 168 @Override 169 public void onDestroy() { 170 mServiceLooper.quit(); 171 super.onDestroy(); 172 } 173 174 @Override 175 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 176 synchronized (mProcesses) { 177 final int size = mProcesses.size(); 178 if (size == 0) { 179 writer.printf("No monitored processes"); 180 return; 181 } 182 writer.printf("Monitored dumpstate processes\n"); 183 writer.printf("-----------------------------\n"); 184 for (int i = 0; i < size; i++) { 185 writer.printf("%s\n", mProcesses.valueAt(i)); 186 } 187 } 188 } 189 190 private final class ServiceHandler extends Handler { 191 public ServiceHandler(Looper looper) { 192 super(looper); 193 } 194 195 @Override 196 public void handleMessage(Message msg) { 197 if (msg.what == MSG_POLL) { 198 poll(); 199 return; 200 } 201 202 if (msg.what != MSG_SERVICE_COMMAND) { 203 // Sanity check. 204 Log.e(TAG, "Invalid message type: " + msg.what); 205 return; 206 } 207 208 // At this point it's handling onStartCommand(), with the intent passed as an Extra. 209 if (!(msg.obj instanceof Intent)) { 210 // Sanity check. 211 Log.e(TAG, "Internal error: invalid msg.obj: " + msg.obj); 212 return; 213 } 214 final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT); 215 final Intent intent; 216 if (parcel instanceof Intent) { 217 // The real intent was passed to BugreportReceiver, which delegated to the service. 218 intent = (Intent) parcel; 219 } else { 220 intent = (Intent) msg.obj; 221 } 222 final String action = intent.getAction(); 223 final int pid = intent.getIntExtra(EXTRA_PID, 0); 224 final int max = intent.getIntExtra(EXTRA_MAX, -1); 225 final String name = intent.getStringExtra(EXTRA_NAME); 226 227 if (DEBUG) Log.v(TAG, "action: " + action + ", name: " + name + ", pid: " + pid 228 + ", max: "+ max); 229 switch (action) { 230 case INTENT_BUGREPORT_STARTED: 231 if (!startProgress(name, pid, max)) { 232 stopSelfWhenDone(); 233 return; 234 } 235 poll(); 236 break; 237 case INTENT_BUGREPORT_FINISHED: 238 if (pid == 0) { 239 // Shouldn't happen, unless BUGREPORT_FINISHED is received from a legacy, 240 // out-of-sync dumpstate process. 241 Log.w(TAG, "Missing " + EXTRA_PID + " on intent " + intent); 242 } 243 onBugreportFinished(pid, intent); 244 break; 245 case INTENT_BUGREPORT_SHARE: 246 shareBugreport(pid); 247 break; 248 case INTENT_BUGREPORT_CANCEL: 249 cancel(pid); 250 break; 251 default: 252 Log.w(TAG, "Unsupported intent: " + action); 253 } 254 return; 255 256 } 257 258 private void poll() { 259 if (pollProgress()) { 260 // Keep polling... 261 sendEmptyMessageDelayed(MSG_POLL, POLLING_FREQUENCY); 262 } else { 263 Log.i(TAG, "Stopped polling"); 264 } 265 } 266 } 267 268 /** 269 * Creates the {@link BugreportInfo} for a process and issue a system notification to 270 * indicate its progress. 271 * 272 * @return whether it succeeded or not. 273 */ 274 private boolean startProgress(String name, int pid, int max) { 275 if (name == null) { 276 Log.w(TAG, "Missing " + EXTRA_NAME + " on start intent"); 277 } 278 if (pid == -1) { 279 Log.e(TAG, "Missing " + EXTRA_PID + " on start intent"); 280 return false; 281 } 282 if (max <= 0) { 283 Log.e(TAG, "Invalid value for extra " + EXTRA_MAX + ": " + max); 284 return false; 285 } 286 287 final BugreportInfo info = new BugreportInfo(getApplicationContext(), pid, name, max); 288 synchronized (mProcesses) { 289 if (mProcesses.indexOfKey(pid) >= 0) { 290 Log.w(TAG, "PID " + pid + " already watched"); 291 } else { 292 mProcesses.put(info.pid, info); 293 } 294 } 295 updateProgress(info); 296 return true; 297 } 298 299 /** 300 * Updates the system notification for a given bugreport. 301 */ 302 private void updateProgress(BugreportInfo info) { 303 if (info.max <= 0 || info.progress < 0) { 304 Log.e(TAG, "Invalid progress values for " + info); 305 return; 306 } 307 308 final Context context = getApplicationContext(); 309 final NumberFormat nf = NumberFormat.getPercentInstance(); 310 nf.setMinimumFractionDigits(2); 311 nf.setMaximumFractionDigits(2); 312 final String percentText = nf.format((double) info.progress / info.max); 313 final Action cancelAction = new Action.Builder(null, context.getString( 314 com.android.internal.R.string.cancel), newCancelIntent(context, info)).build(); 315 316 final String title = context.getString(R.string.bugreport_in_progress_title); 317 final String name = 318 info.name != null ? info.name : context.getString(R.string.bugreport_unnamed); 319 320 final Notification notification = new Notification.Builder(context) 321 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) 322 .setContentTitle(title) 323 .setTicker(title) 324 .setContentText(name) 325 .setContentInfo(percentText) 326 .setProgress(info.max, info.progress, false) 327 .setOngoing(true) 328 .setLocalOnly(true) 329 .setColor(context.getColor( 330 com.android.internal.R.color.system_notification_accent_color)) 331 .addAction(cancelAction) 332 .build(); 333 334 NotificationManager.from(context).notify(TAG, info.pid, notification); 335 } 336 337 /** 338 * Creates a {@link PendingIntent} for a notification action used to cancel a bugreport. 339 */ 340 private static PendingIntent newCancelIntent(Context context, BugreportInfo info) { 341 final Intent intent = new Intent(INTENT_BUGREPORT_CANCEL); 342 intent.setClass(context, BugreportProgressService.class); 343 intent.putExtra(EXTRA_PID, info.pid); 344 return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); 345 } 346 347 /** 348 * Finalizes the progress on a given bugreport and cancel its notification. 349 */ 350 private void stopProgress(int pid) { 351 synchronized (mProcesses) { 352 if (mProcesses.indexOfKey(pid) < 0) { 353 Log.w(TAG, "PID not watched: " + pid); 354 } else { 355 mProcesses.remove(pid); 356 } 357 stopSelfWhenDone(); 358 } 359 if (DEBUG) Log.v(TAG, "stopProgress(" + pid + "): cancel notification"); 360 NotificationManager.from(getApplicationContext()).cancel(TAG, pid); 361 } 362 363 /** 364 * Cancels a bugreport upon user's request. 365 */ 366 private void cancel(int pid) { 367 Log.i(TAG, "Cancelling PID " + pid + " on user's request"); 368 SystemProperties.set(CTL_STOP, BUGREPORT_SERVICE); 369 stopProgress(pid); 370 } 371 372 /** 373 * Poll {@link SystemProperties} to get the progress on each monitored process. 374 * 375 * @return whether it should keep polling. 376 */ 377 private boolean pollProgress() { 378 synchronized (mProcesses) { 379 final int total = mProcesses.size(); 380 if (total == 0) { 381 Log.d(TAG, "No process to poll progress."); 382 } 383 int activeProcesses = 0; 384 for (int i = 0; i < total; i++) { 385 final int pid = mProcesses.keyAt(i); 386 final BugreportInfo info = mProcesses.valueAt(i); 387 if (info.finished) { 388 if (DEBUG) Log.v(TAG, "Skipping finished process " + pid); 389 continue; 390 } 391 activeProcesses++; 392 final String progressKey = DUMPSTATE_PREFIX + pid + PROGRESS_SUFFIX; 393 final int progress = SystemProperties.getInt(progressKey, 0); 394 if (progress == 0) { 395 Log.v(TAG, "System property " + progressKey + " is not set yet"); 396 continue; 397 } 398 final int max = SystemProperties.getInt(DUMPSTATE_PREFIX + pid + MAX_SUFFIX, 0); 399 final boolean maxChanged = max > 0 && max != info.max; 400 final boolean progressChanged = progress > 0 && progress != info.progress; 401 402 if (progressChanged || maxChanged) { 403 if (progressChanged) { 404 if (DEBUG) Log.v(TAG, "Updating progress for PID " + pid + " from " 405 + info.progress + " to " + progress); 406 info.progress = progress; 407 } 408 if (maxChanged) { 409 Log.i(TAG, "Updating max progress for PID " + pid + " from " + info.max 410 + " to " + max); 411 info.max = max; 412 } 413 info.lastUpdate = System.currentTimeMillis(); 414 updateProgress(info); 415 } else { 416 long inactiveTime = System.currentTimeMillis() - info.lastUpdate; 417 if (inactiveTime >= INACTIVITY_TIMEOUT) { 418 Log.w(TAG, "No progress update for process " + pid + " since " 419 + info.getFormattedLastUpdate()); 420 stopProgress(info.pid); 421 } 422 } 423 } 424 if (DEBUG) Log.v(TAG, "pollProgress() total=" + total + ", actives=" + activeProcesses); 425 return activeProcesses > 0; 426 } 427 } 428 429 /** 430 * Finishes the service when it's not monitoring any more processes. 431 */ 432 private void stopSelfWhenDone() { 433 synchronized (mProcesses) { 434 if (mProcesses.size() > 0) { 435 if (DEBUG) Log.v(TAG, "Staying alive, waiting for pids " + mProcesses); 436 return; 437 } 438 Log.v(TAG, "No more pids to handle, shutting down"); 439 stopSelf(); 440 } 441 } 442 443 private void onBugreportFinished(int pid, Intent intent) { 444 final Context context = getApplicationContext(); 445 BugreportInfo info; 446 synchronized (mProcesses) { 447 info = mProcesses.get(pid); 448 if (info == null) { 449 // Happens when BUGREPORT_FINISHED was received without a BUGREPORT_STARTED 450 Log.v(TAG, "Creating info for untracked pid " + pid); 451 info = new BugreportInfo(context, pid); 452 mProcesses.put(pid, info); 453 } 454 info.bugreportFile = getFileExtra(intent, EXTRA_BUGREPORT); 455 info.screenshotFile = getFileExtra(intent, EXTRA_SCREENSHOT); 456 } 457 458 final Configuration conf = context.getResources().getConfiguration(); 459 if ((conf.uiMode & Configuration.UI_MODE_TYPE_MASK) != Configuration.UI_MODE_TYPE_WATCH) { 460 triggerLocalNotification(context, info); 461 } 462 } 463 464 /** 465 * Responsible for triggering a notification that allows the user to start a "share" intent with 466 * the bugreport. On watches we have other methods to allow the user to start this intent 467 * (usually by triggering it on another connected device); we don't need to display the 468 * notification in this case. 469 */ 470 private static void triggerLocalNotification(final Context context, final BugreportInfo info) { 471 if (!info.bugreportFile.exists() || !info.bugreportFile.canRead()) { 472 Log.e(TAG, "Could not read bugreport file " + info.bugreportFile); 473 Toast.makeText(context, context.getString(R.string.bugreport_unreadable_text), 474 Toast.LENGTH_LONG).show(); 475 return; 476 } 477 478 boolean isPlainText = info.bugreportFile.getName().toLowerCase().endsWith(".txt"); 479 if (!isPlainText) { 480 // Already zipped, send it right away. 481 sendBugreportNotification(context, info); 482 } else { 483 // Asynchronously zip the file first, then send it. 484 sendZippedBugreportNotification(context, info); 485 } 486 } 487 488 private static Intent buildWarningIntent(Context context, Intent sendIntent) { 489 final Intent intent = new Intent(context, BugreportWarningActivity.class); 490 intent.putExtra(Intent.EXTRA_INTENT, sendIntent); 491 return intent; 492 } 493 494 /** 495 * Build {@link Intent} that can be used to share the given bugreport. 496 */ 497 private static Intent buildSendIntent(Context context, Uri bugreportUri, Uri screenshotUri) { 498 final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); 499 final String mimeType = "application/vnd.android.bugreport"; 500 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 501 intent.addCategory(Intent.CATEGORY_DEFAULT); 502 intent.setType(mimeType); 503 504 intent.putExtra(Intent.EXTRA_SUBJECT, bugreportUri.getLastPathSegment()); 505 506 // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String. 507 // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually 508 // create the ClipData object with the attachments URIs. 509 String messageBody = String.format("Build info: %s\nSerial number:%s", 510 SystemProperties.get("ro.build.description"), SystemProperties.get("ro.serialno")); 511 intent.putExtra(Intent.EXTRA_TEXT, messageBody); 512 final ClipData clipData = new ClipData(null, new String[] { mimeType }, 513 new ClipData.Item(null, null, null, bugreportUri)); 514 final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri); 515 if (screenshotUri != null) { 516 clipData.addItem(new ClipData.Item(null, null, null, screenshotUri)); 517 attachments.add(screenshotUri); 518 } 519 intent.setClipData(clipData); 520 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments); 521 522 final Account sendToAccount = findSendToAccount(context); 523 if (sendToAccount != null) { 524 intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.name }); 525 } 526 527 return intent; 528 } 529 530 /** 531 * Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE} 532 * intent, but issuing a warning dialog the first time. 533 */ 534 private void shareBugreport(int pid) { 535 final Context context = getApplicationContext(); 536 final BugreportInfo info; 537 synchronized (mProcesses) { 538 info = mProcesses.get(pid); 539 if (info == null) { 540 // Should not happen, so log if it does... 541 Log.e(TAG, "INTERNAL ERROR: no info for PID " + pid + ": " + mProcesses); 542 return; 543 } 544 } 545 // Files are kept on private storage, so turn into Uris that we can 546 // grant temporary permissions for. 547 final Uri bugreportUri = getUri(context, info.bugreportFile); 548 final Uri screenshotUri = getUri(context, info.screenshotFile); 549 550 final Intent sendIntent = buildSendIntent(context, bugreportUri, screenshotUri); 551 final Intent notifIntent; 552 553 // Send through warning dialog by default 554 if (getWarningState(context, STATE_SHOW) == STATE_SHOW) { 555 notifIntent = buildWarningIntent(context, sendIntent); 556 } else { 557 notifIntent = sendIntent; 558 } 559 notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 560 561 // Send the share intent... 562 context.startActivity(notifIntent); 563 564 // ... and stop watching this process. 565 stopProgress(pid); 566 } 567 568 /** 569 * Sends a notitication indicating the bugreport has finished so use can share it. 570 */ 571 private static void sendBugreportNotification(Context context, BugreportInfo info) { 572 final Intent shareIntent = new Intent(INTENT_BUGREPORT_SHARE); 573 shareIntent.setClass(context, BugreportProgressService.class); 574 shareIntent.setAction(INTENT_BUGREPORT_SHARE); 575 shareIntent.putExtra(EXTRA_PID, info.pid); 576 577 final String title = context.getString(R.string.bugreport_finished_title); 578 final Notification.Builder builder = new Notification.Builder(context) 579 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) 580 .setContentTitle(title) 581 .setTicker(title) 582 .setContentText(context.getString(R.string.bugreport_finished_text)) 583 .setContentIntent(PendingIntent.getService(context, 0, shareIntent, 584 PendingIntent.FLAG_CANCEL_CURRENT)) 585 .setDeleteIntent(newCancelIntent(context, info)) 586 .setLocalOnly(true) 587 .setColor(context.getColor( 588 com.android.internal.R.color.system_notification_accent_color)); 589 590 NotificationManager.from(context).notify(TAG, info.pid, builder.build()); 591 } 592 593 /** 594 * Sends a zipped bugreport notification. 595 */ 596 private static void sendZippedBugreportNotification(final Context context, 597 final BugreportInfo info) { 598 new AsyncTask<Void, Void, Void>() { 599 @Override 600 protected Void doInBackground(Void... params) { 601 info.bugreportFile = zipBugreport(info.bugreportFile); 602 sendBugreportNotification(context, info); 603 return null; 604 } 605 }.execute(); 606 } 607 608 /** 609 * Zips a bugreport file, returning the path to the new file (or to the 610 * original in case of failure). 611 */ 612 private static File zipBugreport(File bugreportFile) { 613 String bugreportPath = bugreportFile.getAbsolutePath(); 614 String zippedPath = bugreportPath.replace(".txt", ".zip"); 615 Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath); 616 File bugreportZippedFile = new File(zippedPath); 617 try (InputStream is = new FileInputStream(bugreportFile); 618 ZipOutputStream zos = new ZipOutputStream( 619 new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) { 620 ZipEntry entry = new ZipEntry(bugreportFile.getName()); 621 entry.setTime(bugreportFile.lastModified()); 622 zos.putNextEntry(entry); 623 int totalBytes = Streams.copy(is, zos); 624 Log.v(TAG, "size of original bugreport: " + totalBytes + " bytes"); 625 zos.closeEntry(); 626 // Delete old file; 627 boolean deleted = bugreportFile.delete(); 628 if (deleted) { 629 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")"); 630 } else { 631 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")"); 632 } 633 return bugreportZippedFile; 634 } catch (IOException e) { 635 Log.e(TAG, "exception zipping file " + zippedPath, e); 636 return bugreportFile; // Return original. 637 } 638 } 639 640 /** 641 * Find the best matching {@link Account} based on build properties. 642 */ 643 private static Account findSendToAccount(Context context) { 644 final AccountManager am = (AccountManager) context.getSystemService( 645 Context.ACCOUNT_SERVICE); 646 647 String preferredDomain = SystemProperties.get("sendbug.preferred.domain"); 648 if (!preferredDomain.startsWith("@")) { 649 preferredDomain = "@" + preferredDomain; 650 } 651 652 final Account[] accounts = am.getAccounts(); 653 Account foundAccount = null; 654 for (Account account : accounts) { 655 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { 656 if (!preferredDomain.isEmpty()) { 657 // if we have a preferred domain and it matches, return; otherwise keep 658 // looking 659 if (account.name.endsWith(preferredDomain)) { 660 return account; 661 } else { 662 foundAccount = account; 663 } 664 // if we don't have a preferred domain, just return since it looks like 665 // an email address 666 } else { 667 return account; 668 } 669 } 670 } 671 return foundAccount; 672 } 673 674 private static Uri getUri(Context context, File file) { 675 return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null; 676 } 677 678 static File getFileExtra(Intent intent, String key) { 679 final String path = intent.getStringExtra(key); 680 if (path != null) { 681 return new File(path); 682 } else { 683 return null; 684 } 685 } 686 687 /** 688 * Information about a bugreport process while its in progress. 689 */ 690 private static final class BugreportInfo { 691 private final Context context; 692 693 /** 694 * {@code pid} of the {@code dumpstate} process generating the bugreport. 695 */ 696 final int pid; 697 698 /** 699 * Name of the bugreport, will be used to rename the final files. 700 * <p> 701 * Initial value is the bugreport filename reported by {@code dumpstate}, but user can 702 * change it later to a more meaningful name. 703 */ 704 String name; 705 706 /** 707 * Maximum progress of the bugreport generation. 708 */ 709 int max; 710 711 /** 712 * Current progress of the bugreport generation. 713 */ 714 int progress; 715 716 /** 717 * Time of the last progress update. 718 */ 719 long lastUpdate = System.currentTimeMillis(); 720 721 /** 722 * Path of the main bugreport file. 723 */ 724 File bugreportFile; 725 726 /** 727 * Path of the screenshot file. 728 */ 729 File screenshotFile; 730 731 /** 732 * Whether dumpstate sent an intent informing it has finished. 733 */ 734 boolean finished; 735 736 /** 737 * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED. 738 */ 739 BugreportInfo(Context context, int pid, String name, int max) { 740 this.context = context; 741 this.pid = pid; 742 this.name = name; 743 this.max = max; 744 } 745 746 /** 747 * Constructor for untracked bugreports - typically called upon receiving BUGREPORT_FINISHED 748 * without a previous call to BUGREPORT_STARTED. 749 */ 750 BugreportInfo(Context context, int pid) { 751 this(context, pid, null, 0); 752 this.finished = true; 753 } 754 755 String getFormattedLastUpdate() { 756 return DateUtils.formatDateTime(context, lastUpdate, 757 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 758 } 759 760 @Override 761 public String toString() { 762 final float percent = ((float) progress * 100 / max); 763 return "pid: " + pid + ", name: " + name + ", finished: " + finished 764 + "\n\tfile: " + bugreportFile + "\n\tscreenshot: " + screenshotFile 765 + "\n\tprogress: " + progress + "/" + max + "(" + percent + ")" 766 + "\n\tlast_update: " + getFormattedLastUpdate(); 767 } 768 } 769} 770