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 // Since BugreportProvider and BugreportProgressService aren't tightly coupled, 790 // we need to make sure they are explicitly tied to a single unique notification URI 791 // so that the service can alert the provider of changes it has done (ie. new bug 792 // reports) 793 // See { @link Cursor#setNotificationUri } and {@link ContentResolver#notifyChanges } 794 final Uri notificationUri = BugreportStorageProvider.getNotificationUri(); 795 mContext.getContentResolver().notifyChange(notificationUri, null, false); 796 797 if (bugreportFile == null) { 798 // Should never happen, dumpstate always set the file. 799 Log.wtf(TAG, "Missing " + EXTRA_BUGREPORT + " on intent " + intent); 800 return; 801 } 802 mInfoDialog.onBugreportFinished(); 803 BugreportInfo info = getInfo(id); 804 if (info == null) { 805 // Happens when BUGREPORT_FINISHED was received without a BUGREPORT_STARTED first. 806 Log.v(TAG, "Creating info for untracked ID " + id); 807 info = new BugreportInfo(mContext, id); 808 mProcesses.put(id, new DumpstateListener(info)); 809 } 810 info.renameScreenshots(mScreenshotsDir); 811 info.bugreportFile = bugreportFile; 812 813 final int max = intent.getIntExtra(EXTRA_MAX, -1); 814 if (max != -1) { 815 MetricsLogger.histogram(this, "dumpstate_duration", max); 816 info.max = max; 817 } 818 819 final File screenshot = getFileExtra(intent, EXTRA_SCREENSHOT); 820 if (screenshot != null) { 821 info.addScreenshot(screenshot); 822 } 823 824 final String shareTitle = intent.getStringExtra(EXTRA_TITLE); 825 if (!TextUtils.isEmpty(shareTitle)) { 826 info.title = shareTitle; 827 final String shareDescription = intent.getStringExtra(EXTRA_DESCRIPTION); 828 if (!TextUtils.isEmpty(shareDescription)) { 829 info.shareDescription= shareDescription; 830 } 831 Log.d(TAG, "Bugreport title is " + info.title + "," 832 + " shareDescription is " + info.shareDescription); 833 } 834 info.finished = true; 835 836 // Stop running on foreground, otherwise share notification cannot be dismissed. 837 stopForegroundWhenDone(id); 838 839 triggerLocalNotification(mContext, info); 840 } 841 842 /** 843 * Responsible for triggering a notification that allows the user to start a "share" intent with 844 * the bugreport. On watches we have other methods to allow the user to start this intent 845 * (usually by triggering it on another connected device); we don't need to display the 846 * notification in this case. 847 */ triggerLocalNotification(final Context context, final BugreportInfo info)848 private void triggerLocalNotification(final Context context, final BugreportInfo info) { 849 if (!info.bugreportFile.exists() || !info.bugreportFile.canRead()) { 850 Log.e(TAG, "Could not read bugreport file " + info.bugreportFile); 851 Toast.makeText(context, R.string.bugreport_unreadable_text, Toast.LENGTH_LONG).show(); 852 stopProgress(info.id); 853 return; 854 } 855 856 boolean isPlainText = info.bugreportFile.getName().toLowerCase().endsWith(".txt"); 857 if (!isPlainText) { 858 // Already zipped, send it right away. 859 sendBugreportNotification(info, mTakingScreenshot); 860 } else { 861 // Asynchronously zip the file first, then send it. 862 sendZippedBugreportNotification(info, mTakingScreenshot); 863 } 864 } 865 buildWarningIntent(Context context, Intent sendIntent)866 private static Intent buildWarningIntent(Context context, Intent sendIntent) { 867 final Intent intent = new Intent(context, BugreportWarningActivity.class); 868 intent.putExtra(Intent.EXTRA_INTENT, sendIntent); 869 return intent; 870 } 871 872 /** 873 * Build {@link Intent} that can be used to share the given bugreport. 874 */ buildSendIntent(Context context, BugreportInfo info)875 private static Intent buildSendIntent(Context context, BugreportInfo info) { 876 // Files are kept on private storage, so turn into Uris that we can 877 // grant temporary permissions for. 878 final Uri bugreportUri; 879 try { 880 bugreportUri = getUri(context, info.bugreportFile); 881 } catch (IllegalArgumentException e) { 882 // Should not happen on production, but happens when a Shell is sideloaded and 883 // FileProvider cannot find a configured root for it. 884 Log.wtf(TAG, "Could not get URI for " + info.bugreportFile, e); 885 return null; 886 } 887 888 final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); 889 final String mimeType = "application/vnd.android.bugreport"; 890 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 891 intent.addCategory(Intent.CATEGORY_DEFAULT); 892 intent.setType(mimeType); 893 894 final String subject = !TextUtils.isEmpty(info.title) ? 895 info.title : bugreportUri.getLastPathSegment(); 896 intent.putExtra(Intent.EXTRA_SUBJECT, subject); 897 898 // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String. 899 // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually 900 // create the ClipData object with the attachments URIs. 901 final StringBuilder messageBody = new StringBuilder("Build info: ") 902 .append(SystemProperties.get("ro.build.description")) 903 .append("\nSerial number: ") 904 .append(SystemProperties.get("ro.serialno")); 905 int descriptionLength = 0; 906 if (!TextUtils.isEmpty(info.description)) { 907 messageBody.append("\nDescription: ").append(info.description); 908 descriptionLength = info.description.length(); 909 } 910 intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString()); 911 final ClipData clipData = new ClipData(null, new String[] { mimeType }, 912 new ClipData.Item(null, null, null, bugreportUri)); 913 Log.d(TAG, "share intent: bureportUri=" + bugreportUri); 914 final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri); 915 for (File screenshot : info.screenshotFiles) { 916 final Uri screenshotUri = getUri(context, screenshot); 917 Log.d(TAG, "share intent: screenshotUri=" + screenshotUri); 918 clipData.addItem(new ClipData.Item(null, null, null, screenshotUri)); 919 attachments.add(screenshotUri); 920 } 921 intent.setClipData(clipData); 922 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments); 923 924 final Pair<UserHandle, Account> sendToAccount = findSendToAccount(context, 925 SystemProperties.get("sendbug.preferred.domain")); 926 if (sendToAccount != null) { 927 intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.second.name }); 928 929 // TODO Open the chooser activity on work profile by default. 930 // If we just use startActivityAsUser(), then the launched app couldn't read 931 // attachments. 932 // We probably need to change ChooserActivity to take an extra argument for the 933 // default profile. 934 } 935 936 // Log what was sent to the intent 937 Log.d(TAG, "share intent: EXTRA_SUBJECT=" + subject + ", EXTRA_TEXT=" + messageBody.length() 938 + " chars, description=" + descriptionLength + " chars"); 939 940 return intent; 941 } 942 943 /** 944 * Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE} 945 * intent, but issuing a warning dialog the first time. 946 */ shareBugreport(int id, BugreportInfo sharedInfo)947 private void shareBugreport(int id, BugreportInfo sharedInfo) { 948 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SHARE); 949 BugreportInfo info = getInfo(id); 950 if (info == null) { 951 // Service was terminated but notification persisted 952 info = sharedInfo; 953 Log.d(TAG, "shareBugreport(): no info for ID " + id + " on managed processes (" 954 + mProcesses + "), using info from intent instead (" + info + ")"); 955 } else { 956 Log.v(TAG, "shareBugReport(): id " + id + " info = " + info); 957 } 958 959 addDetailsToZipFile(info); 960 961 final Intent sendIntent = buildSendIntent(mContext, info); 962 if (sendIntent == null) { 963 Log.w(TAG, "Stopping progres on ID " + id + " because share intent could not be built"); 964 stopProgress(id); 965 return; 966 } 967 968 final Intent notifIntent; 969 boolean useChooser = true; 970 971 // Send through warning dialog by default 972 if (getWarningState(mContext, STATE_UNKNOWN) != STATE_HIDE) { 973 notifIntent = buildWarningIntent(mContext, sendIntent); 974 // No need to show a chooser in this case. 975 useChooser = false; 976 } else { 977 notifIntent = sendIntent; 978 } 979 notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 980 981 // Send the share intent... 982 if (useChooser) { 983 sendShareIntent(mContext, notifIntent); 984 } else { 985 mContext.startActivity(notifIntent); 986 } 987 988 // ... and stop watching this process. 989 stopProgress(id); 990 } 991 sendShareIntent(Context context, Intent intent)992 static void sendShareIntent(Context context, Intent intent) { 993 final Intent chooserIntent = Intent.createChooser(intent, 994 context.getResources().getText(R.string.bugreport_intent_chooser_title)); 995 996 // Since we may be launched behind lockscreen, make sure that ChooserActivity doesn't finish 997 // itself in onStop. 998 chooserIntent.putExtra(ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP, true); 999 // Starting the activity from a service. 1000 chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1001 context.startActivity(chooserIntent); 1002 } 1003 1004 /** 1005 * Sends a notification indicating the bugreport has finished so use can share it. 1006 */ sendBugreportNotification(BugreportInfo info, boolean takingScreenshot)1007 private void sendBugreportNotification(BugreportInfo info, boolean takingScreenshot) { 1008 1009 // Since adding the details can take a while, do it before notifying user. 1010 addDetailsToZipFile(info); 1011 1012 final Intent shareIntent = new Intent(INTENT_BUGREPORT_SHARE); 1013 shareIntent.setClass(mContext, BugreportProgressService.class); 1014 shareIntent.setAction(INTENT_BUGREPORT_SHARE); 1015 shareIntent.putExtra(EXTRA_ID, info.id); 1016 shareIntent.putExtra(EXTRA_INFO, info); 1017 1018 String content; 1019 content = takingScreenshot ? 1020 mContext.getString(R.string.bugreport_finished_pending_screenshot_text) 1021 : mContext.getString(R.string.bugreport_finished_text); 1022 final String title; 1023 if (TextUtils.isEmpty(info.title)) { 1024 title = mContext.getString(R.string.bugreport_finished_title, info.id); 1025 } else { 1026 title = info.title; 1027 if (!TextUtils.isEmpty(info.shareDescription)) { 1028 if(!takingScreenshot) content = info.shareDescription; 1029 } 1030 } 1031 1032 final Notification.Builder builder = newBaseNotification(mContext) 1033 .setContentTitle(title) 1034 .setTicker(title) 1035 .setContentText(content) 1036 .setContentIntent(PendingIntent.getService(mContext, info.id, shareIntent, 1037 PendingIntent.FLAG_UPDATE_CURRENT)) 1038 .setDeleteIntent(newCancelIntent(mContext, info)); 1039 1040 if (!TextUtils.isEmpty(info.name)) { 1041 builder.setSubText(info.name); 1042 } 1043 1044 Log.v(TAG, "Sending 'Share' notification for ID " + info.id + ": " + title); 1045 NotificationManager.from(mContext).notify(info.id, builder.build()); 1046 } 1047 1048 /** 1049 * Sends a notification indicating the bugreport is being updated so the user can wait until it 1050 * finishes - at this point there is nothing to be done other than waiting, hence it has no 1051 * pending action. 1052 */ sendBugreportBeingUpdatedNotification(Context context, int id)1053 private void sendBugreportBeingUpdatedNotification(Context context, int id) { 1054 final String title = context.getString(R.string.bugreport_updating_title); 1055 final Notification.Builder builder = newBaseNotification(context) 1056 .setContentTitle(title) 1057 .setTicker(title) 1058 .setContentText(context.getString(R.string.bugreport_updating_wait)); 1059 Log.v(TAG, "Sending 'Updating zip' notification for ID " + id + ": " + title); 1060 sendForegroundabledNotification(id, builder.build()); 1061 } 1062 newBaseNotification(Context context)1063 private static Notification.Builder newBaseNotification(Context context) { 1064 if (sNotificationBundle.isEmpty()) { 1065 // Rename notifcations from "Shell" to "Android System" 1066 sNotificationBundle.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, 1067 context.getString(com.android.internal.R.string.android_system_label)); 1068 } 1069 return new Notification.Builder(context, NOTIFICATION_CHANNEL_ID) 1070 .addExtras(sNotificationBundle) 1071 .setCategory(Notification.CATEGORY_SYSTEM) 1072 .setSmallIcon( 1073 isTv(context) ? R.drawable.ic_bug_report_black_24dp 1074 : com.android.internal.R.drawable.stat_sys_adb) 1075 .setLocalOnly(true) 1076 .setColor(context.getColor( 1077 com.android.internal.R.color.system_notification_accent_color)) 1078 .extend(new Notification.TvExtender()); 1079 } 1080 1081 /** 1082 * Sends a zipped bugreport notification. 1083 */ sendZippedBugreportNotification( final BugreportInfo info, final boolean takingScreenshot)1084 private void sendZippedBugreportNotification( final BugreportInfo info, 1085 final boolean takingScreenshot) { 1086 new AsyncTask<Void, Void, Void>() { 1087 @Override 1088 protected Void doInBackground(Void... params) { 1089 zipBugreport(info); 1090 sendBugreportNotification(info, takingScreenshot); 1091 return null; 1092 } 1093 }.execute(); 1094 } 1095 1096 /** 1097 * Zips a bugreport file, returning the path to the new file (or to the 1098 * original in case of failure). 1099 */ zipBugreport(BugreportInfo info)1100 private static void zipBugreport(BugreportInfo info) { 1101 final String bugreportPath = info.bugreportFile.getAbsolutePath(); 1102 final String zippedPath = bugreportPath.replace(".txt", ".zip"); 1103 Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath); 1104 final File bugreportZippedFile = new File(zippedPath); 1105 try (InputStream is = new FileInputStream(info.bugreportFile); 1106 ZipOutputStream zos = new ZipOutputStream( 1107 new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) { 1108 addEntry(zos, info.bugreportFile.getName(), is); 1109 // Delete old file 1110 final boolean deleted = info.bugreportFile.delete(); 1111 if (deleted) { 1112 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")"); 1113 } else { 1114 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")"); 1115 } 1116 info.bugreportFile = bugreportZippedFile; 1117 } catch (IOException e) { 1118 Log.e(TAG, "exception zipping file " + zippedPath, e); 1119 } 1120 } 1121 1122 /** 1123 * Adds the user-provided info into the bugreport zip file. 1124 * <p> 1125 * If user provided a title, it will be saved into a {@code title.txt} entry; similarly, the 1126 * description will be saved on {@code description.txt}. 1127 */ addDetailsToZipFile(BugreportInfo info)1128 private void addDetailsToZipFile(BugreportInfo info) { 1129 synchronized (mLock) { 1130 addDetailsToZipFileLocked(info); 1131 } 1132 } 1133 addDetailsToZipFileLocked(BugreportInfo info)1134 private void addDetailsToZipFileLocked(BugreportInfo info) { 1135 if (info.bugreportFile == null) { 1136 // One possible reason is a bug in the Parcelization code. 1137 Log.wtf(TAG, "addDetailsToZipFile(): no bugreportFile on " + info); 1138 return; 1139 } 1140 if (TextUtils.isEmpty(info.title) && TextUtils.isEmpty(info.description)) { 1141 Log.d(TAG, "Not touching zip file since neither title nor description are set"); 1142 return; 1143 } 1144 if (info.addedDetailsToZip || info.addingDetailsToZip) { 1145 Log.d(TAG, "Already added details to zip file for " + info); 1146 return; 1147 } 1148 info.addingDetailsToZip = true; 1149 1150 // It's not possible to add a new entry into an existing file, so we need to create a new 1151 // zip, copy all entries, then rename it. 1152 sendBugreportBeingUpdatedNotification(mContext, info.id); // ...and that takes time 1153 1154 final File dir = info.bugreportFile.getParentFile(); 1155 final File tmpZip = new File(dir, "tmp-" + info.bugreportFile.getName()); 1156 Log.d(TAG, "Writing temporary zip file (" + tmpZip + ") with title and/or description"); 1157 try (ZipFile oldZip = new ZipFile(info.bugreportFile); 1158 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip))) { 1159 1160 // First copy contents from original zip. 1161 Enumeration<? extends ZipEntry> entries = oldZip.entries(); 1162 while (entries.hasMoreElements()) { 1163 final ZipEntry entry = entries.nextElement(); 1164 final String entryName = entry.getName(); 1165 if (!entry.isDirectory()) { 1166 addEntry(zos, entryName, entry.getTime(), oldZip.getInputStream(entry)); 1167 } else { 1168 Log.w(TAG, "skipping directory entry: " + entryName); 1169 } 1170 } 1171 1172 // Then add the user-provided info. 1173 addEntry(zos, "title.txt", info.title); 1174 addEntry(zos, "description.txt", info.description); 1175 } catch (IOException e) { 1176 Log.e(TAG, "exception zipping file " + tmpZip, e); 1177 Toast.makeText(mContext, R.string.bugreport_add_details_to_zip_failed, 1178 Toast.LENGTH_LONG).show(); 1179 return; 1180 } finally { 1181 // Make sure it only tries to add details once, even it fails the first time. 1182 info.addedDetailsToZip = true; 1183 info.addingDetailsToZip = false; 1184 stopForegroundWhenDone(info.id); 1185 } 1186 1187 if (!tmpZip.renameTo(info.bugreportFile)) { 1188 Log.e(TAG, "Could not rename " + tmpZip + " to " + info.bugreportFile); 1189 } 1190 } 1191 addEntry(ZipOutputStream zos, String entry, String text)1192 private static void addEntry(ZipOutputStream zos, String entry, String text) 1193 throws IOException { 1194 if (DEBUG) Log.v(TAG, "adding entry '" + entry + "': " + text); 1195 if (!TextUtils.isEmpty(text)) { 1196 addEntry(zos, entry, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))); 1197 } 1198 } 1199 addEntry(ZipOutputStream zos, String entryName, InputStream is)1200 private static void addEntry(ZipOutputStream zos, String entryName, InputStream is) 1201 throws IOException { 1202 addEntry(zos, entryName, System.currentTimeMillis(), is); 1203 } 1204 addEntry(ZipOutputStream zos, String entryName, long timestamp, InputStream is)1205 private static void addEntry(ZipOutputStream zos, String entryName, long timestamp, 1206 InputStream is) throws IOException { 1207 final ZipEntry entry = new ZipEntry(entryName); 1208 entry.setTime(timestamp); 1209 zos.putNextEntry(entry); 1210 final int totalBytes = Streams.copy(is, zos); 1211 if (DEBUG) Log.v(TAG, "size of '" + entryName + "' entry: " + totalBytes + " bytes"); 1212 zos.closeEntry(); 1213 } 1214 1215 /** 1216 * Find the best matching {@link Account} based on build properties. If none found, returns 1217 * the first account that looks like an email address. 1218 */ 1219 @VisibleForTesting findSendToAccount(Context context, String preferredDomain)1220 static Pair<UserHandle, Account> findSendToAccount(Context context, String preferredDomain) { 1221 final UserManager um = context.getSystemService(UserManager.class); 1222 final AccountManager am = context.getSystemService(AccountManager.class); 1223 1224 if (preferredDomain != null && !preferredDomain.startsWith("@")) { 1225 preferredDomain = "@" + preferredDomain; 1226 } 1227 1228 Pair<UserHandle, Account> first = null; 1229 1230 for (UserHandle user : um.getUserProfiles()) { 1231 final Account[] accounts; 1232 try { 1233 accounts = am.getAccountsAsUser(user.getIdentifier()); 1234 } catch (RuntimeException e) { 1235 Log.e(TAG, "Could not get accounts for preferred domain " + preferredDomain 1236 + " for user " + user, e); 1237 continue; 1238 } 1239 if (DEBUG) Log.d(TAG, "User: " + user + " Number of accounts: " + accounts.length); 1240 for (Account account : accounts) { 1241 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { 1242 final Pair<UserHandle, Account> candidate = Pair.create(user, account); 1243 1244 if (!TextUtils.isEmpty(preferredDomain)) { 1245 // if we have a preferred domain and it matches, return; otherwise keep 1246 // looking 1247 if (account.name.endsWith(preferredDomain)) { 1248 return candidate; 1249 } 1250 // if we don't have a preferred domain, just return since it looks like 1251 // an email address 1252 } else { 1253 return candidate; 1254 } 1255 if (first == null) { 1256 first = candidate; 1257 } 1258 } 1259 } 1260 } 1261 return first; 1262 } 1263 getUri(Context context, File file)1264 static Uri getUri(Context context, File file) { 1265 return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null; 1266 } 1267 getFileExtra(Intent intent, String key)1268 static File getFileExtra(Intent intent, String key) { 1269 final String path = intent.getStringExtra(key); 1270 if (path != null) { 1271 return new File(path); 1272 } else { 1273 return null; 1274 } 1275 } 1276 1277 /** 1278 * Dumps an intent, extracting the relevant extras. 1279 */ dumpIntent(Intent intent)1280 static String dumpIntent(Intent intent) { 1281 if (intent == null) { 1282 return "NO INTENT"; 1283 } 1284 String action = intent.getAction(); 1285 if (action == null) { 1286 // Happens when BugreportReceiver calls startService... 1287 action = "no action"; 1288 } 1289 final StringBuilder buffer = new StringBuilder(action).append(" extras: "); 1290 addExtra(buffer, intent, EXTRA_ID); 1291 addExtra(buffer, intent, EXTRA_PID); 1292 addExtra(buffer, intent, EXTRA_MAX); 1293 addExtra(buffer, intent, EXTRA_NAME); 1294 addExtra(buffer, intent, EXTRA_DESCRIPTION); 1295 addExtra(buffer, intent, EXTRA_BUGREPORT); 1296 addExtra(buffer, intent, EXTRA_SCREENSHOT); 1297 addExtra(buffer, intent, EXTRA_INFO); 1298 addExtra(buffer, intent, EXTRA_TITLE); 1299 1300 if (intent.hasExtra(EXTRA_ORIGINAL_INTENT)) { 1301 buffer.append(SHORT_EXTRA_ORIGINAL_INTENT).append(": "); 1302 final Intent originalIntent = intent.getParcelableExtra(EXTRA_ORIGINAL_INTENT); 1303 buffer.append(dumpIntent(originalIntent)); 1304 } else { 1305 buffer.append("no ").append(SHORT_EXTRA_ORIGINAL_INTENT); 1306 } 1307 1308 return buffer.toString(); 1309 } 1310 1311 private static final String SHORT_EXTRA_ORIGINAL_INTENT = 1312 EXTRA_ORIGINAL_INTENT.substring(EXTRA_ORIGINAL_INTENT.lastIndexOf('.') + 1); 1313 addExtra(StringBuilder buffer, Intent intent, String name)1314 private static void addExtra(StringBuilder buffer, Intent intent, String name) { 1315 final String shortName = name.substring(name.lastIndexOf('.') + 1); 1316 if (intent.hasExtra(name)) { 1317 buffer.append(shortName).append('=').append(intent.getExtra(name)); 1318 } else { 1319 buffer.append("no ").append(shortName); 1320 } 1321 buffer.append(", "); 1322 } 1323 setSystemProperty(String key, String value)1324 private static boolean setSystemProperty(String key, String value) { 1325 try { 1326 if (DEBUG) Log.v(TAG, "Setting system property " + key + " to " + value); 1327 SystemProperties.set(key, value); 1328 } catch (IllegalArgumentException e) { 1329 Log.e(TAG, "Could not set property " + key + " to " + value, e); 1330 return false; 1331 } 1332 return true; 1333 } 1334 1335 /** 1336 * Updates the system property used by {@code dumpstate} to rename the final bugreport files. 1337 */ setBugreportNameProperty(int pid, String name)1338 private boolean setBugreportNameProperty(int pid, String name) { 1339 Log.d(TAG, "Updating bugreport name to " + name); 1340 final String key = DUMPSTATE_PREFIX + pid + NAME_SUFFIX; 1341 return setSystemProperty(key, name); 1342 } 1343 1344 /** 1345 * Updates the user-provided details of a bugreport. 1346 */ updateBugreportInfo(int id, String name, String title, String description)1347 private void updateBugreportInfo(int id, String name, String title, String description) { 1348 final BugreportInfo info = getInfo(id); 1349 if (info == null) { 1350 return; 1351 } 1352 if (title != null && !title.equals(info.title)) { 1353 Log.d(TAG, "updating bugreport title: " + title); 1354 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_TITLE_CHANGED); 1355 } 1356 info.title = title; 1357 if (description != null && !description.equals(info.description)) { 1358 Log.d(TAG, "updating bugreport description: " + description.length() + " chars"); 1359 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_DESCRIPTION_CHANGED); 1360 } 1361 info.description = description; 1362 if (name != null && !name.equals(info.name)) { 1363 Log.d(TAG, "updating bugreport name: " + name); 1364 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_NAME_CHANGED); 1365 info.name = name; 1366 updateProgress(info); 1367 } 1368 } 1369 collapseNotificationBar()1370 private void collapseNotificationBar() { 1371 sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); 1372 } 1373 newLooper(String name)1374 private static Looper newLooper(String name) { 1375 final HandlerThread thread = new HandlerThread(name, THREAD_PRIORITY_BACKGROUND); 1376 thread.start(); 1377 return thread.getLooper(); 1378 } 1379 1380 /** 1381 * Takes a screenshot and save it to the given location. 1382 */ takeScreenshot(Context context, String path)1383 private static boolean takeScreenshot(Context context, String path) { 1384 final Bitmap bitmap = Screenshooter.takeScreenshot(); 1385 if (bitmap == null) { 1386 return false; 1387 } 1388 try (final FileOutputStream fos = new FileOutputStream(path)) { 1389 if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)) { 1390 ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(150); 1391 return true; 1392 } else { 1393 Log.e(TAG, "Failed to save screenshot on " + path); 1394 } 1395 } catch (IOException e ) { 1396 Log.e(TAG, "Failed to save screenshot on " + path, e); 1397 return false; 1398 } finally { 1399 bitmap.recycle(); 1400 } 1401 return false; 1402 } 1403 isTv(Context context)1404 private static boolean isTv(Context context) { 1405 return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK); 1406 } 1407 1408 /** 1409 * Checks whether a character is valid on bugreport names. 1410 */ 1411 @VisibleForTesting isValid(char c)1412 static boolean isValid(char c) { 1413 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') 1414 || c == '_' || c == '-'; 1415 } 1416 1417 /** 1418 * Helper class encapsulating the UI elements and logic used to display a dialog where user 1419 * can change the details of a bugreport. 1420 */ 1421 private final class BugreportInfoDialog { 1422 private EditText mInfoName; 1423 private EditText mInfoTitle; 1424 private EditText mInfoDescription; 1425 private AlertDialog mDialog; 1426 private Button mOkButton; 1427 private int mId; 1428 private int mPid; 1429 1430 /** 1431 * Last "committed" value of the bugreport name. 1432 * <p> 1433 * Once initially set, it's only updated when user clicks the OK button. 1434 */ 1435 private String mSavedName; 1436 1437 /** 1438 * Last value of the bugreport name as entered by the user. 1439 * <p> 1440 * Every time it's changed the equivalent system property is changed as well, but if the 1441 * user clicks CANCEL, the old value (stored on {@code mSavedName} is restored. 1442 * <p> 1443 * This logic handles the corner-case scenario where {@code dumpstate} finishes after the 1444 * user changed the name but didn't clicked OK yet (for example, because the user is typing 1445 * the description). The only drawback is that if the user changes the name while 1446 * {@code dumpstate} is running but clicks CANCEL after it finishes, then the final name 1447 * will be the one that has been canceled. But when {@code dumpstate} finishes the {code 1448 * name} UI is disabled and the old name restored anyways, so the user will be "alerted" of 1449 * such drawback. 1450 */ 1451 private String mTempName; 1452 1453 /** 1454 * Sets its internal state and displays the dialog. 1455 */ 1456 @MainThread initialize(final Context context, BugreportInfo info)1457 void initialize(final Context context, BugreportInfo info) { 1458 final String dialogTitle = 1459 context.getString(R.string.bugreport_info_dialog_title, info.id); 1460 // First initializes singleton. 1461 if (mDialog == null) { 1462 @SuppressLint("InflateParams") 1463 // It's ok pass null ViewRoot on AlertDialogs. 1464 final View view = View.inflate(context, R.layout.dialog_bugreport_info, null); 1465 1466 mInfoName = (EditText) view.findViewById(R.id.name); 1467 mInfoTitle = (EditText) view.findViewById(R.id.title); 1468 mInfoDescription = (EditText) view.findViewById(R.id.description); 1469 1470 mInfoName.setOnFocusChangeListener(new OnFocusChangeListener() { 1471 1472 @Override 1473 public void onFocusChange(View v, boolean hasFocus) { 1474 if (hasFocus) { 1475 return; 1476 } 1477 sanitizeName(); 1478 } 1479 }); 1480 1481 mDialog = new AlertDialog.Builder(context) 1482 .setView(view) 1483 .setTitle(dialogTitle) 1484 .setCancelable(true) 1485 .setPositiveButton(context.getString(R.string.save), 1486 null) 1487 .setNegativeButton(context.getString(com.android.internal.R.string.cancel), 1488 new DialogInterface.OnClickListener() 1489 { 1490 @Override 1491 public void onClick(DialogInterface dialog, int id) 1492 { 1493 MetricsLogger.action(context, 1494 MetricsEvent.ACTION_BUGREPORT_DETAILS_CANCELED); 1495 if (!mTempName.equals(mSavedName)) { 1496 // Must restore dumpstate's name since it was changed 1497 // before user clicked OK. 1498 setBugreportNameProperty(mPid, mSavedName); 1499 } 1500 } 1501 }) 1502 .create(); 1503 1504 mDialog.getWindow().setAttributes( 1505 new WindowManager.LayoutParams( 1506 WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG)); 1507 1508 } else { 1509 // Re-use view, but reset fields first. 1510 mDialog.setTitle(dialogTitle); 1511 mInfoName.setText(null); 1512 mInfoName.setEnabled(true); 1513 mInfoTitle.setText(null); 1514 mInfoDescription.setText(null); 1515 } 1516 1517 // Then set fields. 1518 mSavedName = mTempName = info.name; 1519 mId = info.id; 1520 mPid = info.pid; 1521 if (!TextUtils.isEmpty(info.name)) { 1522 mInfoName.setText(info.name); 1523 } 1524 if (!TextUtils.isEmpty(info.title)) { 1525 mInfoTitle.setText(info.title); 1526 } 1527 if (!TextUtils.isEmpty(info.description)) { 1528 mInfoDescription.setText(info.description); 1529 } 1530 1531 // And finally display it. 1532 mDialog.show(); 1533 1534 // TODO: in a traditional AlertDialog, when the positive button is clicked the 1535 // dialog is always closed, but we need to validate the name first, so we need to 1536 // get a reference to it, which is only available after it's displayed. 1537 // It would be cleaner to use a regular dialog instead, but let's keep this 1538 // workaround for now and change it later, when we add another button to take 1539 // extra screenshots. 1540 if (mOkButton == null) { 1541 mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); 1542 mOkButton.setOnClickListener(new View.OnClickListener() { 1543 1544 @Override 1545 public void onClick(View view) { 1546 MetricsLogger.action(context, MetricsEvent.ACTION_BUGREPORT_DETAILS_SAVED); 1547 sanitizeName(); 1548 final String name = mInfoName.getText().toString(); 1549 final String title = mInfoTitle.getText().toString(); 1550 final String description = mInfoDescription.getText().toString(); 1551 1552 updateBugreportInfo(mId, name, title, description); 1553 mDialog.dismiss(); 1554 } 1555 }); 1556 } 1557 } 1558 1559 /** 1560 * Sanitizes the user-provided value for the {@code name} field, automatically replacing 1561 * invalid characters if necessary. 1562 */ sanitizeName()1563 private void sanitizeName() { 1564 String name = mInfoName.getText().toString(); 1565 if (name.equals(mTempName)) { 1566 if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name); 1567 return; 1568 } 1569 final StringBuilder safeName = new StringBuilder(name.length()); 1570 boolean changed = false; 1571 for (int i = 0; i < name.length(); i++) { 1572 final char c = name.charAt(i); 1573 if (isValid(c)) { 1574 safeName.append(c); 1575 } else { 1576 changed = true; 1577 safeName.append('_'); 1578 } 1579 } 1580 if (changed) { 1581 Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'"); 1582 name = safeName.toString(); 1583 mInfoName.setText(name); 1584 } 1585 mTempName = name; 1586 1587 // Must update system property for the cases where dumpstate finishes 1588 // while the user is still entering other fields (like title or 1589 // description) 1590 setBugreportNameProperty(mPid, name); 1591 } 1592 1593 /** 1594 * Notifies the dialog that the bugreport has finished so it disables the {@code name} 1595 * field. 1596 * <p>Once the bugreport is finished dumpstate has already generated the final files, so 1597 * changing the name would have no effect. 1598 */ onBugreportFinished()1599 void onBugreportFinished() { 1600 if (mInfoName != null) { 1601 mInfoName.setEnabled(false); 1602 mInfoName.setText(mSavedName); 1603 } 1604 } 1605 cancel()1606 void cancel() { 1607 if (mDialog != null) { 1608 mDialog.cancel(); 1609 } 1610 } 1611 } 1612 1613 /** 1614 * Information about a bugreport process while its in progress. 1615 */ 1616 private static final class BugreportInfo implements Parcelable { 1617 private final Context context; 1618 1619 /** 1620 * Sequential, user-friendly id used to identify the bugreport. 1621 */ 1622 final int id; 1623 1624 /** 1625 * {@code pid} of the {@code dumpstate} process generating the bugreport. 1626 */ 1627 final int pid; 1628 1629 /** 1630 * Name of the bugreport, will be used to rename the final files. 1631 * <p> 1632 * Initial value is the bugreport filename reported by {@code dumpstate}, but user can 1633 * change it later to a more meaningful name. 1634 */ 1635 String name; 1636 1637 /** 1638 * User-provided, one-line summary of the bug; when set, will be used as the subject 1639 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent. 1640 */ 1641 String title; 1642 1643 /** 1644 * User-provided, detailed description of the bugreport; when set, will be added to the body 1645 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent. 1646 */ 1647 String description; 1648 1649 /** 1650 * Maximum progress of the bugreport generation as displayed by the UI. 1651 */ 1652 int max; 1653 1654 /** 1655 * Current progress of the bugreport generation as displayed by the UI. 1656 */ 1657 int progress; 1658 1659 /** 1660 * Maximum progress of the bugreport generation as reported by dumpstate. 1661 */ 1662 int realMax; 1663 1664 /** 1665 * Current progress of the bugreport generation as reported by dumpstate. 1666 */ 1667 int realProgress; 1668 1669 /** 1670 * Time of the last progress update. 1671 */ 1672 long lastUpdate = System.currentTimeMillis(); 1673 1674 /** 1675 * Time of the last progress update when Parcel was created. 1676 */ 1677 String formattedLastUpdate; 1678 1679 /** 1680 * Path of the main bugreport file. 1681 */ 1682 File bugreportFile; 1683 1684 /** 1685 * Path of the screenshot files. 1686 */ 1687 List<File> screenshotFiles = new ArrayList<>(1); 1688 1689 /** 1690 * Whether dumpstate sent an intent informing it has finished. 1691 */ 1692 boolean finished; 1693 1694 /** 1695 * Whether the details entries have been added to the bugreport yet. 1696 */ 1697 boolean addingDetailsToZip; 1698 boolean addedDetailsToZip; 1699 1700 /** 1701 * Internal counter used to name screenshot files. 1702 */ 1703 int screenshotCounter; 1704 1705 /** 1706 * Descriptive text that will be shown to the user in the notification message. 1707 */ 1708 String shareDescription; 1709 1710 /** 1711 * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_STARTED. 1712 */ BugreportInfo(Context context, int id, int pid, String name, int max)1713 BugreportInfo(Context context, int id, int pid, String name, int max) { 1714 this.context = context; 1715 this.id = id; 1716 this.pid = pid; 1717 this.name = name; 1718 this.max = this.realMax = max; 1719 } 1720 1721 /** 1722 * Constructor for untracked bugreports - typically called upon receiving BUGREPORT_FINISHED 1723 * without a previous call to BUGREPORT_STARTED. 1724 */ BugreportInfo(Context context, int id)1725 BugreportInfo(Context context, int id) { 1726 this(context, id, id, null, 0); 1727 this.finished = true; 1728 } 1729 1730 /** 1731 * Gets the name for next screenshot file. 1732 */ getPathNextScreenshot()1733 String getPathNextScreenshot() { 1734 screenshotCounter ++; 1735 return "screenshot-" + pid + "-" + screenshotCounter + ".png"; 1736 } 1737 1738 /** 1739 * Saves the location of a taken screenshot so it can be sent out at the end. 1740 */ addScreenshot(File screenshot)1741 void addScreenshot(File screenshot) { 1742 screenshotFiles.add(screenshot); 1743 } 1744 1745 /** 1746 * Rename all screenshots files so that they contain the user-generated name instead of pid. 1747 */ renameScreenshots(File screenshotDir)1748 void renameScreenshots(File screenshotDir) { 1749 if (TextUtils.isEmpty(name)) { 1750 return; 1751 } 1752 final List<File> renamedFiles = new ArrayList<>(screenshotFiles.size()); 1753 for (File oldFile : screenshotFiles) { 1754 final String oldName = oldFile.getName(); 1755 final String newName = oldName.replaceFirst(Integer.toString(pid), name); 1756 final File newFile; 1757 if (!newName.equals(oldName)) { 1758 final File renamedFile = new File(screenshotDir, newName); 1759 Log.d(TAG, "Renaming screenshot file " + oldFile + " to " + renamedFile); 1760 newFile = oldFile.renameTo(renamedFile) ? renamedFile : oldFile; 1761 } else { 1762 Log.w(TAG, "Name didn't change: " + oldName); // Shouldn't happen. 1763 newFile = oldFile; 1764 } 1765 renamedFiles.add(newFile); 1766 } 1767 screenshotFiles = renamedFiles; 1768 } 1769 getFormattedLastUpdate()1770 String getFormattedLastUpdate() { 1771 if (context == null) { 1772 // Restored from Parcel 1773 return formattedLastUpdate == null ? 1774 Long.toString(lastUpdate) : formattedLastUpdate; 1775 } 1776 return DateUtils.formatDateTime(context, lastUpdate, 1777 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 1778 } 1779 1780 @Override toString()1781 public String toString() { 1782 final float percent = ((float) progress * 100 / max); 1783 final float realPercent = ((float) realProgress * 100 / realMax); 1784 1785 final StringBuilder builder = new StringBuilder() 1786 .append("\tid: ").append(id) 1787 .append(", pid: ").append(pid) 1788 .append(", name: ").append(name) 1789 .append(", finished: ").append(finished) 1790 .append("\n\ttitle: ").append(title) 1791 .append("\n\tdescription: "); 1792 if (description == null) { 1793 builder.append("null"); 1794 } else { 1795 if (TextUtils.getTrimmedLength(description) == 0) { 1796 builder.append("empty "); 1797 } 1798 builder.append("(").append(description.length()).append(" chars)"); 1799 } 1800 1801 return builder 1802 .append("\n\tfile: ").append(bugreportFile) 1803 .append("\n\tscreenshots: ").append(screenshotFiles) 1804 .append("\n\tprogress: ").append(progress).append("/").append(max) 1805 .append(" (").append(percent).append(")") 1806 .append("\n\treal progress: ").append(realProgress).append("/").append(realMax) 1807 .append(" (").append(realPercent).append(")") 1808 .append("\n\tlast_update: ").append(getFormattedLastUpdate()) 1809 .append("\n\taddingDetailsToZip: ").append(addingDetailsToZip) 1810 .append(" addedDetailsToZip: ").append(addedDetailsToZip) 1811 .append("\n\tshareDescription: ").append(shareDescription) 1812 .toString(); 1813 } 1814 1815 // Parcelable contract BugreportInfo(Parcel in)1816 protected BugreportInfo(Parcel in) { 1817 context = null; 1818 id = in.readInt(); 1819 pid = in.readInt(); 1820 name = in.readString(); 1821 title = in.readString(); 1822 description = in.readString(); 1823 max = in.readInt(); 1824 progress = in.readInt(); 1825 realMax = in.readInt(); 1826 realProgress = in.readInt(); 1827 lastUpdate = in.readLong(); 1828 formattedLastUpdate = in.readString(); 1829 bugreportFile = readFile(in); 1830 1831 int screenshotSize = in.readInt(); 1832 for (int i = 1; i <= screenshotSize; i++) { 1833 screenshotFiles.add(readFile(in)); 1834 } 1835 1836 finished = in.readInt() == 1; 1837 screenshotCounter = in.readInt(); 1838 shareDescription = in.readString(); 1839 } 1840 1841 @Override writeToParcel(Parcel dest, int flags)1842 public void writeToParcel(Parcel dest, int flags) { 1843 dest.writeInt(id); 1844 dest.writeInt(pid); 1845 dest.writeString(name); 1846 dest.writeString(title); 1847 dest.writeString(description); 1848 dest.writeInt(max); 1849 dest.writeInt(progress); 1850 dest.writeInt(realMax); 1851 dest.writeInt(realProgress); 1852 dest.writeLong(lastUpdate); 1853 dest.writeString(getFormattedLastUpdate()); 1854 writeFile(dest, bugreportFile); 1855 1856 dest.writeInt(screenshotFiles.size()); 1857 for (File screenshotFile : screenshotFiles) { 1858 writeFile(dest, screenshotFile); 1859 } 1860 1861 dest.writeInt(finished ? 1 : 0); 1862 dest.writeInt(screenshotCounter); 1863 dest.writeString(shareDescription); 1864 } 1865 1866 @Override describeContents()1867 public int describeContents() { 1868 return 0; 1869 } 1870 writeFile(Parcel dest, File file)1871 private void writeFile(Parcel dest, File file) { 1872 dest.writeString(file == null ? null : file.getPath()); 1873 } 1874 readFile(Parcel in)1875 private File readFile(Parcel in) { 1876 final String path = in.readString(); 1877 return path == null ? null : new File(path); 1878 } 1879 1880 @SuppressWarnings("unused") 1881 public static final Parcelable.Creator<BugreportInfo> CREATOR = 1882 new Parcelable.Creator<BugreportInfo>() { 1883 @Override 1884 public BugreportInfo createFromParcel(Parcel source) { 1885 return new BugreportInfo(source); 1886 } 1887 1888 @Override 1889 public BugreportInfo[] newArray(int size) { 1890 return new BugreportInfo[size]; 1891 } 1892 }; 1893 1894 } 1895 1896 private final class DumpstateListener extends IDumpstateListener.Stub 1897 implements DeathRecipient { 1898 1899 private final BugreportInfo info; 1900 private IDumpstateToken token; 1901 DumpstateListener(BugreportInfo info)1902 DumpstateListener(BugreportInfo info) { 1903 this.info = info; 1904 } 1905 1906 /** 1907 * Connects to the {@code dumpstate} binder to receive updates. 1908 */ connect()1909 boolean connect() { 1910 if (token != null) { 1911 Log.d(TAG, "connect(): " + info.id + " already connected"); 1912 return true; 1913 } 1914 final IBinder service = ServiceManager.getService("dumpstate"); 1915 if (service == null) { 1916 Log.d(TAG, "dumpstate service not bound yet"); 1917 return true; 1918 } 1919 final IDumpstate dumpstate = IDumpstate.Stub.asInterface(service); 1920 try { 1921 token = dumpstate.setListener("Shell", this); 1922 if (token != null) { 1923 token.asBinder().linkToDeath(this, 0); 1924 } 1925 } catch (Exception e) { 1926 Log.e(TAG, "Could not set dumpstate listener: " + e); 1927 } 1928 return token != null; 1929 } 1930 1931 @Override binderDied()1932 public void binderDied() { 1933 if (!info.finished) { 1934 // TODO: linkToDeath() might be called BEFORE Shell received the 1935 // BUGREPORT_FINISHED broadcast, in which case the statements below 1936 // spam logcat (but are harmless). 1937 // The right, long-term solution is to provide an onFinished() callback 1938 // on IDumpstateListener and call it instead of using a broadcast. 1939 Log.w(TAG, "Dumpstate process died:\n" + info); 1940 stopProgress(info.id); 1941 } 1942 token.asBinder().unlinkToDeath(this, 0); 1943 } 1944 1945 @Override onProgressUpdated(int progress)1946 public void onProgressUpdated(int progress) throws RemoteException { 1947 /* 1948 * Checks whether the progress changed in a way that should be displayed to the user: 1949 * - info.progress / info.max represents the displayed progress 1950 * - info.realProgress / info.realMax represents the real progress 1951 * - since the real progress can decrease, the displayed progress is only updated if it 1952 * increases 1953 * - the displayed progress is capped at a maximum (like 99%) 1954 */ 1955 info.realProgress = progress; 1956 final int oldPercentage = (CAPPED_MAX * info.progress) / info.max; 1957 int newPercentage = (CAPPED_MAX * info.realProgress) / info.realMax; 1958 int max = info.realMax; 1959 1960 if (newPercentage > CAPPED_PROGRESS) { 1961 progress = newPercentage = CAPPED_PROGRESS; 1962 max = CAPPED_MAX; 1963 } 1964 1965 if (newPercentage > oldPercentage) { 1966 if (DEBUG) { 1967 if (progress != info.progress) { 1968 Log.v(TAG, "Updating progress for PID " + info.pid + "(id: " + info.id 1969 + ") from " + info.progress + " to " + progress); 1970 } 1971 if (max != info.max) { 1972 Log.v(TAG, "Updating max progress for PID " + info.pid + "(id: " + info.id 1973 + ") from " + info.max + " to " + max); 1974 } 1975 } 1976 info.progress = progress; 1977 info.max = max; 1978 info.lastUpdate = System.currentTimeMillis(); 1979 1980 updateProgress(info); 1981 } 1982 } 1983 1984 @Override onMaxProgressUpdated(int maxProgress)1985 public void onMaxProgressUpdated(int maxProgress) throws RemoteException { 1986 Log.d(TAG, "onMaxProgressUpdated: " + maxProgress); 1987 info.realMax = maxProgress; 1988 } 1989 dump(String prefix, PrintWriter pw)1990 public void dump(String prefix, PrintWriter pw) { 1991 pw.print(prefix); pw.print("token: "); pw.println(token); 1992 } 1993 } 1994 } 1995