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 in %. 180 private static final int CAPPED_PROGRESS = 99; 181 private static final int CAPPED_MAX = 100; 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(R.drawable.ic_bug_report_black_24dp) 1083 .setLocalOnly(true) 1084 .setColor(context.getColor( 1085 com.android.internal.R.color.system_notification_accent_color)) 1086 .extend(new Notification.TvExtender()); 1087 } 1088 1089 /** 1090 * Sends a zipped bugreport notification. 1091 */ sendZippedBugreportNotification( final BugreportInfo info, final boolean takingScreenshot)1092 private void sendZippedBugreportNotification( final BugreportInfo info, 1093 final boolean takingScreenshot) { 1094 new AsyncTask<Void, Void, Void>() { 1095 @Override 1096 protected Void doInBackground(Void... params) { 1097 Looper.prepare(); 1098 zipBugreport(info); 1099 sendBugreportNotification(info, takingScreenshot); 1100 return null; 1101 } 1102 }.execute(); 1103 } 1104 1105 /** 1106 * Zips a bugreport file, returning the path to the new file (or to the 1107 * original in case of failure). 1108 */ zipBugreport(BugreportInfo info)1109 private static void zipBugreport(BugreportInfo info) { 1110 final String bugreportPath = info.bugreportFile.getAbsolutePath(); 1111 final String zippedPath = bugreportPath.replace(".txt", ".zip"); 1112 Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath); 1113 final File bugreportZippedFile = new File(zippedPath); 1114 try (InputStream is = new FileInputStream(info.bugreportFile); 1115 ZipOutputStream zos = new ZipOutputStream( 1116 new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) { 1117 addEntry(zos, info.bugreportFile.getName(), is); 1118 // Delete old file 1119 final boolean deleted = info.bugreportFile.delete(); 1120 if (deleted) { 1121 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")"); 1122 } else { 1123 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")"); 1124 } 1125 info.bugreportFile = bugreportZippedFile; 1126 } catch (IOException e) { 1127 Log.e(TAG, "exception zipping file " + zippedPath, e); 1128 } 1129 } 1130 1131 /** 1132 * Adds the user-provided info into the bugreport zip file. 1133 * <p> 1134 * If user provided a title, it will be saved into a {@code title.txt} entry; similarly, the 1135 * description will be saved on {@code description.txt}. 1136 */ addDetailsToZipFile(BugreportInfo info)1137 private void addDetailsToZipFile(BugreportInfo info) { 1138 synchronized (mLock) { 1139 addDetailsToZipFileLocked(info); 1140 } 1141 } 1142 addDetailsToZipFileLocked(BugreportInfo info)1143 private void addDetailsToZipFileLocked(BugreportInfo info) { 1144 if (info.bugreportFile == null) { 1145 // One possible reason is a bug in the Parcelization code. 1146 Log.wtf(TAG, "addDetailsToZipFile(): no bugreportFile on " + info); 1147 return; 1148 } 1149 if (TextUtils.isEmpty(info.title) && TextUtils.isEmpty(info.description)) { 1150 Log.d(TAG, "Not touching zip file since neither title nor description are set"); 1151 return; 1152 } 1153 if (info.addedDetailsToZip || info.addingDetailsToZip) { 1154 Log.d(TAG, "Already added details to zip file for " + info); 1155 return; 1156 } 1157 info.addingDetailsToZip = true; 1158 1159 // It's not possible to add a new entry into an existing file, so we need to create a new 1160 // zip, copy all entries, then rename it. 1161 sendBugreportBeingUpdatedNotification(mContext, info.id); // ...and that takes time 1162 1163 final File dir = info.bugreportFile.getParentFile(); 1164 final File tmpZip = new File(dir, "tmp-" + info.bugreportFile.getName()); 1165 Log.d(TAG, "Writing temporary zip file (" + tmpZip + ") with title and/or description"); 1166 try (ZipFile oldZip = new ZipFile(info.bugreportFile); 1167 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip))) { 1168 1169 // First copy contents from original zip. 1170 Enumeration<? extends ZipEntry> entries = oldZip.entries(); 1171 while (entries.hasMoreElements()) { 1172 final ZipEntry entry = entries.nextElement(); 1173 final String entryName = entry.getName(); 1174 if (!entry.isDirectory()) { 1175 addEntry(zos, entryName, entry.getTime(), oldZip.getInputStream(entry)); 1176 } else { 1177 Log.w(TAG, "skipping directory entry: " + entryName); 1178 } 1179 } 1180 1181 // Then add the user-provided info. 1182 addEntry(zos, "title.txt", info.title); 1183 addEntry(zos, "description.txt", info.description); 1184 } catch (IOException e) { 1185 Log.e(TAG, "exception zipping file " + tmpZip, e); 1186 Toast.makeText(mContext, R.string.bugreport_add_details_to_zip_failed, 1187 Toast.LENGTH_LONG).show(); 1188 return; 1189 } finally { 1190 // Make sure it only tries to add details once, even it fails the first time. 1191 info.addedDetailsToZip = true; 1192 info.addingDetailsToZip = false; 1193 stopForegroundWhenDone(info.id); 1194 } 1195 1196 if (!tmpZip.renameTo(info.bugreportFile)) { 1197 Log.e(TAG, "Could not rename " + tmpZip + " to " + info.bugreportFile); 1198 } 1199 } 1200 addEntry(ZipOutputStream zos, String entry, String text)1201 private static void addEntry(ZipOutputStream zos, String entry, String text) 1202 throws IOException { 1203 if (DEBUG) Log.v(TAG, "adding entry '" + entry + "': " + text); 1204 if (!TextUtils.isEmpty(text)) { 1205 addEntry(zos, entry, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))); 1206 } 1207 } 1208 addEntry(ZipOutputStream zos, String entryName, InputStream is)1209 private static void addEntry(ZipOutputStream zos, String entryName, InputStream is) 1210 throws IOException { 1211 addEntry(zos, entryName, System.currentTimeMillis(), is); 1212 } 1213 addEntry(ZipOutputStream zos, String entryName, long timestamp, InputStream is)1214 private static void addEntry(ZipOutputStream zos, String entryName, long timestamp, 1215 InputStream is) throws IOException { 1216 final ZipEntry entry = new ZipEntry(entryName); 1217 entry.setTime(timestamp); 1218 zos.putNextEntry(entry); 1219 final int totalBytes = Streams.copy(is, zos); 1220 if (DEBUG) Log.v(TAG, "size of '" + entryName + "' entry: " + totalBytes + " bytes"); 1221 zos.closeEntry(); 1222 } 1223 1224 /** 1225 * Find the best matching {@link Account} based on build properties. If none found, returns 1226 * the first account that looks like an email address. 1227 */ 1228 @VisibleForTesting findSendToAccount(Context context, String preferredDomain)1229 static Pair<UserHandle, Account> findSendToAccount(Context context, String preferredDomain) { 1230 final UserManager um = context.getSystemService(UserManager.class); 1231 final AccountManager am = context.getSystemService(AccountManager.class); 1232 1233 if (preferredDomain != null && !preferredDomain.startsWith("@")) { 1234 preferredDomain = "@" + preferredDomain; 1235 } 1236 1237 Pair<UserHandle, Account> first = null; 1238 1239 for (UserHandle user : um.getUserProfiles()) { 1240 final Account[] accounts; 1241 try { 1242 accounts = am.getAccountsAsUser(user.getIdentifier()); 1243 } catch (RuntimeException e) { 1244 Log.e(TAG, "Could not get accounts for preferred domain " + preferredDomain 1245 + " for user " + user, e); 1246 continue; 1247 } 1248 if (DEBUG) Log.d(TAG, "User: " + user + " Number of accounts: " + accounts.length); 1249 for (Account account : accounts) { 1250 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { 1251 final Pair<UserHandle, Account> candidate = Pair.create(user, account); 1252 1253 if (!TextUtils.isEmpty(preferredDomain)) { 1254 // if we have a preferred domain and it matches, return; otherwise keep 1255 // looking 1256 if (account.name.endsWith(preferredDomain)) { 1257 return candidate; 1258 } 1259 // if we don't have a preferred domain, just return since it looks like 1260 // an email address 1261 } else { 1262 return candidate; 1263 } 1264 if (first == null) { 1265 first = candidate; 1266 } 1267 } 1268 } 1269 } 1270 return first; 1271 } 1272 getUri(Context context, File file)1273 static Uri getUri(Context context, File file) { 1274 return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null; 1275 } 1276 getFileExtra(Intent intent, String key)1277 static File getFileExtra(Intent intent, String key) { 1278 final String path = intent.getStringExtra(key); 1279 if (path != null) { 1280 return new File(path); 1281 } else { 1282 return null; 1283 } 1284 } 1285 1286 /** 1287 * Dumps an intent, extracting the relevant extras. 1288 */ dumpIntent(Intent intent)1289 static String dumpIntent(Intent intent) { 1290 if (intent == null) { 1291 return "NO INTENT"; 1292 } 1293 String action = intent.getAction(); 1294 if (action == null) { 1295 // Happens when BugreportReceiver calls startService... 1296 action = "no action"; 1297 } 1298 final StringBuilder buffer = new StringBuilder(action).append(" extras: "); 1299 addExtra(buffer, intent, EXTRA_ID); 1300 addExtra(buffer, intent, EXTRA_PID); 1301 addExtra(buffer, intent, EXTRA_MAX); 1302 addExtra(buffer, intent, EXTRA_NAME); 1303 addExtra(buffer, intent, EXTRA_DESCRIPTION); 1304 addExtra(buffer, intent, EXTRA_BUGREPORT); 1305 addExtra(buffer, intent, EXTRA_SCREENSHOT); 1306 addExtra(buffer, intent, EXTRA_INFO); 1307 addExtra(buffer, intent, EXTRA_TITLE); 1308 1309 if (intent.hasExtra(EXTRA_ORIGINAL_INTENT)) { 1310 buffer.append(SHORT_EXTRA_ORIGINAL_INTENT).append(": "); 1311 final Intent originalIntent = intent.getParcelableExtra(EXTRA_ORIGINAL_INTENT); 1312 buffer.append(dumpIntent(originalIntent)); 1313 } else { 1314 buffer.append("no ").append(SHORT_EXTRA_ORIGINAL_INTENT); 1315 } 1316 1317 return buffer.toString(); 1318 } 1319 1320 private static final String SHORT_EXTRA_ORIGINAL_INTENT = 1321 EXTRA_ORIGINAL_INTENT.substring(EXTRA_ORIGINAL_INTENT.lastIndexOf('.') + 1); 1322 addExtra(StringBuilder buffer, Intent intent, String name)1323 private static void addExtra(StringBuilder buffer, Intent intent, String name) { 1324 final String shortName = name.substring(name.lastIndexOf('.') + 1); 1325 if (intent.hasExtra(name)) { 1326 buffer.append(shortName).append('=').append(intent.getExtra(name)); 1327 } else { 1328 buffer.append("no ").append(shortName); 1329 } 1330 buffer.append(", "); 1331 } 1332 setSystemProperty(String key, String value)1333 private static boolean setSystemProperty(String key, String value) { 1334 try { 1335 if (DEBUG) Log.v(TAG, "Setting system property " + key + " to " + value); 1336 SystemProperties.set(key, value); 1337 } catch (IllegalArgumentException e) { 1338 Log.e(TAG, "Could not set property " + key + " to " + value, e); 1339 return false; 1340 } 1341 return true; 1342 } 1343 1344 /** 1345 * Updates the system property used by {@code dumpstate} to rename the final bugreport files. 1346 */ setBugreportNameProperty(int pid, String name)1347 private boolean setBugreportNameProperty(int pid, String name) { 1348 Log.d(TAG, "Updating bugreport name to " + name); 1349 final String key = DUMPSTATE_PREFIX + pid + NAME_SUFFIX; 1350 return setSystemProperty(key, name); 1351 } 1352 1353 /** 1354 * Updates the user-provided details of a bugreport. 1355 */ updateBugreportInfo(int id, String name, String title, String description)1356 private void updateBugreportInfo(int id, String name, String title, String description) { 1357 final BugreportInfo info = getInfo(id); 1358 if (info == null) { 1359 return; 1360 } 1361 if (title != null && !title.equals(info.title)) { 1362 Log.d(TAG, "updating bugreport title: " + title); 1363 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_TITLE_CHANGED); 1364 } 1365 info.title = title; 1366 if (description != null && !description.equals(info.description)) { 1367 Log.d(TAG, "updating bugreport description: " + description.length() + " chars"); 1368 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_DESCRIPTION_CHANGED); 1369 } 1370 info.description = description; 1371 if (name != null && !name.equals(info.name)) { 1372 Log.d(TAG, "updating bugreport name: " + name); 1373 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_NAME_CHANGED); 1374 info.name = name; 1375 updateProgress(info); 1376 } 1377 } 1378 collapseNotificationBar()1379 private void collapseNotificationBar() { 1380 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 1381 } 1382 newLooper(String name)1383 private static Looper newLooper(String name) { 1384 final HandlerThread thread = new HandlerThread(name, THREAD_PRIORITY_BACKGROUND); 1385 thread.start(); 1386 return thread.getLooper(); 1387 } 1388 1389 /** 1390 * Takes a screenshot and save it to the given location. 1391 */ takeScreenshot(Context context, String path)1392 private static boolean takeScreenshot(Context context, String path) { 1393 final Bitmap bitmap = Screenshooter.takeScreenshot(); 1394 if (bitmap == null) { 1395 return false; 1396 } 1397 try (final FileOutputStream fos = new FileOutputStream(path)) { 1398 if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)) { 1399 ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(150); 1400 return true; 1401 } else { 1402 Log.e(TAG, "Failed to save screenshot on " + path); 1403 } 1404 } catch (IOException e ) { 1405 Log.e(TAG, "Failed to save screenshot on " + path, e); 1406 return false; 1407 } finally { 1408 bitmap.recycle(); 1409 } 1410 return false; 1411 } 1412 isTv(Context context)1413 static boolean isTv(Context context) { 1414 return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK); 1415 } 1416 1417 /** 1418 * Checks whether a character is valid on bugreport names. 1419 */ 1420 @VisibleForTesting isValid(char c)1421 static boolean isValid(char c) { 1422 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') 1423 || c == '_' || c == '-'; 1424 } 1425 1426 /** 1427 * Helper class encapsulating the UI elements and logic used to display a dialog where user 1428 * can change the details of a bugreport. 1429 */ 1430 private final class BugreportInfoDialog { 1431 private EditText mInfoName; 1432 private EditText mInfoTitle; 1433 private EditText mInfoDescription; 1434 private AlertDialog mDialog; 1435 private Button mOkButton; 1436 private int mId; 1437 private int mPid; 1438 1439 /** 1440 * Last "committed" value of the bugreport name. 1441 * <p> 1442 * Once initially set, it's only updated when user clicks the OK button. 1443 */ 1444 private String mSavedName; 1445 1446 /** 1447 * Last value of the bugreport name as entered by the user. 1448 * <p> 1449 * Every time it's changed the equivalent system property is changed as well, but if the 1450 * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored. 1451 * <p> 1452 * This logic handles the corner-case scenario where {@code dumpstate} finishes after the 1453 * user changed the name but didn't clicked OK yet (for example, because the user is typing 1454 * the description). The only drawback is that if the user changes the name while 1455 * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name 1456 * will be the one that has been canceled. But when {@code dumpstate} finishes the {code 1457 * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of 1458 * such drawback. 1459 */ 1460 private String mTempName; 1461 1462 /** 1463 * Sets its internal state and displays the dialog. 1464 */ 1465 @MainThread initialize(final Context context, BugreportInfo info)1466 void initialize(final Context context, BugreportInfo info) { 1467 final String dialogTitle = 1468 context.getString(R.string.bugreport_info_dialog_title, info.id); 1469 final Context themedContext = new ContextThemeWrapper( 1470 context, com.android.internal.R.style.Theme_DeviceDefault_DayNight); 1471 // First initializes singleton. 1472 if (mDialog == null) { 1473 @SuppressLint("InflateParams") 1474 // It's ok pass null ViewRoot on AlertDialogs. 1475 final View view = View.inflate(themedContext, R.layout.dialog_bugreport_info, null); 1476 1477 mInfoName = (EditText) view.findViewById(R.id.name); 1478 mInfoTitle = (EditText) view.findViewById(R.id.title); 1479 mInfoDescription = (EditText) view.findViewById(R.id.description); 1480 1481 mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() { 1482 1483 @Override 1484 public void onFocusChange(View v, boolean hasFocus) { 1485 if (hasFocus) { 1486 return; 1487 } 1488 sanitizeName(); 1489 } 1490 }); 1491 1492 mDialog = new AlertDialog.Builder(themedContext) 1493 .setView(view) 1494 .setTitle(dialogTitle) 1495 .setCancelable(true) 1496 .setPositiveButton(context.getString(R.string.save), 1497 null) 1498 .setNegativeButton(context.getString(com.android.internal.R.string.cancel), 1499 new DialogInterface.OnClickListener() 1500 { 1501 @Override 1502 public void onClick(DialogInterface dialog, int id) 1503 { 1504 MetricsLogger.action(context, 1505 MetricsEvent.ACTION_BUGREPORT_DETAILS_CANCELED); 1506 if (!mTempName.equals(mSavedName)) { 1507 // Must restore dumpstate's name since it was changed 1508 // before user clicked OK. 1509 setBugreportNameProperty(mPid, mSavedName); 1510 } 1511 } 1512 }) 1513 .create(); 1514 1515 mDialog.getWindow().setAttributes( 1516 new WindowManager.LayoutParams( 1517 WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG)); 1518 1519 } else { 1520 // Re-use view, but reset fields first. 1521 mDialog.setTitle(dialogTitle); 1522 mInfoName.setText(null); 1523 mInfoName.setEnabled(true); 1524 mInfoTitle.setText(null); 1525 mInfoDescription.setText(null); 1526 } 1527 1528 // Then set fields. 1529 mSavedName = mTempName = info.name; 1530 mId = info.id; 1531 mPid = info.pid; 1532 if (!TextUtils.isEmpty(info.name)) { 1533 mInfoName.setText(info.name); 1534 } 1535 if (!TextUtils.isEmpty(info.title)) { 1536 mInfoTitle.setText(info.title); 1537 } 1538 if (!TextUtils.isEmpty(info.description)) { 1539 mInfoDescription.setText(info.description); 1540 } 1541 1542 // And finally display it. 1543 mDialog.show(); 1544 1545 // TODO: in a traditional AlertDialog, when the positive button is clicked the 1546 // dialog is always closed, but we need to validate the name first, so we need to 1547 // get a reference to it, which is only available after it's displayed. 1548 // It would be cleaner to use a regular dialog instead, but let's keep this 1549 // workaround for now and change it later, when we add another button to take 1550 // extra screenshots. 1551 if (mOkButton == null) { 1552 mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); 1553 mOkButton.setOnClickListener(new View.OnClickListener() { 1554 1555 @Override 1556 public void onClick(View view) { 1557 MetricsLogger.action(context, MetricsEvent.ACTION_BUGREPORT_DETAILS_SAVED); 1558 sanitizeName(); 1559 final String name = mInfoName.getText().toString(); 1560 final String title = mInfoTitle.getText().toString(); 1561 final String description = mInfoDescription.getText().toString(); 1562 1563 updateBugreportInfo(mId, name, title, description); 1564 mDialog.dismiss(); 1565 } 1566 }); 1567 } 1568 } 1569 1570 /** 1571 * Sanitizes the user-provided value for the {@code name} field, automatically replacing 1572 * invalid characters if necessary. 1573 */ sanitizeName()1574 private void sanitizeName() { 1575 String name = mInfoName.getText().toString(); 1576 if (name.equals(mTempName)) { 1577 if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name); 1578 return; 1579 } 1580 final StringBuilder safeName = new StringBuilder(name.length()); 1581 boolean changed = false; 1582 for (int i = 0; i < name.length(); i++) { 1583 final char c = name.charAt(i); 1584 if (isValid(c)) { 1585 safeName.append(c); 1586 } else { 1587 changed = true; 1588 safeName.append('_'); 1589 } 1590 } 1591 if (changed) { 1592 Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'"); 1593 name = safeName.toString(); 1594 mInfoName.setText(name); 1595 } 1596 mTempName = name; 1597 1598 // Must update system property for the cases where dumpstate finishes 1599 // while the user is still entering other fields (like title or 1600 // description) 1601 setBugreportNameProperty(mPid, name); 1602 } 1603 1604 /** 1605 * Notifies the dialog that the bugreport has finished so it disables the {@code name} 1606 * field. 1607 * <p>Once the bugreport is finished dumpstate has already generated the final files, so 1608 * changing the name would have no effect. 1609 */ onBugreportFinished()1610 void onBugreportFinished() { 1611 if (mInfoName != null) { 1612 mInfoName.setEnabled(false); 1613 mInfoName.setText(mSavedName); 1614 } 1615 } 1616 cancel()1617 void cancel() { 1618 if (mDialog != null) { 1619 mDialog.cancel(); 1620 } 1621 } 1622 } 1623 1624 /** 1625 * Information about a bugreport process while its in progress. 1626 */ 1627 private static final class BugreportInfo implements Parcelable { 1628 private final Context context; 1629 1630 /** 1631 * Sequential, user-friendly id used to identify the bugreport. 1632 */ 1633 final int id; 1634 1635 /** 1636 * {@code pid} of the {@code dumpstate} process generating the bugreport. 1637 */ 1638 final int pid; 1639 1640 /** 1641 * Name of the bugreport, will be used to rename the final files. 1642 * <p> 1643 * Initial value is the bugreport filename reported by {@code dumpstate}, but user can 1644 * change it later to a more meaningful name. 1645 */ 1646 String name; 1647 1648 /** 1649 * User-provided, one-line summary of the bug; when set, will be used as the subject 1650 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent. 1651 */ 1652 String title; 1653 1654 /** 1655 * User-provided, detailed description of the bugreport; when set, will be added to the body 1656 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent. 1657 */ 1658 String description; 1659 1660 /** 1661 * Maximum progress of the bugreport generation as displayed by the UI. 1662 */ 1663 int max; 1664 1665 /** 1666 * Current progress of the bugreport generation as displayed by the UI. 1667 */ 1668 int progress; 1669 1670 /** 1671 * Maximum progress of the bugreport generation as reported by dumpstate. 1672 */ 1673 int realMax; 1674 1675 /** 1676 * Current progress of the bugreport generation as reported by dumpstate. 1677 */ 1678 int realProgress; 1679 1680 /** 1681 * Time of the last progress update. 1682 */ 1683 long lastUpdate = System.currentTimeMillis(); 1684 1685 /** 1686 * Time of the last progress update when Parcel was created. 1687 */ 1688 String formattedLastUpdate; 1689 1690 /** 1691 * Path of the main bugreport file. 1692 */ 1693 File bugreportFile; 1694 1695 /** 1696 * Path of the screenshot files. 1697 */ 1698 List<File> screenshotFiles = new ArrayList<>(1); 1699 1700 /** 1701 * Whether dumpstate sent an intent informing it has finished. 1702 */ 1703 boolean finished; 1704 1705 /** 1706 * Whether the details entries have been added to the bugreport yet. 1707 */ 1708 boolean addingDetailsToZip; 1709 boolean addedDetailsToZip; 1710 1711 /** 1712 * Internal counter used to name screenshot files. 1713 */ 1714 int screenshotCounter; 1715 1716 /** 1717 * Descriptive text that will be shown to the user in the notification message. 1718 */ 1719 String shareDescription; 1720 1721 /** 1722 * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED. 1723 */ BugreportInfo(Context context, int id, int pid, String name, int max)1724 BugreportInfo(Context context, int id, int pid, String name, int max) { 1725 this.context = context; 1726 this.id = id; 1727 this.pid = pid; 1728 this.name = name; 1729 this.max = this.realMax = max; 1730 } 1731 1732 /** 1733 * Constructor for untracked bugreports - typically called upon receiving BUGREPORT_FINISHED 1734 * without a previous call to BUGREPORT_STARTED. 1735 */ BugreportInfo(Context context, int id)1736 BugreportInfo(Context context, int id) { 1737 this(context, id, id, null, 0); 1738 this.finished = true; 1739 } 1740 1741 /** 1742 * Gets the name for next screenshot file. 1743 */ getPathNextScreenshot()1744 String getPathNextScreenshot() { 1745 screenshotCounter ++; 1746 return "screenshot-" + pid + "-" + screenshotCounter + ".png"; 1747 } 1748 1749 /** 1750 * Saves the location of a taken screenshot so it can be sent out at the end. 1751 */ addScreenshot(File screenshot)1752 void addScreenshot(File screenshot) { 1753 screenshotFiles.add(screenshot); 1754 } 1755 1756 /** 1757 * Rename all screenshots files so that they contain the user-generated name instead of pid. 1758 */ renameScreenshots(File screenshotDir)1759 void renameScreenshots(File screenshotDir) { 1760 if (TextUtils.isEmpty(name)) { 1761 return; 1762 } 1763 final List<File> renamedFiles = new ArrayList<>(screenshotFiles.size()); 1764 for (File oldFile : screenshotFiles) { 1765 final String oldName = oldFile.getName(); 1766 final String newName = oldName.replaceFirst(Integer.toString(pid), name); 1767 final File newFile; 1768 if (!newName.equals(oldName)) { 1769 final File renamedFile = new File(screenshotDir, newName); 1770 Log.d(TAG, "Renaming screenshot file " + oldFile + " to " + renamedFile); 1771 newFile = oldFile.renameTo(renamedFile) ? renamedFile : oldFile; 1772 } else { 1773 Log.w(TAG, "Name didn't change: " + oldName); // Shouldn't happen. 1774 newFile = oldFile; 1775 } 1776 renamedFiles.add(newFile); 1777 } 1778 screenshotFiles = renamedFiles; 1779 } 1780 getFormattedLastUpdate()1781 String getFormattedLastUpdate() { 1782 if (context == null) { 1783 // Restored from Parcel 1784 return formattedLastUpdate == null ? 1785 Long.toString(lastUpdate) : formattedLastUpdate; 1786 } 1787 return DateUtils.formatDateTime(context, lastUpdate, 1788 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 1789 } 1790 1791 @Override toString()1792 public String toString() { 1793 final float percent = ((float) progress * 100 / max); 1794 final float realPercent = ((float) realProgress * 100 / realMax); 1795 1796 final StringBuilder builder = new StringBuilder() 1797 .append("\tid: ").append(id) 1798 .append(", pid: ").append(pid) 1799 .append(", name: ").append(name) 1800 .append(", finished: ").append(finished) 1801 .append("\n\ttitle: ").append(title) 1802 .append("\n\tdescription: "); 1803 if (description == null) { 1804 builder.append("null"); 1805 } else { 1806 if (TextUtils.getTrimmedLength(description) == 0) { 1807 builder.append("empty "); 1808 } 1809 builder.append("(").append(description.length()).append(" chars)"); 1810 } 1811 1812 return builder 1813 .append("\n\tfile: ").append(bugreportFile) 1814 .append("\n\tscreenshots: ").append(screenshotFiles) 1815 .append("\n\tprogress: ").append(progress).append("/").append(max) 1816 .append(" (").append(percent).append(")") 1817 .append("\n\treal progress: ").append(realProgress).append("/").append(realMax) 1818 .append(" (").append(realPercent).append(")") 1819 .append("\n\tlast_update: ").append(getFormattedLastUpdate()) 1820 .append("\n\taddingDetailsToZip: ").append(addingDetailsToZip) 1821 .append(" addedDetailsToZip: ").append(addedDetailsToZip) 1822 .append("\n\tshareDescription: ").append(shareDescription) 1823 .toString(); 1824 } 1825 1826 // Parcelable contract BugreportInfo(Parcel in)1827 protected BugreportInfo(Parcel in) { 1828 context = null; 1829 id = in.readInt(); 1830 pid = in.readInt(); 1831 name = in.readString(); 1832 title = in.readString(); 1833 description = in.readString(); 1834 max = in.readInt(); 1835 progress = in.readInt(); 1836 realMax = in.readInt(); 1837 realProgress = in.readInt(); 1838 lastUpdate = in.readLong(); 1839 formattedLastUpdate = in.readString(); 1840 bugreportFile = readFile(in); 1841 1842 int screenshotSize = in.readInt(); 1843 for (int i = 1; i <= screenshotSize; i++) { 1844 screenshotFiles.add(readFile(in)); 1845 } 1846 1847 finished = in.readInt() == 1; 1848 screenshotCounter = in.readInt(); 1849 shareDescription = in.readString(); 1850 } 1851 1852 @Override writeToParcel(Parcel dest, int flags)1853 public void writeToParcel(Parcel dest, int flags) { 1854 dest.writeInt(id); 1855 dest.writeInt(pid); 1856 dest.writeString(name); 1857 dest.writeString(title); 1858 dest.writeString(description); 1859 dest.writeInt(max); 1860 dest.writeInt(progress); 1861 dest.writeInt(realMax); 1862 dest.writeInt(realProgress); 1863 dest.writeLong(lastUpdate); 1864 dest.writeString(getFormattedLastUpdate()); 1865 writeFile(dest, bugreportFile); 1866 1867 dest.writeInt(screenshotFiles.size()); 1868 for (File screenshotFile : screenshotFiles) { 1869 writeFile(dest, screenshotFile); 1870 } 1871 1872 dest.writeInt(finished ? 1 : 0); 1873 dest.writeInt(screenshotCounter); 1874 dest.writeString(shareDescription); 1875 } 1876 1877 @Override describeContents()1878 public int describeContents() { 1879 return 0; 1880 } 1881 writeFile(Parcel dest, File file)1882 private void writeFile(Parcel dest, File file) { 1883 dest.writeString(file == null ? null : file.getPath()); 1884 } 1885 readFile(Parcel in)1886 private File readFile(Parcel in) { 1887 final String path = in.readString(); 1888 return path == null ? null : new File(path); 1889 } 1890 1891 @SuppressWarnings("unused") 1892 public static final Parcelable.Creator<BugreportInfo> CREATOR = 1893 new Parcelable.Creator<BugreportInfo>() { 1894 @Override 1895 public BugreportInfo createFromParcel(Parcel source) { 1896 return new BugreportInfo(source); 1897 } 1898 1899 @Override 1900 public BugreportInfo[] newArray(int size) { 1901 return new BugreportInfo[size]; 1902 } 1903 }; 1904 1905 } 1906 1907 private final class DumpstateListener extends IDumpstateListener.Stub 1908 implements DeathRecipient { 1909 1910 private final BugreportInfo info; 1911 private IDumpstateToken token; 1912 DumpstateListener(BugreportInfo info)1913 DumpstateListener(BugreportInfo info) { 1914 this.info = info; 1915 } 1916 1917 /** 1918 * Connects to the {@code dumpstate} binder to receive updates. 1919 */ connect()1920 boolean connect() { 1921 if (token != null) { 1922 Log.d(TAG, "connect(): " + info.id + " already connected"); 1923 return true; 1924 } 1925 final IBinder service = ServiceManager.getService("dumpstate"); 1926 if (service == null) { 1927 Log.d(TAG, "dumpstate service not bound yet"); 1928 return true; 1929 } 1930 final IDumpstate dumpstate = IDumpstate.Stub.asInterface(service); 1931 try { 1932 token = dumpstate.setListener("Shell", this, /* perSectionDetails= */ false); 1933 if (token != null) { 1934 token.asBinder().linkToDeath(this, 0); 1935 } 1936 } catch (Exception e) { 1937 Log.e(TAG, "Could not set dumpstate listener: " + e); 1938 } 1939 return token != null; 1940 } 1941 1942 @Override binderDied()1943 public void binderDied() { 1944 if (!info.finished) { 1945 // TODO: linkToDeath() might be called BEFORE Shell received the 1946 // BUGREPORT_FINISHED broadcast, in which case the statements below 1947 // spam logcat (but are harmless). 1948 // The right, long-term solution is to provide an onFinished() callback 1949 // on IDumpstateListener and call it instead of using a broadcast. 1950 Log.w(TAG, "Dumpstate process died:\n" + info); 1951 stopProgress(info.id); 1952 } 1953 token.asBinder().unlinkToDeath(this, 0); 1954 } 1955 1956 @Override onProgress(int progress)1957 public void onProgress(int progress) throws RemoteException { 1958 if (progress > CAPPED_PROGRESS) { 1959 progress = CAPPED_PROGRESS; 1960 } 1961 updateProgressInfo(progress, CAPPED_MAX); 1962 } 1963 1964 @Override onError(int errorCode)1965 public void onError(int errorCode) throws RemoteException { 1966 // TODO(b/111441001): implement 1967 } 1968 1969 @Override onFinished()1970 public void onFinished() throws RemoteException { 1971 // TODO(b/111441001): implement 1972 } 1973 dump(String prefix, PrintWriter pw)1974 public void dump(String prefix, PrintWriter pw) { 1975 pw.print(prefix); pw.print("token: "); pw.println(token); 1976 } 1977 updateProgressInfo(int progress, int max)1978 private void updateProgressInfo(int progress, int max) { 1979 if (DEBUG) { 1980 if (progress != info.progress) { 1981 Log.v(TAG, "Updating progress for PID " + info.pid + "(id: " + info.id 1982 + ") from " + info.progress + " to " + progress); 1983 } 1984 if (max != info.max) { 1985 Log.v(TAG, "Updating max progress for PID " + info.pid + "(id: " + info.id 1986 + ") from " + info.max + " to " + max); 1987 } 1988 } 1989 info.progress = progress; 1990 info.max = max; 1991 info.lastUpdate = System.currentTimeMillis(); 1992 1993 updateProgress(info); 1994 } 1995 } 1996 } 1997