• 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.google.android.car.bugreport;
17 
18 import static com.google.android.car.bugreport.PackageUtils.getPackageVersion;
19 
20 import android.annotation.FloatRange;
21 import android.annotation.StringRes;
22 import android.app.Notification;
23 import android.app.NotificationChannel;
24 import android.app.NotificationManager;
25 import android.app.PendingIntent;
26 import android.app.Service;
27 import android.car.Car;
28 import android.car.CarBugreportManager;
29 import android.car.CarNotConnectedException;
30 import android.content.Intent;
31 import android.os.Binder;
32 import android.os.Build;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.IBinder;
36 import android.os.Message;
37 import android.os.ParcelFileDescriptor;
38 import android.util.Log;
39 import android.widget.Toast;
40 
41 import com.google.common.util.concurrent.AtomicDouble;
42 
43 import libcore.io.IoUtils;
44 
45 import java.io.BufferedOutputStream;
46 import java.io.DataInputStream;
47 import java.io.DataOutputStream;
48 import java.io.File;
49 import java.io.FileInputStream;
50 import java.io.FileOutputStream;
51 import java.io.IOException;
52 import java.io.InputStream;
53 import java.io.OutputStream;
54 import java.util.Enumeration;
55 import java.util.concurrent.Executors;
56 import java.util.concurrent.ScheduledExecutorService;
57 import java.util.concurrent.TimeUnit;
58 import java.util.concurrent.atomic.AtomicBoolean;
59 import java.util.zip.ZipEntry;
60 import java.util.zip.ZipFile;
61 import java.util.zip.ZipOutputStream;
62 
63 /**
64  * Service that captures screenshot and bug report using dumpstate and bluetooth snoop logs.
65  *
66  * <p>After collecting all the logs it updates the {@link MetaBugReport} using {@link
67  * BugStorageProvider}, which in turn schedules bug report to upload.
68  */
69 public class BugReportService extends Service {
70     private static final String TAG = BugReportService.class.getSimpleName();
71 
72     /**
73      * Extra data from intent - current bug report.
74      */
75     static final String EXTRA_META_BUG_REPORT = "meta_bug_report";
76 
77     // Wait a short time before starting to capture the bugreport and the screen, so that
78     // bugreport activity can detach from the view tree.
79     // It is ugly to have a timeout, but it is ok here because such a delay should not really
80     // cause bugreport to be tainted with so many other events. If in the future we want to change
81     // this, the best option is probably to wait for onDetach events from view tree.
82     private static final int ACTIVITY_FINISH_DELAY_MILLIS = 1000;
83 
84     private static final String BT_SNOOP_LOG_LOCATION = "/data/misc/bluetooth/logs/btsnoop_hci.log";
85     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
86 
87     /** Notifications on this channel will silently appear in notification bar. */
88     private static final String PROGRESS_CHANNEL_ID = "BUGREPORT_PROGRESS_CHANNEL";
89 
90     /** Notifications on this channel will pop-up. */
91     private static final String STATUS_CHANNEL_ID = "BUGREPORT_STATUS_CHANNEL";
92 
93     private static final int BUGREPORT_IN_PROGRESS_NOTIF_ID = 1;
94 
95     /** The notification is shown when bugreport is collected. */
96     static final int BUGREPORT_FINISHED_NOTIF_ID = 2;
97 
98     private static final String OUTPUT_ZIP_FILE = "output_file.zip";
99     private static final String EXTRA_OUTPUT_ZIP_FILE = "extra_output_file.zip";
100 
101     private static final String MESSAGE_FAILURE_DUMPSTATE = "Failed to grab dumpstate";
102     private static final String MESSAGE_FAILURE_ZIP = "Failed to zip files";
103 
104     private static final int PROGRESS_HANDLER_EVENT_PROGRESS = 1;
105     private static final String PROGRESS_HANDLER_DATA_PROGRESS = "progress";
106 
107     static final float MAX_PROGRESS_VALUE = 100f;
108 
109     /** Binder given to clients. */
110     private final IBinder mBinder = new ServiceBinder();
111 
112     private final AtomicBoolean mIsCollectingBugReport = new AtomicBoolean(false);
113     private final AtomicDouble mBugReportProgress = new AtomicDouble(0);
114 
115     private MetaBugReport mMetaBugReport;
116     private NotificationManager mNotificationManager;
117     private ScheduledExecutorService mSingleThreadExecutor;
118     private BugReportProgressListener mBugReportProgressListener;
119     private Car mCar;
120     private CarBugreportManager mBugreportManager;
121     private CarBugreportManager.CarBugreportManagerCallback mCallback;
122 
123     /** A handler on the main thread. */
124     private Handler mHandler;
125 
126     /** A listener that's notified when bugreport progress changes. */
127     interface BugReportProgressListener {
128         /**
129          * Called when bug report progress changes.
130          *
131          * @param progress - a bug report progress in [0.0, 100.0].
132          */
onProgress(float progress)133         void onProgress(float progress);
134     }
135 
136     /** Client binder. */
137     public class ServiceBinder extends Binder {
getService()138         BugReportService getService() {
139             // Return this instance of LocalService so clients can call public methods
140             return BugReportService.this;
141         }
142     }
143 
144     /** A handler on a main thread. */
145     private class BugReportHandler extends Handler {
146         @Override
handleMessage(Message message)147         public void handleMessage(Message message) {
148             switch (message.what) {
149                 case PROGRESS_HANDLER_EVENT_PROGRESS:
150                     if (mBugReportProgressListener != null) {
151                         float progress = message.getData().getFloat(PROGRESS_HANDLER_DATA_PROGRESS);
152                         mBugReportProgressListener.onProgress(progress);
153                     }
154                     showProgressNotification();
155                     break;
156                 default:
157                     Log.d(TAG, "Unknown event " + message.what + ", ignoring.");
158             }
159         }
160     }
161 
162     @Override
onCreate()163     public void onCreate() {
164         mNotificationManager = getSystemService(NotificationManager.class);
165         mNotificationManager.createNotificationChannel(new NotificationChannel(
166                 PROGRESS_CHANNEL_ID,
167                 getString(R.string.notification_bugreport_channel_name),
168                 NotificationManager.IMPORTANCE_DEFAULT));
169         mNotificationManager.createNotificationChannel(new NotificationChannel(
170                 STATUS_CHANNEL_ID,
171                 getString(R.string.notification_bugreport_channel_name),
172                 NotificationManager.IMPORTANCE_HIGH));
173         mSingleThreadExecutor = Executors.newSingleThreadScheduledExecutor();
174         mHandler = new BugReportHandler();
175         mCar = Car.createCar(this);
176         try {
177             mBugreportManager = (CarBugreportManager) mCar.getCarManager(Car.CAR_BUGREPORT_SERVICE);
178         } catch (CarNotConnectedException | NoClassDefFoundError e) {
179             Log.w(TAG, "Couldn't get CarBugreportManager", e);
180         }
181     }
182 
183     @Override
onStartCommand(final Intent intent, int flags, int startId)184     public int onStartCommand(final Intent intent, int flags, int startId) {
185         if (mIsCollectingBugReport.get()) {
186             Log.w(TAG, "bug report is already being collected, ignoring");
187             Toast.makeText(this, R.string.toast_bug_report_in_progress, Toast.LENGTH_SHORT).show();
188             return START_NOT_STICKY;
189         }
190         Log.i(TAG, String.format("Will start collecting bug report, version=%s",
191                 getPackageVersion(this)));
192         mIsCollectingBugReport.set(true);
193         mBugReportProgress.set(0);
194 
195         startForeground(BUGREPORT_IN_PROGRESS_NOTIF_ID, buildProgressNotification());
196         showProgressNotification();
197 
198         Bundle extras = intent.getExtras();
199         mMetaBugReport = extras.getParcelable(EXTRA_META_BUG_REPORT);
200 
201         collectBugReport();
202 
203         // If the service process gets killed due to heavy memory pressure, do not restart.
204         return START_NOT_STICKY;
205     }
206 
207     /** Shows an updated progress notification. */
showProgressNotification()208     private void showProgressNotification() {
209         if (isCollectingBugReport()) {
210             mNotificationManager.notify(
211                     BUGREPORT_IN_PROGRESS_NOTIF_ID, buildProgressNotification());
212         }
213     }
214 
buildProgressNotification()215     private Notification buildProgressNotification() {
216         return new Notification.Builder(this, PROGRESS_CHANNEL_ID)
217                 .setContentTitle(getText(R.string.notification_bugreport_in_progress))
218                 .setSubText(String.format("%.1f%%", mBugReportProgress.get()))
219                 .setSmallIcon(R.drawable.download_animation)
220                 .setCategory(Notification.CATEGORY_STATUS)
221                 .setOngoing(true)
222                 .setProgress((int) MAX_PROGRESS_VALUE, (int) mBugReportProgress.get(), false)
223                 .build();
224     }
225 
226     /** Returns true if bugreporting is in progress. */
isCollectingBugReport()227     public boolean isCollectingBugReport() {
228         return mIsCollectingBugReport.get();
229     }
230 
231     /** Returns current bugreport progress. */
getBugReportProgress()232     public float getBugReportProgress() {
233         return (float) mBugReportProgress.get();
234     }
235 
236     /** Sets a bugreport progress listener. The listener is called on a main thread. */
setBugReportProgressListener(BugReportProgressListener listener)237     public void setBugReportProgressListener(BugReportProgressListener listener) {
238         mBugReportProgressListener = listener;
239     }
240 
241     /** Removes the bugreport progress listener. */
removeBugReportProgressListener()242     public void removeBugReportProgressListener() {
243         mBugReportProgressListener = null;
244     }
245 
246     @Override
onBind(Intent intent)247     public IBinder onBind(Intent intent) {
248         return mBinder;
249     }
250 
showToast(@tringRes int resId)251     private void showToast(@StringRes int resId) {
252         // run on ui thread.
253         mHandler.post(() -> Toast.makeText(this, getText(resId), Toast.LENGTH_LONG).show());
254     }
255 
collectBugReport()256     private void collectBugReport() {
257         if (Build.IS_USERDEBUG || Build.IS_ENG) {
258             mSingleThreadExecutor.schedule(
259                     this::grabBtSnoopLog, ACTIVITY_FINISH_DELAY_MILLIS, TimeUnit.MILLISECONDS);
260         }
261         mSingleThreadExecutor.schedule(
262                 this::saveBugReport, ACTIVITY_FINISH_DELAY_MILLIS, TimeUnit.MILLISECONDS);
263     }
264 
grabBtSnoopLog()265     private void grabBtSnoopLog() {
266         Log.i(TAG, "Grabbing bt snoop log");
267         File result = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(),
268                 "-btsnoop.bin.log");
269         try {
270             copyBinaryStream(new FileInputStream(new File(BT_SNOOP_LOG_LOCATION)),
271                     new FileOutputStream(result));
272         } catch (IOException e) {
273             // this regularly happens when snooplog is not enabled so do not log as an error
274             Log.i(TAG, "Failed to grab bt snooplog, continuing to take bug report.", e);
275         }
276     }
277 
saveBugReport()278     private void saveBugReport() {
279         Log.i(TAG, "Dumpstate to file");
280         File outputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(), OUTPUT_ZIP_FILE);
281         File extraOutputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(),
282                 EXTRA_OUTPUT_ZIP_FILE);
283         try (ParcelFileDescriptor outFd = ParcelFileDescriptor.open(outputFile,
284                 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE);
285              ParcelFileDescriptor extraOutFd = ParcelFileDescriptor.open(extraOutputFile,
286                 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE)) {
287             requestBugReport(outFd, extraOutFd);
288         } catch (IOException | RuntimeException e) {
289             Log.e(TAG, "Failed to grab dump state", e);
290             BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
291                     MESSAGE_FAILURE_DUMPSTATE);
292             showToast(R.string.toast_status_dump_state_failed);
293         }
294     }
295 
sendProgressEventToHandler(float progress)296     private void sendProgressEventToHandler(float progress) {
297         Message message = new Message();
298         message.what = PROGRESS_HANDLER_EVENT_PROGRESS;
299         message.getData().putFloat(PROGRESS_HANDLER_DATA_PROGRESS, progress);
300         mHandler.sendMessage(message);
301     }
302 
requestBugReport(ParcelFileDescriptor outFd, ParcelFileDescriptor extraOutFd)303     private void requestBugReport(ParcelFileDescriptor outFd, ParcelFileDescriptor extraOutFd) {
304         if (DEBUG) {
305             Log.d(TAG, "Requesting a bug report from CarBugReportManager.");
306         }
307         mCallback = new CarBugreportManager.CarBugreportManagerCallback() {
308             @Override
309             public void onError(int errorCode) {
310                 Log.e(TAG, "Bugreport failed " + errorCode);
311                 showToast(R.string.toast_status_failed);
312                 // TODO(b/133520419): show this error on Info page or add to zip file.
313                 scheduleZipTask();
314                 // We let the UI know that bug reporting is finished, because the next step is to
315                 // zip everything and upload.
316                 mBugReportProgress.set(MAX_PROGRESS_VALUE);
317                 sendProgressEventToHandler(MAX_PROGRESS_VALUE);
318             }
319 
320             @Override
321             public void onProgress(@FloatRange(from = 0f, to = MAX_PROGRESS_VALUE) float progress) {
322                 mBugReportProgress.set(progress);
323                 sendProgressEventToHandler(progress);
324             }
325 
326             @Override
327             public void onFinished() {
328                 Log.i(TAG, "Bugreport finished");
329                 scheduleZipTask();
330                 mBugReportProgress.set(MAX_PROGRESS_VALUE);
331                 sendProgressEventToHandler(MAX_PROGRESS_VALUE);
332             }
333         };
334         mBugreportManager.requestBugreport(outFd, extraOutFd, mCallback);
335     }
336 
scheduleZipTask()337     private void scheduleZipTask() {
338         mSingleThreadExecutor.submit(this::zipDirectoryAndScheduleForUpload);
339     }
340 
341     /**
342      * Shows a clickable bugreport finished notification. When clicked it opens
343      * {@link BugReportInfoActivity}.
344      */
showBugReportFinishedNotification()345     private void showBugReportFinishedNotification() {
346         Intent intent = new Intent(getApplicationContext(), BugReportInfoActivity.class);
347         PendingIntent startBugReportInfoActivity =
348                 PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
349         Notification notification = new Notification
350                 .Builder(getApplicationContext(), STATUS_CHANNEL_ID)
351                 .setContentTitle(getText(R.string.notification_bugreport_finished_title))
352                 .setContentText(getText(JobSchedulingUtils.uploadByDefault()
353                         ? R.string.notification_bugreport_auto_upload_finished_text
354                         : R.string.notification_bugreport_manual_upload_finished_text))
355                 .setCategory(Notification.CATEGORY_STATUS)
356                 .setSmallIcon(R.drawable.ic_upload)
357                 .setContentIntent(startBugReportInfoActivity)
358                 .build();
359         mNotificationManager.notify(BUGREPORT_FINISHED_NOTIF_ID, notification);
360     }
361 
zipDirectoryAndScheduleForUpload()362     private void zipDirectoryAndScheduleForUpload() {
363         try {
364             // When OutputStream from openBugReportFile is closed, BugStorageProvider automatically
365             // schedules an upload job.
366             zipDirectoryToOutputStream(
367                     FileUtils.createTempDir(this, mMetaBugReport.getTimestamp()),
368                     BugStorageUtils.openBugReportFile(this, mMetaBugReport));
369             showBugReportFinishedNotification();
370         } catch (IOException e) {
371             Log.e(TAG, "Failed to zip files", e);
372             BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
373                     MESSAGE_FAILURE_ZIP);
374             showToast(R.string.toast_status_failed);
375         }
376         mIsCollectingBugReport.set(false);
377         showToast(R.string.toast_status_finished);
378         mHandler.post(() -> stopForeground(true));
379     }
380 
381     @Override
onDestroy()382     public void onDestroy() {
383         if (DEBUG) {
384             Log.d(TAG, "Service destroyed");
385         }
386     }
387 
copyBinaryStream(InputStream in, OutputStream out)388     private static void copyBinaryStream(InputStream in, OutputStream out) throws IOException {
389         OutputStream writer = null;
390         InputStream reader = null;
391         try {
392             writer = new DataOutputStream(out);
393             reader = new DataInputStream(in);
394             rawCopyStream(writer, reader);
395         } finally {
396             IoUtils.closeQuietly(reader);
397             IoUtils.closeQuietly(writer);
398         }
399     }
400 
401     // does not close the reader or writer.
rawCopyStream(OutputStream writer, InputStream reader)402     private static void rawCopyStream(OutputStream writer, InputStream reader) throws IOException {
403         int read;
404         byte[] buf = new byte[8192];
405         while ((read = reader.read(buf, 0, buf.length)) > 0) {
406             writer.write(buf, 0, read);
407         }
408     }
409 
410     /**
411      * Compresses a directory into a zip file. The method is not recursive. Any sub-directory
412      * contained in the main directory and any files contained in the sub-directories will be
413      * skipped.
414      *
415      * @param dirToZip  The path of the directory to zip
416      * @param outStream The output stream to write the zip file to
417      * @throws IOException if the directory does not exist, its files cannot be read, or the output
418      *                     zip file cannot be written.
419      */
zipDirectoryToOutputStream(File dirToZip, OutputStream outStream)420     private void zipDirectoryToOutputStream(File dirToZip, OutputStream outStream)
421             throws IOException {
422         if (!dirToZip.isDirectory()) {
423             throw new IOException("zip directory does not exist");
424         }
425         Log.v(TAG, "zipping directory " + dirToZip.getAbsolutePath());
426 
427         File[] listFiles = dirToZip.listFiles();
428         ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(outStream));
429         try {
430             for (File file : listFiles) {
431                 if (file.isDirectory()) {
432                     continue;
433                 }
434                 String filename = file.getName();
435 
436                 // only for the zipped output file, we add invidiual entries to zip file
437                 if (filename.equals(OUTPUT_ZIP_FILE) || filename.equals(EXTRA_OUTPUT_ZIP_FILE)) {
438                     extractZippedFileToOutputStream(file, zipStream);
439                 } else {
440                     FileInputStream reader = new FileInputStream(file);
441                     addFileToOutputStream(filename, reader, zipStream);
442                 }
443             }
444         } finally {
445             zipStream.close();
446             outStream.close();
447         }
448         // Zipping successful, now cleanup the temp dir.
449         FileUtils.deleteDirectory(dirToZip);
450     }
451 
extractZippedFileToOutputStream(File file, ZipOutputStream zipStream)452     private void extractZippedFileToOutputStream(File file, ZipOutputStream zipStream)
453             throws IOException {
454         ZipFile zipFile = new ZipFile(file);
455         Enumeration<? extends ZipEntry> entries = zipFile.entries();
456         while (entries.hasMoreElements()) {
457             ZipEntry entry = entries.nextElement();
458             InputStream stream = zipFile.getInputStream(entry);
459             addFileToOutputStream(entry.getName(), stream, zipStream);
460         }
461     }
462 
addFileToOutputStream(String filename, InputStream reader, ZipOutputStream zipStream)463     private void addFileToOutputStream(String filename, InputStream reader,
464             ZipOutputStream zipStream) throws IOException {
465         ZipEntry entry = new ZipEntry(filename);
466         zipStream.putNextEntry(entry);
467         rawCopyStream(zipStream, reader);
468         zipStream.closeEntry();
469         reader.close();
470     }
471 }
472