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