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