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.app.Notification; 27 import android.app.NotificationChannel; 28 import android.app.NotificationManager; 29 import android.app.PendingIntent; 30 import android.app.Service; 31 import android.car.Car; 32 import android.car.CarBugreportManager; 33 import android.car.CarNotConnectedException; 34 import android.content.Context; 35 import android.content.Intent; 36 import android.content.pm.ServiceInfo; 37 import android.hardware.display.DisplayManager; 38 import android.media.AudioManager; 39 import android.media.Ringtone; 40 import android.media.RingtoneManager; 41 import android.net.Uri; 42 import android.os.Binder; 43 import android.os.Handler; 44 import android.os.IBinder; 45 import android.os.Message; 46 import android.os.ParcelFileDescriptor; 47 import android.util.Log; 48 import android.view.Display; 49 import android.widget.Toast; 50 51 import androidx.annotation.FloatRange; 52 import androidx.annotation.StringRes; 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 buildStartBugReportIntent(Context context)211 static Intent buildStartBugReportIntent(Context context) { 212 Intent intent = new Intent(context, BugReportService.class); 213 intent.setAction(ACTION_START_AUDIO_LATER); 214 return intent; 215 } 216 217 @Override onCreate()218 public void onCreate() { 219 Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled."); 220 221 DisplayManager dm = getSystemService(DisplayManager.class); 222 Display primaryDisplay = dm.getDisplay(DEFAULT_DISPLAY); 223 mWindowContext = createDisplayContext(primaryDisplay) 224 .createWindowContext(TYPE_APPLICATION_OVERLAY, null); 225 226 mNotificationManager = getSystemService(NotificationManager.class); 227 mNotificationManager.createNotificationChannel(new NotificationChannel( 228 PROGRESS_CHANNEL_ID, 229 getString(R.string.notification_bugreport_channel_name), 230 NotificationManager.IMPORTANCE_DEFAULT)); 231 mNotificationManager.createNotificationChannel(new NotificationChannel( 232 STATUS_CHANNEL_ID, 233 getString(R.string.notification_bugreport_channel_name), 234 NotificationManager.IMPORTANCE_HIGH)); 235 mSingleThreadExecutor = Executors.newSingleThreadScheduledExecutor(); 236 mHandler = new BugReportHandler(); 237 mHandlerStartedToast = new Handler(); 238 mConfig = Config.create(getApplicationContext()); 239 } 240 241 @Override onDestroy()242 public void onDestroy() { 243 if (DEBUG) { 244 Log.d(TAG, "Service destroyed"); 245 } 246 disconnectFromCarService(); 247 } 248 249 @Override onStartCommand(Intent intent, int flags, int startId)250 public int onStartCommand(Intent intent, int flags, int startId) { 251 if (mIsCollectingBugReport.get()) { 252 Log.w(TAG, "bug report is already being collected, ignoring"); 253 Toast.makeText(mWindowContext, 254 R.string.toast_bug_report_in_progress, Toast.LENGTH_SHORT).show(); 255 return START_NOT_STICKY; 256 } 257 258 Log.i(TAG, String.format("Will start collecting bug report, version=%s", 259 getPackageVersion(this))); 260 261 String action = intent == null ? null : intent.getAction(); 262 if (ACTION_START_AUDIO_LATER.equals(action) || ACTION_START_SILENT.equals(action)) { 263 Log.i(TAG, "Starting a TYPE_AUDIO_LATER bugreport."); 264 mMetaBugReport = 265 BugReportActivity.createBugReport(this, MetaBugReport.TYPE_AUDIO_LATER); 266 } else if (ACTION_COLLECT_BUGREPORT.equals(action)) { 267 int bugReportId = intent.getIntExtra(EXTRA_META_BUG_REPORT_ID, /* defaultValue= */ -1); 268 mMetaBugReport = BugStorageUtils.findBugReport(this, bugReportId).orElseThrow( 269 () -> new RuntimeException("Failed to find bug report with id " + bugReportId)); 270 } else { 271 Log.w(TAG, "No action provided, ignoring"); 272 return START_NOT_STICKY; 273 } 274 275 mIsCollectingBugReport.set(true); 276 mBugReportProgress.set(0); 277 278 startForeground(BUGREPORT_IN_PROGRESS_NOTIF_ID, buildProgressNotification(), 279 ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE); 280 showProgressNotification(); 281 282 collectBugReport(); 283 284 // Show a short lived "bugreport started" toast message after a short delay. 285 mHandlerStartedToast.postDelayed(() -> { 286 Toast.makeText(mWindowContext, 287 getText(R.string.toast_bug_report_started), Toast.LENGTH_LONG).show(); 288 }, BUGREPORT_STARTED_TOAST_DELAY_MILLIS); 289 290 // If the service process gets killed due to heavy memory pressure, do not restart. 291 return START_NOT_STICKY; 292 } 293 onCarLifecycleChanged(Car car, boolean ready)294 private void onCarLifecycleChanged(Car car, boolean ready) { 295 // not ready - car service is crashed or is restarting. 296 if (!ready) { 297 mBugreportManager = null; 298 mCar = null; 299 300 // NOTE: dumpstate still might be running, but we can't kill it or reconnect to it 301 // so we ignore it. 302 handleBugReportManagerError(CAR_BUGREPORT_SERVICE_NOT_AVAILABLE); 303 return; 304 } 305 try { 306 mBugreportManager = (CarBugreportManager) car.getCarManager(Car.CAR_BUGREPORT_SERVICE); 307 } catch (CarNotConnectedException | NoClassDefFoundError e) { 308 throw new IllegalStateException("Failed to get CarBugreportManager.", e); 309 } 310 } 311 312 /** Shows an updated progress notification. */ showProgressNotification()313 private void showProgressNotification() { 314 if (isCollectingBugReport()) { 315 mNotificationManager.notify( 316 BUGREPORT_IN_PROGRESS_NOTIF_ID, buildProgressNotification()); 317 } 318 } 319 buildProgressNotification()320 private Notification buildProgressNotification() { 321 Intent intent = new Intent(getApplicationContext(), BugReportInfoActivity.class); 322 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 323 PendingIntent startBugReportInfoActivity = 324 PendingIntent.getActivity(getApplicationContext(), /* requestCode= */ 0, intent, 325 PendingIntent.FLAG_IMMUTABLE); 326 return new Notification.Builder(this, PROGRESS_CHANNEL_ID) 327 .setContentTitle(getText(R.string.notification_bugreport_in_progress)) 328 .setContentText(mMetaBugReport.getTitle()) 329 .setSubText(String.format("%.1f%%", mBugReportProgress.get())) 330 .setSmallIcon(R.drawable.download_animation) 331 .setCategory(Notification.CATEGORY_STATUS) 332 .setOngoing(true) 333 .setProgress((int) MAX_PROGRESS_VALUE, (int) mBugReportProgress.get(), false) 334 .setContentIntent(startBugReportInfoActivity) 335 .build(); 336 } 337 338 /** Returns true if bugreporting is in progress. */ isCollectingBugReport()339 public boolean isCollectingBugReport() { 340 return mIsCollectingBugReport.get(); 341 } 342 343 /** Returns current bugreport progress. */ getBugReportProgress()344 public float getBugReportProgress() { 345 return (float) mBugReportProgress.get(); 346 } 347 348 /** Sets a bugreport progress listener. The listener is called on a main thread. */ setBugReportProgressListener(BugReportProgressListener listener)349 public void setBugReportProgressListener(BugReportProgressListener listener) { 350 mBugReportProgressListener = listener; 351 } 352 353 /** Removes the bugreport progress listener. */ removeBugReportProgressListener()354 public void removeBugReportProgressListener() { 355 mBugReportProgressListener = null; 356 } 357 358 @Override onBind(Intent intent)359 public IBinder onBind(Intent intent) { 360 return mBinder; 361 } 362 showToast(@tringRes int resId)363 private void showToast(@StringRes int resId) { 364 // run on ui thread. 365 mHandler.post( 366 () -> Toast.makeText(mWindowContext, getText(resId), Toast.LENGTH_LONG).show()); 367 } 368 disconnectFromCarService()369 private void disconnectFromCarService() { 370 if (mCar != null) { 371 mCar.disconnect(); 372 mCar = null; 373 } 374 mBugreportManager = null; 375 } 376 connectToCarServiceSync()377 private void connectToCarServiceSync() { 378 if (mCar == null || !(mCar.isConnected() || mCar.isConnecting())) { 379 mCar = Car.createCar(this, /* handler= */ null, 380 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, this::onCarLifecycleChanged); 381 } 382 } 383 collectBugReport()384 private void collectBugReport() { 385 // Connect to the car service before collecting bugreport, because when car service crashes, 386 // BugReportService doesn't automatically reconnect to it. 387 connectToCarServiceSync(); 388 389 if (Config.isDebuggable()) { 390 mSingleThreadExecutor.schedule( 391 this::grabBtSnoopLog, ACTIVITY_FINISH_DELAY_MILLIS, TimeUnit.MILLISECONDS); 392 } 393 mSingleThreadExecutor.schedule( 394 this::saveBugReport, ACTIVITY_FINISH_DELAY_MILLIS, TimeUnit.MILLISECONDS); 395 } 396 grabBtSnoopLog()397 private void grabBtSnoopLog() { 398 Log.i(TAG, "Grabbing bt snoop log"); 399 File result = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(), 400 "-btsnoop.bin.log"); 401 File snoopFile = new File(BT_SNOOP_LOG_LOCATION); 402 if (!snoopFile.exists()) { 403 Log.w(TAG, BT_SNOOP_LOG_LOCATION + " not found, skipping"); 404 return; 405 } 406 try (FileInputStream input = new FileInputStream(snoopFile); 407 FileOutputStream output = new FileOutputStream(result)) { 408 ByteStreams.copy(input, output); 409 } catch (IOException e) { 410 // this regularly happens when snooplog is not enabled so do not log as an error 411 Log.i(TAG, "Failed to grab bt snooplog, continuing to take bug report.", e); 412 } 413 } 414 saveBugReport()415 private void saveBugReport() { 416 Log.i(TAG, "Dumpstate to file"); 417 File outputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(), OUTPUT_ZIP_FILE); 418 File extraOutputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(), 419 EXTRA_OUTPUT_ZIP_FILE); 420 try (ParcelFileDescriptor outFd = ParcelFileDescriptor.open(outputFile, 421 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE); 422 ParcelFileDescriptor extraOutFd = ParcelFileDescriptor.open(extraOutputFile, 423 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE)) { 424 requestBugReport(outFd, extraOutFd); 425 } catch (IOException | RuntimeException e) { 426 Log.e(TAG, "Failed to grab dump state", e); 427 BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED, 428 MESSAGE_FAILURE_DUMPSTATE); 429 showToast(R.string.toast_status_dump_state_failed); 430 disconnectFromCarService(); 431 mIsCollectingBugReport.set(false); 432 } 433 } 434 sendProgressEventToHandler(float progress)435 private void sendProgressEventToHandler(float progress) { 436 Message message = new Message(); 437 message.what = PROGRESS_HANDLER_EVENT_PROGRESS; 438 message.getData().putFloat(PROGRESS_HANDLER_DATA_PROGRESS, progress); 439 mHandler.sendMessage(message); 440 } 441 requestBugReport(ParcelFileDescriptor outFd, ParcelFileDescriptor extraOutFd)442 private void requestBugReport(ParcelFileDescriptor outFd, ParcelFileDescriptor extraOutFd) { 443 if (DEBUG) { 444 Log.d(TAG, "Requesting a bug report from CarBugReportManager."); 445 } 446 mCallback = new CarBugreportManager.CarBugreportManagerCallback() { 447 @Override 448 public void onError(int errorCode) { 449 Log.e(TAG, "CarBugreportManager failed: " + errorCode); 450 disconnectFromCarService(); 451 handleBugReportManagerError(errorCode); 452 } 453 454 @Override 455 public void onProgress(@FloatRange(from = 0f, to = MAX_PROGRESS_VALUE) float progress) { 456 mBugReportProgress.set(progress); 457 sendProgressEventToHandler(progress); 458 } 459 460 @Override 461 public void onFinished() { 462 Log.d(TAG, "CarBugreportManager finished"); 463 disconnectFromCarService(); 464 mBugReportProgress.set(MAX_PROGRESS_VALUE); 465 sendProgressEventToHandler(MAX_PROGRESS_VALUE); 466 mSingleThreadExecutor.submit(BugReportService.this::zipDirectoryAndUpdateStatus); 467 } 468 }; 469 if (mBugreportManager == null) { 470 mHandler.post(() -> Toast.makeText(mWindowContext, 471 "Car service is not ready", Toast.LENGTH_LONG).show()); 472 Log.e(TAG, "CarBugReportManager is not ready"); 473 return; 474 } 475 mBugreportManager.requestBugreport(outFd, extraOutFd, mCallback); 476 } 477 handleBugReportManagerError(int errorCode)478 private void handleBugReportManagerError(int errorCode) { 479 if (mMetaBugReport == null) { 480 Log.w(TAG, "No bugreport is running"); 481 mIsCollectingBugReport.set(false); 482 return; 483 } 484 // We let the UI know that bug reporting is finished, because the next step is to 485 // zip everything and upload. 486 mBugReportProgress.set(MAX_PROGRESS_VALUE); 487 sendProgressEventToHandler(MAX_PROGRESS_VALUE); 488 showToast(R.string.toast_status_failed); 489 BugStorageUtils.setBugReportStatus( 490 BugReportService.this, mMetaBugReport, 491 Status.STATUS_WRITE_FAILED, getBugReportFailureStatusMessage(errorCode)); 492 mHandler.postDelayed(() -> { 493 mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID); 494 stopForeground(true); 495 }, STOP_SERVICE_DELAY_MILLIS); 496 mHandlerStartedToast.removeCallbacksAndMessages(null); 497 mMetaBugReport = null; 498 mIsCollectingBugReport.set(false); 499 } 500 getBugReportFailureStatusMessage(int errorCode)501 private static String getBugReportFailureStatusMessage(int errorCode) { 502 switch (errorCode) { 503 case CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED: 504 case CAR_BUGREPORT_DUMPSTATE_FAILED: 505 return "Failed to connect to dumpstate. Retry again after a minute."; 506 case CAR_BUGREPORT_SERVICE_NOT_AVAILABLE: 507 return "Car service is not available. Retry again."; 508 default: 509 return "Car service bugreport collection failed: " + errorCode; 510 } 511 } 512 513 /** 514 * Shows a clickable bugreport finished notification. When clicked it opens 515 * {@link BugReportInfoActivity}. 516 */ showBugReportFinishedNotification(Context context, MetaBugReport bug)517 static void showBugReportFinishedNotification(Context context, MetaBugReport bug) { 518 Intent intent = new Intent(context, BugReportInfoActivity.class); 519 PendingIntent startBugReportInfoActivity = 520 PendingIntent.getActivity(context.getApplicationContext(), 521 /* requestCode= */ 0, intent, PendingIntent.FLAG_IMMUTABLE); 522 Notification notification = new Notification 523 .Builder(context, STATUS_CHANNEL_ID) 524 .setContentTitle(context.getText(R.string.notification_bugreport_finished_title)) 525 .setContentText(bug.getTitle()) 526 .setCategory(Notification.CATEGORY_STATUS) 527 .setSmallIcon(R.drawable.ic_upload) 528 .setContentIntent(startBugReportInfoActivity) 529 .build(); 530 context.getSystemService(NotificationManager.class) 531 .notify(BUGREPORT_FINISHED_NOTIF_ID, notification); 532 } 533 534 /** Moves extra screenshots from a screenshot directory to a given directory. */ moveExtraScreenshots(File destinationDir)535 private void moveExtraScreenshots(File destinationDir) { 536 String screenshotDirPath = ScreenshotUtils.getScreenshotDir(); 537 if (screenshotDirPath == null) { 538 return; 539 } 540 File screenshotDir = new File(screenshotDirPath); 541 if (!screenshotDir.isDirectory()) { 542 return; 543 } 544 for (File file : screenshotDir.listFiles()) { 545 if (file.isDirectory()) { 546 continue; 547 } 548 String destinationPath = destinationDir.getPath() + "/" + file.getName(); 549 try { 550 Files.move(Paths.get(file.getPath()), Paths.get(destinationPath)); 551 Log.i(TAG, "Move a screenshot" + file.getPath() + " to " + destinationPath); 552 } catch (IOException e) { 553 Log.e(TAG, "Cannot move a screenshot" + file.getName() + " to bugreport.", e); 554 } 555 } 556 } 557 558 /** 559 * Zips the temp directory, writes to the system user's {@link FileUtils#getPendingDir} and 560 * updates the bug report status. Note that audio file is always stored in cache directory and 561 * moved by {@link com.android.car.bugreport.BugReportActivity.AddAudioToBugReportAsyncTask}, so 562 * not zipped by this method. 563 * 564 * <p>For {@link MetaBugReport#TYPE_AUDIO_FIRST}: Sets status to either STATUS_UPLOAD_PENDING 565 * or 566 * STATUS_PENDING_USER_ACTION and shows a regular notification. 567 * 568 * <p>For {@link MetaBugReport#TYPE_AUDIO_LATER}: Sets status to STATUS_AUDIO_PENDING and shows 569 * a dialog to record audio message. 570 */ zipDirectoryAndUpdateStatus()571 private void zipDirectoryAndUpdateStatus() { 572 try { 573 // All the generated zip files, images and audio messages are located in this dir. 574 // This is located under the current user. 575 String bugreportFileName = FileUtils.getZipFileName(mMetaBugReport); 576 Log.d(TAG, "Zipping bugreport into " + bugreportFileName); 577 mMetaBugReport = BugStorageUtils.update(this, 578 mMetaBugReport.toBuilder().setBugReportFileName(bugreportFileName).build()); 579 File bugReportTempDir = FileUtils.getTempDir(this, mMetaBugReport.getTimestamp()); 580 581 Log.d(TAG, "Adding extra screenshots into " + bugReportTempDir.getAbsolutePath()); 582 moveExtraScreenshots(bugReportTempDir); 583 584 zipDirectoryToOutputStream(bugReportTempDir, 585 BugStorageUtils.openBugReportFileToWrite(this, mMetaBugReport)); 586 } catch (IOException e) { 587 Log.e(TAG, "Failed to zip files", e); 588 BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED, 589 MESSAGE_FAILURE_ZIP); 590 showToast(R.string.toast_status_failed); 591 return; 592 } 593 if (mMetaBugReport.getType() == MetaBugReport.TYPE_AUDIO_LATER) { 594 BugStorageUtils.setBugReportStatus(BugReportService.this, 595 mMetaBugReport, Status.STATUS_AUDIO_PENDING, /* message= */ ""); 596 playNotificationSound(); 597 startActivity(BugReportActivity.buildAddAudioIntent(this, mMetaBugReport.getId())); 598 } else { 599 // NOTE: If bugreport is TYPE_AUDIO_FIRST, it will already contain an audio message. 600 Status status = mConfig.isAutoUpload() 601 ? Status.STATUS_UPLOAD_PENDING : Status.STATUS_PENDING_USER_ACTION; 602 BugStorageUtils.setBugReportStatus(BugReportService.this, 603 mMetaBugReport, status, /* message= */ ""); 604 showBugReportFinishedNotification(this, mMetaBugReport); 605 } 606 mHandler.post(() -> { 607 mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID); 608 stopForeground(true); 609 }); 610 mHandlerStartedToast.removeCallbacksAndMessages(null); 611 mMetaBugReport = null; 612 mIsCollectingBugReport.set(false); 613 } 614 playNotificationSound()615 private void playNotificationSound() { 616 Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); 617 Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), notification); 618 if (ringtone == null) { 619 Log.w(TAG, "No notification ringtone found."); 620 return; 621 } 622 float volume = ringtone.getVolume(); 623 // Use volume from audio manager, otherwise default ringtone volume can be too loud. 624 AudioManager audioManager = getSystemService(AudioManager.class); 625 if (audioManager != null) { 626 int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION); 627 int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION); 628 volume = (currentVolume + 0.0f) / maxVolume; 629 } 630 Log.v(TAG, "Using volume " + volume); 631 ringtone.setVolume(volume); 632 ringtone.play(); 633 } 634 635 /** 636 * Compresses a directory into a zip file. The method is not recursive. Any sub-directory 637 * contained in the main directory and any files contained in the sub-directories will be 638 * skipped. 639 * 640 * @param dirToZip The path of the directory to zip 641 * @param outStream The output stream to write the zip file to 642 * @throws IOException if the directory does not exist, its files cannot be read, or the output 643 * zip file cannot be written. 644 */ zipDirectoryToOutputStream(File dirToZip, OutputStream outStream)645 private void zipDirectoryToOutputStream(File dirToZip, OutputStream outStream) 646 throws IOException { 647 if (!dirToZip.isDirectory()) { 648 throw new IOException("zip directory does not exist"); 649 } 650 Log.v(TAG, "zipping directory " + dirToZip.getAbsolutePath()); 651 652 File[] listFiles = dirToZip.listFiles(); 653 try (ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(outStream))) { 654 for (File file : listFiles) { 655 if (file.isDirectory()) { 656 continue; 657 } 658 String filename = file.getName(); 659 // only for the zipped output file, we add individual entries to zip file. 660 if (Objects.equals(filename, OUTPUT_ZIP_FILE) 661 || Objects.equals(filename, EXTRA_OUTPUT_ZIP_FILE)) { 662 ZipUtils.extractZippedFileToZipStream(file, zipStream); 663 } else { 664 ZipUtils.addFileToZipStream(file, zipStream); 665 } 666 } 667 } finally { 668 outStream.close(); 669 } 670 // Zipping successful, now cleanup the temp dir. 671 FileUtils.deleteDirectory(dirToZip); 672 } 673 } 674