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