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