blob: 981faab93722e9fd6b87d66367ade7e85d69a7e5 [file] [log] [blame]
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -07001/*
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 */
16package com.google.android.car.bugreport;
17
18import static com.google.android.car.bugreport.PackageUtils.getPackageVersion;
19
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -070020import android.annotation.FloatRange;
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070021import android.annotation.StringRes;
22import android.app.Notification;
23import android.app.NotificationChannel;
24import android.app.NotificationManager;
25import android.app.Service;
26import android.car.Car;
27import android.car.CarBugreportManager;
28import android.car.CarNotConnectedException;
29import android.content.Intent;
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070030import android.os.Binder;
Selim Guruna85b7e72019-06-07 11:01:42 -070031import android.os.Build;
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070032import android.os.Bundle;
33import android.os.Handler;
34import android.os.IBinder;
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -070035import android.os.Message;
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070036import android.os.ParcelFileDescriptor;
37import android.util.Log;
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070038import android.widget.Toast;
39
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -070040import com.google.common.util.concurrent.AtomicDouble;
41
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070042import libcore.io.IoUtils;
43
44import java.io.BufferedOutputStream;
45import java.io.DataInputStream;
46import java.io.DataOutputStream;
47import java.io.File;
48import java.io.FileInputStream;
49import java.io.FileOutputStream;
50import java.io.IOException;
51import java.io.InputStream;
52import java.io.OutputStream;
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070053import java.util.Enumeration;
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070054import java.util.concurrent.Executors;
55import java.util.concurrent.ScheduledExecutorService;
56import java.util.concurrent.TimeUnit;
57import java.util.concurrent.atomic.AtomicBoolean;
58import java.util.zip.ZipEntry;
59import java.util.zip.ZipFile;
60import java.util.zip.ZipOutputStream;
61
62/**
63 * Service that captures screenshot and bug report using dumpstate and bluetooth snoop logs.
64 *
65 * <p>After collecting all the logs it updates the {@link MetaBugReport} using {@link
66 * BugStorageProvider}, which in turn schedules bug report to upload.
67 */
68public class BugReportService extends Service {
69 private static final String TAG = BugReportService.class.getSimpleName();
70
71 /**
72 * Extra data from intent - current bug report.
73 */
74 static final String EXTRA_META_BUG_REPORT = "meta_bug_report";
75
76 // Wait a short time before starting to capture the bugreport and the screen, so that
77 // bugreport activity can detach from the view tree.
78 // It is ugly to have a timeout, but it is ok here because such a delay should not really
79 // cause bugreport to be tainted with so many other events. If in the future we want to change
80 // this, the best option is probably to wait for onDetach events from view tree.
81 private static final int ACTIVITY_FINISH_DELAY = 1000; //in milliseconds
82
83 private static final String BT_SNOOP_LOG_LOCATION = "/data/misc/bluetooth/logs/btsnoop_hci.log";
84 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
85
86 private static final String NOTIFICATION_STATUS_CHANNEL_ID = "BUGREPORT_STATUS_CHANNEL_ID";
87 private static final int BUGREPORT_IN_PROGRESS_NOTIF_ID = 1;
88
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070089 private static final String OUTPUT_ZIP_FILE = "output_file.zip";
Selim Guruna85b7e72019-06-07 11:01:42 -070090 private static final String EXTRA_OUTPUT_ZIP_FILE = "extra_output_file.zip";
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070091
92 private static final String MESSAGE_FAILURE_DUMPSTATE = "Failed to grab dumpstate";
93 private static final String MESSAGE_FAILURE_ZIP = "Failed to zip files";
94
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -070095 private static final int PROGRESS_HANDLER_EVENT_PROGRESS = 1;
96 private static final String PROGRESS_HANDLER_DATA_PROGRESS = "progress";
97
98 static final float MAX_PROGRESS_VALUE = 100f;
99
100 /** Binder given to clients. */
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700101 private final IBinder mBinder = new ServiceBinder();
102
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700103 private final AtomicBoolean mIsCollectingBugReport = new AtomicBoolean(false);
104 private final AtomicDouble mBugReportProgress = new AtomicDouble(0);
105
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700106 private MetaBugReport mMetaBugReport;
107 private NotificationManager mNotificationManager;
108 private NotificationChannel mNotificationChannel;
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700109 private ScheduledExecutorService mSingleThreadExecutor;
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700110 private BugReportProgressListener mBugReportProgressListener;
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700111 private Car mCar;
112 private CarBugreportManager mBugreportManager;
113 private CarBugreportManager.CarBugreportManagerCallback mCallback;
114
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700115 /** A handler on the main thread. */
116 private Handler mHandler;
117
118 /** A listener that's notified when bugreport progress changes. */
119 interface BugReportProgressListener {
120 /**
121 * Called when bug report progress changes.
122 *
123 * @param progress - a bug report progress in [0.0, 100.0].
124 */
125 void onProgress(float progress);
126 }
127
128 /** Client binder. */
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700129 public class ServiceBinder extends Binder {
130 BugReportService getService() {
131 // Return this instance of LocalService so clients can call public methods
132 return BugReportService.this;
133 }
134 }
135
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700136 /** A handler on a main thread. */
137 private class BugReportHandler extends Handler {
138 @Override
139 public void handleMessage(Message message) {
140 switch (message.what) {
141 case PROGRESS_HANDLER_EVENT_PROGRESS:
142 if (mBugReportProgressListener != null) {
143 float progress = message.getData().getFloat(PROGRESS_HANDLER_DATA_PROGRESS);
144 mBugReportProgressListener.onProgress(progress);
145 }
146 break;
147 default:
148 Log.d(TAG, "Unknown event " + message.what + ", ignoring.");
149 }
150 }
151 }
152
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700153 @Override
154 public void onCreate() {
155 mNotificationManager = getSystemService(NotificationManager.class);
156 mNotificationChannel = new NotificationChannel(
157 NOTIFICATION_STATUS_CHANNEL_ID,
158 getString(R.string.notification_bugreport_channel_name),
159 NotificationManager.IMPORTANCE_MIN);
160 mNotificationManager.createNotificationChannel(mNotificationChannel);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700161 mSingleThreadExecutor = Executors.newSingleThreadScheduledExecutor();
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700162 mHandler = new BugReportHandler();
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700163 mCar = Car.createCar(this);
164 try {
165 mBugreportManager = (CarBugreportManager) mCar.getCarManager(Car.CAR_BUGREPORT_SERVICE);
166 } catch (CarNotConnectedException | NoClassDefFoundError e) {
167 Log.w(TAG, "Couldn't get CarBugreportManager", e);
168 }
169 }
170
171 @Override
172 public int onStartCommand(final Intent intent, int flags, int startId) {
173 if (mIsCollectingBugReport.get()) {
174 Log.w(TAG, "bug report is already being collected, ignoring");
175 Toast.makeText(this, R.string.toast_bug_report_in_progress, Toast.LENGTH_SHORT).show();
176 return START_NOT_STICKY;
177 }
178 Log.i(TAG, String.format("Will start collecting bug report, version=%s",
179 getPackageVersion(this)));
180 mIsCollectingBugReport.set(true);
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700181 mBugReportProgress.set(0);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700182
183 Notification notification =
184 new Notification.Builder(this, NOTIFICATION_STATUS_CHANNEL_ID)
185 .setContentTitle(getText(R.string.notification_bugreport_started))
186 .setSmallIcon(R.drawable.download_animation)
187 .build();
188 startForeground(BUGREPORT_IN_PROGRESS_NOTIF_ID, notification);
189
190 Bundle extras = intent.getExtras();
191 mMetaBugReport = extras.getParcelable(EXTRA_META_BUG_REPORT);
192
193 collectBugReport();
194
195 // If the service process gets killed due to heavy memory pressure, do not restart.
196 return START_NOT_STICKY;
197 }
198
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700199 /** Returns true if bugreporting is in progress. */
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700200 public boolean isCollectingBugReport() {
201 return mIsCollectingBugReport.get();
202 }
203
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700204 /** Returns current bugreport progress. */
205 public float getBugReportProgress() {
206 return (float) mBugReportProgress.get();
207 }
208
209 /** Sets a bugreport progress listener. The listener is called on a main thread. */
210 public void setBugReportProgressListener(BugReportProgressListener listener) {
211 mBugReportProgressListener = listener;
212 }
213
214 /** Removes the bugreport progress listener. */
215 public void removeBugReportProgressListener() {
216 mBugReportProgressListener = null;
217 }
218
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700219 @Override
220 public IBinder onBind(Intent intent) {
221 return mBinder;
222 }
223
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700224 private void showToast(@StringRes int resId) {
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700225 // run on ui thread.
226 mHandler.post(() -> Toast.makeText(this, getText(resId), Toast.LENGTH_LONG).show());
227 }
228
229 private void collectBugReport() {
Selim Guruna85b7e72019-06-07 11:01:42 -0700230 if (Build.IS_USERDEBUG || Build.IS_ENG) {
231 mSingleThreadExecutor.schedule(
232 this::grabBtSnoopLog, ACTIVITY_FINISH_DELAY, TimeUnit.MILLISECONDS);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700233 }
Selim Guruna85b7e72019-06-07 11:01:42 -0700234 mSingleThreadExecutor.schedule(
235 this::saveBugReport, ACTIVITY_FINISH_DELAY, TimeUnit.MILLISECONDS);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700236 }
237
238 private void grabBtSnoopLog() {
239 Log.i(TAG, "Grabbing bt snoop log");
240 File result = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(),
241 "-btsnoop.bin.log");
242 try {
243 copyBinaryStream(new FileInputStream(new File(BT_SNOOP_LOG_LOCATION)),
244 new FileOutputStream(result));
245 } catch (IOException e) {
246 // this regularly happens when snooplog is not enabled so do not log as an error
247 Log.i(TAG, "Failed to grab bt snooplog, continuing to take bug report.", e);
248 }
249 }
250
Selim Guruna85b7e72019-06-07 11:01:42 -0700251 private void saveBugReport() {
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700252 Log.i(TAG, "Dumpstate to file");
253 File outputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(), OUTPUT_ZIP_FILE);
Selim Guruna85b7e72019-06-07 11:01:42 -0700254 File extraOutputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(),
255 EXTRA_OUTPUT_ZIP_FILE);
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700256 try (ParcelFileDescriptor outFd = ParcelFileDescriptor.open(outputFile,
Selim Guruna85b7e72019-06-07 11:01:42 -0700257 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE);
258 ParcelFileDescriptor extraOutFd = ParcelFileDescriptor.open(extraOutputFile,
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700259 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE)) {
Selim Guruna85b7e72019-06-07 11:01:42 -0700260 requestBugReport(outFd, extraOutFd);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700261 } catch (IOException | RuntimeException e) {
262 Log.e(TAG, "Failed to grab dump state", e);
263 BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
264 MESSAGE_FAILURE_DUMPSTATE);
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700265 showToast(R.string.toast_status_dump_state_failed);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700266 }
267 }
268
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700269 private void sendProgressEventToHandler(float progress) {
270 Message message = new Message();
271 message.what = PROGRESS_HANDLER_EVENT_PROGRESS;
272 message.getData().putFloat(PROGRESS_HANDLER_DATA_PROGRESS, progress);
273 mHandler.sendMessage(message);
274 }
275
Selim Guruna85b7e72019-06-07 11:01:42 -0700276 private void requestBugReport(ParcelFileDescriptor outFd, ParcelFileDescriptor extraOutFd) {
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700277 if (DEBUG) {
278 Log.d(TAG, "Requesting a bug report from CarBugReportManager.");
279 }
280 mCallback = new CarBugreportManager.CarBugreportManagerCallback() {
281 @Override
282 public void onError(int errorCode) {
283 Log.e(TAG, "Bugreport failed " + errorCode);
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700284 showToast(R.string.toast_status_failed);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700285 // TODO(b/133520419): show this error on Info page or add to zip file.
286 scheduleZipTask();
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700287 // We let the UI know that bug reporting is finished, because the next step is to
288 // zip everything and upload.
289 mBugReportProgress.set(MAX_PROGRESS_VALUE);
290 sendProgressEventToHandler(MAX_PROGRESS_VALUE);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700291 }
292
293 @Override
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700294 public void onProgress(@FloatRange(from = 0f, to = MAX_PROGRESS_VALUE) float progress) {
295 mBugReportProgress.set(progress);
296 sendProgressEventToHandler(progress);
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700297 }
298
299 @Override
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700300 public void onFinished() {
301 Log.i(TAG, "Bugreport finished");
302 scheduleZipTask();
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700303 mBugReportProgress.set(MAX_PROGRESS_VALUE);
304 sendProgressEventToHandler(MAX_PROGRESS_VALUE);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700305 }
306 };
Selim Guruna85b7e72019-06-07 11:01:42 -0700307 mBugreportManager.requestBugreport(outFd, extraOutFd, mCallback);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700308 }
309
310 private void scheduleZipTask() {
311 mSingleThreadExecutor.submit(this::zipDirectoryAndScheduleForUpload);
312 }
313
314 private void zipDirectoryAndScheduleForUpload() {
315 try {
316 // When OutputStream from openBugReportFile is closed, BugStorageProvider automatically
317 // schedules an upload job.
318 zipDirectoryToOutputStream(
319 FileUtils.createTempDir(this, mMetaBugReport.getTimestamp()),
320 BugStorageUtils.openBugReportFile(this, mMetaBugReport));
321 } catch (IOException e) {
322 Log.e(TAG, "Failed to zip files", e);
323 BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
324 MESSAGE_FAILURE_ZIP);
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700325 showToast(R.string.toast_status_failed);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700326 }
327 mIsCollectingBugReport.set(false);
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700328 showToast(R.string.toast_status_finished);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700329 }
330
331 @Override
332 public void onDestroy() {
333 if (DEBUG) {
334 Log.d(TAG, "Service destroyed");
335 }
336 }
337
338 private static void copyBinaryStream(InputStream in, OutputStream out) throws IOException {
339 OutputStream writer = null;
340 InputStream reader = null;
341 try {
342 writer = new DataOutputStream(out);
343 reader = new DataInputStream(in);
344 rawCopyStream(writer, reader);
345 } finally {
346 IoUtils.closeQuietly(reader);
347 IoUtils.closeQuietly(writer);
348 }
349 }
350
351 // does not close the reader or writer.
352 private static void rawCopyStream(OutputStream writer, InputStream reader) throws IOException {
353 int read;
354 byte[] buf = new byte[8192];
355 while ((read = reader.read(buf, 0, buf.length)) > 0) {
356 writer.write(buf, 0, read);
357 }
358 }
359
360 /**
361 * Compresses a directory into a zip file. The method is not recursive. Any sub-directory
362 * contained in the main directory and any files contained in the sub-directories will be
363 * skipped.
364 *
365 * @param dirToZip The path of the directory to zip
366 * @param outStream The output stream to write the zip file to
367 * @throws IOException if the directory does not exist, its files cannot be read, or the output
368 * zip file cannot be written.
369 */
370 private void zipDirectoryToOutputStream(File dirToZip, OutputStream outStream)
371 throws IOException {
372 if (!dirToZip.isDirectory()) {
373 throw new IOException("zip directory does not exist");
374 }
375 Log.v(TAG, "zipping directory " + dirToZip.getAbsolutePath());
376
377 File[] listFiles = dirToZip.listFiles();
378 ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(outStream));
379 try {
380 for (File file : listFiles) {
381 if (file.isDirectory()) {
382 continue;
383 }
384 String filename = file.getName();
Selim Guruna85b7e72019-06-07 11:01:42 -0700385
386 // only for the zipped output file, we add invidiual entries to zip file
387 if (filename.equals(OUTPUT_ZIP_FILE) || filename.equals(EXTRA_OUTPUT_ZIP_FILE)) {
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700388 extractZippedFileToOutputStream(file, zipStream);
389 } else {
390 FileInputStream reader = new FileInputStream(file);
391 addFileToOutputStream(filename, reader, zipStream);
392 }
393 }
394 } finally {
395 zipStream.close();
396 outStream.close();
397 }
398 // Zipping successful, now cleanup the temp dir.
399 FileUtils.deleteDirectory(dirToZip);
400 }
401
402 private void extractZippedFileToOutputStream(File file, ZipOutputStream zipStream)
403 throws IOException {
404 ZipFile zipFile = new ZipFile(file);
405 Enumeration<? extends ZipEntry> entries = zipFile.entries();
406 while (entries.hasMoreElements()) {
407 ZipEntry entry = entries.nextElement();
408 InputStream stream = zipFile.getInputStream(entry);
409 addFileToOutputStream(entry.getName(), stream, zipStream);
410 }
411 }
412
413 private void addFileToOutputStream(String filename, InputStream reader,
414 ZipOutputStream zipStream) throws IOException {
415 ZipEntry entry = new ZipEntry(filename);
416 zipStream.putNextEntry(entry);
417 rawCopyStream(zipStream, reader);
418 zipStream.closeEntry();
419 reader.close();
420 }
421}