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