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