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