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.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.content.pm.ServiceInfo;
37 import android.hardware.display.DisplayManager;
38 import android.media.AudioManager;
39 import android.media.Ringtone;
40 import android.media.RingtoneManager;
41 import android.net.Uri;
42 import android.os.Binder;
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.view.Display;
49 import android.widget.Toast;
50 
51 import androidx.annotation.FloatRange;
52 import androidx.annotation.StringRes;
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 
buildStartBugReportIntent(Context context)211     static Intent buildStartBugReportIntent(Context context) {
212         Intent intent = new Intent(context, BugReportService.class);
213         intent.setAction(ACTION_START_AUDIO_LATER);
214         return intent;
215     }
216 
217     @Override
onCreate()218     public void onCreate() {
219         Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled.");
220 
221         DisplayManager dm = getSystemService(DisplayManager.class);
222         Display primaryDisplay = dm.getDisplay(DEFAULT_DISPLAY);
223         mWindowContext = createDisplayContext(primaryDisplay)
224                 .createWindowContext(TYPE_APPLICATION_OVERLAY, null);
225 
226         mNotificationManager = getSystemService(NotificationManager.class);
227         mNotificationManager.createNotificationChannel(new NotificationChannel(
228                 PROGRESS_CHANNEL_ID,
229                 getString(R.string.notification_bugreport_channel_name),
230                 NotificationManager.IMPORTANCE_DEFAULT));
231         mNotificationManager.createNotificationChannel(new NotificationChannel(
232                 STATUS_CHANNEL_ID,
233                 getString(R.string.notification_bugreport_channel_name),
234                 NotificationManager.IMPORTANCE_HIGH));
235         mSingleThreadExecutor = Executors.newSingleThreadScheduledExecutor();
236         mHandler = new BugReportHandler();
237         mHandlerStartedToast = new Handler();
238         mConfig = Config.create(getApplicationContext());
239     }
240 
241     @Override
onDestroy()242     public void onDestroy() {
243         if (DEBUG) {
244             Log.d(TAG, "Service destroyed");
245         }
246         disconnectFromCarService();
247     }
248 
249     @Override
onStartCommand(Intent intent, int flags, int startId)250     public int onStartCommand(Intent intent, int flags, int startId) {
251         if (mIsCollectingBugReport.get()) {
252             Log.w(TAG, "bug report is already being collected, ignoring");
253             Toast.makeText(mWindowContext,
254                     R.string.toast_bug_report_in_progress, Toast.LENGTH_SHORT).show();
255             return START_NOT_STICKY;
256         }
257 
258         Log.i(TAG, String.format("Will start collecting bug report, version=%s",
259                 getPackageVersion(this)));
260 
261         String action = intent == null ? null : intent.getAction();
262         if (ACTION_START_AUDIO_LATER.equals(action) || ACTION_START_SILENT.equals(action)) {
263             Log.i(TAG, "Starting a TYPE_AUDIO_LATER bugreport.");
264             mMetaBugReport =
265                     BugReportActivity.createBugReport(this, MetaBugReport.TYPE_AUDIO_LATER);
266         } else if (ACTION_COLLECT_BUGREPORT.equals(action)) {
267             int bugReportId = intent.getIntExtra(EXTRA_META_BUG_REPORT_ID, /* defaultValue= */ -1);
268             mMetaBugReport = BugStorageUtils.findBugReport(this, bugReportId).orElseThrow(
269                     () -> new RuntimeException("Failed to find bug report with id " + bugReportId));
270         } else {
271             Log.w(TAG, "No action provided, ignoring");
272             return START_NOT_STICKY;
273         }
274 
275         mIsCollectingBugReport.set(true);
276         mBugReportProgress.set(0);
277 
278         startForeground(BUGREPORT_IN_PROGRESS_NOTIF_ID, buildProgressNotification(),
279                 ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
280         showProgressNotification();
281 
282         collectBugReport();
283 
284         // Show a short lived "bugreport started" toast message after a short delay.
285         mHandlerStartedToast.postDelayed(() -> {
286             Toast.makeText(mWindowContext,
287                     getText(R.string.toast_bug_report_started), Toast.LENGTH_LONG).show();
288         }, BUGREPORT_STARTED_TOAST_DELAY_MILLIS);
289 
290         // If the service process gets killed due to heavy memory pressure, do not restart.
291         return START_NOT_STICKY;
292     }
293 
onCarLifecycleChanged(Car car, boolean ready)294     private void onCarLifecycleChanged(Car car, boolean ready) {
295         // not ready - car service is crashed or is restarting.
296         if (!ready) {
297             mBugreportManager = null;
298             mCar = null;
299 
300             // NOTE: dumpstate still might be running, but we can't kill it or reconnect to it
301             //       so we ignore it.
302             handleBugReportManagerError(CAR_BUGREPORT_SERVICE_NOT_AVAILABLE);
303             return;
304         }
305         try {
306             mBugreportManager = (CarBugreportManager) car.getCarManager(Car.CAR_BUGREPORT_SERVICE);
307         } catch (CarNotConnectedException | NoClassDefFoundError e) {
308             throw new IllegalStateException("Failed to get CarBugreportManager.", e);
309         }
310     }
311 
312     /** Shows an updated progress notification. */
showProgressNotification()313     private void showProgressNotification() {
314         if (isCollectingBugReport()) {
315             mNotificationManager.notify(
316                     BUGREPORT_IN_PROGRESS_NOTIF_ID, buildProgressNotification());
317         }
318     }
319 
buildProgressNotification()320     private Notification buildProgressNotification() {
321         Intent intent = new Intent(getApplicationContext(), BugReportInfoActivity.class);
322         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
323         PendingIntent startBugReportInfoActivity =
324                 PendingIntent.getActivity(getApplicationContext(), /* requestCode= */ 0, intent,
325                         PendingIntent.FLAG_IMMUTABLE);
326         return new Notification.Builder(this, PROGRESS_CHANNEL_ID)
327                 .setContentTitle(getText(R.string.notification_bugreport_in_progress))
328                 .setContentText(mMetaBugReport.getTitle())
329                 .setSubText(String.format("%.1f%%", mBugReportProgress.get()))
330                 .setSmallIcon(R.drawable.download_animation)
331                 .setCategory(Notification.CATEGORY_STATUS)
332                 .setOngoing(true)
333                 .setProgress((int) MAX_PROGRESS_VALUE, (int) mBugReportProgress.get(), false)
334                 .setContentIntent(startBugReportInfoActivity)
335                 .build();
336     }
337 
338     /** Returns true if bugreporting is in progress. */
isCollectingBugReport()339     public boolean isCollectingBugReport() {
340         return mIsCollectingBugReport.get();
341     }
342 
343     /** Returns current bugreport progress. */
getBugReportProgress()344     public float getBugReportProgress() {
345         return (float) mBugReportProgress.get();
346     }
347 
348     /** Sets a bugreport progress listener. The listener is called on a main thread. */
setBugReportProgressListener(BugReportProgressListener listener)349     public void setBugReportProgressListener(BugReportProgressListener listener) {
350         mBugReportProgressListener = listener;
351     }
352 
353     /** Removes the bugreport progress listener. */
removeBugReportProgressListener()354     public void removeBugReportProgressListener() {
355         mBugReportProgressListener = null;
356     }
357 
358     @Override
onBind(Intent intent)359     public IBinder onBind(Intent intent) {
360         return mBinder;
361     }
362 
showToast(@tringRes int resId)363     private void showToast(@StringRes int resId) {
364         // run on ui thread.
365         mHandler.post(
366                 () -> Toast.makeText(mWindowContext, getText(resId), Toast.LENGTH_LONG).show());
367     }
368 
disconnectFromCarService()369     private void disconnectFromCarService() {
370         if (mCar != null) {
371             mCar.disconnect();
372             mCar = null;
373         }
374         mBugreportManager = null;
375     }
376 
connectToCarServiceSync()377     private void connectToCarServiceSync() {
378         if (mCar == null || !(mCar.isConnected() || mCar.isConnecting())) {
379             mCar = Car.createCar(this, /* handler= */ null,
380                     Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, this::onCarLifecycleChanged);
381         }
382     }
383 
collectBugReport()384     private void collectBugReport() {
385         // Connect to the car service before collecting bugreport, because when car service crashes,
386         // BugReportService doesn't automatically reconnect to it.
387         connectToCarServiceSync();
388 
389         if (Config.isDebuggable()) {
390             mSingleThreadExecutor.schedule(
391                     this::grabBtSnoopLog, ACTIVITY_FINISH_DELAY_MILLIS, TimeUnit.MILLISECONDS);
392         }
393         mSingleThreadExecutor.schedule(
394                 this::saveBugReport, ACTIVITY_FINISH_DELAY_MILLIS, TimeUnit.MILLISECONDS);
395     }
396 
grabBtSnoopLog()397     private void grabBtSnoopLog() {
398         Log.i(TAG, "Grabbing bt snoop log");
399         File result = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(),
400                 "-btsnoop.bin.log");
401         File snoopFile = new File(BT_SNOOP_LOG_LOCATION);
402         if (!snoopFile.exists()) {
403             Log.w(TAG, BT_SNOOP_LOG_LOCATION + " not found, skipping");
404             return;
405         }
406         try (FileInputStream input = new FileInputStream(snoopFile);
407              FileOutputStream output = new FileOutputStream(result)) {
408             ByteStreams.copy(input, output);
409         } catch (IOException e) {
410             // this regularly happens when snooplog is not enabled so do not log as an error
411             Log.i(TAG, "Failed to grab bt snooplog, continuing to take bug report.", e);
412         }
413     }
414 
saveBugReport()415     private void saveBugReport() {
416         Log.i(TAG, "Dumpstate to file");
417         File outputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(), OUTPUT_ZIP_FILE);
418         File extraOutputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(),
419                 EXTRA_OUTPUT_ZIP_FILE);
420         try (ParcelFileDescriptor outFd = ParcelFileDescriptor.open(outputFile,
421                 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE);
422              ParcelFileDescriptor extraOutFd = ParcelFileDescriptor.open(extraOutputFile,
423                      ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE)) {
424             requestBugReport(outFd, extraOutFd);
425         } catch (IOException | RuntimeException e) {
426             Log.e(TAG, "Failed to grab dump state", e);
427             BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
428                     MESSAGE_FAILURE_DUMPSTATE);
429             showToast(R.string.toast_status_dump_state_failed);
430             disconnectFromCarService();
431             mIsCollectingBugReport.set(false);
432         }
433     }
434 
sendProgressEventToHandler(float progress)435     private void sendProgressEventToHandler(float progress) {
436         Message message = new Message();
437         message.what = PROGRESS_HANDLER_EVENT_PROGRESS;
438         message.getData().putFloat(PROGRESS_HANDLER_DATA_PROGRESS, progress);
439         mHandler.sendMessage(message);
440     }
441 
requestBugReport(ParcelFileDescriptor outFd, ParcelFileDescriptor extraOutFd)442     private void requestBugReport(ParcelFileDescriptor outFd, ParcelFileDescriptor extraOutFd) {
443         if (DEBUG) {
444             Log.d(TAG, "Requesting a bug report from CarBugReportManager.");
445         }
446         mCallback = new CarBugreportManager.CarBugreportManagerCallback() {
447             @Override
448             public void onError(int errorCode) {
449                 Log.e(TAG, "CarBugreportManager failed: " + errorCode);
450                 disconnectFromCarService();
451                 handleBugReportManagerError(errorCode);
452             }
453 
454             @Override
455             public void onProgress(@FloatRange(from = 0f, to = MAX_PROGRESS_VALUE) float progress) {
456                 mBugReportProgress.set(progress);
457                 sendProgressEventToHandler(progress);
458             }
459 
460             @Override
461             public void onFinished() {
462                 Log.d(TAG, "CarBugreportManager finished");
463                 disconnectFromCarService();
464                 mBugReportProgress.set(MAX_PROGRESS_VALUE);
465                 sendProgressEventToHandler(MAX_PROGRESS_VALUE);
466                 mSingleThreadExecutor.submit(BugReportService.this::zipDirectoryAndUpdateStatus);
467             }
468         };
469         if (mBugreportManager == null) {
470             mHandler.post(() -> Toast.makeText(mWindowContext,
471                     "Car service is not ready", Toast.LENGTH_LONG).show());
472             Log.e(TAG, "CarBugReportManager is not ready");
473             return;
474         }
475         mBugreportManager.requestBugreport(outFd, extraOutFd, mCallback);
476     }
477 
handleBugReportManagerError(int errorCode)478     private void handleBugReportManagerError(int errorCode) {
479         if (mMetaBugReport == null) {
480             Log.w(TAG, "No bugreport is running");
481             mIsCollectingBugReport.set(false);
482             return;
483         }
484         // We let the UI know that bug reporting is finished, because the next step is to
485         // zip everything and upload.
486         mBugReportProgress.set(MAX_PROGRESS_VALUE);
487         sendProgressEventToHandler(MAX_PROGRESS_VALUE);
488         showToast(R.string.toast_status_failed);
489         BugStorageUtils.setBugReportStatus(
490                 BugReportService.this, mMetaBugReport,
491                 Status.STATUS_WRITE_FAILED, getBugReportFailureStatusMessage(errorCode));
492         mHandler.postDelayed(() -> {
493             mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID);
494             stopForeground(true);
495         }, STOP_SERVICE_DELAY_MILLIS);
496         mHandlerStartedToast.removeCallbacksAndMessages(null);
497         mMetaBugReport = null;
498         mIsCollectingBugReport.set(false);
499     }
500 
getBugReportFailureStatusMessage(int errorCode)501     private static String getBugReportFailureStatusMessage(int errorCode) {
502         switch (errorCode) {
503             case CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED:
504             case CAR_BUGREPORT_DUMPSTATE_FAILED:
505                 return "Failed to connect to dumpstate. Retry again after a minute.";
506             case CAR_BUGREPORT_SERVICE_NOT_AVAILABLE:
507                 return "Car service is not available. Retry again.";
508             default:
509                 return "Car service bugreport collection failed: " + errorCode;
510         }
511     }
512 
513     /**
514      * Shows a clickable bugreport finished notification. When clicked it opens
515      * {@link BugReportInfoActivity}.
516      */
showBugReportFinishedNotification(Context context, MetaBugReport bug)517     static void showBugReportFinishedNotification(Context context, MetaBugReport bug) {
518         Intent intent = new Intent(context, BugReportInfoActivity.class);
519         PendingIntent startBugReportInfoActivity =
520                 PendingIntent.getActivity(context.getApplicationContext(),
521                         /* requestCode= */ 0, intent, PendingIntent.FLAG_IMMUTABLE);
522         Notification notification = new Notification
523                 .Builder(context, STATUS_CHANNEL_ID)
524                 .setContentTitle(context.getText(R.string.notification_bugreport_finished_title))
525                 .setContentText(bug.getTitle())
526                 .setCategory(Notification.CATEGORY_STATUS)
527                 .setSmallIcon(R.drawable.ic_upload)
528                 .setContentIntent(startBugReportInfoActivity)
529                 .build();
530         context.getSystemService(NotificationManager.class)
531                 .notify(BUGREPORT_FINISHED_NOTIF_ID, notification);
532     }
533 
534     /** Moves extra screenshots from a screenshot directory to a given directory. */
moveExtraScreenshots(File destinationDir)535     private void moveExtraScreenshots(File destinationDir) {
536         String screenshotDirPath = ScreenshotUtils.getScreenshotDir();
537         if (screenshotDirPath == null) {
538             return;
539         }
540         File screenshotDir = new File(screenshotDirPath);
541         if (!screenshotDir.isDirectory()) {
542             return;
543         }
544         for (File file : screenshotDir.listFiles()) {
545             if (file.isDirectory()) {
546                 continue;
547             }
548             String destinationPath = destinationDir.getPath() + "/" + file.getName();
549             try {
550                 Files.move(Paths.get(file.getPath()), Paths.get(destinationPath));
551                 Log.i(TAG, "Move a screenshot" + file.getPath() + " to " + destinationPath);
552             } catch (IOException e) {
553                 Log.e(TAG, "Cannot move a screenshot" + file.getName() + " to bugreport.", e);
554             }
555         }
556     }
557 
558     /**
559      * Zips the temp directory, writes to the system user's {@link FileUtils#getPendingDir} and
560      * updates the bug report status. Note that audio file is always stored in cache directory and
561      * moved by {@link com.android.car.bugreport.BugReportActivity.AddAudioToBugReportAsyncTask}, so
562      * not zipped by this method.
563      *
564      * <p>For {@link MetaBugReport#TYPE_AUDIO_FIRST}: Sets status to either STATUS_UPLOAD_PENDING
565      * or
566      * STATUS_PENDING_USER_ACTION and shows a regular notification.
567      *
568      * <p>For {@link MetaBugReport#TYPE_AUDIO_LATER}: Sets status to STATUS_AUDIO_PENDING and shows
569      * a dialog to record audio message.
570      */
zipDirectoryAndUpdateStatus()571     private void zipDirectoryAndUpdateStatus() {
572         try {
573             // All the generated zip files, images and audio messages are located in this dir.
574             // This is located under the current user.
575             String bugreportFileName = FileUtils.getZipFileName(mMetaBugReport);
576             Log.d(TAG, "Zipping bugreport into " + bugreportFileName);
577             mMetaBugReport = BugStorageUtils.update(this,
578                     mMetaBugReport.toBuilder().setBugReportFileName(bugreportFileName).build());
579             File bugReportTempDir = FileUtils.getTempDir(this, mMetaBugReport.getTimestamp());
580 
581             Log.d(TAG, "Adding extra screenshots into " + bugReportTempDir.getAbsolutePath());
582             moveExtraScreenshots(bugReportTempDir);
583 
584             zipDirectoryToOutputStream(bugReportTempDir,
585                     BugStorageUtils.openBugReportFileToWrite(this, mMetaBugReport));
586         } catch (IOException e) {
587             Log.e(TAG, "Failed to zip files", e);
588             BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
589                     MESSAGE_FAILURE_ZIP);
590             showToast(R.string.toast_status_failed);
591             return;
592         }
593         if (mMetaBugReport.getType() == MetaBugReport.TYPE_AUDIO_LATER) {
594             BugStorageUtils.setBugReportStatus(BugReportService.this,
595                     mMetaBugReport, Status.STATUS_AUDIO_PENDING, /* message= */ "");
596             playNotificationSound();
597             startActivity(BugReportActivity.buildAddAudioIntent(this, mMetaBugReport.getId()));
598         } else {
599             // NOTE: If bugreport is TYPE_AUDIO_FIRST, it will already contain an audio message.
600             Status status = mConfig.isAutoUpload()
601                     ? Status.STATUS_UPLOAD_PENDING : Status.STATUS_PENDING_USER_ACTION;
602             BugStorageUtils.setBugReportStatus(BugReportService.this,
603                     mMetaBugReport, status, /* message= */ "");
604             showBugReportFinishedNotification(this, mMetaBugReport);
605         }
606         mHandler.post(() -> {
607             mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID);
608             stopForeground(true);
609         });
610         mHandlerStartedToast.removeCallbacksAndMessages(null);
611         mMetaBugReport = null;
612         mIsCollectingBugReport.set(false);
613     }
614 
playNotificationSound()615     private void playNotificationSound() {
616         Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
617         Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), notification);
618         if (ringtone == null) {
619             Log.w(TAG, "No notification ringtone found.");
620             return;
621         }
622         float volume = ringtone.getVolume();
623         // Use volume from audio manager, otherwise default ringtone volume can be too loud.
624         AudioManager audioManager = getSystemService(AudioManager.class);
625         if (audioManager != null) {
626             int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION);
627             int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION);
628             volume = (currentVolume + 0.0f) / maxVolume;
629         }
630         Log.v(TAG, "Using volume " + volume);
631         ringtone.setVolume(volume);
632         ringtone.play();
633     }
634 
635     /**
636      * Compresses a directory into a zip file. The method is not recursive. Any sub-directory
637      * contained in the main directory and any files contained in the sub-directories will be
638      * skipped.
639      *
640      * @param dirToZip  The path of the directory to zip
641      * @param outStream The output stream to write the zip file to
642      * @throws IOException if the directory does not exist, its files cannot be read, or the output
643      *                     zip file cannot be written.
644      */
zipDirectoryToOutputStream(File dirToZip, OutputStream outStream)645     private void zipDirectoryToOutputStream(File dirToZip, OutputStream outStream)
646             throws IOException {
647         if (!dirToZip.isDirectory()) {
648             throw new IOException("zip directory does not exist");
649         }
650         Log.v(TAG, "zipping directory " + dirToZip.getAbsolutePath());
651 
652         File[] listFiles = dirToZip.listFiles();
653         try (ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(outStream))) {
654             for (File file : listFiles) {
655                 if (file.isDirectory()) {
656                     continue;
657                 }
658                 String filename = file.getName();
659                 // only for the zipped output file, we add individual entries to zip file.
660                 if (Objects.equals(filename, OUTPUT_ZIP_FILE)
661                         || Objects.equals(filename, EXTRA_OUTPUT_ZIP_FILE)) {
662                     ZipUtils.extractZippedFileToZipStream(file, zipStream);
663                 } else {
664                     ZipUtils.addFileToZipStream(file, zipStream);
665                 }
666             }
667         } finally {
668             outStream.close();
669         }
670         // Zipping successful, now cleanup the temp dir.
671         FileUtils.deleteDirectory(dirToZip);
672     }
673 }
674