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