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 PendingIntent startBugReportInfoActivity = 296 PendingIntent.getActivity(getApplicationContext(), 0, intent, 0); 297 return new Notification.Builder(this, PROGRESS_CHANNEL_ID) 298 .setContentTitle(getText(R.string.notification_bugreport_in_progress)) 299 .setContentText(mMetaBugReport.getTitle()) 300 .setSubText(String.format("%.1f%%", mBugReportProgress.get())) 301 .setSmallIcon(R.drawable.download_animation) 302 .setCategory(Notification.CATEGORY_STATUS) 303 .setOngoing(true) 304 .setProgress((int) MAX_PROGRESS_VALUE, (int) mBugReportProgress.get(), false) 305 .setContentIntent(startBugReportInfoActivity) 306 .build(); 307 } 308 309 /** Returns true if bugreporting is in progress. */ isCollectingBugReport()310 public boolean isCollectingBugReport() { 311 return mIsCollectingBugReport.get(); 312 } 313 314 /** Returns current bugreport progress. */ getBugReportProgress()315 public float getBugReportProgress() { 316 return (float) mBugReportProgress.get(); 317 } 318 319 /** Sets a bugreport progress listener. The listener is called on a main thread. */ setBugReportProgressListener(BugReportProgressListener listener)320 public void setBugReportProgressListener(BugReportProgressListener listener) { 321 mBugReportProgressListener = listener; 322 } 323 324 /** Removes the bugreport progress listener. */ removeBugReportProgressListener()325 public void removeBugReportProgressListener() { 326 mBugReportProgressListener = null; 327 } 328 329 @Override onBind(Intent intent)330 public IBinder onBind(Intent intent) { 331 return mBinder; 332 } 333 showToast(@tringRes int resId)334 private void showToast(@StringRes int resId) { 335 // run on ui thread. 336 mHandler.post( 337 () -> Toast.makeText(mWindowContext, getText(resId), Toast.LENGTH_LONG).show()); 338 } 339 disconnectFromCarService()340 private void disconnectFromCarService() { 341 if (mCar != null) { 342 mCar.disconnect(); 343 mCar = null; 344 } 345 mBugreportManager = null; 346 } 347 connectToCarServiceSync()348 private void connectToCarServiceSync() { 349 if (mCar == null || !(mCar.isConnected() || mCar.isConnecting())) { 350 mCar = Car.createCar(this, /* handler= */ null, 351 Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, this::onCarLifecycleChanged); 352 } 353 } 354 collectBugReport()355 private void collectBugReport() { 356 // Connect to the car service before collecting bugreport, because when car service crashes, 357 // BugReportService doesn't automatically reconnect to it. 358 connectToCarServiceSync(); 359 360 if (Build.IS_USERDEBUG || Build.IS_ENG) { 361 mSingleThreadExecutor.schedule( 362 this::grabBtSnoopLog, ACTIVITY_FINISH_DELAY_MILLIS, TimeUnit.MILLISECONDS); 363 } 364 mSingleThreadExecutor.schedule( 365 this::saveBugReport, ACTIVITY_FINISH_DELAY_MILLIS, TimeUnit.MILLISECONDS); 366 } 367 grabBtSnoopLog()368 private void grabBtSnoopLog() { 369 Log.i(TAG, "Grabbing bt snoop log"); 370 File result = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(), 371 "-btsnoop.bin.log"); 372 File snoopFile = new File(BT_SNOOP_LOG_LOCATION); 373 if (!snoopFile.exists()) { 374 Log.w(TAG, BT_SNOOP_LOG_LOCATION + " not found, skipping"); 375 return; 376 } 377 try (FileInputStream input = new FileInputStream(snoopFile); 378 FileOutputStream output = new FileOutputStream(result)) { 379 ByteStreams.copy(input, output); 380 } catch (IOException e) { 381 // this regularly happens when snooplog is not enabled so do not log as an error 382 Log.i(TAG, "Failed to grab bt snooplog, continuing to take bug report.", e); 383 } 384 } 385 saveBugReport()386 private void saveBugReport() { 387 Log.i(TAG, "Dumpstate to file"); 388 File outputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(), OUTPUT_ZIP_FILE); 389 File extraOutputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(), 390 EXTRA_OUTPUT_ZIP_FILE); 391 try (ParcelFileDescriptor outFd = ParcelFileDescriptor.open(outputFile, 392 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE); 393 ParcelFileDescriptor extraOutFd = ParcelFileDescriptor.open(extraOutputFile, 394 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE)) { 395 requestBugReport(outFd, extraOutFd); 396 } catch (IOException | RuntimeException e) { 397 Log.e(TAG, "Failed to grab dump state", e); 398 BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED, 399 MESSAGE_FAILURE_DUMPSTATE); 400 showToast(R.string.toast_status_dump_state_failed); 401 disconnectFromCarService(); 402 mIsCollectingBugReport.set(false); 403 } 404 } 405 sendProgressEventToHandler(float progress)406 private void sendProgressEventToHandler(float progress) { 407 Message message = new Message(); 408 message.what = PROGRESS_HANDLER_EVENT_PROGRESS; 409 message.getData().putFloat(PROGRESS_HANDLER_DATA_PROGRESS, progress); 410 mHandler.sendMessage(message); 411 } 412 requestBugReport(ParcelFileDescriptor outFd, ParcelFileDescriptor extraOutFd)413 private void requestBugReport(ParcelFileDescriptor outFd, ParcelFileDescriptor extraOutFd) { 414 if (DEBUG) { 415 Log.d(TAG, "Requesting a bug report from CarBugReportManager."); 416 } 417 mCallback = new CarBugreportManager.CarBugreportManagerCallback() { 418 @Override 419 public void onError(@CarBugreportErrorCode int errorCode) { 420 Log.e(TAG, "CarBugreportManager failed: " + errorCode); 421 disconnectFromCarService(); 422 handleBugReportManagerError(errorCode); 423 } 424 425 @Override 426 public void onProgress(@FloatRange(from = 0f, to = MAX_PROGRESS_VALUE) float progress) { 427 mBugReportProgress.set(progress); 428 sendProgressEventToHandler(progress); 429 } 430 431 @Override 432 public void onFinished() { 433 Log.d(TAG, "CarBugreportManager finished"); 434 disconnectFromCarService(); 435 mBugReportProgress.set(MAX_PROGRESS_VALUE); 436 sendProgressEventToHandler(MAX_PROGRESS_VALUE); 437 mSingleThreadExecutor.submit(BugReportService.this::zipDirectoryAndUpdateStatus); 438 } 439 }; 440 if (mBugreportManager == null) { 441 mHandler.post(() -> Toast.makeText(mWindowContext, 442 "Car service is not ready", Toast.LENGTH_LONG).show()); 443 Log.e(TAG, "CarBugReportManager is not ready"); 444 return; 445 } 446 mBugreportManager.requestBugreport(outFd, extraOutFd, mCallback); 447 } 448 handleBugReportManagerError( @arBugreportManager.CarBugreportManagerCallback.CarBugreportErrorCode int errorCode)449 private void handleBugReportManagerError( 450 @CarBugreportManager.CarBugreportManagerCallback.CarBugreportErrorCode int errorCode) { 451 if (mMetaBugReport == null) { 452 Log.w(TAG, "No bugreport is running"); 453 mIsCollectingBugReport.set(false); 454 return; 455 } 456 // We let the UI know that bug reporting is finished, because the next step is to 457 // zip everything and upload. 458 mBugReportProgress.set(MAX_PROGRESS_VALUE); 459 sendProgressEventToHandler(MAX_PROGRESS_VALUE); 460 showToast(R.string.toast_status_failed); 461 BugStorageUtils.setBugReportStatus( 462 BugReportService.this, mMetaBugReport, 463 Status.STATUS_WRITE_FAILED, getBugReportFailureStatusMessage(errorCode)); 464 mHandler.postDelayed(() -> { 465 mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID); 466 stopForeground(true); 467 }, STOP_SERVICE_DELAY_MILLIS); 468 mHandlerStartedToast.removeCallbacksAndMessages(null); 469 mMetaBugReport = null; 470 mIsCollectingBugReport.set(false); 471 } 472 getBugReportFailureStatusMessage( @arBugreportManager.CarBugreportManagerCallback.CarBugreportErrorCode int errorCode)473 private static String getBugReportFailureStatusMessage( 474 @CarBugreportManager.CarBugreportManagerCallback.CarBugreportErrorCode int errorCode) { 475 switch (errorCode) { 476 case CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED: 477 case CAR_BUGREPORT_DUMPSTATE_FAILED: 478 return "Failed to connect to dumpstate. Retry again after a minute."; 479 case CAR_BUGREPORT_SERVICE_NOT_AVAILABLE: 480 return "Car service is not available. Retry again."; 481 default: 482 return "Car service bugreport collection failed: " + errorCode; 483 } 484 } 485 486 /** 487 * Shows a clickable bugreport finished notification. When clicked it opens 488 * {@link BugReportInfoActivity}. 489 */ showBugReportFinishedNotification(Context context, MetaBugReport bug)490 static void showBugReportFinishedNotification(Context context, MetaBugReport bug) { 491 Intent intent = new Intent(context, BugReportInfoActivity.class); 492 PendingIntent startBugReportInfoActivity = 493 PendingIntent.getActivity(context, 0, intent, 0); 494 Notification notification = new Notification 495 .Builder(context, STATUS_CHANNEL_ID) 496 .setContentTitle(context.getText(R.string.notification_bugreport_finished_title)) 497 .setContentText(bug.getTitle()) 498 .setCategory(Notification.CATEGORY_STATUS) 499 .setSmallIcon(R.drawable.ic_upload) 500 .setContentIntent(startBugReportInfoActivity) 501 .build(); 502 context.getSystemService(NotificationManager.class) 503 .notify(BUGREPORT_FINISHED_NOTIF_ID, notification); 504 } 505 506 /** 507 * Zips the temp directory, writes to the system user's {@link FileUtils#getPendingDir} and 508 * updates the bug report status. 509 * 510 * <p>For {@link MetaBugReport#TYPE_INTERACTIVE}: Sets status to either STATUS_UPLOAD_PENDING or 511 * STATUS_PENDING_USER_ACTION and shows a regular notification. 512 * 513 * <p>For {@link MetaBugReport#TYPE_SILENT}: Sets status to STATUS_AUDIO_PENDING and shows 514 * a dialog to record audio message. 515 */ zipDirectoryAndUpdateStatus()516 private void zipDirectoryAndUpdateStatus() { 517 try { 518 // All the generated zip files, images and audio messages are located in this dir. 519 // This is located under the current user. 520 String bugreportFileName = FileUtils.getZipFileName(mMetaBugReport); 521 Log.d(TAG, "Zipping bugreport into " + bugreportFileName); 522 mMetaBugReport = BugStorageUtils.update(this, 523 mMetaBugReport.toBuilder().setBugReportFileName(bugreportFileName).build()); 524 File bugReportTempDir = FileUtils.createTempDir(this, mMetaBugReport.getTimestamp()); 525 zipDirectoryToOutputStream(bugReportTempDir, 526 BugStorageUtils.openBugReportFileToWrite(this, mMetaBugReport)); 527 } catch (IOException e) { 528 Log.e(TAG, "Failed to zip files", e); 529 BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED, 530 MESSAGE_FAILURE_ZIP); 531 showToast(R.string.toast_status_failed); 532 return; 533 } 534 if (mMetaBugReport.getType() == MetaBugReport.TYPE_SILENT) { 535 BugStorageUtils.setBugReportStatus(BugReportService.this, 536 mMetaBugReport, Status.STATUS_AUDIO_PENDING, /* message= */ ""); 537 playNotificationSound(); 538 startActivity(BugReportActivity.buildAddAudioIntent(this, mMetaBugReport)); 539 } else { 540 // NOTE: If bugreport type is INTERACTIVE, it will already contain an audio message. 541 Status status = mConfig.getAutoUpload() 542 ? Status.STATUS_UPLOAD_PENDING : Status.STATUS_PENDING_USER_ACTION; 543 BugStorageUtils.setBugReportStatus(BugReportService.this, 544 mMetaBugReport, status, /* message= */ ""); 545 showBugReportFinishedNotification(this, mMetaBugReport); 546 } 547 mHandler.post(() -> { 548 mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID); 549 stopForeground(true); 550 }); 551 mHandlerStartedToast.removeCallbacksAndMessages(null); 552 mMetaBugReport = null; 553 mIsCollectingBugReport.set(false); 554 } 555 playNotificationSound()556 private void playNotificationSound() { 557 Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); 558 Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), notification); 559 if (ringtone == null) { 560 Log.w(TAG, "No notification ringtone found."); 561 return; 562 } 563 float volume = ringtone.getVolume(); 564 // Use volume from audio manager, otherwise default ringtone volume can be too loud. 565 AudioManager audioManager = getSystemService(AudioManager.class); 566 if (audioManager != null) { 567 int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION); 568 int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION); 569 volume = (currentVolume + 0.0f) / maxVolume; 570 } 571 Log.v(TAG, "Using volume " + volume); 572 ringtone.setVolume(volume); 573 ringtone.play(); 574 } 575 576 /** 577 * Compresses a directory into a zip file. The method is not recursive. Any sub-directory 578 * contained in the main directory and any files contained in the sub-directories will be 579 * skipped. 580 * 581 * @param dirToZip The path of the directory to zip 582 * @param outStream The output stream to write the zip file to 583 * @throws IOException if the directory does not exist, its files cannot be read, or the output 584 * zip file cannot be written. 585 */ zipDirectoryToOutputStream(File dirToZip, OutputStream outStream)586 private void zipDirectoryToOutputStream(File dirToZip, OutputStream outStream) 587 throws IOException { 588 if (!dirToZip.isDirectory()) { 589 throw new IOException("zip directory does not exist"); 590 } 591 Log.v(TAG, "zipping directory " + dirToZip.getAbsolutePath()); 592 593 File[] listFiles = dirToZip.listFiles(); 594 try (ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(outStream))) { 595 for (File file : listFiles) { 596 if (file.isDirectory()) { 597 continue; 598 } 599 String filename = file.getName(); 600 // only for the zipped output file, we add individual entries to zip file. 601 if (filename.equals(OUTPUT_ZIP_FILE) || filename.equals(EXTRA_OUTPUT_ZIP_FILE)) { 602 ZipUtils.extractZippedFileToZipStream(file, zipStream); 603 } else { 604 ZipUtils.addFileToZipStream(file, zipStream); 605 } 606 } 607 } finally { 608 outStream.close(); 609 } 610 // Zipping successful, now cleanup the temp dir. 611 FileUtils.deleteDirectory(dirToZip); 612 } 613 } 614