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