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