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