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.app.admin.flags.Flags.onboardingBugreportStorageBugFix; 20 import static android.content.pm.PackageManager.FEATURE_LEANBACK; 21 import static android.content.pm.PackageManager.FEATURE_TELEVISION; 22 import static android.os.Process.THREAD_PRIORITY_BACKGROUND; 23 24 import static com.android.shell.BugreportPrefs.STATE_HIDE; 25 import static com.android.shell.BugreportPrefs.STATE_UNKNOWN; 26 import static com.android.shell.BugreportPrefs.getWarningState; 27 28 import android.accounts.Account; 29 import android.accounts.AccountManager; 30 import android.annotation.MainThread; 31 import android.annotation.Nullable; 32 import android.annotation.SuppressLint; 33 import android.app.ActivityThread; 34 import android.app.AlertDialog; 35 import android.app.Notification; 36 import android.app.Notification.Action; 37 import android.app.NotificationChannel; 38 import android.app.NotificationManager; 39 import android.app.PendingIntent; 40 import android.app.Service; 41 import android.app.admin.DevicePolicyManager; 42 import android.content.ClipData; 43 import android.content.Context; 44 import android.content.DialogInterface; 45 import android.content.Intent; 46 import android.content.pm.PackageManager; 47 import android.content.res.Configuration; 48 import android.graphics.Bitmap; 49 import android.net.Uri; 50 import android.os.AsyncTask; 51 import android.os.Binder; 52 import android.os.BugreportManager; 53 import android.os.BugreportManager.BugreportCallback; 54 import android.os.BugreportParams; 55 import android.os.Bundle; 56 import android.os.FileUtils; 57 import android.os.Handler; 58 import android.os.HandlerThread; 59 import android.os.IBinder; 60 import android.os.Looper; 61 import android.os.Message; 62 import android.os.Parcel; 63 import android.os.ParcelFileDescriptor; 64 import android.os.Parcelable; 65 import android.os.ServiceManager; 66 import android.os.SystemProperties; 67 import android.os.UserHandle; 68 import android.os.UserManager; 69 import android.os.Vibrator; 70 import android.text.TextUtils; 71 import android.text.format.DateUtils; 72 import android.util.Log; 73 import android.util.Pair; 74 import android.util.Patterns; 75 import android.util.PluralsMessageFormatter; 76 import android.util.SparseArray; 77 import android.view.ContextThemeWrapper; 78 import android.view.IWindowManager; 79 import android.view.View; 80 import android.view.WindowManager; 81 import android.widget.Button; 82 import android.widget.EditText; 83 import android.widget.Toast; 84 85 import androidx.core.content.FileProvider; 86 87 import com.android.internal.annotations.GuardedBy; 88 import com.android.internal.annotations.VisibleForTesting; 89 import com.android.internal.app.ChooserActivity; 90 import com.android.internal.logging.MetricsLogger; 91 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 92 93 import libcore.io.Streams; 94 95 import com.google.android.collect.Lists; 96 97 import java.io.BufferedOutputStream; 98 import java.io.ByteArrayInputStream; 99 import java.io.File; 100 import java.io.FileDescriptor; 101 import java.io.FileInputStream; 102 import java.io.FileNotFoundException; 103 import java.io.FileOutputStream; 104 import java.io.IOException; 105 import java.io.InputStream; 106 import java.io.PrintWriter; 107 import java.nio.charset.StandardCharsets; 108 import java.security.MessageDigest; 109 import java.security.NoSuchAlgorithmException; 110 import java.text.NumberFormat; 111 import java.text.SimpleDateFormat; 112 import java.util.ArrayList; 113 import java.util.Arrays; 114 import java.util.Comparator; 115 import java.util.Date; 116 import java.util.Enumeration; 117 import java.util.HashMap; 118 import java.util.List; 119 import java.util.Map; 120 import java.util.concurrent.Executor; 121 import java.util.concurrent.atomic.AtomicBoolean; 122 import java.util.concurrent.atomic.AtomicInteger; 123 import java.util.concurrent.atomic.AtomicLong; 124 import java.util.zip.ZipEntry; 125 import java.util.zip.ZipFile; 126 import java.util.zip.ZipOutputStream; 127 128 /** 129 * Service used to trigger system bugreports. 130 * <p> 131 * The workflow uses Bugreport API({@code BugreportManager}) and is as follows: 132 * <ol> 133 * <li>System apps like Settings or SysUI broadcasts {@code BUGREPORT_REQUESTED}. 134 * <li>{@link BugreportRequestedReceiver} receives the intent and delegates it to this service. 135 * <li>This service calls startBugreport() and passes in local file descriptors to receive 136 * bugreport artifacts. 137 * </ol> 138 */ 139 public class BugreportProgressService extends Service { 140 private static final String TAG = "BugreportProgressService"; 141 private static final boolean DEBUG = false; 142 143 private Intent startSelfIntent; 144 145 private static final String AUTHORITY = "com.android.shell"; 146 147 // External intent used to trigger bugreport API. 148 static final String INTENT_BUGREPORT_REQUESTED = 149 "com.android.internal.intent.action.BUGREPORT_REQUESTED"; 150 151 // Intent sent to notify external apps that bugreport finished 152 static final String INTENT_BUGREPORT_FINISHED = 153 "com.android.internal.intent.action.BUGREPORT_FINISHED"; 154 155 // Internal intents used on notification actions. 156 static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL"; 157 static final String INTENT_BUGREPORT_SHARE = "android.intent.action.BUGREPORT_SHARE"; 158 static final String INTENT_BUGREPORT_DONE = "android.intent.action.BUGREPORT_DONE"; 159 static final String INTENT_BUGREPORT_INFO_LAUNCH = 160 "android.intent.action.BUGREPORT_INFO_LAUNCH"; 161 static final String INTENT_BUGREPORT_SCREENSHOT = 162 "android.intent.action.BUGREPORT_SCREENSHOT"; 163 164 static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT"; 165 static final String EXTRA_BUGREPORT_TYPE = "android.intent.extra.BUGREPORT_TYPE"; 166 static final String EXTRA_BUGREPORT_NONCE = "android.intent.extra.BUGREPORT_NONCE"; 167 static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT"; 168 static final String EXTRA_ID = "android.intent.extra.ID"; 169 static final String EXTRA_NAME = "android.intent.extra.NAME"; 170 static final String EXTRA_TITLE = "android.intent.extra.TITLE"; 171 static final String EXTRA_DESCRIPTION = "android.intent.extra.DESCRIPTION"; 172 static final String EXTRA_ORIGINAL_INTENT = "android.intent.extra.ORIGINAL_INTENT"; 173 static final String EXTRA_INFO = "android.intent.extra.INFO"; 174 static final String EXTRA_EXTRA_ATTACHMENT_URI = 175 "android.intent.extra.EXTRA_ATTACHMENT_URI"; 176 177 private static final int MSG_SERVICE_COMMAND = 1; 178 private static final int MSG_DELAYED_SCREENSHOT = 2; 179 private static final int MSG_SCREENSHOT_REQUEST = 3; 180 private static final int MSG_SCREENSHOT_RESPONSE = 4; 181 182 // Passed to Message.obtain() when msg.arg2 is not used. 183 private static final int UNUSED_ARG2 = -2; 184 185 // Maximum progress displayed in %. 186 private static final int CAPPED_PROGRESS = 99; 187 188 /** Show the progress log every this percent. */ 189 private static final int LOG_PROGRESS_STEP = 10; 190 191 /** 192 * Delay before a screenshot is taken. 193 * <p> 194 * Should be at least 3 seconds, otherwise its toast might show up in the screenshot. 195 */ 196 static final int SCREENSHOT_DELAY_SECONDS = 3; 197 198 /** System property where dumpstate stores last triggered bugreport id */ 199 static final String PROPERTY_LAST_ID = "dumpstate.last_id"; 200 201 private static final String BUGREPORT_SERVICE = "bugreport"; 202 203 /** 204 * Directory on Shell's data storage where screenshots will be stored. 205 * <p> 206 * Must be a path supported by its FileProvider. 207 */ 208 private static final String BUGREPORT_DIR = "bugreports"; 209 210 /** 211 * The directory in which System Trace files from the native System Tracing app are stored for 212 * Wear devices. 213 */ 214 private static final String WEAR_SYSTEM_TRACES_DIRECTORY_ON_DEVICE = "data/local/traces/"; 215 216 /** The directory that contains System Traces in bugreports that include System Traces. */ 217 private static final String WEAR_SYSTEM_TRACES_DIRECTORY_IN_BUGREPORT = "systraces/"; 218 219 private static final String NOTIFICATION_CHANNEL_ID = "bugreports"; 220 221 /** 222 * Always keep the newest 8 bugreport files. 223 */ 224 private static final int MIN_KEEP_COUNT = 8; 225 226 /** 227 * Always keep bugreports taken in the last week. 228 */ 229 private static final long MIN_KEEP_AGE = DateUtils.WEEK_IN_MILLIS; 230 231 private static final String BUGREPORT_MIMETYPE = "application/vnd.android.bugreport"; 232 233 /** Always keep just the last 3 remote bugreport's files around. */ 234 private static final int REMOTE_BUGREPORT_FILES_AMOUNT = 3; 235 236 /** Always keep remote bugreport files created in the last day. */ 237 private static final long REMOTE_MIN_KEEP_AGE = DateUtils.DAY_IN_MILLIS; 238 239 private final Object mLock = new Object(); 240 241 /** Managed bugreport info (keyed by id) */ 242 @GuardedBy("mLock") 243 private final SparseArray<BugreportInfo> mBugreportInfos = new SparseArray<>(); 244 245 private Context mContext; 246 247 private Handler mMainThreadHandler; 248 private ServiceHandler mServiceHandler; 249 private ScreenshotHandler mScreenshotHandler; 250 251 private final BugreportInfoDialog mInfoDialog = new BugreportInfoDialog(); 252 253 private File mBugreportsDir; 254 255 @VisibleForTesting BugreportManager mBugreportManager; 256 257 /** 258 * id of the notification used to set service on foreground. 259 */ 260 private int mForegroundId = -1; 261 262 /** 263 * Flag indicating whether a screenshot is being taken. 264 * <p> 265 * This is the only state that is shared between the 2 handlers and hence must have synchronized 266 * access. 267 */ 268 private boolean mTakingScreenshot; 269 270 /** 271 * The delay timeout before taking a screenshot. 272 */ 273 @VisibleForTesting int mScreenshotDelaySec = SCREENSHOT_DELAY_SECONDS; 274 275 @GuardedBy("sNotificationBundle") 276 private static final Bundle sNotificationBundle = new Bundle(); 277 278 private boolean mIsWatch; 279 private boolean mIsTv; 280 281 @Override onCreate()282 public void onCreate() { 283 mContext = getApplicationContext(); 284 mMainThreadHandler = new Handler(Looper.getMainLooper()); 285 mServiceHandler = new ServiceHandler("BugreportProgressServiceMainThread"); 286 mScreenshotHandler = new ScreenshotHandler("BugreportProgressServiceScreenshotThread"); 287 startSelfIntent = new Intent(this, this.getClass()); 288 289 mBugreportsDir = new File(getFilesDir(), BUGREPORT_DIR); 290 if (!mBugreportsDir.exists()) { 291 Log.i(TAG, "Creating directory " + mBugreportsDir 292 + " to store bugreports and screenshots"); 293 if (!mBugreportsDir.mkdir()) { 294 Log.w(TAG, "Could not create directory " + mBugreportsDir); 295 } 296 } 297 final Configuration conf = mContext.getResources().getConfiguration(); 298 mIsWatch = (conf.uiMode & Configuration.UI_MODE_TYPE_MASK) == 299 Configuration.UI_MODE_TYPE_WATCH; 300 PackageManager packageManager = getPackageManager(); 301 mIsTv = packageManager.hasSystemFeature(FEATURE_LEANBACK) 302 || packageManager.hasSystemFeature(FEATURE_TELEVISION); 303 NotificationManager nm = NotificationManager.from(mContext); 304 nm.createNotificationChannel( 305 new NotificationChannel(NOTIFICATION_CHANNEL_ID, 306 mContext.getString(R.string.bugreport_notification_channel), 307 isTv(this) ? NotificationManager.IMPORTANCE_DEFAULT 308 : NotificationManager.IMPORTANCE_LOW)); 309 mBugreportManager = mContext.getSystemService(BugreportManager.class); 310 } 311 312 @Override onStartCommand(Intent intent, int flags, int startId)313 public int onStartCommand(Intent intent, int flags, int startId) { 314 Log.v(TAG, "onStartCommand(): " + dumpIntent(intent)); 315 if (intent != null) { 316 if (!intent.hasExtra(EXTRA_ORIGINAL_INTENT) && !intent.hasExtra(EXTRA_ID)) { 317 return START_NOT_STICKY; 318 } 319 // Handle it in a separate thread. 320 final Message msg = mServiceHandler.obtainMessage(); 321 msg.what = MSG_SERVICE_COMMAND; 322 msg.obj = intent; 323 mServiceHandler.sendMessage(msg); 324 } 325 326 // If service is killed it cannot be recreated because it would not know which 327 // dumpstate IDs it would have to watch. 328 return START_NOT_STICKY; 329 } 330 331 @Override onBind(Intent intent)332 public IBinder onBind(Intent intent) { 333 return new LocalBinder(); 334 } 335 336 @Override onDestroy()337 public void onDestroy() { 338 mServiceHandler.getLooper().quit(); 339 mScreenshotHandler.getLooper().quit(); 340 super.onDestroy(); 341 } 342 343 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)344 protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 345 synchronized (mLock) { 346 final int size = mBugreportInfos.size(); 347 if (size == 0) { 348 writer.println("No monitored processes"); 349 return; 350 } 351 writer.print("Foreground id: "); writer.println(mForegroundId); 352 writer.println("\n"); 353 writer.println("Monitored dumpstate processes"); 354 writer.println("-----------------------------"); 355 for (int i = 0; i < size; i++) { 356 writer.print("#"); 357 writer.println(i + 1); 358 writer.println(getInfoLocked(mBugreportInfos.keyAt(i))); 359 } 360 } 361 } 362 getFileName(BugreportInfo info, String suffix)363 private static String getFileName(BugreportInfo info, String suffix) { 364 return String.format("%s-%s%s", info.baseName, info.getName(), suffix); 365 } 366 367 private final class BugreportCallbackImpl extends BugreportCallback { 368 369 @GuardedBy("mLock") 370 private final BugreportInfo mInfo; 371 BugreportCallbackImpl(BugreportInfo info)372 BugreportCallbackImpl(BugreportInfo info) { 373 mInfo = info; 374 } 375 376 @Override onProgress(float progress)377 public void onProgress(float progress) { 378 synchronized (mLock) { 379 checkProgressUpdatedLocked(mInfo, (int) progress); 380 } 381 } 382 383 /** 384 * Logs errors and stops the service on which this bugreport was running. 385 * Also stops progress notification (if any). 386 */ 387 @Override onError(@ugreportErrorCode int errorCode)388 public void onError(@BugreportErrorCode int errorCode) { 389 synchronized (mLock) { 390 stopProgressLocked(mInfo.id); 391 mInfo.deleteEmptyFiles(); 392 } 393 Log.e(TAG, "Bugreport API callback onError() errorCode = " + errorCode); 394 return; 395 } 396 397 @Override onFinished()398 public void onFinished() { 399 mInfo.renameBugreportFile(); 400 mInfo.renameScreenshots(); 401 if (mInfo.bugreportFile.length() == 0) { 402 Log.e(TAG, "Bugreport file empty. File path = " + mInfo.bugreportFile); 403 onError(BUGREPORT_ERROR_RUNTIME); 404 return; 405 } 406 synchronized (mLock) { 407 sendBugreportFinishedBroadcastLocked(); 408 mMainThreadHandler.post(() -> mInfoDialog.onBugreportFinished(mInfo)); 409 } 410 } 411 412 @Override onEarlyReportFinished()413 public void onEarlyReportFinished() {} 414 415 /** 416 * Reads bugreport id and links it to the bugreport info to track a bugreport that is in 417 * process. id is incremented in the dumpstate code. 418 * We do not track a bugreport if there is already a bugreport with the same id being 419 * tracked. 420 */ 421 @GuardedBy("mLock") trackInfoWithIdLocked()422 private void trackInfoWithIdLocked() { 423 final int id = SystemProperties.getInt(PROPERTY_LAST_ID, 1); 424 if (mBugreportInfos.get(id) == null) { 425 mInfo.id = id; 426 mBugreportInfos.put(mInfo.id, mInfo); 427 } 428 return; 429 } 430 431 @GuardedBy("mLock") sendBugreportFinishedBroadcastLocked()432 private void sendBugreportFinishedBroadcastLocked() { 433 final String bugreportFilePath = mInfo.bugreportFile.getAbsolutePath(); 434 if (mInfo.type == BugreportParams.BUGREPORT_MODE_REMOTE) { 435 sendRemoteBugreportFinishedBroadcast(mContext, bugreportFilePath, 436 mInfo.bugreportFile, mInfo.nonce); 437 } else { 438 cleanupOldFiles(MIN_KEEP_COUNT, MIN_KEEP_AGE, mBugreportsDir); 439 final Intent intent = new Intent(INTENT_BUGREPORT_FINISHED); 440 intent.putExtra(EXTRA_BUGREPORT, bugreportFilePath); 441 intent.putExtra(EXTRA_SCREENSHOT, getScreenshotForIntent(mInfo)); 442 mContext.sendBroadcast(intent, android.Manifest.permission.DUMP); 443 onBugreportFinished(mInfo); 444 } 445 } 446 } 447 sendRemoteBugreportFinishedBroadcast(Context context, String bugreportFileName, File bugreportFile, long nonce)448 private void sendRemoteBugreportFinishedBroadcast(Context context, 449 String bugreportFileName, File bugreportFile, long nonce) { 450 // Remote bugreports are stored in the same directory as normal bugreports, meaning that 451 // the remote bugreport storage limit will get applied to normal bugreports whenever a 452 // remote bugreport is triggered. The fix in cleanupOldFiles applies the normal bugreport 453 // limit to the remote bugreports as a quick fix. 454 cleanupOldFiles( 455 REMOTE_BUGREPORT_FILES_AMOUNT, REMOTE_MIN_KEEP_AGE, bugreportFile.getParentFile()); 456 final Intent intent = new Intent(DevicePolicyManager.ACTION_REMOTE_BUGREPORT_DISPATCH); 457 final Uri bugreportUri = getUri(context, bugreportFile); 458 final String bugreportHash = generateFileHash(bugreportFileName); 459 if (bugreportHash == null) { 460 Log.e(TAG, "Error generating file hash for remote bugreport"); 461 } 462 intent.setDataAndType(bugreportUri, BUGREPORT_MIMETYPE); 463 intent.putExtra(DevicePolicyManager.EXTRA_REMOTE_BUGREPORT_HASH, bugreportHash); 464 intent.putExtra(DevicePolicyManager.EXTRA_REMOTE_BUGREPORT_NONCE, nonce); 465 intent.putExtra(EXTRA_BUGREPORT, bugreportFileName); 466 context.sendBroadcastAsUser(intent, UserHandle.SYSTEM, 467 android.Manifest.permission.DUMP); 468 } 469 470 /** 471 * Checks if screenshot array is non-empty and returns the first screenshot's path. The first 472 * screenshot is the default screenshot for the bugreport types that take it. 473 */ getScreenshotForIntent(BugreportInfo info)474 private static String getScreenshotForIntent(BugreportInfo info) { 475 if (!info.screenshotFiles.isEmpty()) { 476 final File screenshotFile = info.screenshotFiles.get(0); 477 final String screenshotFilePath = screenshotFile.getAbsolutePath(); 478 return screenshotFilePath; 479 } 480 return null; 481 } 482 generateFileHash(String fileName)483 private static String generateFileHash(String fileName) { 484 String fileHash = null; 485 try { 486 MessageDigest md = MessageDigest.getInstance("SHA-256"); 487 FileInputStream input = new FileInputStream(new File(fileName)); 488 byte[] buffer = new byte[65536]; 489 int size; 490 while ((size = input.read(buffer)) > 0) { 491 md.update(buffer, 0, size); 492 } 493 input.close(); 494 byte[] hashBytes = md.digest(); 495 StringBuilder sb = new StringBuilder(); 496 for (int i = 0; i < hashBytes.length; i++) { 497 sb.append(String.format("%02x", hashBytes[i])); 498 } 499 fileHash = sb.toString(); 500 } catch (IOException | NoSuchAlgorithmException e) { 501 Log.e(TAG, "generating file hash for bugreport file failed " + fileName, e); 502 } 503 return fileHash; 504 } 505 cleanupOldFiles(final int minCount, final long minAge, File bugreportsDir)506 void cleanupOldFiles(final int minCount, final long minAge, File bugreportsDir) { 507 new AsyncTask<Void, Void, Void>() { 508 @Override 509 protected Void doInBackground(Void... params) { 510 try { 511 if (onboardingBugreportStorageBugFix()) { 512 cleanupOldBugreports(); 513 } else { 514 FileUtils.deleteOlderFiles(bugreportsDir, minCount, minAge); 515 } 516 } catch (RuntimeException e) { 517 Log.e(TAG, "RuntimeException deleting old files", e); 518 } 519 return null; 520 } 521 }.execute(); 522 } 523 cleanupOldBugreports()524 private void cleanupOldBugreports() { 525 final File[] files = mBugreportsDir.listFiles(); 526 if (files == null) return; 527 528 // Sort with newest files first 529 Arrays.sort(files, new Comparator<File>() { 530 @Override 531 public int compare(File lhs, File rhs) { 532 return Long.compare(rhs.lastModified(), lhs.lastModified()); 533 } 534 }); 535 536 int normalBugreportFilesCount = 0; 537 int deferredBugreportFilesCount = 0; 538 for (int i = 0; i < files.length; i++) { 539 final File file = files[i]; 540 541 // tmp files are deferred bugreports which have their separate storage limit 542 boolean isDeferredBugreportFile = file.getName().endsWith(".tmp"); 543 if (isDeferredBugreportFile) { 544 deferredBugreportFilesCount++; 545 } else { 546 normalBugreportFilesCount++; 547 } 548 // Keep files newer than minAgeMs 549 final long age = System.currentTimeMillis() - file.lastModified(); 550 final int count = isDeferredBugreportFile 551 ? deferredBugreportFilesCount : normalBugreportFilesCount; 552 if (count > MIN_KEEP_COUNT && age > MIN_KEEP_AGE) { 553 if (file.delete()) { 554 Log.d(TAG, "Deleted old file " + file); 555 } 556 } 557 } 558 } 559 560 /** 561 * Main thread used to handle all requests but taking screenshots. 562 */ 563 private final class ServiceHandler extends Handler { ServiceHandler(String name)564 public ServiceHandler(String name) { 565 super(newLooper(name)); 566 } 567 568 @Override handleMessage(Message msg)569 public void handleMessage(Message msg) { 570 if (msg.what == MSG_DELAYED_SCREENSHOT) { 571 takeScreenshot(msg.arg1, msg.arg2); 572 return; 573 } 574 575 if (msg.what == MSG_SCREENSHOT_RESPONSE) { 576 handleScreenshotResponse(msg); 577 return; 578 } 579 580 if (msg.what != MSG_SERVICE_COMMAND) { 581 // Confidence check. 582 Log.e(TAG, "Invalid message type: " + msg.what); 583 return; 584 } 585 586 // At this point it's handling onStartCommand(), with the intent passed as an Extra. 587 if (!(msg.obj instanceof Intent)) { 588 // Confidence check. 589 Log.wtf(TAG, "handleMessage(): invalid msg.obj type: " + msg.obj); 590 return; 591 } 592 final Parcelable parcel = ((Intent) msg.obj).getParcelableExtra(EXTRA_ORIGINAL_INTENT); 593 Log.v(TAG, "handleMessage(): " + dumpIntent((Intent) parcel)); 594 final Intent intent; 595 if (parcel instanceof Intent) { 596 // The real intent was passed to BugreportRequestedReceiver, 597 // which delegated to the service. 598 intent = (Intent) parcel; 599 } else { 600 intent = (Intent) msg.obj; 601 } 602 final String action = intent.getAction(); 603 final int id = intent.getIntExtra(EXTRA_ID, 0); 604 final String name = intent.getStringExtra(EXTRA_NAME); 605 606 if (DEBUG) 607 Log.v(TAG, "action: " + action + ", name: " + name + ", id: " + id); 608 switch (action) { 609 case INTENT_BUGREPORT_REQUESTED: 610 startBugreportAPI(intent); 611 break; 612 case INTENT_BUGREPORT_INFO_LAUNCH: 613 launchBugreportInfoDialog(id); 614 break; 615 case INTENT_BUGREPORT_SCREENSHOT: 616 takeScreenshot(id); 617 break; 618 case INTENT_BUGREPORT_SHARE: 619 shareBugreport(id, (BugreportInfo) intent.getParcelableExtra(EXTRA_INFO)); 620 break; 621 case INTENT_BUGREPORT_DONE: 622 maybeShowWarningMessageAndCloseNotification(id); 623 break; 624 case INTENT_BUGREPORT_CANCEL: 625 cancel(id); 626 break; 627 default: 628 Log.w(TAG, "Unsupported intent: " + action); 629 } 630 return; 631 632 } 633 } 634 635 /** 636 * Separate thread used only to take screenshots so it doesn't block the main thread. 637 */ 638 private final class ScreenshotHandler extends Handler { ScreenshotHandler(String name)639 public ScreenshotHandler(String name) { 640 super(newLooper(name)); 641 } 642 643 @Override handleMessage(Message msg)644 public void handleMessage(Message msg) { 645 if (msg.what != MSG_SCREENSHOT_REQUEST) { 646 Log.e(TAG, "Invalid message type: " + msg.what); 647 return; 648 } 649 handleScreenshotRequest(msg); 650 } 651 } 652 653 @GuardedBy("mLock") getInfoLocked(int id)654 private BugreportInfo getInfoLocked(int id) { 655 final BugreportInfo bugreportInfo = mBugreportInfos.get(id); 656 if (bugreportInfo == null) { 657 Log.w(TAG, "Not monitoring bugreports with ID " + id); 658 return null; 659 } 660 return bugreportInfo; 661 } 662 getBugreportBaseName(@ugreportParams.BugreportMode int type)663 private String getBugreportBaseName(@BugreportParams.BugreportMode int type) { 664 String buildId = SystemProperties.get("ro.build.id", "UNKNOWN_BUILD"); 665 String deviceName = SystemProperties.get("ro.product.name", "UNKNOWN_DEVICE"); 666 String typeSuffix = null; 667 if (type == BugreportParams.BUGREPORT_MODE_WIFI) { 668 typeSuffix = "wifi"; 669 } else if (type == BugreportParams.BUGREPORT_MODE_TELEPHONY) { 670 typeSuffix = "telephony"; 671 } else { 672 return String.format("bugreport-%s-%s", deviceName, buildId); 673 } 674 return String.format("bugreport-%s-%s-%s", deviceName, buildId, typeSuffix); 675 } 676 startBugreportAPI(Intent intent)677 private void startBugreportAPI(Intent intent) { 678 String shareTitle = intent.getStringExtra(EXTRA_TITLE); 679 String shareDescription = intent.getStringExtra(EXTRA_DESCRIPTION); 680 int bugreportType = intent.getIntExtra(EXTRA_BUGREPORT_TYPE, 681 BugreportParams.BUGREPORT_MODE_INTERACTIVE); 682 long nonce = intent.getLongExtra(EXTRA_BUGREPORT_NONCE, 0); 683 String baseName = getBugreportBaseName(bugreportType); 684 String name = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()); 685 Uri extraAttachment = intent.getParcelableExtra(EXTRA_EXTRA_ATTACHMENT_URI, Uri.class); 686 687 BugreportInfo info = new BugreportInfo(mContext, baseName, name, shareTitle, 688 shareDescription, bugreportType, mBugreportsDir, nonce, extraAttachment); 689 synchronized (mLock) { 690 if (info.bugreportFile.exists()) { 691 Log.e(TAG, "Failed to start bugreport generation, the requested bugreport file " 692 + info.bugreportFile + " already exists"); 693 return; 694 } 695 info.createBugreportFile(); 696 } 697 ParcelFileDescriptor bugreportFd = info.getBugreportFd(); 698 if (bugreportFd == null) { 699 Log.e(TAG, "Failed to start bugreport generation as " 700 + " bugreport parcel file descriptor is null."); 701 return; 702 } 703 info.createScreenshotFile(mBugreportsDir); 704 ParcelFileDescriptor screenshotFd = null; 705 if (isDefaultScreenshotRequired(bugreportType, /* hasScreenshotButton= */ !mIsTv)) { 706 screenshotFd = info.getDefaultScreenshotFd(); 707 if (screenshotFd == null) { 708 Log.e(TAG, "Failed to start bugreport generation as" 709 + " screenshot parcel file descriptor is null. Deleting bugreport file"); 710 FileUtils.closeQuietly(bugreportFd); 711 info.bugreportFile.delete(); 712 return; 713 } 714 } 715 716 final Executor executor = ActivityThread.currentActivityThread().getExecutor(); 717 718 Log.i(TAG, "bugreport type = " + bugreportType 719 + " bugreport file fd: " + bugreportFd 720 + " screenshot file fd: " + screenshotFd); 721 722 BugreportCallbackImpl bugreportCallback = new BugreportCallbackImpl(info); 723 try { 724 synchronized (mLock) { 725 mBugreportManager.startBugreport(bugreportFd, screenshotFd, 726 new BugreportParams(bugreportType), executor, bugreportCallback); 727 bugreportCallback.trackInfoWithIdLocked(); 728 } 729 } catch (RuntimeException e) { 730 Log.i(TAG, "Error in generating bugreports: ", e); 731 // The binder call didn't go through successfully, so need to close the fds. 732 // If the calls went through API takes ownership. 733 FileUtils.closeQuietly(bugreportFd); 734 if (screenshotFd != null) { 735 FileUtils.closeQuietly(screenshotFd); 736 } 737 } 738 } 739 isDefaultScreenshotRequired( @ugreportParams.BugreportMode int bugreportType, boolean hasScreenshotButton)740 private static boolean isDefaultScreenshotRequired( 741 @BugreportParams.BugreportMode int bugreportType, 742 boolean hasScreenshotButton) { 743 // Modify dumpstate#SetOptionsFromMode as well for default system screenshots. 744 // We override dumpstate for interactive bugreports with a screenshot button. 745 return (bugreportType == BugreportParams.BUGREPORT_MODE_INTERACTIVE && !hasScreenshotButton) 746 || bugreportType == BugreportParams.BUGREPORT_MODE_FULL 747 || bugreportType == BugreportParams.BUGREPORT_MODE_WEAR; 748 } 749 getFd(File file)750 private static ParcelFileDescriptor getFd(File file) { 751 try { 752 return ParcelFileDescriptor.open(file, 753 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND); 754 } catch (FileNotFoundException e) { 755 Log.i(TAG, "Error in generating bugreports: ", e); 756 } 757 return null; 758 } 759 createReadWriteFile(File file)760 private static void createReadWriteFile(File file) { 761 try { 762 if (!file.exists()) { 763 file.createNewFile(); 764 file.setReadable(true, true); 765 file.setWritable(true, true); 766 } 767 } catch (IOException e) { 768 Log.e(TAG, "Error in creating bugreport file: ", e); 769 } 770 } 771 772 /** 773 * Updates the system notification for a given bugreport. 774 */ updateProgress(BugreportInfo info)775 private void updateProgress(BugreportInfo info) { 776 if (info.progress.intValue() < 0) { 777 Log.e(TAG, "Invalid progress values for " + info); 778 return; 779 } 780 781 if (info.finished.get()) { 782 Log.w(TAG, "Not sending progress notification because bugreport has finished already (" 783 + info + ")"); 784 return; 785 } 786 787 final NumberFormat nf = NumberFormat.getPercentInstance(); 788 nf.setMinimumFractionDigits(2); 789 nf.setMaximumFractionDigits(2); 790 final String percentageText = nf.format((double) info.progress.intValue() / 100); 791 792 final String title; 793 if (mIsWatch) { 794 // TODO: Remove this workaround when notification progress is implemented on Wear. 795 nf.setMinimumFractionDigits(0); 796 nf.setMaximumFractionDigits(0); 797 final String watchPercentageText = nf.format((double) info.progress.intValue() / 100); 798 title = mContext.getString( 799 R.string.bugreport_in_progress_title, info.id, watchPercentageText); 800 } else { 801 title = mContext.getString(R.string.bugreport_in_progress_title, info.id); 802 } 803 804 final String name = 805 info.getName() != null ? info.getName() 806 : mContext.getString(R.string.bugreport_unnamed); 807 808 final Notification.Builder builder = newBaseNotification(mContext) 809 .setContentTitle(title) 810 .setTicker(title) 811 .setContentText(name) 812 .setProgress(100 /* max value of progress percentage */, 813 info.progress.intValue(), false) 814 .setOngoing(true); 815 816 // Wear and ATV bugreport doesn't need the bug info dialog, screenshot and cancel action. 817 if (!(mIsWatch || mIsTv)) { 818 final Action cancelAction = new Action.Builder(null, mContext.getString( 819 com.android.internal.R.string.cancel), newCancelIntent(mContext, info)).build(); 820 final Intent infoIntent = new Intent(mContext, BugreportProgressService.class); 821 infoIntent.setAction(INTENT_BUGREPORT_INFO_LAUNCH); 822 infoIntent.putExtra(EXTRA_ID, info.id); 823 // Simple notification action button clicks are immutable 824 final PendingIntent infoPendingIntent = 825 PendingIntent.getService(mContext, info.id, infoIntent, 826 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 827 final Action infoAction = new Action.Builder(null, 828 mContext.getString(R.string.bugreport_info_action), 829 infoPendingIntent).build(); 830 final Intent screenshotIntent = new Intent(mContext, BugreportProgressService.class); 831 screenshotIntent.setAction(INTENT_BUGREPORT_SCREENSHOT); 832 screenshotIntent.putExtra(EXTRA_ID, info.id); 833 // Simple notification action button clicks are immutable 834 PendingIntent screenshotPendingIntent = mTakingScreenshot ? null : PendingIntent 835 .getService(mContext, info.id, screenshotIntent, 836 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 837 final Action screenshotAction = new Action.Builder(null, 838 mContext.getString(R.string.bugreport_screenshot_action), 839 screenshotPendingIntent).build(); 840 builder.setContentIntent(infoPendingIntent) 841 .setActions(infoAction, screenshotAction, cancelAction); 842 } 843 // Show a debug log, every LOG_PROGRESS_STEP percent. 844 final int progress = info.progress.intValue(); 845 846 if ((progress == 0) || (progress >= 100) 847 || ((progress / LOG_PROGRESS_STEP) 848 != (info.lastProgress.intValue() / LOG_PROGRESS_STEP))) { 849 Log.d(TAG, "Progress #" + info.id + ": " + percentageText); 850 } 851 info.lastProgress.set(progress); 852 853 sendForegroundabledNotification(info.id, builder.build()); 854 } 855 sendForegroundabledNotification(int id, Notification notification)856 private void sendForegroundabledNotification(int id, Notification notification) { 857 if (mForegroundId >= 0) { 858 if (DEBUG) Log.d(TAG, "Already running as foreground service"); 859 NotificationManager.from(mContext).notify(id, notification); 860 } else { 861 mForegroundId = id; 862 Log.d(TAG, "Start running as foreground service on id " + mForegroundId); 863 // Explicitly starting the service so that stopForeground() does not crash 864 // Workaround for b/140997620 865 startForegroundService(startSelfIntent); 866 startForeground(mForegroundId, notification); 867 } 868 } 869 870 /** 871 * Creates a {@link PendingIntent} for a notification action used to cancel a bugreport. 872 */ newCancelIntent(Context context, BugreportInfo info)873 private static PendingIntent newCancelIntent(Context context, BugreportInfo info) { 874 final Intent intent = new Intent(INTENT_BUGREPORT_CANCEL); 875 intent.setClass(context, BugreportProgressService.class); 876 intent.putExtra(EXTRA_ID, info.id); 877 return PendingIntent.getService(context, info.id, intent, 878 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 879 } 880 881 /** 882 * Creates a {@link PendingIntent} for a notification action used to show warning about the 883 * sensitivity of bugreport data and then close bugreport notification. 884 * 885 * Note that, the warning message may not be shown if the user has chosen not to see the 886 * message anymore. 887 */ newBugreportDoneIntent(Context context, BugreportInfo info)888 private static PendingIntent newBugreportDoneIntent(Context context, BugreportInfo info) { 889 final Intent intent = new Intent(INTENT_BUGREPORT_DONE); 890 intent.setClass(context, BugreportProgressService.class); 891 intent.putExtra(EXTRA_ID, info.id); 892 return PendingIntent.getService(context, info.id, intent, 893 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); 894 } 895 896 /** 897 * Finalizes the progress on a given bugreport and cancel its notification. 898 */ 899 @GuardedBy("mLock") stopProgressLocked(int id)900 private void stopProgressLocked(int id) { 901 if (mBugreportInfos.indexOfKey(id) < 0) { 902 Log.w(TAG, "ID not watched: " + id); 903 } else { 904 Log.d(TAG, "Removing ID " + id); 905 mBugreportInfos.remove(id); 906 } 907 // Must stop foreground service first, otherwise notif.cancel() will fail below. 908 stopForegroundWhenDoneLocked(id); 909 910 911 Log.d(TAG, "stopProgress(" + id + "): cancel notification"); 912 NotificationManager.from(mContext).cancel(id); 913 914 stopSelfWhenDoneLocked(); 915 } 916 917 /** 918 * Cancels a bugreport upon user's request. 919 */ cancel(int id)920 private void cancel(int id) { 921 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_CANCEL); 922 Log.v(TAG, "cancel: ID=" + id); 923 mInfoDialog.cancel(); 924 synchronized (mLock) { 925 final BugreportInfo info = getInfoLocked(id); 926 if (info != null && !info.finished.get()) { 927 Log.i(TAG, "Cancelling bugreport service (ID=" + id + ") on user's request"); 928 mBugreportManager.cancelBugreport(); 929 info.deleteScreenshots(); 930 info.deleteBugreportFile(); 931 } 932 stopProgressLocked(id); 933 } 934 } 935 936 /** 937 * Fetches a {@link BugreportInfo} for a given process and launches a dialog where the user can 938 * change its values. 939 */ launchBugreportInfoDialog(int id)940 private void launchBugreportInfoDialog(int id) { 941 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_DETAILS); 942 final BugreportInfo info; 943 synchronized (mLock) { 944 info = getInfoLocked(id); 945 } 946 if (info == null) { 947 // Most likely am killed Shell before user tapped the notification. Since system might 948 // be too busy anwyays, it's better to ignore the notification and switch back to the 949 // non-interactive mode (where the bugerport will be shared upon completion). 950 Log.w(TAG, "launchBugreportInfoDialog(): canceling notification because id " + id 951 + " was not found"); 952 // TODO: add test case to make sure notification is canceled. 953 NotificationManager.from(mContext).cancel(id); 954 return; 955 } 956 957 collapseNotificationBar(); 958 959 // Dissmiss keyguard first. 960 final IWindowManager wm = IWindowManager.Stub 961 .asInterface(ServiceManager.getService(Context.WINDOW_SERVICE)); 962 try { 963 wm.dismissKeyguard(null, null); 964 } catch (Exception e) { 965 // ignore it 966 } 967 968 mMainThreadHandler.post(() -> mInfoDialog.initialize(mContext, info)); 969 } 970 971 /** 972 * Starting point for taking a screenshot. 973 * <p> 974 * It first display a toast message and waits {@link #SCREENSHOT_DELAY_SECONDS} seconds before 975 * taking the screenshot. 976 */ takeScreenshot(int id)977 private void takeScreenshot(int id) { 978 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SCREENSHOT); 979 BugreportInfo info; 980 synchronized (mLock) { 981 info = getInfoLocked(id); 982 } 983 if (info == null) { 984 // Most likely am killed Shell before user tapped the notification. Since system might 985 // be too busy anwyays, it's better to ignore the notification and switch back to the 986 // non-interactive mode (where the bugerport will be shared upon completion). 987 Log.w(TAG, "takeScreenshot(): canceling notification because id " + id 988 + " was not found"); 989 // TODO: add test case to make sure notification is canceled. 990 NotificationManager.from(mContext).cancel(id); 991 return; 992 } 993 setTakingScreenshot(true); 994 collapseNotificationBar(); 995 Map<String, Object> arguments = new HashMap<>(); 996 arguments.put("count", mScreenshotDelaySec); 997 final String msg = PluralsMessageFormatter.format( 998 mContext.getResources(), 999 arguments, 1000 com.android.internal.R.string.bugreport_countdown); 1001 Log.i(TAG, msg); 1002 // Show a toast just once, otherwise it might be captured in the screenshot. 1003 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show(); 1004 1005 takeScreenshot(id, mScreenshotDelaySec); 1006 } 1007 1008 /** 1009 * Takes a screenshot after {@code delay} seconds. 1010 */ takeScreenshot(int id, int delay)1011 private void takeScreenshot(int id, int delay) { 1012 if (delay > 0) { 1013 Log.d(TAG, "Taking screenshot for " + id + " in " + delay + " seconds"); 1014 final Message msg = mServiceHandler.obtainMessage(); 1015 msg.what = MSG_DELAYED_SCREENSHOT; 1016 msg.arg1 = id; 1017 msg.arg2 = delay - 1; 1018 mServiceHandler.sendMessageDelayed(msg, DateUtils.SECOND_IN_MILLIS); 1019 return; 1020 } 1021 final BugreportInfo info; 1022 // It's time to take the screenshot: let the proper thread handle it 1023 synchronized (mLock) { 1024 info = getInfoLocked(id); 1025 } 1026 if (info == null) { 1027 return; 1028 } 1029 final String screenshotPath = 1030 new File(mBugreportsDir, info.getPathNextScreenshot()).getAbsolutePath(); 1031 1032 Message.obtain(mScreenshotHandler, MSG_SCREENSHOT_REQUEST, id, UNUSED_ARG2, screenshotPath) 1033 .sendToTarget(); 1034 } 1035 1036 /** 1037 * Sets the internal {@code mTakingScreenshot} state and updates all notifications so their 1038 * SCREENSHOT button is enabled or disabled accordingly. 1039 */ setTakingScreenshot(boolean flag)1040 private void setTakingScreenshot(boolean flag) { 1041 synchronized (mLock) { 1042 mTakingScreenshot = flag; 1043 for (int i = 0; i < mBugreportInfos.size(); i++) { 1044 final BugreportInfo info = getInfoLocked(mBugreportInfos.keyAt(i)); 1045 if (info.finished.get()) { 1046 Log.d(TAG, "Not updating progress for " + info.id + " while taking screenshot" 1047 + " because share notification was already sent"); 1048 continue; 1049 } 1050 updateProgress(info); 1051 } 1052 } 1053 } 1054 handleScreenshotRequest(Message requestMsg)1055 private void handleScreenshotRequest(Message requestMsg) { 1056 String screenshotFile = (String) requestMsg.obj; 1057 boolean taken = takeScreenshot(mContext, screenshotFile); 1058 setTakingScreenshot(false); 1059 1060 Message.obtain(mServiceHandler, MSG_SCREENSHOT_RESPONSE, requestMsg.arg1, taken ? 1 : 0, 1061 screenshotFile).sendToTarget(); 1062 } 1063 handleScreenshotResponse(Message resultMsg)1064 private void handleScreenshotResponse(Message resultMsg) { 1065 final boolean taken = resultMsg.arg2 != 0; 1066 final BugreportInfo info; 1067 synchronized (mLock) { 1068 info = getInfoLocked(resultMsg.arg1); 1069 } 1070 if (info == null) { 1071 return; 1072 } 1073 final File screenshotFile = new File((String) resultMsg.obj); 1074 1075 final String msg; 1076 if (taken) { 1077 info.addScreenshot(screenshotFile); 1078 if (info.finished.get()) { 1079 Log.d(TAG, "Screenshot finished after bugreport; updating share notification"); 1080 info.renameScreenshots(); 1081 sendBugreportNotification(info, mTakingScreenshot); 1082 } 1083 msg = mContext.getString(R.string.bugreport_screenshot_taken); 1084 } else { 1085 msg = mContext.getString(R.string.bugreport_screenshot_failed); 1086 Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show(); 1087 } 1088 Log.d(TAG, msg); 1089 } 1090 1091 /** 1092 * Stop running on foreground once there is no more active bugreports being watched. 1093 */ 1094 @GuardedBy("mLock") stopForegroundWhenDoneLocked(int id)1095 private void stopForegroundWhenDoneLocked(int id) { 1096 if (id != mForegroundId) { 1097 Log.d(TAG, "stopForegroundWhenDoneLocked(" + id + "): ignoring since foreground id is " 1098 + mForegroundId); 1099 return; 1100 } 1101 1102 Log.d(TAG, "detaching foreground from id " + mForegroundId); 1103 stopForeground(Service.STOP_FOREGROUND_DETACH); 1104 mForegroundId = -1; 1105 1106 // Might need to restart foreground using a new notification id. 1107 final int total = mBugreportInfos.size(); 1108 if (total > 0) { 1109 for (int i = 0; i < total; i++) { 1110 final BugreportInfo info = getInfoLocked(mBugreportInfos.keyAt(i)); 1111 if (!info.finished.get()) { 1112 updateProgress(info); 1113 break; 1114 } 1115 } 1116 } 1117 } 1118 1119 /** 1120 * Finishes the service when it's not monitoring any more processes. 1121 */ 1122 @GuardedBy("mLock") stopSelfWhenDoneLocked()1123 private void stopSelfWhenDoneLocked() { 1124 if (mBugreportInfos.size() > 0) { 1125 if (DEBUG) Log.d(TAG, "Staying alive, waiting for IDs " + mBugreportInfos); 1126 return; 1127 } 1128 Log.v(TAG, "No more processes to handle, shutting down"); 1129 stopSelf(); 1130 } 1131 1132 /** 1133 * Wraps up bugreport generation and triggers a notification to either share the bugreport or 1134 * just notify the ending of the bugreport generation, according to the device type. 1135 */ onBugreportFinished(BugreportInfo info)1136 private void onBugreportFinished(BugreportInfo info) { 1137 if (!TextUtils.isEmpty(info.shareTitle)) { 1138 info.setTitle(info.shareTitle); 1139 } 1140 Log.d(TAG, "Bugreport finished with title: " + info.getTitle() 1141 + " and shareDescription: " + info.shareDescription); 1142 info.finished.set(true); 1143 1144 synchronized (mLock) { 1145 // Stop running on foreground, otherwise share notification cannot be dismissed. 1146 stopForegroundWhenDoneLocked(info.id); 1147 } 1148 1149 if (!info.bugreportFile.exists() || !info.bugreportFile.canRead()) { 1150 Log.e(TAG, "Could not read bugreport file " + info.bugreportFile); 1151 Toast.makeText(mContext, R.string.bugreport_unreadable_text, Toast.LENGTH_LONG).show(); 1152 synchronized (mLock) { 1153 stopProgressLocked(info.id); 1154 } 1155 return; 1156 } 1157 1158 triggerLocalNotification(info); 1159 } 1160 1161 /** 1162 * Responsible for triggering a notification that allows the user to start a "share" intent with 1163 * the bugreport. 1164 */ triggerLocalNotification(final BugreportInfo info)1165 private void triggerLocalNotification(final BugreportInfo info) { 1166 boolean isPlainText = info.bugreportFile.getName().toLowerCase().endsWith(".txt"); 1167 if (!isPlainText) { 1168 // Already zipped, send it right away. 1169 sendBugreportNotification(info, mTakingScreenshot); 1170 } else { 1171 // Asynchronously zip the file first, then send it. 1172 sendZippedBugreportNotification(info, mTakingScreenshot); 1173 } 1174 } 1175 buildWarningIntent(Context context, @Nullable Intent sendIntent)1176 private static Intent buildWarningIntent(Context context, @Nullable Intent sendIntent) { 1177 final Intent intent = new Intent(context, BugreportWarningActivity.class); 1178 if (sendIntent != null) { 1179 intent.putExtra(Intent.EXTRA_INTENT, sendIntent); 1180 } 1181 return intent; 1182 } 1183 1184 /** 1185 * Build {@link Intent} that can be used to share the given bugreport. 1186 */ buildSendIntent(Context context, BugreportInfo info)1187 private static Intent buildSendIntent(Context context, BugreportInfo info) { 1188 // Rename files (if required) before sharing 1189 info.renameBugreportFile(); 1190 info.renameScreenshots(); 1191 // Files are kept on private storage, so turn into Uris that we can 1192 // grant temporary permissions for. 1193 final Uri bugreportUri; 1194 try { 1195 bugreportUri = getUri(context, info.bugreportFile); 1196 } catch (IllegalArgumentException e) { 1197 // Should not happen on production, but happens when a Shell is sideloaded and 1198 // FileProvider cannot find a configured root for it. 1199 Log.wtf(TAG, "Could not get URI for " + info.bugreportFile, e); 1200 return null; 1201 } 1202 1203 final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); 1204 final String mimeType = "application/vnd.android.bugreport"; 1205 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 1206 intent.addCategory(Intent.CATEGORY_DEFAULT); 1207 intent.setType(mimeType); 1208 1209 final String subject = !TextUtils.isEmpty(info.getTitle()) 1210 ? info.getTitle() : bugreportUri.getLastPathSegment(); 1211 intent.putExtra(Intent.EXTRA_SUBJECT, subject); 1212 1213 // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String. 1214 // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually 1215 // create the ClipData object with the attachments URIs. 1216 final StringBuilder messageBody = new StringBuilder("Build info: ") 1217 .append(SystemProperties.get("ro.build.description")) 1218 .append("\nSerial number: ") 1219 .append(SystemProperties.get("ro.serialno")); 1220 int descriptionLength = 0; 1221 if (!TextUtils.isEmpty(info.getDescription())) { 1222 messageBody.append("\nDescription: ").append(info.getDescription()); 1223 descriptionLength = info.getDescription().length(); 1224 } 1225 intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString()); 1226 final ClipData clipData = new ClipData(null, new String[] { mimeType }, 1227 new ClipData.Item(null, null, null, bugreportUri)); 1228 Log.d(TAG, "share intent: bureportUri=" + bugreportUri); 1229 final ArrayList<Uri> attachments = Lists.newArrayList(bugreportUri); 1230 for (File screenshot : info.screenshotFiles) { 1231 final Uri screenshotUri = getUri(context, screenshot); 1232 Log.d(TAG, "share intent: screenshotUri=" + screenshotUri); 1233 clipData.addItem(new ClipData.Item(null, null, null, screenshotUri)); 1234 attachments.add(screenshotUri); 1235 } 1236 if (info.extraAttachment != null) { 1237 clipData.addItem(new ClipData.Item(null, null, null, info.extraAttachment)); 1238 attachments.add(info.extraAttachment); 1239 } 1240 intent.setClipData(clipData); 1241 intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments); 1242 1243 final Pair<UserHandle, Account> sendToAccount = findSendToAccount(context, 1244 SystemProperties.get("sendbug.preferred.domain")); 1245 if (sendToAccount != null) { 1246 intent.putExtra(Intent.EXTRA_EMAIL, new String[] { sendToAccount.second.name }); 1247 1248 // TODO Open the chooser activity on work profile by default. 1249 // If we just use startActivityAsUser(), then the launched app couldn't read 1250 // attachments. 1251 // We probably need to change ChooserActivity to take an extra argument for the 1252 // default profile. 1253 } 1254 1255 // Log what was sent to the intent 1256 Log.d(TAG, "share intent: EXTRA_SUBJECT=" + subject + ", EXTRA_TEXT=" + messageBody.length() 1257 + " chars, description=" + descriptionLength + " chars"); 1258 1259 return intent; 1260 } 1261 hasUserDecidedNotToGetWarningMessage()1262 private boolean hasUserDecidedNotToGetWarningMessage() { 1263 return getWarningState(mContext, STATE_UNKNOWN) == STATE_HIDE; 1264 } 1265 maybeShowWarningMessageAndCloseNotification(int id)1266 private void maybeShowWarningMessageAndCloseNotification(int id) { 1267 if (!hasUserDecidedNotToGetWarningMessage()) { 1268 Intent warningIntent; 1269 if (mIsWatch) { 1270 warningIntent = buildWearWarningIntent(); 1271 } else { 1272 warningIntent = buildWarningIntent(mContext, /* sendIntent */ null); 1273 } 1274 warningIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1275 mContext.startActivity(warningIntent); 1276 } 1277 NotificationManager.from(mContext).cancel(id); 1278 } 1279 1280 /** 1281 * Build intent to show warning dialog on Wear after bugreport is done 1282 */ buildWearWarningIntent()1283 private Intent buildWearWarningIntent() { 1284 Intent intent = new Intent(); 1285 intent.setClassName(mContext, getPackageName() + ".WearBugreportWarningActivity"); 1286 if (mContext.getPackageManager().resolveActivity(intent, /* flags */ 0) == null) { 1287 Log.e(TAG, "Cannot find wear bugreport warning activity"); 1288 return buildWarningIntent(mContext, /* sendIntent */ null); 1289 } 1290 return intent; 1291 } 1292 shareBugreport(int id, BugreportInfo sharedInfo)1293 private void shareBugreport(int id, BugreportInfo sharedInfo) { 1294 shareBugreport(id, sharedInfo, !hasUserDecidedNotToGetWarningMessage()); 1295 } 1296 1297 /** 1298 * Shares the bugreport upon user's request by issuing a {@link Intent#ACTION_SEND_MULTIPLE} 1299 * intent, but issuing a warning dialog the first time. 1300 */ shareBugreport(int id, BugreportInfo sharedInfo, boolean showWarning)1301 private void shareBugreport(int id, BugreportInfo sharedInfo, boolean showWarning) { 1302 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_NOTIFICATION_ACTION_SHARE); 1303 BugreportInfo info; 1304 synchronized (mLock) { 1305 info = getInfoLocked(id); 1306 } 1307 if (info == null) { 1308 // Service was terminated but notification persisted 1309 info = sharedInfo; 1310 synchronized (mLock) { 1311 Log.d(TAG, "shareBugreport(): no info for ID " + id + " on managed processes (" 1312 + mBugreportInfos + "), using info from intent instead (" + info + ")"); 1313 } 1314 } else { 1315 Log.v(TAG, "shareBugReport(): id " + id + " info = " + info); 1316 } 1317 1318 addDetailsToZipFile(info); 1319 1320 final Intent sendIntent = buildSendIntent(mContext, info); 1321 if (sendIntent == null) { 1322 Log.w(TAG, "Stopping progres on ID " + id + " because share intent could not be built"); 1323 synchronized (mLock) { 1324 stopProgressLocked(id); 1325 } 1326 return; 1327 } 1328 1329 final Intent notifIntent; 1330 boolean useChooser = true; 1331 1332 // Send through warning dialog by default 1333 if (showWarning) { 1334 notifIntent = buildWarningIntent(mContext, sendIntent); 1335 // No need to show a chooser in this case. 1336 useChooser = false; 1337 } else { 1338 notifIntent = sendIntent; 1339 } 1340 notifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1341 1342 // Send the share intent... 1343 if (useChooser) { 1344 sendShareIntent(mContext, notifIntent); 1345 } else { 1346 mContext.startActivity(notifIntent); 1347 } 1348 synchronized (mLock) { 1349 // ... and stop watching this process. 1350 stopProgressLocked(id); 1351 } 1352 } 1353 sendShareIntent(Context context, Intent intent)1354 static void sendShareIntent(Context context, Intent intent) { 1355 final Intent chooserIntent = Intent.createChooser(intent, 1356 context.getResources().getText(R.string.bugreport_intent_chooser_title)); 1357 1358 // Since we may be launched behind lockscreen, make sure that ChooserActivity doesn't finish 1359 // itself in onStop. 1360 chooserIntent.putExtra(ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP, true); 1361 // Starting the activity from a service. 1362 chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1363 context.startActivity(chooserIntent); 1364 } 1365 1366 /** 1367 * Sends a notification indicating the bugreport has finished so use can share it. 1368 */ sendBugreportNotification(BugreportInfo info, boolean takingScreenshot)1369 private void sendBugreportNotification(BugreportInfo info, boolean takingScreenshot) { 1370 1371 // Since adding the details can take a while, do it before notifying user. 1372 addDetailsToZipFile(info); 1373 1374 String content; 1375 content = takingScreenshot ? 1376 mContext.getString(R.string.bugreport_finished_pending_screenshot_text) 1377 : mContext.getString(R.string.bugreport_finished_text); 1378 final String title; 1379 if (TextUtils.isEmpty(info.getTitle())) { 1380 title = mContext.getString(R.string.bugreport_finished_title, info.id); 1381 } else { 1382 title = info.getTitle(); 1383 if (!TextUtils.isEmpty(info.shareDescription)) { 1384 if(!takingScreenshot) content = info.shareDescription; 1385 } 1386 } 1387 1388 final Notification.Builder builder = newBaseNotification(mContext) 1389 .setContentTitle(title) 1390 .setTicker(title) 1391 .setOnlyAlertOnce(false) 1392 .setContentText(content); 1393 1394 if (!mIsWatch) { 1395 final Intent shareIntent = new Intent(INTENT_BUGREPORT_SHARE); 1396 shareIntent.setClass(mContext, BugreportProgressService.class); 1397 shareIntent.setAction(INTENT_BUGREPORT_SHARE); 1398 shareIntent.putExtra(EXTRA_ID, info.id); 1399 shareIntent.putExtra(EXTRA_INFO, info); 1400 1401 builder.setContentIntent(PendingIntent.getService(mContext, info.id, shareIntent, 1402 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) 1403 .setDeleteIntent(newCancelIntent(mContext, info)); 1404 } else { 1405 // Device is a watch 1406 if (hasUserDecidedNotToGetWarningMessage()) { 1407 // No action button needed for the notification. User can swipe to dimiss. 1408 builder.setActions(new Action[0]); 1409 } else { 1410 // Add action button to lead user to the warning screen. 1411 builder.setActions( 1412 new Action.Builder( 1413 null, mContext.getString(R.string.bugreport_info_action), 1414 newBugreportDoneIntent(mContext, info)).build()); 1415 } 1416 } 1417 1418 if (!TextUtils.isEmpty(info.getName())) { 1419 builder.setSubText(info.getName()); 1420 } 1421 1422 Log.v(TAG, "Sending 'Share' notification for ID " + info.id + ": " + title); 1423 NotificationManager.from(mContext).notify(info.id, builder.build()); 1424 } 1425 1426 /** 1427 * Sends a notification indicating the bugreport is being updated so the user can wait until it 1428 * finishes - at this point there is nothing to be done other than waiting, hence it has no 1429 * pending action. 1430 */ sendBugreportBeingUpdatedNotification(Context context, int id)1431 private void sendBugreportBeingUpdatedNotification(Context context, int id) { 1432 final String title = context.getString(R.string.bugreport_updating_title); 1433 final Notification.Builder builder = newBaseNotification(context) 1434 .setContentTitle(title) 1435 .setTicker(title) 1436 .setContentText(context.getString(R.string.bugreport_updating_wait)); 1437 Log.v(TAG, "Sending 'Updating zip' notification for ID " + id + ": " + title); 1438 sendForegroundabledNotification(id, builder.build()); 1439 } 1440 newBaseNotification(Context context)1441 private static Notification.Builder newBaseNotification(Context context) { 1442 synchronized (sNotificationBundle) { 1443 if (sNotificationBundle.isEmpty()) { 1444 // Rename notifcations from "Shell" to "Android System" 1445 sNotificationBundle.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, 1446 context.getString(com.android.internal.R.string.android_system_label)); 1447 } 1448 } 1449 return new Notification.Builder(context, NOTIFICATION_CHANNEL_ID) 1450 .addExtras(sNotificationBundle) 1451 .setSmallIcon(R.drawable.ic_bug_report_black_24dp) 1452 .setLocalOnly(true) 1453 .setColor(context.getColor( 1454 com.android.internal.R.color.system_notification_accent_color)) 1455 .setOnlyAlertOnce(true) 1456 .extend(new Notification.TvExtender()); 1457 } 1458 1459 /** 1460 * Sends a zipped bugreport notification. 1461 */ sendZippedBugreportNotification( final BugreportInfo info, final boolean takingScreenshot)1462 private void sendZippedBugreportNotification( final BugreportInfo info, 1463 final boolean takingScreenshot) { 1464 new AsyncTask<Void, Void, Void>() { 1465 @Override 1466 protected Void doInBackground(Void... params) { 1467 Looper.prepare(); 1468 zipBugreport(info); 1469 sendBugreportNotification(info, takingScreenshot); 1470 return null; 1471 } 1472 }.execute(); 1473 } 1474 1475 /** 1476 * Zips a bugreport file, returning the path to the new file (or to the 1477 * original in case of failure). 1478 */ zipBugreport(BugreportInfo info)1479 private static void zipBugreport(BugreportInfo info) { 1480 final String bugreportPath = info.bugreportFile.getAbsolutePath(); 1481 final String zippedPath = bugreportPath.replace(".txt", ".zip"); 1482 Log.v(TAG, "zipping " + bugreportPath + " as " + zippedPath); 1483 final File bugreportZippedFile = new File(zippedPath); 1484 try (InputStream is = new FileInputStream(info.bugreportFile); 1485 ZipOutputStream zos = new ZipOutputStream( 1486 new BufferedOutputStream(new FileOutputStream(bugreportZippedFile)))) { 1487 addEntry(zos, info.bugreportFile.getName(), is); 1488 // Delete old file 1489 final boolean deleted = info.bugreportFile.delete(); 1490 if (deleted) { 1491 Log.v(TAG, "deleted original bugreport (" + bugreportPath + ")"); 1492 } else { 1493 Log.e(TAG, "could not delete original bugreport (" + bugreportPath + ")"); 1494 } 1495 info.bugreportFile = bugreportZippedFile; 1496 } catch (IOException e) { 1497 Log.e(TAG, "exception zipping file " + zippedPath, e); 1498 } 1499 } 1500 1501 /** Returns an array of the system trace files collected by the System Tracing native app. */ getSystemTraceFiles()1502 private static File[] getSystemTraceFiles() { 1503 try { 1504 return new File(WEAR_SYSTEM_TRACES_DIRECTORY_ON_DEVICE).listFiles(); 1505 } catch (SecurityException e) { 1506 Log.e(TAG, "Error getting system trace files.", e); 1507 return new File[]{}; 1508 } 1509 } 1510 1511 /** 1512 * Adds the user-provided info into the bugreport zip file. 1513 * <p> 1514 * If user provided a title, it will be saved into a {@code title.txt} entry; similarly, the 1515 * description will be saved on {@code description.txt}. 1516 */ addDetailsToZipFile(BugreportInfo info)1517 private void addDetailsToZipFile(BugreportInfo info) { 1518 synchronized (mLock) { 1519 addDetailsToZipFileLocked(info); 1520 } 1521 } 1522 1523 @GuardedBy("mLock") addDetailsToZipFileLocked(BugreportInfo info)1524 private void addDetailsToZipFileLocked(BugreportInfo info) { 1525 if (info.bugreportFile == null) { 1526 // One possible reason is a bug in the Parcelization code. 1527 Log.wtf(TAG, "addDetailsToZipFile(): no bugreportFile on " + info); 1528 return; 1529 } 1530 1531 File[] systemTracesToIncludeInBugreport = new File[] {}; 1532 if (mIsWatch) { 1533 systemTracesToIncludeInBugreport = getSystemTraceFiles(); 1534 Log.d(TAG, "Found " + systemTracesToIncludeInBugreport.length + " system traces."); 1535 } 1536 1537 if (TextUtils.isEmpty(info.getTitle()) 1538 && TextUtils.isEmpty(info.getDescription()) 1539 && systemTracesToIncludeInBugreport.length == 0) { 1540 Log.d(TAG, "Not touching zip file: no detail to add."); 1541 return; 1542 } 1543 if (info.addedDetailsToZip || info.addingDetailsToZip) { 1544 Log.d(TAG, "Already added details to zip file for " + info); 1545 return; 1546 } 1547 info.addingDetailsToZip = true; 1548 1549 // It's not possible to add a new entry into an existing file, so we need to create a new 1550 // zip, copy all entries, then rename it. 1551 if (!mIsWatch) { 1552 // TODO(b/184854609): re-introduce this notification for Wear. 1553 sendBugreportBeingUpdatedNotification(mContext, info.id); // ...and that takes time 1554 } 1555 1556 final File dir = info.bugreportFile.getParentFile(); 1557 final File tmpZip = new File(dir, "tmp-" + info.bugreportFile.getName()); 1558 Log.d(TAG, "Writing temporary zip file (" + tmpZip + ") with title and/or description"); 1559 try (ZipFile oldZip = new ZipFile(info.bugreportFile); 1560 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(tmpZip))) { 1561 1562 // First copy contents from original zip. 1563 Enumeration<? extends ZipEntry> entries = oldZip.entries(); 1564 while (entries.hasMoreElements()) { 1565 final ZipEntry entry = entries.nextElement(); 1566 final String entryName = entry.getName(); 1567 if (!entry.isDirectory()) { 1568 addEntry(zos, entryName, entry.getTime(), oldZip.getInputStream(entry)); 1569 } else { 1570 Log.w(TAG, "skipping directory entry: " + entryName); 1571 } 1572 } 1573 1574 // Then add the user-provided info. 1575 if (systemTracesToIncludeInBugreport.length != 0) { 1576 for (File trace : systemTracesToIncludeInBugreport) { 1577 addEntry(zos, 1578 WEAR_SYSTEM_TRACES_DIRECTORY_IN_BUGREPORT + trace.getName(), 1579 new FileInputStream(trace)); 1580 } 1581 } 1582 addEntry(zos, "title.txt", info.getTitle()); 1583 addEntry(zos, "description.txt", info.getDescription()); 1584 } catch (IOException e) { 1585 Log.e(TAG, "exception zipping file " + tmpZip, e); 1586 Toast.makeText(mContext, R.string.bugreport_add_details_to_zip_failed, 1587 Toast.LENGTH_LONG).show(); 1588 return; 1589 } finally { 1590 // Make sure it only tries to add details once, even it fails the first time. 1591 info.addedDetailsToZip = true; 1592 info.addingDetailsToZip = false; 1593 stopForegroundWhenDoneLocked(info.id); 1594 } 1595 1596 if (!tmpZip.renameTo(info.bugreportFile)) { 1597 Log.e(TAG, "Could not rename " + tmpZip + " to " + info.bugreportFile); 1598 } 1599 } 1600 addEntry(ZipOutputStream zos, String entry, String text)1601 private static void addEntry(ZipOutputStream zos, String entry, String text) 1602 throws IOException { 1603 if (DEBUG) Log.v(TAG, "adding entry '" + entry + "': " + text); 1604 if (!TextUtils.isEmpty(text)) { 1605 addEntry(zos, entry, new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))); 1606 } 1607 } 1608 addEntry(ZipOutputStream zos, String entryName, InputStream is)1609 private static void addEntry(ZipOutputStream zos, String entryName, InputStream is) 1610 throws IOException { 1611 addEntry(zos, entryName, System.currentTimeMillis(), is); 1612 } 1613 addEntry(ZipOutputStream zos, String entryName, long timestamp, InputStream is)1614 private static void addEntry(ZipOutputStream zos, String entryName, long timestamp, 1615 InputStream is) throws IOException { 1616 final ZipEntry entry = new ZipEntry(entryName); 1617 entry.setTime(timestamp); 1618 zos.putNextEntry(entry); 1619 final int totalBytes = Streams.copy(is, zos); 1620 if (DEBUG) Log.v(TAG, "size of '" + entryName + "' entry: " + totalBytes + " bytes"); 1621 zos.closeEntry(); 1622 } 1623 1624 /** 1625 * Find the best matching {@link Account} based on build properties. If none found, returns 1626 * the first account that looks like an email address. 1627 */ 1628 @VisibleForTesting findSendToAccount(Context context, String preferredDomain)1629 static Pair<UserHandle, Account> findSendToAccount(Context context, String preferredDomain) { 1630 final UserManager um = context.getSystemService(UserManager.class); 1631 final AccountManager am = context.getSystemService(AccountManager.class); 1632 1633 if (preferredDomain != null && !preferredDomain.startsWith("@")) { 1634 preferredDomain = "@" + preferredDomain; 1635 } 1636 1637 Pair<UserHandle, Account> first = null; 1638 1639 for (UserHandle user : um.getUserProfiles()) { 1640 final Account[] accounts; 1641 try { 1642 accounts = am.getAccountsAsUser(user.getIdentifier()); 1643 } catch (RuntimeException e) { 1644 Log.e(TAG, "Could not get accounts for preferred domain " + preferredDomain 1645 + " for user " + user, e); 1646 continue; 1647 } 1648 if (DEBUG) Log.d(TAG, "User: " + user + " Number of accounts: " + accounts.length); 1649 for (Account account : accounts) { 1650 if (Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) { 1651 final Pair<UserHandle, Account> candidate = Pair.create(user, account); 1652 1653 if (!TextUtils.isEmpty(preferredDomain)) { 1654 // if we have a preferred domain and it matches, return; otherwise keep 1655 // looking 1656 if (account.name.endsWith(preferredDomain)) { 1657 return candidate; 1658 } 1659 // if we don't have a preferred domain, just return since it looks like 1660 // an email address 1661 } else { 1662 return candidate; 1663 } 1664 if (first == null) { 1665 first = candidate; 1666 } 1667 } 1668 } 1669 } 1670 return first; 1671 } 1672 getUri(Context context, File file)1673 static Uri getUri(Context context, File file) { 1674 return file != null ? FileProvider.getUriForFile(context, AUTHORITY, file) : null; 1675 } 1676 getFileExtra(Intent intent, String key)1677 static File getFileExtra(Intent intent, String key) { 1678 final String path = intent.getStringExtra(key); 1679 if (path != null) { 1680 return new File(path); 1681 } else { 1682 return null; 1683 } 1684 } 1685 1686 /** 1687 * Dumps an intent, extracting the relevant extras. 1688 */ dumpIntent(Intent intent)1689 static String dumpIntent(Intent intent) { 1690 if (intent == null) { 1691 return "NO INTENT"; 1692 } 1693 String action = intent.getAction(); 1694 if (action == null) { 1695 // Happens when startService is called... 1696 action = "no action"; 1697 } 1698 final StringBuilder buffer = new StringBuilder(action).append(" extras: "); 1699 addExtra(buffer, intent, EXTRA_ID); 1700 addExtra(buffer, intent, EXTRA_NAME); 1701 addExtra(buffer, intent, EXTRA_DESCRIPTION); 1702 addExtra(buffer, intent, EXTRA_BUGREPORT); 1703 addExtra(buffer, intent, EXTRA_SCREENSHOT); 1704 addExtra(buffer, intent, EXTRA_INFO); 1705 addExtra(buffer, intent, EXTRA_TITLE); 1706 1707 if (intent.hasExtra(EXTRA_ORIGINAL_INTENT)) { 1708 buffer.append(SHORT_EXTRA_ORIGINAL_INTENT).append(": "); 1709 final Intent originalIntent = intent.getParcelableExtra(EXTRA_ORIGINAL_INTENT); 1710 buffer.append(dumpIntent(originalIntent)); 1711 } else { 1712 buffer.append("no ").append(SHORT_EXTRA_ORIGINAL_INTENT); 1713 } 1714 1715 return buffer.toString(); 1716 } 1717 1718 private static final String SHORT_EXTRA_ORIGINAL_INTENT = 1719 EXTRA_ORIGINAL_INTENT.substring(EXTRA_ORIGINAL_INTENT.lastIndexOf('.') + 1); 1720 addExtra(StringBuilder buffer, Intent intent, String name)1721 private static void addExtra(StringBuilder buffer, Intent intent, String name) { 1722 final String shortName = name.substring(name.lastIndexOf('.') + 1); 1723 if (intent.hasExtra(name)) { 1724 buffer.append(shortName).append('=').append(intent.getExtra(name)); 1725 } else { 1726 buffer.append("no ").append(shortName); 1727 } 1728 buffer.append(", "); 1729 } 1730 setSystemProperty(String key, String value)1731 private static boolean setSystemProperty(String key, String value) { 1732 try { 1733 if (DEBUG) Log.v(TAG, "Setting system property " + key + " to " + value); 1734 SystemProperties.set(key, value); 1735 } catch (IllegalArgumentException e) { 1736 Log.e(TAG, "Could not set property " + key + " to " + value, e); 1737 return false; 1738 } 1739 return true; 1740 } 1741 1742 /** 1743 * Updates the user-provided details of a bugreport. 1744 */ updateBugreportInfo(int id, String name, String title, String description)1745 private void updateBugreportInfo(int id, String name, String title, String description) { 1746 final BugreportInfo info; 1747 synchronized (mLock) { 1748 info = getInfoLocked(id); 1749 } 1750 if (info == null) { 1751 return; 1752 } 1753 if (title != null && !title.equals(info.getTitle())) { 1754 Log.d(TAG, "updating bugreport title: " + title); 1755 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_TITLE_CHANGED); 1756 } 1757 info.setTitle(title); 1758 if (description != null && !description.equals(info.getDescription())) { 1759 Log.d(TAG, "updating bugreport description: " + description.length() + " chars"); 1760 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_DESCRIPTION_CHANGED); 1761 } 1762 info.setDescription(description); 1763 if (name != null && !name.equals(info.getName())) { 1764 Log.d(TAG, "updating bugreport name: " + name); 1765 MetricsLogger.action(this, MetricsEvent.ACTION_BUGREPORT_DETAILS_NAME_CHANGED); 1766 info.setName(name); 1767 updateProgress(info); 1768 } 1769 } 1770 collapseNotificationBar()1771 private void collapseNotificationBar() { 1772 closeSystemDialogs(); 1773 } 1774 newLooper(String name)1775 private static Looper newLooper(String name) { 1776 final HandlerThread thread = new HandlerThread(name, THREAD_PRIORITY_BACKGROUND); 1777 thread.start(); 1778 return thread.getLooper(); 1779 } 1780 1781 /** 1782 * Takes a screenshot and save it to the given location. 1783 */ takeScreenshot(Context context, String path)1784 private static boolean takeScreenshot(Context context, String path) { 1785 final Bitmap bitmap = Screenshooter.takeScreenshot(); 1786 if (bitmap == null) { 1787 return false; 1788 } 1789 try (final FileOutputStream fos = new FileOutputStream(path)) { 1790 if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)) { 1791 ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(150); 1792 return true; 1793 } else { 1794 Log.e(TAG, "Failed to save screenshot on " + path); 1795 } 1796 } catch (IOException e ) { 1797 Log.e(TAG, "Failed to save screenshot on " + path, e); 1798 return false; 1799 } finally { 1800 bitmap.recycle(); 1801 } 1802 return false; 1803 } 1804 isTv(Context context)1805 static boolean isTv(Context context) { 1806 return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK); 1807 } 1808 1809 /** 1810 * Checks whether a character is valid on bugreport names. 1811 */ 1812 @VisibleForTesting isValid(char c)1813 static boolean isValid(char c) { 1814 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') 1815 || c == '_' || c == '-'; 1816 } 1817 1818 /** 1819 * A local binder with interface to return an instance of BugreportProgressService for the 1820 * purpose of testing. 1821 */ 1822 final class LocalBinder extends Binder { getService()1823 @VisibleForTesting BugreportProgressService getService() { 1824 return BugreportProgressService.this; 1825 } 1826 } 1827 1828 /** 1829 * Helper class encapsulating the UI elements and logic used to display a dialog where user 1830 * can change the details of a bugreport. 1831 */ 1832 private final class BugreportInfoDialog { 1833 private EditText mInfoName; 1834 private EditText mInfoTitle; 1835 private EditText mInfoDescription; 1836 private AlertDialog mDialog; 1837 private Button mOkButton; 1838 private int mId; 1839 1840 /** 1841 * Sets its internal state and displays the dialog. 1842 */ 1843 @MainThread initialize(final Context context, BugreportInfo info)1844 void initialize(final Context context, BugreportInfo info) { 1845 final String dialogTitle = 1846 context.getString(R.string.bugreport_info_dialog_title, info.id); 1847 final Context themedContext = new ContextThemeWrapper( 1848 context, com.android.internal.R.style.Theme_DeviceDefault_DayNight); 1849 // First initializes singleton. 1850 if (mDialog == null) { 1851 @SuppressLint("InflateParams") 1852 // It's ok pass null ViewRoot on AlertDialogs. 1853 final View view = View.inflate(themedContext, R.layout.dialog_bugreport_info, null); 1854 1855 mInfoName = (EditText) view.findViewById(R.id.name); 1856 mInfoTitle = (EditText) view.findViewById(R.id.title); 1857 mInfoDescription = (EditText) view.findViewById(R.id.description); 1858 mDialog = new AlertDialog.Builder(themedContext) 1859 .setView(view) 1860 .setTitle(dialogTitle) 1861 .setCancelable(true) 1862 .setPositiveButton(context.getString(R.string.save), 1863 null) 1864 .setNegativeButton(context.getString(com.android.internal.R.string.cancel), 1865 new DialogInterface.OnClickListener() 1866 { 1867 @Override 1868 public void onClick(DialogInterface dialog, int id) 1869 { 1870 MetricsLogger.action(context, 1871 MetricsEvent.ACTION_BUGREPORT_DETAILS_CANCELED); 1872 } 1873 }) 1874 .create(); 1875 1876 mDialog.getWindow().setAttributes( 1877 new WindowManager.LayoutParams( 1878 WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG)); 1879 1880 } else { 1881 // Re-use view, but reset fields first. 1882 mDialog.setTitle(dialogTitle); 1883 mInfoName.setText(null); 1884 mInfoName.setEnabled(true); 1885 mInfoTitle.setText(null); 1886 mInfoDescription.setText(null); 1887 } 1888 1889 // Then set fields. 1890 mId = info.id; 1891 if (!TextUtils.isEmpty(info.getName())) { 1892 mInfoName.setText(info.getName()); 1893 } 1894 if (!TextUtils.isEmpty(info.getTitle())) { 1895 mInfoTitle.setText(info.getTitle()); 1896 } 1897 if (!TextUtils.isEmpty(info.getDescription())) { 1898 mInfoDescription.setText(info.getDescription()); 1899 } 1900 1901 // And finally display it. 1902 mDialog.show(); 1903 1904 // TODO: in a traditional AlertDialog, when the positive button is clicked the 1905 // dialog is always closed, but we need to validate the name first, so we need to 1906 // get a reference to it, which is only available after it's displayed. 1907 // It would be cleaner to use a regular dialog instead, but let's keep this 1908 // workaround for now and change it later, when we add another button to take 1909 // extra screenshots. 1910 if (mOkButton == null) { 1911 mOkButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); 1912 mOkButton.setOnClickListener(new View.OnClickListener() { 1913 1914 @Override 1915 public void onClick(View view) { 1916 MetricsLogger.action(context, MetricsEvent.ACTION_BUGREPORT_DETAILS_SAVED); 1917 sanitizeName(info.getName()); 1918 final String name = mInfoName.getText().toString(); 1919 final String title = mInfoTitle.getText().toString(); 1920 final String description = mInfoDescription.getText().toString(); 1921 1922 updateBugreportInfo(mId, name, title, description); 1923 mDialog.dismiss(); 1924 } 1925 }); 1926 } 1927 } 1928 1929 /** 1930 * Sanitizes the user-provided value for the {@code name} field, automatically replacing 1931 * invalid characters if necessary. 1932 */ sanitizeName(String savedName)1933 private void sanitizeName(String savedName) { 1934 String name = mInfoName.getText().toString(); 1935 if (name.equals(savedName)) { 1936 if (DEBUG) Log.v(TAG, "name didn't change, no need to sanitize: " + name); 1937 return; 1938 } 1939 final StringBuilder safeName = new StringBuilder(name.length()); 1940 boolean changed = false; 1941 for (int i = 0; i < name.length(); i++) { 1942 final char c = name.charAt(i); 1943 if (isValid(c)) { 1944 safeName.append(c); 1945 } else { 1946 changed = true; 1947 safeName.append('_'); 1948 } 1949 } 1950 if (changed) { 1951 Log.v(TAG, "changed invalid name '" + name + "' to '" + safeName + "'"); 1952 name = safeName.toString(); 1953 mInfoName.setText(name); 1954 } 1955 } 1956 1957 /** 1958 * Notifies the dialog that the bugreport has finished so it disables the {@code name} 1959 * field. 1960 * <p>Once the bugreport is finished dumpstate has already generated the final files, so 1961 * changing the name would have no effect. 1962 */ onBugreportFinished(BugreportInfo info)1963 void onBugreportFinished(BugreportInfo info) { 1964 if (mId == info.id && mInfoName != null) { 1965 mInfoName.setEnabled(false); 1966 mInfoName.setText(null); 1967 if (!TextUtils.isEmpty(info.getName())) { 1968 mInfoName.setText(info.getName()); 1969 } 1970 } 1971 } 1972 cancel()1973 void cancel() { 1974 if (mDialog != null) { 1975 mDialog.cancel(); 1976 } 1977 } 1978 } 1979 1980 /** 1981 * Information about a bugreport process while its in progress. 1982 */ 1983 private static final class BugreportInfo implements Parcelable { 1984 private final Context context; 1985 1986 /** 1987 * Sequential, user-friendly id used to identify the bugreport. 1988 */ 1989 int id; 1990 1991 /** 1992 * Prefix name of the bugreport, this is uneditable. 1993 * The baseName consists of the string "bugreport" + deviceName + buildID 1994 * This will end with the string "wifi"/"telephony" for wifi/telephony bugreports. 1995 * Bugreport zip file name = "<baseName>-<name>.zip" 1996 */ 1997 private final String baseName; 1998 1999 /** 2000 * Suffix name of the bugreport/screenshot, is set to timestamp initially. User can make 2001 * modifications to this using interface. 2002 */ 2003 private String name; 2004 2005 /** 2006 * Initial value of the field name. This is required to rename the files later on, as they 2007 * are created using initial value of name. 2008 */ 2009 private final String initialName; 2010 2011 /** 2012 * User-provided, one-line summary of the bug; when set, will be used as the subject 2013 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent. 2014 */ 2015 private String title; 2016 2017 /** 2018 * One-line summary of the bug; when set, will be used as the subject of the 2019 * {@link Intent#ACTION_SEND_MULTIPLE} intent. This is the predefined title which is 2020 * set initially when the request to take a bugreport is made. This overrides any changes 2021 * in the title that the user makes after the bugreport starts. 2022 */ 2023 private final String shareTitle; 2024 2025 /** 2026 * User-provided, detailed description of the bugreport; when set, will be added to the body 2027 * of the {@link Intent#ACTION_SEND_MULTIPLE} intent. This is shown in the app where the 2028 * bugreport is being shared as an attachment. This is not related/dependant on 2029 * {@code shareDescription}. 2030 */ 2031 private String description; 2032 2033 /** 2034 * Current value of progress (in percentage) of the bugreport generation as 2035 * displayed by the UI. 2036 */ 2037 final AtomicInteger progress = new AtomicInteger(0); 2038 2039 /** 2040 * Last value of progress (in percentage) of the bugreport generation for which 2041 * system notification was updated. 2042 */ 2043 final AtomicInteger lastProgress = new AtomicInteger(0); 2044 2045 /** 2046 * Time of the last progress update. 2047 */ 2048 final AtomicLong lastUpdate = new AtomicLong(System.currentTimeMillis()); 2049 2050 /** 2051 * Time of the last progress update when Parcel was created. 2052 */ 2053 String formattedLastUpdate; 2054 2055 /** 2056 * Path of the main bugreport file. 2057 */ 2058 File bugreportFile; 2059 2060 /** 2061 * Path of the screenshot files. 2062 */ 2063 List<File> screenshotFiles = new ArrayList<>(1); 2064 2065 /** 2066 * Whether dumpstate sent an intent informing it has finished. 2067 */ 2068 final AtomicBoolean finished = new AtomicBoolean(false); 2069 2070 /** 2071 * Whether the details entries have been added to the bugreport yet. 2072 */ 2073 boolean addingDetailsToZip; 2074 boolean addedDetailsToZip; 2075 2076 /** 2077 * Internal counter used to name screenshot files. 2078 */ 2079 int screenshotCounter; 2080 2081 /** 2082 * Descriptive text that will be shown to the user in the notification message. This is the 2083 * predefined description which is set initially when the request to take a bugreport is 2084 * made. 2085 */ 2086 private final String shareDescription; 2087 2088 /** 2089 * Type of the bugreport 2090 */ 2091 final int type; 2092 2093 /** 2094 * Nonce of the bugreport 2095 */ 2096 final long nonce; 2097 2098 @Nullable 2099 public Uri extraAttachment = null; 2100 2101 private final Object mLock = new Object(); 2102 2103 /** 2104 * Constructor for tracked bugreports - typically called upon receiving BUGREPORT_REQUESTED. 2105 */ BugreportInfo(Context context, String baseName, String name, @Nullable String shareTitle, @Nullable String shareDescription, @BugreportParams.BugreportMode int type, File bugreportsDir, long nonce, @Nullable Uri extraAttachment)2106 BugreportInfo(Context context, String baseName, String name, 2107 @Nullable String shareTitle, @Nullable String shareDescription, 2108 @BugreportParams.BugreportMode int type, File bugreportsDir, long nonce, 2109 @Nullable Uri extraAttachment) { 2110 this.context = context; 2111 this.name = this.initialName = name; 2112 this.shareTitle = shareTitle == null ? "" : shareTitle; 2113 this.shareDescription = shareDescription == null ? "" : shareDescription; 2114 this.type = type; 2115 this.nonce = nonce; 2116 this.baseName = baseName; 2117 this.bugreportFile = new File(bugreportsDir, getFileName(this, ".zip")); 2118 this.extraAttachment = extraAttachment; 2119 } 2120 createBugreportFile()2121 void createBugreportFile() { 2122 createReadWriteFile(bugreportFile); 2123 } 2124 createScreenshotFile(File bugreportsDir)2125 void createScreenshotFile(File bugreportsDir) { 2126 File screenshotFile = new File(bugreportsDir, getScreenshotName("default")); 2127 addScreenshot(screenshotFile); 2128 createReadWriteFile(screenshotFile); 2129 } 2130 getBugreportFd()2131 ParcelFileDescriptor getBugreportFd() { 2132 return getFd(bugreportFile); 2133 } 2134 getDefaultScreenshotFd()2135 ParcelFileDescriptor getDefaultScreenshotFd() { 2136 if (screenshotFiles.isEmpty()) { 2137 return null; 2138 } 2139 return getFd(screenshotFiles.get(0)); 2140 } 2141 setTitle(String title)2142 void setTitle(String title) { 2143 synchronized (mLock) { 2144 this.title = title; 2145 } 2146 } 2147 getTitle()2148 String getTitle() { 2149 synchronized (mLock) { 2150 return title; 2151 } 2152 } 2153 setName(String name)2154 void setName(String name) { 2155 synchronized (mLock) { 2156 this.name = name; 2157 } 2158 } 2159 getName()2160 String getName() { 2161 synchronized (mLock) { 2162 return name; 2163 } 2164 } 2165 setDescription(String description)2166 void setDescription(String description) { 2167 synchronized (mLock) { 2168 this.description = description; 2169 } 2170 } 2171 getDescription()2172 String getDescription() { 2173 synchronized (mLock) { 2174 return description; 2175 } 2176 } 2177 2178 /** 2179 * Gets the name for next user triggered screenshot file. 2180 */ getPathNextScreenshot()2181 String getPathNextScreenshot() { 2182 screenshotCounter ++; 2183 return getScreenshotName(Integer.toString(screenshotCounter)); 2184 } 2185 2186 /** 2187 * Gets the name for screenshot file based on the suffix that is passed. 2188 */ getScreenshotName(String suffix)2189 String getScreenshotName(String suffix) { 2190 return "screenshot-" + initialName + "-" + suffix + ".png"; 2191 } 2192 2193 /** 2194 * Saves the location of a taken screenshot so it can be sent out at the end. 2195 */ addScreenshot(File screenshot)2196 void addScreenshot(File screenshot) { 2197 screenshotFiles.add(screenshot); 2198 } 2199 2200 /** 2201 * Deletes all screenshots taken for a given bugreport. 2202 */ deleteScreenshots()2203 private void deleteScreenshots() { 2204 for (File file : screenshotFiles) { 2205 Log.i(TAG, "Deleting screenshot file " + file); 2206 file.delete(); 2207 } 2208 } 2209 2210 /** 2211 * Deletes bugreport file for a given bugreport. 2212 */ deleteBugreportFile()2213 private void deleteBugreportFile() { 2214 Log.i(TAG, "Deleting bugreport file " + bugreportFile); 2215 bugreportFile.delete(); 2216 } 2217 2218 /** 2219 * Deletes empty files for a given bugreport. 2220 */ deleteEmptyFiles()2221 private void deleteEmptyFiles() { 2222 if (bugreportFile.length() == 0) { 2223 Log.i(TAG, "Deleting empty bugreport file: " + bugreportFile); 2224 bugreportFile.delete(); 2225 } 2226 deleteEmptyScreenshots(); 2227 } 2228 2229 /** 2230 * Deletes empty screenshot files. 2231 */ deleteEmptyScreenshots()2232 private void deleteEmptyScreenshots() { 2233 screenshotFiles.removeIf(file -> { 2234 final long length = file.length(); 2235 if (length == 0) { 2236 Log.i(TAG, "Deleting empty screenshot file: " + file); 2237 file.delete(); 2238 } 2239 return length == 0; 2240 }); 2241 } 2242 2243 /** 2244 * Rename all screenshots files so that they contain the new {@code name} instead of the 2245 * {@code initialName} if user has changed it. 2246 */ renameScreenshots()2247 void renameScreenshots() { 2248 deleteEmptyScreenshots(); 2249 if (TextUtils.isEmpty(name) || screenshotFiles.isEmpty()) { 2250 return; 2251 } 2252 final List<File> renamedFiles = new ArrayList<>(screenshotFiles.size()); 2253 for (File oldFile : screenshotFiles) { 2254 final String oldName = oldFile.getName(); 2255 final String newName = oldName.replaceFirst(initialName, name); 2256 final File newFile; 2257 if (!newName.equals(oldName)) { 2258 final File renamedFile = new File(oldFile.getParentFile(), newName); 2259 Log.d(TAG, "Renaming screenshot file " + oldFile + " to " + renamedFile); 2260 newFile = oldFile.renameTo(renamedFile) ? renamedFile : oldFile; 2261 } else { 2262 Log.w(TAG, "Name didn't change: " + oldName); 2263 newFile = oldFile; 2264 } 2265 if (newFile.length() > 0) { 2266 renamedFiles.add(newFile); 2267 } else if (newFile.delete()) { 2268 Log.d(TAG, "screenshot file: " + newFile + " deleted successfully."); 2269 } 2270 } 2271 screenshotFiles = renamedFiles; 2272 } 2273 2274 /** 2275 * Rename bugreport file to include the name given by user via UI 2276 */ renameBugreportFile()2277 void renameBugreportFile() { 2278 File newBugreportFile = new File(bugreportFile.getParentFile(), 2279 getFileName(this, ".zip")); 2280 if (!newBugreportFile.getPath().equals(bugreportFile.getPath())) { 2281 if (bugreportFile.renameTo(newBugreportFile)) { 2282 bugreportFile = newBugreportFile; 2283 } 2284 } 2285 } 2286 getFormattedLastUpdate()2287 String getFormattedLastUpdate() { 2288 if (context == null) { 2289 // Restored from Parcel 2290 return formattedLastUpdate == null ? 2291 Long.toString(lastUpdate.longValue()) : formattedLastUpdate; 2292 } 2293 return DateUtils.formatDateTime(context, lastUpdate.longValue(), 2294 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME); 2295 } 2296 2297 @Override toString()2298 public String toString() { 2299 2300 final StringBuilder builder = new StringBuilder() 2301 .append("\tid: ").append(id) 2302 .append(", baseName: ").append(baseName) 2303 .append(", name: ").append(name) 2304 .append(", initialName: ").append(initialName) 2305 .append(", finished: ").append(finished) 2306 .append("\n\ttitle: ").append(title) 2307 .append("\n\tdescription: "); 2308 if (description == null) { 2309 builder.append("null"); 2310 } else { 2311 if (TextUtils.getTrimmedLength(description) == 0) { 2312 builder.append("empty "); 2313 } 2314 builder.append("(").append(description.length()).append(" chars)"); 2315 } 2316 2317 return builder 2318 .append("\n\tfile: ").append(bugreportFile) 2319 .append("\n\tscreenshots: ").append(screenshotFiles) 2320 .append("\n\tprogress: ").append(progress) 2321 .append("\n\tlast_update: ").append(getFormattedLastUpdate()) 2322 .append("\n\taddingDetailsToZip: ").append(addingDetailsToZip) 2323 .append(" addedDetailsToZip: ").append(addedDetailsToZip) 2324 .append("\n\tshareDescription: ").append(shareDescription) 2325 .append("\n\tshareTitle: ").append(shareTitle) 2326 .toString(); 2327 } 2328 2329 // Parcelable contract BugreportInfo(Parcel in)2330 protected BugreportInfo(Parcel in) { 2331 context = null; 2332 id = in.readInt(); 2333 baseName = in.readString(); 2334 name = in.readString(); 2335 initialName = in.readString(); 2336 title = in.readString(); 2337 shareTitle = in.readString(); 2338 description = in.readString(); 2339 progress.set(in.readInt()); 2340 lastProgress.set(in.readInt()); 2341 lastUpdate.set(in.readLong()); 2342 formattedLastUpdate = in.readString(); 2343 bugreportFile = readFile(in); 2344 2345 int screenshotSize = in.readInt(); 2346 for (int i = 1; i <= screenshotSize; i++) { 2347 screenshotFiles.add(readFile(in)); 2348 } 2349 2350 finished.set(in.readInt() == 1); 2351 addingDetailsToZip = in.readBoolean(); 2352 addedDetailsToZip = in.readBoolean(); 2353 screenshotCounter = in.readInt(); 2354 shareDescription = in.readString(); 2355 type = in.readInt(); 2356 nonce = in.readLong(); 2357 } 2358 2359 @Override writeToParcel(Parcel dest, int flags)2360 public void writeToParcel(Parcel dest, int flags) { 2361 dest.writeInt(id); 2362 dest.writeString(baseName); 2363 dest.writeString(name); 2364 dest.writeString(initialName); 2365 dest.writeString(title); 2366 dest.writeString(shareTitle); 2367 dest.writeString(description); 2368 dest.writeInt(progress.intValue()); 2369 dest.writeInt(lastProgress.intValue()); 2370 dest.writeLong(lastUpdate.longValue()); 2371 dest.writeString(getFormattedLastUpdate()); 2372 writeFile(dest, bugreportFile); 2373 2374 dest.writeInt(screenshotFiles.size()); 2375 for (File screenshotFile : screenshotFiles) { 2376 writeFile(dest, screenshotFile); 2377 } 2378 2379 dest.writeInt(finished.get() ? 1 : 0); 2380 dest.writeBoolean(addingDetailsToZip); 2381 dest.writeBoolean(addedDetailsToZip); 2382 dest.writeInt(screenshotCounter); 2383 dest.writeString(shareDescription); 2384 dest.writeInt(type); 2385 dest.writeLong(nonce); 2386 } 2387 2388 @Override describeContents()2389 public int describeContents() { 2390 return 0; 2391 } 2392 writeFile(Parcel dest, File file)2393 private void writeFile(Parcel dest, File file) { 2394 dest.writeString(file == null ? null : file.getPath()); 2395 } 2396 readFile(Parcel in)2397 private File readFile(Parcel in) { 2398 final String path = in.readString(); 2399 return path == null ? null : new File(path); 2400 } 2401 2402 @SuppressWarnings("unused") 2403 public static final Parcelable.Creator<BugreportInfo> CREATOR = 2404 new Parcelable.Creator<BugreportInfo>() { 2405 @Override 2406 public BugreportInfo createFromParcel(Parcel source) { 2407 return new BugreportInfo(source); 2408 } 2409 2410 @Override 2411 public BugreportInfo[] newArray(int size) { 2412 return new BugreportInfo[size]; 2413 } 2414 }; 2415 } 2416 2417 @GuardedBy("mLock") checkProgressUpdatedLocked(BugreportInfo info, int progress)2418 private void checkProgressUpdatedLocked(BugreportInfo info, int progress) { 2419 if (progress > CAPPED_PROGRESS) { 2420 progress = CAPPED_PROGRESS; 2421 } 2422 if (DEBUG) { 2423 if (progress != info.progress.intValue()) { 2424 Log.v(TAG, "Updating progress for name " + info.getName() + "(id: " + info.id 2425 + ") from " + info.progress.intValue() + " to " + progress); 2426 } 2427 } 2428 info.progress.set(progress); 2429 info.lastUpdate.set(System.currentTimeMillis()); 2430 2431 updateProgress(info); 2432 } 2433 } 2434