Merge "Add screencapture feature to bugreporting" into qt-dev am: 0021b71bbb am: ed9be3922e
am: de06963a75
Change-Id: Ia25e1d083da7f289bd988ebaaa5fe6d2343163e8
diff --git a/car-bugreportd/Android.bp b/car-bugreportd/Android.bp
index e026d0b..36420f0 100644
--- a/car-bugreportd/Android.bp
+++ b/car-bugreportd/Android.bp
@@ -29,7 +29,11 @@
],
shared_libs: [
"libbase",
- "liblog",
"libcutils",
+ "libgui",
+ "libhwui",
+ "liblog",
+ "libui",
+ "libziparchive",
],
}
diff --git a/car-bugreportd/car-bugreportd.rc b/car-bugreportd/car-bugreportd.rc
index 4c405e1..0935a60 100644
--- a/car-bugreportd/car-bugreportd.rc
+++ b/car-bugreportd/car-bugreportd.rc
@@ -1,6 +1,7 @@
service car-bugreportd /system/bin/car-bugreportd
socket car_br_progress_socket stream 0660 shell log
socket car_br_output_socket stream 0660 shell log
+ socket car_br_extra_output_socket stream 0660 shell log
class core
user shell
group log
diff --git a/car-bugreportd/main.cpp b/car-bugreportd/main.cpp
index 055d35a..f2ef87c 100644
--- a/car-bugreportd/main.cpp
+++ b/car-bugreportd/main.cpp
@@ -27,25 +27,34 @@
#include <cutils/sockets.h>
#include <errno.h>
#include <fcntl.h>
+#include <ftw.h>
+#include <gui/SurfaceComposerClient.h>
#include <log/log_main.h>
#include <private/android_filesystem_config.h>
#include <stdio.h>
#include <stdlib.h>
+#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
+#include <ziparchive/zip_writer.h>
#include <chrono>
#include <string>
#include <vector>
namespace {
+// Directory used for keeping temporary files
+constexpr const char* kTempDirectory = "/data/user_de/0/com.android.shell/temp_bugreport_files";
// Socket to write the progress information.
constexpr const char* kCarBrProgressSocket = "car_br_progress_socket";
// Socket to write the zipped bugreport file.
constexpr const char* kCarBrOutputSocket = "car_br_output_socket";
+// Socket to write the extra bugreport zip file. This zip file contains data that does not exist
+// in bugreport file generated by dumpstate.
+constexpr const char* kCarBrExtraOutputSocket = "car_br_extra_output_socket";
// The prefix used by bugreportz protocol to indicate bugreport finished successfully.
constexpr const char* kOkPrefix = "OK:";
// Number of connect attempts to dumpstate socket
@@ -55,6 +64,13 @@
// Wait time for dumpstate. No timeout in dumpstate is longer than 60 seconds. Choose
// a value that is twice longer.
constexpr const int kDumpstateTimeoutInSec = 120;
+// The prefix for screenshot filename in the generated zip file.
+constexpr const char* kScreenshotPrefix = "/screenshot";
+
+using android::OK;
+using android::PhysicalDisplayId;
+using android::status_t;
+using android::SurfaceComposerClient;
// Returns a valid socket descriptor or -1 on failure.
int openSocket(const char* service) {
@@ -96,6 +112,66 @@
return;
}
+// Sends the contents of the zip fileto |outfd|.
+// Returns true if success
+void zipFilesToFd(const std::vector<std::string>& extra_files, int outfd) {
+ // pass fclose as Deleter to close the file when unique_ptr is destroyed.
+ std::unique_ptr<FILE, decltype(fclose)*> outfile = {fdopen(outfd, "wb"), fclose};
+ if (outfile == nullptr) {
+ ALOGE("Failed to open output descriptor");
+ return;
+ }
+ auto writer = std::make_unique<ZipWriter>(outfile.get());
+
+ int error = 0;
+ for (const auto& filepath : extra_files) {
+ const auto name = android::base::Basename(filepath);
+
+ error = writer->StartEntry(name.c_str(), 0);
+ if (error) {
+ ALOGE("Failed to start entry %s", writer->ErrorCodeString(error));
+ return;
+ }
+ android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(filepath.c_str(), O_RDONLY)));
+ if (fd == -1) {
+ return;
+ }
+ while (1) {
+ char buffer[65536];
+
+ ssize_t bytes_read = TEMP_FAILURE_RETRY(read(fd, buffer, sizeof(buffer)));
+ if (bytes_read == 0) {
+ break;
+ }
+ if (bytes_read == -1) {
+ if (errno == EAGAIN) {
+ ALOGE("timed out while reading %s", name.c_str());
+ } else {
+ ALOGE("read terminated abnormally (%s)", strerror(errno));
+ }
+ // fail immediately
+ return;
+ }
+ error = writer->WriteBytes(buffer, bytes_read);
+ if (error) {
+ ALOGE("WriteBytes() failed %s", ZipWriter::ErrorCodeString(error));
+ // fail immediately
+ return;
+ }
+ }
+
+ error = writer->FinishEntry();
+ if (error) {
+ ALOGE("failed to finish entry %s", writer->ErrorCodeString(error));
+ continue;
+ }
+ }
+ error = writer->Finish();
+ if (error) {
+ ALOGE("failed to finish zip writer %s", writer->ErrorCodeString(error));
+ }
+}
+
int copyTo(int fd_in, int fd_out, void* buffer, size_t buffer_len) {
ssize_t bytes_read = TEMP_FAILURE_RETRY(read(fd_in, buffer, buffer_len));
if (bytes_read == 0) {
@@ -182,6 +258,7 @@
line.append(1, c);
}
}
+ *out_bytes_written += bytes_read;
}
s.reset();
// Process final line, in case it didn't finish with newline.
@@ -194,6 +271,168 @@
return true;
}
+bool waitpid_with_timeout(pid_t pid, int timeout_secs, int* status) {
+ sigset_t child_mask, old_mask;
+ sigemptyset(&child_mask);
+ sigaddset(&child_mask, SIGCHLD);
+
+ if (sigprocmask(SIG_BLOCK, &child_mask, &old_mask) == -1) {
+ ALOGE("*** sigprocmask failed: %s\n", strerror(errno));
+ return false;
+ }
+
+ timespec ts = {.tv_sec = timeout_secs, .tv_nsec = 0};
+ int ret = TEMP_FAILURE_RETRY(sigtimedwait(&child_mask, nullptr, &ts));
+ int saved_errno = errno;
+
+ // Set the signals back the way they were.
+ if (sigprocmask(SIG_SETMASK, &old_mask, nullptr) == -1) {
+ ALOGE("*** sigprocmask failed: %s\n", strerror(errno));
+ if (ret == 0) {
+ return false;
+ }
+ }
+ if (ret == -1) {
+ errno = saved_errno;
+ if (errno == EAGAIN) {
+ errno = ETIMEDOUT;
+ } else {
+ ALOGE("*** sigtimedwait failed: %s\n", strerror(errno));
+ }
+ return false;
+ }
+
+ pid_t child_pid = waitpid(pid, status, WNOHANG);
+ if (child_pid != pid) {
+ if (child_pid != -1) {
+ ALOGE("*** Waiting for pid %d, got pid %d instead\n", pid, child_pid);
+ } else {
+ ALOGE("*** waitpid failed: %s\n", strerror(errno));
+ }
+ return false;
+ }
+ return true;
+}
+
+// Runs the given command. Kills the command if it does not finish by timeout.
+int runCommand(int timeout_secs, const char* file, std::vector<const char*> args) {
+ pid_t pid = fork();
+
+ // handle error case
+ if (pid < 0) {
+ ALOGE("fork failed %s", strerror(errno));
+ return pid;
+ }
+
+ // handle child case
+ if (pid == 0) {
+ /* make sure the child dies when parent dies */
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+
+ /* just ignore SIGPIPE, will go down with parent's */
+ struct sigaction sigact;
+ memset(&sigact, 0, sizeof(sigact));
+ sigact.sa_handler = SIG_IGN;
+ sigaction(SIGPIPE, &sigact, nullptr);
+
+ execvp(file, (char**)args.data());
+ // execvp's result will be handled after waitpid_with_timeout() below, but
+ // if it failed, it's safer to exit dumpstate.
+ ALOGE("execvp on command %s failed (error: %s)", file, strerror(errno));
+ _exit(EXIT_FAILURE);
+ }
+
+ // handle parent case
+ int status;
+ bool ret = waitpid_with_timeout(pid, timeout_secs, &status);
+
+ if (!ret) {
+ if (errno == ETIMEDOUT) {
+ ALOGE("command %s timed out (killing pid %d)", file, pid);
+ } else {
+ ALOGE("command %s: Error (killing pid %d)\n", file, pid);
+ }
+ kill(pid, SIGTERM);
+ if (!waitpid_with_timeout(pid, 5, nullptr)) {
+ kill(pid, SIGKILL);
+ if (!waitpid_with_timeout(pid, 5, nullptr)) {
+ ALOGE("could not kill command '%s' (pid %d) even with SIGKILL.\n", file, pid);
+ }
+ }
+ return -1;
+ }
+
+ if (WIFSIGNALED(status)) {
+ ALOGE("command '%s' failed: killed by signal %d\n", file, WTERMSIG(status));
+ } else if (WIFEXITED(status) && WEXITSTATUS(status) > 0) {
+ status = WEXITSTATUS(status);
+ ALOGE("command '%s' failed: exit code %d\n", file, status);
+ }
+
+ return status;
+}
+
+void takeScreenshotForDisplayId(int id, const char* tmp_dir,
+ std::vector<std::string>* extra_files) {
+ std::string id_as_string = std::to_string(id);
+ std::string filename = std::string(tmp_dir) + kScreenshotPrefix + id_as_string + ".png";
+ std::vector<const char*> args { "-p", "-d", id_as_string.c_str(), filename.c_str(), nullptr };
+ ALOGI("capturing screen for display (%d) as %s", id, filename.c_str());
+ int status = runCommand(10, "/system/bin/screencap", args);
+ if (status == 0) {
+ LOG(INFO) << "Screenshot saved for display:" << id_as_string;
+ }
+ // add the file regardless of the exit status of the screencap util.
+ extra_files->push_back(filename);
+
+ LOG(ERROR) << "Failed to take screenshot for display:" << id_as_string;
+}
+
+void takeScreenshot(const char* tmp_dir, std::vector<std::string>* extra_files) {
+ // Now send the screencaptures
+ std::vector<PhysicalDisplayId> ids = SurfaceComposerClient::getPhysicalDisplayIds();
+
+ for (PhysicalDisplayId display_id : ids) {
+ takeScreenshotForDisplayId(display_id, tmp_dir, extra_files);
+ }
+}
+
+bool recursiveRemoveDir(const std::string& path) {
+ auto callback = [](const char* child, const struct stat*, int file_type, struct FTW*) -> int {
+ if (file_type == FTW_DP) {
+ if (rmdir(child) == -1) {
+ ALOGE("rmdir(%s): %s", child, strerror(errno));
+ return -1;
+ }
+ } else if (file_type == FTW_F) {
+ if (unlink(child) == -1) {
+ ALOGE("unlink(%s): %s", child, strerror(errno));
+ return -1;
+ }
+ }
+ return 0;
+ };
+ // do a file tree walk with a sufficiently large depth.
+ return nftw(path.c_str(), callback, 128, FTW_DEPTH) == 0;
+}
+
+status_t createTempDir(const char* dir) {
+ struct stat sb;
+ if (TEMP_FAILURE_RETRY(stat(dir, &sb)) == 0) {
+ if (!recursiveRemoveDir(dir)) {
+ return -errno;
+ }
+ } else if (errno != ENOENT) {
+ ALOGE("Failed to stat %s ", dir);
+ return -errno;
+ }
+ if (TEMP_FAILURE_RETRY(mkdir(dir, 0700)) == -1) {
+ ALOGE("Failed to mkdir %s", dir);
+ return -errno;
+ }
+ return OK;
+}
+
// Removes bugreport
void cleanupBugreportFile(const std::string& zip_path) {
if (unlink(zip_path.c_str()) != 0) {
@@ -204,10 +443,16 @@
} // namespace
int main(void) {
- ALOGE("Starting bugreport collecting service");
+ ALOGI("Starting bugreport collecting service");
auto t0 = std::chrono::steady_clock::now();
+ std::vector<std::string> extra_files;
+ if (createTempDir(kTempDirectory) == OK) {
+ // take screenshots of the physical displays as early as possible
+ takeScreenshot(kTempDirectory, &extra_files);
+ }
+
// Start the dumpstatez service.
android::base::SetProperty("ctl.start", "car-dumpstatez");
@@ -231,6 +476,14 @@
close(output_socket);
}
+ int extra_output_socket = openSocket(kCarBrExtraOutputSocket);
+ if (extra_output_socket != -1 && ret_val) {
+ zipFilesToFd(extra_files, extra_output_socket);
+ }
+ if (extra_output_socket != -1) {
+ close(extra_output_socket);
+ }
+
auto delta = std::chrono::duration_cast<std::chrono::duration<double>>(
std::chrono::steady_clock::now() - t0)
.count();
@@ -239,6 +492,8 @@
ALOGI("bugreport %s in %.02fs, %zu bytes written", result.c_str(), delta, bytes_written);
cleanupBugreportFile(zip_path);
+ recursiveRemoveDir(kTempDirectory);
+
// No matter how doBugreport() finished, let's try to explicitly stop
// car-dumpstatez in case it stalled.
android::base::SetProperty("ctl.stop", "car-dumpstatez");
diff --git a/car-lib/src/android/car/CarBugreportManager.java b/car-lib/src/android/car/CarBugreportManager.java
index b0f7a03..b74c446 100644
--- a/car-lib/src/android/car/CarBugreportManager.java
+++ b/car-lib/src/android/car/CarBugreportManager.java
@@ -159,26 +159,34 @@
}
/**
- * Request a bug report. A zipped bugreport is generated in the background.
+ * Request a bug report. A zipped (i.e. legacy) bugreport is generated in the background
+ * using dumpstate. This API also generates extra files that does not exist in the legacy
+ * bugreport and makes them available through a extra output file. Currently the extra
+ * output contains the screenshots for all the physical displays.
*
* <p>The file descriptor is closed when bugreport is written or if an exception happens.
*
* @param output the zipped bugreport file
+ * @param extraOutput a zip file that contains extra files generated for automotive.
* @param callback the callback for reporting dump status
*/
@RequiresPermission(android.Manifest.permission.DUMP)
- public void requestZippedBugreport(
- @NonNull ParcelFileDescriptor output, @NonNull CarBugreportManagerCallback callback) {
+ public void requestBugreport(
+ @NonNull ParcelFileDescriptor output,
+ @NonNull ParcelFileDescriptor extraOutput,
+ @NonNull CarBugreportManagerCallback callback) {
Preconditions.checkNotNull(output);
+ Preconditions.checkNotNull(extraOutput);
Preconditions.checkNotNull(callback);
try {
CarBugreportManagerCallbackWrapper wrapper =
new CarBugreportManagerCallbackWrapper(callback, mHandler);
- mService.requestZippedBugreport(output, wrapper);
+ mService.requestBugreport(output, extraOutput, wrapper);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} finally {
IoUtils.closeQuietly(output);
+ IoUtils.closeQuietly(extraOutput);
}
}
diff --git a/car-lib/src/android/car/ICarBugreportService.aidl b/car-lib/src/android/car/ICarBugreportService.aidl
index 1ec4e91..2673c80 100644
--- a/car-lib/src/android/car/ICarBugreportService.aidl
+++ b/car-lib/src/android/car/ICarBugreportService.aidl
@@ -28,7 +28,10 @@
/**
* Starts bugreport service to capture a zipped bugreport. The caller needs to provide
* two file descriptors. The "output" file descriptor will be used to provide the actual
- * zip file. The file descriptor is written by the service and will be read by the client.
+ * zip file. The "extra_output" file descriptor will be provided to add files that does not
+ * exist in the original file.
+ * The file descriptor is written by the service and will be read by the client.
*/
- void requestZippedBugreport(in ParcelFileDescriptor output, ICarBugreportCallback callback) = 1;
+ void requestBugreport(in ParcelFileDescriptor output,
+ in ParcelFileDescriptor extraOutput, ICarBugreportCallback callback) = 1;
}
diff --git a/car_product/sepolicy/private/file_contexts b/car_product/sepolicy/private/file_contexts
index 908c853..9666f70 100644
--- a/car_product/sepolicy/private/file_contexts
+++ b/car_product/sepolicy/private/file_contexts
@@ -6,3 +6,4 @@
/system/bin/car-bugreportd u:object_r:dumpstate_exec:s0
/dev/socket/car_br_progress_socket u:object_r:dumpstate_socket:s0
/dev/socket/car_br_output_socket u:object_r:dumpstate_socket:s0
+/dev/socket/car_br_extra_output_socket u:object_r:dumpstate_socket:s0
diff --git a/service/src/com/android/car/CarBugreportManagerService.java b/service/src/com/android/car/CarBugreportManagerService.java
index 572f17d..52d0d33 100644
--- a/service/src/com/android/car/CarBugreportManagerService.java
+++ b/service/src/com/android/car/CarBugreportManagerService.java
@@ -52,7 +52,7 @@
import java.io.PrintWriter;
/**
- * Bugreport service for cars. Should *only* be used on userdebug or eng builds.
+ * Bugreport service for cars.
*/
public class CarBugreportManagerService extends ICarBugreportService.Stub implements
CarServiceBase {
@@ -75,8 +75,10 @@
// definition.
private static final String BUGREPORT_PROGRESS_SOCKET = "car_br_progress_socket";
private static final String BUGREPORT_OUTPUT_SOCKET = "car_br_output_socket";
+ private static final String BUGREPORT_EXTRA_OUTPUT_SOCKET = "car_br_extra_output_socket";
private static final int SOCKET_CONNECTION_MAX_RETRY = 10;
+ private static final int SOCKET_CONNECTION_RETRY_DELAY_IN_MS = 5000;
private final Context mContext;
private final Object mLock = new Object();
@@ -108,7 +110,8 @@
@Override
@RequiresPermission(android.Manifest.permission.DUMP)
- public void requestZippedBugreport(ParcelFileDescriptor data, ICarBugreportCallback callback) {
+ public void requestBugreport(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
+ ICarBugreportCallback callback) {
// Check the caller has proper permission
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP,
@@ -138,23 +141,24 @@
}
synchronized (mLock) {
- requestZippedBugReportLocked(data, callback);
+ requestBugReportLocked(output, extraOutput, callback);
}
}
@GuardedBy("mLock")
- private void requestZippedBugReportLocked(
- ParcelFileDescriptor data, ICarBugreportCallback callback) {
+ private void requestBugReportLocked(ParcelFileDescriptor output,
+ ParcelFileDescriptor extraOutput, ICarBugreportCallback callback) {
if (mIsServiceRunning) {
Slog.w(TAG, "Bugreport Service already running");
reportError(callback, CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS);
return;
}
mIsServiceRunning = true;
- mHandler.post(() -> startBugreportd(data, callback));
+ mHandler.post(() -> startBugreportd(output, extraOutput, callback));
}
- private void startBugreportd(ParcelFileDescriptor data, ICarBugreportCallback callback) {
+ private void startBugreportd(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
+ ICarBugreportCallback callback) {
Slog.i(TAG, "Starting " + BUGREPORTD_SERVICE);
try {
SystemProperties.set("ctl.start", BUGREPORTD_SERVICE);
@@ -163,7 +167,7 @@
reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED);
return;
}
- processBugreportSockets(data, callback);
+ processBugreportSockets(output, extraOutput, callback);
synchronized (mLock) {
mIsServiceRunning = false;
}
@@ -196,9 +200,14 @@
}
}
- private void handleFinished(ParcelFileDescriptor output, ICarBugreportCallback callback) {
+ private void handleFinished(ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
+ ICarBugreportCallback callback) {
Slog.i(TAG, "Finished reading bugreport");
- if (!copySocketToPfd(output, callback)) {
+ // copysockettopfd calls callback.onError on error
+ if (!copySocketToPfd(output, BUGREPORT_OUTPUT_SOCKET, callback)) {
+ return;
+ }
+ if (!copySocketToPfd(extraOutput, BUGREPORT_EXTRA_OUTPUT_SOCKET, callback)) {
return;
}
try {
@@ -216,7 +225,8 @@
* {@code FAIL:message} accordingly.
*/
private void processBugreportSockets(
- ParcelFileDescriptor output, ICarBugreportCallback callback) {
+ ParcelFileDescriptor output, ParcelFileDescriptor extraOutput,
+ ICarBugreportCallback callback) {
LocalSocket localSocket = connectSocket(BUGREPORT_PROGRESS_SOCKET);
if (localSocket == null) {
reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED);
@@ -235,7 +245,7 @@
reportError(callback, CAR_BUGREPORT_DUMPSTATE_FAILED);
return;
} else if (line.startsWith(OK_PREFIX)) {
- handleFinished(output, callback);
+ handleFinished(output, extraOutput, callback);
return;
} else if (!line.startsWith(BEGIN_PREFIX)) {
Slog.w(TAG, "Received unknown progress line from dumpstate: " + line);
@@ -250,8 +260,8 @@
}
private boolean copySocketToPfd(
- ParcelFileDescriptor pfd, ICarBugreportCallback callback) {
- LocalSocket localSocket = connectSocket(BUGREPORT_OUTPUT_SOCKET);
+ ParcelFileDescriptor pfd, String remoteSocket, ICarBugreportCallback callback) {
+ LocalSocket localSocket = connectSocket(remoteSocket);
if (localSocket == null) {
reportError(callback, CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED);
return false;
@@ -292,8 +302,15 @@
// keep retrying until success or reaching timeout.
int retryCount = 0;
while (true) {
- // First connection always fails, so we wait 1 second before trying to connect.
- SystemClock.sleep(/* ms= */ 1000);
+ // There are a few factors impacting the socket delay:
+ // 1. potential system slowness
+ // 2. car-bugreportd takes the screenshots early (before starting dumpstate). This
+ // should be taken into account as the socket opens after screenshots are
+ // captured.
+ // Therefore we are generous in setting the timeout. Most cases should not even
+ // come close to the timeouts, but since bugreports are taken when there is a
+ // system issue, it is hard to guess.
+ SystemClock.sleep(SOCKET_CONNECTION_RETRY_DELAY_IN_MS);
try {
socket.connect(new LocalSocketAddress(socketName,
LocalSocketAddress.Namespace.RESERVED));
@@ -304,7 +321,7 @@
+ " after " + retryCount + " retries", e);
return null;
}
- Log.i(TAG, "Failed to connect to" + socketName + ". Will try again "
+ Log.i(TAG, "Failed to connect to " + socketName + ". Will try again "
+ e.getMessage());
}
}
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
index cce327d..981faab 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
@@ -18,7 +18,6 @@
import static com.google.android.car.bugreport.PackageUtils.getPackageVersion;
import android.annotation.FloatRange;
-import android.annotation.Nullable;
import android.annotation.StringRes;
import android.app.Notification;
import android.app.NotificationChannel;
@@ -28,15 +27,14 @@
import android.car.CarBugreportManager;
import android.car.CarNotConnectedException;
import android.content.Intent;
-import android.hardware.display.DisplayManager;
import android.os.Binder;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.util.Log;
-import android.view.Display;
import android.widget.Toast;
import com.google.common.util.concurrent.AtomicDouble;
@@ -52,9 +50,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.util.ArrayList;
import java.util.Enumeration;
-import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -90,9 +86,8 @@
private static final String NOTIFICATION_STATUS_CHANNEL_ID = "BUGREPORT_STATUS_CHANNEL_ID";
private static final int BUGREPORT_IN_PROGRESS_NOTIF_ID = 1;
- // http://cs/android/frameworks/base/core/java/android/app/ActivityView.java
- private static final String ACTIVITY_VIEW_VIRTUAL_DISPLAY = "ActivityViewVirtualDisplay";
private static final String OUTPUT_ZIP_FILE = "output_file.zip";
+ private static final String EXTRA_OUTPUT_ZIP_FILE = "extra_output_file.zip";
private static final String MESSAGE_FAILURE_DUMPSTATE = "Failed to grab dumpstate";
private static final String MESSAGE_FAILURE_ZIP = "Failed to zip files";
@@ -232,61 +227,12 @@
}
private void collectBugReport() {
- // Order is important when capturing. Screenshot should be first
- mSingleThreadExecutor.schedule(
- this::takeAllScreenshots, ACTIVITY_FINISH_DELAY, TimeUnit.MILLISECONDS);
- mSingleThreadExecutor.schedule(
- this::grabBtSnoopLog, ACTIVITY_FINISH_DELAY, TimeUnit.MILLISECONDS);
- mSingleThreadExecutor.schedule(
- this::dumpStateToFile, ACTIVITY_FINISH_DELAY, TimeUnit.MILLISECONDS);
- }
-
- private void takeAllScreenshots() {
- for (int displayId : getAvailableDisplayIds()) {
- takeScreenshot(displayId);
+ if (Build.IS_USERDEBUG || Build.IS_ENG) {
+ mSingleThreadExecutor.schedule(
+ this::grabBtSnoopLog, ACTIVITY_FINISH_DELAY, TimeUnit.MILLISECONDS);
}
- }
-
- @Nullable
- private File takeScreenshot(int displayId) {
- Log.i(TAG, String.format("takeScreenshot displayId=%d", displayId));
- File result = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(),
- "-" + displayId + "-screenshot.png");
- try {
- if (DEBUG) {
- Log.d(TAG, "Screen output: " + result.getName());
- }
-
- java.lang.Process process = Runtime.getRuntime()
- .exec("/system/bin/screencap -d " + displayId + " -p "
- + result.getAbsolutePath());
-
- // Waits for the command to finish.
- int err = process.waitFor();
- if (DEBUG) {
- Log.d(TAG, "screencap process finished: " + err);
- }
- return result;
- } catch (IOException | InterruptedException e) {
- Log.e(TAG, "screencap process failed: ", e);
- showToast(R.string.toast_status_screencap_failed);
- }
- return null;
- }
-
- private List<Integer> getAvailableDisplayIds() {
- DisplayManager displayManager = getSystemService(DisplayManager.class);
- ArrayList<Integer> displayIds = new ArrayList<>();
- for (Display d : displayManager.getDisplays()) {
- Log.v(TAG,
- "getAvailableDisplayIds: d.Name=" + d.getName() + ", d.id=" + d.getDisplayId());
- // We skip virtual displays as they are not captured by screencap.
- if (d.getName().contains(ACTIVITY_VIEW_VIRTUAL_DISPLAY)) {
- continue;
- }
- displayIds.add(d.getDisplayId());
- }
- return displayIds;
+ mSingleThreadExecutor.schedule(
+ this::saveBugReport, ACTIVITY_FINISH_DELAY, TimeUnit.MILLISECONDS);
}
private void grabBtSnoopLog() {
@@ -302,13 +248,16 @@
}
}
- private void dumpStateToFile() {
+ private void saveBugReport() {
Log.i(TAG, "Dumpstate to file");
File outputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(), OUTPUT_ZIP_FILE);
-
+ File extraOutputFile = FileUtils.getFile(this, mMetaBugReport.getTimestamp(),
+ EXTRA_OUTPUT_ZIP_FILE);
try (ParcelFileDescriptor outFd = ParcelFileDescriptor.open(outputFile,
+ ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE);
+ ParcelFileDescriptor extraOutFd = ParcelFileDescriptor.open(extraOutputFile,
ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE)) {
- requestBugReport(outFd);
+ requestBugReport(outFd, extraOutFd);
} catch (IOException | RuntimeException e) {
Log.e(TAG, "Failed to grab dump state", e);
BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
@@ -324,7 +273,7 @@
mHandler.sendMessage(message);
}
- private void requestBugReport(ParcelFileDescriptor outFd) {
+ private void requestBugReport(ParcelFileDescriptor outFd, ParcelFileDescriptor extraOutFd) {
if (DEBUG) {
Log.d(TAG, "Requesting a bug report from CarBugReportManager.");
}
@@ -355,7 +304,7 @@
sendProgressEventToHandler(MAX_PROGRESS_VALUE);
}
};
- mBugreportManager.requestZippedBugreport(outFd, mCallback);
+ mBugreportManager.requestBugreport(outFd, extraOutFd, mCallback);
}
private void scheduleZipTask() {
@@ -433,8 +382,9 @@
continue;
}
String filename = file.getName();
- // only for the OUTPUT_FILE, we add invidiual entries to zip file
- if (filename.equals(OUTPUT_ZIP_FILE)) {
+
+ // only for the zipped output file, we add invidiual entries to zip file
+ if (filename.equals(OUTPUT_ZIP_FILE) || filename.equals(EXTRA_OUTPUT_ZIP_FILE)) {
extractZippedFileToOutputStream(file, zipStream);
} else {
FileInputStream reader = new FileInputStream(file);
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/SystemUtils.java b/tests/BugReportApp/src/com/google/android/car/bugreport/SystemUtils.java
deleted file mode 100644
index 6a47298..0000000
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/SystemUtils.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.car.bugreport;
-
-import android.os.Build;
-import android.util.Log;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-
-/**
- * Android System utility class.
- */
-final class SystemUtils {
- private static final String TAG = SystemUtils.class.getSimpleName();
-
- private static final String ENFORCING = "Enforcing";
- private static final String PERMISSIVE = "Permissive";
-
- private static final String VOLVO_FINGERPRINT_PREFIX = "VolvoCars";
-
- /**
- * Returns {@code true} if SELinux is {@code Enforcing}. If it can't verify it returns {@code
- * true} because most likely SELinux is not allowing running {@code getenforce}.
- *
- * <p>Returns {@code false} only when SELinux is {@code Permissive}.
- */
- static boolean isSeLinuxEnforcing() {
- StringBuilder result = new StringBuilder();
- Process p;
- try {
- p = Runtime.getRuntime().exec("getenforce");
- p.waitFor();
- BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
- String line;
- while ((line = reader.readLine()) != null) {
- result.append(line);
- }
- } catch (Exception e) {
- Log.e(TAG, "OS does not support getenforce", e);
- // If getenforce is not available to the device, assume the device is not enforcing
- return false;
- }
- String response = result.toString();
- if (ENFORCING.equals(response)) {
- return true;
- } else if (PERMISSIVE.equals(response)) {
- return false;
- } else {
- Log.e(TAG, "getenforce returned unexpected value, assuming selinux is enforcing!");
- // If getenforce doesn't return anything, most likely SELinux is in enforcing mode.
- return true;
- }
- }
-
- /** Returns {@code true} if the app is running on Volvo cars. */
- static boolean isVolvo() {
- return Build.FINGERPRINT.startsWith(VOLVO_FINGERPRINT_PREFIX);
- }
-
- /** Returns {@code true} if the app is running on Android P. */
- static boolean isAndroidP() {
- return Build.VERSION.SDK_INT == Build.VERSION_CODES.P;
- }
-
- private SystemUtils() {
- }
-}