blob: cce327d2739e51e5a22da3e235f1842e369bb42c [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.Nullable;
22import android.annotation.StringRes;
23import android.app.Notification;
24import android.app.NotificationChannel;
25import android.app.NotificationManager;
26import android.app.Service;
27import android.car.Car;
28import android.car.CarBugreportManager;
29import android.car.CarNotConnectedException;
30import android.content.Intent;
31import android.hardware.display.DisplayManager;
32import android.os.Binder;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.IBinder;
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -070036import android.os.Message;
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070037import android.os.ParcelFileDescriptor;
38import android.util.Log;
39import android.view.Display;
40import android.widget.Toast;
41
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -070042import com.google.common.util.concurrent.AtomicDouble;
43
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070044import libcore.io.IoUtils;
45
46import java.io.BufferedOutputStream;
47import java.io.DataInputStream;
48import java.io.DataOutputStream;
49import java.io.File;
50import java.io.FileInputStream;
51import java.io.FileOutputStream;
52import java.io.IOException;
53import java.io.InputStream;
54import java.io.OutputStream;
55import java.util.ArrayList;
56import java.util.Enumeration;
57import java.util.List;
58import java.util.concurrent.Executors;
59import java.util.concurrent.ScheduledExecutorService;
60import java.util.concurrent.TimeUnit;
61import java.util.concurrent.atomic.AtomicBoolean;
62import java.util.zip.ZipEntry;
63import java.util.zip.ZipFile;
64import java.util.zip.ZipOutputStream;
65
66/**
67 * Service that captures screenshot and bug report using dumpstate and bluetooth snoop logs.
68 *
69 * <p>After collecting all the logs it updates the {@link MetaBugReport} using {@link
70 * BugStorageProvider}, which in turn schedules bug report to upload.
71 */
72public class BugReportService extends Service {
73 private static final String TAG = BugReportService.class.getSimpleName();
74
75 /**
76 * Extra data from intent - current bug report.
77 */
78 static final String EXTRA_META_BUG_REPORT = "meta_bug_report";
79
80 // Wait a short time before starting to capture the bugreport and the screen, so that
81 // bugreport activity can detach from the view tree.
82 // It is ugly to have a timeout, but it is ok here because such a delay should not really
83 // cause bugreport to be tainted with so many other events. If in the future we want to change
84 // this, the best option is probably to wait for onDetach events from view tree.
85 private static final int ACTIVITY_FINISH_DELAY = 1000; //in milliseconds
86
87 private static final String BT_SNOOP_LOG_LOCATION = "/data/misc/bluetooth/logs/btsnoop_hci.log";
88 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
89
90 private static final String NOTIFICATION_STATUS_CHANNEL_ID = "BUGREPORT_STATUS_CHANNEL_ID";
91 private static final int BUGREPORT_IN_PROGRESS_NOTIF_ID = 1;
92
93 // http://cs/android/frameworks/base/core/java/android/app/ActivityView.java
94 private static final String ACTIVITY_VIEW_VIRTUAL_DISPLAY = "ActivityViewVirtualDisplay";
95 private static final String OUTPUT_ZIP_FILE = "output_file.zip";
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -070096
97 private static final String MESSAGE_FAILURE_DUMPSTATE = "Failed to grab dumpstate";
98 private static final String MESSAGE_FAILURE_ZIP = "Failed to zip files";
99
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700100 private static final int PROGRESS_HANDLER_EVENT_PROGRESS = 1;
101 private static final String PROGRESS_HANDLER_DATA_PROGRESS = "progress";
102
103 static final float MAX_PROGRESS_VALUE = 100f;
104
105 /** Binder given to clients. */
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700106 private final IBinder mBinder = new ServiceBinder();
107
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700108 private final AtomicBoolean mIsCollectingBugReport = new AtomicBoolean(false);
109 private final AtomicDouble mBugReportProgress = new AtomicDouble(0);
110
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700111 private MetaBugReport mMetaBugReport;
112 private NotificationManager mNotificationManager;
113 private NotificationChannel mNotificationChannel;
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700114 private ScheduledExecutorService mSingleThreadExecutor;
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700115 private BugReportProgressListener mBugReportProgressListener;
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700116 private Car mCar;
117 private CarBugreportManager mBugreportManager;
118 private CarBugreportManager.CarBugreportManagerCallback mCallback;
119
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700120 /** A handler on the main thread. */
121 private Handler mHandler;
122
123 /** A listener that's notified when bugreport progress changes. */
124 interface BugReportProgressListener {
125 /**
126 * Called when bug report progress changes.
127 *
128 * @param progress - a bug report progress in [0.0, 100.0].
129 */
130 void onProgress(float progress);
131 }
132
133 /** Client binder. */
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700134 public class ServiceBinder extends Binder {
135 BugReportService getService() {
136 // Return this instance of LocalService so clients can call public methods
137 return BugReportService.this;
138 }
139 }
140
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700141 /** A handler on a main thread. */
142 private class BugReportHandler extends Handler {
143 @Override
144 public void handleMessage(Message message) {
145 switch (message.what) {
146 case PROGRESS_HANDLER_EVENT_PROGRESS:
147 if (mBugReportProgressListener != null) {
148 float progress = message.getData().getFloat(PROGRESS_HANDLER_DATA_PROGRESS);
149 mBugReportProgressListener.onProgress(progress);
150 }
151 break;
152 default:
153 Log.d(TAG, "Unknown event " + message.what + ", ignoring.");
154 }
155 }
156 }
157
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700158 @Override
159 public void onCreate() {
160 mNotificationManager = getSystemService(NotificationManager.class);
161 mNotificationChannel = new NotificationChannel(
162 NOTIFICATION_STATUS_CHANNEL_ID,
163 getString(R.string.notification_bugreport_channel_name),
164 NotificationManager.IMPORTANCE_MIN);
165 mNotificationManager.createNotificationChannel(mNotificationChannel);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700166 mSingleThreadExecutor = Executors.newSingleThreadScheduledExecutor();
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700167 mHandler = new BugReportHandler();
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700168 mCar = Car.createCar(this);
169 try {
170 mBugreportManager = (CarBugreportManager) mCar.getCarManager(Car.CAR_BUGREPORT_SERVICE);
171 } catch (CarNotConnectedException | NoClassDefFoundError e) {
172 Log.w(TAG, "Couldn't get CarBugreportManager", e);
173 }
174 }
175
176 @Override
177 public int onStartCommand(final Intent intent, int flags, int startId) {
178 if (mIsCollectingBugReport.get()) {
179 Log.w(TAG, "bug report is already being collected, ignoring");
180 Toast.makeText(this, R.string.toast_bug_report_in_progress, Toast.LENGTH_SHORT).show();
181 return START_NOT_STICKY;
182 }
183 Log.i(TAG, String.format("Will start collecting bug report, version=%s",
184 getPackageVersion(this)));
185 mIsCollectingBugReport.set(true);
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700186 mBugReportProgress.set(0);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700187
188 Notification notification =
189 new Notification.Builder(this, NOTIFICATION_STATUS_CHANNEL_ID)
190 .setContentTitle(getText(R.string.notification_bugreport_started))
191 .setSmallIcon(R.drawable.download_animation)
192 .build();
193 startForeground(BUGREPORT_IN_PROGRESS_NOTIF_ID, notification);
194
195 Bundle extras = intent.getExtras();
196 mMetaBugReport = extras.getParcelable(EXTRA_META_BUG_REPORT);
197
198 collectBugReport();
199
200 // If the service process gets killed due to heavy memory pressure, do not restart.
201 return START_NOT_STICKY;
202 }
203
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700204 /** Returns true if bugreporting is in progress. */
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700205 public boolean isCollectingBugReport() {
206 return mIsCollectingBugReport.get();
207 }
208
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700209 /** Returns current bugreport progress. */
210 public float getBugReportProgress() {
211 return (float) mBugReportProgress.get();
212 }
213
214 /** Sets a bugreport progress listener. The listener is called on a main thread. */
215 public void setBugReportProgressListener(BugReportProgressListener listener) {
216 mBugReportProgressListener = listener;
217 }
218
219 /** Removes the bugreport progress listener. */
220 public void removeBugReportProgressListener() {
221 mBugReportProgressListener = null;
222 }
223
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700224 @Override
225 public IBinder onBind(Intent intent) {
226 return mBinder;
227 }
228
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700229 private void showToast(@StringRes int resId) {
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700230 // run on ui thread.
231 mHandler.post(() -> Toast.makeText(this, getText(resId), Toast.LENGTH_LONG).show());
232 }
233
234 private void collectBugReport() {
235 // Order is important when capturing. Screenshot should be first
236 mSingleThreadExecutor.schedule(
237 this::takeAllScreenshots, ACTIVITY_FINISH_DELAY, TimeUnit.MILLISECONDS);
238 mSingleThreadExecutor.schedule(
239 this::grabBtSnoopLog, ACTIVITY_FINISH_DELAY, TimeUnit.MILLISECONDS);
240 mSingleThreadExecutor.schedule(
241 this::dumpStateToFile, ACTIVITY_FINISH_DELAY, TimeUnit.MILLISECONDS);
242 }
243
244 private void takeAllScreenshots() {
245 for (int displayId : getAvailableDisplayIds()) {
246 takeScreenshot(displayId);
247 }
248 }
249
250 @Nullable
251 private File takeScreenshot(int displayId) {
252 Log.i(TAG, String.format("takeScreenshot displayId=%d", displayId));
253 File result = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(),
254 "-" + displayId + "-screenshot.png");
255 try {
256 if (DEBUG) {
257 Log.d(TAG, "Screen output: " + result.getName());
258 }
259
260 java.lang.Process process = Runtime.getRuntime()
261 .exec("/system/bin/screencap -d " + displayId + " -p "
262 + result.getAbsolutePath());
263
264 // Waits for the command to finish.
265 int err = process.waitFor();
266 if (DEBUG) {
267 Log.d(TAG, "screencap process finished: " + err);
268 }
269 return result;
270 } catch (IOException | InterruptedException e) {
271 Log.e(TAG, "screencap process failed: ", e);
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700272 showToast(R.string.toast_status_screencap_failed);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700273 }
274 return null;
275 }
276
277 private List<Integer> getAvailableDisplayIds() {
278 DisplayManager displayManager = getSystemService(DisplayManager.class);
279 ArrayList<Integer> displayIds = new ArrayList<>();
280 for (Display d : displayManager.getDisplays()) {
281 Log.v(TAG,
282 "getAvailableDisplayIds: d.Name=" + d.getName() + ", d.id=" + d.getDisplayId());
283 // We skip virtual displays as they are not captured by screencap.
284 if (d.getName().contains(ACTIVITY_VIEW_VIRTUAL_DISPLAY)) {
285 continue;
286 }
287 displayIds.add(d.getDisplayId());
288 }
289 return displayIds;
290 }
291
292 private void grabBtSnoopLog() {
293 Log.i(TAG, "Grabbing bt snoop log");
294 File result = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(),
295 "-btsnoop.bin.log");
296 try {
297 copyBinaryStream(new FileInputStream(new File(BT_SNOOP_LOG_LOCATION)),
298 new FileOutputStream(result));
299 } catch (IOException e) {
300 // this regularly happens when snooplog is not enabled so do not log as an error
301 Log.i(TAG, "Failed to grab bt snooplog, continuing to take bug report.", e);
302 }
303 }
304
305 private void dumpStateToFile() {
306 Log.i(TAG, "Dumpstate to file");
307 File outputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(), OUTPUT_ZIP_FILE);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700308
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700309 try (ParcelFileDescriptor outFd = ParcelFileDescriptor.open(outputFile,
310 ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE)) {
311 requestBugReport(outFd);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700312 } catch (IOException | RuntimeException e) {
313 Log.e(TAG, "Failed to grab dump state", e);
314 BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
315 MESSAGE_FAILURE_DUMPSTATE);
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700316 showToast(R.string.toast_status_dump_state_failed);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700317 }
318 }
319
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700320 private void sendProgressEventToHandler(float progress) {
321 Message message = new Message();
322 message.what = PROGRESS_HANDLER_EVENT_PROGRESS;
323 message.getData().putFloat(PROGRESS_HANDLER_DATA_PROGRESS, progress);
324 mHandler.sendMessage(message);
325 }
326
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700327 private void requestBugReport(ParcelFileDescriptor outFd) {
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700328 if (DEBUG) {
329 Log.d(TAG, "Requesting a bug report from CarBugReportManager.");
330 }
331 mCallback = new CarBugreportManager.CarBugreportManagerCallback() {
332 @Override
333 public void onError(int errorCode) {
334 Log.e(TAG, "Bugreport failed " + errorCode);
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700335 showToast(R.string.toast_status_failed);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700336 // TODO(b/133520419): show this error on Info page or add to zip file.
337 scheduleZipTask();
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700338 // We let the UI know that bug reporting is finished, because the next step is to
339 // zip everything and upload.
340 mBugReportProgress.set(MAX_PROGRESS_VALUE);
341 sendProgressEventToHandler(MAX_PROGRESS_VALUE);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700342 }
343
344 @Override
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700345 public void onProgress(@FloatRange(from = 0f, to = MAX_PROGRESS_VALUE) float progress) {
346 mBugReportProgress.set(progress);
347 sendProgressEventToHandler(progress);
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700348 }
349
350 @Override
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700351 public void onFinished() {
352 Log.i(TAG, "Bugreport finished");
353 scheduleZipTask();
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700354 mBugReportProgress.set(MAX_PROGRESS_VALUE);
355 sendProgressEventToHandler(MAX_PROGRESS_VALUE);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700356 }
357 };
Zhomart Mukhamejanovad23f272019-06-04 20:27:24 -0700358 mBugreportManager.requestZippedBugreport(outFd, mCallback);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700359 }
360
361 private void scheduleZipTask() {
362 mSingleThreadExecutor.submit(this::zipDirectoryAndScheduleForUpload);
363 }
364
365 private void zipDirectoryAndScheduleForUpload() {
366 try {
367 // When OutputStream from openBugReportFile is closed, BugStorageProvider automatically
368 // schedules an upload job.
369 zipDirectoryToOutputStream(
370 FileUtils.createTempDir(this, mMetaBugReport.getTimestamp()),
371 BugStorageUtils.openBugReportFile(this, mMetaBugReport));
372 } catch (IOException e) {
373 Log.e(TAG, "Failed to zip files", e);
374 BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
375 MESSAGE_FAILURE_ZIP);
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700376 showToast(R.string.toast_status_failed);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700377 }
378 mIsCollectingBugReport.set(false);
Zhomart Mukhamejanovfcfd3892019-06-05 09:30:53 -0700379 showToast(R.string.toast_status_finished);
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700380 }
381
382 @Override
383 public void onDestroy() {
384 if (DEBUG) {
385 Log.d(TAG, "Service destroyed");
386 }
387 }
388
389 private static void copyBinaryStream(InputStream in, OutputStream out) throws IOException {
390 OutputStream writer = null;
391 InputStream reader = null;
392 try {
393 writer = new DataOutputStream(out);
394 reader = new DataInputStream(in);
395 rawCopyStream(writer, reader);
396 } finally {
397 IoUtils.closeQuietly(reader);
398 IoUtils.closeQuietly(writer);
399 }
400 }
401
402 // does not close the reader or writer.
403 private static void rawCopyStream(OutputStream writer, InputStream reader) throws IOException {
404 int read;
405 byte[] buf = new byte[8192];
406 while ((read = reader.read(buf, 0, buf.length)) > 0) {
407 writer.write(buf, 0, read);
408 }
409 }
410
411 /**
412 * Compresses a directory into a zip file. The method is not recursive. Any sub-directory
413 * contained in the main directory and any files contained in the sub-directories will be
414 * skipped.
415 *
416 * @param dirToZip The path of the directory to zip
417 * @param outStream The output stream to write the zip file to
418 * @throws IOException if the directory does not exist, its files cannot be read, or the output
419 * zip file cannot be written.
420 */
421 private void zipDirectoryToOutputStream(File dirToZip, OutputStream outStream)
422 throws IOException {
423 if (!dirToZip.isDirectory()) {
424 throw new IOException("zip directory does not exist");
425 }
426 Log.v(TAG, "zipping directory " + dirToZip.getAbsolutePath());
427
428 File[] listFiles = dirToZip.listFiles();
429 ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(outStream));
430 try {
431 for (File file : listFiles) {
432 if (file.isDirectory()) {
433 continue;
434 }
435 String filename = file.getName();
Zhomart Mukhamejanov590b2492019-05-31 18:03:11 -0700436 // only for the OUTPUT_FILE, we add invidiual entries to zip file
437 if (filename.equals(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
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
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}