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