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