1 /* 2 * Copyright (C) 2019 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 package com.android.car.bugreport; 17 18 import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED; 19 import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_FAILED; 20 import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_SERVICE_NOT_AVAILABLE; 21 import static android.view.Display.DEFAULT_DISPLAY; 22 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 23 24 import static com.android.car.bugreport.PackageUtils.getPackageVersion; 25 26 import android.annotation.FloatRange; 27 import android.annotation.StringRes; 28 import android.app.Notification; 29 import android.app.NotificationChannel; 30 import android.app.NotificationManager; 31 import android.app.PendingIntent; 32 import android.app.Service; 33 import android.car.Car; 34 import android.car.CarBugreportManager; 35 import android.car.CarNotConnectedException; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.content.pm.ServiceInfo; 39 import android.hardware.display.DisplayManager; 40 import android.media.AudioManager; 41 import android.media.Ringtone; 42 import android.media.RingtoneManager; 43 import android.net.Uri; 44 import android.os.Binder; 45 import android.os.Build; 46 import android.os.Handler; 47 import android.os.IBinder; 48 import android.os.Message; 49 import android.os.ParcelFileDescriptor; 50 import android.util.Log; 51 import android.view.Display; 52 import android.widget.Toast; 53 54 import com.google.common.base.Preconditions; 55 import com.google.common.io.ByteStreams; 56 import com.google.common.util.concurrent.AtomicDouble; 57 58 import java.io.BufferedOutputStream; 59 import java.io.File; 60 import java.io.FileInputStream; 61 import java.io.FileOutputStream; 62 import java.io.IOException; 63 import java.io.OutputStream; 64 import java.nio.file.Files; 65 import java.nio.file.Paths; 66 import java.util.Objects; 67 import java.util.concurrent.Executors; 68 import java.util.concurrent.ScheduledExecutorService; 69 import java.util.concurrent.TimeUnit; 70 import java.util.concurrent.atomic.AtomicBoolean; 71 import java.util.zip.ZipOutputStream; 72 73 /** 74 * Service that captures screenshot and bug report using dumpstate and bluetooth snoop logs. 75 * 76 * <p>After collecting all the logs it sets the {@link MetaBugReport} status to 77 * {@link Status#STATUS_AUDIO_PENDING} or {@link Status#STATUS_PENDING_USER_ACTION} depending 78 * on {@link MetaBugReport#getType}. 79 * 80 * <p>If the service is started with action {@link #ACTION_START_AUDIO_LATER}, it will start 81 * bugreporting without showing dialog and recording audio message, see 82 * {@link MetaBugReport#TYPE_AUDIO_LATER}. 83 */ 84 public class BugReportService extends Service { 85 private static final String TAG = BugReportService.class.getSimpleName(); 86 87 /** 88 * Extra data from intent - current bug report. 89 */ 90 static final String EXTRA_META_BUG_REPORT_ID = "meta_bug_report_id"; 91 92 /** 93 * Collects bugreport for the existing {@link MetaBugReport}, which must be provided using 94 * {@link EXTRA_META_BUG_REPORT_ID}. 95 */ 96 static final String ACTION_COLLECT_BUGREPORT = 97 "com.android.car.bugreport.action.COLLECT_BUGREPORT"; 98 99 /** Starts {@link MetaBugReport#TYPE_AUDIO_LATER} bugreporting. */ 100 private static final String ACTION_START_AUDIO_LATER = 101 "com.android.car.bugreport.action.START_AUDIO_LATER"; 102 103 /** @deprecated use {@link #ACTION_START_AUDIO_LATER}. */ 104 private static final String ACTION_START_SILENT = 105 "com.android.car.bugreport.action.START_SILENT"; 106 107 // Wait a short time before starting to capture the bugreport and the screen, so that 108 // bugreport activity can detach from the view tree. 109 // It is ugly to have a timeout, but it is ok here because such a delay should not really 110 // cause bugreport to be tainted with so many other events. If in the future we want to change 111 // this, the best option is probably to wait for onDetach events from view tree. 112 private static final int ACTIVITY_FINISH_DELAY_MILLIS = 1000; 113 114 /** Stop the service only after some delay, to allow toasts to show on the screen. */ 115 private static final int STOP_SERVICE_DELAY_MILLIS = 1000; 116 117 /** 118 * Wait a short time before showing "bugreport started" toast message, because the service 119 * will take a screenshot of the screen. 120 */ 121 private static final int BUGREPORT_STARTED_TOAST_DELAY_MILLIS = 2000; 122 123 private static final String BT_SNOOP_LOG_LOCATION = "/data/misc/bluetooth/logs/btsnoop_hci.log"; 124 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 125 126 /** Notifications on this channel will silently appear in notification bar. */ 127 private static final String PROGRESS_CHANNEL_ID = "BUGREPORT_PROGRESS_CHANNEL"; 128 129 /** Notifications on this channel will pop-up. */ 130 private static final String STATUS_CHANNEL_ID = "BUGREPORT_STATUS_CHANNEL"; 131 132 /** Persistent notification is shown when bugreport is in progress or waiting for audio. */ 133 private static final int BUGREPORT_IN_PROGRESS_NOTIF_ID = 1; 134 135 /** Dismissible notification is shown when bugreport is collected. */ 136 static final int BUGREPORT_FINISHED_NOTIF_ID = 2; 137 138 private static final String OUTPUT_ZIP_FILE = "output_file.zip"; 139 private static final String EXTRA_OUTPUT_ZIP_FILE = "extra_output_file.zip"; 140 141 private static final String MESSAGE_FAILURE_DUMPSTATE = "Failed to grab dumpstate"; 142 private static final String MESSAGE_FAILURE_ZIP = "Failed to zip files"; 143 144 private static final int PROGRESS_HANDLER_EVENT_PROGRESS = 1; 145 private static final String PROGRESS_HANDLER_DATA_PROGRESS = "progress"; 146 147 static final float MAX_PROGRESS_VALUE = 100f; 148 149 /** Binder given to clients. */ 150 private final IBinder mBinder = new ServiceBinder(); 151 152 /** True if {@link BugReportService} is already collecting bugreport, including zipping. */ 153 private final AtomicBoolean mIsCollectingBugReport = new AtomicBoolean(false); 154 private final AtomicDouble mBugReportProgress = new AtomicDouble(0); 155 156 private MetaBugReport mMetaBugReport; 157 private NotificationManager mNotificationManager; 158 private ScheduledExecutorService mSingleThreadExecutor; 159 private BugReportProgressListener mBugReportProgressListener; 160 private Car mCar; 161 private CarBugreportManager mBugreportManager; 162 private CarBugreportManager.CarBugreportManagerCallback mCallback; 163 private Config mConfig; 164 private Context mWindowContext; 165 166 /** A handler on the main thread. */ 167 private Handler mHandler; 168 /** 169 * A handler to the main thread to show toast messages, it will be cleared when the service 170 * finishes. We need to clear it otherwise when bugreport fails, it will show "bugreport start" 171 * toast, which will confuse users. 172 */ 173 private Handler mHandlerStartedToast; 174 175 /** A listener that's notified when bugreport progress changes. */ 176 interface BugReportProgressListener { 177 /** 178 * Called when bug report progress changes. 179 * 180 * @param progress - a bug report progress in [0.0, 100.0]. 181 */ onProgress(float progress)182 void onProgress(float progress); 183 } 184 185 /** Client binder. */ 186 public class ServiceBinder extends Binder { getService()187 BugReportService getService() { 188 // Return this instance of LocalService so clients can call public methods 189 return BugReportService.this; 190 } 191 } 192 193 /** A handler on the main thread. */ 194 private class BugReportHandler extends Handler { 195 @Override handleMessage(Message message)196 public void handleMessage(Message message) { 197 switch (message.what) { 198 case PROGRESS_HANDLER_EVENT_PROGRESS: 199 if (mBugReportProgressListener != null) { 200 float progress = message.getData().getFloat(PROGRESS_HANDLER_DATA_PROGRESS); 201 mBugReportProgressListener.onProgress(progress); 202 } 203 showProgressNotification(); 204 break; 205 default: 206 Log.d(TAG, "Unknown event " + message.what + ", ignoring."); 207 } 208 } 209 } 210 211 @Override onCreate()212 public void onCreate() { 213 Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled."); 214 215 DisplayManager dm = getSystemService(DisplayManager.class); 216 Display primaryDisplay = dm.getDisplay(DEFAULT_DISPLAY); 217 mWindowContext = createDisplayContext(primaryDisplay) 218 .createWindowContext(TYPE_APPLICATION_OVERLAY, null); 219 220 mNotificationManager = getSystemService(NotificationManager.class); 221 mNotificationManager.createNotificationChannel(new NotificationChannel( 222 PROGRESS_CHANNEL_ID, 223 getString(R.string.notification_bugreport_channel_name), 224 NotificationManager.IMPORTANCE_DEFAULT)); 225 mNotificationManager.createNotificationChannel(new NotificationChannel( 226 STATUS_CHANNEL_ID, 227 getString(R.string.notification_bugreport_channel_name), 228 NotificationManager.IMPORTANCE_HIGH)); 229 mSingleThreadExecutor = Executors.newSingleThreadScheduledExecutor(); 230 mHandler = new BugReportHandler(); 231 mHandlerStartedToast = new Handler(); 232 mConfig = Config.create(); 233 } 234 235 @Override onDestroy()236 public void onDestroy() { 237 if (DEBUG) { 238 Log.d(TAG, "Service destroyed"); 239 } 240 disconnectFromCarService(); 241 } 242 243 @Override onStartCommand(Intent intent, int flags, int startId)244 public int onStartCommand(Intent intent, int flags, int startId) { 245 if (mIsCollectingBugReport.get()) { 246 Log.w(TAG, "bug report is already being collected, ignoring"); 247 Toast.makeText(mWindowContext, 248 R.string.toast_bug_report_in_progress, Toast.LENGTH_SHORT).show(); 249 return START_NOT_STICKY; 250 } 251 252 Log.i(TAG, String.format("Will start collecting bug report, version=%s", 253 getPackageVersion(this))); 254 255 String action = intent == null ? null : intent.getAction(); 256 if (ACTION_START_AUDIO_LATER.equals(action) || ACTION_START_SILENT.equals(action)) { 257 Log.i(TAG, "Starting a TYPE_AUDIO_LATER bugreport."); 258 mMetaBugReport = 259 BugReportActivity.createBugReport(this, MetaBugReport.TYPE_AUDIO_LATER); 260 } else if (ACTION_COLLECT_BUGREPORT.equals(action)) { 261 int bugReportId = intent.getIntExtra(EXTRA_META_BUG_REPORT_ID, /* defaultValue= */ -1); 262 mMetaBugReport = BugStorageUtils.findBugReport(this, bugReportId).orElseThrow( 263 () -> new RuntimeException("Failed to find bug report with id " + bugReportId)); 264 } else { 265 Log.w(TAG, "No action provided, ignoring"); 266 return START_NOT_STICKY; 267 } 268 269 mIsCollectingBugReport.set(true); 270 mBugReportProgress.set(0); 271 272 startForeground(BUGREPORT_IN_PROGRESS_NOTIF_ID, buildProgressNotification(), 273 ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE); 274 showProgressNotification(); 275 276 collectBugReport(); 277 278 // Show a short lived "bugreport started" toast message after a short delay. 279 mHandlerStartedToast.postDelayed(() -> { 280 Toast.makeText(mWindowContext, 281 getText(R.string.toast_bug_report_started), Toast.LENGTH_LONG).show(); 282 }, BUGREPORT_STARTED_TOAST_DELAY_MILLIS); 283 284 // If the service process gets killed due to heavy memory pressure, do not restart. 285 return START_NOT_STICKY; 286 } 287 onCarLifecycleChanged(Car car, boolean ready)288 private void onCarLifecycleChanged(Car car, boolean ready) { 289 // not ready - car service is crashed or is restarting. 290 if (!ready) { 291 mBugreportManager = null; 292 mCar = null; 293 294 // NOTE: dumpstate still might be running, but we can't kill it or reconnect to it 295 // so we ignore it. 296 handleBugReportManagerError(CAR_BUGREPORT_SERVICE_NOT_AVAILABLE); 297 return; 298 } 299 try { 300 mBugreportManager = (CarBugreportManager) car.getCarManager(Car.CAR_BUGREPORT_SERVICE); 301 } catch (CarNotConnectedException | NoClassDefFoundError e) { 302 throw new IllegalStateException("Failed to get CarBugreportManager.", e); 303 } 304 } 305 306 /** Shows an updated progress notification. */ showProgressNotification()307 private void showProgressNotification() { 308 if (isCollectingBugReport()) { 309 mNotificationManager.notify( 310 BUGREPORT_IN_PROGRESS_NOTIF_ID, buildProgressNotification()); 311 } 312 } 313 buildProgressNotification()314 private Notification buildProgressNotification() { 315 Intent intent = new Intent(getApplicationContext(), BugReportInfoActivity.class); 316 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 317 PendingIntent startBugReportInfoActivity = 318 PendingIntent.getActivity(getApplicationContext(), /* requestCode= */ 0, intent, 319 PendingIntent.FLAG_IMMUTABLE); 320 return new Notification.Builder(this, PROGRESS_CHANNEL_ID) 321 .setContentTitle(getText(R.string.notification_bugreport_in_progress)) 322 .setContentText(mMetaBugReport.getTitle()) 323 .setSubText(String.format("%.1f%%", mBugReportProgress.get())) 324 .setSmallIcon(R.drawable.download_animation) 325 .setCategory(Notification.CATEGORY_STATUS) 326 .setOngoing(true) 327 .setProgress((int) MAX_PROGRESS_VALUE, (int) mBugReportProgress.get(), false) 328 .setContentIntent(startBugReportInfoActivity) 329 .build(); 330 } 331 332 /** Returns true if bugreporting is in progress. */ isCollectingBugReport()333 public boolean isCollectingBugReport() { 334 return mIsCollectingBugReport.get(); 335 } 336 337 /** Returns current bugreport progress. */ getBugReportProgress()338 public float getBugReportProgress() { 339 return (float) mBugReportProgress.get(); 340 } 341 342 /** Sets a bugreport progress listener. The listener is called on a main thread. */ setBugReportProgressListener(BugReportProgressListener listener)343 public void setBugReportProgressListener(BugReportProgressListener listener) { 344 mBugReportProgressListener = listener; 345 } 346 347 /** Removes the bugreport progress listener. */ removeBugReportProgressListener()348 public void removeBugReportProgressListener() { 349 mBugReportProgressListener = null; 350 } 351 352 @Override onBind(Intent intent)353 public IBinder onBind(Intent intent) { 354 return mBinder; 355 } 356 showToast(@tringRes int resId)357 private void showToast(@StringRes int resId) { 358 // run on ui thread. 359 mHandler.post( 360 () -> Toast.makeText(mWindowContext, getText(resId), Toast.LENGTH_LONG).show()); 361 } 362 disconnectFromCarService()363 private void disconnectFromCarService() { 364 if (mCar != null) { 365 mCar.disconnect(); 366 mCar = null; 367 } 368 mBugreportManager = null; 369 } 370 connectToCarServiceSync()371 private void connectToCarServiceSync() { 372 if (mCar == null || !(mCar.isConnected() || mCar.isConnecting())) { 373 mCar = Car.createCar(this, /* handler= */ null, 374 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, this::onCarLifecycleChanged); 375 } 376 } 377 collectBugReport()378 private void collectBugReport() { 379 // Connect to the car service before collecting bugreport, because when car service crashes, 380 // BugReportService doesn't automatically reconnect to it. 381 connectToCarServiceSync(); 382 383 if (Build.IS_USERDEBUG || Build.IS_ENG) { 384 mSingleThreadExecutor.schedule( 385 this::grabBtSnoopLog, ACTIVITY_FINISH_DELAY_MILLIS, TimeUnit.MILLISECONDS); 386 } 387 mSingleThreadExecutor.schedule( 388 this::saveBugReport, ACTIVITY_FINISH_DELAY_MILLIS, TimeUnit.MILLISECONDS); 389 } 390 grabBtSnoopLog()391 private void grabBtSnoopLog() { 392 Log.i(TAG, "Grabbing bt snoop log"); 393 File result = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(), 394 "-btsnoop.bin.log"); 395 File snoopFile = new File(BT_SNOOP_LOG_LOCATION); 396 if (!snoopFile.exists()) { 397 Log.w(TAG, BT_SNOOP_LOG_LOCATION + " not found, skipping"); 398 return; 399 } 400 try (FileInputStream input = new FileInputStream(snoopFile); 401 FileOutputStream output = new FileOutputStream(result)) { 402 ByteStreams.copy(input, output); 403 } catch (IOException e) { 404 // this regularly happens when snooplog is not enabled so do not log as an error 405 Log.i(TAG, "Failed to grab bt snooplog, continuing to take bug report.", e); 406 } 407 } 408 saveBugReport()409 private void saveBugReport() { 410 Log.i(TAG, "Dumpstate to file"); 411 File outputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(), OUTPUT_ZIP_FILE); 412 File extraOutputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(), 413 EXTRA_OUTPUT_ZIP_FILE); 414 try (ParcelFileDescriptor outFd = ParcelFileDescriptor.open(outputFile, 415 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE); 416 ParcelFileDescriptor extraOutFd = ParcelFileDescriptor.open(extraOutputFile, 417 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE)) { 418 requestBugReport(outFd, extraOutFd); 419 } catch (IOException | RuntimeException e) { 420 Log.e(TAG, "Failed to grab dump state", e); 421 BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED, 422 MESSAGE_FAILURE_DUMPSTATE); 423 showToast(R.string.toast_status_dump_state_failed); 424 disconnectFromCarService(); 425 mIsCollectingBugReport.set(false); 426 } 427 } 428 sendProgressEventToHandler(float progress)429 private void sendProgressEventToHandler(float progress) { 430 Message message = new Message(); 431 message.what = PROGRESS_HANDLER_EVENT_PROGRESS; 432 message.getData().putFloat(PROGRESS_HANDLER_DATA_PROGRESS, progress); 433 mHandler.sendMessage(message); 434 } 435 requestBugReport(ParcelFileDescriptor outFd, ParcelFileDescriptor extraOutFd)436 private void requestBugReport(ParcelFileDescriptor outFd, ParcelFileDescriptor extraOutFd) { 437 if (DEBUG) { 438 Log.d(TAG, "Requesting a bug report from CarBugReportManager."); 439 } 440 mCallback = new CarBugreportManager.CarBugreportManagerCallback() { 441 @Override 442 public void onError(@CarBugreportErrorCode int errorCode) { 443 Log.e(TAG, "CarBugreportManager failed: " + errorCode); 444 disconnectFromCarService(); 445 handleBugReportManagerError(errorCode); 446 } 447 448 @Override 449 public void onProgress(@FloatRange(from = 0f, to = MAX_PROGRESS_VALUE) float progress) { 450 mBugReportProgress.set(progress); 451 sendProgressEventToHandler(progress); 452 } 453 454 @Override 455 public void onFinished() { 456 Log.d(TAG, "CarBugreportManager finished"); 457 disconnectFromCarService(); 458 mBugReportProgress.set(MAX_PROGRESS_VALUE); 459 sendProgressEventToHandler(MAX_PROGRESS_VALUE); 460 mSingleThreadExecutor.submit(BugReportService.this::zipDirectoryAndUpdateStatus); 461 } 462 }; 463 if (mBugreportManager == null) { 464 mHandler.post(() -> Toast.makeText(mWindowContext, 465 "Car service is not ready", Toast.LENGTH_LONG).show()); 466 Log.e(TAG, "CarBugReportManager is not ready"); 467 return; 468 } 469 mBugreportManager.requestBugreport(outFd, extraOutFd, mCallback); 470 } 471 handleBugReportManagerError( @arBugreportManager.CarBugreportManagerCallback.CarBugreportErrorCode int errorCode)472 private void handleBugReportManagerError( 473 @CarBugreportManager.CarBugreportManagerCallback.CarBugreportErrorCode int errorCode) { 474 if (mMetaBugReport == null) { 475 Log.w(TAG, "No bugreport is running"); 476 mIsCollectingBugReport.set(false); 477 return; 478 } 479 // We let the UI know that bug reporting is finished, because the next step is to 480 // zip everything and upload. 481 mBugReportProgress.set(MAX_PROGRESS_VALUE); 482 sendProgressEventToHandler(MAX_PROGRESS_VALUE); 483 showToast(R.string.toast_status_failed); 484 BugStorageUtils.setBugReportStatus( 485 BugReportService.this, mMetaBugReport, 486 Status.STATUS_WRITE_FAILED, getBugReportFailureStatusMessage(errorCode)); 487 mHandler.postDelayed(() -> { 488 mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID); 489 stopForeground(true); 490 }, STOP_SERVICE_DELAY_MILLIS); 491 mHandlerStartedToast.removeCallbacksAndMessages(null); 492 mMetaBugReport = null; 493 mIsCollectingBugReport.set(false); 494 } 495 getBugReportFailureStatusMessage( @arBugreportManager.CarBugreportManagerCallback.CarBugreportErrorCode int errorCode)496 private static String getBugReportFailureStatusMessage( 497 @CarBugreportManager.CarBugreportManagerCallback.CarBugreportErrorCode int errorCode) { 498 switch (errorCode) { 499 case CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED: 500 case CAR_BUGREPORT_DUMPSTATE_FAILED: 501 return "Failed to connect to dumpstate. Retry again after a minute."; 502 case CAR_BUGREPORT_SERVICE_NOT_AVAILABLE: 503 return "Car service is not available. Retry again."; 504 default: 505 return "Car service bugreport collection failed: " + errorCode; 506 } 507 } 508 509 /** 510 * Shows a clickable bugreport finished notification. When clicked it opens 511 * {@link BugReportInfoActivity}. 512 */ showBugReportFinishedNotification(Context context, MetaBugReport bug)513 static void showBugReportFinishedNotification(Context context, MetaBugReport bug) { 514 Intent intent = new Intent(context, BugReportInfoActivity.class); 515 PendingIntent startBugReportInfoActivity = 516 PendingIntent.getActivity(context.getApplicationContext(), 517 /* requestCode= */ 0, intent, PendingIntent.FLAG_IMMUTABLE); 518 Notification notification = new Notification 519 .Builder(context, STATUS_CHANNEL_ID) 520 .setContentTitle(context.getText(R.string.notification_bugreport_finished_title)) 521 .setContentText(bug.getTitle()) 522 .setCategory(Notification.CATEGORY_STATUS) 523 .setSmallIcon(R.drawable.ic_upload) 524 .setContentIntent(startBugReportInfoActivity) 525 .build(); 526 context.getSystemService(NotificationManager.class) 527 .notify(BUGREPORT_FINISHED_NOTIF_ID, notification); 528 } 529 530 /** Moves extra screenshots from a screenshot directory to a given directory. */ moveExtraScreenshots(File destinationDir)531 private void moveExtraScreenshots(File destinationDir) { 532 String screenshotDirPath = ScreenshotUtils.getScreenshotDir(); 533 if (screenshotDirPath == null) { 534 return; 535 } 536 File screenshotDir = new File(screenshotDirPath); 537 if (!screenshotDir.isDirectory()) { 538 return; 539 } 540 for (File file : screenshotDir.listFiles()) { 541 if (file.isDirectory()) { 542 continue; 543 } 544 String destinationPath = destinationDir.getPath() + "/" + file.getName(); 545 try { 546 Files.move(Paths.get(file.getPath()), Paths.get(destinationPath)); 547 Log.i(TAG, "Move a screenshot" + file.getPath() + " to " + destinationPath); 548 } catch (IOException e) { 549 Log.e(TAG, "Cannot move a screenshot" + file.getName() + " to bugreport.", e); 550 } 551 } 552 } 553 554 /** 555 * Zips the temp directory, writes to the system user's {@link FileUtils#getPendingDir} and 556 * updates the bug report status. Note that audio file is always stored in cache directory and 557 * moved by {@link com.android.car.bugreport.BugReportActivity.AddAudioToBugReportAsyncTask}, so 558 * not zipped by this method. 559 * 560 * <p>For {@link MetaBugReport#TYPE_AUDIO_FIRST}: Sets status to either STATUS_UPLOAD_PENDING 561 * or 562 * STATUS_PENDING_USER_ACTION and shows a regular notification. 563 * 564 * <p>For {@link MetaBugReport#TYPE_AUDIO_LATER}: Sets status to STATUS_AUDIO_PENDING and shows 565 * a dialog to record audio message. 566 */ zipDirectoryAndUpdateStatus()567 private void zipDirectoryAndUpdateStatus() { 568 try { 569 // All the generated zip files, images and audio messages are located in this dir. 570 // This is located under the current user. 571 String bugreportFileName = FileUtils.getZipFileName(mMetaBugReport); 572 Log.d(TAG, "Zipping bugreport into " + bugreportFileName); 573 mMetaBugReport = BugStorageUtils.update(this, 574 mMetaBugReport.toBuilder().setBugReportFileName(bugreportFileName).build()); 575 File bugReportTempDir = FileUtils.getTempDir(this, mMetaBugReport.getTimestamp()); 576 577 Log.d(TAG, "Adding extra screenshots into " + bugReportTempDir.getAbsolutePath()); 578 moveExtraScreenshots(bugReportTempDir); 579 580 zipDirectoryToOutputStream(bugReportTempDir, 581 BugStorageUtils.openBugReportFileToWrite(this, mMetaBugReport)); 582 } catch (IOException e) { 583 Log.e(TAG, "Failed to zip files", e); 584 BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED, 585 MESSAGE_FAILURE_ZIP); 586 showToast(R.string.toast_status_failed); 587 return; 588 } 589 if (mMetaBugReport.getType() == MetaBugReport.TYPE_AUDIO_LATER) { 590 BugStorageUtils.setBugReportStatus(BugReportService.this, 591 mMetaBugReport, Status.STATUS_AUDIO_PENDING, /* message= */ ""); 592 playNotificationSound(); 593 startActivity(BugReportActivity.buildAddAudioIntent(this, mMetaBugReport.getId())); 594 } else { 595 // NOTE: If bugreport is TYPE_AUDIO_FIRST, it will already contain an audio message. 596 Status status = mConfig.isAutoUpload() 597 ? Status.STATUS_UPLOAD_PENDING : Status.STATUS_PENDING_USER_ACTION; 598 BugStorageUtils.setBugReportStatus(BugReportService.this, 599 mMetaBugReport, status, /* message= */ ""); 600 showBugReportFinishedNotification(this, mMetaBugReport); 601 } 602 mHandler.post(() -> { 603 mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID); 604 stopForeground(true); 605 }); 606 mHandlerStartedToast.removeCallbacksAndMessages(null); 607 mMetaBugReport = null; 608 mIsCollectingBugReport.set(false); 609 } 610 playNotificationSound()611 private void playNotificationSound() { 612 Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); 613 Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), notification); 614 if (ringtone == null) { 615 Log.w(TAG, "No notification ringtone found."); 616 return; 617 } 618 float volume = ringtone.getVolume(); 619 // Use volume from audio manager, otherwise default ringtone volume can be too loud. 620 AudioManager audioManager = getSystemService(AudioManager.class); 621 if (audioManager != null) { 622 int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION); 623 int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION); 624 volume = (currentVolume + 0.0f) / maxVolume; 625 } 626 Log.v(TAG, "Using volume " + volume); 627 ringtone.setVolume(volume); 628 ringtone.play(); 629 } 630 631 /** 632 * Compresses a directory into a zip file. The method is not recursive. Any sub-directory 633 * contained in the main directory and any files contained in the sub-directories will be 634 * skipped. 635 * 636 * @param dirToZip The path of the directory to zip 637 * @param outStream The output stream to write the zip file to 638 * @throws IOException if the directory does not exist, its files cannot be read, or the output 639 * zip file cannot be written. 640 */ zipDirectoryToOutputStream(File dirToZip, OutputStream outStream)641 private void zipDirectoryToOutputStream(File dirToZip, OutputStream outStream) 642 throws IOException { 643 if (!dirToZip.isDirectory()) { 644 throw new IOException("zip directory does not exist"); 645 } 646 Log.v(TAG, "zipping directory " + dirToZip.getAbsolutePath()); 647 648 File[] listFiles = dirToZip.listFiles(); 649 try (ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(outStream))) { 650 for (File file : listFiles) { 651 if (file.isDirectory()) { 652 continue; 653 } 654 String filename = file.getName(); 655 // only for the zipped output file, we add individual entries to zip file. 656 if (Objects.equals(filename, OUTPUT_ZIP_FILE) 657 || Objects.equals(filename, EXTRA_OUTPUT_ZIP_FILE)) { 658 ZipUtils.extractZippedFileToZipStream(file, zipStream); 659 } else { 660 ZipUtils.addFileToZipStream(file, zipStream); 661 } 662 } 663 } finally { 664 outStream.close(); 665 } 666 // Zipping successful, now cleanup the temp dir. 667 FileUtils.deleteDirectory(dirToZip); 668 } 669 } 670