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