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