[RESTRICT AUTOMERGE] Gracefull handle car-service and dumpstate errors.
- BugReportService: Connects to car-service when bugreport is requested.
- BugReportService disconnects from car-service right when bugreporting
is finished or failed.
When it's failed it changes the status of bugreport entry in DB.
- if car-service fails, stop BugReportService after some delay
to allow toast messages to show.
Bug: 149311456
Bug: 149228567
Test: on a elk bench
Test: crashed car-service and checked if the app handled it properly
Change-Id: I07f61a2162c15c95ffe68b098ec4f5e1c4b47093
diff --git a/tests/BugReportApp/AndroidManifest.xml b/tests/BugReportApp/AndroidManifest.xml
index 5da91d4..dcccb83 100644
--- a/tests/BugReportApp/AndroidManifest.xml
+++ b/tests/BugReportApp/AndroidManifest.xml
@@ -16,8 +16,8 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.car.bugreport"
- android:versionCode="12"
- android:versionName="1.7.1">
+ android:versionCode="13"
+ android:versionName="1.7.2">
<uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
index a5aee0f..20fe5f9 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
@@ -137,26 +137,6 @@
}
};
- private final ServiceConnection mCarServiceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- try {
- mDrivingStateManager = (CarDrivingStateManager) mCar.getCarManager(
- Car.CAR_DRIVING_STATE_SERVICE);
- mDrivingStateManager.registerListener(
- BugReportActivity.this::onCarDrivingStateChanged);
- // Call onCarDrivingStateChanged(), because it's not called when Car is connected.
- onCarDrivingStateChanged(mDrivingStateManager.getCurrentCarDrivingState());
- } catch (CarNotConnectedException e) {
- Log.w(TAG, "Failed to get CarDrivingStateManager.", e);
- }
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- }
- };
-
/**
* Builds an intent that starts {@link BugReportActivity} to add audio message to the existing
* bug report.
@@ -227,6 +207,10 @@
}
private void onCarDrivingStateChanged(CarDrivingStateEvent event) {
+ if (mShowBugReportsButton == null) {
+ Log.w(TAG, "Cannot handle driving state change, UI is not ready");
+ return;
+ }
// When adding audio message to the existing bugreport, do not show "Show Bug Reports"
// button, users either should explicitly Submit or Cancel.
if (mAudioRecordingStarted && !mIsNewBugReport) {
@@ -260,8 +244,8 @@
// We need to minimize system state change when performing SILENT bug report.
mConfig = new Config();
mConfig.start();
- mCar = Car.createCar(this, mCarServiceConnection);
- mCar.connect();
+ mCar = Car.createCar(this, /* handler= */ null,
+ Car.CAR_WAIT_TIMEOUT_DO_NOT_WAIT, this::onCarLifecycleChanged);
mInProgressTitleText = findViewById(R.id.in_progress_title_text);
mProgressBar = findViewById(R.id.progress_bar);
@@ -287,6 +271,27 @@
}
}
+ private void onCarLifecycleChanged(Car car, boolean ready) {
+ if (!ready) {
+ mDrivingStateManager = null;
+ mCar = null;
+ Log.d(TAG, "Car service is not ready, ignoring");
+ // If car service is not ready for this activity, just ignore it - as it's only
+ // used to control UX restrictions.
+ return;
+ }
+ try {
+ mDrivingStateManager = (CarDrivingStateManager) car.getCarManager(
+ Car.CAR_DRIVING_STATE_SERVICE);
+ mDrivingStateManager.registerListener(
+ BugReportActivity.this::onCarDrivingStateChanged);
+ // Call onCarDrivingStateChanged(), because it's not called when Car is connected.
+ onCarDrivingStateChanged(mDrivingStateManager.getCurrentCarDrivingState());
+ } catch (CarNotConnectedException e) {
+ Log.w(TAG, "Failed to get CarDrivingStateManager", e);
+ }
+ }
+
private void showInProgressUi() {
mSubmitBugReportLayout.setVisibility(View.GONE);
mInProgressLayout.setVisibility(View.VISIBLE);
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 f1acd83..2bebd72 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
@@ -15,6 +15,10 @@
*/
package com.google.android.car.bugreport;
+import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED;
+import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_DUMPSTATE_FAILED;
+import static android.car.CarBugreportManager.CarBugreportManagerCallback.CAR_BUGREPORT_SERVICE_NOT_AVAILABLE;
+
import static com.google.android.car.bugreport.PackageUtils.getPackageVersion;
import android.annotation.FloatRange;
@@ -89,6 +93,9 @@
// this, the best option is probably to wait for onDetach events from view tree.
private static final int ACTIVITY_FINISH_DELAY_MILLIS = 1000;
+ /** Stop the service only after some delay, to allow toasts to show on the screen. */
+ private static final int STOP_SERVICE_DELAY_MILLIS = 1000;
+
/**
* Wait a short time before showing "bugreport started" toast message, because the service
* will take a screenshot of the screen.
@@ -124,6 +131,7 @@
/** Binder given to clients. */
private final IBinder mBinder = new ServiceBinder();
+ /** True if {@link BugReportService} is already collecting bugreport, including zipping. */
private final AtomicBoolean mIsCollectingBugReport = new AtomicBoolean(false);
private final AtomicDouble mBugReportProgress = new AtomicDouble(0);
@@ -143,7 +151,7 @@
* finishes. We need to clear it otherwise when bugreport fails, it will show "bugreport start"
* toast, which will confuse users.
*/
- private Handler mHandlerToast;
+ private Handler mHandlerStartedToast;
/** A listener that's notified when bugreport progress changes. */
interface BugReportProgressListener {
@@ -196,16 +204,9 @@
NotificationManager.IMPORTANCE_HIGH));
mSingleThreadExecutor = Executors.newSingleThreadScheduledExecutor();
mHandler = new BugReportHandler();
- mHandlerToast = new Handler();
+ mHandlerStartedToast = new Handler();
mConfig = new Config();
mConfig.start();
- // Synchronously connect to the car service.
- mCar = Car.createCar(this);
- try {
- mBugreportManager = (CarBugreportManager) mCar.getCarManager(Car.CAR_BUGREPORT_SERVICE);
- } catch (CarNotConnectedException | NoClassDefFoundError e) {
- throw new IllegalStateException("Failed to get CarBugreportManager.", e);
- }
}
@Override
@@ -213,7 +214,7 @@
if (DEBUG) {
Log.d(TAG, "Service destroyed");
}
- mCar.disconnect();
+ disconnectFromCarService();
}
@Override
@@ -243,7 +244,7 @@
collectBugReport();
// Show a short lived "bugreport started" toast message after a short delay.
- mHandlerToast.postDelayed(() -> {
+ mHandlerStartedToast.postDelayed(() -> {
Toast.makeText(this,
getText(R.string.toast_bug_report_started), Toast.LENGTH_LONG).show();
}, BUGREPORT_STARTED_TOAST_DELAY_MILLIS);
@@ -252,6 +253,24 @@
return START_NOT_STICKY;
}
+ private void onCarLifecycleChanged(Car car, boolean ready) {
+ // not ready - car service is crashed or is restarting.
+ if (!ready) {
+ mBugreportManager = null;
+ mCar = null;
+
+ // NOTE: dumpstate still might be running, but we can't kill it or reconnect to it
+ // so we ignore it.
+ handleBugReportManagerError(CAR_BUGREPORT_SERVICE_NOT_AVAILABLE);
+ return;
+ }
+ try {
+ mBugreportManager = (CarBugreportManager) car.getCarManager(Car.CAR_BUGREPORT_SERVICE);
+ } catch (CarNotConnectedException | NoClassDefFoundError e) {
+ throw new IllegalStateException("Failed to get CarBugreportManager.", e);
+ }
+ }
+
/** Shows an updated progress notification. */
private void showProgressNotification() {
if (isCollectingBugReport()) {
@@ -306,7 +325,26 @@
mHandler.post(() -> Toast.makeText(this, getText(resId), Toast.LENGTH_LONG).show());
}
+ private void disconnectFromCarService() {
+ if (mCar != null) {
+ mCar.disconnect();
+ mCar = null;
+ }
+ mBugreportManager = null;
+ }
+
+ private void connectToCarServiceSync() {
+ if (mCar == null || !(mCar.isConnected() || mCar.isConnecting())) {
+ mCar = Car.createCar(this, /* handler= */ null,
+ Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, this::onCarLifecycleChanged);
+ }
+ }
+
private void collectBugReport() {
+ // Connect to the car service before collecting bugreport, because when car service crashes,
+ // BugReportService doesn't automatically reconnect to it.
+ connectToCarServiceSync();
+
if (Build.IS_USERDEBUG || Build.IS_ENG) {
mSingleThreadExecutor.schedule(
this::grabBtSnoopLog, ACTIVITY_FINISH_DELAY_MILLIS, TimeUnit.MILLISECONDS);
@@ -348,6 +386,8 @@
BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
MESSAGE_FAILURE_DUMPSTATE);
showToast(R.string.toast_status_dump_state_failed);
+ disconnectFromCarService();
+ mIsCollectingBugReport.set(false);
}
}
@@ -364,22 +404,10 @@
}
mCallback = new CarBugreportManager.CarBugreportManagerCallback() {
@Override
- public void onError(int errorCode) {
+ public void onError(@CarBugreportErrorCode int errorCode) {
Log.e(TAG, "CarBugreportManager failed: " + errorCode);
- // We let the UI know that bug reporting is finished, because the next step is to
- // zip everything and upload.
- mBugReportProgress.set(MAX_PROGRESS_VALUE);
- sendProgressEventToHandler(MAX_PROGRESS_VALUE);
- showToast(R.string.toast_status_failed);
- BugStorageUtils.setBugReportStatus(
- BugReportService.this, mMetaBugReport,
- Status.STATUS_WRITE_FAILED, "CarBugreportManager failed: " + errorCode);
- mIsCollectingBugReport.set(false);
- mHandler.post(() -> {
- mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID);
- stopForeground(true);
- });
- mHandlerToast.removeCallbacksAndMessages(null);
+ disconnectFromCarService();
+ handleBugReportManagerError(errorCode);
}
@Override
@@ -391,14 +419,58 @@
@Override
public void onFinished() {
Log.d(TAG, "CarBugreportManager finished");
+ disconnectFromCarService();
mBugReportProgress.set(MAX_PROGRESS_VALUE);
sendProgressEventToHandler(MAX_PROGRESS_VALUE);
mSingleThreadExecutor.submit(BugReportService.this::zipDirectoryAndUpdateStatus);
}
};
+ if (mBugreportManager == null) {
+ mHandler.post(() -> Toast.makeText(this,
+ "Car service is not ready", Toast.LENGTH_LONG).show());
+ Log.e(TAG, "CarBugReportManager is not ready");
+ return;
+ }
mBugreportManager.requestBugreport(outFd, extraOutFd, mCallback);
}
+ private void handleBugReportManagerError(
+ @CarBugreportManager.CarBugreportManagerCallback.CarBugreportErrorCode int errorCode) {
+ if (mMetaBugReport == null) {
+ Log.w(TAG, "No bugreport is running");
+ mIsCollectingBugReport.set(false);
+ return;
+ }
+ // We let the UI know that bug reporting is finished, because the next step is to
+ // zip everything and upload.
+ mBugReportProgress.set(MAX_PROGRESS_VALUE);
+ sendProgressEventToHandler(MAX_PROGRESS_VALUE);
+ showToast(R.string.toast_status_failed);
+ BugStorageUtils.setBugReportStatus(
+ BugReportService.this, mMetaBugReport,
+ Status.STATUS_WRITE_FAILED, getBugReportFailureStatusMessage(errorCode));
+ mHandler.postDelayed(() -> {
+ mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID);
+ stopForeground(true);
+ }, STOP_SERVICE_DELAY_MILLIS);
+ mHandlerStartedToast.removeCallbacksAndMessages(null);
+ mMetaBugReport = null;
+ mIsCollectingBugReport.set(false);
+ }
+
+ private static String getBugReportFailureStatusMessage(
+ @CarBugreportManager.CarBugreportManagerCallback.CarBugreportErrorCode int errorCode) {
+ switch (errorCode) {
+ case CAR_BUGREPORT_DUMPSTATE_CONNECTION_FAILED:
+ case CAR_BUGREPORT_DUMPSTATE_FAILED:
+ return "Failed to connect to dumpstate. Retry again after a minute.";
+ case CAR_BUGREPORT_SERVICE_NOT_AVAILABLE:
+ return "Car service is not available. Retry again.";
+ default:
+ return "Car service bugreport collection failed: " + errorCode;
+ }
+ }
+
/**
* Shows a clickable bugreport finished notification. When clicked it opens
* {@link BugReportInfoActivity}.
@@ -440,7 +512,6 @@
File bugReportTempDir = FileUtils.createTempDir(this, mMetaBugReport.getTimestamp());
zipDirectoryToOutputStream(bugReportTempDir,
BugStorageUtils.openBugReportFileToWrite(this, mMetaBugReport));
- mIsCollectingBugReport.set(false);
} catch (IOException e) {
Log.e(TAG, "Failed to zip files", e);
BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_WRITE_FAILED,
@@ -465,7 +536,9 @@
mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID);
stopForeground(true);
});
- mHandlerToast.removeCallbacksAndMessages(null);
+ mHandlerStartedToast.removeCallbacksAndMessages(null);
+ mMetaBugReport = null;
+ mIsCollectingBugReport.set(false);
}
private void playNotificationSound() {