Clean-up duplicated product configuration am: 8ff906e642 am: de5ef6972f
Change-Id: I69cde8fc665b9227cf89609704cc31642cea9f33
diff --git a/FrameworkPackageStubs/AndroidManifest.xml b/FrameworkPackageStubs/AndroidManifest.xml
index e1ad903..c88ffdb 100644
--- a/FrameworkPackageStubs/AndroidManifest.xml
+++ b/FrameworkPackageStubs/AndroidManifest.xml
@@ -55,6 +55,15 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.dir/audio"/>
</intent-filter>
+ <intent-filter android:priority="-1">
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:scheme="content" />
+ <data android:scheme="file" />
+ <data android:mimeType="video/*" />
+ <data android:mimeType="image/*" />
+ </intent-filter>
</activity>
<!-- Settings package stubs -->
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index 67a00e3..aa2a607 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -24,6 +24,8 @@
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SystemApi;
+import android.app.Activity;
+import android.app.Service;
import android.car.cluster.CarInstrumentClusterManager;
import android.car.cluster.ClusterActivityState;
import android.car.content.pm.CarPackageManager;
@@ -47,6 +49,7 @@
import android.car.vms.VmsSubscriberManager;
import android.content.ComponentName;
import android.content.Context;
+import android.content.ContextWrapper;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
@@ -55,6 +58,7 @@
import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.TransactionTooLargeException;
import android.os.UserHandle;
import android.util.Log;
@@ -680,6 +684,8 @@
private final Context mContext;
+ private final Exception mConstructionStack;
+
private final Object mLock = new Object();
@GuardedBy("mLock")
@@ -725,11 +731,10 @@
mConnectionState = STATE_CONNECTED;
mService = newService;
}
- if (mServiceConnectionListenerClient != null) {
- mServiceConnectionListenerClient.onServiceConnected(name, service);
- }
if (mStatusChangeCallback != null) {
mStatusChangeCallback.onLifecycleChanged(Car.this, true);
+ } else if (mServiceConnectionListenerClient != null) {
+ mServiceConnectionListenerClient.onServiceConnected(name, service);
}
}
@@ -742,11 +747,13 @@
}
handleCarDisconnectLocked();
}
- if (mServiceConnectionListenerClient != null) {
- mServiceConnectionListenerClient.onServiceDisconnected(name);
- }
if (mStatusChangeCallback != null) {
mStatusChangeCallback.onLifecycleChanged(Car.this, false);
+ } else if (mServiceConnectionListenerClient != null) {
+ mServiceConnectionListenerClient.onServiceDisconnected(name);
+ } else {
+ // This client does not handle car service restart, so should be terminated.
+ finishClient();
}
}
};
@@ -768,7 +775,9 @@
/**
* A factory method that creates Car instance for all Car API access.
- * @param context
+ * @param context App's Context. This should not be null. If you are passing
+ * {@link ContextWrapper}, make sure that its base Context is non-null as well.
+ * Otherwise it will throw {@link java.lang.NullPointerException}.
* @param serviceConnectionListener listener for monitoring service connection.
* @param handler the handler on which the callback should execute, or null to execute on the
* service's main thread. Note: the service connection listener will be always on the main
@@ -780,6 +789,7 @@
@Deprecated
public static Car createCar(Context context, ServiceConnection serviceConnectionListener,
@Nullable Handler handler) {
+ assertNonNullContext(context);
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
Log.e(TAG_CAR, "FEATURE_AUTOMOTIVE not declared while android.car is used");
return null;
@@ -821,7 +831,9 @@
/**
* Creates new {@link Car} object which connected synchronously to Car Service and ready to use.
*
- * @param context application's context
+ * @param context App's Context. This should not be null. If you are passing
+ * {@link ContextWrapper}, make sure that its base Context is non-null as well.
+ * Otherwise it will throw {@link java.lang.NullPointerException}.
* @param handler the handler on which the manager's callbacks will be executed, or null to
* execute on the application's main thread.
*
@@ -829,6 +841,7 @@
*/
@Nullable
public static Car createCar(Context context, @Nullable Handler handler) {
+ assertNonNullContext(context);
Car car = null;
IBinder service = null;
boolean started = false;
@@ -842,6 +855,8 @@
}
if (service != null) {
if (!started) { // specialization for most common case.
+ // Do this to crash client when car service crashes.
+ car.startCarService();
return car;
}
break;
@@ -904,6 +919,9 @@
* {@link CarServiceLifecycleListener#onLifecycleChanged(Car, boolean)} and avoid the
* needs to check if returned {@link Car} is connected or not from returned {@link Car}.</p>
*
+ * @param context App's Context. This should not be null. If you are passing
+ * {@link ContextWrapper}, make sure that its base Context is non-null as well.
+ * Otherwise it will throw {@link java.lang.NullPointerException}.
* @param handler dispatches all Car*Manager events to this Handler. Exception is
* {@link CarServiceLifecycleListener} which will be always dispatched to main
* thread. Passing null leads into dispatching all Car*Manager callbacks to main
@@ -920,7 +938,7 @@
public static Car createCar(@NonNull Context context,
@Nullable Handler handler, long waitTimeoutMs,
@NonNull CarServiceLifecycleListener statusChangeListener) {
- Preconditions.checkNotNull(context);
+ assertNonNullContext(context);
Preconditions.checkNotNull(statusChangeListener);
Car car = null;
IBinder service = null;
@@ -1001,6 +1019,15 @@
return car;
}
+ private static void assertNonNullContext(Context context) {
+ Preconditions.checkNotNull(context);
+ if (context instanceof ContextWrapper
+ && ((ContextWrapper) context).getBaseContext() == null) {
+ throw new NullPointerException(
+ "ContextWrapper with null base passed as Context, forgot to set base Context?");
+ }
+ }
+
private void dispatchCarReadyToMainThread(boolean isMainThread) {
if (isMainThread) {
mStatusChangeCallback.onLifecycleChanged(this, true);
@@ -1027,6 +1054,13 @@
}
mServiceConnectionListenerClient = serviceConnectionListener;
mStatusChangeCallback = statusChangeListener;
+ // Store construction stack so that client can get help when it crashes when car service
+ // crashes.
+ if (serviceConnectionListener == null && statusChangeListener == null) {
+ mConstructionStack = new RuntimeException();
+ } else {
+ mConstructionStack = null;
+ }
}
/**
@@ -1136,12 +1170,15 @@
@Nullable
public Object getCarManager(String serviceName) {
CarManagerBase manager;
- ICar service = getICarOrThrow();
synchronized (mLock) {
+ if (mService == null) {
+ Log.w(TAG_CAR, "getCarManager not working while car service not ready");
+ return null;
+ }
manager = mServiceMap.get(serviceName);
if (manager == null) {
try {
- IBinder binder = service.getCarService(serviceName);
+ IBinder binder = mService.getCarService(serviceName);
if (binder == null) {
Log.w(TAG_CAR, "getCarManager could not get binder for service:"
+ serviceName);
@@ -1155,7 +1192,7 @@
}
mServiceMap.put(serviceName, manager);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
}
@@ -1171,84 +1208,156 @@
return CONNECTION_TYPE_EMBEDDED;
}
+ /** @hide */
+ Context getContext() {
+ return mContext;
+ }
+
+ /** @hide */
+ Handler getEventHandler() {
+ return mEventHandler;
+ }
+
+ /** @hide */
+ <T> T handleRemoteExceptionFromCarService(RemoteException e, T returnValue) {
+ handleRemoteExceptionFromCarService(e);
+ return returnValue;
+ }
+
+ /** @hide */
+ void handleRemoteExceptionFromCarService(RemoteException e) {
+ if (e instanceof TransactionTooLargeException) {
+ Log.w(TAG_CAR, "Car service threw TransactionTooLargeException", e);
+ throw new CarTransactionException(e, "Car service threw TransactionTooLargException");
+ } else {
+ Log.w(TAG_CAR, "Car service has crashed", e);
+ }
+ }
+
+
+ private void finishClient() {
+ if (mContext == null) {
+ throw new IllegalStateException("Car service has crashed, null Context");
+ }
+ if (mContext instanceof Activity) {
+ Activity activity = (Activity) mContext;
+ if (!activity.isFinishing()) {
+ Log.w(TAG_CAR,
+ "Car service crashed, client not handling it, finish Activity, created "
+ + "from " + mConstructionStack);
+ activity.finish();
+ }
+ return;
+ } else if (mContext instanceof Service) {
+ Service service = (Service) mContext;
+ throw new IllegalStateException("Car service has crashed, client not handle it:"
+ + service.getPackageName() + "," + service.getClass().getSimpleName(),
+ mConstructionStack);
+ }
+ throw new IllegalStateException("Car service crashed, client not handling it.",
+ mConstructionStack);
+ }
+
+ /** @hide */
+ public static <T> T handleRemoteExceptionFromCarService(Service service, RemoteException e,
+ T returnValue) {
+ handleRemoteExceptionFromCarService(service, e);
+ return returnValue;
+ }
+
+ /** @hide */
+ public static void handleRemoteExceptionFromCarService(Service service, RemoteException e) {
+ if (e instanceof TransactionTooLargeException) {
+ Log.w(TAG_CAR, "Car service threw TransactionTooLargeException, client:"
+ + service.getPackageName() + ","
+ + service.getClass().getSimpleName(), e);
+ throw new CarTransactionException(e, "Car service threw TransactionTooLargeException, "
+ + "client: %s, %s", service.getPackageName(), service.getClass().getSimpleName());
+ } else {
+ Log.w(TAG_CAR, "Car service has crashed, client:"
+ + service.getPackageName() + ","
+ + service.getClass().getSimpleName(), e);
+ service.stopSelf();
+ }
+ }
+
@Nullable
private CarManagerBase createCarManager(String serviceName, IBinder binder) {
CarManagerBase manager = null;
switch (serviceName) {
case AUDIO_SERVICE:
- manager = new CarAudioManager(binder, mContext, mEventHandler);
+ manager = new CarAudioManager(this, binder);
break;
case SENSOR_SERVICE:
- manager = new CarSensorManager(binder, mContext, mEventHandler);
+ manager = new CarSensorManager(this, binder);
break;
case INFO_SERVICE:
- manager = new CarInfoManager(binder);
+ manager = new CarInfoManager(this, binder);
break;
case APP_FOCUS_SERVICE:
- manager = new CarAppFocusManager(binder, mEventHandler);
+ manager = new CarAppFocusManager(this, binder);
break;
case PACKAGE_SERVICE:
- manager = new CarPackageManager(binder, mContext);
+ manager = new CarPackageManager(this, binder);
break;
case CAR_NAVIGATION_SERVICE:
- manager = new CarNavigationStatusManager(binder);
+ manager = new CarNavigationStatusManager(this, binder);
break;
case CABIN_SERVICE:
- manager = new CarCabinManager(binder, mContext, mEventHandler);
+ manager = new CarCabinManager(this, binder);
break;
case DIAGNOSTIC_SERVICE:
- manager = new CarDiagnosticManager(binder, mContext, mEventHandler);
+ manager = new CarDiagnosticManager(this, binder);
break;
case HVAC_SERVICE:
- manager = new CarHvacManager(binder, mContext, mEventHandler);
+ manager = new CarHvacManager(this, binder);
break;
case POWER_SERVICE:
- manager = new CarPowerManager(binder, mContext, mEventHandler);
+ manager = new CarPowerManager(this, binder);
break;
case PROJECTION_SERVICE:
- manager = new CarProjectionManager(binder, mEventHandler);
+ manager = new CarProjectionManager(this, binder);
break;
case PROPERTY_SERVICE:
- manager = new CarPropertyManager(ICarProperty.Stub.asInterface(binder),
- mEventHandler);
+ manager = new CarPropertyManager(this, ICarProperty.Stub.asInterface(binder));
break;
case VENDOR_EXTENSION_SERVICE:
- manager = new CarVendorExtensionManager(binder, mEventHandler);
+ manager = new CarVendorExtensionManager(this, binder);
break;
case CAR_INSTRUMENT_CLUSTER_SERVICE:
- manager = new CarInstrumentClusterManager(binder, mEventHandler);
+ manager = new CarInstrumentClusterManager(this, binder);
break;
case TEST_SERVICE:
/* CarTestManager exist in static library. So instead of constructing it here,
* only pass binder wrapper so that CarTestManager can be constructed outside. */
- manager = new CarTestManagerBinderWrapper(binder);
+ manager = new CarTestManagerBinderWrapper(this, binder);
break;
case VMS_SUBSCRIBER_SERVICE:
- manager = new VmsSubscriberManager(binder);
+ manager = new VmsSubscriberManager(this, binder);
break;
case BLUETOOTH_SERVICE:
- manager = new CarBluetoothManager(binder, mContext);
+ manager = new CarBluetoothManager(this, binder);
break;
case STORAGE_MONITORING_SERVICE:
- manager = new CarStorageMonitoringManager(binder, mEventHandler);
+ manager = new CarStorageMonitoringManager(this, binder);
break;
case CAR_DRIVING_STATE_SERVICE:
- manager = new CarDrivingStateManager(binder, mContext, mEventHandler);
+ manager = new CarDrivingStateManager(this, binder);
break;
case CAR_UX_RESTRICTION_SERVICE:
- manager = new CarUxRestrictionsManager(binder, mContext, mEventHandler);
+ manager = new CarUxRestrictionsManager(this, binder);
break;
case CAR_CONFIGURATION_SERVICE:
- manager = new CarConfigurationManager(binder);
+ manager = new CarConfigurationManager(this, binder);
break;
case CAR_TRUST_AGENT_ENROLLMENT_SERVICE:
- manager = new CarTrustAgentEnrollmentManager(binder, mContext, mEventHandler);
+ manager = new CarTrustAgentEnrollmentManager(this, binder);
break;
case CAR_MEDIA_SERVICE:
- manager = new CarMediaManager(binder);
+ manager = new CarMediaManager(this, binder);
break;
case CAR_BUGREPORT_SERVICE:
- manager = new CarBugreportManager(binder, mContext);
+ manager = new CarBugreportManager(this, binder);
break;
default:
break;
@@ -1281,15 +1390,6 @@
}
}
- private ICar getICarOrThrow() throws IllegalStateException {
- synchronized (mLock) {
- if (mService == null) {
- throw new IllegalStateException("not connected");
- }
- return mService;
- }
- }
-
private void tearDownCarManagersLocked() {
// All disconnected handling should be only doing its internal cleanup.
for (CarManagerBase manager: mServiceMap.values()) {
diff --git a/car-lib/src/android/car/CarAppFocusManager.java b/car-lib/src/android/car/CarAppFocusManager.java
index ff0c25d..b8936af 100644
--- a/car-lib/src/android/car/CarAppFocusManager.java
+++ b/car-lib/src/android/car/CarAppFocusManager.java
@@ -17,7 +17,6 @@
package android.car;
import android.annotation.IntDef;
-import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
@@ -35,7 +34,7 @@
* should run in the system, and other app setting the flag for the matching app should
* lead into other app to stop.
*/
-public final class CarAppFocusManager implements CarManagerBase {
+public final class CarAppFocusManager extends CarManagerBase {
/**
* Listener to get notification for app getting information on application type status changes.
*/
@@ -114,7 +113,6 @@
public @interface AppFocusRequestResult {}
private final IAppFocus mService;
- private final Handler mHandler;
private final Map<OnAppFocusChangedListener, IAppFocusListenerImpl> mChangeBinders =
new HashMap<>();
private final Map<OnAppFocusOwnershipCallback, IAppFocusOwnershipCallbackImpl>
@@ -123,9 +121,9 @@
/**
* @hide
*/
- CarAppFocusManager(IBinder service, Handler handler) {
+ CarAppFocusManager(Car car, IBinder service) {
+ super(car);
mService = IAppFocus.Stub.asInterface(service);
- mHandler = handler;
}
/**
@@ -149,7 +147,7 @@
try {
mService.registerFocusListener(binder, appType);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -169,7 +167,8 @@
try {
mService.unregisterFocusListener(binder, appType);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
+ // continue for local clean-up
}
synchronized (this) {
binder.removeAppType(appType);
@@ -197,7 +196,7 @@
mService.unregisterFocusListener(binder, appType);
}
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -209,7 +208,7 @@
try {
return mService.getActiveAppTypes();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, new int[0]);
}
}
@@ -229,7 +228,7 @@
try {
return mService.isOwningFocus(binder, appType);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -261,7 +260,7 @@
try {
return mService.requestAppFocus(binder, appType);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, APP_FOCUS_REQUEST_FAILED);
}
}
@@ -286,7 +285,8 @@
try {
mService.abandonAppFocus(binder, appType);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
+ // continue for local clean-up
}
synchronized (this) {
binder.removeAppType(appType);
@@ -314,7 +314,7 @@
mService.abandonAppFocus(binder, appType);
}
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -359,11 +359,8 @@
if (manager == null || listener == null) {
return;
}
- manager.mHandler.post(new Runnable() {
- @Override
- public void run() {
- listener.onAppFocusChanged(appType, active);
- }
+ manager.getEventHandler().post(() -> {
+ listener.onAppFocusChanged(appType, active);
});
}
}
@@ -403,11 +400,8 @@
if (manager == null || callback == null) {
return;
}
- manager.mHandler.post(new Runnable() {
- @Override
- public void run() {
- callback.onAppFocusOwnershipLost(appType);
- }
+ manager.getEventHandler().post(() -> {
+ callback.onAppFocusOwnershipLost(appType);
});
}
@@ -418,11 +412,8 @@
if (manager == null || callback == null) {
return;
}
- manager.mHandler.post(new Runnable() {
- @Override
- public void run() {
- callback.onAppFocusOwnershipGranted(appType);
- }
+ manager.getEventHandler().post(() -> {
+ callback.onAppFocusOwnershipGranted(appType);
});
}
}
diff --git a/car-lib/src/android/car/CarBluetoothManager.java b/car-lib/src/android/car/CarBluetoothManager.java
index c85cec7..c8e46ca 100644
--- a/car-lib/src/android/car/CarBluetoothManager.java
+++ b/car-lib/src/android/car/CarBluetoothManager.java
@@ -17,7 +17,6 @@
import android.Manifest;
import android.annotation.RequiresPermission;
-import android.content.Context;
import android.os.IBinder;
import android.os.RemoteException;
@@ -26,9 +25,8 @@
*
* @hide
*/
-public final class CarBluetoothManager implements CarManagerBase {
+public final class CarBluetoothManager extends CarManagerBase {
private static final String TAG = "CarBluetoothManager";
- private final Context mContext;
private final ICarBluetooth mService;
/**
@@ -50,7 +48,7 @@
try {
mService.connectDevices();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -59,8 +57,8 @@
*
* @hide
*/
- public CarBluetoothManager(IBinder service, Context context) {
- mContext = context;
+ public CarBluetoothManager(Car car, IBinder service) {
+ super(car);
mService = ICarBluetooth.Stub.asInterface(service);
}
diff --git a/car-lib/src/android/car/CarBugreportManager.java b/car-lib/src/android/car/CarBugreportManager.java
index f5d3b1d..99f2c7c 100644
--- a/car-lib/src/android/car/CarBugreportManager.java
+++ b/car-lib/src/android/car/CarBugreportManager.java
@@ -20,7 +20,6 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
-import android.content.Context;
import android.os.Handler;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
@@ -39,10 +38,9 @@
*
* @hide
*/
-public final class CarBugreportManager implements CarManagerBase {
+public final class CarBugreportManager extends CarManagerBase {
private final ICarBugreportService mService;
- private Handler mHandler;
/**
* Callback from carbugreport manager. Callback methods are always called on the main thread.
@@ -153,9 +151,9 @@
*
* Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead.
*/
- public CarBugreportManager(IBinder service, Context context) {
+ public CarBugreportManager(Car car, IBinder service) {
+ super(car);
mService = ICarBugreportService.Stub.asInterface(service);
- mHandler = new Handler(context.getMainLooper());
}
/**
@@ -185,10 +183,10 @@
Preconditions.checkNotNull(callback);
try {
CarBugreportManagerCallbackWrapper wrapper =
- new CarBugreportManagerCallbackWrapper(callback, mHandler);
+ new CarBugreportManagerCallbackWrapper(callback, getEventHandler());
mService.requestBugreport(output, extraOutput, wrapper);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
} finally {
IoUtils.closeQuietly(output);
IoUtils.closeQuietly(extraOutput);
diff --git a/car-lib/src/android/car/CarInfoManager.java b/car-lib/src/android/car/CarInfoManager.java
index bf09495..e9a170d 100644
--- a/car-lib/src/android/car/CarInfoManager.java
+++ b/car-lib/src/android/car/CarInfoManager.java
@@ -29,7 +29,7 @@
* Utility to retrieve various static information from car. Each data are grouped as {@link Bundle}
* and relevant data can be checked from {@link Bundle} using pre-specified keys.
*/
-public final class CarInfoManager implements CarManagerBase{
+public final class CarInfoManager extends CarManagerBase {
private final CarPropertyManager mCarPropertyMgr;
/**
@@ -261,9 +261,10 @@
}
/** @hide */
- CarInfoManager(IBinder service) {
+ CarInfoManager(Car car, IBinder service) {
+ super(car);
ICarProperty mCarPropertyService = ICarProperty.Stub.asInterface(service);
- mCarPropertyMgr = new CarPropertyManager(mCarPropertyService, null);
+ mCarPropertyMgr = new CarPropertyManager(car, mCarPropertyService);
}
/** @hide */
diff --git a/car-lib/src/android/car/CarManagerBase.java b/car-lib/src/android/car/CarManagerBase.java
index 737f356..8d30fdf 100644
--- a/car-lib/src/android/car/CarManagerBase.java
+++ b/car-lib/src/android/car/CarManagerBase.java
@@ -16,10 +16,44 @@
package android.car;
+import android.content.Context;
+import android.os.Handler;
+import android.os.RemoteException;
+
/**
- * Common interface for Car*Manager
+ * Common base class for Car*Manager
* @hide
*/
-public interface CarManagerBase {
- void onCarDisconnected();
+public abstract class CarManagerBase {
+
+ protected final Car mCar;
+
+ public CarManagerBase(Car car) {
+ mCar = car;
+ }
+
+ protected Context getContext() {
+ return mCar.getContext();
+ }
+
+ protected Handler getEventHandler() {
+ return mCar.getEventHandler();
+ }
+
+ protected <T> T handleRemoteExceptionFromCarService(RemoteException e, T returnValue) {
+ return mCar.handleRemoteExceptionFromCarService(e, returnValue);
+ }
+
+ protected void handleRemoteExceptionFromCarService(RemoteException e) {
+ mCar.handleRemoteExceptionFromCarService(e);
+ }
+
+ /**
+ * Handle disconnection of car service (=crash). As car service has crashed already, this call
+ * should only clean up any listeners / callbacks passed from client. Clearing object passed
+ * to car service is not necessary as car service has crashed. Note that Car*Manager will not
+ * work any more as all binders are invalid. Client should re-create all Car*Managers when
+ * car service is restarted.
+ */
+ protected abstract void onCarDisconnected();
}
diff --git a/car-lib/src/android/car/CarProjectionManager.java b/car-lib/src/android/car/CarProjectionManager.java
index 4c177f7..bc4107f 100644
--- a/car-lib/src/android/car/CarProjectionManager.java
+++ b/car-lib/src/android/car/CarProjectionManager.java
@@ -69,7 +69,7 @@
* @hide
*/
@SystemApi
-public final class CarProjectionManager implements CarManagerBase {
+public final class CarProjectionManager extends CarManagerBase {
private static final String TAG = CarProjectionManager.class.getSimpleName();
private final Binder mToken = new Binder();
@@ -194,7 +194,6 @@
public static final int PROJECTION_AP_FAILED = 2;
private final ICarProjection mService;
- private final Handler mHandler;
private final Executor mHandlerExecutor;
@GuardedBy("mLock")
@@ -241,9 +240,10 @@
/**
* @hide
*/
- public CarProjectionManager(IBinder service, Handler handler) {
+ public CarProjectionManager(Car car, IBinder service) {
+ super(car);
mService = ICarProjection.Stub.asInterface(service);
- mHandler = handler;
+ Handler handler = getEventHandler();
mHandlerExecutor = handler::post;
}
@@ -448,7 +448,8 @@
mService.unregisterKeyEventHandler(mBinderHandler);
}
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
+ return;
}
mHandledEvents = events;
@@ -466,7 +467,7 @@
try {
mService.registerProjectionRunner(serviceIntent);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
}
@@ -483,7 +484,7 @@
try {
mService.unregisterProjectionRunner(serviceIntent);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
}
@@ -507,14 +508,14 @@
public void startProjectionAccessPoint(@NonNull ProjectionAccessPointCallback callback) {
Preconditions.checkNotNull(callback, "callback cannot be null");
synchronized (mLock) {
- Looper looper = mHandler.getLooper();
+ Looper looper = getEventHandler().getLooper();
ProjectionAccessPointCallbackProxy proxy =
new ProjectionAccessPointCallbackProxy(this, looper, callback);
try {
mService.startProjectionAccessPoint(proxy.getMessenger(), mAccessPointProxyToken);
mProjectionAccessPointCallbackProxy = proxy;
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
}
@@ -535,7 +536,7 @@
}
return channelList;
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, Collections.emptyList());
}
}
@@ -556,7 +557,7 @@
try {
mService.stopProjectionAccessPoint(mAccessPointProxyToken);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -575,7 +576,7 @@
try {
return mService.requestBluetoothProfileInhibit(device, profile, mToken);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -593,7 +594,7 @@
try {
return mService.releaseBluetoothProfileInhibit(device, profile, mToken);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -611,7 +612,7 @@
try {
mService.updateProjectionStatus(status, mToken);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -634,12 +635,12 @@
try {
mService.registerProjectionStatusListener(mCarProjectionStatusListener);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
} else {
// Already subscribed to Car Service, immediately notify listener with the current
// projection status in the event handler thread.
- mHandler.post(() ->
+ getEventHandler().post(() ->
listener.onProjectionStatusChanged(
mCarProjectionStatusListener.mCurrentState,
mCarProjectionStatusListener.mCurrentPackageName,
@@ -671,7 +672,7 @@
mService.unregisterProjectionStatusListener(mCarProjectionStatusListener);
mCarProjectionStatusListener = null;
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -695,7 +696,7 @@
try {
return mService.getProjectionOptions();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, Bundle.EMPTY);
}
}
@@ -838,7 +839,7 @@
List<ProjectionStatus> details) {
CarProjectionManager mgr = mManagerRef.get();
if (mgr != null) {
- mgr.mHandler.post(() -> {
+ mgr.getEventHandler().post(() -> {
mCurrentState = projectionState;
mCurrentPackageName = packageName;
mDetails = Collections.unmodifiableList(details);
diff --git a/car-lib/src/android/car/CarTransactionException.java b/car-lib/src/android/car/CarTransactionException.java
new file mode 100644
index 0000000..c0e7e2f
--- /dev/null
+++ b/car-lib/src/android/car/CarTransactionException.java
@@ -0,0 +1,31 @@
+/*
+ * 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 android.car;
+
+/**
+ * Runtime exception thrown when a transaction-level binder exception has occurred.
+ *
+ * This allows clients to log or recover transaction errors as appropriate.
+ *
+ * @hide
+ */
+public class CarTransactionException extends UnsupportedOperationException {
+ CarTransactionException(Throwable cause, String msg, Object... args) {
+ super(String.format(msg, args), cause);
+ }
+}
+
diff --git a/car-lib/src/android/car/VehiclePropertyIds.java b/car-lib/src/android/car/VehiclePropertyIds.java
index c335e58..ada2b2d 100644
--- a/car-lib/src/android/car/VehiclePropertyIds.java
+++ b/car-lib/src/android/car/VehiclePropertyIds.java
@@ -338,38 +338,44 @@
/**
* Distance units for display
* Requires permission {@link Car#PERMISSION_READ_DISPLAY_UNITS} to read the property.
- * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} to write the property.
+ * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} and
+ * {@link Car#PERMISSION_VENDOR_EXTENSION}to write the property.
*/
public static final int DISTANCE_DISPLAY_UNITS = 289408512;
/**
* Fuel volume units for display
* Requires permission {@link Car#PERMISSION_READ_DISPLAY_UNITS} to read the property.
- * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} to write the property.
+ * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} and
+ * {@link Car#PERMISSION_VENDOR_EXTENSION}to write the property.
*/
public static final int FUEL_VOLUME_DISPLAY_UNITS = 289408513;
/**
* Tire pressure units for display
* Requires permission {@link Car#PERMISSION_READ_DISPLAY_UNITS} to read the property.
- * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} to write the property.
+ * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} and
+ * {@link Car#PERMISSION_VENDOR_EXTENSION}to write the property.
*/
public static final int TIRE_PRESSURE_DISPLAY_UNITS = 289408514;
/**
* EV battery units for display
* Requires permission {@link Car#PERMISSION_READ_DISPLAY_UNITS} to read the property.
- * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} to write the property.
+ * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} and
+ * {@link Car#PERMISSION_VENDOR_EXTENSION}to write the property.
*/
public static final int EV_BATTERY_DISPLAY_UNITS = 289408515;
/**
* Speed Units for display
* Requires permission {@link Car#PERMISSION_READ_DISPLAY_UNITS} to read the property.
- * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} to write the property.
+ * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} and
+ * {@link Car#PERMISSION_VENDOR_EXTENSION}to write the property.
* @hide
*/
public static final int VEHICLE_SPEED_DISPLAY_UNITS = 289408516;
/**
* Fuel consumption units for display
* Requires permission {@link Car#PERMISSION_READ_DISPLAY_UNITS} to read the property.
- * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} to write the property.
+ * Requires permission {@link Car#PERMISSION_CONTROL_DISPLAY_UNITS} and
+ * {@link Car#PERMISSION_VENDOR_EXTENSION}to write the property.
*/
public static final int FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME = 287311364;
/**
diff --git a/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java b/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
index 54d10e2..2b633a0 100644
--- a/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
+++ b/car-lib/src/android/car/cluster/CarInstrumentClusterManager.java
@@ -17,10 +17,10 @@
package android.car.cluster;
import android.annotation.SystemApi;
+import android.car.Car;
import android.car.CarManagerBase;
import android.content.Intent;
import android.os.Bundle;
-import android.os.Handler;
import android.os.IBinder;
/**
@@ -35,7 +35,7 @@
*/
@Deprecated
@SystemApi
-public class CarInstrumentClusterManager implements CarManagerBase {
+public class CarInstrumentClusterManager extends CarManagerBase {
/**
* @deprecated use {@link android.car.Car#CATEGORY_NAVIGATION} instead
*
@@ -101,7 +101,8 @@
}
/** @hide */
- public CarInstrumentClusterManager(IBinder service, Handler handler) {
+ public CarInstrumentClusterManager(Car car, IBinder service) {
+ super(car);
// No-op
}
diff --git a/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl b/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
index 7deecc7..4f41796 100644
--- a/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
+++ b/car-lib/src/android/car/cluster/renderer/IInstrumentCluster.aidl
@@ -28,6 +28,8 @@
/**
* Returns {@link IInstrumentClusterNavigation} that will be passed to the navigation
* application.
+ *
+ * TODO(b/141992448) : remove blocking call
*/
IInstrumentClusterNavigation getNavigationService();
diff --git a/car-lib/src/android/car/cluster/renderer/IInstrumentClusterHelper.aidl b/car-lib/src/android/car/cluster/renderer/IInstrumentClusterHelper.aidl
new file mode 100644
index 0000000..680e241
--- /dev/null
+++ b/car-lib/src/android/car/cluster/renderer/IInstrumentClusterHelper.aidl
@@ -0,0 +1,49 @@
+/*
+ * 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 android.car.cluster.renderer;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Helper binder API for InstrumentClusterRenderingService. This contains binder calls to car
+ * service.
+ *
+ * @hide
+ */
+interface IInstrumentClusterHelper {
+ /**
+ * Start an activity to specified display / user. The activity is considered as
+ * in fixed mode for the display and will be re-launched if the activity crashes, the package
+ * is updated or goes to background for whatever reason.
+ * Only one activity can exist in fixed mode for the target display and calling this multiple
+ * times with different {@code Intent} will lead into making all previous activities into
+ * non-fixed normal state (= will not be re-launched.)
+ *
+ * Do not change binder transaction number.
+ */
+ boolean startFixedActivityModeForDisplayAndUser(in Intent intent,
+ in Bundle activityOptionsBundle, int userId) = 0;
+ /**
+ * The activity lauched on the display is no longer in fixed mode. Re-launching or finishing
+ * should not trigger re-launfhing any more. Note that Activity for non-current user will
+ * be auto-stopped and there is no need to call this for user swiching. Note that this does not
+ * stop the activity but it will not be re-launched any more.
+ *
+ * Do not change binder transaction number.
+ */
+ void stopFixedActivityMode(int displayId) = 1;
+}
diff --git a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
index 6996119..ede4b6a 100644
--- a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
+++ b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
@@ -22,6 +22,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.annotation.UserIdInt;
import android.app.ActivityOptions;
import android.app.Service;
import android.car.Car;
@@ -82,20 +83,35 @@
*/
@SystemApi
public abstract class InstrumentClusterRenderingService extends Service {
+ /**
+ * Key to pass IInstrumentClusterHelper binder in onBind call {@link Intent} through extra
+ * {@link Bundle). Both extra bundle and binder itself use this key.
+ *
+ * @hide
+ */
+ public static final String EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER =
+ "android.car.cluster.renderer.IInstrumentClusterHelper";
+
private static final String TAG = CarLibLog.TAG_CLUSTER;
private static final String BITMAP_QUERY_WIDTH = "w";
private static final String BITMAP_QUERY_HEIGHT = "h";
+ private static final String BITMAP_QUERY_OFFLANESALPHA = "offLanesAlpha";
+
+ private final Handler mUiHandler = new Handler(Looper.getMainLooper());
private final Object mLock = new Object();
+ // Main thread only
private RendererBinder mRendererBinder;
- private Handler mUiHandler = new Handler(Looper.getMainLooper());
private ActivityOptions mActivityOptions;
private ClusterActivityState mActivityState;
private ComponentName mNavigationComponent;
@GuardedBy("mLock")
private ContextOwner mNavContextOwner;
+ @GuardedBy("mLock")
+ private IInstrumentClusterHelper mInstrumentClusterHelper;
+
private static final int IMAGE_CACHE_SIZE_BYTES = 4 * 1024 * 1024; /* 4 mb */
private final LruCache<String, Bitmap> mCache = new LruCache<String, Bitmap>(
IMAGE_CACHE_SIZE_BYTES) {
@@ -157,6 +173,18 @@
Log.d(TAG, "onBind, intent: " + intent);
}
+ Bundle bundle = intent.getBundleExtra(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER);
+ IBinder binder = null;
+ if (bundle != null) {
+ binder = bundle.getBinder(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER);
+ }
+ if (binder == null) {
+ Log.wtf(TAG, "IInstrumentClusterHelper not passed through binder");
+ } else {
+ synchronized (mLock) {
+ mInstrumentClusterHelper = IInstrumentClusterHelper.Stub.asInterface(binder);
+ }
+ }
if (mRendererBinder == null) {
mRendererBinder = new RendererBinder(getNavigationRenderer());
}
@@ -196,6 +224,76 @@
public void onNavigationComponentReleased() {
}
+ @Nullable
+ private IInstrumentClusterHelper getClusterHelper() {
+ synchronized (mLock) {
+ if (mInstrumentClusterHelper == null) {
+ Log.w("mInstrumentClusterHelper still null, should wait until onBind",
+ new RuntimeException());
+ }
+ return mInstrumentClusterHelper;
+ }
+ }
+
+ /**
+ * Start Activity in fixed mode.
+ *
+ * <p>Activity launched in this way will stay visible across crash, package updatge
+ * or other Activity launch. So this should be carefully used for case like apps running
+ * in instrument cluster.</p>
+ *
+ * <p> Only one Activity can stay in this mode for a display and launching other Activity
+ * with this call means old one get out of the mode. Alternatively
+ * {@link #stopFixedActivityMode(int)} can be called to get the top activitgy out of this
+ * mode.</p>
+ *
+ * @param intent Should include specific {@code ComponentName}.
+ * @param options Should include target display.
+ * @param userId Target user id
+ * @return {@code true} if succeeded. {@code false} may mean the target component is not ready
+ * or available. Note that failure can happen during early boot-up stage even if the
+ * target Activity is in normal state and client should retry when it fails. Once it is
+ * successfully launched, car service will guarantee that it is running across crash or
+ * other events.
+ *
+ * @hide
+ */
+ protected boolean startFixedActivityModeForDisplayAndUser(@NonNull Intent intent,
+ @NonNull ActivityOptions options, @UserIdInt int userId) {
+ IInstrumentClusterHelper helper = getClusterHelper();
+ if (helper == null) {
+ return false;
+ }
+ try {
+ return helper.startFixedActivityModeForDisplayAndUser(intent, options.toBundle(),
+ userId);
+ } catch (RemoteException e) {
+ Log.w("Remote exception from car service", e);
+ // Probably car service will restart and rebind. So do nothing.
+ }
+ return false;
+ }
+
+
+ /**
+ * Stop fixed mode for top Activity in the display. Crashing or launching other Activity
+ * will not re-launch the top Activity any more.
+ *
+ * @hide
+ */
+ protected void stopFixedActivityMode(int displayId) {
+ IInstrumentClusterHelper helper = getClusterHelper();
+ if (helper == null) {
+ return;
+ }
+ try {
+ helper.stopFixedActivityMode(displayId);
+ } catch (RemoteException e) {
+ Log.w("Remote exception from car service, displayId:" + displayId, e);
+ // Probably car service will restart and rebind. So do nothing.
+ }
+ }
+
/**
* Updates the cluster navigation activity by checking which activity to show (an activity of
* the {@link #mNavContextOwner}). If not yet launched, it will do so.
@@ -370,16 +468,19 @@
@CallSuper
@Override
protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
- writer.println("**" + getClass().getSimpleName() + "**");
- writer.println("renderer binder: " + mRendererBinder);
- if (mRendererBinder != null) {
- writer.println("navigation renderer: " + mRendererBinder.mNavigationRenderer);
+ synchronized (mLock) {
+ writer.println("**" + getClass().getSimpleName() + "**");
+ writer.println("renderer binder: " + mRendererBinder);
+ if (mRendererBinder != null) {
+ writer.println("navigation renderer: " + mRendererBinder.mNavigationRenderer);
+ }
+ writer.println("navigation focus owner: " + getNavigationContextOwner());
+ writer.println("activity options: " + mActivityOptions);
+ writer.println("activity state: " + mActivityState);
+ writer.println("current nav component: " + mNavigationComponent);
+ writer.println("current nav packages: " + getNavigationContextOwner().mPackageNames);
+ writer.println("mInstrumentClusterHelper" + mInstrumentClusterHelper);
}
- writer.println("navigation focus owner: " + getNavigationContextOwner());
- writer.println("activity options: " + mActivityOptions);
- writer.println("activity state: " + mActivityState);
- writer.println("current nav component: " + mNavigationComponent);
- writer.println("current nav packages: " + getNavigationContextOwner().mPackageNames);
}
private class RendererBinder extends IInstrumentCluster.Stub {
@@ -539,8 +640,18 @@
}
/**
+ * See {@link #getBitmap(Uri, int, int, float)}
+ *
+ * @throws IllegalArgumentException if {@code width} or {@code height} is not greater than 0.
+ */
+ @Nullable
+ public Bitmap getBitmap(Uri uri, int width, int height) {
+ return getBitmap(uri, width, height, 1f);
+ }
+
+ /**
* Fetches a bitmap from the navigation context owner (application holding navigation focus)
- * of the given width and height. The fetched bitmaps are cached.
+ * of the given width and height and off lane opacity. The fetched bitmaps are cached.
* It returns null if:
* <ul>
* <li>there is no navigation context owner
@@ -549,13 +660,17 @@
* </ul>
* This is a costly operation. Returned bitmaps should be fetched on a secondary thread.
*
- * @throws IllegalArgumentException if {@code width} or {@code height} is not greater than 0.
+ * @throws IllegalArgumentException if width, height <= 0, or 0 > offLanesAlpha > 1
+ * @hide
*/
@Nullable
- public Bitmap getBitmap(Uri uri, int width, int height) {
+ public Bitmap getBitmap(Uri uri, int width, int height, float offLanesAlpha) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Width and height must be > 0");
}
+ if (offLanesAlpha < 0 || offLanesAlpha > 1) {
+ throw new IllegalArgumentException("offLanesAlpha must be between [0, 1]");
+ }
try {
ContextOwner contextOwner = getNavigationContextOwner();
@@ -567,6 +682,7 @@
uri = uri.buildUpon()
.appendQueryParameter(BITMAP_QUERY_WIDTH, String.valueOf(width))
.appendQueryParameter(BITMAP_QUERY_HEIGHT, String.valueOf(height))
+ .appendQueryParameter(BITMAP_QUERY_OFFLANESALPHA, String.valueOf(offLanesAlpha))
.build();
String host = uri.getHost();
diff --git a/car-lib/src/android/car/content/pm/CarAppBlockingPolicyService.java b/car-lib/src/android/car/content/pm/CarAppBlockingPolicyService.java
index 5b0a6bd..f95063a 100644
--- a/car-lib/src/android/car/content/pm/CarAppBlockingPolicyService.java
+++ b/car-lib/src/android/car/content/pm/CarAppBlockingPolicyService.java
@@ -17,6 +17,7 @@
import android.annotation.SystemApi;
import android.app.Service;
+import android.car.Car;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
@@ -76,7 +77,7 @@
try {
setter.setAppBlockingPolicy(policy);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ Car.handleRemoteExceptionFromCarService(CarAppBlockingPolicyService.this, e);
}
}
}
diff --git a/car-lib/src/android/car/content/pm/CarPackageManager.java b/car-lib/src/android/car/content/pm/CarPackageManager.java
index d23633d..7498c65 100644
--- a/car-lib/src/android/car/content/pm/CarPackageManager.java
+++ b/car-lib/src/android/car/content/pm/CarPackageManager.java
@@ -19,9 +19,9 @@
import android.annotation.IntDef;
import android.annotation.SystemApi;
import android.annotation.TestApi;
+import android.car.Car;
import android.car.CarManagerBase;
import android.content.ComponentName;
-import android.content.Context;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
@@ -32,7 +32,7 @@
/**
* Provides car specific API related with package management.
*/
-public final class CarPackageManager implements CarManagerBase {
+public final class CarPackageManager extends CarManagerBase {
private static final String TAG = "CarPackageManager";
/**
@@ -70,12 +70,11 @@
public @interface SetPolicyFlags {}
private final ICarPackageManager mService;
- private final Context mContext;
/** @hide */
- public CarPackageManager(IBinder service, Context context) {
+ public CarPackageManager(Car car, IBinder service) {
+ super(car);
mService = ICarPackageManager.Stub.asInterface(service);
- mContext = context;
}
/** @hide */
@@ -115,7 +114,7 @@
try {
mService.setAppBlockingPolicy(packageName, policy, flags);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -128,7 +127,7 @@
try {
mService.restartTask(taskId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -151,7 +150,7 @@
try {
return mService.isActivityBackedBySafeActivity(activityName);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -165,7 +164,7 @@
try {
mService.setEnableActivityBlocking(enable);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -181,7 +180,7 @@
try {
return mService.isActivityDistractionOptimized(packageName, className);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -197,7 +196,7 @@
try {
return mService.isServiceDistractionOptimized(packageName, className);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
}
diff --git a/car-lib/src/android/car/diagnostic/CarDiagnosticManager.java b/car-lib/src/android/car/diagnostic/CarDiagnosticManager.java
index 1559dd4..c9c8b6f 100644
--- a/car-lib/src/android/car/diagnostic/CarDiagnosticManager.java
+++ b/car-lib/src/android/car/diagnostic/CarDiagnosticManager.java
@@ -23,8 +23,6 @@
import android.car.CarLibLog;
import android.car.CarManagerBase;
import android.car.diagnostic.ICarDiagnosticEventListener.Stub;
-import android.content.Context;
-import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
@@ -47,7 +45,7 @@
* @hide
*/
@SystemApi
-public final class CarDiagnosticManager implements CarManagerBase {
+public final class CarDiagnosticManager extends CarManagerBase {
public static final int FRAME_TYPE_LIVE = 0;
public static final int FRAME_TYPE_FREEZE = 1;
@@ -70,15 +68,16 @@
/** Handles call back into clients. */
private final SingleMessageHandler<CarDiagnosticEvent> mHandlerCallback;
- private CarDiagnosticEventListenerToService mListenerToService;
+ private final CarDiagnosticEventListenerToService mListenerToService;
private final CarPermission mVendorExtensionPermission;
/** @hide */
- public CarDiagnosticManager(IBinder service, Context context, Handler handler) {
+ public CarDiagnosticManager(Car car, IBinder service) {
+ super(car);
mService = ICarDiagnostic.Stub.asInterface(service);
- mHandlerCallback = new SingleMessageHandler<CarDiagnosticEvent>(handler.getLooper(),
- MSG_DIAGNOSTIC_EVENTS) {
+ mHandlerCallback = new SingleMessageHandler<CarDiagnosticEvent>(
+ getEventHandler().getLooper(), MSG_DIAGNOSTIC_EVENTS) {
@Override
protected void handleEvent(CarDiagnosticEvent event) {
CarDiagnosticListeners listeners;
@@ -90,7 +89,9 @@
}
}
};
- mVendorExtensionPermission = new CarPermission(context, Car.PERMISSION_VENDOR_EXTENSION);
+ mVendorExtensionPermission = new CarPermission(getContext(),
+ Car.PERMISSION_VENDOR_EXTENSION);
+ mListenerToService = new CarDiagnosticEventListenerToService(this);
}
@Override
@@ -98,7 +99,6 @@
public void onCarDisconnected() {
synchronized(mActiveListeners) {
mActiveListeners.clear();
- mListenerToService = null;
}
}
@@ -137,9 +137,6 @@
OnDiagnosticEventListener listener, @FrameType int frameType, int rate) {
assertFrameType(frameType);
synchronized(mActiveListeners) {
- if (null == mListenerToService) {
- mListenerToService = new CarDiagnosticEventListenerToService(this);
- }
boolean needsServerUpdate = false;
CarDiagnosticListeners listeners = mActiveListeners.get(frameType);
if (listeners == null) {
@@ -184,7 +181,8 @@
mService.unregisterDiagnosticListener(frameType,
mListenerToService);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
+ // continue for local clean-up
}
mActiveListeners.remove(frameType);
} else if (needsServerUpdate) {
@@ -197,7 +195,7 @@
try {
return mService.registerOrUpdateDiagnosticListener(frameType, rate, mListenerToService);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -212,7 +210,7 @@
try {
return mService.getLatestLiveFrame();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -229,7 +227,7 @@
try {
return mService.getFreezeFrameTimestamps();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, new long[0]);
}
}
@@ -246,7 +244,7 @@
try {
return mService.getFreezeFrame(timestamp);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -264,7 +262,7 @@
try {
return mService.clearFreezeFrames(timestamps);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -276,7 +274,7 @@
try {
return mService.isLiveFrameSupported();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -288,7 +286,7 @@
try {
return mService.isFreezeFrameNotificationSupported();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -300,7 +298,7 @@
try {
return mService.isGetFreezeFrameSupported();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -318,7 +316,7 @@
try {
return mService.isClearFreezeFramesSupported();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -336,7 +334,7 @@
try {
return mService.isSelectiveClearFreezeFramesSupported();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
diff --git a/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java b/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java
index 9b0626f..4053c5c 100644
--- a/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java
+++ b/car-lib/src/android/car/drivingstate/CarDrivingStateManager.java
@@ -22,7 +22,6 @@
import android.annotation.TestApi;
import android.car.Car;
import android.car.CarManagerBase;
-import android.content.Context;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -40,13 +39,12 @@
*/
@SystemApi
@TestApi
-public final class CarDrivingStateManager implements CarManagerBase {
+public final class CarDrivingStateManager extends CarManagerBase {
private static final String TAG = "CarDrivingStateMgr";
private static final boolean DBG = false;
private static final boolean VDBG = false;
private static final int MSG_HANDLE_DRIVING_STATE_CHANGE = 0;
- private final Context mContext;
private final ICarDrivingState mDrivingService;
private final EventCallbackHandler mEventCallbackHandler;
private CarDrivingStateEventListener mDrvStateEventListener;
@@ -54,10 +52,10 @@
/** @hide */
- public CarDrivingStateManager(IBinder service, Context context, Handler handler) {
- mContext = context;
+ public CarDrivingStateManager(Car car, IBinder service) {
+ super(car);
mDrivingService = ICarDrivingState.Stub.asInterface(service);
- mEventCallbackHandler = new EventCallbackHandler(this, handler.getLooper());
+ mEventCallbackHandler = new EventCallbackHandler(this, getEventHandler().getLooper());
}
/** @hide */
@@ -111,7 +109,7 @@
// register to the Service for getting notified
mDrivingService.registerDrivingStateChangeListener(mListenerToService);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -134,7 +132,7 @@
mDrvStateEventListener = null;
mListenerToService = null;
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -151,7 +149,7 @@
try {
return mDrivingService.getCurrentDrivingState();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -172,7 +170,7 @@
try {
mDrivingService.injectDrivingState(event);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
diff --git a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
index 9a3d5cf..be194b8 100644
--- a/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
+++ b/car-lib/src/android/car/drivingstate/CarUxRestrictionsManager.java
@@ -22,7 +22,6 @@
import android.annotation.RequiresPermission;
import android.car.Car;
import android.car.CarManagerBase;
-import android.content.Context;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -43,7 +42,7 @@
* API to register and get the User Experience restrictions imposed based on the car's driving
* state.
*/
-public final class CarUxRestrictionsManager implements CarManagerBase {
+public final class CarUxRestrictionsManager extends CarManagerBase {
private static final String TAG = "CarUxRManager";
private static final boolean DBG = false;
private static final boolean VDBG = false;
@@ -80,7 +79,6 @@
@Retention(RetentionPolicy.SOURCE)
public @interface UxRestrictionMode {}
- private final Context mContext;
private int mDisplayId = Display.INVALID_DISPLAY;
private final ICarUxRestrictionsManager mUxRService;
private final EventCallbackHandler mEventCallbackHandler;
@@ -89,10 +87,11 @@
private CarUxRestrictionsChangeListenerToService mListenerToService;
/** @hide */
- public CarUxRestrictionsManager(IBinder service, Context context, Handler handler) {
- mContext = context;
+ public CarUxRestrictionsManager(Car car, IBinder service) {
+ super(car);
mUxRService = ICarUxRestrictionsManager.Stub.asInterface(service);
- mEventCallbackHandler = new EventCallbackHandler(this, handler.getLooper());
+ mEventCallbackHandler = new EventCallbackHandler(this,
+ getEventHandler().getLooper());
}
/** @hide */
@@ -152,7 +151,7 @@
// register to the Service to listen for changes.
mUxRService.registerUxRestrictionsChangeListener(mListenerToService, displayId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -172,7 +171,7 @@
try {
mUxRService.unregisterUxRestrictionsChangeListener(mListenerToService);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -197,7 +196,7 @@
try {
return mUxRService.saveUxRestrictionsConfigurationForNextBoot(configs);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -219,7 +218,7 @@
try {
return mUxRService.getCurrentUxRestrictions(displayId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -233,7 +232,7 @@
try {
return mUxRService.setRestrictionMode(mode);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -248,7 +247,7 @@
try {
return mUxRService.getRestrictionMode();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -288,7 +287,7 @@
try {
return mUxRService.getStagedConfigs();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -304,7 +303,7 @@
try {
return mUxRService.getConfigs();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -399,7 +398,7 @@
return mDisplayId;
}
- mDisplayId = mContext.getDisplayId();
+ mDisplayId = getContext().getDisplayId();
Log.i(TAG, "Context returns display ID " + mDisplayId);
if (mDisplayId == Display.INVALID_DISPLAY) {
diff --git a/car-lib/src/android/car/hardware/CarSensorManager.java b/car-lib/src/android/car/hardware/CarSensorManager.java
index d61cb2e..082c8eb 100644
--- a/car-lib/src/android/car/hardware/CarSensorManager.java
+++ b/car-lib/src/android/car/hardware/CarSensorManager.java
@@ -26,9 +26,7 @@
import android.car.hardware.property.CarPropertyManager;
import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback;
import android.car.hardware.property.ICarProperty;
-import android.content.Context;
import android.os.Bundle;
-import android.os.Handler;
import android.os.IBinder;
import android.util.ArraySet;
import android.util.Log;
@@ -46,7 +44,7 @@
* API for monitoring car sensor data.
*/
@Deprecated
-public final class CarSensorManager implements CarManagerBase {
+public final class CarSensorManager extends CarManagerBase {
private static final String TAG = "CarSensorManager";
private final CarPropertyManager mCarPropertyMgr;
/** @hide */
@@ -304,9 +302,10 @@
}
/** @hide */
- public CarSensorManager(IBinder service, Context context, Handler handler) {
+ public CarSensorManager(Car car, IBinder service) {
+ super(car);
ICarProperty mCarPropertyService = ICarProperty.Stub.asInterface(service);
- mCarPropertyMgr = new CarPropertyManager(mCarPropertyService, handler);
+ mCarPropertyMgr = new CarPropertyManager(car, mCarPropertyService);
}
/** @hide */
diff --git a/car-lib/src/android/car/hardware/CarVendorExtensionManager.java b/car-lib/src/android/car/hardware/CarVendorExtensionManager.java
index e7df3b0..b796156 100644
--- a/car-lib/src/android/car/hardware/CarVendorExtensionManager.java
+++ b/car-lib/src/android/car/hardware/CarVendorExtensionManager.java
@@ -22,7 +22,6 @@
import android.car.hardware.property.CarPropertyManager;
import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback;
import android.car.hardware.property.ICarProperty;
-import android.os.Handler;
import android.os.IBinder;
import android.util.ArraySet;
@@ -44,7 +43,7 @@
*/
@Deprecated
@SystemApi
-public final class CarVendorExtensionManager implements CarManagerBase {
+public final class CarVendorExtensionManager extends CarManagerBase {
private final static boolean DBG = false;
private final static String TAG = CarVendorExtensionManager.class.getSimpleName();
@@ -84,9 +83,10 @@
* <p>Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead.
* @hide
*/
- public CarVendorExtensionManager(IBinder service, Handler handler) {
+ public CarVendorExtensionManager(Car car, IBinder service) {
+ super(car);
ICarProperty mCarPropertyService = ICarProperty.Stub.asInterface(service);
- mPropertyManager = new CarPropertyManager(mCarPropertyService, handler);
+ mPropertyManager = new CarPropertyManager(car, mCarPropertyService);
}
/**
@@ -206,6 +206,9 @@
/** @hide */
@Override
public void onCarDisconnected() {
+ synchronized (mLock) {
+ mCallbacks.clear();
+ }
mPropertyManager.onCarDisconnected();
}
private static class CarPropertyEventListenerToBase implements CarPropertyEventCallback {
diff --git a/car-lib/src/android/car/hardware/cabin/CarCabinManager.java b/car-lib/src/android/car/hardware/cabin/CarCabinManager.java
index 1c41a2b..7318176 100644
--- a/car-lib/src/android/car/hardware/cabin/CarCabinManager.java
+++ b/car-lib/src/android/car/hardware/cabin/CarCabinManager.java
@@ -25,8 +25,6 @@
import android.car.hardware.property.CarPropertyManager;
import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback;
import android.car.hardware.property.ICarProperty;
-import android.content.Context;
-import android.os.Handler;
import android.os.IBinder;
import android.util.ArraySet;
@@ -58,7 +56,7 @@
*/
@Deprecated
@SystemApi
-public final class CarCabinManager implements CarManagerBase {
+public final class CarCabinManager extends CarManagerBase {
private final static boolean DBG = false;
private final static String TAG = "CarCabinManager";
private final CarPropertyManager mCarPropertyMgr;
@@ -470,9 +468,10 @@
* @param handler
* @hide
*/
- public CarCabinManager(IBinder service, Context context, Handler handler) {
+ public CarCabinManager(Car car, IBinder service) {
+ super(car);
ICarProperty mCarPropertyService = ICarProperty.Stub.asInterface(service);
- mCarPropertyMgr = new CarPropertyManager(mCarPropertyService, handler);
+ mCarPropertyMgr = new CarPropertyManager(car, mCarPropertyService);
}
/**
@@ -594,6 +593,10 @@
/** @hide */
@Override
public void onCarDisconnected() {
+ // TODO(b/142730969) Fix synchronization to use separate mLock
+ synchronized (this) {
+ mCallbacks.clear();
+ }
mCarPropertyMgr.onCarDisconnected();
}
}
diff --git a/car-lib/src/android/car/hardware/hvac/CarHvacManager.java b/car-lib/src/android/car/hardware/hvac/CarHvacManager.java
index b2b8014..3ab7631 100644
--- a/car-lib/src/android/car/hardware/hvac/CarHvacManager.java
+++ b/car-lib/src/android/car/hardware/hvac/CarHvacManager.java
@@ -25,8 +25,6 @@
import android.car.hardware.property.CarPropertyManager;
import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback;
import android.car.hardware.property.ICarProperty;
-import android.content.Context;
-import android.os.Handler;
import android.os.IBinder;
import android.util.ArraySet;
import android.util.Log;
@@ -46,7 +44,7 @@
*/
@Deprecated
@SystemApi
-public final class CarHvacManager implements CarManagerBase {
+public final class CarHvacManager extends CarManagerBase {
private final static boolean DBG = false;
private final static String TAG = "CarHvacManager";
private final CarPropertyManager mCarPropertyMgr;
@@ -301,14 +299,15 @@
*
* Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead.
* @param service
- * @param context
- * @param handler
+ *
* @hide
*/
- public CarHvacManager(IBinder service, Context context, Handler handler) {
+ public CarHvacManager(Car car, IBinder service) {
+ super(car);
ICarProperty mCarPropertyService = ICarProperty.Stub.asInterface(service);
- mCarPropertyMgr = new CarPropertyManager(mCarPropertyService, handler);
+ mCarPropertyMgr = new CarPropertyManager(car, mCarPropertyService);
}
+
/**
* Implement wrappers for contained CarPropertyManager object
* @param callback
@@ -432,6 +431,10 @@
/** @hide */
public void onCarDisconnected() {
+ // TODO(b/142730482) Fix synchronization to use separate mLock
+ synchronized (this) {
+ mCallbacks.clear();
+ }
mCarPropertyMgr.onCarDisconnected();
}
}
diff --git a/car-lib/src/android/car/hardware/power/CarPowerManager.java b/car-lib/src/android/car/hardware/power/CarPowerManager.java
index 3d9a23a..4b0e8cf 100644
--- a/car-lib/src/android/car/hardware/power/CarPowerManager.java
+++ b/car-lib/src/android/car/hardware/power/CarPowerManager.java
@@ -19,8 +19,6 @@
import android.annotation.SystemApi;
import android.car.Car;
import android.car.CarManagerBase;
-import android.content.Context;
-import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
@@ -35,15 +33,18 @@
* @hide
*/
@SystemApi
-public class CarPowerManager implements CarManagerBase {
+public class CarPowerManager extends CarManagerBase {
private final static boolean DBG = false;
private final static String TAG = "CarPowerManager";
private final Object mLock = new Object();
private final ICarPower mService;
+ @GuardedBy("mLock")
private CarPowerStateListener mListener;
+ @GuardedBy("mLock")
private CarPowerStateListenerWithCompletion mListenerWithCompletion;
+ @GuardedBy("mLock")
private CompletableFuture<Void> mFuture;
@GuardedBy("mLock")
private ICarPowerStateListener mListenerToService;
@@ -131,7 +132,8 @@
* @param handler
* @hide
*/
- public CarPowerManager(IBinder service, Context context, Handler handler) {
+ public CarPowerManager(Car car, IBinder service) {
+ super(car);
mService = ICarPower.Stub.asInterface(service);
}
@@ -143,7 +145,7 @@
try {
mService.requestShutdownOnNextSuspend();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -155,7 +157,7 @@
try {
mService.scheduleNextWakeupTime(seconds);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -209,13 +211,23 @@
@Override
public void onStateChanged(int state) throws RemoteException {
if (useCompletion) {
- // Update CompletableFuture. This will recreate it or just clean it up.
- updateFuture(state);
+ CarPowerStateListenerWithCompletion listenerWithCompletion;
+ CompletableFuture<Void> future;
+ synchronized (mLock) {
+ // Update CompletableFuture. This will recreate it or just clean it up.
+ updateFutureLocked(state);
+ listenerWithCompletion = mListenerWithCompletion;
+ future = mFuture;
+ }
// Notify user that the state has changed and supply a future
- mListenerWithCompletion.onStateChanged(state, mFuture);
+ listenerWithCompletion.onStateChanged(state, future);
} else {
+ CarPowerStateListener listener;
+ synchronized (mLock) {
+ listener = mListener;
+ }
// Notify the user without supplying a future
- mListener.onStateChanged(state);
+ listener.onStateChanged(state);
}
}
};
@@ -227,7 +239,7 @@
}
mListenerToService = listenerToService;
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
}
@@ -243,7 +255,7 @@
mListenerToService = null;
mListener = null;
mListenerWithCompletion = null;
- cleanupFuture();
+ cleanupFutureLocked();
}
if (listenerToService == null) {
@@ -254,12 +266,12 @@
try {
mService.unregisterListener(listenerToService);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
- private void updateFuture(int state) {
- cleanupFuture();
+ private void updateFutureLocked(int state) {
+ cleanupFutureLocked();
if (state == CarPowerStateListener.SHUTDOWN_PREPARE) {
// Create a CompletableFuture and pass it to the listener.
// When the listener completes the future, tell
@@ -269,16 +281,20 @@
if (exception != null && !(exception instanceof CancellationException)) {
Log.e(TAG, "Exception occurred while waiting for future", exception);
}
+ ICarPowerStateListener listenerToService;
+ synchronized (mLock) {
+ listenerToService = mListenerToService;
+ }
try {
- mService.finished(mListenerToService);
+ mService.finished(listenerToService);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
});
}
}
- private void cleanupFuture() {
+ private void cleanupFutureLocked() {
if (mFuture != null) {
if (!mFuture.isDone()) {
mFuture.cancel(false);
@@ -290,13 +306,9 @@
/** @hide */
@Override
public void onCarDisconnected() {
- ICarPowerStateListener listenerToService;
synchronized (mLock) {
- listenerToService = mListenerToService;
- }
-
- if (listenerToService != null) {
- clearListener();
+ mListener = null;
+ mListenerWithCompletion = null;
}
}
}
diff --git a/car-lib/src/android/car/hardware/property/CarPropertyManager.java b/car-lib/src/android/car/hardware/property/CarPropertyManager.java
index 3f7da1d..e3651da 100644
--- a/car-lib/src/android/car/hardware/property/CarPropertyManager.java
+++ b/car-lib/src/android/car/hardware/property/CarPropertyManager.java
@@ -21,6 +21,7 @@
import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.car.Car;
import android.car.CarManagerBase;
import android.car.hardware.CarPropertyConfig;
import android.car.hardware.CarPropertyValue;
@@ -44,7 +45,7 @@
* For details about the individual properties, see the descriptions in
* hardware/interfaces/automotive/vehicle/types.hal
*/
-public class CarPropertyManager implements CarManagerBase {
+public class CarPropertyManager extends CarManagerBase {
private static final boolean DBG = false;
private static final String TAG = "CarPropertyManager";
private static final int MSG_GENERIC_EVENT = 0;
@@ -93,11 +94,12 @@
* Get an instance of the CarPropertyManager.
*
* Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead.
+ * @param car Car instance
* @param service ICarProperty instance
- * @param handler The handler to deal with CarPropertyEvent.
* @hide
*/
- public CarPropertyManager(@NonNull ICarProperty service, @Nullable Handler handler) {
+ public CarPropertyManager(Car car, @NonNull ICarProperty service) {
+ super(car);
mService = service;
try {
List<CarPropertyConfig> configs = mService.getPropertyList();
@@ -108,11 +110,12 @@
Log.e(TAG, "getPropertyList exception ", e);
throw new RuntimeException(e);
}
- if (handler == null) {
+ Handler eventHandler = getEventHandler();
+ if (eventHandler == null) {
mHandler = null;
return;
}
- mHandler = new SingleMessageHandler<CarPropertyEvent>(handler.getLooper(),
+ mHandler = new SingleMessageHandler<CarPropertyEvent>(eventHandler.getLooper(),
MSG_GENERIC_EVENT) {
@Override
protected void handleEvent(CarPropertyEvent event) {
@@ -206,7 +209,7 @@
try {
mService.registerListener(propertyId, rate, mCarPropertyEventToService);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
return true;
}
@@ -274,7 +277,8 @@
try {
mService.unregisterListener(propertyId, mCarPropertyEventToService);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
+ // continue for local clean-up
}
mActivePropertyListener.remove(propertyId);
} else if (needsServerUpdate) {
@@ -327,7 +331,7 @@
try {
return mService.getReadPermission(propId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, "");
}
}
@@ -346,7 +350,7 @@
try {
return mService.getWritePermission(propId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, "");
}
}
@@ -363,7 +367,7 @@
return (propValue != null)
&& (propValue.getStatus() == CarPropertyValue.STATUS_AVAILABLE);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -449,7 +453,7 @@
}
return propVal;
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -466,7 +470,7 @@
CarPropertyValue<E> propVal = mService.getProperty(propId, areaId);
return propVal;
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -488,7 +492,7 @@
try {
mService.setProperty(new CarPropertyValue<>(propId, areaId, val));
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
diff --git a/car-lib/src/android/car/input/CarInputHandlingService.java b/car-lib/src/android/car/input/CarInputHandlingService.java
index 518cee1..0ea990f 100644
--- a/car-lib/src/android/car/input/CarInputHandlingService.java
+++ b/car-lib/src/android/car/input/CarInputHandlingService.java
@@ -19,6 +19,7 @@
import android.annotation.MainThread;
import android.annotation.SystemApi;
import android.app.Service;
+import android.car.Car;
import android.car.CarLibLog;
import android.content.Intent;
import android.os.Bundle;
@@ -101,7 +102,7 @@
try {
callbackBinder.transact(INPUT_CALLBACK_BINDER_CODE, dataIn, null, IBinder.FLAG_ONEWAY);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ Car.handleRemoteExceptionFromCarService(this, e);
}
}
diff --git a/car-lib/src/android/car/media/CarAudioManager.java b/car-lib/src/android/car/media/CarAudioManager.java
index dcd4514..2bc8fd7 100644
--- a/car-lib/src/android/car/media/CarAudioManager.java
+++ b/car-lib/src/android/car/media/CarAudioManager.java
@@ -22,10 +22,8 @@
import android.car.Car;
import android.car.CarLibLog;
import android.car.CarManagerBase;
-import android.content.Context;
import android.media.AudioAttributes;
import android.os.Bundle;
-import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
@@ -52,7 +50,7 @@
* - There is exactly one audio zone, which is the primary zone
* - Each volume group represents a controllable STREAM_TYPE, same as AudioManager
*/
-public final class CarAudioManager implements CarManagerBase {
+public final class CarAudioManager extends CarManagerBase {
/**
* Zone id of the primary audio zone.
@@ -114,7 +112,7 @@
try {
return mService.isDynamicRoutingEnabled();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -147,7 +145,7 @@
try {
mService.setGroupVolume(zoneId, groupId, index, flags);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -177,7 +175,7 @@
try {
return mService.getGroupMaxVolume(zoneId, groupId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, 0);
}
}
@@ -207,7 +205,7 @@
try {
return mService.getGroupMinVolume(zoneId, groupId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, 0);
}
}
@@ -240,7 +238,7 @@
try {
return mService.getGroupVolume(zoneId, groupId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, 0);
}
}
@@ -259,7 +257,7 @@
try {
mService.setFadeTowardFront(value);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -278,7 +276,7 @@
try {
mService.setBalanceTowardRight(value);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -300,7 +298,8 @@
try {
return mService.getExternalSources();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
+ return new String[0];
}
}
@@ -330,7 +329,7 @@
try {
return mService.createAudioPatch(sourceAddress, usage, gainInMillibels);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -350,7 +349,7 @@
try {
mService.releaseAudioPatch(patch);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -379,7 +378,7 @@
try {
return mService.getVolumeGroupCount(zoneId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, 0);
}
}
@@ -409,7 +408,7 @@
try {
return mService.getVolumeGroupIdForUsage(zoneId, usage);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, 0);
}
}
@@ -436,7 +435,7 @@
try {
return mService.getAudioZoneIds();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, new int[0]);
}
}
@@ -453,7 +452,7 @@
try {
return mService.getZoneIdForUid(uid);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, 0);
}
}
@@ -470,7 +469,7 @@
try {
return mService.setZoneIdForUid(zoneId, uid);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -486,7 +485,7 @@
try {
return mService.clearZoneIdForUid(uid);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -523,7 +522,7 @@
try {
return mService.getZoneIdForDisplayPortId(displayPortId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, 0);
}
}
@@ -541,7 +540,7 @@
try {
return mService.getUsagesForVolumeGroupId(zoneId, groupId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, new int[0]);
}
}
@@ -552,16 +551,16 @@
try {
mService.unregisterVolumeCallback(mCarVolumeCallbackImpl.asBinder());
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
}
/** @hide */
- public CarAudioManager(IBinder service, Context context, Handler handler) {
+ public CarAudioManager(Car car, IBinder service) {
+ super(car);
mService = ICarAudio.Stub.asInterface(service);
mCarVolumeCallbacks = new ArrayList<>();
-
try {
mService.registerVolumeCallback(mCarVolumeCallbackImpl.asBinder());
} catch (RemoteException e) {
diff --git a/car-lib/src/android/car/media/CarMediaManager.java b/car-lib/src/android/car/media/CarMediaManager.java
index 12c2dc8..8537ed6 100644
--- a/car-lib/src/android/car/media/CarMediaManager.java
+++ b/car-lib/src/android/car/media/CarMediaManager.java
@@ -29,7 +29,7 @@
* API for updating and receiving updates to the primary media source in the car.
* @hide
*/
-public final class CarMediaManager implements CarManagerBase {
+public final class CarMediaManager extends CarManagerBase {
private final ICarMedia mService;
private Map<MediaSourceChangedListener, ICarMediaSourceListener> mCallbackMap = new HashMap();
@@ -40,7 +40,8 @@
* Should not be obtained directly by clients, use {@link Car#getCarManager(String)} instead.
* @hide
*/
- public CarMediaManager(IBinder service) {
+ public CarMediaManager(Car car, IBinder service) {
+ super(car);
mService = ICarMedia.Stub.asInterface(service);
}
@@ -67,7 +68,7 @@
try {
return mService.getMediaSource();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -81,7 +82,7 @@
try {
mService.setMediaSource(componentName);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -102,7 +103,7 @@
mCallbackMap.put(callback, binderCallback);
mService.registerMediaSourceListener(binderCallback);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -117,12 +118,14 @@
ICarMediaSourceListener binderCallback = mCallbackMap.remove(callback);
mService.unregisterMediaSourceListener(binderCallback);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
/** @hide */
@Override
public synchronized void onCarDisconnected() {
+ // TODO(b/142733057) Fix synchronization to use separate mLock
+ mCallbackMap.clear();
}
}
diff --git a/car-lib/src/android/car/navigation/CarNavigationStatusManager.java b/car-lib/src/android/car/navigation/CarNavigationStatusManager.java
index a70e3c8..2aa2f10 100644
--- a/car-lib/src/android/car/navigation/CarNavigationStatusManager.java
+++ b/car-lib/src/android/car/navigation/CarNavigationStatusManager.java
@@ -24,14 +24,13 @@
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
-import android.util.Log;
/**
* API for providing navigation status for instrument cluster.
* @hide
*/
@SystemApi
-public final class CarNavigationStatusManager implements CarManagerBase {
+public final class CarNavigationStatusManager extends CarManagerBase {
private static final String TAG = CarLibLog.TAG_NAV;
private final IInstrumentClusterNavigation mService;
@@ -40,7 +39,8 @@
* Only for CarServiceLoader
* @hide
*/
- public CarNavigationStatusManager(IBinder service) {
+ public CarNavigationStatusManager(Car car, IBinder service) {
+ super(car);
mService = IInstrumentClusterNavigation.Stub.asInterface(service);
}
@@ -67,14 +67,13 @@
try {
mService.onNavigationStateChanged(bundle);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
/** @hide */
@Override
public void onCarDisconnected() {
- Log.e(TAG, "Car service disconnected");
}
/** Returns navigation features of instrument cluster */
@@ -83,7 +82,7 @@
try {
return mService.getInstrumentClusterInfo();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
}
diff --git a/car-lib/src/android/car/settings/CarConfigurationManager.java b/car-lib/src/android/car/settings/CarConfigurationManager.java
index 34d5f4a..626ad39 100644
--- a/car-lib/src/android/car/settings/CarConfigurationManager.java
+++ b/car-lib/src/android/car/settings/CarConfigurationManager.java
@@ -16,6 +16,7 @@
package android.car.settings;
+import android.car.Car;
import android.car.CarManagerBase;
import android.os.IBinder;
import android.os.RemoteException;
@@ -23,13 +24,14 @@
/**
* Manager that exposes car configuration values that are stored on the system.
*/
-public class CarConfigurationManager implements CarManagerBase {
+public class CarConfigurationManager extends CarManagerBase {
private static final String TAG = "CarConfigurationManager";
private final ICarConfigurationManager mConfigurationService;
/** @hide */
- public CarConfigurationManager(IBinder service) {
+ public CarConfigurationManager(Car car, IBinder service) {
+ super(car);
mConfigurationService = ICarConfigurationManager.Stub.asInterface(service);
}
@@ -42,7 +44,7 @@
try {
return mConfigurationService.getSpeedBumpConfiguration();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
diff --git a/car-lib/src/android/car/settings/CarSettings.java b/car-lib/src/android/car/settings/CarSettings.java
index ab7c906..d81b7ad 100644
--- a/car-lib/src/android/car/settings/CarSettings.java
+++ b/car-lib/src/android/car/settings/CarSettings.java
@@ -153,5 +153,14 @@
*/
public static final String KEY_ENABLE_INITIAL_NOTICE_SCREEN_TO_USER =
"android.car.ENABLE_INITIAL_NOTICE_SCREEN_TO_USER";
+
+ /**
+ * Key to indicate Setup Wizard is in progress. It differs from USER_SETUP_COMPLETE in
+ * that this flag can be reset to 0 in deferred Setup Wizard flow.
+ * The value is boolean (1 or 0).
+ * @hide
+ */
+ public static final String KEY_SETUP_WIZARD_IN_PROGRESS =
+ "android.car.SETUP_WIZARD_IN_PROGRESS";
}
}
diff --git a/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java b/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java
index ff7b099..69c092b 100644
--- a/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java
+++ b/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java
@@ -19,7 +19,6 @@
import android.annotation.SystemApi;
import android.car.Car;
import android.car.CarManagerBase;
-import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
@@ -37,7 +36,7 @@
* @hide
*/
@SystemApi
-public final class CarStorageMonitoringManager implements CarManagerBase {
+public final class CarStorageMonitoringManager extends CarManagerBase {
private static final String TAG = CarStorageMonitoringManager.class.getSimpleName();
private static final int MSG_IO_STATS_EVENT = 0;
@@ -77,9 +76,10 @@
/**
* @hide
*/
- public CarStorageMonitoringManager(IBinder service, Handler handler) {
+ public CarStorageMonitoringManager(Car car, IBinder service) {
+ super(car);
mService = ICarStorageMonitoring.Stub.asInterface(service);
- mMessageHandler = new SingleMessageHandler<IoStats>(handler, MSG_IO_STATS_EVENT) {
+ mMessageHandler = new SingleMessageHandler<IoStats>(getEventHandler(), MSG_IO_STATS_EVENT) {
@Override
protected void handleEvent(IoStats event) {
for (IoStatsListener listener : mListeners) {
@@ -112,7 +112,7 @@
try {
return mService.getPreEolIndicatorStatus();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, PRE_EOL_INFO_UNKNOWN);
}
}
@@ -130,7 +130,7 @@
try {
return mService.getWearEstimate();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -150,7 +150,7 @@
try {
return mService.getWearEstimateHistory();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, Collections.emptyList());
}
}
@@ -169,7 +169,7 @@
try {
return mService.getBootIoStats();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, Collections.emptyList());
}
}
@@ -199,7 +199,7 @@
try {
return mService.getShutdownDiskWriteAmount();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, 0);
}
}
@@ -216,7 +216,7 @@
try {
return mService.getAggregateIoStats();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, Collections.emptyList());
}
}
@@ -236,7 +236,7 @@
try {
return mService.getIoStatsDeltas();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, Collections.emptyList());
}
}
@@ -259,7 +259,7 @@
}
mListeners.add(listener);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -277,7 +277,7 @@
mListenerToService = null;
}
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
}
diff --git a/car-lib/src/android/car/test/CarTestManagerBinderWrapper.java b/car-lib/src/android/car/test/CarTestManagerBinderWrapper.java
index 5e39ea3..0a167cf 100644
--- a/car-lib/src/android/car/test/CarTestManagerBinderWrapper.java
+++ b/car-lib/src/android/car/test/CarTestManagerBinderWrapper.java
@@ -15,6 +15,7 @@
*/
package android.car.test;
+import android.car.Car;
import android.car.CarManagerBase;
import android.os.IBinder;
@@ -22,10 +23,17 @@
* Only for system testing
* @hide
*/
-public class CarTestManagerBinderWrapper implements CarManagerBase {
+public class CarTestManagerBinderWrapper extends CarManagerBase {
public final IBinder binder;
public CarTestManagerBinderWrapper(IBinder binder) {
+ super(null); // This will not work safely but is only for keeping API.
+ this.binder = binder;
+ }
+
+ /** @hide */
+ public CarTestManagerBinderWrapper(Car car, IBinder binder) {
+ super(car);
this.binder = binder;
}
diff --git a/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java b/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java
index c82d515..9881420 100644
--- a/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java
+++ b/car-lib/src/android/car/trust/CarTrustAgentEnrollmentManager.java
@@ -24,8 +24,8 @@
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.bluetooth.BluetoothDevice;
+import android.car.Car;
import android.car.CarManagerBase;
-import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -39,6 +39,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
+import java.util.Collections;
import java.util.List;
@@ -67,7 +68,7 @@
* @hide
*/
@SystemApi
-public final class CarTrustAgentEnrollmentManager implements CarManagerBase {
+public final class CarTrustAgentEnrollmentManager extends CarManagerBase {
private static final String TAG = "CarTrustEnrollMgr";
private static final String KEY_HANDLE = "handle";
private static final String KEY_ACTIVE = "active";
@@ -81,7 +82,6 @@
private static final int MSG_ENROLL_TOKEN_STATE_CHANGED = 7;
private static final int MSG_ENROLL_TOKEN_REMOVED = 8;
- private final Context mContext;
private final ICarTrustAgentEnrollment mEnrollmentService;
private Object mListenerLock = new Object();
@GuardedBy("mListenerLock")
@@ -114,10 +114,10 @@
/** @hide */
- public CarTrustAgentEnrollmentManager(IBinder service, Context context, Handler handler) {
- mContext = context;
+ public CarTrustAgentEnrollmentManager(Car car, IBinder service) {
+ super(car);
mEnrollmentService = ICarTrustAgentEnrollment.Stub.asInterface(service);
- mEventCallbackHandler = new EventCallbackHandler(this, handler.getLooper());
+ mEventCallbackHandler = new EventCallbackHandler(this, getEventHandler().getLooper());
}
/** @hide */
@@ -134,7 +134,7 @@
try {
mEnrollmentService.startEnrollmentAdvertising();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -146,7 +146,7 @@
try {
mEnrollmentService.stopEnrollmentAdvertising();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -161,7 +161,7 @@
try {
mEnrollmentService.enrollmentHandshakeAccepted(device);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -173,7 +173,7 @@
try {
mEnrollmentService.terminateEnrollmentHandshake();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -194,7 +194,7 @@
try {
return mEnrollmentService.isEscrowTokenActive(handle, uid);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, false);
}
}
@@ -209,7 +209,7 @@
try {
mEnrollmentService.removeEscrowToken(handle, uid);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -223,7 +223,7 @@
try {
mEnrollmentService.removeAllTrustedDevices(uid);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -238,7 +238,7 @@
try {
mEnrollmentService.setTrustedDeviceEnrollmentEnabled(isEnabled);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -253,7 +253,7 @@
try {
mEnrollmentService.setTrustedDeviceUnlockEnabled(isEnabled);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -278,7 +278,7 @@
mEnrollmentService.registerEnrollmentCallback(mListenerToEnrollmentService);
mEnrollmentCallback = callback;
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
}
@@ -290,7 +290,7 @@
try {
mEnrollmentService.unregisterEnrollmentCallback(mListenerToEnrollmentService);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
mEnrollmentCallback = null;
}
@@ -318,7 +318,7 @@
mEnrollmentService.registerBleCallback(mListenerToBleService);
mBleCallback = callback;
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
}
@@ -330,7 +330,7 @@
try {
mEnrollmentService.unregisterBleCallback(mListenerToBleService);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
mBleCallback = null;
}
@@ -351,7 +351,7 @@
try {
return mEnrollmentService.getEnrolledDeviceInfosForUser(uid);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, Collections.emptyList());
}
}
diff --git a/car-lib/src/android/car/vms/VmsPublisherClientService.java b/car-lib/src/android/car/vms/VmsPublisherClientService.java
index 309d0ee..ea75707 100644
--- a/car-lib/src/android/car/vms/VmsPublisherClientService.java
+++ b/car-lib/src/android/car/vms/VmsPublisherClientService.java
@@ -20,6 +20,7 @@
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.app.Service;
+import android.car.Car;
import android.content.Intent;
import android.os.Binder;
import android.os.Build;
@@ -114,7 +115,7 @@
try {
mVmsPublisherService.publish(token, layer, publisherId, payload);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ Car.handleRemoteExceptionFromCarService(this, e);
}
}
@@ -134,7 +135,7 @@
mVmsPublisherService.setLayersOffering(token, offering);
VmsOperationRecorder.get().setLayersOffering(offering);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ Car.handleRemoteExceptionFromCarService(this, e);
}
}
@@ -172,6 +173,7 @@
publisherId = mVmsPublisherService.getPublisherId(publisherInfo);
Log.i(TAG, "Assigned publisher ID: " + publisherId);
} catch (RemoteException e) {
+ // This will crash. To prevent crash, safer invalid return value should be defined.
throw e.rethrowFromSystemServer();
}
VmsOperationRecorder.get().getPublisherId(publisherId);
@@ -191,7 +193,7 @@
try {
return mVmsPublisherService.getSubscriptions();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return Car.handleRemoteExceptionFromCarService(this, e, null);
}
}
diff --git a/car-lib/src/android/car/vms/VmsSubscriberManager.java b/car-lib/src/android/car/vms/VmsSubscriberManager.java
index c02d3a5..edde982 100644
--- a/car-lib/src/android/car/vms/VmsSubscriberManager.java
+++ b/car-lib/src/android/car/vms/VmsSubscriberManager.java
@@ -19,6 +19,7 @@
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import android.car.Car;
import android.car.CarManagerBase;
import android.os.Binder;
import android.os.IBinder;
@@ -39,7 +40,7 @@
* @hide
*/
@SystemApi
-public final class VmsSubscriberManager implements CarManagerBase {
+public final class VmsSubscriberManager extends CarManagerBase {
private static final String TAG = "VmsSubscriberManager";
private final IVmsSubscriberService mVmsSubscriberService;
@@ -75,7 +76,8 @@
*
* @hide
*/
- public VmsSubscriberManager(IBinder service) {
+ public VmsSubscriberManager(Car car, IBinder service) {
+ super(car);
mVmsSubscriberService = IVmsSubscriberService.Stub.asInterface(service);
mSubscriberManagerClient = new IVmsSubscriberClient.Stub() {
@Override
@@ -133,7 +135,7 @@
try {
mVmsSubscriberService.addVmsSubscriberToNotifications(mSubscriberManagerClient);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -148,7 +150,7 @@
try {
mVmsSubscriberService.removeVmsSubscriberToNotifications(mSubscriberManagerClient);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
} finally {
synchronized (mClientCallbackLock) {
mClientCallback = null;
@@ -168,7 +170,7 @@
try {
return mVmsSubscriberService.getPublisherInfo(publisherId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -182,7 +184,7 @@
try {
return mVmsSubscriberService.getAvailableLayers();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ return handleRemoteExceptionFromCarService(e, null);
}
}
@@ -199,7 +201,7 @@
mVmsSubscriberService.addVmsSubscriber(mSubscriberManagerClient, layer);
VmsOperationRecorder.get().subscribe(layer);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -218,7 +220,7 @@
mSubscriberManagerClient, layer, publisherId);
VmsOperationRecorder.get().subscribe(layer, publisherId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -231,7 +233,7 @@
mVmsSubscriberService.addVmsSubscriberPassive(mSubscriberManagerClient);
VmsOperationRecorder.get().startMonitoring();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -248,7 +250,7 @@
mVmsSubscriberService.removeVmsSubscriber(mSubscriberManagerClient, layer);
VmsOperationRecorder.get().unsubscribe(layer);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -266,7 +268,7 @@
mSubscriberManagerClient, layer, publisherId);
VmsOperationRecorder.get().unsubscribe(layer, publisherId);
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -278,7 +280,7 @@
mVmsSubscriberService.removeVmsSubscriberPassive(mSubscriberManagerClient);
VmsOperationRecorder.get().stopMonitoring();
} catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -326,5 +328,9 @@
*/
@Override
public void onCarDisconnected() {
+ synchronized (mClientCallbackLock) {
+ mClientCallback = null;
+ mExecutor = null;
+ }
}
}
diff --git a/car-systemtest-lib/src/android/car/test/CarTestManager.java b/car-systemtest-lib/src/android/car/test/CarTestManager.java
index 52b01a6..3ee1067 100644
--- a/car-systemtest-lib/src/android/car/test/CarTestManager.java
+++ b/car-systemtest-lib/src/android/car/test/CarTestManager.java
@@ -27,18 +27,19 @@
* @hide
*/
@SystemApi
-public final class CarTestManager implements CarManagerBase {
+public final class CarTestManager extends CarManagerBase {
private final ICarTest mService;
- public CarTestManager(IBinder carServiceBinder) {
+ public CarTestManager(Car car, IBinder carServiceBinder) {
+ super(car);
mService = ICarTest.Stub.asInterface(carServiceBinder);
}
@Override
public void onCarDisconnected() {
- // should not happen for embedded
+ // test will fail. nothing to do.
}
/**
@@ -52,7 +53,7 @@
try {
mService.stopCarService(token);
} catch (RemoteException e) {
- handleRemoteException(e);
+ handleRemoteExceptionFromCarService(e);
}
}
@@ -66,12 +67,7 @@
try {
mService.startCarService(token);
} catch (RemoteException e) {
- handleRemoteException(e);
+ handleRemoteExceptionFromCarService(e);
}
}
-
- private static void handleRemoteException(RemoteException e) {
- // let test fail
- throw new RuntimeException(e);
- }
}
diff --git a/car-usb-handler/src/android/car/usb/handler/BootUsbService.java b/car-usb-handler/src/android/car/usb/handler/BootUsbService.java
index 909be07..5d29f54 100644
--- a/car-usb-handler/src/android/car/usb/handler/BootUsbService.java
+++ b/car-usb-handler/src/android/car/usb/handler/BootUsbService.java
@@ -46,14 +46,14 @@
static final String USB_DEVICE_LIST_KEY = "usb_device_list";
private ArrayList<UsbDevice> mDeviceList;
-
- private class UserSwitchBroadcastReceiver extends BroadcastReceiver {
+ private boolean mReceiverRegistered = false;
+ private final BroadcastReceiver mUserSwitchBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
processDevices();
- unregisterReceiver(this);
+ unregisterUserSwitchReceiver();
}
- }
+ };
@Override
public Binder onBind(Intent intent) {
@@ -86,8 +86,7 @@
// immediately.
if (ActivityManager.getCurrentUser() == UserHandle.USER_SYSTEM) {
Log.d(TAG, "Current user is still the system user, waiting for user switch");
- registerReceiver(
- new UserSwitchBroadcastReceiver(), new IntentFilter(ACTION_USER_SWITCHED));
+ registerUserSwitchReceiver();
} else {
processDevices();
}
@@ -95,6 +94,11 @@
return START_NOT_STICKY;
}
+ @Override
+ public void onDestroy() {
+ unregisterUserSwitchReceiver();
+ }
+
private void processDevices() {
Log.d(TAG, "Processing connected USB devices and starting handlers");
for (UsbDevice device : mDeviceList) {
@@ -110,4 +114,18 @@
manageDevice.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivityAsUser(manageDevice, UserHandle.CURRENT);
}
+
+ private void registerUserSwitchReceiver() {
+ if (!mReceiverRegistered) {
+ registerReceiver(mUserSwitchBroadcastReceiver, new IntentFilter(ACTION_USER_SWITCHED));
+ mReceiverRegistered = true;
+ }
+ }
+
+ private void unregisterUserSwitchReceiver() {
+ if (mReceiverRegistered) {
+ unregisterReceiver(mUserSwitchBroadcastReceiver);
+ mReceiverRegistered = false;
+ }
+ }
}
diff --git a/car_product/build/car_base.mk b/car_product/build/car_base.mk
index 5132890..72feacf 100644
--- a/car_product/build/car_base.mk
+++ b/car_product/build/car_base.mk
@@ -73,3 +73,6 @@
$(call inherit-product, $(SRC_TARGET_DIR)/product/core_minimal.mk)
+# Default dex optimization configurations
+PRODUCT_PROPERTY_OVERRIDES += \
+ pm.dexopt.disable_bg_dexopt=true
diff --git a/car_product/init/init.bootstat.rc b/car_product/init/init.bootstat.rc
index 5c5e796..4122ea4 100644
--- a/car_product/init/init.bootstat.rc
+++ b/car_product/init/init.bootstat.rc
@@ -4,4 +4,4 @@
# This is a common source of Android security bugs.
#
on property:boot.car_service_created=1
- exec - root root -- /system/bin/bootstat -r car_service_created
+ exec - system log -- /system/bin/bootstat -r car_service_created
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/config.xml b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
index 6b5ddac..1d5fd14 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
@@ -93,4 +93,8 @@
<string name="config_dataUsageSummaryComponent">com.android.car.settings/com.android.car.settings.datausage.DataWarningAndLimitActivity</string>
<bool name="config_automotiveHideNavBarForKeyboard">true</bool>
+
+ <!-- Turn off Wallpaper service -->
+ <bool name="config_enableWallpaperService">false</bool>
+
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values/strings.xml
index 9217647..34fd92f 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/strings.xml
@@ -18,4 +18,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- Default name of the owner user [CHAR LIMIT=20] -->
<string name="owner_name">Driver</string>
+ <string name="permlab_accessCoarseLocation">access approximate location only in the foreground</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/config.xml b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/config.xml
index d8d8516..7fd5d38 100644
--- a/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/config.xml
+++ b/car_product/overlay/frameworks/base/packages/CarSystemUI/res/values/config.xml
@@ -80,4 +80,41 @@
<!-- Keep the notification background when the container has been expanded. The children will
expand inline within the container, so it can keep its original background. -->
<bool name="config_showGroupNotificationBgWhenExpanded">true</bool>
+
+ <!--
+ Service components below were copied verbatim from frameworks/base/packages/SystemUI/res/values/config.xml,
+ then the services that are not needed by automotive were commented out (to improve boot and user switch time).
+ -->
+ <string-array name="config_systemUIServiceComponents" translatable="false">
+ <item>com.android.systemui.util.NotificationChannels</item>
+ <item>com.android.systemui.statusbar.CommandQueue$CommandQueueStart</item>
+ <item>com.android.systemui.keyguard.KeyguardViewMediator</item>
+<!--
+ <item>com.android.systemui.recents.Recents</item>
+-->
+<!--
+ <item>com.android.systemui.volume.VolumeUI</item>
+-->
+ <item>com.android.systemui.stackdivider.Divider</item>
+ <item>com.android.systemui.SystemBars</item>
+ <item>com.android.systemui.usb.StorageNotification</item>
+ <item>com.android.systemui.power.PowerUI</item>
+ <item>com.android.systemui.media.RingtonePlayer</item>
+ <item>com.android.systemui.keyboard.KeyboardUI</item>
+<!--
+ <item>com.android.systemui.pip.PipUI</item>
+-->
+ <item>com.android.systemui.shortcut.ShortcutKeyDispatcher</item>
+ <item>@string/config_systemUIVendorServiceComponent</item>
+ <item>com.android.systemui.util.leak.GarbageMonitor$Service</item>
+ <item>com.android.systemui.LatencyTester</item>
+ <item>com.android.systemui.globalactions.GlobalActionsComponent</item>
+ <item>com.android.systemui.ScreenDecorations</item>
+ <item>com.android.systemui.biometrics.BiometricDialogImpl</item>
+ <item>com.android.systemui.SliceBroadcastRelayHandler</item>
+ <item>com.android.systemui.SizeCompatModeActivityController</item>
+ <item>com.android.systemui.statusbar.notification.InstantAppNotifier</item>
+ <item>com.android.systemui.theme.ThemeOverlayController</item>
+ </string-array>
+
</resources>
diff --git a/car_product/sepolicy/private/bluetooth.te b/car_product/sepolicy/private/bluetooth.te
new file mode 100644
index 0000000..6ba74c2
--- /dev/null
+++ b/car_product/sepolicy/private/bluetooth.te
@@ -0,0 +1 @@
+allow bluetooth mediametrics_service:service_manager find;
diff --git a/car_product/sepolicy/private/carservice_app.te b/car_product/sepolicy/private/carservice_app.te
index bc1d74c..05c7b3f 100644
--- a/car_product/sepolicy/private/carservice_app.te
+++ b/car_product/sepolicy/private/carservice_app.te
@@ -12,7 +12,8 @@
# Allow Car Service to register/access itself with ServiceManager
add_service(carservice_app, carservice_service)
-allow carservice_app wifi_service:service_manager find;
+# Allow Car Service to register its stats service with ServiceManager
+add_service(carservice_app, carstats_service)
# Allow Car Service to access certain system services.
# Keep alphabetically sorted.
@@ -41,6 +42,8 @@
telecom_service
uimode_service
voiceinteraction_service
+ wifi_service
+ wifiscanner_service
}:service_manager find;
# Read and write /data/data subdirectory.
diff --git a/car_product/sepolicy/private/service_contexts b/car_product/sepolicy/private/service_contexts
index 7ac544c..38d994c 100644
--- a/car_product/sepolicy/private/service_contexts
+++ b/car_product/sepolicy/private/service_contexts
@@ -1,2 +1,3 @@
car_service u:object_r:carservice_service:s0
+car_stats u:object_r:carstats_service:s0
com.android.car.procfsinspector u:object_r:procfsinspector_service:s0
diff --git a/car_product/sepolicy/private/statsd.te b/car_product/sepolicy/private/statsd.te
new file mode 100644
index 0000000..1a17418
--- /dev/null
+++ b/car_product/sepolicy/private/statsd.te
@@ -0,0 +1,2 @@
+# Allow statsd to pull atoms from car_stats service
+allow statsd carstats_service:service_manager find;
diff --git a/car_product/sepolicy/public/property_contexts b/car_product/sepolicy/public/property_contexts
index 8dbe0bc..9646ac9 100644
--- a/car_product/sepolicy/public/property_contexts
+++ b/car_product/sepolicy/public/property_contexts
@@ -1 +1,3 @@
+android.car.number_pre_created_guests u:object_r:car_bootuser_prop:s0
+android.car.number_pre_created_users u:object_r:car_bootuser_prop:s0
android.car.systemuser.bootuseroverrideid u:object_r:car_bootuser_prop:s0
diff --git a/car_product/sepolicy/public/service.te b/car_product/sepolicy/public/service.te
index 87426f4..c6a2e30 100644
--- a/car_product/sepolicy/public/service.te
+++ b/car_product/sepolicy/public/service.te
@@ -1,2 +1,3 @@
type carservice_service, app_api_service, service_manager_type;
+type carstats_service, service_manager_type;
type procfsinspector_service, service_manager_type;
diff --git a/car_product/sepolicy/public/te_macros b/car_product/sepolicy/public/te_macros
new file mode 100644
index 0000000..963afdc
--- /dev/null
+++ b/car_product/sepolicy/public/te_macros
@@ -0,0 +1,7 @@
+# Define a macro to allow extra HAL dump
+define(`dump_extra_hal', `
+ hal_client_domain(dumpstate, $1);
+ allow $1_server dumpstate:fifo_file write;
+ allow $1_server dumpstate:fd use;
+ allow dumpstate $1:process signal;
+')
diff --git a/car_product/sepolicy/test/kitchensink_app.te b/car_product/sepolicy/test/kitchensink_app.te
index 0ab9c43..af00c45 100644
--- a/car_product/sepolicy/test/kitchensink_app.te
+++ b/car_product/sepolicy/test/kitchensink_app.te
@@ -10,6 +10,8 @@
accessibility_service
activity_service
activity_task_service
+ audio_service
+ audioserver_service
autofill_service
carservice_service
connectivity_service
@@ -20,6 +22,7 @@
input_method_service
input_service
location_service
+ mediaserver_service
network_management_service
power_service
sensorservice_service
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index ebea889..e2db410 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -475,6 +475,16 @@
android:label="@string/car_permission_label_enroll_trust"
android:description="@string/car_permission_desc_enroll_trust" />
+ <!-- Allows a test application to control car service's testing mode.
+ This is only for platform level testing.
+ <p>Protection level: signature|privileged
+ -->
+ <permission
+ android:name="android.car.permission.CAR_TEST_SERVICE"
+ android:protectionLevel="signature|privileged"
+ android:label="@string/car_permission_label_car_test_service"
+ android:description="@string/car_permission_desc_car_test_service" />
+
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.DEVICE_POWER" />
<uses-permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS" />
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 3eb1007..c756510 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -20,10 +20,6 @@
<!-- Resources to configure car service based on each OEM's preference. -->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <!-- Configuration to enable media center to autoplay when the media source is changed.
- If this is set to true, media will play automatically when a media source is selected and
- the selected media source supports media playback. -->
- <bool name="autoPlayOnMediaSourceChanged">false</bool>
<!-- Configuration to enable usage of dynamic audio routing. If this is set to false,
dynamic audio routing is disabled and audio works in legacy mode. It may be useful
@@ -236,4 +232,17 @@
resolve permission by itself to use any higher priority window type.
Setting this string to empty will disable the feature. -->
<string name="config_userNoticeUiService" translatable="false">com.google.android.car.kitchensink/.UserNoiticeDemoUiService</string>
+
+ <!-- Configuration to enable media center to autoplay when the media source is changed.
+ There are 3 supported configurations:
+ 0 - never play on change
+ 1 - always play
+ 2 - adaptive, play based on last remembered playback state -->
+ <integer name="config_mediaSourceChangedAutoplay">2</integer>
+ <!-- Configuration to enable media center to autoplay on boot -->
+ <integer name="config_mediaBootAutoplay">2</integer>
+
+ <!-- Disable switching the user while the system is resuming from Suspend to RAM.
+ This default says to prevent changing the user during Resume. -->
+ <bool name="config_disableUserSwitchDuringResume" translatable="false">true</bool>
</resources>
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index ef731b8..1589180 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -256,6 +256,11 @@
<string name="car_permission_label_enroll_trust">Enroll Trusted Device</string>
<string name="car_permission_desc_enroll_trust">Allow Trusted Device Enrollment</string>
+ <!-- Permission text: Control car's test mode [CHAR LIMIT=NONE] -->
+ <string name="car_permission_label_car_test_service">Control car\u2019s test mode</string>
+ <!-- Permission text: Control car's test mode [CHAR LIMIT=NONE] -->
+ <string name="car_permission_desc_car_test_service">Control car\u2019s test mode</string>
+
<!-- The default name of device enrolled as trust device [CHAR LIMIT=NONE] -->
<string name="trust_device_default_name">My Device</string>
diff --git a/service/src/com/android/car/BinderInterfaceContainer.java b/service/src/com/android/car/BinderInterfaceContainer.java
index a03b633..5d57b16 100644
--- a/service/src/com/android/car/BinderInterfaceContainer.java
+++ b/service/src/com/android/car/BinderInterfaceContainer.java
@@ -122,8 +122,10 @@
public synchronized void clear() {
Collection<BinderInterface<T>> interfaces = getInterfaces();
for (BinderInterface<T> bInterface : interfaces) {
- removeBinder(bInterface.binderInterface);
+ IBinder binder = bInterface.binderInterface.asBinder();
+ binder.unlinkToDeath(bInterface, 0);
}
+ mBinders.clear();
}
private void handleBinderDeath(BinderInterface<T> bInterface) {
diff --git a/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java b/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
index 4bb2790..c0f393a 100644
--- a/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
+++ b/service/src/com/android/car/BluetoothDeviceConnectionPolicy.java
@@ -117,7 +117,7 @@
}
}
}
- private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
+ private final BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
/**
* Create a new BluetoothDeviceConnectionPolicy object, responsible for encapsulating the
@@ -153,6 +153,7 @@
mUserId = userId;
mContext = Objects.requireNonNull(context);
mCarBluetoothService = bluetoothService;
+ mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
mBluetoothAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter());
}
@@ -160,9 +161,8 @@
* Setup the Bluetooth profile service connections and Vehicle Event listeners.
* and start the state machine -{@link BluetoothAutoConnectStateMachine}
*/
- public synchronized void init() {
+ public void init() {
logd("init()");
- mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
IntentFilter profileFilter = new IntentFilter();
profileFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
mContext.registerReceiverAsUser(mBluetoothBroadcastReceiver, UserHandle.CURRENT,
@@ -190,7 +190,7 @@
* Clean up slate. Close the Bluetooth profile service connections and quit the state machine -
* {@link BluetoothAutoConnectStateMachine}
*/
- public synchronized void release() {
+ public void release() {
logd("release()");
if (mCarPowerManager != null) {
mCarPowerManager.clearListener();
@@ -198,7 +198,6 @@
}
if (mBluetoothBroadcastReceiver != null) {
mContext.unregisterReceiver(mBluetoothBroadcastReceiver);
- mBluetoothBroadcastReceiver = null;
}
}
@@ -250,7 +249,7 @@
/**
* Print the verbose status of the object
*/
- public synchronized void dump(PrintWriter writer, String indent) {
+ public void dump(PrintWriter writer, String indent) {
writer.println(indent + TAG + ":");
writer.println(indent + "\tUserId: " + mUserId);
}
diff --git a/service/src/com/android/car/BluetoothProfileDeviceManager.java b/service/src/com/android/car/BluetoothProfileDeviceManager.java
index 4f6a82a..cfd45e5 100644
--- a/service/src/com/android/car/BluetoothProfileDeviceManager.java
+++ b/service/src/com/android/car/BluetoothProfileDeviceManager.java
@@ -46,6 +46,8 @@
import android.util.Log;
import android.util.SparseArray;
+import com.android.internal.annotations.GuardedBy;
+
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -114,23 +116,30 @@
}, new int[] {}));
}
+ // Fixed per-profile information for the profile this object manages
private final int mProfileId;
private final String mSettingsKey;
private final String mProfileConnectionAction;
private final ParcelUuid[] mProfileUuids;
private final int[] mProfileTriggers;
+
+ // Central priority list of devices
+ private final Object mPrioritizedDevicesLock = new Object();
+ @GuardedBy("mPrioritizedDevicesLock")
private ArrayList<BluetoothDevice> mPrioritizedDevices;
- private BluetoothAdapter mBluetoothAdapter;
- private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
-
- private ICarBluetoothUserService mBluetoothUserProxies;
-
+ // Auto connection process state
private final Object mAutoConnectLock = new Object();
+ @GuardedBy("mAutoConnectLock")
private boolean mConnecting = false;
+ @GuardedBy("mAutoConnectLock")
private int mAutoConnectPriority;
+ @GuardedBy("mAutoConnectLock")
private ArrayList<BluetoothDevice> mAutoConnectingDevices;
+ private final BluetoothAdapter mBluetoothAdapter;
+ private final BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
+ private final ICarBluetoothUserService mBluetoothUserProxies;
private final Handler mHandler = new Handler(Looper.getMainLooper());
/**
@@ -313,6 +322,7 @@
mProfileUuids = bpi.mUuids;
mProfileTriggers = bpi.mProfileTriggers;
+ mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
mBluetoothAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter());
}
@@ -327,7 +337,7 @@
mAutoConnectPriority = -1;
mAutoConnectingDevices = null;
}
- mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
+
IntentFilter profileFilter = new IntentFilter();
profileFilter.addAction(mProfileConnectionAction);
profileFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
@@ -347,7 +357,6 @@
if (mContext != null) {
mContext.unregisterReceiver(mBluetoothBroadcastReceiver);
}
- mBluetoothBroadcastReceiver = null;
}
cancelAutoConnecting();
commit();
@@ -392,7 +401,7 @@
}
}
- synchronized (this) {
+ synchronized (mPrioritizedDevicesLock) {
mPrioritizedDevices = devices;
}
@@ -408,7 +417,7 @@
private boolean commit() {
StringBuilder sb = new StringBuilder();
String delimiter = "";
- synchronized (this) {
+ synchronized (mPrioritizedDevicesLock) {
for (BluetoothDevice device : mPrioritizedDevices) {
sb.append(delimiter);
sb.append(device.getAddress());
@@ -434,7 +443,7 @@
addDevice(device); // No-op if device is already in the priority list
}
- synchronized (this) {
+ synchronized (mPrioritizedDevicesLock) {
ArrayList<BluetoothDevice> devices = getDeviceListSnapshot();
for (BluetoothDevice device : devices) {
if (!bondedDevices.contains(device)) {
@@ -451,7 +460,7 @@
*/
public ArrayList<BluetoothDevice> getDeviceListSnapshot() {
ArrayList<BluetoothDevice> devices = new ArrayList<>();
- synchronized (this) {
+ synchronized (mPrioritizedDevicesLock) {
devices = (ArrayList) mPrioritizedDevices.clone();
}
return devices;
@@ -462,12 +471,14 @@
*
* @param device - The device you wish to add
*/
- public synchronized void addDevice(BluetoothDevice device) {
+ public void addDevice(BluetoothDevice device) {
if (device == null) return;
- if (mPrioritizedDevices.contains(device)) return;
- logd("Add device " + device);
- mPrioritizedDevices.add(device);
- commit();
+ synchronized (mPrioritizedDevicesLock) {
+ if (mPrioritizedDevices.contains(device)) return;
+ logd("Add device " + device);
+ mPrioritizedDevices.add(device);
+ commit();
+ }
}
/**
@@ -475,12 +486,14 @@
*
* @param device - The device you wish to remove
*/
- public synchronized void removeDevice(BluetoothDevice device) {
+ public void removeDevice(BluetoothDevice device) {
if (device == null) return;
- if (!mPrioritizedDevices.contains(device)) return;
- logd("Remove device " + device);
- mPrioritizedDevices.remove(device);
- commit();
+ synchronized (mPrioritizedDevicesLock) {
+ if (!mPrioritizedDevices.contains(device)) return;
+ logd("Remove device " + device);
+ mPrioritizedDevices.remove(device);
+ commit();
+ }
}
/**
@@ -489,10 +502,12 @@
* @param device - The device you want the priority of
* @return The priority of the device, or -1 if the device is not in the list
*/
- public synchronized int getDeviceConnectionPriority(BluetoothDevice device) {
+ public int getDeviceConnectionPriority(BluetoothDevice device) {
if (device == null) return -1;
logd("Get connection priority of " + device);
- return mPrioritizedDevices.indexOf(device);
+ synchronized (mPrioritizedDevicesLock) {
+ return mPrioritizedDevices.indexOf(device);
+ }
}
/**
@@ -505,16 +520,18 @@
* @param device - The device you want to set the priority of
* @param priority - The priority you want to the device to have
*/
- public synchronized void setDeviceConnectionPriority(BluetoothDevice device, int priority) {
- if (device == null || priority < 0 || priority > mPrioritizedDevices.size()
- || getDeviceConnectionPriority(device) == priority) return;
- if (mPrioritizedDevices.contains(device)) {
- mPrioritizedDevices.remove(device);
- if (priority > mPrioritizedDevices.size()) priority = mPrioritizedDevices.size();
+ public void setDeviceConnectionPriority(BluetoothDevice device, int priority) {
+ synchronized (mPrioritizedDevicesLock) {
+ if (device == null || priority < 0 || priority > mPrioritizedDevices.size()
+ || getDeviceConnectionPriority(device) == priority) return;
+ if (mPrioritizedDevices.contains(device)) {
+ mPrioritizedDevices.remove(device);
+ if (priority > mPrioritizedDevices.size()) priority = mPrioritizedDevices.size();
+ }
+ logd("Set connection priority of " + device + " to " + priority);
+ mPrioritizedDevices.add(priority, device);
+ commit();
}
- logd("Set connection priority of " + device + " to " + priority);
- mPrioritizedDevices.add(priority, device);
- commit();
}
/**
diff --git a/service/src/com/android/car/BluetoothProfileInhibitManager.java b/service/src/com/android/car/BluetoothProfileInhibitManager.java
index 4dcc6b7..691592f 100644
--- a/service/src/com/android/car/BluetoothProfileInhibitManager.java
+++ b/service/src/com/android/car/BluetoothProfileInhibitManager.java
@@ -56,14 +56,16 @@
private final int mUserId;
private final ICarBluetoothUserService mBluetoothUserProxies;
- @GuardedBy("this")
+ private final Object mProfileInhibitsLock = new Object();
+
+ @GuardedBy("mProfileInhibitsLock")
private final SetMultimap<BluetoothConnection, InhibitRecord> mProfileInhibits =
new SetMultimap<>();
- @GuardedBy("this")
+ @GuardedBy("mProfileInhibitsLock")
private final HashSet<InhibitRecord> mRestoredInhibits = new HashSet<>();
- @GuardedBy("this")
+ @GuardedBy("mProfileInhibitsLock")
private final HashSet<BluetoothConnection> mAlreadyDisabledProfiles = new HashSet<>();
private final Handler mHandler = new Handler(Looper.getMainLooper());
@@ -190,7 +192,7 @@
}
public boolean removeSelf() {
- synchronized (BluetoothProfileInhibitManager.this) {
+ synchronized (mProfileInhibitsLock) {
if (mRemoved) {
return true;
}
@@ -328,9 +330,7 @@
BluetoothConnection params = new BluetoothConnection(profile, device);
InhibitRecord record;
- synchronized (this) {
- record = findInhibitRecord(params, token);
- }
+ record = findInhibitRecord(params, token);
if (record == null) {
Log.e(TAG, "Record not found");
@@ -343,64 +343,66 @@
/**
* Add a profile inhibit record, disabling the profile if necessary.
*/
- private synchronized boolean addInhibitRecord(InhibitRecord record) {
- BluetoothConnection params = record.getParams();
- if (!isProxyAvailable(params.getProfile())) {
- return false;
- }
-
- Set<InhibitRecord> previousRecords = mProfileInhibits.get(params);
- if (findInhibitRecord(params, record.getToken()) != null) {
- Log.e(TAG, "Inhibit request already registered - skipping duplicate");
- return false;
- }
-
- try {
- record.getToken().linkToDeath(record, 0);
- } catch (RemoteException e) {
- Log.e(TAG, "Could not link to death on inhibit token (already dead?)", e);
- return false;
- }
-
- boolean isNewlyAdded = previousRecords.isEmpty();
- mProfileInhibits.put(params, record);
-
- if (isNewlyAdded) {
- try {
- int priority =
- mBluetoothUserProxies.getProfilePriority(
- params.getProfile(),
- params.getDevice());
- if (priority == BluetoothProfile.PRIORITY_OFF) {
- // This profile was already disabled (and not as the result of an inhibit).
- // Add it to the already-disabled list, and do nothing else.
- mAlreadyDisabledProfiles.add(params);
-
- logd("Profile " + Utils.getProfileName(params.getProfile())
- + " already disabled for device " + params.getDevice()
- + " - suppressing re-enable");
- } else {
- mBluetoothUserProxies.setProfilePriority(
- params.getProfile(),
- params.getDevice(),
- BluetoothProfile.PRIORITY_OFF);
- mBluetoothUserProxies.bluetoothDisconnectFromProfile(
- params.getProfile(),
- params.getDevice());
- logd("Disabled profile "
- + Utils.getProfileName(params.getProfile())
- + " for device " + params.getDevice());
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Could not disable profile", e);
- record.getToken().unlinkToDeath(record, 0);
- mProfileInhibits.remove(params, record);
+ private boolean addInhibitRecord(InhibitRecord record) {
+ synchronized (mProfileInhibitsLock) {
+ BluetoothConnection params = record.getParams();
+ if (!isProxyAvailable(params.getProfile())) {
return false;
}
- }
- commit();
- return true;
+ Set<InhibitRecord> previousRecords = mProfileInhibits.get(params);
+ if (findInhibitRecord(params, record.getToken()) != null) {
+ Log.e(TAG, "Inhibit request already registered - skipping duplicate");
+ return false;
+ }
+
+ try {
+ record.getToken().linkToDeath(record, 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not link to death on inhibit token (already dead?)", e);
+ return false;
+ }
+
+ boolean isNewlyAdded = previousRecords.isEmpty();
+ mProfileInhibits.put(params, record);
+
+ if (isNewlyAdded) {
+ try {
+ int priority =
+ mBluetoothUserProxies.getProfilePriority(
+ params.getProfile(),
+ params.getDevice());
+ if (priority == BluetoothProfile.PRIORITY_OFF) {
+ // This profile was already disabled (and not as the result of an inhibit).
+ // Add it to the already-disabled list, and do nothing else.
+ mAlreadyDisabledProfiles.add(params);
+
+ logd("Profile " + Utils.getProfileName(params.getProfile())
+ + " already disabled for device " + params.getDevice()
+ + " - suppressing re-enable");
+ } else {
+ mBluetoothUserProxies.setProfilePriority(
+ params.getProfile(),
+ params.getDevice(),
+ BluetoothProfile.PRIORITY_OFF);
+ mBluetoothUserProxies.bluetoothDisconnectFromProfile(
+ params.getProfile(),
+ params.getDevice());
+ logd("Disabled profile "
+ + Utils.getProfileName(params.getProfile())
+ + " for device " + params.getDevice());
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not disable profile", e);
+ record.getToken().unlinkToDeath(record, 0);
+ mProfileInhibits.remove(params, record);
+ return false;
+ }
+ }
+
+ commit();
+ return true;
+ }
}
/**
@@ -411,41 +413,45 @@
* @return InhibitRecord for the connection parameters and token if exists, null otherwise.
*/
private InhibitRecord findInhibitRecord(BluetoothConnection params, IBinder token) {
- return mProfileInhibits.get(params)
- .stream()
- .filter(r -> r.getToken() == token)
- .findAny()
- .orElse(null);
+ synchronized (mProfileInhibitsLock) {
+ return mProfileInhibits.get(params)
+ .stream()
+ .filter(r -> r.getToken() == token)
+ .findAny()
+ .orElse(null);
+ }
}
/**
* Remove a given profile inhibit record, reconnecting if necessary.
*/
- private synchronized boolean removeInhibitRecord(InhibitRecord record) {
- BluetoothConnection params = record.getParams();
- if (!isProxyAvailable(params.getProfile())) {
- return false;
- }
- if (!mProfileInhibits.containsEntry(params, record)) {
- Log.e(TAG, "Record already removed");
- // Removing something a second time vacuously succeeds.
- return true;
- }
-
- // Re-enable profile before unlinking and removing the record, in case of error.
- // The profile should be re-enabled if this record is the only one left for that
- // device and profile combination.
- if (mProfileInhibits.get(params).size() == 1) {
- if (!restoreProfilePriority(params)) {
+ private boolean removeInhibitRecord(InhibitRecord record) {
+ synchronized (mProfileInhibitsLock) {
+ BluetoothConnection params = record.getParams();
+ if (!isProxyAvailable(params.getProfile())) {
return false;
}
+ if (!mProfileInhibits.containsEntry(params, record)) {
+ Log.e(TAG, "Record already removed");
+ // Removing something a second time vacuously succeeds.
+ return true;
+ }
+
+ // Re-enable profile before unlinking and removing the record, in case of error.
+ // The profile should be re-enabled if this record is the only one left for that
+ // device and profile combination.
+ if (mProfileInhibits.get(params).size() == 1) {
+ if (!restoreProfilePriority(params)) {
+ return false;
+ }
+ }
+
+ record.getToken().unlinkToDeath(record, 0);
+ mProfileInhibits.remove(params, record);
+
+ commit();
+ return true;
}
-
- record.getToken().unlinkToDeath(record, 0);
- mProfileInhibits.remove(params, record);
-
- commit();
- return true;
}
/**
@@ -504,46 +510,51 @@
* Keep trying to remove all profile inhibits that were restored from settings
* until all such inhibits have been removed.
*/
- private synchronized void removeRestoredProfileInhibits() {
- tryRemoveRestoredProfileInhibits();
+ private void removeRestoredProfileInhibits() {
+ synchronized (mProfileInhibitsLock) {
+ tryRemoveRestoredProfileInhibits();
- if (!mRestoredInhibits.isEmpty()) {
- logd("Could not remove all restored profile inhibits - "
- + "trying again in " + RESTORE_BACKOFF_MILLIS + "ms");
- mHandler.postDelayed(
- this::removeRestoredProfileInhibits,
- RESTORED_PROFILE_INHIBIT_TOKEN,
- RESTORE_BACKOFF_MILLIS);
+ if (!mRestoredInhibits.isEmpty()) {
+ logd("Could not remove all restored profile inhibits - "
+ + "trying again in " + RESTORE_BACKOFF_MILLIS + "ms");
+ mHandler.postDelayed(
+ this::removeRestoredProfileInhibits,
+ RESTORED_PROFILE_INHIBIT_TOKEN,
+ RESTORE_BACKOFF_MILLIS);
+ }
}
}
/**
* Release all active inhibit records prior to user switch or shutdown
*/
- private synchronized void releaseAllInhibitsBeforeUnbind() {
+ private void releaseAllInhibitsBeforeUnbind() {
logd("Unbinding CarBluetoothUserService - releasing all profile inhibits");
- for (BluetoothConnection params : mProfileInhibits.keySet()) {
- for (InhibitRecord record : mProfileInhibits.get(params)) {
- record.removeSelf();
+
+ synchronized (mProfileInhibitsLock) {
+ for (BluetoothConnection params : mProfileInhibits.keySet()) {
+ for (InhibitRecord record : mProfileInhibits.get(params)) {
+ record.removeSelf();
+ }
}
+
+ // Some inhibits might be hanging around because they couldn't be cleaned up.
+ // Make sure they get persisted...
+ commit();
+
+ // ...then clear them from the map.
+ mProfileInhibits.clear();
+
+ // We don't need to maintain previously-disabled profiles any more - they were already
+ // skipped in saveProfileInhibitsToSettings() above, and they don't need any
+ // further handling when the user resumes.
+ mAlreadyDisabledProfiles.clear();
+
+ // Clean up bookkeeping for restored inhibits. (If any are still around, they'll be
+ // restored again when this user restarts.)
+ mHandler.removeCallbacksAndMessages(RESTORED_PROFILE_INHIBIT_TOKEN);
+ mRestoredInhibits.clear();
}
-
- // Some inhibits might be hanging around because they couldn't be cleaned up.
- // Make sure they get persisted...
- commit();
-
- // ...then clear them from the map.
- mProfileInhibits.clear();
-
- // We don't need to maintain previously-disabled profiles any more - they were already
- // skipped in saveProfileInhibitsToSettings() above, and they don't need any
- // further handling when the user resumes.
- mAlreadyDisabledProfiles.clear();
-
- // Clean up bookkeeping for restored inhibits. (If any are still around, they'll be
- // restored again when this user restarts.)
- mHandler.removeCallbacksAndMessages(RESTORED_PROFILE_INHIBIT_TOKEN);
- mRestoredInhibits.clear();
}
/**
@@ -564,7 +575,7 @@
/**
* Print the verbose status of the object
*/
- public synchronized void dump(PrintWriter writer, String indent) {
+ public void dump(PrintWriter writer, String indent) {
writer.println(indent + TAG + ":");
// User metadata
@@ -572,7 +583,7 @@
// Current inhibits
String inhibits;
- synchronized (this) {
+ synchronized (mProfileInhibitsLock) {
inhibits = mProfileInhibits.keySet().toString();
}
writer.println(indent + "\tInhibited profiles: " + inhibits);
diff --git a/service/src/com/android/car/CarBluetoothService.java b/service/src/com/android/car/CarBluetoothService.java
index 0c992e1..54d15a1 100644
--- a/service/src/com/android/car/CarBluetoothService.java
+++ b/service/src/com/android/car/CarBluetoothService.java
@@ -28,6 +28,8 @@
import android.util.Log;
import android.util.SparseArray;
+import com.android.internal.annotations.GuardedBy;
+
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -63,20 +65,31 @@
BluetoothProfile.PAN
);
+ // Each time PerUserCarService connects we need to get new Bluetooth profile proxies and refresh
+ // all our internal objects to use them. When it disconnects we're to assume our proxies are
+ // invalid. This lock protects all our internal objects.
+ private final Object mPerUserLock = new Object();
+
// Set of Bluetooth Profile Device Managers, own the priority connection lists, updated on user
// switch
- private SparseArray<BluetoothProfileDeviceManager> mProfileDeviceManagers = new SparseArray<>();
+ private final SparseArray<BluetoothProfileDeviceManager> mProfileDeviceManagers =
+ new SparseArray<>();
// Profile-Inhibit Manager that will temporarily inhibit connections on profiles, per user
+ @GuardedBy("mPerUserLock")
private BluetoothProfileInhibitManager mInhibitManager = null;
// Default Bluetooth device connection policy, per user, enabled with an overlay
private final boolean mUseDefaultPolicy;
+ @GuardedBy("mPerUserLock")
private BluetoothDeviceConnectionPolicy mBluetoothDeviceConnectionPolicy = null;
// Listen for user switch events from the PerUserCarService
+ @GuardedBy("mPerUserLock")
private int mUserId;
+ @GuardedBy("mPerUserLock")
private ICarUserService mCarUserService;
+ @GuardedBy("mPerUserLock")
private ICarBluetoothUserService mCarBluetoothUserService;
private final PerUserCarServiceHelper mUserServiceHelper;
private final PerUserCarServiceHelper.ServiceCallback mUserServiceCallback =
@@ -84,8 +97,14 @@
@Override
public void onServiceConnected(ICarUserService carUserService) {
logd("Connected to PerUserCarService");
- synchronized (this) {
+ synchronized (mPerUserLock) {
+ // Explicitly clear out existing per-user objects since we can't rely on the
+ // onServiceDisconnected and onPreUnbind calls to always be called before this
+ destroyUser();
+
mCarUserService = carUserService;
+
+ // Create new objects with our new set of profile proxies
initializeUser();
}
}
@@ -99,9 +118,7 @@
@Override
public void onServiceDisconnected() {
logd("Disconnected from PerUserCarService");
- synchronized (this) {
- mCarUserService = null;
- }
+ destroyUser();
}
};
@@ -126,7 +143,7 @@
* Wait for the user service helper to report a user before initializing a user.
*/
@Override
- public synchronized void init() {
+ public void init() {
logd("init()");
mUserServiceHelper.registerServiceCallback(mUserServiceCallback);
}
@@ -137,11 +154,10 @@
* Clean up the user context once we've detached from the user service helper, if any.
*/
@Override
- public synchronized void release() {
+ public void release() {
logd("release()");
mUserServiceHelper.unregisterServiceCallback(mUserServiceCallback);
destroyUser();
- mCarUserService = null;
}
/**
@@ -158,32 +174,37 @@
*
* Only call this following a known user switch once we've connected to the user service helper.
*/
- private synchronized void initializeUser() {
+ private void initializeUser() {
logd("Initializing new user");
- mUserId = ActivityManager.getCurrentUser();
- createBluetoothUserService();
- createBluetoothProfileDeviceManagers();
- createBluetoothProfileInhibitManager();
+ synchronized (mPerUserLock) {
+ mUserId = ActivityManager.getCurrentUser();
+ createBluetoothUserService();
+ createBluetoothProfileDeviceManagers();
+ createBluetoothProfileInhibitManager();
- // Determine if we need to begin the default policy
- mBluetoothDeviceConnectionPolicy = null;
- if (mUseDefaultPolicy) {
- createBluetoothDeviceConnectionPolicy();
+ // Determine if we need to begin the default policy
+ mBluetoothDeviceConnectionPolicy = null;
+ if (mUseDefaultPolicy) {
+ createBluetoothDeviceConnectionPolicy();
+ }
+ logd("Switched to user " + mUserId);
}
- logd("Switched to user " + mUserId);
}
/**
* Destroy the current user context, defined by the set of profile proxies, profile device
* managers, inhibit manager and the policy.
*/
- private synchronized void destroyUser() {
+ private void destroyUser() {
logd("Destroying user " + mUserId);
- destroyBluetoothDeviceConnectionPolicy();
- destroyBluetoothProfileInhibitManager();
- destroyBluetoothProfileDeviceManagers();
- destroyBluetoothUserService();
- mUserId = -1;
+ synchronized (mPerUserLock) {
+ destroyBluetoothDeviceConnectionPolicy();
+ destroyBluetoothProfileInhibitManager();
+ destroyBluetoothProfileDeviceManagers();
+ destroyBluetoothUserService();
+ mCarUserService = null;
+ mUserId = -1;
+ }
}
/**
@@ -192,17 +213,17 @@
* Also sets up the connection proxy objects required to communicate with the Bluetooth
* Profile Services.
*/
- private synchronized void createBluetoothUserService() {
- if (mCarUserService != null) {
- try {
- mCarBluetoothUserService = mCarUserService.getBluetoothUserService();
- mCarBluetoothUserService.setupBluetoothConnectionProxies();
- } catch (RemoteException e) {
- Log.e(TAG, "Remote Service Exception on ServiceConnection Callback: "
- + e.getMessage());
+ private void createBluetoothUserService() {
+ synchronized (mPerUserLock) {
+ if (mCarUserService != null) {
+ try {
+ mCarBluetoothUserService = mCarUserService.getBluetoothUserService();
+ mCarBluetoothUserService.setupBluetoothConnectionProxies();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Remote Service Exception on ServiceConnection Callback: "
+ + e.getMessage());
+ }
}
- } else {
- logd("PerUserCarService not connected. Cannot get bluetooth user proxy objects");
}
}
@@ -210,109 +231,130 @@
* Close out the Per User Car Bluetooth profile proxy connections and destroys the Car Bluetooth
* User Service object.
*/
- private synchronized void destroyBluetoothUserService() {
- if (mCarBluetoothUserService == null) return;
- try {
- mCarBluetoothUserService.closeBluetoothConnectionProxies();
- } catch (RemoteException e) {
- Log.e(TAG, "Remote Service Exception on ServiceConnection Callback: "
- + e.getMessage());
+ private void destroyBluetoothUserService() {
+ synchronized (mPerUserLock) {
+ if (mCarBluetoothUserService == null) return;
+ try {
+ mCarBluetoothUserService.closeBluetoothConnectionProxies();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Remote Service Exception on ServiceConnection Callback: "
+ + e.getMessage());
+ }
+ mCarBluetoothUserService = null;
}
- mCarBluetoothUserService = null;
}
/**
* Clears out Profile Device Managers and re-creates them for the current user.
*/
- private synchronized void createBluetoothProfileDeviceManagers() {
- mProfileDeviceManagers.clear();
- if (mUserId == -1) {
- logd("No foreground user, cannot create profile device managers");
- return;
- }
- for (int profileId : sManagedProfiles) {
- BluetoothProfileDeviceManager deviceManager = BluetoothProfileDeviceManager.create(
- mContext, mUserId, mCarBluetoothUserService, profileId);
- if (deviceManager == null) {
- logd("Failed to create profile device manager for "
- + Utils.getProfileName(profileId));
- continue;
+ private void createBluetoothProfileDeviceManagers() {
+ synchronized (mPerUserLock) {
+ if (mUserId == -1) {
+ logd("No foreground user, cannot create profile device managers");
+ return;
}
- mProfileDeviceManagers.put(profileId, deviceManager);
- logd("Created profile device manager for " + Utils.getProfileName(profileId));
- }
+ for (int profileId : sManagedProfiles) {
+ BluetoothProfileDeviceManager deviceManager = mProfileDeviceManagers.get(profileId);
+ if (deviceManager != null) {
+ deviceManager.stop();
+ mProfileDeviceManagers.remove(profileId);
+ logd("Existing device manager removed for profile "
+ + Utils.getProfileName(profileId));
+ }
- for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
- int key = mProfileDeviceManagers.keyAt(i);
- BluetoothProfileDeviceManager deviceManager =
- (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(key);
- deviceManager.start();
+ deviceManager = BluetoothProfileDeviceManager.create(mContext, mUserId,
+ mCarBluetoothUserService, profileId);
+ if (deviceManager == null) {
+ logd("Failed to create profile device manager for "
+ + Utils.getProfileName(profileId));
+ continue;
+ }
+ mProfileDeviceManagers.put(profileId, deviceManager);
+ logd("Created profile device manager for " + Utils.getProfileName(profileId));
+ }
+
+ for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
+ int key = mProfileDeviceManagers.keyAt(i);
+ BluetoothProfileDeviceManager deviceManager =
+ (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(key);
+ deviceManager.start();
+ }
}
}
/**
* Stops and clears the entire set of Profile Device Managers.
*/
- private synchronized void destroyBluetoothProfileDeviceManagers() {
- for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
- int key = mProfileDeviceManagers.keyAt(i);
- BluetoothProfileDeviceManager deviceManager =
- (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(key);
- deviceManager.stop();
+ private void destroyBluetoothProfileDeviceManagers() {
+ synchronized (mPerUserLock) {
+ for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
+ int key = mProfileDeviceManagers.keyAt(i);
+ BluetoothProfileDeviceManager deviceManager =
+ (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(key);
+ deviceManager.stop();
+ }
+ mProfileDeviceManagers.clear();
}
- mProfileDeviceManagers.clear();
}
/**
* Creates an instance of a BluetoothProfileInhibitManager under the current user
*/
- private synchronized void createBluetoothProfileInhibitManager() {
+ private void createBluetoothProfileInhibitManager() {
logd("Creating inhibit manager");
- if (mUserId == -1) {
- logd("No foreground user, cannot create profile inhibit manager");
- return;
+ synchronized (mPerUserLock) {
+ if (mUserId == -1) {
+ logd("No foreground user, cannot create profile inhibit manager");
+ return;
+ }
+ mInhibitManager = new BluetoothProfileInhibitManager(mContext, mUserId,
+ mCarBluetoothUserService);
+ mInhibitManager.start();
}
- mInhibitManager = new BluetoothProfileInhibitManager(mContext, mUserId,
- mCarBluetoothUserService);
- mInhibitManager.start();
}
/**
* Destroys the current instance of a BluetoothProfileInhibitManager, if one exists
*/
- private synchronized void destroyBluetoothProfileInhibitManager() {
+ private void destroyBluetoothProfileInhibitManager() {
logd("Destroying inhibit manager");
- if (mInhibitManager == null) return;
- mInhibitManager.stop();
- mInhibitManager = null;
+ synchronized (mPerUserLock) {
+ if (mInhibitManager == null) return;
+ mInhibitManager.stop();
+ mInhibitManager = null;
+ }
}
/**
* Creates an instance of a BluetoothDeviceConnectionPolicy under the current user
*/
- private synchronized void createBluetoothDeviceConnectionPolicy() {
+ private void createBluetoothDeviceConnectionPolicy() {
logd("Creating device connection policy");
- if (mUserId == -1) {
- logd("No foreground user, cannot create device connection policy");
- return;
+ synchronized (mPerUserLock) {
+ if (mUserId == -1) {
+ logd("No foreground user, cannot create device connection policy");
+ return;
+ }
+ mBluetoothDeviceConnectionPolicy = BluetoothDeviceConnectionPolicy.create(mContext,
+ mUserId, this);
+ if (mBluetoothDeviceConnectionPolicy == null) {
+ logd("Failed to create default Bluetooth device connection policy.");
+ return;
+ }
+ mBluetoothDeviceConnectionPolicy.init();
}
- mBluetoothDeviceConnectionPolicy = BluetoothDeviceConnectionPolicy.create(mContext, mUserId,
- this);
- if (mBluetoothDeviceConnectionPolicy == null) {
- logd("Failed to create default Bluetooth device connection policy.");
- return;
- }
- mBluetoothDeviceConnectionPolicy.init();
}
/**
* Destroys the current instance of a BluetoothDeviceConnectionPolicy, if one exists
*/
- private synchronized void destroyBluetoothDeviceConnectionPolicy() {
+ private void destroyBluetoothDeviceConnectionPolicy() {
logd("Destroying device connection policy");
- if (mBluetoothDeviceConnectionPolicy != null) {
- mBluetoothDeviceConnectionPolicy.release();
- mBluetoothDeviceConnectionPolicy = null;
+ synchronized (mPerUserLock) {
+ if (mBluetoothDeviceConnectionPolicy != null) {
+ mBluetoothDeviceConnectionPolicy.release();
+ mBluetoothDeviceConnectionPolicy = null;
+ }
}
}
@@ -322,7 +364,9 @@
* @return true if the default policy is active, false otherwise
*/
public boolean isUsingDefaultConnectionPolicy() {
- return mBluetoothDeviceConnectionPolicy != null;
+ synchronized (mPerUserLock) {
+ return mBluetoothDeviceConnectionPolicy != null;
+ }
}
/**
@@ -332,7 +376,7 @@
public void connectDevices() {
enforceBluetoothAdminPermission();
logd("Connect devices for each profile");
- synchronized (this) {
+ synchronized (mPerUserLock) {
for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
int key = mProfileDeviceManagers.keyAt(i);
BluetoothProfileDeviceManager deviceManager =
@@ -350,7 +394,7 @@
*/
public List<BluetoothDevice> getProfileDevicePriorityList(int profile) {
enforceBluetoothAdminPermission();
- synchronized (this) {
+ synchronized (mPerUserLock) {
BluetoothProfileDeviceManager deviceManager =
(BluetoothProfileDeviceManager) mProfileDeviceManagers.get(profile);
if (deviceManager != null) {
@@ -369,7 +413,7 @@
*/
public int getDeviceConnectionPriority(int profile, BluetoothDevice device) {
enforceBluetoothAdminPermission();
- synchronized (this) {
+ synchronized (mPerUserLock) {
BluetoothProfileDeviceManager deviceManager =
(BluetoothProfileDeviceManager) mProfileDeviceManagers.get(profile);
if (deviceManager != null) {
@@ -388,7 +432,7 @@
*/
public void setDeviceConnectionPriority(int profile, BluetoothDevice device, int priority) {
enforceBluetoothAdminPermission();
- synchronized (this) {
+ synchronized (mPerUserLock) {
BluetoothProfileDeviceManager deviceManager =
(BluetoothProfileDeviceManager) mProfileDeviceManagers.get(profile);
if (deviceManager != null) {
@@ -408,11 +452,13 @@
* owning the token dies, the request will automatically be released
* @return True if the profile was successfully inhibited, false if an error occurred.
*/
- synchronized boolean requestProfileInhibit(BluetoothDevice device, int profile, IBinder token) {
+ boolean requestProfileInhibit(BluetoothDevice device, int profile, IBinder token) {
logd("Request profile inhibit: profile " + Utils.getProfileName(profile)
+ ", device " + device.getAddress());
- if (mInhibitManager == null) return false;
- return mInhibitManager.requestProfileInhibit(device, profile, token);
+ synchronized (mPerUserLock) {
+ if (mInhibitManager == null) return false;
+ return mInhibitManager.requestProfileInhibit(device, profile, token);
+ }
}
/**
@@ -425,11 +471,13 @@
* {@link #requestBluetoothProfileInhibit}.
* @return True if the request was released, false if an error occurred.
*/
- synchronized boolean releaseProfileInhibit(BluetoothDevice device, int profile, IBinder token) {
+ boolean releaseProfileInhibit(BluetoothDevice device, int profile, IBinder token) {
logd("Release profile inhibit: profile " + Utils.getProfileName(profile)
+ ", device " + device.getAddress());
- if (mInhibitManager == null) return false;
- return mInhibitManager.releaseProfileInhibit(device, profile, token);
+ synchronized (mPerUserLock) {
+ if (mInhibitManager == null) return false;
+ return mInhibitManager.releaseProfileInhibit(device, profile, token);
+ }
}
/**
@@ -452,29 +500,31 @@
* Print out the verbose debug status of this object
*/
@Override
- public synchronized void dump(PrintWriter writer) {
+ public void dump(PrintWriter writer) {
writer.println("*" + TAG + "*");
- writer.println("\tUser ID: " + mUserId);
- writer.println("\tUser Proxies: " + (mCarBluetoothUserService != null ? "Yes" : "No"));
+ synchronized (mPerUserLock) {
+ writer.println("\tUser ID: " + mUserId);
+ writer.println("\tUser Proxies: " + (mCarBluetoothUserService != null ? "Yes" : "No"));
- // Profile Device Manager statuses
- for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
- int key = mProfileDeviceManagers.keyAt(i);
- BluetoothProfileDeviceManager deviceManager =
- (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(key);
- deviceManager.dump(writer, "\t");
- }
+ // Profile Device Manager statuses
+ for (int i = 0; i < mProfileDeviceManagers.size(); i++) {
+ int key = mProfileDeviceManagers.keyAt(i);
+ BluetoothProfileDeviceManager deviceManager =
+ (BluetoothProfileDeviceManager) mProfileDeviceManagers.get(key);
+ deviceManager.dump(writer, "\t");
+ }
- // Profile Inhibits
- if (mInhibitManager != null) mInhibitManager.dump(writer, "\t");
- else writer.println("\tBluetoothProfileInhibitManager: null");
+ // Profile Inhibits
+ if (mInhibitManager != null) mInhibitManager.dump(writer, "\t");
+ else writer.println("\tBluetoothProfileInhibitManager: null");
- // Device Connection Policy
- writer.println("\tUsing default policy? " + (mUseDefaultPolicy ? "Yes" : "No"));
- if (mBluetoothDeviceConnectionPolicy == null) {
- writer.println("\tBluetoothDeviceConnectionPolicy: null");
- } else {
- mBluetoothDeviceConnectionPolicy.dump(writer, "\t");
+ // Device Connection Policy
+ writer.println("\tUsing default policy? " + (mUseDefaultPolicy ? "Yes" : "No"));
+ if (mBluetoothDeviceConnectionPolicy == null) {
+ writer.println("\tBluetoothDeviceConnectionPolicy: null");
+ } else {
+ mBluetoothDeviceConnectionPolicy.dump(writer, "\t");
+ }
}
}
diff --git a/service/src/com/android/car/CarDrivingStateService.java b/service/src/com/android/car/CarDrivingStateService.java
index 584228d..66a2a7c 100644
--- a/service/src/com/android/car/CarDrivingStateService.java
+++ b/service/src/com/android/car/CarDrivingStateService.java
@@ -30,11 +30,15 @@
import android.content.Context;
import android.hardware.automotive.vehicle.V2_0.VehicleGear;
import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.io.PrintWriter;
import java.util.LinkedList;
import java.util.List;
@@ -60,6 +64,8 @@
VehicleProperty.PERF_VEHICLE_SPEED,
VehicleProperty.GEAR_SELECTION,
VehicleProperty.PARKING_BRAKE_ON};
+ private final HandlerThread mClientDispatchThread;
+ private final Handler mClientDispatchHandler;
private CarDrivingStateEvent mCurrentDrivingState;
// For dumpsys logging
private final LinkedList<Utils.TransitionLog> mTransitionLogs = new LinkedList<>();
@@ -75,6 +81,9 @@
mContext = context;
mPropertyService = propertyService;
mCurrentDrivingState = createDrivingStateEvent(CarDrivingStateEvent.DRIVING_STATE_UNKNOWN);
+ mClientDispatchThread = new HandlerThread("ClientDispatchThread");
+ mClientDispatchThread.start();
+ mClientDispatchHandler = new Handler(mClientDispatchThread.getLooper());
}
@Override
@@ -316,7 +325,8 @@
* Handle events coming from {@link CarPropertyService}. Compute the driving state, map it to
* the corresponding UX Restrictions and dispatch the events to the registered clients.
*/
- private synchronized void handlePropertyEvent(CarPropertyEvent event) {
+ @VisibleForTesting
+ synchronized void handlePropertyEvent(CarPropertyEvent event) {
if (event.getEventType() != CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE) {
return;
}
@@ -387,9 +397,13 @@
if (DBG) {
Log.d(TAG, "dispatching to " + mDrivingStateClients.size() + " clients");
}
- for (DrivingStateClient client : mDrivingStateClients) {
- client.dispatchEventToClients(mCurrentDrivingState);
- }
+ // Dispatch to clients on a separate thread to prevent a deadlock
+ final CarDrivingStateEvent currentDrivingStateEvent = mCurrentDrivingState;
+ mClientDispatchHandler.post(() -> {
+ for (DrivingStateClient client : mDrivingStateClients) {
+ client.dispatchEventToClients(currentDrivingStateEvent);
+ }
+ });
}
}
diff --git a/service/src/com/android/car/CarLocalServices.java b/service/src/com/android/car/CarLocalServices.java
index 6c756b8..ea1b6f1 100644
--- a/service/src/com/android/car/CarLocalServices.java
+++ b/service/src/com/android/car/CarLocalServices.java
@@ -17,6 +17,7 @@
package com.android.car;
import android.annotation.Nullable;
+import android.car.Car;
import android.car.hardware.power.CarPowerManager;
import android.content.Context;
import android.util.ArrayMap;
@@ -90,10 +91,12 @@
*/
@Nullable
public static CarPowerManager createCarPowerManager(Context context) {
+ // This does not require connection as binder will be passed to CarPowerManager directly.
+ Car car = new Car(context, /* service= */null, /* handler= */ null);
CarPowerManagementService service = getService(CarPowerManagementService.class);
if (service == null) {
return null;
}
- return new CarPowerManager(service, context, null);
+ return new CarPowerManager(car, service);
}
}
diff --git a/service/src/com/android/car/CarLocationService.java b/service/src/com/android/car/CarLocationService.java
index 2f37e61..d4daac2 100644
--- a/service/src/com/android/car/CarLocationService.java
+++ b/service/src/com/android/car/CarLocationService.java
@@ -297,7 +297,7 @@
if (location == null) {
logd("Not storing null location");
} else {
- logd("Storing location: " + location);
+ logd("Storing location");
AtomicFile atomicFile = new AtomicFile(getLocationCacheFile());
FileOutputStream fos = null;
try {
@@ -437,7 +437,7 @@
}
}
}
- logd("Injected location " + location + " with result " + success + " on attempt "
+ logd("Injected location with result " + success + " on attempt "
+ attemptCount);
if (success) {
return;
diff --git a/service/src/com/android/car/CarMediaService.java b/service/src/com/android/car/CarMediaService.java
index 4841b0d..f3c9b79 100644
--- a/service/src/com/android/car/CarMediaService.java
+++ b/service/src/com/android/car/CarMediaService.java
@@ -77,6 +77,13 @@
private static final String SHARED_PREF = "com.android.car.media.car_media_service";
private static final String COMPONENT_NAME_SEPARATOR = ",";
private static final String MEDIA_CONNECTION_ACTION = "com.android.car.media.MEDIA_CONNECTION";
+ private static final String EXTRA_AUTOPLAY = "com.android.car.media.autoplay";
+
+ // XML configuration options for autoplay on media source change.
+ private static final int AUTOPLAY_CONFIG_NEVER = 0;
+ private static final int AUTOPLAY_CONFIG_ALWAYS = 1;
+ // This mode uses the last stored playback state to determine whether to resume playback
+ private static final int AUTOPLAY_CONFIG_ADAPTIVE = 2;
private final Context mContext;
private final UserManager mUserManager;
@@ -89,8 +96,8 @@
// null if playback has not been started yet.
private MediaController mActiveUserMediaController;
private SessionChangedListener mSessionsListener;
- private boolean mStartPlayback;
- private boolean mPlayOnMediaSourceChanged;
+ private int mPlayOnMediaSourceChangedConfig;
+ private int mPlayOnBootConfig;
private boolean mPendingInit;
private int mCurrentUser;
@@ -148,11 +155,7 @@
if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
Log.d(CarLog.TAG_MEDIA, "Switched to user " + mCurrentUser);
}
- if (mUserManager.isUserUnlocked(mCurrentUser)) {
- initUser();
- } else {
- mPendingInit = true;
- }
+ maybeInitUser();
}
};
@@ -175,23 +178,39 @@
userSwitchFilter.addAction(Intent.ACTION_USER_SWITCHED);
mContext.registerReceiver(mUserSwitchReceiver, userSwitchFilter);
- mPlayOnMediaSourceChanged =
- mContext.getResources().getBoolean(R.bool.autoPlayOnMediaSourceChanged);
+ mPlayOnMediaSourceChangedConfig =
+ mContext.getResources().getInteger(R.integer.config_mediaSourceChangedAutoplay);
+ mPlayOnBootConfig = mContext.getResources().getInteger(R.integer.config_mediaBootAutoplay);
mCurrentUser = ActivityManager.getCurrentUser();
- updateMediaSessionCallbackForCurrentUser();
}
@Override
+ // This method is called from ICarImpl after CarMediaService is created.
public void init() {
- // Nothing to do. Reason: this method is only called once after rebooting, but we need to
- // init user state each time a new user is unlocked, so this method is not the right
- // place to call initUser().
+ maybeInitUser();
+ }
+
+ private void maybeInitUser() {
+ if (mCurrentUser == 0) {
+ return;
+ }
+ if (mUserManager.isUserUnlocked(mCurrentUser)) {
+ initUser();
+ } else {
+ mPendingInit = true;
+ }
}
private void initUser() {
+ // SharedPreferences are shared among different users thus only need initialized once. And
+ // they should be initialized after user 0 is unlocked because SharedPreferences in
+ // credential encrypted storage are not available until after user 0 is unlocked.
+ // initUser() is called when the current foreground user is unlocked, and by that time user
+ // 0 has been unlocked already, so initializing SharedPreferences in initUser() is fine.
if (mSharedPrefs == null) {
mSharedPrefs = mContext.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE);
}
+
if (mIsPackageUpdateReceiverRegistered) {
mContext.unregisterReceiver(mPackageUpdateReceiver);
}
@@ -200,22 +219,51 @@
mPackageUpdateFilter, null, null);
mIsPackageUpdateReceiverRegistered = true;
- mPrimaryMediaComponent = getLastMediaSource();
+ mPrimaryMediaComponent =
+ isCurrentUserEphemeral() ? getDefaultMediaSource() : getLastMediaSource();
mActiveUserMediaController = null;
- String key = PLAYBACK_STATE_KEY + mCurrentUser;
- mStartPlayback =
- mSharedPrefs.getInt(key, PlaybackState.STATE_NONE) == PlaybackState.STATE_PLAYING;
+
updateMediaSessionCallbackForCurrentUser();
notifyListeners();
- // Start a service on the current user that binds to the media browser of the current media
- // source. We start a new service because this one runs on user 0, and MediaBrowser doesn't
- // provide an API to connect on a specific user.
+ startMediaConnectorService(shouldStartPlayback(mPlayOnBootConfig), currentUser);
+ }
+
+ /**
+ * Starts a service on the current user that binds to the media browser of the current media
+ * source. We start a new service because this one runs on user 0, and MediaBrowser doesn't
+ * provide an API to connect on a specific user. Additionally, this service will attempt to
+ * resume playback using the MediaSession obtained via the media browser connection, which
+ * is more reliable than using active MediaSessions from MediaSessionManager.
+ */
+ private void startMediaConnectorService(boolean startPlayback, UserHandle currentUser) {
Intent serviceStart = new Intent(MEDIA_CONNECTION_ACTION);
serviceStart.setPackage(mContext.getResources().getString(R.string.serviceMediaConnection));
+ serviceStart.putExtra(EXTRA_AUTOPLAY, startPlayback);
mContext.startForegroundServiceAsUser(serviceStart, currentUser);
}
+ private boolean sharedPrefsInitialized() {
+ if (mSharedPrefs == null) {
+ // It shouldn't reach this but let's be cautious.
+ Log.e(CarLog.TAG_MEDIA, "SharedPreferences are not initialized!");
+ String className = getClass().getName();
+ for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
+ // Let's print the useful logs only.
+ String log = ste.toString();
+ if (log.contains(className)) {
+ Log.e(CarLog.TAG_MEDIA, log);
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isCurrentUserEphemeral() {
+ return mUserManager.getUserInfo(mCurrentUser).isEphemeral();
+ }
+
@Override
public void release() {
mMediaSessionUpdater.unregisterCallbacks();
@@ -297,10 +345,9 @@
if (!unlocked) {
return;
}
- // No need to handle user0, non current foreground user, or ephemeral user.
+ // No need to handle user0, non current foreground user.
if (userHandle == UserHandle.USER_SYSTEM
- || userHandle != ActivityManager.getCurrentUser()
- || mUserManager.getUserInfo(userHandle).isEphemeral()) {
+ || userHandle != ActivityManager.getCurrentUser()) {
return;
}
if (mPendingInit) {
@@ -342,9 +389,12 @@
/**
* Attempts to stop the current source using MediaController.TransportControls.stop()
+ * This method also unregisters callbacks to the active media controller before calling stop(),
+ * to preserve the PlaybackState before stopping.
*/
- private void stop() {
+ private void stopAndUnregisterCallback() {
if (mActiveUserMediaController != null) {
+ mActiveUserMediaController.unregisterCallback(mMediaControllerCallback);
if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
Log.d(CarLog.TAG_MEDIA, "stopping " + mActiveUserMediaController.getPackageName());
}
@@ -472,25 +522,26 @@
return;
}
- stop();
+ stopAndUnregisterCallback();
- mStartPlayback = mPlayOnMediaSourceChanged;
+ mActiveUserMediaController = null;
mPreviousMediaComponent = mPrimaryMediaComponent;
mPrimaryMediaComponent = componentName;
updateActiveMediaController(mMediaSessionManager
.getActiveSessionsForUser(null, ActivityManager.getCurrentUser()));
- if (mSharedPrefs != null) {
- if (mPrimaryMediaComponent != null && !TextUtils.isEmpty(
- mPrimaryMediaComponent.flattenToString())) {
+ if (mPrimaryMediaComponent != null && !TextUtils.isEmpty(
+ mPrimaryMediaComponent.flattenToString())) {
+ if (!isCurrentUserEphemeral()) {
saveLastMediaSource(mPrimaryMediaComponent);
- mRemovedMediaSourcePackage = null;
}
- } else {
- // Shouldn't reach this unless there is some other error in CarService
- Log.e(CarLog.TAG_MEDIA, "Error trying to save last media source, prefs uninitialized");
+ mRemovedMediaSourcePackage = null;
}
+
notifyListeners();
+
+ startMediaConnectorService(shouldStartPlayback(mPlayOnMediaSourceChangedConfig),
+ new UserHandle(mCurrentUser));
}
private void notifyListeners() {
@@ -509,9 +560,9 @@
private MediaController.Callback mMediaControllerCallback = new MediaController.Callback() {
@Override
public void onPlaybackStateChanged(PlaybackState state) {
- savePlaybackState(state);
- // Try to start playback if the new state allows the play action
- maybeRestartPlayback(state);
+ if (!isCurrentUserEphemeral()) {
+ savePlaybackState(state);
+ }
}
};
@@ -564,7 +615,6 @@
return false;
}
-
private boolean isMediaService(@NonNull ComponentName componentName) {
return getMediaService(componentName) != null;
}
@@ -612,6 +662,9 @@
}
private void saveLastMediaSource(@NonNull ComponentName component) {
+ if (!sharedPrefsInitialized()) {
+ return;
+ }
String componentName = component.flattenToString();
String key = SOURCE_KEY + mCurrentUser;
String serialized = mSharedPrefs.getString(key, null);
@@ -627,17 +680,22 @@
}
private ComponentName getLastMediaSource() {
- String key = SOURCE_KEY + mCurrentUser;
- String serialized = mSharedPrefs.getString(key, null);
- if (!TextUtils.isEmpty(serialized)) {
- for (String name : getComponentNameList(serialized)) {
- ComponentName componentName = ComponentName.unflattenFromString(name);
- if (isMediaService(componentName)) {
- return componentName;
+ if (sharedPrefsInitialized()) {
+ String key = SOURCE_KEY + mCurrentUser;
+ String serialized = mSharedPrefs.getString(key, null);
+ if (!TextUtils.isEmpty(serialized)) {
+ for (String name : getComponentNameList(serialized)) {
+ ComponentName componentName = ComponentName.unflattenFromString(name);
+ if (isMediaService(componentName)) {
+ return componentName;
+ }
}
}
}
+ return getDefaultMediaSource();
+ }
+ private ComponentName getDefaultMediaSource() {
String defaultMediaSource = mContext.getString(R.string.default_media_source);
ComponentName defaultComponent = ComponentName.unflattenFromString(defaultMediaSource);
if (isMediaService(defaultComponent)) {
@@ -656,24 +714,20 @@
}
private void savePlaybackState(PlaybackState playbackState) {
+ if (!sharedPrefsInitialized()) {
+ return;
+ }
int state = playbackState != null ? playbackState.getState() : PlaybackState.STATE_NONE;
- if (state == PlaybackState.STATE_PLAYING) {
- // No longer need to request play if audio was resumed already via some other means,
- // e.g. Assistant starts playback, user uses hardware button, etc.
- mStartPlayback = false;
- }
- if (mSharedPrefs != null) {
- String key = PLAYBACK_STATE_KEY + mCurrentUser;
- mSharedPrefs.edit().putInt(key, state).apply();
- }
+ String key = getPlaybackStateKey();
+ mSharedPrefs.edit().putInt(key, state).apply();
}
- private void maybeRestartPlayback(PlaybackState state) {
- if (mStartPlayback && state != null
- && (state.getActions() & PlaybackState.ACTION_PLAY) != 0) {
- play();
- mStartPlayback = false;
- }
+ /**
+ * Builds a string key for saving the playback state for a specific media source (and user)
+ */
+ private String getPlaybackStateKey() {
+ return PLAYBACK_STATE_KEY + mCurrentUser
+ + (mPrimaryMediaComponent == null ? "" : mPrimaryMediaComponent.flattenToString());
}
/**
@@ -691,19 +745,42 @@
for (MediaController controller : mediaControllers) {
if (matchPrimaryMediaSource(controller.getPackageName(), getClassName(controller))) {
mActiveUserMediaController = controller;
+ PlaybackState state = mActiveUserMediaController.getPlaybackState();
+ if (!isCurrentUserEphemeral()) {
+ savePlaybackState(state);
+ }
// Specify Handler to receive callbacks on, to avoid defaulting to the calling
// thread; this method can be called from the MediaSessionManager callback.
// Using the version of this method without passing a handler causes a
// RuntimeException for failing to create a Handler.
- PlaybackState state = mActiveUserMediaController.getPlaybackState();
- savePlaybackState(state);
mActiveUserMediaController.registerCallback(mMediaControllerCallback, mHandler);
- maybeRestartPlayback(state);
return;
}
}
}
+ /**
+ * Returns whether we should autoplay the current media source
+ */
+ private boolean shouldStartPlayback(int config) {
+ switch (config) {
+ case AUTOPLAY_CONFIG_NEVER:
+ return false;
+ case AUTOPLAY_CONFIG_ALWAYS:
+ return true;
+ case AUTOPLAY_CONFIG_ADAPTIVE:
+ if (!sharedPrefsInitialized()) {
+ return false;
+ }
+ return mSharedPrefs.getInt(getPlaybackStateKey(), PlaybackState.STATE_NONE)
+ == PlaybackState.STATE_PLAYING;
+ default:
+ Log.e(CarLog.TAG_MEDIA, "Unsupported playback configuration: " + config);
+ return false;
+ }
+
+ }
+
@NonNull
private static String getClassName(@NonNull MediaController controller) {
Bundle sessionExtras = controller.getExtras();
diff --git a/service/src/com/android/car/CarPowerManagementService.java b/service/src/com/android/car/CarPowerManagementService.java
index c1341da..a1f067b 100644
--- a/service/src/com/android/car/CarPowerManagementService.java
+++ b/service/src/com/android/car/CarPowerManagementService.java
@@ -54,6 +54,10 @@
*/
public class CarPowerManagementService extends ICarPower.Stub implements
CarServiceBase, PowerHalService.PowerEventListener {
+
+ private final Object mLock = new Object();
+ private final Object mSimulationWaitObject = new Object();
+
private final Context mContext;
private final PowerHalService mHal;
private final SystemInterface mSystemInterface;
@@ -62,32 +66,40 @@
// The listeners that must indicate asynchronous completion by calling finished().
private final PowerManagerCallbackList mPowerManagerListenersWithCompletion =
new PowerManagerCallbackList();
- private final Set<IBinder> mListenersWeAreWaitingFor = new HashSet<>();
- private final Object mSimulationSleepObject = new Object();
- @GuardedBy("this")
+ @GuardedBy("mSimulationWaitObject")
+ private boolean mWakeFromSimulatedSleep;
+ @GuardedBy("mSimulationWaitObject")
+ private boolean mInSimulatedDeepSleepMode;
+
+ @GuardedBy("mLock")
+ private final Set<IBinder> mListenersWeAreWaitingFor = new HashSet<>();
+ @GuardedBy("mLock")
private CpmsState mCurrentState;
- @GuardedBy("this")
+ @GuardedBy("mLock")
private Timer mTimer;
- @GuardedBy("this")
+ @GuardedBy("mLock")
private long mProcessingStartTime;
- @GuardedBy("this")
+ @GuardedBy("mLock")
private long mLastSleepEntryTime;
- @GuardedBy("this")
+ @GuardedBy("mLock")
private final LinkedList<CpmsState> mPendingPowerStates = new LinkedList<>();
- @GuardedBy("this")
+ @GuardedBy("mLock")
private HandlerThread mHandlerThread;
- @GuardedBy("this")
+ @GuardedBy("mLock")
private PowerHandler mHandler;
- @GuardedBy("this")
+ @GuardedBy("mLock")
private boolean mTimerActive;
- @GuardedBy("mSimulationSleepObject")
- private boolean mInSimulatedDeepSleepMode = false;
- @GuardedBy("mSimulationSleepObject")
- private boolean mWakeFromSimulatedSleep = false;
- private int mNextWakeupSec = 0;
- private boolean mShutdownOnFinish = false;
+ @GuardedBy("mLock")
+ private int mNextWakeupSec;
+ @GuardedBy("mLock")
+ private boolean mShutdownOnFinish;
+ @GuardedBy("mLock")
+ private boolean mShutdownOnNextSuspend;
+ @GuardedBy("mLock")
private boolean mIsBooting = true;
+ @GuardedBy("mLock")
+ private boolean mIsResuming;
private final CarUserManagerHelper mCarUserManagerHelper;
@@ -160,7 +172,7 @@
@Override
public void init() {
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
mHandlerThread = new HandlerThread(CarLog.TAG_POWER);
mHandlerThread.start();
mHandler = new PowerHandler(mHandlerThread.getLooper());
@@ -180,11 +192,12 @@
@Override
public void release() {
HandlerThread handlerThread;
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
releaseTimerLocked();
mCurrentState = null;
mHandler.cancelAll();
handlerThread = mHandlerThread;
+ mListenersWeAreWaitingFor.clear();
}
handlerThread.quitSafely();
try {
@@ -194,7 +207,6 @@
}
mSystemInterface.stopDisplayStateMonitoring();
mPowerManagerListeners.kill();
- mListenersWeAreWaitingFor.clear();
mSystemInterface.releaseAllWakeLocks();
}
@@ -205,6 +217,7 @@
writer.print(",mProcessingStartTime:" + mProcessingStartTime);
writer.print(",mLastSleepEntryTime:" + mLastSleepEntryTime);
writer.print(",mNextWakeupSec:" + mNextWakeupSec);
+ writer.print(",mShutdownOnNextSuspend:" + mShutdownOnNextSuspend);
writer.print(",mShutdownOnFinish:" + mShutdownOnFinish);
writer.println(",sShutdownPrepareTimeMs:" + sShutdownPrepareTimeMs);
}
@@ -212,7 +225,7 @@
@Override
public void onApPowerStateChange(PowerState state) {
PowerHandler handler;
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
mPendingPowerStates.addFirst(new CpmsState(state));
handler = mHandler;
}
@@ -220,8 +233,11 @@
}
@VisibleForTesting
- protected void clearIsBooting() {
- mIsBooting = false;
+ protected void clearIsBootingOrResuming() {
+ synchronized (mLock) {
+ mIsBooting = false;
+ mIsResuming = false;
+ }
}
/**
@@ -230,7 +246,7 @@
private void onApPowerStateChange(int apState, int carPowerStateListenerState) {
CpmsState newState = new CpmsState(apState, carPowerStateListenerState);
PowerHandler handler;
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
mPendingPowerStates.addFirst(newState);
handler = mHandler;
}
@@ -240,7 +256,7 @@
private void doHandlePowerStateChange() {
CpmsState state;
PowerHandler handler;
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
state = mPendingPowerStates.peekFirst();
mPendingPowerStates.clear();
if (state == null) {
@@ -295,6 +311,7 @@
mHal.sendWaitForVhal();
break;
case CarPowerStateListener.SHUTDOWN_CANCELLED:
+ mShutdownOnNextSuspend = false; // This cancels the "NextSuspend"
mHal.sendShutdownCancel();
break;
case CarPowerStateListener.SUSPEND_EXIT:
@@ -304,10 +321,30 @@
}
private void handleOn() {
- // Do not switch user if it is booting as there can be a race with CarServiceHelperService
- if (mIsBooting) {
- mIsBooting = false;
- } else {
+ // Some OEMs have their own user-switching logic, which may not be coordinated with this
+ // code. To avoid contention, we don't switch users when we coming alive. The OEM's code
+ // should do the switch.
+ boolean allowUserSwitch = true;
+ synchronized (mLock) {
+ if (mIsBooting) {
+ // The system is booting, so don't switch users
+ allowUserSwitch = false;
+ mIsBooting = false;
+ mIsResuming = false;
+ Log.i(CarLog.TAG_POWER, "User switch disallowed while booting");
+ } else if (mIsResuming) {
+ // The system is resuming after a suspension. Optionally disable user switching.
+ allowUserSwitch = !mContext.getResources()
+ .getBoolean(R.bool.config_disableUserSwitchDuringResume);
+ mIsBooting = false;
+ mIsResuming = false;
+ if (!allowUserSwitch) {
+ Log.i(CarLog.TAG_POWER, "User switch disallowed while resuming");
+ }
+ }
+ }
+
+ if (allowUserSwitch) {
int targetUserId = mCarUserManagerHelper.getInitialUser();
if (targetUserId != UserHandle.USER_SYSTEM
&& targetUserId != mCarUserManagerHelper.getCurrentForegroundUserId()) {
@@ -323,9 +360,12 @@
private void handleShutdownPrepare(CpmsState newState) {
mSystemInterface.setDisplayState(false);
// Shutdown on finish if the system doesn't support deep sleep or doesn't allow it.
- mShutdownOnFinish |= !mHal.isDeepSleepAllowed()
- || !mSystemInterface.isSystemSupportingDeepSleep()
- || !newState.mCanSleep;
+ synchronized (mLock) {
+ mShutdownOnFinish = mShutdownOnNextSuspend
+ || !mHal.isDeepSleepAllowed()
+ || !mSystemInterface.isSystemSupportingDeepSleep()
+ || !newState.mCanSleep;
+ }
if (newState.mCanPostpone) {
Log.i(CarLog.TAG_POWER, "starting shutdown prepare");
sendPowerManagerEvent(CarPowerStateListener.SHUTDOWN_PREPARE);
@@ -333,7 +373,7 @@
doHandlePreprocessing();
} else {
Log.i(CarLog.TAG_POWER, "starting shutdown immediately");
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
releaseTimerLocked();
}
// Notify hal that we are shutting down and since it is immediate, don't schedule next
@@ -355,21 +395,27 @@
private void handleWaitForFinish(CpmsState state) {
sendPowerManagerEvent(state.mCarPowerStateListenerState);
+ int wakeupSec;
+ synchronized (mLock) {
+ wakeupSec = mNextWakeupSec;
+ }
switch (state.mCarPowerStateListenerState) {
case CarPowerStateListener.SUSPEND_ENTER:
- mHal.sendSleepEntry(mNextWakeupSec);
+ mHal.sendSleepEntry(wakeupSec);
break;
case CarPowerStateListener.SHUTDOWN_ENTER:
- mHal.sendShutdownStart(mNextWakeupSec);
+ mHal.sendShutdownStart(wakeupSec);
break;
}
}
private void handleFinish() {
- boolean mustShutDown;
boolean simulatedMode;
- synchronized (mSimulationSleepObject) {
+ synchronized (mSimulationWaitObject) {
simulatedMode = mInSimulatedDeepSleepMode;
+ }
+ boolean mustShutDown;
+ synchronized (mLock) {
mustShutDown = mShutdownOnFinish && !simulatedMode;
}
if (mustShutDown) {
@@ -378,17 +424,16 @@
} else {
doHandleDeepSleep(simulatedMode);
}
+ mShutdownOnNextSuspend = false;
}
- @GuardedBy("this")
+ @GuardedBy("mLock")
private void releaseTimerLocked() {
- synchronized (CarPowerManagementService.this) {
- if (mTimer != null) {
- mTimer.cancel();
- }
- mTimer = null;
- mTimerActive = false;
+ if (mTimer != null) {
+ mTimer.cancel();
}
+ mTimer = null;
+ mTimerActive = false;
}
private void doHandlePreprocessing() {
@@ -407,7 +452,7 @@
}
Log.i(CarLog.TAG_POWER, "processing before shutdown expected for: "
+ sShutdownPrepareTimeMs + " ms, adding polling:" + pollingCount);
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
mProcessingStartTime = SystemClock.elapsedRealtime();
releaseTimerLocked();
mTimer = new Timer();
@@ -433,7 +478,7 @@
// see the list go empty and we will think that we are done.
boolean haveSomeCompleters = false;
PowerManagerCallbackList completingListeners = new PowerManagerCallbackList();
- synchronized (mListenersWeAreWaitingFor) {
+ synchronized (mLock) {
mListenersWeAreWaitingFor.clear();
int idx = mPowerManagerListenersWithCompletion.beginBroadcast();
while (idx-- > 0) {
@@ -464,7 +509,7 @@
listener.onStateChanged(newState);
} catch (RemoteException e) {
// It's likely the connection snapped. Let binder death handle the situation.
- Log.e(CarLog.TAG_POWER, "onStateChanged() call failed: " + e, e);
+ Log.e(CarLog.TAG_POWER, "onStateChanged() call failed", e);
}
}
listenerList.finishBroadcast();
@@ -475,28 +520,33 @@
// enterDeepSleep should force sleep entry even if wake lock is kept.
mSystemInterface.switchToPartialWakeLock();
PowerHandler handler;
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
handler = mHandler;
}
handler.cancelProcessingComplete();
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
mLastSleepEntryTime = SystemClock.elapsedRealtime();
}
int nextListenerState;
if (simulatedMode) {
- simulateSleepByLooping();
+ simulateSleepByWaiting();
nextListenerState = CarPowerStateListener.SHUTDOWN_CANCELLED;
} else {
boolean sleepSucceeded = mSystemInterface.enterDeepSleep();
if (!sleepSucceeded) {
- // VHAL should transition CPMS to shutdown.
+ // Suspend failed! VHAL should transition CPMS to shutdown.
Log.e(CarLog.TAG_POWER, "Sleep did not succeed. Now attempting to shut down.");
mSystemInterface.shutdown();
+ return;
}
nextListenerState = CarPowerStateListener.SUSPEND_EXIT;
}
- // On wake, reset nextWakeup time. If not set again, system will suspend/shutdown forever.
- mNextWakeupSec = 0;
+ // On resume, reset nextWakeup time. If not set again, system will suspend/shutdown forever.
+ synchronized (mLock) {
+ mIsResuming = true;
+ mNextWakeupSec = 0;
+ }
+ Log.i(CarLog.TAG_POWER, "Resuming after suspending");
mSystemInterface.refreshDisplayBrightness();
onApPowerStateChange(CpmsState.WAIT_FOR_VHAL, nextListenerState);
}
@@ -537,26 +587,24 @@
}
private void doHandleProcessingComplete() {
- synchronized (CarPowerManagementService.this) {
+ int listenerState;
+ synchronized (mLock) {
releaseTimerLocked();
if (!mShutdownOnFinish && mLastSleepEntryTime > mProcessingStartTime) {
// entered sleep after processing start. So this could be duplicate request.
Log.w(CarLog.TAG_POWER, "Duplicate sleep entry request, ignore");
return;
}
+ listenerState = mShutdownOnFinish
+ ? CarPowerStateListener.SHUTDOWN_ENTER : CarPowerStateListener.SUSPEND_ENTER;
}
-
- if (mShutdownOnFinish) {
- onApPowerStateChange(CpmsState.WAIT_FOR_FINISH, CarPowerStateListener.SHUTDOWN_ENTER);
- } else {
- onApPowerStateChange(CpmsState.WAIT_FOR_FINISH, CarPowerStateListener.SUSPEND_ENTER);
- }
+ onApPowerStateChange(CpmsState.WAIT_FOR_FINISH, listenerState);
}
@Override
public void onDisplayBrightnessChange(int brightness) {
PowerHandler handler;
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
handler = mHandler;
}
handler.handleDisplayBrightnessChange(brightness);
@@ -572,7 +620,7 @@
public void handleMainDisplayChanged(boolean on) {
PowerHandler handler;
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
handler = mHandler;
}
handler.handleMainDisplayStateChange(on);
@@ -586,8 +634,13 @@
mHal.sendDisplayBrightness(brightness);
}
- public synchronized Handler getHandler() {
- return mHandler;
+ /**
+ * Get the PowerHandler that we use to change power states
+ */
+ public Handler getHandler() {
+ synchronized (mLock) {
+ return mHandler;
+ }
}
// Binder interface for general use.
@@ -628,7 +681,9 @@
@Override
public void requestShutdownOnNextSuspend() {
ICarImpl.assertPermission(mContext, Car.PERMISSION_CAR_POWER);
- mShutdownOnFinish = true;
+ synchronized (mLock) {
+ mShutdownOnNextSuspend = true;
+ }
}
@Override
@@ -639,27 +694,31 @@
}
@Override
- public synchronized void scheduleNextWakeupTime(int seconds) {
+ public void scheduleNextWakeupTime(int seconds) {
if (seconds < 0) {
- Log.w(CarLog.TAG_POWER, "Next wake up can not be in negative time. Ignoring!");
+ Log.w(CarLog.TAG_POWER, "Next wake up time is negative. Ignoring!");
return;
}
- if (!mHal.isTimedWakeupAllowed()) {
- Log.w(CarLog.TAG_POWER, "Setting timed wakeups are disabled in HAL. Skipping");
- mNextWakeupSec = 0;
- return;
- }
- if (mNextWakeupSec == 0 || mNextWakeupSec > seconds) {
- mNextWakeupSec = seconds;
- } else {
- Log.d(CarLog.TAG_POWER, "Tried to schedule next wake up, but already had shorter "
- + "scheduled time");
+ boolean timedWakeupAllowed = mHal.isTimedWakeupAllowed();
+ synchronized (mLock) {
+ if (!timedWakeupAllowed) {
+ Log.w(CarLog.TAG_POWER, "Setting timed wakeups are disabled in HAL. Skipping");
+ mNextWakeupSec = 0;
+ return;
+ }
+ if (mNextWakeupSec == 0 || mNextWakeupSec > seconds) {
+ // The new value is sooner than the old value. Take the new value.
+ mNextWakeupSec = seconds;
+ } else {
+ Log.d(CarLog.TAG_POWER, "Tried to schedule next wake up, but already had shorter "
+ + "scheduled time");
+ }
}
}
private void finishedImpl(IBinder binder) {
boolean allAreComplete = false;
- synchronized (mListenersWeAreWaitingFor) {
+ synchronized (mLock) {
boolean oneWasRemoved = mListenersWeAreWaitingFor.remove(binder);
allAreComplete = oneWasRemoved && mListenersWeAreWaitingFor.isEmpty();
}
@@ -673,7 +732,7 @@
|| mCurrentState.mState == CpmsState.SIMULATE_SLEEP) {
PowerHandler powerHandler;
// All apps are ready to shutdown/suspend.
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
if (!mShutdownOnFinish) {
if (mLastSleepEntryTime > mProcessingStartTime
&& mLastSleepEntryTime < SystemClock.elapsedRealtime()) {
@@ -765,7 +824,7 @@
@Override
public void run() {
- synchronized (CarPowerManagementService.this) {
+ synchronized (mLock) {
if (!mTimerActive) {
// Ignore timer expiration since we got cancelled
return;
@@ -927,9 +986,9 @@
}
handler.handlePowerStateChange();
- synchronized (mSimulationSleepObject) {
+ synchronized (mSimulationWaitObject) {
mWakeFromSimulatedSleep = true;
- mSimulationSleepObject.notify();
+ mSimulationWaitObject.notify();
}
}
@@ -940,12 +999,12 @@
* that is not directly derived from a VehicleApPowerStateReq.
*/
public void forceSimulatedSuspend() {
- synchronized (mSimulationSleepObject) {
+ synchronized (mSimulationWaitObject) {
mInSimulatedDeepSleepMode = true;
mWakeFromSimulatedSleep = false;
}
PowerHandler handler;
- synchronized (this) {
+ synchronized (mLock) {
mPendingPowerStates.addFirst(new CpmsState(CpmsState.SIMULATE_SLEEP,
CarPowerStateListener.SHUTDOWN_PREPARE));
handler = mHandler;
@@ -956,19 +1015,20 @@
// In a real Deep Sleep, the hardware removes power from the CPU (but retains power
// on the RAM). This puts the processor to sleep. Upon some external signal, power
// is re-applied to the CPU, and processing resumes right where it left off.
- // We simulate this behavior by simply going into a loop.
- // We exit the loop when forceResume() is called.
- private void simulateSleepByLooping() {
- Log.i(CarLog.TAG_POWER, "Starting to simulate Deep Sleep by looping");
- synchronized (mSimulationSleepObject) {
+ // We simulate this behavior by calling wait().
+ // We continue from wait() when forceSimulatedResume() is called.
+ private void simulateSleepByWaiting() {
+ Log.i(CarLog.TAG_POWER, "Starting to simulate Deep Sleep by waiting");
+ synchronized (mSimulationWaitObject) {
while (!mWakeFromSimulatedSleep) {
try {
- mSimulationSleepObject.wait();
+ mSimulationWaitObject.wait();
} catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt(); // Restore interrupted status
}
}
mInSimulatedDeepSleepMode = false;
}
- Log.i(CarLog.TAG_POWER, "Exit Deep Sleep simulation loop");
+ Log.i(CarLog.TAG_POWER, "Exit Deep Sleep simulation");
}
}
diff --git a/service/src/com/android/car/CarProjectionService.java b/service/src/com/android/car/CarProjectionService.java
index 6f9ae8d..3f8f84c 100644
--- a/service/src/com/android/car/CarProjectionService.java
+++ b/service/src/com/android/car/CarProjectionService.java
@@ -158,6 +158,20 @@
}
};
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int currState = intent.getIntExtra(EXTRA_WIFI_AP_STATE, WIFI_AP_STATE_DISABLED);
+ int prevState = intent.getIntExtra(EXTRA_PREVIOUS_WIFI_AP_STATE,
+ WIFI_AP_STATE_DISABLED);
+ int errorCode = intent.getIntExtra(EXTRA_WIFI_AP_FAILURE_REASON, 0);
+ String ifaceName = intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME);
+ int mode = intent.getIntExtra(EXTRA_WIFI_AP_MODE,
+ WifiManager.IFACE_IP_MODE_UNSPECIFIED);
+ handleWifiApStateChange(currState, prevState, errorCode, ifaceName, mode);
+ }
+ };
+
private boolean mBound;
private Intent mRegisteredService;
@@ -645,6 +659,11 @@
public void onStopped() {
Log.i(TAG, "Local-only hotspot stopped.");
synchronized (mLock) {
+ if (mLocalOnlyHotspotReservation != null) {
+ // We must explicitly released old reservation object, otherwise it may
+ // unexpectedly stop LOHS later because it overrode finalize() method.
+ mLocalOnlyHotspotReservation.close();
+ }
mLocalOnlyHotspotReservation = null;
}
sendApStopped();
@@ -732,22 +751,7 @@
@Override
public void init() {
mContext.registerReceiver(
- new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- final int currState = intent.getIntExtra(EXTRA_WIFI_AP_STATE,
- WIFI_AP_STATE_DISABLED);
- final int prevState = intent.getIntExtra(EXTRA_PREVIOUS_WIFI_AP_STATE,
- WIFI_AP_STATE_DISABLED);
- final int errorCode = intent.getIntExtra(EXTRA_WIFI_AP_FAILURE_REASON, 0);
- final String ifaceName =
- intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME);
- final int mode = intent.getIntExtra(EXTRA_WIFI_AP_MODE,
- WifiManager.IFACE_IP_MODE_UNSPECIFIED);
- handleWifiApStateChange(currState, prevState, errorCode, ifaceName, mode);
- }
- },
- new IntentFilter(WifiManager.WIFI_AP_STATE_CHANGED_ACTION));
+ mBroadcastReceiver, new IntentFilter(WifiManager.WIFI_AP_STATE_CHANGED_ACTION));
}
private void handleWifiApStateChange(int currState, int prevState, int errorCode,
@@ -774,6 +778,7 @@
synchronized (mLock) {
mKeyEventHandlers.clear();
}
+ mContext.unregisterReceiver(mBroadcastReceiver);
}
@Override
diff --git a/service/src/com/android/car/CarPropertyService.java b/service/src/com/android/car/CarPropertyService.java
index 3b4e388..c9a14c9 100644
--- a/service/src/com/android/car/CarPropertyService.java
+++ b/service/src/com/android/car/CarPropertyService.java
@@ -18,6 +18,7 @@
import static java.lang.Integer.toHexString;
+import android.car.Car;
import android.car.hardware.CarPropertyConfig;
import android.car.hardware.CarPropertyValue;
import android.car.hardware.property.CarPropertyEvent;
@@ -348,6 +349,10 @@
return;
}
ICarImpl.assertPermission(mContext, mHal.getWritePermission(propId));
+ // need an extra permission for writing display units properties.
+ if (mHal.isDisplayUnitsProperty(propId)) {
+ ICarImpl.assertPermission(mContext, Car.PERMISSION_VENDOR_EXTENSION);
+ }
mHal.setProperty(prop);
}
diff --git a/service/src/com/android/car/CarService.java b/service/src/com/android/car/CarService.java
index 509ecdd..044c791 100644
--- a/service/src/com/android/car/CarService.java
+++ b/service/src/com/android/car/CarService.java
@@ -25,6 +25,7 @@
import android.os.Build;
import android.os.IBinder;
import android.os.IHwBinder.DeathRecipient;
+import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
@@ -41,6 +42,8 @@
public class CarService extends Service {
+ private static final boolean RESTART_CAR_SERVICE_WHEN_VHAL_CRASH = true;
+
private static final long WAIT_FOR_VEHICLE_HAL_TIMEOUT_MS = 10_000;
private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
@@ -96,6 +99,7 @@
linkToDeath(mVehicle, mVehicleDeathRecipient);
ServiceManager.addService("car_service", mICarImpl);
+ ServiceManager.addService("car_stats", mICarImpl.getStatsService());
SystemProperties.set("boot.car_service_created", "1");
super.onCreate();
}
@@ -178,7 +182,13 @@
@Override
public void serviceDied(long cookie) {
- Log.w(CarLog.TAG_SERVICE, "Vehicle HAL died.");
+ if (RESTART_CAR_SERVICE_WHEN_VHAL_CRASH) {
+ Log.wtf(CarLog.TAG_SERVICE, "***Vehicle HAL died. Car service will restart***");
+ Process.killProcess(Process.myPid());
+ return;
+ }
+
+ Log.wtf(CarLog.TAG_SERVICE, "***Vehicle HAL died.***");
try {
mVehicle.unlinkToDeath(this);
diff --git a/service/src/com/android/car/CarServiceBase.java b/service/src/com/android/car/CarServiceBase.java
index e014cf0..c4b578d 100644
--- a/service/src/com/android/car/CarServiceBase.java
+++ b/service/src/com/android/car/CarServiceBase.java
@@ -36,7 +36,4 @@
default void vehicleHalReconnected() {}
void dump(PrintWriter writer);
-
- /** Called when we only want to dump metrics instead of everything else. */
- default void dumpMetrics(PrintWriter writer) {};
}
diff --git a/service/src/com/android/car/CarTestService.java b/service/src/com/android/car/CarTestService.java
index 8c3f64d..d72d87d 100644
--- a/service/src/com/android/car/CarTestService.java
+++ b/service/src/com/android/car/CarTestService.java
@@ -22,6 +22,8 @@
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
@@ -39,6 +41,9 @@
private final Context mContext;
private final ICarImpl mICarImpl;
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
private final Map<IBinder, TokenDeathRecipient> mTokens = new HashMap<>();
CarTestService(Context context, ICarImpl carImpl) {
@@ -69,7 +74,7 @@
Log.d(TAG, "stopCarService, token: " + token);
ICarImpl.assertPermission(mContext, Car.PERMISSION_CAR_TEST_SERVICE);
- synchronized (this) {
+ synchronized (mLock) {
if (mTokens.containsKey(token)) {
Log.w(TAG, "Calling stopCarService twice with the same token.");
return;
@@ -80,7 +85,7 @@
token.linkToDeath(deathRecipient, 0);
if (mTokens.size() == 1) {
- mICarImpl.release();
+ CarServiceUtils.runOnMainSync(mICarImpl::release);
}
}
}
@@ -92,15 +97,17 @@
releaseToken(token);
}
- private synchronized void releaseToken(IBinder token) {
+ private void releaseToken(IBinder token) {
Log.d(TAG, "releaseToken, token: " + token);
- DeathRecipient deathRecipient = mTokens.remove(token);
- if (deathRecipient != null) {
- token.unlinkToDeath(deathRecipient, 0);
- }
+ synchronized (mLock) {
+ DeathRecipient deathRecipient = mTokens.remove(token);
+ if (deathRecipient != null) {
+ token.unlinkToDeath(deathRecipient, 0);
+ }
- if (mTokens.size() == 0) {
- CarServiceUtils.runOnMain(mICarImpl::init);
+ if (mTokens.size() == 0) {
+ CarServiceUtils.runOnMainSync(mICarImpl::init);
+ }
}
}
@@ -116,4 +123,4 @@
releaseToken(mToken);
}
}
-}
\ No newline at end of file
+}
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index fe7c4d6..0c50da9 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -18,12 +18,15 @@
import android.annotation.MainThread;
import android.app.ActivityManager;
+import android.app.ActivityOptions;
import android.app.UiModeManager;
import android.car.Car;
import android.car.ICar;
import android.car.cluster.renderer.IInstrumentClusterNavigation;
import android.car.userlib.CarUserManagerHelper;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.hardware.automotive.vehicle.V2_0.IVehicle;
@@ -37,12 +40,14 @@
import android.util.Slog;
import android.util.TimingsTraceLog;
+import com.android.car.am.FixedActivityService;
import com.android.car.audio.CarAudioService;
import com.android.car.cluster.InstrumentClusterService;
import com.android.car.garagemode.GarageModeService;
import com.android.car.hal.VehicleHal;
import com.android.car.internal.FeatureConfiguration;
import com.android.car.pm.CarPackageManagerService;
+import com.android.car.stats.CarStatsService;
import com.android.car.systeminterface.SystemInterface;
import com.android.car.trust.CarTrustedDeviceService;
import com.android.car.user.CarUserNoticeService;
@@ -63,6 +68,7 @@
public static final String INTERNAL_INPUT_SERVICE = "internal_input";
public static final String INTERNAL_SYSTEM_ACTIVITY_MONITORING_SERVICE =
"system_activity_monitoring";
+ public static final String INTERNAL_VMS_MANAGER = "vms_manager";
private final Context mContext;
private final VehicleHal mHal;
@@ -80,6 +86,7 @@
private final CarPropertyService mCarPropertyService;
private final CarNightService mCarNightService;
private final AppFocusService mAppFocusService;
+ private final FixedActivityService mFixedActivityService;
private final GarageModeService mGarageModeService;
private final InstrumentClusterService mInstrumentClusterService;
private final CarLocationService mCarLocationService;
@@ -99,6 +106,7 @@
private final VmsSubscriberService mVmsSubscriberService;
private final VmsPublisherService mVmsPublisherService;
private final CarBugreportManagerService mCarBugreportManagerService;
+ private final CarStatsService mCarStatsService;
private final CarServiceBase[] mAllServices;
@@ -149,18 +157,21 @@
mAppFocusService = new AppFocusService(serviceContext, mSystemActivityMonitoringService);
mCarAudioService = new CarAudioService(serviceContext);
mCarNightService = new CarNightService(serviceContext, mCarPropertyService);
+ mFixedActivityService = new FixedActivityService(serviceContext);
mInstrumentClusterService = new InstrumentClusterService(serviceContext,
mAppFocusService, mCarInputService);
mSystemStateControllerService = new SystemStateControllerService(
serviceContext, mCarAudioService, this);
+ mCarStatsService = new CarStatsService(serviceContext);
mVmsBrokerService = new VmsBrokerService();
mVmsClientManager = new VmsClientManager(
- serviceContext, mVmsBrokerService, mCarUserService, mUserManagerHelper,
+ // CarStatsService needs to be passed to the constructor due to HAL init order
+ serviceContext, mCarStatsService, mCarUserService, mVmsBrokerService,
mHal.getVmsHal());
mVmsSubscriberService = new VmsSubscriberService(
serviceContext, mVmsBrokerService, mVmsClientManager, mHal.getVmsHal());
mVmsPublisherService = new VmsPublisherService(
- serviceContext, mVmsBrokerService, mVmsClientManager);
+ serviceContext, mCarStatsService, mVmsBrokerService, mVmsClientManager);
mCarDiagnosticService = new CarDiagnosticService(serviceContext, mHal.getDiagnosticHal());
mCarStorageMonitoringService = new CarStorageMonitoringService(serviceContext,
systemInterface);
@@ -177,6 +188,7 @@
CarLocalServices.addService(SystemInterface.class, mSystemInterface);
CarLocalServices.addService(CarDrivingStateService.class, mCarDrivingStateService);
CarLocalServices.addService(PerUserCarServiceHelper.class, mPerUserCarServiceHelper);
+ CarLocalServices.addService(FixedActivityService.class, mFixedActivityService);
// Be careful with order. Service depending on other service should be inited later.
List<CarServiceBase> allServices = new ArrayList<>();
@@ -193,6 +205,7 @@
allServices.add(mAppFocusService);
allServices.add(mCarAudioService);
allServices.add(mCarNightService);
+ allServices.add(mFixedActivityService);
allServices.add(mInstrumentClusterService);
allServices.add(mSystemStateControllerService);
allServices.add(mPerUserCarServiceHelper);
@@ -231,7 +244,6 @@
mAllServices[i].release();
}
mHal.release();
- CarLocalServices.removeAllServices();
}
void vehicleHalReconnected(IVehicle vehicle) {
@@ -365,6 +377,8 @@
return mCarInputService;
case INTERNAL_SYSTEM_ACTIVITY_MONITORING_SERVICE:
return mSystemActivityMonitoringService;
+ case INTERNAL_VMS_MANAGER:
+ return mVmsClientManager;
default:
Log.w(CarLog.TAG_SERVICE, "getCarInternalService for unknown service:" +
serviceName);
@@ -372,6 +386,10 @@
}
}
+ CarStatsService getStatsService() {
+ return mCarStatsService;
+ }
+
public static void assertVehicleHalMockPermission(Context context) {
assertPermission(context, Car.PERMISSION_MOCK_VEHICLE_HAL);
}
@@ -463,7 +481,7 @@
writer.println("*FutureConfig, DEFAULT:" + FeatureConfiguration.DEFAULT);
writer.println("*Dump all services*");
- dumpAllServices(writer, false);
+ dumpAllServices(writer);
writer.println("*Dump Vehicle HAL*");
writer.println("Vehicle HAL Interface: " + mVehicleInterfaceName);
@@ -475,8 +493,9 @@
e.printStackTrace(writer);
}
} else if ("--metrics".equals(args[0])) {
- writer.println("*Dump car service metrics*");
- dumpAllServices(writer, true);
+ // Strip the --metrics flag when passing dumpsys arguments to CarStatsService
+ // allowing for nested flag selection
+ mCarStatsService.dump(fd, writer, Arrays.copyOfRange(args, 1, args.length));
} else if ("--vms-hal".equals(args[0])) {
mHal.getVmsHal().dumpMetrics(fd);
} else if (Build.IS_USERDEBUG || Build.IS_ENG) {
@@ -486,23 +505,18 @@
}
}
- private void dumpAllServices(PrintWriter writer, boolean dumpMetricsOnly) {
+ private void dumpAllServices(PrintWriter writer) {
for (CarServiceBase service : mAllServices) {
- dumpService(service, writer, dumpMetricsOnly);
+ dumpService(service, writer);
}
if (mCarTestService != null) {
- dumpService(mCarTestService, writer, dumpMetricsOnly);
+ dumpService(mCarTestService, writer);
}
-
}
- private void dumpService(CarServiceBase service, PrintWriter writer, boolean dumpMetricsOnly) {
+ private void dumpService(CarServiceBase service, PrintWriter writer) {
try {
- if (dumpMetricsOnly) {
- service.dumpMetrics(writer);
- } else {
- service.dump(writer);
- }
+ service.dump(writer);
} catch (Exception e) {
writer.println("Failed dumping: " + service.getClass().getName());
e.printStackTrace(writer);
@@ -540,6 +554,8 @@
private static final String COMMAND_SUSPEND = "suspend";
private static final String COMMAND_ENABLE_TRUSTED_DEVICE = "enable-trusted-device";
private static final String COMMAND_REMOVE_TRUSTED_DEVICES = "remove-trusted-devices";
+ private static final String COMMAND_START_FIXED_ACTIVITY_MODE = "start-fixed-activity-mode";
+ private static final String COMMAND_STOP_FIXED_ACTIVITY_MODE = "stop-fixed-activity-mode";
private static final String PARAM_DAY_MODE = "day";
private static final String PARAM_NIGHT_MODE = "night";
@@ -584,6 +600,11 @@
+ " wireless projection");
pw.println("\t--metrics");
pw.println("\t When used with dumpsys, only metrics will be in the dumpsys output.");
+ pw.println("\tstart-fixed-activity displayId packageName activityName");
+ pw.println("\t Start an Activity the specified display as fixed mode");
+ pw.println("\tstop-fixed-mode displayId");
+ pw.println("\t Stop fixed Activity mode for the given display. "
+ + "The Activity will not be restarted upon crash.");
}
public void exec(String[] args, PrintWriter writer) {
@@ -710,12 +731,62 @@
.removeAllTrustedDevices(
mUserManagerHelper.getCurrentForegroundUserId());
break;
+ case COMMAND_START_FIXED_ACTIVITY_MODE:
+ handleStartFixedActivity(args, writer);
+ break;
+ case COMMAND_STOP_FIXED_ACTIVITY_MODE:
+ handleStopFixedMode(args, writer);
+ break;
default:
writer.println("Unknown command: \"" + arg + "\"");
dumpHelp(writer);
}
}
+ private void handleStartFixedActivity(String[] args, PrintWriter writer) {
+ if (args.length != 4) {
+ writer.println("Incorrect number of arguments");
+ dumpHelp(writer);
+ return;
+ }
+ int displayId;
+ try {
+ displayId = Integer.parseInt(args[1]);
+ } catch (NumberFormatException e) {
+ writer.println("Wrong display id:" + args[1]);
+ return;
+ }
+ String packageName = args[2];
+ String activityName = args[3];
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(packageName, activityName));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchDisplayId(displayId);
+ if (!mFixedActivityService.startFixedActivityModeForDisplayAndUser(intent, options,
+ displayId, ActivityManager.getCurrentUser())) {
+ writer.println("Failed to start");
+ return;
+ }
+ writer.println("Succeeded");
+ }
+
+ private void handleStopFixedMode(String[] args, PrintWriter writer) {
+ if (args.length != 2) {
+ writer.println("Incorrect number of arguments");
+ dumpHelp(writer);
+ return;
+ }
+ int displayId;
+ try {
+ displayId = Integer.parseInt(args[1]);
+ } catch (NumberFormatException e) {
+ writer.println("Wrong display id:" + args[1]);
+ return;
+ }
+ mFixedActivityService.stopFixedActivityMode(displayId);
+ }
+
private void forceDayNightMode(String arg, PrintWriter writer) {
int mode;
switch (arg) {
diff --git a/service/src/com/android/car/OnShutdownReboot.java b/service/src/com/android/car/OnShutdownReboot.java
index b68ff5b..5a14371 100644
--- a/service/src/com/android/car/OnShutdownReboot.java
+++ b/service/src/com/android/car/OnShutdownReboot.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
@@ -52,10 +53,10 @@
OnShutdownReboot(Context context) {
mContext = context;
- IntentFilter shutdownFilter = new IntentFilter(Intent.ACTION_SHUTDOWN);
- IntentFilter rebootFilter = new IntentFilter(Intent.ACTION_REBOOT);
- mContext.registerReceiver(mReceiver, shutdownFilter);
- mContext.registerReceiver(mReceiver, rebootFilter);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_SHUTDOWN);
+ filter.addAction(Intent.ACTION_REBOOT);
+ mContext.registerReceiver(mReceiver, filter);
}
OnShutdownReboot addAction(BiConsumer<Context, Intent> action) {
diff --git a/service/src/com/android/car/VmsLayersAvailability.java b/service/src/com/android/car/VmsLayersAvailability.java
index 2e17a89..a9bbc7a 100644
--- a/service/src/com/android/car/VmsLayersAvailability.java
+++ b/service/src/com/android/car/VmsLayersAvailability.java
@@ -109,9 +109,6 @@
mPotentialLayersAndPublishers.clear();
mAvailableAssociatedLayers = Collections.EMPTY_SET;
mUnavailableAssociatedLayers = Collections.EMPTY_SET;
- if (mSeq + 1 < mSeq) {
- throw new IllegalStateException("Sequence is about to loop");
- }
mSeq += 1;
}
}
diff --git a/service/src/com/android/car/VmsPublisherService.java b/service/src/com/android/car/VmsPublisherService.java
index 3029a9e..def10dd 100644
--- a/service/src/com/android/car/VmsPublisherService.java
+++ b/service/src/com/android/car/VmsPublisherService.java
@@ -29,16 +29,16 @@
import android.util.ArrayMap;
import android.util.Log;
+import com.android.car.stats.CarStatsService;
import com.android.car.vms.VmsBrokerService;
import com.android.car.vms.VmsClientManager;
-import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.Map;
-import java.util.Objects;
import java.util.Set;
+import java.util.function.IntSupplier;
/**
@@ -50,75 +50,35 @@
private static final boolean DBG = false;
private static final String TAG = "VmsPublisherService";
- @VisibleForTesting
- static final String PACKET_COUNT_FORMAT = "Packet count for layer %s: %d\n";
-
- @VisibleForTesting
- static final String PACKET_SIZE_FORMAT = "Total packet size for layer %s: %d (bytes)\n";
-
- @VisibleForTesting
- static final String PACKET_FAILURE_COUNT_FORMAT =
- "Total packet failure count for layer %s from %s to %s: %d\n";
-
- @VisibleForTesting
- static final String PACKET_FAILURE_SIZE_FORMAT =
- "Total packet failure size for layer %s from %s to %s: %d (bytes)\n";
-
private final Context mContext;
+ private final CarStatsService mStatsService;
private final VmsBrokerService mBrokerService;
private final VmsClientManager mClientManager;
+ private final IntSupplier mGetCallingUid;
private final Map<String, PublisherProxy> mPublisherProxies = Collections.synchronizedMap(
new ArrayMap<>());
- @GuardedBy("mPacketCounts")
- private final Map<VmsLayer, PacketCountAndSize> mPacketCounts = new ArrayMap<>();
- @GuardedBy("mPacketFailureCounts")
- private final Map<PacketFailureKey, PacketCountAndSize> mPacketFailureCounts = new ArrayMap<>();
-
- // PacketCountAndSize keeps track of the cumulative size and number of packets of a specific
- // VmsLayer that we have seen.
- private class PacketCountAndSize {
- long mCount;
- long mSize;
- }
-
- // PacketFailureKey is a triple of the VmsLayer, the publisher and subscriber for which a packet
- // failed to be sent.
- private class PacketFailureKey {
- VmsLayer mVmsLayer;
- String mPublisher;
- String mSubscriber;
-
- PacketFailureKey(VmsLayer vmsLayer, String publisher, String subscriber) {
- mVmsLayer = vmsLayer;
- mPublisher = publisher;
- mSubscriber = subscriber;
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof PacketFailureKey)) {
- return false;
- }
-
- PacketFailureKey otherKey = (PacketFailureKey) o;
- return Objects.equals(mVmsLayer, otherKey.mVmsLayer) && Objects.equals(mPublisher,
- otherKey.mPublisher) && Objects.equals(mSubscriber, otherKey.mSubscriber);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mVmsLayer, mPublisher, mSubscriber);
- }
- }
-
- public VmsPublisherService(
+ VmsPublisherService(
Context context,
+ CarStatsService statsService,
VmsBrokerService brokerService,
VmsClientManager clientManager) {
+ this(context, statsService, brokerService, clientManager, Binder::getCallingUid);
+ }
+
+ @VisibleForTesting
+ VmsPublisherService(
+ Context context,
+ CarStatsService statsService,
+ VmsBrokerService brokerService,
+ VmsClientManager clientManager,
+ IntSupplier getCallingUid) {
mContext = context;
+ mStatsService = statsService;
mBrokerService = brokerService;
mClientManager = clientManager;
+ mGetCallingUid = getCallingUid;
+
mClientManager.setPublisherService(this);
}
@@ -133,35 +93,8 @@
@Override
public void dump(PrintWriter writer) {
- dumpMetrics(writer);
- }
-
- @Override
- public void dumpMetrics(PrintWriter writer) {
writer.println("*" + getClass().getSimpleName() + "*");
writer.println("mPublisherProxies: " + mPublisherProxies.size());
- synchronized (mPacketCounts) {
- for (Map.Entry<VmsLayer, PacketCountAndSize> entry : mPacketCounts.entrySet()) {
- VmsLayer layer = entry.getKey();
- PacketCountAndSize countAndSize = entry.getValue();
- writer.format(PACKET_COUNT_FORMAT, layer, countAndSize.mCount);
- writer.format(PACKET_SIZE_FORMAT, layer, countAndSize.mSize);
- }
- }
- synchronized (mPacketFailureCounts) {
- for (Map.Entry<PacketFailureKey, PacketCountAndSize> entry :
- mPacketFailureCounts.entrySet()) {
- PacketFailureKey key = entry.getKey();
- PacketCountAndSize countAndSize = entry.getValue();
- VmsLayer layer = key.mVmsLayer;
- String publisher = key.mPublisher;
- String subscriber = key.mSubscriber;
- writer.format(PACKET_FAILURE_COUNT_FORMAT, layer, publisher, subscriber,
- countAndSize.mCount);
- writer.format(PACKET_FAILURE_SIZE_FORMAT, layer, publisher, subscriber,
- countAndSize.mSize);
- }
- }
}
/**
@@ -236,26 +169,6 @@
mBrokerService.setPublisherLayersOffering(token, offering);
}
- private void incrementPacketCount(VmsLayer layer, long size) {
- synchronized (mPacketCounts) {
- PacketCountAndSize countAndSize = mPacketCounts.computeIfAbsent(layer,
- i -> new PacketCountAndSize());
- countAndSize.mCount++;
- countAndSize.mSize += size;
- }
- }
-
- private void incrementPacketFailure(VmsLayer layer, String publisher, String subscriber,
- long size) {
- synchronized (mPacketFailureCounts) {
- PacketFailureKey key = new PacketFailureKey(layer, publisher, subscriber);
- PacketCountAndSize countAndSize = mPacketFailureCounts.computeIfAbsent(key,
- i -> new PacketCountAndSize());
- countAndSize.mCount++;
- countAndSize.mSize += size;
- }
- }
-
@Override
public void publish(IBinder token, VmsLayer layer, int publisherId, byte[] payload) {
assertPermission(token);
@@ -268,7 +181,8 @@
}
int payloadLength = payload != null ? payload.length : 0;
- incrementPacketCount(layer, payloadLength);
+ mStatsService.getVmsClientLogger(mGetCallingUid.getAsInt())
+ .logPacketSent(layer, payloadLength);
// Send the message to subscribers
Set<IVmsSubscriberClient> listeners =
@@ -277,17 +191,21 @@
if (DBG) Log.d(TAG, String.format("Number of subscribers: %d", listeners.size()));
if (listeners.size() == 0) {
- // An empty string for the last argument is a special value signalizing zero
- // subscribers for the VMS_PACKET_FAILURE_REPORTED atom.
- incrementPacketFailure(layer, mName, "", payloadLength);
+ // A negative UID signals that the packet had zero subscribers
+ mStatsService.getVmsClientLogger(-1)
+ .logPacketDropped(layer, payloadLength);
}
for (IVmsSubscriberClient listener : listeners) {
+ int subscriberUid = mClientManager.getSubscriberUid(listener);
try {
listener.onVmsMessageReceived(layer, payload);
+ mStatsService.getVmsClientLogger(subscriberUid)
+ .logPacketReceived(layer, payloadLength);
} catch (RemoteException ex) {
+ mStatsService.getVmsClientLogger(subscriberUid)
+ .logPacketDropped(layer, payloadLength);
String subscriberName = mClientManager.getPackageName(listener);
- incrementPacketFailure(layer, mName, subscriberName, payloadLength);
Log.e(TAG, String.format("Unable to publish to listener: %s", subscriberName));
}
}
diff --git a/service/src/com/android/car/am/FixedActivityService.java b/service/src/com/android/car/am/FixedActivityService.java
new file mode 100644
index 0000000..d3e3c6f
--- /dev/null
+++ b/service/src/com/android/car/am/FixedActivityService.java
@@ -0,0 +1,606 @@
+/*
+ * 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.android.car.am;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.os.Process.INVALID_UID;
+
+import static com.android.car.CarLog.TAG_AM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityManager.StackInfo;
+import android.app.ActivityOptions;
+import android.app.IActivityManager;
+import android.app.IProcessObserver;
+import android.app.TaskStackListener;
+import android.car.hardware.power.CarPowerManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.HandlerThread;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.Display;
+
+import com.android.car.CarLocalServices;
+import com.android.car.CarServiceBase;
+import com.android.car.user.CarUserService;
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Monitors top activity for a display and guarantee activity in fixed mode is re-launched if it has
+ * crashed or gone to background for whatever reason.
+ *
+ * <p>This component also monitors the upddate of the target package and re-launch it once
+ * update is complete.</p>
+ */
+public final class FixedActivityService implements CarServiceBase {
+
+ private static final boolean DBG = false;
+
+ private static final long RECHECK_INTERVAL_MS = 500;
+ private static final int MAX_NUMBER_OF_CONSECUTIVE_CRASH_RETRY = 5;
+ // If process keep running without crashing, will reset consecutive crash counts.
+ private static final long CRASH_FORGET_INTERVAL_MS = 2 * 60 * 1000; // 2 mins
+
+ private static class RunningActivityInfo {
+ @NonNull
+ public final Intent intent;
+
+ @NonNull
+ public final ActivityOptions activityOptions;
+
+ @UserIdInt
+ public final int userId;
+
+ @GuardedBy("mLock")
+ public boolean isVisible;
+ @GuardedBy("mLock")
+ public long lastLaunchTimeMs = 0;
+ @GuardedBy("mLock")
+ public int consecutiveRetries = 0;
+ @GuardedBy("mLock")
+ public int taskId = INVALID_TASK_ID;
+ @GuardedBy("mLock")
+ public int previousTaskId = INVALID_TASK_ID;
+ @GuardedBy("mLock")
+ public boolean inBackground;
+ @GuardedBy("mLock")
+ public boolean failureLogged;
+
+ RunningActivityInfo(@NonNull Intent intent, @NonNull ActivityOptions activityOptions,
+ @UserIdInt int userId) {
+ this.intent = intent;
+ this.activityOptions = activityOptions;
+ this.userId = userId;
+ }
+
+ private void resetCrashCounterLocked() {
+ consecutiveRetries = 0;
+ failureLogged = false;
+ }
+
+ @Override
+ public String toString() {
+ return "RunningActivityInfo{intent:" + intent + ",activityOptions:" + activityOptions
+ + ",userId:" + userId + ",isVisible:" + isVisible
+ + ",lastLaunchTimeMs:" + lastLaunchTimeMs
+ + ",consecutiveRetries:" + consecutiveRetries + ",taskId:" + taskId + "}";
+ }
+ }
+
+ private final Context mContext;
+
+ private final IActivityManager mAm;
+
+ private final UserManager mUm;
+
+ private final CarUserService.UserCallback mUserCallback = new CarUserService.UserCallback() {
+ @Override
+ public void onUserLockChanged(@UserIdInt int userId, boolean unlocked) {
+ // Nothing to do
+ }
+
+ @Override
+ public void onSwitchUser(@UserIdInt int userId) {
+ synchronized (mLock) {
+ mRunningActivities.clear();
+ }
+ }
+ };
+
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
+ || Intent.ACTION_PACKAGE_REPLACED.equals(
+ action)) {
+ Uri packageData = intent.getData();
+ if (packageData == null) {
+ Log.w(TAG_AM, "null packageData");
+ return;
+ }
+ String packageName = packageData.getSchemeSpecificPart();
+ if (packageName == null) {
+ Log.w(TAG_AM, "null packageName");
+ return;
+ }
+ int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
+ int userId = UserHandle.getUserId(uid);
+ boolean tryLaunch = false;
+ synchronized (mLock) {
+ for (int i = 0; i < mRunningActivities.size(); i++) {
+ RunningActivityInfo info = mRunningActivities.valueAt(i);
+ ComponentName component = info.intent.getComponent();
+ // should do this for all activities as the same package can cover multiple
+ // displays.
+ if (packageName.equals(component.getPackageName())
+ && info.userId == userId) {
+ Log.i(TAG_AM, "Package updated:" + packageName
+ + ",user:" + userId);
+ info.resetCrashCounterLocked();
+ tryLaunch = true;
+ }
+ }
+ }
+ if (tryLaunch) {
+ launchIfNecessary();
+ }
+ }
+ }
+ };
+
+ // It says listener but is actually callback.
+ private final TaskStackListener mTaskStackListener = new TaskStackListener() {
+ @Override
+ public void onTaskStackChanged() {
+ launchIfNecessary();
+ }
+
+ @Override
+ public void onTaskCreated(int taskId, ComponentName componentName) {
+ launchIfNecessary();
+ }
+
+ @Override
+ public void onTaskRemoved(int taskId) {
+ launchIfNecessary();
+ }
+
+ @Override
+ public void onTaskMovedToFront(int taskId) {
+ launchIfNecessary();
+ }
+
+ @Override
+ public void onTaskRemovalStarted(int taskId) {
+ launchIfNecessary();
+ }
+ };
+
+ private final IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
+ @Override
+ public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
+ launchIfNecessary();
+ }
+
+ @Override
+ public void onForegroundServicesChanged(int pid, int uid, int fgServiceTypes) {
+ // ignore
+ }
+
+ @Override
+ public void onProcessDied(int pid, int uid) {
+ launchIfNecessary();
+ }
+ };
+
+ private final HandlerThread mHandlerThread = new HandlerThread(
+ FixedActivityService.class.getSimpleName());
+
+ private final Runnable mActivityCheckRunnable = () -> {
+ launchIfNecessary();
+ };
+
+ private final Object mLock = new Object();
+
+ // key: displayId
+ @GuardedBy("mLock")
+ private final SparseArray<RunningActivityInfo> mRunningActivities =
+ new SparseArray<>(/* capacity= */ 1); // default to one cluster only case
+
+ @GuardedBy("mLock")
+ private boolean mEventMonitoringActive;
+
+ @GuardedBy("mLock")
+ private CarPowerManager mCarPowerManager;
+
+ private final CarPowerManager.CarPowerStateListener mCarPowerStateListener = (state) -> {
+ if (state != CarPowerManager.CarPowerStateListener.ON) {
+ return;
+ }
+ synchronized (mLock) {
+ for (int i = 0; i < mRunningActivities.size(); i++) {
+ RunningActivityInfo info = mRunningActivities.valueAt(i);
+ info.resetCrashCounterLocked();
+ }
+ }
+ launchIfNecessary();
+ };
+
+ public FixedActivityService(Context context) {
+ mContext = context;
+ mAm = ActivityManager.getService();
+ mUm = context.getSystemService(UserManager.class);
+ mHandlerThread.start();
+ }
+
+ @Override
+ public void init() {
+ // nothing to do
+ }
+
+ @Override
+ public void release() {
+ stopMonitoringEvents();
+ }
+
+ @Override
+ public void dump(PrintWriter writer) {
+ writer.println("*FixedActivityService*");
+ synchronized (mLock) {
+ writer.println("mRunningActivities:" + mRunningActivities
+ + " ,mEventMonitoringActive:" + mEventMonitoringActive);
+ }
+ }
+
+ private void postRecheck(long delayMs) {
+ mHandlerThread.getThreadHandler().postDelayed(mActivityCheckRunnable, delayMs);
+ }
+
+ private void startMonitoringEvents() {
+ CarPowerManager carPowerManager;
+ synchronized (mLock) {
+ if (mEventMonitoringActive) {
+ return;
+ }
+ mEventMonitoringActive = true;
+ carPowerManager = CarLocalServices.createCarPowerManager(mContext);
+ mCarPowerManager = carPowerManager;
+ }
+ CarUserService userService = CarLocalServices.getService(CarUserService.class);
+ userService.addUserCallback(mUserCallback);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addDataScheme("package");
+ mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
+ /* broadcastPermission= */ null, /* scheduler= */ null);
+ try {
+ mAm.registerTaskStackListener(mTaskStackListener);
+ mAm.registerProcessObserver(mProcessObserver);
+ } catch (RemoteException e) {
+ Log.e(TAG_AM, "remote exception from AM", e);
+ }
+ try {
+ carPowerManager.setListener(mCarPowerStateListener);
+ } catch (Exception e) {
+ // should not happen
+ Log.e(TAG_AM, "Got exception from CarPowerManager", e);
+ }
+ }
+
+ private void stopMonitoringEvents() {
+ CarPowerManager carPowerManager;
+ synchronized (mLock) {
+ if (!mEventMonitoringActive) {
+ return;
+ }
+ mEventMonitoringActive = false;
+ carPowerManager = mCarPowerManager;
+ mCarPowerManager = null;
+ }
+ if (carPowerManager != null) {
+ carPowerManager.clearListener();
+ }
+ mHandlerThread.getThreadHandler().removeCallbacks(mActivityCheckRunnable);
+ CarUserService userService = CarLocalServices.getService(CarUserService.class);
+ userService.removeUserCallback(mUserCallback);
+ try {
+ mAm.unregisterTaskStackListener(mTaskStackListener);
+ mAm.unregisterProcessObserver(mProcessObserver);
+ } catch (RemoteException e) {
+ Log.e(TAG_AM, "remote exception from AM", e);
+ }
+ mContext.unregisterReceiver(mBroadcastReceiver);
+ }
+
+ @Nullable
+ private List<StackInfo> getStackInfos() {
+ try {
+ return mAm.getAllStackInfos();
+ } catch (RemoteException e) {
+ Log.e(TAG_AM, "remote exception from AM", e);
+ }
+ return null;
+ }
+
+ /**
+ * Launches all stored fixed mode activities if necessary.
+ * @param displayId Display id to check if it is visible. If check is not necessary, should pass
+ * {@link Display#INVALID_DISPLAY}.
+ * @return true if fixed Activity for given {@code displayId} is visible / successfully
+ * launched. It will return false for {@link Display#INVALID_DISPLAY} {@code displayId}.
+ */
+ private boolean launchIfNecessary(int displayId) {
+ List<StackInfo> infos = getStackInfos();
+ if (infos == null) {
+ Log.e(TAG_AM, "cannot get StackInfo from AM");
+ return false;
+ }
+ long now = SystemClock.elapsedRealtime();
+ synchronized (mLock) {
+ if (mRunningActivities.size() == 0) {
+ // it must have been stopped.
+ if (DBG) {
+ Log.i(TAG_AM, "empty activity list", new RuntimeException());
+ }
+ return false;
+ }
+ for (int i = 0; i < mRunningActivities.size(); i++) {
+ mRunningActivities.valueAt(i).isVisible = false;
+ }
+ for (StackInfo stackInfo : infos) {
+ RunningActivityInfo activityInfo = mRunningActivities.get(stackInfo.displayId);
+ if (activityInfo == null) {
+ continue;
+ }
+ int topUserId = stackInfo.taskUserIds[stackInfo.taskUserIds.length - 1];
+ if (activityInfo.intent.getComponent().equals(stackInfo.topActivity)
+ && activityInfo.userId == topUserId && stackInfo.visible) {
+ // top one is matching.
+ activityInfo.isVisible = true;
+ activityInfo.taskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
+ continue;
+ }
+ activityInfo.previousTaskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
+ Log.i(TAG_AM, "Unmatched top activity will be removed:"
+ + stackInfo.topActivity + " top task id:" + activityInfo.previousTaskId
+ + " user:" + topUserId + " display:" + stackInfo.displayId);
+ activityInfo.inBackground = false;
+ for (int i = 0; i < stackInfo.taskIds.length - 1; i++) {
+ if (activityInfo.taskId == stackInfo.taskIds[i]) {
+ activityInfo.inBackground = true;
+ }
+ }
+ if (!activityInfo.inBackground) {
+ activityInfo.taskId = INVALID_TASK_ID;
+ }
+ }
+ for (int i = 0; i < mRunningActivities.size(); i++) {
+ RunningActivityInfo activityInfo = mRunningActivities.valueAt(i);
+ long timeSinceLastLaunchMs = now - activityInfo.lastLaunchTimeMs;
+ if (activityInfo.isVisible) {
+ if (timeSinceLastLaunchMs >= CRASH_FORGET_INTERVAL_MS) {
+ activityInfo.consecutiveRetries = 0;
+ }
+ continue;
+ }
+ if (!isComponentAvailable(activityInfo.intent.getComponent(),
+ activityInfo.userId) || !isUserAllowedToLaunchActivity(
+ activityInfo.userId)) {
+ continue;
+ }
+ // For 1st call (consecutiveRetries == 0), do not wait as there can be no posting
+ // for recheck.
+ if (activityInfo.consecutiveRetries > 0 && (timeSinceLastLaunchMs
+ < RECHECK_INTERVAL_MS)) {
+ // wait until next check interval comes.
+ continue;
+ }
+ if (activityInfo.consecutiveRetries >= MAX_NUMBER_OF_CONSECUTIVE_CRASH_RETRY) {
+ // re-tried too many times, give up for now.
+ if (!activityInfo.failureLogged) {
+ activityInfo.failureLogged = true;
+ Log.w(TAG_AM, "Too many relaunch failure of fixed activity:"
+ + activityInfo);
+ }
+ continue;
+ }
+
+ Log.i(TAG_AM, "Launching Activity for fixed mode. Intent:" + activityInfo.intent
+ + ",userId:" + UserHandle.of(activityInfo.userId) + ",displayId:"
+ + mRunningActivities.keyAt(i));
+ // Increase retry count if task is not in background. In case like other app is
+ // launched and the target activity is still in background, do not consider it
+ // as retry.
+ if (!activityInfo.inBackground) {
+ activityInfo.consecutiveRetries++;
+ }
+ try {
+ postRecheck(RECHECK_INTERVAL_MS);
+ postRecheck(CRASH_FORGET_INTERVAL_MS);
+ mContext.startActivityAsUser(activityInfo.intent,
+ activityInfo.activityOptions.toBundle(),
+ UserHandle.of(activityInfo.userId));
+ activityInfo.isVisible = true;
+ activityInfo.lastLaunchTimeMs = SystemClock.elapsedRealtime();
+ } catch (Exception e) { // Catch all for any app related issues.
+ Log.w(TAG_AM, "Cannot start activity:" + activityInfo.intent, e);
+ }
+ }
+ RunningActivityInfo activityInfo = mRunningActivities.get(displayId);
+ if (activityInfo == null) {
+ return false;
+ }
+ return activityInfo.isVisible;
+ }
+ }
+
+ private void launchIfNecessary() {
+ launchIfNecessary(Display.INVALID_DISPLAY);
+ }
+
+ private void logComponentNotFound(ComponentName component, @UserIdInt int userId,
+ Exception e) {
+ Log.e(TAG_AM, "Specified Component not found:" + component
+ + " for userid:" + userId, e);
+ }
+
+ private boolean isComponentAvailable(ComponentName component, @UserIdInt int userId) {
+ PackageInfo packageInfo;
+ try {
+ packageInfo = mContext.getPackageManager().getPackageInfoAsUser(
+ component.getPackageName(), PackageManager.GET_ACTIVITIES, userId);
+ } catch (PackageManager.NameNotFoundException e) {
+ logComponentNotFound(component, userId, e);
+ return false;
+ }
+ if (packageInfo == null || packageInfo.activities == null) {
+ // may not be necessary but additional safety check
+ logComponentNotFound(component, userId, new RuntimeException());
+ return false;
+ }
+ String fullName = component.getClassName();
+ String shortName = component.getShortClassName();
+ for (ActivityInfo info : packageInfo.activities) {
+ if (info.name.equals(fullName) || info.name.equals(shortName)) {
+ return true;
+ }
+ }
+ logComponentNotFound(component, userId, new RuntimeException());
+ return false;
+ }
+
+ private boolean isUserAllowedToLaunchActivity(@UserIdInt int userId) {
+ int currentUser = ActivityManager.getCurrentUser();
+ if (userId == currentUser) {
+ return true;
+ }
+ int[] profileIds = mUm.getEnabledProfileIds(currentUser);
+ for (int id : profileIds) {
+ if (id == userId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isDisplayAllowedForFixedMode(int displayId) {
+ if (displayId == Display.DEFAULT_DISPLAY || displayId == Display.INVALID_DISPLAY) {
+ Log.w(TAG_AM, "Target display cannot be used for fixed mode, displayId:" + displayId,
+ new RuntimeException());
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks {@link InstrumentClusterRenderingService#startFixedActivityModeForDisplayAndUser(
+ * Intent, ActivityOptions, int)}
+ */
+ public boolean startFixedActivityModeForDisplayAndUser(@NonNull Intent intent,
+ @NonNull ActivityOptions options, int displayId, @UserIdInt int userId) {
+ if (!isDisplayAllowedForFixedMode(displayId)) {
+ return false;
+ }
+ if (options == null) {
+ Log.e(TAG_AM, "startFixedActivityModeForDisplayAndUser, null options");
+ return false;
+ }
+ if (!isUserAllowedToLaunchActivity(userId)) {
+ Log.e(TAG_AM, "startFixedActivityModeForDisplayAndUser, requested user:" + userId
+ + " cannot launch activity, Intent:" + intent);
+ return false;
+ }
+ ComponentName component = intent.getComponent();
+ if (component == null) {
+ Log.e(TAG_AM,
+ "startFixedActivityModeForDisplayAndUser: No component specified for "
+ + "requested Intent"
+ + intent);
+ return false;
+ }
+ if (!isComponentAvailable(component, userId)) {
+ return false;
+ }
+ boolean startMonitoringEvents = false;
+ synchronized (mLock) {
+ if (mRunningActivities.size() == 0) {
+ startMonitoringEvents = true;
+ }
+ RunningActivityInfo activityInfo = mRunningActivities.get(displayId);
+ boolean replaceEntry = true;
+ if (activityInfo != null && activityInfo.intent.equals(intent)
+ && options.equals(activityInfo.activityOptions)
+ && userId == activityInfo.userId) {
+ replaceEntry = false;
+ if (activityInfo.isVisible) { // already shown.
+ return true;
+ }
+ }
+ if (replaceEntry) {
+ activityInfo = new RunningActivityInfo(intent, options, userId);
+ mRunningActivities.put(displayId, activityInfo);
+ }
+ }
+ boolean launched = launchIfNecessary(displayId);
+ if (!launched) {
+ synchronized (mLock) {
+ mRunningActivities.remove(displayId);
+ }
+ }
+ // If first trial fails, let client know and do not retry as it can be wrong setting.
+ if (startMonitoringEvents && launched) {
+ startMonitoringEvents();
+ }
+ return launched;
+ }
+
+ /** Check {@link InstrumentClusterRenderingService#stopFixedActivityMode(int)} */
+ public void stopFixedActivityMode(int displayId) {
+ if (!isDisplayAllowedForFixedMode(displayId)) {
+ return;
+ }
+ boolean stopMonitoringEvents = false;
+ synchronized (mLock) {
+ mRunningActivities.remove(displayId);
+ if (mRunningActivities.size() == 0) {
+ stopMonitoringEvents = true;
+ }
+ }
+ if (stopMonitoringEvents) {
+ stopMonitoringEvents();
+ }
+ }
+}
diff --git a/service/src/com/android/car/audio/CarAudioFocus.java b/service/src/com/android/car/audio/CarAudioFocus.java
index 165caad..286a045 100644
--- a/service/src/com/android/car/audio/CarAudioFocus.java
+++ b/service/src/com/android/car/audio/CarAudioFocus.java
@@ -113,6 +113,10 @@
mAfi.getPackageName())
== PackageManager.PERMISSION_GRANTED);
}
+
+ String getUsageName() {
+ return mAfi.getAttributes().usageToString();
+ }
}
@@ -171,8 +175,9 @@
// The default audio framework's behavior is to remove the previous entry in the stack (no-op
// if the requester is already holding focus).
int evaluateFocusRequest(AudioFocusInfo afi) {
- Log.i(TAG, "Evaluating " + focusEventToString(afi.getGainRequest()) + " request for client "
- + afi.getClientId());
+ Log.i(TAG, "Evaluating " + focusEventToString(afi.getGainRequest())
+ + " request for client " + afi.getClientId()
+ + " with usage " + afi.getAttributes().usageToString());
// Is this a request for premanant focus?
// AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -- Means Notifications should be denied
@@ -588,14 +593,17 @@
public synchronized void dump(String indent, PrintWriter writer) {
writer.printf("%s*CarAudioFocus*\n", indent);
- writer.printf("%s\tCurrent Focus Holders:\n", indent);
+ String innerIndent = indent + "\t";
+ writer.printf("%sCurrent Focus Holders:\n", innerIndent);
for (String clientId : mFocusHolders.keySet()) {
- writer.printf("%s\t\t%s\n", indent, clientId);
+ writer.printf("%s\t%s - %s\n", innerIndent, clientId,
+ mFocusHolders.get(clientId).getUsageName());
}
- writer.printf("%s\tTransient Focus Losers:\n", indent);
+ writer.printf("%sTransient Focus Losers:\n", innerIndent);
for (String clientId : mFocusLosers.keySet()) {
- writer.printf("%s\t\t%s\n", indent, clientId);
+ writer.printf("%s\t%s - %s\n", innerIndent, clientId,
+ mFocusLosers.get(clientId).getUsageName());
}
}
diff --git a/service/src/com/android/car/cluster/InstrumentClusterService.java b/service/src/com/android/car/cluster/InstrumentClusterService.java
index fd16da5..cc0a6b7 100644
--- a/service/src/com/android/car/cluster/InstrumentClusterService.java
+++ b/service/src/com/android/car/cluster/InstrumentClusterService.java
@@ -15,17 +15,23 @@
*/
package com.android.car.cluster;
+import static android.car.cluster.renderer.InstrumentClusterRenderingService.EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER;
+
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.app.ActivityOptions;
import android.car.CarAppFocusManager;
import android.car.cluster.IInstrumentClusterManagerCallback;
import android.car.cluster.IInstrumentClusterManagerService;
import android.car.cluster.renderer.IInstrumentCluster;
+import android.car.cluster.renderer.IInstrumentClusterHelper;
import android.car.cluster.renderer.IInstrumentClusterNavigation;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
@@ -43,6 +49,7 @@
import com.android.car.CarLog;
import com.android.car.CarServiceBase;
import com.android.car.R;
+import com.android.car.am.FixedActivityService;
import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
@@ -69,16 +76,18 @@
*/
@Deprecated
private final ClusterManagerService mClusterManagerService = new ClusterManagerService();
- private final Object mSync = new Object();
- @GuardedBy("mSync")
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
private ContextOwner mNavContextOwner = NO_OWNER;
- @GuardedBy("mSync")
+ @GuardedBy("mLock")
private IInstrumentCluster mRendererService;
// If renderer service crashed / stopped and this class fails to rebind with it immediately,
// we should wait some time before next attempt. This may happen during APK update for example.
+ @GuardedBy("mLock")
private DeferredRebinder mDeferredRebinder;
// Whether {@link android.car.cluster.renderer.InstrumentClusterRendererService} is bound
// (although not necessarily connected)
+ @GuardedBy("mLock")
private boolean mRendererBound = false;
/**
@@ -92,7 +101,7 @@
}
IInstrumentCluster service = IInstrumentCluster.Stub.asInterface(binder);
ContextOwner navContextOwner;
- synchronized (mSync) {
+ synchronized (mLock) {
mRendererService = service;
navContextOwner = mNavContextOwner;
}
@@ -107,19 +116,41 @@
Log.d(TAG, "onServiceDisconnected, name: " + name);
}
mContext.unbindService(this);
- mRendererBound = false;
-
- synchronized (mSync) {
+ DeferredRebinder rebinder;
+ synchronized (mLock) {
+ mRendererBound = false;
mRendererService = null;
+ if (mDeferredRebinder == null) {
+ mDeferredRebinder = new DeferredRebinder();
+ }
+ rebinder = mDeferredRebinder;
}
-
- if (mDeferredRebinder == null) {
- mDeferredRebinder = new DeferredRebinder();
- }
- mDeferredRebinder.rebind();
+ rebinder.rebind();
}
};
+ private final IInstrumentClusterHelper mInstrumentClusterHelper =
+ new IInstrumentClusterHelper.Stub() {
+ @Override
+ public boolean startFixedActivityModeForDisplayAndUser(Intent intent,
+ Bundle activityOptionsBundle, int userId) {
+ Binder.clearCallingIdentity();
+ ActivityOptions options = new ActivityOptions(activityOptionsBundle);
+ FixedActivityService service = CarLocalServices.getService(
+ FixedActivityService.class);
+ return service.startFixedActivityModeForDisplayAndUser(intent, options,
+ options.getLaunchDisplayId(), userId);
+ }
+
+ @Override
+ public void stopFixedActivityMode(int displayId) {
+ Binder.clearCallingIdentity();
+ FixedActivityService service = CarLocalServices.getService(
+ FixedActivityService.class);
+ service.stopFixedActivityMode(displayId);
+ }
+ };
+
public InstrumentClusterService(Context context, AppFocusService appFocusService,
CarInputService carInputService) {
mContext = context;
@@ -181,7 +212,7 @@
IInstrumentCluster service;
ContextOwner requester = new ContextOwner(uid, pid);
ContextOwner newOwner = acquire ? requester : NO_OWNER;
- synchronized (mSync) {
+ synchronized (mLock) {
if ((acquire && Objects.equals(mNavContextOwner, requester))
|| (!acquire && !Objects.equals(mNavContextOwner, requester))) {
// Nothing to do here. Either the same owner is acquiring twice, or someone is
@@ -221,6 +252,11 @@
Intent intent = new Intent();
intent.setComponent(ComponentName.unflattenFromString(rendererService));
+ // Litle bit inefficiency here as Intent.getIBinderExtra() is a hidden API.
+ Bundle bundle = new Bundle();
+ bundle.putBinder(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER,
+ mInstrumentClusterHelper.asBinder());
+ intent.putExtra(EXTRA_BUNDLE_KEY_FOR_INSTRUMENT_CLUSTER_HELPER, bundle);
return mContext.bindServiceAsUser(intent, mRendererServiceConnection,
Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, UserHandle.SYSTEM);
}
@@ -262,7 +298,7 @@
private IInstrumentCluster getInstrumentClusterRendererService() {
IInstrumentCluster service;
- synchronized (mSync) {
+ synchronized (mLock) {
service = mRendererService;
}
return service;
diff --git a/service/src/com/android/car/hal/PropertyHalService.java b/service/src/com/android/car/hal/PropertyHalService.java
index 545fc2b..484e667 100644
--- a/service/src/com/android/car/hal/PropertyHalService.java
+++ b/service/src/com/android/car/hal/PropertyHalService.java
@@ -183,6 +183,14 @@
}
/**
+ * Return true if property is a display_units property
+ * @param propId
+ */
+ public boolean isDisplayUnitsProperty(int propId) {
+ return mPropIds.isPropertyToChangeUnits(propId);
+ }
+
+ /**
* Set the property value.
* @param prop
*/
diff --git a/service/src/com/android/car/hal/PropertyHalServiceIds.java b/service/src/com/android/car/hal/PropertyHalServiceIds.java
index 5409b4d..82a89d7 100644
--- a/service/src/com/android/car/hal/PropertyHalServiceIds.java
+++ b/service/src/com/android/car/hal/PropertyHalServiceIds.java
@@ -26,6 +26,8 @@
import android.util.Pair;
import android.util.SparseArray;
+import java.util.HashSet;
+
/**
* Helper class to define which property IDs are used by PropertyHalService. This class binds the
* read and write permissions to the property ID.
@@ -39,11 +41,12 @@
* properties.
*/
private final SparseArray<Pair<String, String>> mProps;
+ private final HashSet<Integer> mPropForUnits;
private static final String TAG = "PropertyHalServiceIds";
public PropertyHalServiceIds() {
mProps = new SparseArray<>();
-
+ mPropForUnits = new HashSet<>();
// Add propertyId and read/write permissions
// Cabin Properties
mProps.put(VehicleProperty.DOOR_POS, new Pair<>(
@@ -385,24 +388,31 @@
mProps.put(VehicleProperty.CABIN_LIGHTS_SWITCH, new Pair<>(
Car.PERMISSION_CONTROL_INTERIOR_LIGHTS,
Car.PERMISSION_CONTROL_INTERIOR_LIGHTS));
+ // Display_Units
mProps.put(VehicleProperty.DISTANCE_DISPLAY_UNITS, new Pair<>(
Car.PERMISSION_READ_DISPLAY_UNITS,
Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+ mPropForUnits.add(VehicleProperty.DISTANCE_DISPLAY_UNITS);
mProps.put(VehicleProperty.FUEL_VOLUME_DISPLAY_UNITS, new Pair<>(
Car.PERMISSION_READ_DISPLAY_UNITS,
Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+ mPropForUnits.add(VehicleProperty.FUEL_VOLUME_DISPLAY_UNITS);
mProps.put(VehicleProperty.TIRE_PRESSURE_DISPLAY_UNITS, new Pair<>(
Car.PERMISSION_READ_DISPLAY_UNITS,
Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+ mPropForUnits.add(VehicleProperty.TIRE_PRESSURE_DISPLAY_UNITS);
mProps.put(VehicleProperty.EV_BATTERY_DISPLAY_UNITS, new Pair<>(
Car.PERMISSION_READ_DISPLAY_UNITS,
Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+ mPropForUnits.add(VehicleProperty.EV_BATTERY_DISPLAY_UNITS);
mProps.put(VehicleProperty.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME, new Pair<>(
Car.PERMISSION_READ_DISPLAY_UNITS,
Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+ mPropForUnits.add(VehicleProperty.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME);
mProps.put(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS, new Pair<>(
Car.PERMISSION_READ_DISPLAY_UNITS,
Car.PERMISSION_CONTROL_DISPLAY_UNITS));
+ mPropForUnits.add(VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS);
}
/**
@@ -469,4 +479,11 @@
return insertVendorProperty(propId);
}
}
+
+ /**
+ * Check if the property is one of display units properties.
+ */
+ public boolean isPropertyToChangeUnits(int propertyId) {
+ return mPropForUnits.contains(propertyId);
+ }
}
diff --git a/service/src/com/android/car/hal/VmsHalService.java b/service/src/com/android/car/hal/VmsHalService.java
index bba5d5f..99263d7 100644
--- a/service/src/com/android/car/hal/VmsHalService.java
+++ b/service/src/com/android/car/hal/VmsHalService.java
@@ -17,8 +17,6 @@
import static com.android.car.CarServiceUtils.toByteArray;
-import static java.lang.Integer.toHexString;
-
import android.car.VehicleAreaType;
import android.car.vms.IVmsPublisherClient;
import android.car.vms.IVmsPublisherService;
@@ -43,6 +41,7 @@
import android.hardware.automotive.vehicle.V2_0.VmsOfferingMessageIntegerValuesIndex;
import android.hardware.automotive.vehicle.V2_0.VmsPublisherInformationIntegerValuesIndex;
import android.hardware.automotive.vehicle.V2_0.VmsStartSessionMessageIntegerValuesIndex;
+import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
@@ -54,7 +53,6 @@
import androidx.annotation.VisibleForTesting;
-import com.android.car.CarLog;
import com.android.car.vms.VmsClientManager;
import java.io.FileDescriptor;
@@ -87,6 +85,7 @@
private final int mCoreId;
private final MessageQueue mMessageQueue;
private final int mClientMetricsProperty;
+ private final boolean mPropagatePropertyException;
private volatile boolean mIsSupported = false;
private VmsClientManager mClientManager;
@@ -186,11 +185,7 @@
int messageType = msg.what;
VehiclePropValue vehicleProp = (VehiclePropValue) msg.obj;
if (DBG) Log.d(TAG, "Sending " + VmsMessageType.toString(messageType) + " message");
- try {
- setPropertyValue(vehicleProp);
- } catch (RemoteException e) {
- Log.e(TAG, "While sending " + VmsMessageType.toString(messageType));
- }
+ setPropertyValue(vehicleProp);
return true;
}
}
@@ -199,15 +194,17 @@
* Constructor used by {@link VehicleHal}
*/
VmsHalService(Context context, VehicleHal vehicleHal) {
- this(context, vehicleHal, SystemClock::uptimeMillis);
+ this(context, vehicleHal, SystemClock::uptimeMillis, (Build.IS_ENG || Build.IS_USERDEBUG));
}
@VisibleForTesting
- VmsHalService(Context context, VehicleHal vehicleHal, Supplier<Long> getCoreId) {
+ VmsHalService(Context context, VehicleHal vehicleHal, Supplier<Long> getCoreId,
+ boolean propagatePropertyException) {
mVehicleHal = vehicleHal;
mCoreId = (int) (getCoreId.get() % Integer.MAX_VALUE);
mMessageQueue = new MessageQueue();
mClientMetricsProperty = getClientMetricsProperty(context);
+ mPropagatePropertyException = propagatePropertyException;
}
private static int getClientMetricsProperty(Context context) {
@@ -326,8 +323,9 @@
VehiclePropValue vehicleProp = null;
try {
vehicleProp = mVehicleHal.get(mClientMetricsProperty);
- } catch (PropertyTimeoutException e) {
- Log.e(TAG, "Timeout while reading metrics from client");
+ } catch (PropertyTimeoutException | RuntimeException e) {
+ // Failures to retrieve metrics should be non-fatal
+ Log.e(TAG, "While reading metrics from client", e);
}
if (vehicleProp == null) {
if (DBG) Log.d(TAG, "Metrics unavailable");
@@ -395,7 +393,7 @@
Log.e(TAG, "Unexpected message type: " + messageType);
}
} catch (IndexOutOfBoundsException | RemoteException e) {
- Log.e(TAG, "While handling: " + messageType, e);
+ Log.e(TAG, "While handling " + VmsMessageType.toString(messageType), e);
}
}
}
@@ -425,9 +423,8 @@
mSubscriptionStateSequence = -1;
mAvailableLayersSequence = -1;
- // Enqueue an acknowledgement message
- mMessageQueue.enqueue(VmsMessageType.START_SESSION,
- createStartSessionMessage(mCoreId, clientId));
+ // Send acknowledgement message
+ setPropertyValue(createStartSessionMessage(mCoreId, clientId));
}
// Notify client manager of connection
@@ -678,7 +675,7 @@
mPublisherService.getSubscriptions()));
}
- private void setPropertyValue(VehiclePropValue vehicleProp) throws RemoteException {
+ private void setPropertyValue(VehiclePropValue vehicleProp) {
int messageType = vehicleProp.value.int32Values.get(
VmsBaseMessageIntegerValuesIndex.MESSAGE_TYPE);
@@ -690,11 +687,11 @@
try {
mVehicleHal.set(vehicleProp);
- } catch (PropertyTimeoutException e) {
- Log.e(CarLog.TAG_PROPERTY,
- "set, property not ready 0x" + toHexString(HAL_PROPERTY_ID));
- throw new RemoteException(
- "Timeout while sending " + VmsMessageType.toString(messageType));
+ } catch (PropertyTimeoutException | RuntimeException e) {
+ Log.e(TAG, "While sending " + VmsMessageType.toString(messageType), e.getCause());
+ if (mPropagatePropertyException) {
+ throw new IllegalStateException(e);
+ }
}
}
diff --git a/service/src/com/android/car/pm/ActivityBlockingActivity.java b/service/src/com/android/car/pm/ActivityBlockingActivity.java
index 9dcb70a..9756523 100644
--- a/service/src/com/android/car/pm/ActivityBlockingActivity.java
+++ b/service/src/com/android/car/pm/ActivityBlockingActivity.java
@@ -79,14 +79,19 @@
// restrictions are lifted.
// This Activity should be launched only after car service is initialized. Currently this
// Activity is only launched from CPMS. So this is safe to do.
- mCar = Car.createCar(this);
- mUxRManager = (CarUxRestrictionsManager) mCar.getCarManager(
- Car.CAR_UX_RESTRICTION_SERVICE);
- // This activity would have been launched only in a restricted state.
- // But ensuring when the service connection is established, that we are still
- // in a restricted state.
- handleUxRChange(mUxRManager.getCurrentCarUxRestrictions());
- mUxRManager.registerListener(ActivityBlockingActivity.this::handleUxRChange);
+ mCar = Car.createCar(this, /* handler= */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
+ (car, ready) -> {
+ if (!ready) {
+ return;
+ }
+ mUxRManager = (CarUxRestrictionsManager) car.getCarManager(
+ Car.CAR_UX_RESTRICTION_SERVICE);
+ // This activity would have been launched only in a restricted state.
+ // But ensuring when the service connection is established, that we are still
+ // in a restricted state.
+ handleUxRChange(mUxRManager.getCurrentCarUxRestrictions());
+ mUxRManager.registerListener(ActivityBlockingActivity.this::handleUxRChange);
+ });
}
@Override
diff --git a/service/src/com/android/car/pm/CarPackageManagerService.java b/service/src/com/android/car/pm/CarPackageManagerService.java
index 01d5500..54d577a 100644
--- a/service/src/com/android/car/pm/CarPackageManagerService.java
+++ b/service/src/com/android/car/pm/CarPackageManagerService.java
@@ -213,7 +213,7 @@
if (DBG_POLICY_SET) {
Log.i(CarLog.TAG_PACKAGE, "policy setting from binder call, client:" + packageName);
}
- doSetAppBlockingPolicy(packageName, policy, flags, true /*setNow*/);
+ doSetAppBlockingPolicy(packageName, policy, flags);
}
/**
@@ -224,8 +224,8 @@
mSystemActivityMonitoringService.restartTask(taskId);
}
- private void doSetAppBlockingPolicy(String packageName, CarAppBlockingPolicy policy, int flags,
- boolean setNow) {
+ private void doSetAppBlockingPolicy(String packageName, CarAppBlockingPolicy policy,
+ int flags) {
if (mContext.checkCallingOrSelfPermission(Car.PERMISSION_CONTROL_APP_BLOCKING)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
@@ -240,15 +240,22 @@
throw new IllegalArgumentException(
"Cannot set both FLAG_SET_POLICY_ADD and FLAG_SET_POLICY_REMOVE flag");
}
- mHandler.requestUpdatingPolicy(packageName, policy, flags);
- if (setNow) {
- mHandler.requestPolicySetting();
+ synchronized (this) {
if ((flags & CarPackageManager.FLAG_SET_POLICY_WAIT_FOR_CHANGE) != 0) {
- synchronized (policy) {
- try {
- policy.wait();
- } catch (InterruptedException e) {
+ mWaitingPolicies.add(policy);
+ }
+ }
+ mHandler.requestUpdatingPolicy(packageName, policy, flags);
+ if ((flags & CarPackageManager.FLAG_SET_POLICY_WAIT_FOR_CHANGE) != 0) {
+ synchronized (this) {
+ try {
+ while (mWaitingPolicies.contains(policy)) {
+ wait();
}
+ } catch (InterruptedException e) {
+ // Pass it over binder call
+ throw new IllegalStateException(
+ "Interrupted while waiting for policy completion", e);
}
}
}
@@ -394,7 +401,8 @@
}
mProxies.clear();
}
- wakeupClientsWaitingForPolicySettingLocked();
+ mWaitingPolicies.clear();
+ notifyAll();
}
mContext.unregisterReceiver(mPackageParsingEventReceiver);
mContext.unregisterReceiver(mUserSwitchedEventReceiver);
@@ -456,23 +464,6 @@
notifyAll();
}
- @GuardedBy("this")
- private void wakeupClientsWaitingForPolicySettingLocked() {
- for (CarAppBlockingPolicy waitingPolicy : mWaitingPolicies) {
- synchronized (waitingPolicy) {
- waitingPolicy.notifyAll();
- }
- }
- mWaitingPolicies.clear();
- }
-
- private void doSetPolicy() {
- synchronized (this) {
- wakeupClientsWaitingForPolicySettingLocked();
- }
- blockTopActivitiesIfNecessary();
- }
-
private void doUpdatePolicy(String packageName, CarAppBlockingPolicy policy, int flags) {
if (DBG_POLICY_SET) {
Log.i(CarLog.TAG_PACKAGE, "setting policy from:" + packageName + ",policy:" + policy +
@@ -497,7 +488,8 @@
clientPolicy.replaceWhitelists(whitelistWrapper);
}
if ((flags & CarPackageManager.FLAG_SET_POLICY_WAIT_FOR_CHANGE) != 0) {
- mWaitingPolicies.add(policy);
+ mWaitingPolicies.remove(policy);
+ notifyAll();
}
if (DBG_POLICY_SET) {
Log.i(CarLog.TAG_PACKAGE, "policy set:" + dumpPoliciesLocked(false));
@@ -855,7 +847,6 @@
policyIntent.setAction(CarAppBlockingPolicyService.SERVICE_INTERFACE);
List<ResolveInfo> policyInfos = mPackageManager.queryIntentServices(policyIntent, 0);
if (policyInfos == null) { //no need to wait for service binding and retrieval.
- mHandler.requestPolicySetting();
return;
}
LinkedList<AppBlockingPolicyProxy> proxies = new LinkedList<>();
@@ -892,7 +883,6 @@
private void doHandlePolicyConnection(AppBlockingPolicyProxy proxy,
CarAppBlockingPolicy policy) {
- boolean shouldSetPolicy = false;
synchronized (this) {
if (mProxies == null) {
proxy.disconnect();
@@ -900,7 +890,6 @@
}
mProxies.remove(proxy);
if (mProxies.size() == 0) {
- shouldSetPolicy = true;
mProxies = null;
}
}
@@ -910,13 +899,10 @@
Log.i(CarLog.TAG_PACKAGE, "policy setting from policy service:" +
proxy.getPackageName());
}
- doSetAppBlockingPolicy(proxy.getPackageName(), policy, 0, false /*setNow*/);
+ doSetAppBlockingPolicy(proxy.getPackageName(), policy, 0);
}
} finally {
proxy.disconnect();
- if (shouldSetPolicy) {
- mHandler.requestPolicySetting();
- }
}
}
@@ -1186,11 +1172,10 @@
* Reading policy and setting policy can take time. Run it in a separate handler thread.
*/
private class PackageHandler extends Handler {
- private final int MSG_INIT = 0;
- private final int MSG_PARSE_PKG = 1;
- private final int MSG_SET_POLICY = 2;
- private final int MSG_UPDATE_POLICY = 3;
- private final int MSG_RELEASE = 4;
+ private static final int MSG_INIT = 0;
+ private static final int MSG_PARSE_PKG = 1;
+ private static final int MSG_UPDATE_POLICY = 2;
+ private static final int MSG_RELEASE = 3;
private PackageHandler(Looper looper) {
super(looper);
@@ -1203,17 +1188,11 @@
private void requestRelease() {
removeMessages(MSG_INIT);
- removeMessages(MSG_SET_POLICY);
removeMessages(MSG_UPDATE_POLICY);
Message msg = obtainMessage(MSG_RELEASE);
sendMessage(msg);
}
- private void requestPolicySetting() {
- Message msg = obtainMessage(MSG_SET_POLICY);
- sendMessage(msg);
- }
-
private void requestUpdatingPolicy(String packageName, CarAppBlockingPolicy policy,
int flags) {
Pair<String, CarAppBlockingPolicy> pair = new Pair<>(packageName, policy);
@@ -1242,9 +1221,6 @@
case MSG_PARSE_PKG:
doParseInstalledPackages();
break;
- case MSG_SET_POLICY:
- doSetPolicy();
- break;
case MSG_UPDATE_POLICY:
Pair<String, CarAppBlockingPolicy> pair =
(Pair<String, CarAppBlockingPolicy>) msg.obj;
diff --git a/service/src/com/android/car/pm/VendorServiceController.java b/service/src/com/android/car/pm/VendorServiceController.java
index 189370e..b20dc89 100644
--- a/service/src/com/android/car/pm/VendorServiceController.java
+++ b/service/src/com/android/car/pm/VendorServiceController.java
@@ -251,7 +251,7 @@
* Represents connection to the vendor service.
*/
private static class VendorServiceConnection implements ServiceConnection {
- private static final int REBIND_DELAY_MS = 1000;
+ private static final int REBIND_DELAY_MS = 5000;
private static final int MAX_RECENT_FAILURES = 5;
private static final int FAILURE_COUNTER_RESET_TIMEOUT = 5 * 60 * 1000; // 5 min.
private static final int MSG_REBIND = 0;
diff --git a/service/src/com/android/car/stats/CarStatsService.java b/service/src/com/android/car/stats/CarStatsService.java
new file mode 100644
index 0000000..d74fb8c
--- /dev/null
+++ b/service/src/com/android/car/stats/CarStatsService.java
@@ -0,0 +1,181 @@
+/*
+ * 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.android.car.stats;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.StatsLogEventWrapper;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.StatsLog;
+
+import com.android.car.stats.VmsClientLogger.ConnectionState;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.car.ICarStatsService;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Implementation of {@link ICarStatsService}, for reporting pulled atoms via statsd.
+ *
+ * Also implements collection and dumpsys reporting of atoms in CSV format.
+ */
+public class CarStatsService extends ICarStatsService.Stub {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "CarStatsService";
+ private static final String VMS_CONNECTION_STATS_DUMPSYS_HEADER =
+ "uid,packageName,attempts,connected,disconnected,terminated,errors";
+
+ private static final Function<VmsClientLogger, String> VMS_CONNECTION_STATS_DUMPSYS_FORMAT =
+ entry -> String.format(Locale.US,
+ "%d,%s,%d,%d,%d,%d,%d",
+ entry.getUid(), entry.getPackageName(),
+ entry.getConnectionStateCount(ConnectionState.CONNECTING),
+ entry.getConnectionStateCount(ConnectionState.CONNECTED),
+ entry.getConnectionStateCount(ConnectionState.DISCONNECTED),
+ entry.getConnectionStateCount(ConnectionState.TERMINATED),
+ entry.getConnectionStateCount(ConnectionState.CONNECTION_ERROR));
+
+ private static final String VMS_CLIENT_STATS_DUMPSYS_HEADER =
+ "uid,layerType,layerChannel,layerVersion,"
+ + "txBytes,txPackets,rxBytes,rxPackets,droppedBytes,droppedPackets";
+
+ private static final Function<VmsClientStats, String> VMS_CLIENT_STATS_DUMPSYS_FORMAT =
+ entry -> String.format(
+ "%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
+ entry.getUid(),
+ entry.getLayerType(), entry.getLayerChannel(), entry.getLayerVersion(),
+ entry.getTxBytes(), entry.getTxPackets(),
+ entry.getRxBytes(), entry.getRxPackets(),
+ entry.getDroppedBytes(), entry.getDroppedPackets());
+
+ private static final Comparator<VmsClientStats> VMS_CLIENT_STATS_ORDER =
+ Comparator.comparingInt(VmsClientStats::getUid)
+ .thenComparingInt(VmsClientStats::getLayerType)
+ .thenComparingInt(VmsClientStats::getLayerChannel)
+ .thenComparingInt(VmsClientStats::getLayerVersion);
+
+ private final Context mContext;
+ private final PackageManager mPackageManager;
+
+ @GuardedBy("mVmsClientStats")
+ private final Map<Integer, VmsClientLogger> mVmsClientStats = new ArrayMap<>();
+
+ public CarStatsService(Context context) {
+ mContext = context;
+ mPackageManager = context.getPackageManager();
+ }
+
+ /**
+ * Gets a logger for the VMS client with a given UID.
+ */
+ public VmsClientLogger getVmsClientLogger(int clientUid) {
+ synchronized (mVmsClientStats) {
+ return mVmsClientStats.computeIfAbsent(
+ clientUid,
+ uid -> {
+ String packageName = mPackageManager.getNameForUid(uid);
+ if (DEBUG) {
+ Log.d(TAG, "Created VmsClientLog: " + packageName);
+ }
+ return new VmsClientLogger(uid, packageName);
+ });
+ }
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ List<String> flags = Arrays.asList(args);
+ if (args.length == 0 || flags.contains("--vms-client")) {
+ dumpVmsStats(writer);
+ }
+ }
+
+ @Override
+ public StatsLogEventWrapper[] pullData(int tagId) {
+ mContext.enforceCallingPermission(Manifest.permission.DUMP, null);
+ if (tagId != StatsLog.VMS_CLIENT_STATS) {
+ Log.w(TAG, "Unexpected tagId: " + tagId);
+ return null;
+ }
+
+ List<StatsLogEventWrapper> ret = new ArrayList<>();
+ long elapsedNanos = SystemClock.elapsedRealtimeNanos();
+ long wallClockNanos = SystemClock.currentTimeMicro() * 1000L;
+ pullVmsClientStats(tagId, elapsedNanos, wallClockNanos, ret);
+ return ret.toArray(new StatsLogEventWrapper[0]);
+ }
+
+ private void dumpVmsStats(PrintWriter writer) {
+ synchronized (mVmsClientStats) {
+ writer.println(VMS_CONNECTION_STATS_DUMPSYS_HEADER);
+ mVmsClientStats.values().stream()
+ // Unknown UID will not have connection stats
+ .filter(entry -> entry.getUid() > 0)
+ // Sort stats by UID
+ .sorted(Comparator.comparingInt(VmsClientLogger::getUid))
+ .forEachOrdered(entry -> writer.println(
+ VMS_CONNECTION_STATS_DUMPSYS_FORMAT.apply(entry)));
+ writer.println();
+
+ writer.println(VMS_CLIENT_STATS_DUMPSYS_HEADER);
+ dumpVmsClientStats(entry -> writer.println(
+ VMS_CLIENT_STATS_DUMPSYS_FORMAT.apply(entry)));
+ }
+ }
+
+ private void pullVmsClientStats(int tagId, long elapsedNanos, long wallClockNanos,
+ List<StatsLogEventWrapper> pulledData) {
+ dumpVmsClientStats((entry) -> {
+ StatsLogEventWrapper e =
+ new StatsLogEventWrapper(tagId, elapsedNanos, wallClockNanos);
+ e.writeInt(entry.getUid());
+
+ e.writeInt(entry.getLayerType());
+ e.writeInt(entry.getLayerChannel());
+ e.writeInt(entry.getLayerVersion());
+
+ e.writeLong(entry.getTxBytes());
+ e.writeLong(entry.getTxPackets());
+ e.writeLong(entry.getRxBytes());
+ e.writeLong(entry.getRxPackets());
+ e.writeLong(entry.getDroppedBytes());
+ e.writeLong(entry.getDroppedPackets());
+ pulledData.add(e);
+ });
+ }
+
+ private void dumpVmsClientStats(Consumer<VmsClientStats> dumpFn) {
+ synchronized (mVmsClientStats) {
+ mVmsClientStats.values().stream()
+ .flatMap(log -> log.getLayerEntries().stream())
+ .sorted(VMS_CLIENT_STATS_ORDER)
+ .forEachOrdered(dumpFn);
+ }
+ }
+}
diff --git a/service/src/com/android/car/stats/VmsClientLogger.java b/service/src/com/android/car/stats/VmsClientLogger.java
new file mode 100644
index 0000000..948db05
--- /dev/null
+++ b/service/src/com/android/car/stats/VmsClientLogger.java
@@ -0,0 +1,150 @@
+/*
+ * 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.android.car.stats;
+
+import android.annotation.Nullable;
+import android.car.vms.VmsLayer;
+import android.util.ArrayMap;
+import android.util.StatsLog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+/**
+ * Logger for per-client VMS statistics.
+ */
+public class VmsClientLogger {
+ /**
+ * Constants used for identifying client connection states.
+ */
+ public static class ConnectionState {
+ // Attempting to connect to the client
+ public static final int CONNECTING =
+ StatsLog.VMS_CLIENT_CONNECTION_STATE_CHANGED__STATE__CONNECTING;
+ // Client connection established
+ public static final int CONNECTED =
+ StatsLog.VMS_CLIENT_CONNECTION_STATE_CHANGED__STATE__CONNECTED;
+ // Client connection closed unexpectedly
+ public static final int DISCONNECTED =
+ StatsLog.VMS_CLIENT_CONNECTION_STATE_CHANGED__STATE__DISCONNECTED;
+ // Client connection closed by VMS
+ public static final int TERMINATED =
+ StatsLog.VMS_CLIENT_CONNECTION_STATE_CHANGED__STATE__TERMINATED;
+ // Error establishing the client connection
+ public static final int CONNECTION_ERROR =
+ StatsLog.VMS_CLIENT_CONNECTION_STATE_CHANGED__STATE__CONNECTION_ERROR;
+ }
+
+ private final Object mLock = new Object();
+
+ private final int mUid;
+ private final String mPackageName;
+
+ @GuardedBy("mLock")
+ private Map<Integer, AtomicLong> mConnectionStateCounters = new ArrayMap<>();
+
+ @GuardedBy("mLock")
+ private final Map<VmsLayer, VmsClientStats> mLayerStats = new ArrayMap<>();
+
+ VmsClientLogger(int clientUid, @Nullable String clientPackage) {
+ mUid = clientUid;
+ mPackageName = clientPackage != null ? clientPackage : "";
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Logs a connection state change for the client.
+ *
+ * @param connectionState New connection state
+ */
+ public void logConnectionState(int connectionState) {
+ StatsLog.write(StatsLog.VMS_CLIENT_CONNECTION_STATE_CHANGED,
+ mUid, mPackageName, connectionState);
+
+ AtomicLong counter;
+ synchronized (mLock) {
+ counter = mConnectionStateCounters.computeIfAbsent(connectionState,
+ ignored -> new AtomicLong());
+ }
+ counter.incrementAndGet();
+ }
+
+ long getConnectionStateCount(int connectionState) {
+ AtomicLong counter;
+ synchronized (mLock) {
+ counter = mConnectionStateCounters.get(connectionState);
+ }
+ return counter == null ? 0L : counter.get();
+ }
+
+ /**
+ * Logs that a packet was published by the client.
+ *
+ * @param layer Layer of packet
+ * @param size Size of packet
+ */
+ public void logPacketSent(VmsLayer layer, long size) {
+ getLayerEntry(layer).packetSent(size);
+ }
+
+ /**
+ * Logs that a packet was received successfully by the client.
+ *
+ * @param layer Layer of packet
+ * @param size Size of packet
+ */
+ public void logPacketReceived(VmsLayer layer, long size) {
+ getLayerEntry(layer).packetReceived(size);
+ }
+
+ /**
+ * Logs that a packet was dropped due to an error delivering to the client.
+ *
+ * @param layer Layer of packet
+ * @param size Size of packet
+ */
+ public void logPacketDropped(VmsLayer layer, long size) {
+ getLayerEntry(layer).packetDropped(size);
+ }
+
+ Collection<VmsClientStats> getLayerEntries() {
+ synchronized (mLock) {
+ return mLayerStats.values().stream()
+ .map(VmsClientStats::new) // Make a deep copy of the entries
+ .collect(Collectors.toList());
+ }
+ }
+
+ private VmsClientStats getLayerEntry(VmsLayer layer) {
+ synchronized (mLock) {
+ return mLayerStats.computeIfAbsent(
+ layer,
+ (k) -> new VmsClientStats(mUid, layer));
+ }
+ }
+}
diff --git a/service/src/com/android/car/stats/VmsClientStats.java b/service/src/com/android/car/stats/VmsClientStats.java
new file mode 100644
index 0000000..9fbe1dd
--- /dev/null
+++ b/service/src/com/android/car/stats/VmsClientStats.java
@@ -0,0 +1,174 @@
+/*
+ * 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.android.car.stats;
+
+import android.car.vms.VmsLayer;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * Java representation of VmsClientStats statsd atom.
+ *
+ * All access to this class is synchronized through VmsClientLog.
+ */
+class VmsClientStats {
+ private final Object mLock = new Object();
+
+ private final int mUid;
+
+ private final int mLayerType;
+ private final int mLayerChannel;
+ private final int mLayerVersion;
+
+ @GuardedBy("mLock")
+ private long mTxBytes;
+ @GuardedBy("mLock")
+ private long mTxPackets;
+
+ @GuardedBy("mLock")
+ private long mRxBytes;
+ @GuardedBy("mLock")
+ private long mRxPackets;
+
+ @GuardedBy("mLock")
+ private long mDroppedBytes;
+ @GuardedBy("mLock")
+ private long mDroppedPackets;
+
+ /**
+ * Constructor for a VmsClientStats entry.
+ *
+ * @param uid UID of client package.
+ * @param layer Vehicle Maps Service layer.
+ */
+ VmsClientStats(int uid, VmsLayer layer) {
+ mUid = uid;
+
+ mLayerType = layer.getType();
+ mLayerChannel = layer.getSubtype();
+ mLayerVersion = layer.getVersion();
+ }
+
+ /**
+ * Copy constructor for entries exported from {@link VmsClientLogger}.
+ */
+ VmsClientStats(VmsClientStats other) {
+ synchronized (other.mLock) {
+ this.mUid = other.mUid;
+
+ this.mLayerType = other.mLayerType;
+ this.mLayerChannel = other.mLayerChannel;
+ this.mLayerVersion = other.mLayerVersion;
+
+ this.mTxBytes = other.mTxBytes;
+ this.mTxPackets = other.mTxPackets;
+ this.mRxBytes = other.mRxBytes;
+ this.mRxPackets = other.mRxPackets;
+ this.mDroppedBytes = other.mDroppedBytes;
+ this.mDroppedPackets = other.mDroppedPackets;
+ }
+ }
+
+ /**
+ * Records that a packet was sent by a publisher client.
+ *
+ * @param size Size of packet.
+ */
+ void packetSent(long size) {
+ synchronized (mLock) {
+ mTxBytes += size;
+ mTxPackets++;
+ }
+ }
+
+ /**
+ * Records that a packet was successfully received by a subscriber client.
+ *
+ * @param size Size of packet.
+ */
+ void packetReceived(long size) {
+ synchronized (mLock) {
+ mRxBytes += size;
+ mRxPackets++;
+ }
+ }
+
+ /**
+ * Records that a packet was dropped while being delivered to a subscriber client.
+ *
+ * @param size Size of packet.
+ */
+ void packetDropped(long size) {
+ synchronized (mLock) {
+ mDroppedBytes += size;
+ mDroppedPackets++;
+ }
+ }
+
+ int getUid() {
+ return mUid;
+ }
+
+ int getLayerType() {
+ return mLayerType;
+ }
+
+ int getLayerChannel() {
+ return mLayerChannel;
+ }
+
+ int getLayerVersion() {
+ return mLayerVersion;
+ }
+
+ long getTxBytes() {
+ synchronized (mLock) {
+ return mTxBytes;
+ }
+ }
+
+ long getTxPackets() {
+ synchronized (mLock) {
+ return mTxPackets;
+ }
+ }
+
+ long getRxBytes() {
+ synchronized (mLock) {
+ return mRxBytes;
+ }
+ }
+
+ long getRxPackets() {
+ synchronized (mLock) {
+ return mRxPackets;
+ }
+ }
+
+ long getDroppedBytes() {
+ synchronized (mLock) {
+ return mDroppedBytes;
+ }
+ }
+
+ long getDroppedPackets() {
+ synchronized (mLock) {
+ return mDroppedPackets;
+ }
+ }
+}
+
diff --git a/service/src/com/android/car/systeminterface/TimeInterface.java b/service/src/com/android/car/systeminterface/TimeInterface.java
index dea1153..fd350a5 100644
--- a/service/src/com/android/car/systeminterface/TimeInterface.java
+++ b/service/src/com/android/car/systeminterface/TimeInterface.java
@@ -19,6 +19,9 @@
import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
import android.os.SystemClock;
+
+import com.android.internal.annotations.GuardedBy;
+
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -42,16 +45,34 @@
void cancelAllActions();
class DefaultImpl implements TimeInterface {
- private final ScheduledExecutorService mExecutor = newSingleThreadScheduledExecutor();
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private ScheduledExecutorService mExecutor;
@Override
public void scheduleAction(Runnable r, long delayMs) {
- mExecutor.scheduleAtFixedRate(r, delayMs, delayMs, TimeUnit.MILLISECONDS);
+ ScheduledExecutorService executor;
+ synchronized (mLock) {
+ executor = mExecutor;
+ if (executor == null) {
+ executor = newSingleThreadScheduledExecutor();
+ mExecutor = executor;
+ }
+ }
+ executor.scheduleAtFixedRate(r, delayMs, delayMs, TimeUnit.MILLISECONDS);
}
@Override
public void cancelAllActions() {
- mExecutor.shutdownNow();
+ ScheduledExecutorService executor;
+ synchronized (mLock) {
+ executor = mExecutor;
+ mExecutor = null;
+ }
+ if (executor != null) {
+ executor.shutdownNow();
+ }
}
}
}
diff --git a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
index 0f54647..7f2923d 100644
--- a/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
+++ b/service/src/com/android/car/trust/CarTrustAgentEnrollmentService.java
@@ -16,6 +16,7 @@
package com.android.car.trust;
+import static android.car.Car.PERMISSION_CAR_ENROLL_TRUST;
import static android.car.trust.CarTrustAgentEnrollmentManager.ENROLLMENT_HANDSHAKE_FAILURE;
import static android.car.trust.CarTrustAgentEnrollmentManager.ENROLLMENT_NOT_ALLOWED;
@@ -33,6 +34,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.app.ActivityManager;
import android.bluetooth.BluetoothDevice;
import android.car.encryptionrunner.EncryptionRunner;
@@ -52,6 +54,7 @@
import android.util.Log;
import com.android.car.BLEStreamProtos.BLEOperationProto.OperationType;
+import com.android.car.ICarImpl;
import com.android.car.R;
import com.android.car.Utils;
import com.android.internal.annotations.GuardedBy;
@@ -171,7 +174,9 @@
* the enrollment of the trusted device.
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public void startEnrollmentAdvertising() {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
if (!mTrustedDeviceService.getSharedPrefs()
.getBoolean(TRUSTED_DEVICE_ENROLLMENT_ENABLED_KEY, true)) {
Log.e(TAG, "Trusted Device Enrollment disabled");
@@ -192,7 +197,9 @@
* Stop BLE advertisement for Enrollment
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public void stopEnrollmentAdvertising() {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
logEnrollmentEvent(STOP_ENROLLMENT_ADVERTISING);
addEnrollmentServiceLog("stopEnrollmentAdvertising");
mCarTrustAgentBleManager.stopEnrollmentAdvertising();
@@ -205,7 +212,9 @@
* @param device the remote Bluetooth device that will receive the signal.
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public void enrollmentHandshakeAccepted(BluetoothDevice device) {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
logEnrollmentEvent(ENROLLMENT_HANDSHAKE_ACCEPTED);
addEnrollmentServiceLog("enrollmentHandshakeAccepted");
if (device == null || !device.equals(mRemoteEnrollmentDevice)) {
@@ -226,7 +235,9 @@
* navigated away from the app before completing enrollment.
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public void terminateEnrollmentHandshake() {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
addEnrollmentServiceLog("terminateEnrollmentHandshake");
// Disconnect from BLE
mCarTrustAgentBleManager.disconnectRemoteDevice();
@@ -252,7 +263,9 @@
* @return True if the escrow token is active, false if not
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public boolean isEscrowTokenActive(long handle, int uid) {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
if (mTokenActiveStateMap.get(handle) != null) {
return mTokenActiveStateMap.get(handle);
}
@@ -266,7 +279,9 @@
* @param uid user id
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public void removeEscrowToken(long handle, int uid) {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
mEnrollmentDelegate.removeEscrowToken(handle, uid);
addEnrollmentServiceLog("removeEscrowToken (handle:" + handle + " uid:" + uid + ")");
}
@@ -277,7 +292,9 @@
* @param uid user id
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public void removeAllTrustedDevices(int uid) {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
for (TrustedDeviceInfo device : getEnrolledDeviceInfosForUser(uid)) {
removeEscrowToken(device.getHandle(), uid);
}
@@ -291,7 +308,9 @@
* @param isEnabled {@code true} to enable; {@code false} to disable the feature.
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public void setTrustedDeviceEnrollmentEnabled(boolean isEnabled) {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
SharedPreferences.Editor editor = mTrustedDeviceService.getSharedPrefs().edit();
editor.putBoolean(TRUSTED_DEVICE_ENROLLMENT_ENABLED_KEY, isEnabled);
if (!editor.commit()) {
@@ -308,7 +327,9 @@
* back.
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public void setTrustedDeviceUnlockEnabled(boolean isEnabled) {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
mTrustedDeviceService.getCarTrustAgentUnlockService()
.setTrustedDeviceUnlockEnabled(isEnabled);
}
@@ -322,7 +343,9 @@
*/
@NonNull
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public List<TrustedDeviceInfo> getEnrolledDeviceInfosForUser(int uid) {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
Set<String> enrolledDeviceInfos = mTrustedDeviceService.getSharedPrefs().getStringSet(
String.valueOf(uid), new HashSet<>());
List<TrustedDeviceInfo> trustedDeviceInfos = new ArrayList<>(enrolledDeviceInfos.size());
@@ -342,7 +365,9 @@
* @param listener {@link ICarTrustAgentEnrollmentCallback}
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public synchronized void registerEnrollmentCallback(ICarTrustAgentEnrollmentCallback listener) {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
if (listener == null) {
throw new IllegalArgumentException("Listener is null");
}
@@ -835,8 +860,10 @@
* @param listener client to unregister
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public synchronized void unregisterEnrollmentCallback(
ICarTrustAgentEnrollmentCallback listener) {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
if (listener == null) {
throw new IllegalArgumentException("Listener is null");
}
@@ -858,7 +885,9 @@
* @param listener {@link ICarTrustAgentBleCallback}
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public synchronized void registerBleCallback(ICarTrustAgentBleCallback listener) {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
if (listener == null) {
throw new IllegalArgumentException("Listener is null");
}
@@ -903,7 +932,9 @@
* @param listener client to unregister
*/
@Override
+ @RequiresPermission(PERMISSION_CAR_ENROLL_TRUST)
public synchronized void unregisterBleCallback(ICarTrustAgentBleCallback listener) {
+ ICarImpl.assertTrustAgentEnrollmentPermission(mContext);
if (listener == null) {
throw new IllegalArgumentException("Listener is null");
}
diff --git a/service/src/com/android/car/vms/VmsClientManager.java b/service/src/com/android/car/vms/VmsClientManager.java
index 710793c..f2c4813 100644
--- a/service/src/com/android/car/vms/VmsClientManager.java
+++ b/service/src/com/android/car/vms/VmsClientManager.java
@@ -17,14 +17,11 @@
package com.android.car.vms;
import android.car.Car;
-import android.car.userlib.CarUserManagerHelper;
import android.car.vms.IVmsPublisherClient;
import android.car.vms.IVmsSubscriberClient;
-import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
@@ -32,6 +29,7 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
+import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
@@ -42,16 +40,17 @@
import com.android.car.R;
import com.android.car.VmsPublisherService;
import com.android.car.hal.VmsHalService;
+import com.android.car.stats.CarStatsService;
+import com.android.car.stats.VmsClientLogger;
+import com.android.car.stats.VmsClientLogger.ConnectionState;
import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
import java.util.Collection;
-import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
-import java.util.concurrent.atomic.AtomicLong;
import java.util.function.IntSupplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -70,12 +69,12 @@
private final Context mContext;
private final PackageManager mPackageManager;
- private final Handler mHandler;
private final UserManager mUserManager;
private final CarUserService mUserService;
- private final CarUserManagerHelper mUserManagerHelper;
- private final int mMillisBeforeRebind;
+ private final CarStatsService mStatsService;
+ private final Handler mHandler;
private final IntSupplier mGetCallingUid;
+ private final int mMillisBeforeRebind;
private final Object mLock = new Object();
@@ -97,10 +96,7 @@
private int mCurrentUser;
@GuardedBy("mLock")
- private final Map<IBinder, SubscriberConnection> mSubscribers = new HashMap<>();
-
- @GuardedBy("mRebindCounts")
- private final Map<String, AtomicLong> mRebindCounts = new ArrayMap<>();
+ private final Map<IBinder, SubscriberConnection> mSubscribers = new ArrayMap<>();
@VisibleForTesting
final Runnable mSystemUserUnlockedListener = () -> {
@@ -111,22 +107,25 @@
};
@VisibleForTesting
- final BroadcastReceiver mUserSwitchReceiver = new BroadcastReceiver() {
+ public final CarUserService.UserCallback mUserCallback = new CarUserService.UserCallback() {
@Override
- public void onReceive(Context context, Intent intent) {
- if (DBG) Log.d(TAG, "Received " + intent);
+ public void onSwitchUser(int userId) {
synchronized (mLock) {
- int currentUserId = mUserManagerHelper.getCurrentForegroundUserId();
- if (mCurrentUser != currentUserId) {
+ if (mCurrentUser != userId) {
+ mCurrentUser = userId;
terminate(mCurrentUserClients);
terminate(mSubscribers.values().stream()
- .filter(subscriber -> subscriber.mUserId != currentUserId)
+ .filter(subscriber -> subscriber.mUserId != mCurrentUser)
.filter(subscriber -> subscriber.mUserId != UserHandle.USER_SYSTEM));
}
- mCurrentUser = currentUserId;
+ }
+ bindToUserClients();
+ }
- if (mUserManager.isUserUnlocked(mCurrentUser)) {
- bindToSystemClients();
+ @Override
+ public void onUserLockChanged(int userId, boolean unlocked) {
+ synchronized (mLock) {
+ if (mCurrentUser == userId && unlocked) {
bindToUserClients();
}
}
@@ -137,33 +136,34 @@
* Constructor for client manager.
*
* @param context Context to use for registering receivers and binding services.
- * @param brokerService Service managing the VMS publisher/subscriber state.
+ * @param statsService Service for logging client metrics.
* @param userService User service for registering system unlock listener.
- * @param userManagerHelper User manager for querying current user state.
+ * @param brokerService Service managing the VMS publisher/subscriber state.
* @param halService Service providing the HAL client interface
*/
- public VmsClientManager(Context context, VmsBrokerService brokerService,
- CarUserService userService, CarUserManagerHelper userManagerHelper,
+ public VmsClientManager(Context context, CarStatsService statsService,
+ CarUserService userService, VmsBrokerService brokerService,
VmsHalService halService) {
- this(context, brokerService, userService, userManagerHelper, halService,
- Binder::getCallingUid);
+ this(context, statsService, userService, brokerService, halService,
+ new Handler(Looper.getMainLooper()), Binder::getCallingUid);
}
@VisibleForTesting
- VmsClientManager(Context context, VmsBrokerService brokerService,
- CarUserService userService, CarUserManagerHelper userManagerHelper,
- VmsHalService halService, IntSupplier getCallingUid) {
+ VmsClientManager(Context context, CarStatsService statsService,
+ CarUserService userService, VmsBrokerService brokerService,
+ VmsHalService halService, Handler handler, IntSupplier getCallingUid) {
mContext = context;
mPackageManager = context.getPackageManager();
- mHandler = new Handler(Looper.getMainLooper());
- mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+ mStatsService = statsService;
mUserService = userService;
- mUserManagerHelper = userManagerHelper;
- mCurrentUser = mUserManagerHelper.getCurrentForegroundUserId();
+ mCurrentUser = UserHandle.USER_NULL;
mBrokerService = brokerService;
- mMillisBeforeRebind = mContext.getResources().getInteger(
- com.android.car.R.integer.millisecondsBeforeRebindToVmsPublisher);
+ mHandler = handler;
mGetCallingUid = getCallingUid;
+ mMillisBeforeRebind = context.getResources().getInteger(
+ com.android.car.R.integer.millisecondsBeforeRebindToVmsPublisher);
+
halService.setClientManager(this);
}
@@ -181,17 +181,12 @@
@Override
public void init() {
mUserService.runOnUser0Unlock(mSystemUserUnlockedListener);
-
- IntentFilter userSwitchFilter = new IntentFilter();
- userSwitchFilter.addAction(Intent.ACTION_USER_SWITCHED);
- userSwitchFilter.addAction(Intent.ACTION_USER_UNLOCKED);
- mContext.registerReceiverAsUser(mUserSwitchReceiver, UserHandle.ALL, userSwitchFilter, null,
- null);
+ mUserService.addUserCallback(mUserCallback);
}
@Override
public void release() {
- mContext.unregisterReceiver(mUserSwitchReceiver);
+ mUserService.removeUserCallback(mUserCallback);
synchronized (mLock) {
if (mHalClient != null) {
mPublisherService.onClientDisconnected(HAL_CLIENT_NAME);
@@ -204,11 +199,6 @@
@Override
public void dump(PrintWriter writer) {
- dumpMetrics(writer);
- }
-
- @Override
- public void dumpMetrics(PrintWriter writer) {
writer.println("*" + getClass().getSimpleName() + "*");
synchronized (mLock) {
writer.println("mCurrentUser:" + mCurrentUser);
@@ -224,12 +214,6 @@
writer.printf("\t%s\n", subscriber);
}
}
- synchronized (mRebindCounts) {
- writer.println("mRebindCounts:");
- for (Map.Entry<String, AtomicLong> entry : mRebindCounts.entrySet()) {
- writer.printf("\t%s: %s\n", entry.getKey(), entry.getValue());
- }
- }
}
@@ -240,7 +224,8 @@
*/
public void addSubscriber(IVmsSubscriberClient subscriberClient) {
if (subscriberClient == null) {
- Log.e(TAG, "Trying to add a null subscriber: " + getCallingPackage());
+ Log.e(TAG, "Trying to add a null subscriber: "
+ + getCallingPackage(mGetCallingUid.getAsInt()));
throw new IllegalArgumentException("subscriber cannot be null.");
}
@@ -251,13 +236,14 @@
return;
}
- int subscriberUserId = UserHandle.getUserId(mGetCallingUid.getAsInt());
+ int callingUid = mGetCallingUid.getAsInt();
+ int subscriberUserId = UserHandle.getUserId(callingUid);
if (subscriberUserId != mCurrentUser && subscriberUserId != UserHandle.USER_SYSTEM) {
throw new SecurityException("Caller must be foreground user or system");
}
SubscriberConnection subscriber = new SubscriberConnection(
- subscriberClient, getCallingPackage(), subscriberUserId);
+ subscriberClient, callingUid, getCallingPackage(callingUid), subscriberUserId);
if (DBG) Log.d(TAG, "Registering subscriber: " + subscriber);
try {
subscriberBinder.linkToDeath(subscriber, 0);
@@ -286,9 +272,21 @@
* Returns all active subscriber clients.
*/
public Collection<IVmsSubscriberClient> getAllSubscribers() {
- return mSubscribers.values().stream()
- .map(subscriber -> subscriber.mClient)
- .collect(Collectors.toList());
+ synchronized (mLock) {
+ return mSubscribers.values().stream()
+ .map(subscriber -> subscriber.mClient)
+ .collect(Collectors.toList());
+ }
+ }
+
+ /**
+ * Gets the application UID associated with a subscriber client.
+ */
+ public int getSubscriberUid(IVmsSubscriberClient subscriberClient) {
+ synchronized (mLock) {
+ SubscriberConnection subscriber = mSubscribers.get(subscriberClient.asBinder());
+ return subscriber != null ? subscriber.mUid : Process.INVALID_UID;
+ }
}
/**
@@ -303,9 +301,6 @@
/**
* Registers the HAL client connections.
- *
- * @param publisherClient
- * @param subscriberClient
*/
public void onHalConnected(IVmsPublisherClient publisherClient,
IVmsSubscriberClient subscriberClient) {
@@ -313,9 +308,11 @@
mHalClient = publisherClient;
mPublisherService.onClientConnected(HAL_CLIENT_NAME, mHalClient);
mSubscribers.put(subscriberClient.asBinder(),
- new SubscriberConnection(subscriberClient, HAL_CLIENT_NAME,
+ new SubscriberConnection(subscriberClient, Process.myUid(), HAL_CLIENT_NAME,
UserHandle.USER_SYSTEM));
}
+ mStatsService.getVmsClientLogger(Process.myUid())
+ .logConnectionState(ConnectionState.CONNECTED);
}
/**
@@ -325,14 +322,13 @@
synchronized (mLock) {
if (mHalClient != null) {
mPublisherService.onClientDisconnected(HAL_CLIENT_NAME);
+ mStatsService.getVmsClientLogger(Process.myUid())
+ .logConnectionState(ConnectionState.DISCONNECTED);
}
mHalClient = null;
terminate(mSubscribers.values().stream()
.filter(subscriber -> HAL_CLIENT_NAME.equals(subscriber.mPackageName)));
}
- synchronized (mRebindCounts) {
- mRebindCounts.computeIfAbsent(HAL_CLIENT_NAME, k -> new AtomicLong()).incrementAndGet();
- }
}
private void dumpConnections(PrintWriter writer,
@@ -359,15 +355,24 @@
}
private void bindToUserClients() {
+ bindToSystemClients(); // Bind system clients on user switch, if they are not already bound.
synchronized (mLock) {
+ if (mCurrentUser == UserHandle.USER_NULL) {
+ Log.e(TAG, "Unknown user in foreground.");
+ return;
+ }
// To avoid the risk of double-binding, clients running as the system user must only
// ever be bound in bindToSystemClients().
- // In a headless multi-user system, the system user will never be in the foreground.
if (mCurrentUser == UserHandle.USER_SYSTEM) {
Log.e(TAG, "System user in foreground. Userspace clients will not be bound.");
return;
}
+ if (!mUserManager.isUserUnlockingOrUnlocked(mCurrentUser)) {
+ Log.i(TAG, "Waiting for foreground user " + mCurrentUser + " to be unlocked.");
+ return;
+ }
+
String[] clientNames = mContext.getResources().getStringArray(
R.array.vmsPublisherUserClients);
Log.i(TAG, "Attempting to bind " + clientNames.length + " user client(s)");
@@ -400,13 +405,17 @@
return;
}
+ VmsClientLogger statsLog = mStatsService.getVmsClientLogger(
+ UserHandle.getUid(userHandle.getIdentifier(), serviceInfo.applicationInfo.uid));
+
if (!Car.PERMISSION_BIND_VMS_CLIENT.equals(serviceInfo.permission)) {
Log.e(TAG, "Client service: " + clientName
+ " does not require " + Car.PERMISSION_BIND_VMS_CLIENT + " permission");
+ statsLog.logConnectionState(ConnectionState.CONNECTION_ERROR);
return;
}
- PublisherConnection connection = new PublisherConnection(name, userHandle);
+ PublisherConnection connection = new PublisherConnection(name, userHandle, statsLog);
if (connection.bind()) {
Log.i(TAG, "Client bound: " + connection);
connectionMap.put(clientName, connection);
@@ -424,15 +433,17 @@
private final ComponentName mName;
private final UserHandle mUser;
private final String mFullName;
+ private final VmsClientLogger mStatsLog;
private boolean mIsBound = false;
private boolean mIsTerminated = false;
private boolean mRebindScheduled = false;
private IVmsPublisherClient mClientService;
- PublisherConnection(ComponentName name, UserHandle user) {
+ PublisherConnection(ComponentName name, UserHandle user, VmsClientLogger statsLog) {
mName = name;
mUser = user;
mFullName = mName.flattenToString() + " U=" + mUser.getIdentifier();
+ mStatsLog = statsLog;
}
synchronized boolean bind() {
@@ -442,6 +453,7 @@
if (mIsTerminated) {
return false;
}
+ mStatsLog.logConnectionState(ConnectionState.CONNECTING);
if (DBG) Log.d(TAG, "binding: " + mFullName);
Intent intent = new Intent();
@@ -453,6 +465,10 @@
Log.e(TAG, "While binding " + mFullName, e);
}
+ if (!mIsBound) {
+ mStatsLog.logConnectionState(ConnectionState.CONNECTION_ERROR);
+ }
+
return mIsBound;
}
@@ -496,23 +512,20 @@
// If the client is not currently bound, unbind() will have no effect.
unbind();
bind();
- synchronized (mRebindCounts) {
- mRebindCounts.computeIfAbsent(mName.getPackageName(), k -> new AtomicLong())
- .incrementAndGet();
- }
}
synchronized void terminate() {
if (DBG) Log.d(TAG, "terminating: " + mFullName);
mIsTerminated = true;
- notifyOnDisconnect();
+ notifyOnDisconnect(ConnectionState.TERMINATED);
unbind();
}
- synchronized void notifyOnDisconnect() {
+ synchronized void notifyOnDisconnect(int connectionState) {
if (mClientService != null) {
mPublisherService.onClientDisconnected(mFullName);
mClientService = null;
+ mStatsLog.logConnectionState(connectionState);
}
}
@@ -521,19 +534,20 @@
if (DBG) Log.d(TAG, "onServiceConnected: " + mFullName);
mClientService = IVmsPublisherClient.Stub.asInterface(service);
mPublisherService.onClientConnected(mFullName, mClientService);
+ mStatsLog.logConnectionState(ConnectionState.CONNECTED);
}
@Override
public void onServiceDisconnected(ComponentName name) {
if (DBG) Log.d(TAG, "onServiceDisconnected: " + mFullName);
- notifyOnDisconnect();
+ notifyOnDisconnect(ConnectionState.DISCONNECTED);
scheduleRebind();
}
@Override
public void onBindingDied(ComponentName name) {
if (DBG) Log.d(TAG, "onBindingDied: " + mFullName);
- notifyOnDisconnect();
+ notifyOnDisconnect(ConnectionState.DISCONNECTED);
scheduleRebind();
}
@@ -549,8 +563,8 @@
}
// If we're in a binder call, returns back the package name of the caller of the binder call.
- private String getCallingPackage() {
- String packageName = mPackageManager.getNameForUid(mGetCallingUid.getAsInt());
+ private String getCallingPackage(int uid) {
+ String packageName = mPackageManager.getNameForUid(uid);
if (packageName == null) {
return UNKNOWN_PACKAGE;
} else {
@@ -560,12 +574,14 @@
private class SubscriberConnection implements IBinder.DeathRecipient {
private final IVmsSubscriberClient mClient;
+ private final int mUid;
private final String mPackageName;
private final int mUserId;
- SubscriberConnection(IVmsSubscriberClient subscriberClient, String packageName,
+ SubscriberConnection(IVmsSubscriberClient subscriberClient, int uid, String packageName,
int userId) {
mClient = subscriberClient;
+ mUid = uid;
mPackageName = packageName;
mUserId = userId;
}
diff --git a/tests/BugReportApp/Android.mk b/tests/BugReportApp/Android.mk
index f948b82..186fe4b 100644
--- a/tests/BugReportApp/Android.mk
+++ b/tests/BugReportApp/Android.mk
@@ -37,7 +37,8 @@
LOCAL_DEX_PREOPT := false
LOCAL_JAVA_LIBRARIES += \
- android.car
+ android.car \
+ br_google_auto_value_target
LOCAL_STATIC_JAVA_LIBRARIES := \
androidx.recyclerview_recyclerview \
@@ -56,6 +57,11 @@
LOCAL_REQUIRED_MODULES := privapp_whitelist_com.google.android.car.bugreport
+# Explicitly define annotation processors even if javac can find them from
+# LOCAL_STATIC_JAVA_LIBRARIES.
+LOCAL_ANNOTATION_PROCESSORS := br_google_auto_value
+LOCAL_ANNOTATION_PROCESSOR_CLASSES := com.google.auto.value.processor.AutoValueProcessor
+
include $(BUILD_PACKAGE)
# ==== prebuilt library ========================
@@ -76,3 +82,28 @@
br_apache_commons:$(COMMON_LIBS_PATH)/org/eclipse/tycho/tycho-bundles-external/0.18.1/eclipse/plugins/org.apache.commons.codec_1.4.0.v201209201156.jar
include $(BUILD_MULTI_PREBUILT)
+
+# Following shenanigans are needed for LOCAL_ANNOTATION_PROCESSORS.
+
+# ==== prebuilt host libraries ========================
+include $(CLEAR_VARS)
+
+LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
+ br_google_auto_value:../../../../../prebuilts/tools/common/m2/repository/com/google/auto/value/auto-value/1.5.2/auto-value-1.5.2.jar
+
+include $(BUILD_HOST_PREBUILT)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_CLASS := JAVA_LIBRARIES
+LOCAL_MODULE := br_google_auto_value_target
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := ../../../../../prebuilts/tools/common/m2/repository/com/google/auto/value/auto-value/1.5.2/auto-value-1.5.2.jar
+LOCAL_UNINSTALLABLE_MODULE := true
+
+include $(BUILD_PREBUILT)
+
+include $(CLEAR_VARS)
+
+# Use the following include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/BugReportApp/AndroidManifest.xml b/tests/BugReportApp/AndroidManifest.xml
index 90958c7..d0b4cf3 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="8"
- android:versionName="1.6.0">
+ android:versionCode="11"
+ android:versionName="1.7.0">
<uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
@@ -29,8 +29,13 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.DUMP"/>
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
+ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
- <application android:label="@string/app_name" android:icon="@drawable/ic_launcher">
+ <application android:label="@string/app_name"
+ android:icon="@drawable/ic_launcher"
+ android:requestLegacyExternalStorage="true">
<activity android:name=".BugReportInfoActivity"
android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
android:exported="true"
@@ -42,15 +47,26 @@
</intent-filter>
</activity>
- <!-- singleInstance allows starting bugreport dialog when BugReportInfoActivity is open. -->
+ <!--
+ singleInstance allows starting bugreport dialog when BugReportInfoActivity is open.
+ -->
<activity android:name=".BugReportActivity"
android:theme="@android:style/Theme.DeviceDefault.Dialog"
android:exported="true"
- android:launchMode="singleInstance">
+ android:launchMode="singleInstance"
+ android:excludeFromRecents="true">
+ <meta-data android:name="distractionOptimized" android:value="true"/>
+ <intent-filter>
+ <action android:name="com.google.android.car.bugreport.action.START_SILENT"/>
+ </intent-filter>
</activity>
<service android:name=".BugReportService"
- android:exported="false"/>
+ android:exported="true">
+ <intent-filter>
+ <action android:name="com.google.android.car.bugreport.action.START_SILENT"/>
+ </intent-filter>
+ </service>
<service android:name="com.google.android.car.bugreport.UploadJob"
android:permission="android.permission.BIND_JOB_SERVICE"
diff --git a/tests/BugReportApp/README.md b/tests/BugReportApp/README.md
index 2dac8e9..7ed9375 100644
--- a/tests/BugReportApp/README.md
+++ b/tests/BugReportApp/README.md
@@ -38,16 +38,31 @@
[overlayed](https://source.android.com/setup/develop/new-device#use-resource-overlays)
for specific products.
+### Config
+
+Configs are defined in `Config.java`.
+
### System Properties
-- `android.car.bugreport.disableautoupload` - set it to `true` to disable auto-upload to Google
- Cloud, and allow users to manually upload or copy the bugreports to flash drive.
+- `android.car.bugreport.enableautoupload` - please see Config#ENABLE_AUTO_UPLOAD to learn more.
+- `android.car.bugreport.force_enable` - set it `true` to enable bugreport app on **all builds**.
### Upload configuration
BugReport app uses `res/raw/gcs_credentials.json` for authentication and
`res/values/configs.xml` for obtaining GCS bucket name.
+## Starting bugreporting
+
+The app supports following intents:
+
+1. `adb shell am start com.google.android.car.bugreport/.BugReportActivity`
+ - generates `MetaBugReport.Type.INTERACTIVE` bug report, shows audio message dialog before
+ collecting bugreport.
+2. `adb shell am start-foreground-service -a com.google.android.car.bugreport.action.START_SILENT com.google.android.car.bugreport/.BugReportService`
+ - generates `MetaBugReport.Type.SILENT` bug report, without audio message. It shows audio dialog
+ after collecting bugreport.
+
## Testing
### Manually testing the app using the test script
diff --git a/tests/BugReportApp/res/layout/bug_info_view.xml b/tests/BugReportApp/res/layout/bug_info_view.xml
index 3736c00..199b368 100644
--- a/tests/BugReportApp/res/layout/bug_info_view.xml
+++ b/tests/BugReportApp/res/layout/bug_info_view.xml
@@ -26,99 +26,68 @@
android:orientation="vertical">
<TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="TITLE"
- android:textColor="@android:color/holo_green_light"
- android:textSize="28sp" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="USER"
- android:textColor="@android:color/holo_red_dark"
- android:textSize="28sp" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="BUG TIME"
- android:textColor="@android:color/holo_red_light"
- android:textSize="28sp" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="STATUS"
- android:textColor="@android:color/holo_orange_light"
- android:textSize="28sp" />
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="OTHER"
- android:textColor="@android:color/holo_blue_dark"
- android:textSize="28sp" />
-
- <Button
- android:id="@+id/bug_info_upload_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/bug_report_user_action_button_padding"
- android:textSize="@dimen/bug_report_button_text_size"
- android:text="@string/bugreport_upload_button_text" />
-
- </LinearLayout>
-
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:paddingLeft="10dp"
- android:orientation="vertical">
-
- <TextView
android:id="@+id/bug_info_row_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
- android:textSize="28sp" />
+ android:textColor="@*android:color/car_yellow_500"
+ android:textSize="@dimen/bug_report_default_text_size" />
-
- <TextView
- android:id="@+id/bug_info_row_user"
+ <LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textSize="28sp" />
-
-
- <TextView
- android:id="@+id/bug_info_row_timestamp"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="28sp" />
-
-
- <TextView
- android:id="@+id/bug_info_row_status"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textSize="28sp" />
-
+ android:orientation="horizontal">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="@dimen/bug_report_horizontal_layout_children_margin"
+ android:text="@string/bugreport_info_status"
+ android:textSize="@dimen/bug_report_default_text_size" />
+ <TextView
+ android:id="@+id/bug_info_row_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/bug_report_default_text_size" />
+ </LinearLayout>
<TextView
android:id="@+id/bug_info_row_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textSize="28sp" />
+ android:visibility="gone"
+ android:textSize="@dimen/bug_report_default_text_size" />
- <Button
- android:id="@+id/bug_info_move_button"
+ <LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/bug_report_user_action_button_padding"
- android:textSize="@dimen/bug_report_button_text_size"
- android:text="@string/bugreport_move_button_text" />
-
+ android:orientation="horizontal">
+ <Button
+ android:id="@+id/bug_info_add_audio_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/bug_report_user_action_button_padding"
+ android:layout_marginRight="@dimen/bug_report_horizontal_layout_children_margin"
+ android:visibility="gone"
+ android:textSize="@dimen/bug_report_button_text_size"
+ android:text="@string/bugreport_add_audio_button_text" />
+ <Button
+ android:id="@+id/bug_info_upload_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/bug_report_user_action_button_padding"
+ android:layout_marginRight="@dimen/bug_report_horizontal_layout_children_margin"
+ android:visibility="gone"
+ android:textSize="@dimen/bug_report_button_text_size"
+ android:text="@string/bugreport_upload_button_text" />
+ <Button
+ android:id="@+id/bug_info_move_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/bug_report_user_action_button_padding"
+ android:visibility="gone"
+ android:textSize="@dimen/bug_report_button_text_size"
+ android:text="@string/bugreport_move_button_text" />
+ </LinearLayout>
</LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/tests/BugReportApp/res/layout/bug_report_activity.xml b/tests/BugReportApp/res/layout/bug_report_activity.xml
index d5dce22..888c628 100644
--- a/tests/BugReportApp/res/layout/bug_report_activity.xml
+++ b/tests/BugReportApp/res/layout/bug_report_activity.xml
@@ -16,15 +16,16 @@
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@color/bugreport_background"
+ android:padding="@dimen/bug_report_padding"
+ android:orientation="vertical">
<LinearLayout
android:id="@+id/submit_bug_report_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:padding="@dimen/bug_report_padding"
- android:background="@color/bugreport_background"
android:visibility="gone"
android:orientation="vertical">
<TextView
@@ -34,6 +35,16 @@
android:textColor="@color/bugreport_text"
android:gravity="center"
android:text="@string/bugreport_dialog_title"/>
+ <TextView
+ android:id="@+id/bug_report_add_audio_to_existing"
+ android:layout_marginTop="@dimen/bug_report_voice_recording_margin_top"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="@color/bugreport_text"
+ android:gravity="center"
+ android:visibility="gone"
+ android:text="@string/bugreport_dialog_add_audio_to_existing"/>
<com.google.android.car.bugreport.VoiceRecordingView
android:id="@+id/voice_recording_view"
android:layout_marginTop="@dimen/bug_report_voice_recording_margin_top"
@@ -65,23 +76,12 @@
android:layout_marginTop="@dimen/bug_report_button_margin_top"
android:padding="@dimen/bug_report_secondary_button_padding"
android:text="@string/bugreport_dialog_cancel"/>
- <Button
- android:id="@+id/button_show_bugreports"
- style="@style/standard_button"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/bug_report_button_margin_top"
- android:padding="@dimen/bug_report_small_button_padding"
- android:visibility="gone"
- android:text="@string/bugreport_dialog_show_bugreports"/>
</LinearLayout>
<LinearLayout
android:id="@+id/in_progress_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="@color/bugreport_background"
- android:padding="@dimen/bug_report_padding"
android:visibility="gone"
android:orientation="vertical">
<TextView
@@ -115,4 +115,14 @@
android:padding="@dimen/bug_report_secondary_button_padding"
android:text="@string/bugreport_dialog_close"/>
</LinearLayout>
+
+ <Button
+ android:id="@+id/button_show_bugreports"
+ style="@style/standard_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/bug_report_button_margin_top"
+ android:padding="@dimen/bug_report_small_button_padding"
+ android:visibility="gone"
+ android:text="@string/bugreport_dialog_show_bugreports"/>
</LinearLayout>
diff --git a/tests/BugReportApp/res/values/dimens.xml b/tests/BugReportApp/res/values/dimens.xml
index 4a5f270..7f7967f 100644
--- a/tests/BugReportApp/res/values/dimens.xml
+++ b/tests/BugReportApp/res/values/dimens.xml
@@ -19,6 +19,8 @@
<!-- Margin between edge of BugReportActivity dialog and content -->
<dimen name="bug_report_padding">30dp</dimen>
+ <dimen name="bug_report_default_text_size">28dp</dimen>
+
<!-- VoiceRecordingView dimensions -->
<dimen name="bug_report_voice_recording_margin_top">20dp</dimen>
<dimen name="bug_report_voice_recording_height">40dp</dimen>
@@ -34,4 +36,6 @@
<!-- ProgressBar dimensions -->
<dimen name="bug_report_progress_bar_margin_top">32dp</dimen>
+ <!-- Horizontal layout children margins. -->
+ <dimen name="bug_report_horizontal_layout_children_margin">12dp</dimen>
</resources>
diff --git a/tests/BugReportApp/res/values/strings.xml b/tests/BugReportApp/res/values/strings.xml
index bca00fa..b3b125a 100644
--- a/tests/BugReportApp/res/values/strings.xml
+++ b/tests/BugReportApp/res/values/strings.xml
@@ -15,35 +15,43 @@
limitations under the License.
-->
<resources>
- <string name="app_name" translatable="false">Bug Report</string>
+ <string name="app_name">Bug Report</string>
- <string name="bugreport_info_quit" translatable="false">Close</string>
- <string name="bugreport_info_start" translatable="false">Start Bug Report</string>
+ <string name="bugreport_info_quit">Close</string>
+ <string name="bugreport_info_start">Start Bug Report</string>
+ <string name="bugreport_info_status">Status:</string>
- <string name="bugreport_dialog_submit" translatable="false">Submit</string>
- <string name="bugreport_dialog_cancel" translatable="false">Cancel</string>
- <string name="bugreport_dialog_show_bugreports" translatable="false">Show Bug Reports</string>
- <string name="bugreport_dialog_close" translatable="false">Close</string>
- <string name="bugreport_dialog_title" translatable="false">Speak & Describe The Issue</string>
- <string name="bugreport_dialog_recording_finished" translatable="false">Recording finished</string>
- <string name="bugreport_dialog_in_progress_title" translatable="false">A bug report is already being collected</string>
- <string name="bugreport_dialog_in_progress_title_finished" translatable="false">A bug report has been collected</string>
- <string name="bugreport_move_button_text" translatable="false">Move</string>
- <string name="bugreport_upload_button_text" translatable="false">Upload</string>
+ <string name="bugreport_dialog_submit">Submit</string>
+ <string name="bugreport_dialog_cancel">Cancel</string>
+ <!-- A button: uploads bugreport with recorded audio message. -->
+ <string name="bugreport_dialog_upload">Upload</string>
+ <!-- A button: saves recorded audio message. -->
+ <string name="bugreport_dialog_save">Save</string>
+ <string name="bugreport_dialog_show_bugreports">Show Bug Reports</string>
+ <string name="bugreport_dialog_close">Close</string>
+ <string name="bugreport_dialog_title">Speak & Describe The Issue</string>
+ <!-- %s is the timestamp of a bugreport. -->
+ <string name="bugreport_dialog_add_audio_to_existing">Audio message for bug report at %s</string>
+ <string name="bugreport_dialog_recording_finished">Recording finished</string>
+ <string name="bugreport_dialog_in_progress_title">A bug report is already being collected</string>
+ <string name="bugreport_dialog_in_progress_title_finished">A bug report has been collected</string>
+ <!-- A button to add audio message to the bugreport. It will show Save button on the dialog. -->
+ <string name="bugreport_add_audio_button_text">Add Audio</string>
+ <!-- A button to add audio message to the bugreport; it will show Upload button on the dialog. -->
+ <string name="bugreport_add_audio_upload_button_text">Add Audio & Upload</string>
+ <string name="bugreport_move_button_text">Move to USB</string>
+ <string name="bugreport_upload_button_text">Upload</string>
+ <string name="bugreport_upload_gcs_button_text">Upload to GCS</string>
- <string name="toast_permissions_denied" translatable="false">Please grant permissions</string>
- <string name="toast_bug_report_in_progress" translatable="false">Bug report already being collected</string>
- <string name="toast_timed_out" translatable="false">Timed out, cancelling</string>
- <string name="toast_status_failed" translatable="false">Bug report failed</string>
- <string name="toast_status_finished" translatable="false">Bug report finished</string>
- <string name="toast_status_pending_upload" translatable="false">Bug report ready for upload</string>
- <string name="toast_status_screencap_failed" translatable="false">Screen capture failed</string>
- <string name="toast_status_dump_state_failed" translatable="false">Dump state failed</string>
+ <string name="toast_permissions_denied">Please grant permissions</string>
+ <string name="toast_bug_report_in_progress">Bug report already being collected</string>
+ <string name="toast_bug_report_started">Bug reporting is started</string>
+ <string name="toast_status_failed">Bug report failed</string>
+ <string name="toast_status_screencap_failed">Screen capture failed</string>
+ <string name="toast_status_dump_state_failed">Dump state failed</string>
<!-- Notification strings -->
- <string name="notification_bugreport_in_progress" translatable="false">Bug report is in progress</string>
- <string name="notification_bugreport_finished_title" translatable="false">Bug report is collected</string>
- <string name="notification_bugreport_manual_upload_finished_text" translatable="false">Please upload it when the car is parked</string>
- <string name="notification_bugreport_auto_upload_finished_text" translatable="false">The report will be upload automatically</string>
- <string name="notification_bugreport_channel_name" translatable="false">Bug report status channel</string>
+ <string name="notification_bugreport_in_progress">Bug report is in progress</string>
+ <string name="notification_bugreport_finished_title">Bug report is collected</string>
+ <string name="notification_bugreport_channel_name">Bug report status channel</string>
</resources>
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugInfoAdapter.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugInfoAdapter.java
index ad2e38f..f63d937 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugInfoAdapter.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugInfoAdapter.java
@@ -23,63 +23,77 @@
import androidx.recyclerview.widget.RecyclerView;
+import java.util.ArrayList;
import java.util.List;
+/**
+ * Shows bugreport title, status, status message and user action buttons. "Upload to Google" button
+ * is enabled when the status is {@link Status#STATUS_PENDING_USER_ACTION}, "Move to USB" button is
+ * enabled only when status is {@link Status#STATUS_PENDING_USER_ACTION} and USB device is plugged
+ * in.
+ */
public class BugInfoAdapter extends RecyclerView.Adapter<BugInfoAdapter.BugInfoViewHolder> {
-
static final int BUTTON_TYPE_UPLOAD = 0;
static final int BUTTON_TYPE_MOVE = 1;
+ static final int BUTTON_TYPE_ADD_AUDIO = 2;
/** Provides a handler for click events*/
interface ItemClickedListener {
- /** onItemClicked handles click events differently depending on provided buttonType and
- * uses additional information provided in metaBugReport. */
- void onItemClicked(int buttonType, MetaBugReport metaBugReport);
+ /**
+ * Handles click events differently depending on provided buttonType and
+ * uses additional information provided in metaBugReport.
+ *
+ * @param buttonType One of {@link #BUTTON_TYPE_UPLOAD}, {@link #BUTTON_TYPE_MOVE} or
+ * {@link #BUTTON_TYPE_ADD_AUDIO}.
+ * @param metaBugReport Selected bugreport.
+ * @param holder ViewHolder of the clicked item.
+ */
+ void onItemClicked(int buttonType, MetaBugReport metaBugReport, BugInfoViewHolder holder);
}
/**
* Reference to each bug report info views.
*/
- public static class BugInfoViewHolder extends RecyclerView.ViewHolder {
+ static class BugInfoViewHolder extends RecyclerView.ViewHolder {
/** Title view */
- public TextView titleView;
-
- /** User view */
- public TextView userView;
-
- /** TimeStamp View */
- public TextView timestampView;
+ TextView mTitleView;
/** Status View */
- public TextView statusView;
+ TextView mStatusView;
/** Message View */
- public TextView messageView;
+ TextView mMessageView;
/** Move Button */
- public Button moveButton;
+ Button mMoveButton;
/** Upload Button */
- public Button uploadButton;
+ Button mUploadButton;
+
+ /** Add Audio Button */
+ Button mAddAudioButton;
BugInfoViewHolder(View v) {
super(v);
- titleView = itemView.findViewById(R.id.bug_info_row_title);
- userView = itemView.findViewById(R.id.bug_info_row_user);
- timestampView = itemView.findViewById(R.id.bug_info_row_timestamp);
- statusView = itemView.findViewById(R.id.bug_info_row_status);
- messageView = itemView.findViewById(R.id.bug_info_row_message);
- moveButton = itemView.findViewById(R.id.bug_info_move_button);
- uploadButton = itemView.findViewById(R.id.bug_info_upload_button);
+ mTitleView = itemView.findViewById(R.id.bug_info_row_title);
+ mStatusView = itemView.findViewById(R.id.bug_info_row_status);
+ mMessageView = itemView.findViewById(R.id.bug_info_row_message);
+ mMoveButton = itemView.findViewById(R.id.bug_info_move_button);
+ mUploadButton = itemView.findViewById(R.id.bug_info_upload_button);
+ mAddAudioButton = itemView.findViewById(R.id.bug_info_add_audio_button);
}
}
- private final List<MetaBugReport> mDataset;
+ private List<MetaBugReport> mDataset;
private final ItemClickedListener mItemClickedListener;
+ private final Config mConfig;
- BugInfoAdapter(List<MetaBugReport> dataSet, ItemClickedListener itemClickedListener) {
- mDataset = dataSet;
+ BugInfoAdapter(ItemClickedListener itemClickedListener, Config config) {
mItemClickedListener = itemClickedListener;
+ mDataset = new ArrayList<>();
+ mConfig = config;
+ // Allow RecyclerView to efficiently update UI; getItemId() is implemented below.
+ setHasStableIds(true);
}
@Override
@@ -93,22 +107,74 @@
@Override
public void onBindViewHolder(BugInfoViewHolder holder, int position) {
MetaBugReport bugreport = mDataset.get(position);
- holder.titleView.setText(mDataset.get(position).getTitle());
- holder.userView.setText(mDataset.get(position).getUsername());
- holder.timestampView.setText(mDataset.get(position).getTimestamp());
- holder.statusView.setText(Status.toString(mDataset.get(position).getStatus()));
- holder.messageView.setText(mDataset.get(position).getStatusMessage());
- if (bugreport.getStatus() == Status.STATUS_PENDING_USER_ACTION.getValue()
- || bugreport.getStatus() == Status.STATUS_MOVE_FAILED.getValue()
- || bugreport.getStatus() == Status.STATUS_UPLOAD_FAILED.getValue()) {
- holder.moveButton.setOnClickListener(
- view -> mItemClickedListener.onItemClicked(BUTTON_TYPE_MOVE, bugreport));
- holder.uploadButton.setOnClickListener(
- view -> mItemClickedListener.onItemClicked(BUTTON_TYPE_UPLOAD, bugreport));
+ holder.mTitleView.setText(bugreport.getTitle());
+ holder.mStatusView.setText(Status.toString(bugreport.getStatus()));
+ holder.mMessageView.setText(bugreport.getStatusMessage());
+ if (bugreport.getStatusMessage().isEmpty()) {
+ holder.mMessageView.setVisibility(View.GONE);
} else {
- holder.moveButton.setEnabled(false);
- holder.uploadButton.setEnabled(false);
+ holder.mMessageView.setVisibility(View.VISIBLE);
}
+ boolean enableUserActionButtons =
+ bugreport.getStatus() == Status.STATUS_PENDING_USER_ACTION.getValue()
+ || bugreport.getStatus() == Status.STATUS_MOVE_FAILED.getValue()
+ || bugreport.getStatus() == Status.STATUS_UPLOAD_FAILED.getValue();
+ if (enableUserActionButtons) {
+ holder.mMoveButton.setEnabled(true);
+ holder.mMoveButton.setVisibility(View.VISIBLE);
+ holder.mMoveButton.setOnClickListener(
+ view -> mItemClickedListener.onItemClicked(BUTTON_TYPE_MOVE, bugreport,
+ holder));
+ } else {
+ holder.mMoveButton.setEnabled(false);
+ holder.mMoveButton.setVisibility(View.GONE);
+ }
+ // Always enable upload to GCS button, because the app is enabled only for userdebug,
+ // and sometimes Config might not be properly set.
+ if (enableUserActionButtons) {
+ holder.mUploadButton.setText(R.string.bugreport_upload_gcs_button_text);
+ holder.mUploadButton.setEnabled(true);
+ holder.mUploadButton.setVisibility(View.VISIBLE);
+ holder.mUploadButton.setOnClickListener(
+ view -> mItemClickedListener.onItemClicked(BUTTON_TYPE_UPLOAD, bugreport,
+ holder));
+ } else {
+ holder.mUploadButton.setVisibility(View.GONE);
+ holder.mUploadButton.setEnabled(false);
+ }
+ if (bugreport.getStatus() == Status.STATUS_AUDIO_PENDING.getValue()) {
+ if (mConfig.getAutoUpload()) {
+ holder.mAddAudioButton.setText(R.string.bugreport_add_audio_upload_button_text);
+ } else {
+ holder.mAddAudioButton.setText(R.string.bugreport_add_audio_button_text);
+ }
+ holder.mAddAudioButton.setEnabled(true);
+ holder.mAddAudioButton.setVisibility(View.VISIBLE);
+ holder.mAddAudioButton.setOnClickListener(view ->
+ mItemClickedListener.onItemClicked(BUTTON_TYPE_ADD_AUDIO, bugreport, holder));
+ } else {
+ holder.mAddAudioButton.setEnabled(false);
+ holder.mAddAudioButton.setVisibility(View.GONE);
+ }
+ }
+
+ /** Sets dataSet; it copies the list, because it modifies it in this adapter. */
+ void setDataset(List<MetaBugReport> bugReports) {
+ mDataset = new ArrayList<>(bugReports);
+ notifyDataSetChanged();
+ }
+
+ /** Update a bug report in the data set. */
+ void updateBugReportInDataSet(MetaBugReport bugReport, int position) {
+ if (position != RecyclerView.NO_POSITION) {
+ mDataset.set(position, bugReport);
+ notifyItemChanged(position);
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mDataset.get(position).getId();
}
@Override
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 456192e..a66ce0f 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportActivity.java
@@ -15,7 +15,6 @@
*/
package com.google.android.car.bugreport;
-import static com.google.android.car.bugreport.BugReportService.EXTRA_META_BUG_REPORT;
import static com.google.android.car.bugreport.BugReportService.MAX_PROGRESS_VALUE;
import android.Manifest;
@@ -25,6 +24,7 @@
import android.car.drivingstate.CarDrivingStateEvent;
import android.car.drivingstate.CarDrivingStateManager;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
@@ -41,14 +41,19 @@
import android.util.Log;
import android.view.View;
import android.view.Window;
+import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
+import com.google.common.base.Preconditions;
+import com.google.common.io.ByteStreams;
+
import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.util.Arrays;
import java.util.Date;
import java.util.Random;
@@ -65,33 +70,52 @@
public class BugReportActivity extends Activity {
private static final String TAG = BugReportActivity.class.getSimpleName();
+ /** Starts silent (no audio message recording) bugreporting. */
+ private static final String ACTION_START_SILENT =
+ "com.google.android.car.bugreport.action.START_SILENT";
+
+ /** This is deprecated action. Please start SILENT bugreport using {@link BugReportService}. */
+ private static final String ACTION_ADD_AUDIO =
+ "com.google.android.car.bugreport.action.ADD_AUDIO";
+
private static final int VOICE_MESSAGE_MAX_DURATION_MILLIS = 60 * 1000;
private static final int AUDIO_PERMISSIONS_REQUEST_ID = 1;
- private static final DateFormat BUG_REPORT_TIMESTAMP_DATE_FORMAT =
- new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
+ private static final String EXTRA_BUGREPORT_ID = "bugreport-id";
+ /**
+ * NOTE: mRecorder related messages are cleared when the activity finishes.
+ */
private final Handler mHandler = new Handler(Looper.getMainLooper());
+ /** Look up string length, e.g. [ABCDEF]. */
+ static final int LOOKUP_STRING_LENGTH = 6;
+
private TextView mInProgressTitleText;
private ProgressBar mProgressBar;
private TextView mProgressText;
+ private TextView mAddAudioText;
private VoiceRecordingView mVoiceRecordingView;
private View mVoiceRecordingFinishedView;
private View mSubmitBugReportLayout;
private View mInProgressLayout;
private View mShowBugReportsButton;
+ private Button mSubmitButton;
private boolean mBound;
private boolean mAudioRecordingStarted;
- private boolean mBugReportServiceStarted;
+ private boolean mIsNewBugReport;
+ private boolean mIsOnActivityStartedWithBugReportServiceBoundCalled;
+ private boolean mIsSubmitButtonClicked;
private BugReportService mService;
private MediaRecorder mRecorder;
private MetaBugReport mMetaBugReport;
+ private File mAudioFile;
private Car mCar;
private CarDrivingStateManager mDrivingStateManager;
private AudioManager mAudioManager;
private AudioFocusRequest mLastAudioFocusRequest;
+ private Config mConfig;
/** Defines callbacks for service binding, passed to bindService() */
private ServiceConnection mConnection = new ServiceConnection() {
@@ -100,7 +124,7 @@
BugReportService.ServiceBinder binder = (BugReportService.ServiceBinder) service;
mService = binder.getService();
mBound = true;
- startAudioMessageRecording();
+ onActivityStartedWithBugReportServiceBound();
}
@Override
@@ -110,7 +134,7 @@
}
};
- private final ServiceConnection mServiceConnection = new ServiceConnection() {
+ private final ServiceConnection mCarServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
try {
@@ -118,6 +142,8 @@
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);
}
@@ -128,30 +154,24 @@
}
};
+ /**
+ * Builds an intent that starts {@link BugReportActivity} to add audio message to the existing
+ * bug report.
+ */
+ static Intent buildAddAudioIntent(Context context, MetaBugReport bug) {
+ Intent addAudioIntent = new Intent(context, BugReportActivity.class);
+ addAudioIntent.setAction(ACTION_ADD_AUDIO);
+ addAudioIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ addAudioIntent.putExtra(EXTRA_BUGREPORT_ID, bug.getId());
+ return addAudioIntent;
+ }
+
@Override
public void onCreate(Bundle savedInstanceState) {
+ Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled.");
+
super.onCreate(savedInstanceState);
-
requestWindowFeature(Window.FEATURE_NO_TITLE);
- setContentView(R.layout.bug_report_activity);
-
- mInProgressTitleText = findViewById(R.id.in_progress_title_text);
- mProgressBar = findViewById(R.id.progress_bar);
- mProgressText = findViewById(R.id.progress_text);
- mVoiceRecordingView = findViewById(R.id.voice_recording_view);
- mVoiceRecordingFinishedView = findViewById(R.id.voice_recording_finished_text_view);
- mSubmitBugReportLayout = findViewById(R.id.submit_bug_report_layout);
- mInProgressLayout = findViewById(R.id.in_progress_layout);
- mShowBugReportsButton = findViewById(R.id.button_show_bugreports);
-
- mShowBugReportsButton.setOnClickListener(this::buttonShowBugReportsClick);
- findViewById(R.id.button_submit).setOnClickListener(this::buttonSubmitClick);
- findViewById(R.id.button_cancel).setOnClickListener(this::buttonCancelClick);
- findViewById(R.id.button_close).setOnClickListener(this::buttonCancelClick);
-
- mCar = Car.createCar(this, mServiceConnection);
- mCar.connect();
- mAudioManager = getSystemService(AudioManager.class);
// Bind to BugReportService.
Intent intent = new Intent(this, BugReportService.class);
@@ -163,26 +183,35 @@
super.onStart();
if (mBound) {
- startAudioMessageRecording();
+ onActivityStartedWithBugReportServiceBound();
}
}
@Override
protected void onStop() {
super.onStop();
- if (!mBugReportServiceStarted && mAudioRecordingStarted) {
+ // If SUBMIT button is clicked, cancelling audio has been taken care of.
+ if (!mIsSubmitButtonClicked) {
cancelAudioMessageRecording();
}
if (mBound) {
mService.removeBugReportProgressListener();
}
+ // Reset variables for the next onStart().
+ mAudioRecordingStarted = false;
+ mIsSubmitButtonClicked = false;
+ mIsOnActivityStartedWithBugReportServiceBoundCalled = false;
+ mMetaBugReport = null;
+ mAudioFile = null;
}
@Override
public void onDestroy() {
super.onDestroy();
- mHandler.removeCallbacksAndMessages(null);
+ if (mRecorder != null) {
+ mHandler.removeCallbacksAndMessages(/* token= */ mRecorder);
+ }
if (mBound) {
unbindService(mConnection);
mBound = false;
@@ -194,7 +223,14 @@
}
private void onCarDrivingStateChanged(CarDrivingStateEvent event) {
- if (event.eventValue == CarDrivingStateEvent.DRIVING_STATE_PARKED) {
+ // 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) {
+ mShowBugReportsButton.setVisibility(View.GONE);
+ return;
+ }
+ if (event.eventValue == CarDrivingStateEvent.DRIVING_STATE_PARKED
+ || event.eventValue == CarDrivingStateEvent.DRIVING_STATE_IDLING) {
mShowBugReportsButton.setVisibility(View.VISIBLE);
} else {
mShowBugReportsButton.setVisibility(View.GONE);
@@ -210,6 +246,43 @@
}
}
+ private void prepareUi() {
+ if (mSubmitBugReportLayout != null) {
+ return;
+ }
+ setContentView(R.layout.bug_report_activity);
+
+ // Connect to the services here, because they are used only when showing the dialog.
+ // We need to minimize system state change when performing SILENT bug report.
+ mConfig = new Config();
+ mConfig.start();
+ mCar = Car.createCar(this, mCarServiceConnection);
+ mCar.connect();
+
+ mInProgressTitleText = findViewById(R.id.in_progress_title_text);
+ mProgressBar = findViewById(R.id.progress_bar);
+ mProgressText = findViewById(R.id.progress_text);
+ mAddAudioText = findViewById(R.id.bug_report_add_audio_to_existing);
+ mVoiceRecordingView = findViewById(R.id.voice_recording_view);
+ mVoiceRecordingFinishedView = findViewById(R.id.voice_recording_finished_text_view);
+ mSubmitBugReportLayout = findViewById(R.id.submit_bug_report_layout);
+ mInProgressLayout = findViewById(R.id.in_progress_layout);
+ mShowBugReportsButton = findViewById(R.id.button_show_bugreports);
+ mSubmitButton = findViewById(R.id.button_submit);
+
+ mShowBugReportsButton.setOnClickListener(this::buttonShowBugReportsClick);
+ mSubmitButton.setOnClickListener(this::buttonSubmitClick);
+ findViewById(R.id.button_cancel).setOnClickListener(this::buttonCancelClick);
+ findViewById(R.id.button_close).setOnClickListener(this::buttonCancelClick);
+
+ if (mIsNewBugReport) {
+ mSubmitButton.setText(R.string.bugreport_dialog_submit);
+ } else {
+ mSubmitButton.setText(mConfig.getAutoUpload()
+ ? R.string.bugreport_dialog_upload : R.string.bugreport_dialog_save);
+ }
+ }
+
private void showInProgressUi() {
mSubmitBugReportLayout.setVisibility(View.GONE);
mInProgressLayout.setVisibility(View.VISIBLE);
@@ -231,7 +304,6 @@
mShowBugReportsButton.setVisibility(View.GONE);
if (mDrivingStateManager != null) {
try {
- // Call onCarDrivingStateChanged(), because it's not called when Car is connected.
onCarDrivingStateChanged(mDrivingStateManager.getCurrentCarDrivingState());
} catch (CarNotConnectedException e) {
Log.e(TAG, "Failed to get current driving state.", e);
@@ -244,28 +316,89 @@
*
* <p>This method expected to be called when the activity is started and bound to the service.
*/
- private void startAudioMessageRecording() {
- mService.setBugReportProgressListener(this::onProgressChanged);
+ private void onActivityStartedWithBugReportServiceBound() {
+ if (mIsOnActivityStartedWithBugReportServiceBoundCalled) {
+ return;
+ }
+ mIsOnActivityStartedWithBugReportServiceBoundCalled = true;
if (mService.isCollectingBugReport()) {
Log.i(TAG, "Bug report is already being collected.");
+ mService.setBugReportProgressListener(this::onProgressChanged);
+ prepareUi();
showInProgressUi();
return;
}
+ if (ACTION_START_SILENT.equals(getIntent().getAction())) {
+ Log.i(TAG, "Starting a silent bugreport.");
+ MetaBugReport bugReport = createBugReport(this, MetaBugReport.TYPE_SILENT);
+ startBugReportCollection(bugReport);
+ finish();
+ return;
+ }
+
+ // Close the notification shade and other dialogs when showing BugReportActivity dialog.
+ sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+
+ if (ACTION_ADD_AUDIO.equals(getIntent().getAction())) {
+ addAudioToExistingBugReport(
+ getIntent().getIntExtra(EXTRA_BUGREPORT_ID, /* defaultValue= */ -1));
+ return;
+ }
+
+ Log.i(TAG, "Starting an interactive bugreport.");
+ createNewBugReportWithAudioMessage();
+ }
+
+ private void addAudioToExistingBugReport(int bugreportId) {
+ MetaBugReport bug = BugStorageUtils.findBugReport(this, bugreportId).orElseThrow(
+ () -> new RuntimeException("Failed to find bug report with id " + bugreportId));
+ Log.i(TAG, "Adding audio to the existing bugreport " + bug.getTimestamp());
+ if (bug.getStatus() != Status.STATUS_AUDIO_PENDING.getValue()) {
+ Log.e(TAG, "Failed to add audio, bad status, expected "
+ + Status.STATUS_AUDIO_PENDING.getValue() + ", got " + bug.getStatus());
+ finish();
+ }
+ File audioFile;
+ try {
+ audioFile = File.createTempFile("audio", "mp3", getCacheDir());
+ } catch (IOException e) {
+ throw new RuntimeException("failed to create temp audio file");
+ }
+ startAudioMessageRecording(/* isNewBugReport= */ false, bug, audioFile);
+ }
+
+ private void createNewBugReportWithAudioMessage() {
+ MetaBugReport bug = createBugReport(this, MetaBugReport.TYPE_INTERACTIVE);
+ startAudioMessageRecording(
+ /* isNewBugReport= */ true,
+ bug,
+ FileUtils.getFileWithSuffix(this, bug.getTimestamp(), "-message.3gp"));
+ }
+
+ /** Shows a dialog UI and starts recording audio message. */
+ private void startAudioMessageRecording(
+ boolean isNewBugReport, MetaBugReport bug, File audioFile) {
if (mAudioRecordingStarted) {
Log.i(TAG, "Audio message recording is already started.");
return;
}
-
mAudioRecordingStarted = true;
+ mAudioManager = getSystemService(AudioManager.class);
+ mIsNewBugReport = isNewBugReport;
+ mMetaBugReport = bug;
+ mAudioFile = audioFile;
+ prepareUi();
showSubmitBugReportUi(/* isRecording= */ true);
-
- Date initiatedAt = new Date();
- String timestamp = BUG_REPORT_TIMESTAMP_DATE_FORMAT.format(initiatedAt);
- String username = getCurrentUserName();
- String title = BugReportTitleGenerator.generateBugReportTitle(initiatedAt, username);
- mMetaBugReport = BugStorageUtils.createBugReport(this, title, timestamp, username);
+ if (isNewBugReport) {
+ mAddAudioText.setVisibility(View.GONE);
+ } else {
+ mAddAudioText.setVisibility(View.VISIBLE);
+ mAddAudioText.setText(String.format(
+ getString(R.string.bugreport_dialog_add_audio_to_existing),
+ mMetaBugReport.getTimestamp()));
+ }
if (!hasRecordPermissions()) {
requestRecordPermissions();
@@ -282,10 +415,17 @@
return;
}
stopAudioRecording();
- File tempDir = FileUtils.getTempDir(this, mMetaBugReport.getTimestamp());
- new DeleteDirectoryAsyncTask().execute(tempDir);
- BugStorageUtils.setBugReportStatus(this, mMetaBugReport, Status.STATUS_USER_CANCELLED, "");
- Log.i(TAG, "Bug report is cancelled");
+ if (mIsNewBugReport) {
+ // The app creates a temp dir only for new INTERACTIVE bugreports.
+ File tempDir = FileUtils.getTempDir(this, mMetaBugReport.getTimestamp());
+ new DeleteFilesAndDirectoriesAsyncTask().execute(tempDir);
+ } else {
+ BugStorageUtils.deleteBugReportFiles(this, mMetaBugReport.getId());
+ new DeleteFilesAndDirectoriesAsyncTask().execute(mAudioFile);
+ }
+ BugStorageUtils.setBugReportStatus(
+ this, mMetaBugReport, Status.STATUS_USER_CANCELLED, "");
+ Log.i(TAG, "Bug report " + mMetaBugReport.getTimestamp() + " is cancelled");
mAudioRecordingStarted = false;
}
@@ -294,36 +434,46 @@
}
private void buttonSubmitClick(View view) {
- startBugReportingInService();
+ stopAudioRecording();
+ mIsSubmitButtonClicked = true;
+ if (mIsNewBugReport) {
+ Log.i(TAG, "Starting bugreport service.");
+ startBugReportCollection(mMetaBugReport);
+ } else {
+ Log.i(TAG, "Adding audio file to the bugreport " + mMetaBugReport.getTimestamp());
+ new AddAudioToBugReportAsyncTask(this, mConfig, mMetaBugReport, mAudioFile).execute();
+ }
+ setResult(Activity.RESULT_OK);
finish();
}
+ /** Starts the {@link BugReportService} to collect bug report. */
+ private void startBugReportCollection(MetaBugReport bug) {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(BugReportService.EXTRA_META_BUG_REPORT, bug);
+ Intent intent = new Intent(this, BugReportService.class);
+ intent.putExtras(bundle);
+ startForegroundService(intent);
+ }
+
/**
* Starts {@link BugReportInfoActivity} and finishes current activity, so it won't be running
- * in the background and closing {@link BugReportInfoActivity} will not open it again.
+ * in the background and closing {@link BugReportInfoActivity} will not open the current
+ * activity again.
*/
private void buttonShowBugReportsClick(View view) {
+ // First cancel the audio recording, then delete the bug report from database.
cancelAudioMessageRecording();
// Delete the bugreport from database, otherwise pressing "Show Bugreports" button will
// create unnecessary cancelled bugreports.
if (mMetaBugReport != null) {
- BugStorageUtils.deleteBugReport(this, mMetaBugReport.getId());
+ BugStorageUtils.completeDeleteBugReport(this, mMetaBugReport.getId());
}
Intent intent = new Intent(this, BugReportInfoActivity.class);
startActivity(intent);
finish();
}
- private void startBugReportingInService() {
- stopAudioRecording();
- Bundle bundle = new Bundle();
- bundle.putParcelable(EXTRA_META_BUG_REPORT, mMetaBugReport);
- Intent intent = new Intent(this, BugReportService.class);
- intent.putExtras(bundle);
- startService(intent);
- mBugReportServiceStarted = true;
- }
-
private void requestRecordPermissions() {
requestPermissions(
new String[]{Manifest.permission.RECORD_AUDIO}, AUDIO_PERMISSIONS_REQUEST_ID);
@@ -343,7 +493,9 @@
for (int i = 0; i < grantResults.length; i++) {
if (Manifest.permission.RECORD_AUDIO.equals(permissions[i])
&& grantResults[i] == PackageManager.PERMISSION_GRANTED) {
- startRecordingWithPermission();
+ // Start recording from UI thread, otherwise when MediaRecord#start() fails,
+ // stack trace gets confusing.
+ mHandler.post(this::startRecordingWithPermission);
return;
}
}
@@ -361,9 +513,7 @@
}
private void startRecordingWithPermission() {
- File recordingFile = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(),
- "-message.3gp");
- Log.i(TAG, "Started voice recording, and saving audio to " + recordingFile);
+ Log.i(TAG, "Started voice recording, and saving audio to " + mAudioFile);
mLastAudioFocusRequest = new AudioFocusRequest.Builder(
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
@@ -388,12 +538,12 @@
Log.i(TAG, "OnMediaRecorderInfo: what=" + what + ", extra=" + extra));
mRecorder.setOnErrorListener((MediaRecorder recorder, int what, int extra) ->
Log.i(TAG, "OnMediaRecorderError: what=" + what + ", extra=" + extra));
- mRecorder.setOutputFile(recordingFile);
+ mRecorder.setOutputFile(mAudioFile);
try {
mRecorder.prepare();
} catch (IOException e) {
- Log.e(TAG, "Failed on MediaRecorder#prepare(), filename: " + recordingFile, e);
+ Log.e(TAG, "Failed on MediaRecorder#prepare(), filename: " + mAudioFile, e);
finish();
return;
}
@@ -401,19 +551,21 @@
mRecorder.start();
mVoiceRecordingView.setRecorder(mRecorder);
+ // Messages with token mRecorder are cleared when the activity finishes or recording stops.
mHandler.postDelayed(() -> {
Log.i(TAG, "Timed out while recording voice message, cancelling.");
stopAudioRecording();
showSubmitBugReportUi(/* isRecording= */ false);
- }, VOICE_MESSAGE_MAX_DURATION_MILLIS);
+ }, /* token= */ mRecorder, VOICE_MESSAGE_MAX_DURATION_MILLIS);
}
private void stopAudioRecording() {
if (mRecorder != null) {
Log.i(TAG, "Recording ended, stopping the MediaRecorder.");
+ mHandler.removeCallbacksAndMessages(/* token= */ mRecorder);
try {
mRecorder.stop();
- } catch (IllegalStateException e) {
+ } catch (RuntimeException e) {
// Sometimes MediaRecorder doesn't start and stopping it throws an error.
// We just log these cases, no need to crash the app.
Log.w(TAG, "Couldn't stop media recorder", e);
@@ -430,11 +582,24 @@
mVoiceRecordingView.setRecorder(null);
}
- private String getCurrentUserName() {
- UserManager um = UserManager.get(this);
+ private static String getCurrentUserName(Context context) {
+ UserManager um = UserManager.get(context);
return um.getUserName();
}
+ /**
+ * Creates a {@link MetaBugReport} and saves it in a local sqlite database.
+ *
+ * @param context an Android context.
+ * @param type bug report type, {@link MetaBugReport.BugReportType}.
+ */
+ static MetaBugReport createBugReport(Context context, int type) {
+ String timestamp = MetaBugReport.toBugReportTimestamp(new Date());
+ String username = getCurrentUserName(context);
+ String title = BugReportTitleGenerator.generateBugReportTitle(timestamp, username);
+ return BugStorageUtils.createBugReport(context, title, timestamp, username, type);
+ }
+
/** A helper class to generate bugreport title. */
private static final class BugReportTitleGenerator {
/** Contains easily readable characters. */
@@ -442,17 +607,14 @@
new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P',
'R', 'S', 'T', 'U', 'W', 'X', 'Y', 'Z'};
- private static final int LOOKUP_STRING_LENGTH = 6;
-
/**
* Generates a bugreport title from given timestamp and username.
*
* <p>Example: "[A45E8] Feedback from user Driver at 2019-09-21_12:00:00"
*/
- static String generateBugReportTitle(Date initiatedAt, String username) {
+ static String generateBugReportTitle(String timestamp, String username) {
// Lookup string is used to search a bug in Buganizer (see b/130915969).
String lookupString = generateRandomString(LOOKUP_STRING_LENGTH);
- String timestamp = BUG_REPORT_TIMESTAMP_DATE_FORMAT.format(initiatedAt);
return "[" + lookupString + "] Feedback from user " + username + " at " + timestamp;
}
@@ -467,15 +629,66 @@
}
}
- /** AsyncTask that recursively deletes directories. */
- private static class DeleteDirectoryAsyncTask extends AsyncTask<File, Void, Void> {
+ /** AsyncTask that recursively deletes files and directories. */
+ private static class DeleteFilesAndDirectoriesAsyncTask extends AsyncTask<File, Void, Void> {
@Override
protected Void doInBackground(File... files) {
for (File file : files) {
Log.i(TAG, "Deleting " + file.getAbsolutePath());
- FileUtils.deleteDirectory(file);
+ if (file.isFile()) {
+ file.delete();
+ } else {
+ FileUtils.deleteDirectory(file);
+ }
}
return null;
}
}
+
+ /**
+ * AsyncTask that moves audio file to the system user's {@link FileUtils#getPendingDir} and
+ * sets status to either STATUS_UPLOAD_PENDING or STATUS_PENDING_USER_ACTION.
+ */
+ private static class AddAudioToBugReportAsyncTask extends AsyncTask<Void, Void, Void> {
+ private final Context mContext;
+ private final Config mConfig;
+ private final File mAudioFile;
+ private final MetaBugReport mOriginalBug;
+
+ AddAudioToBugReportAsyncTask(
+ Context context, Config config, MetaBugReport bug, File audioFile) {
+ mContext = context;
+ mConfig = config;
+ mOriginalBug = bug;
+ mAudioFile = audioFile;
+ }
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ String audioFileName = FileUtils.getAudioFileName(
+ MetaBugReport.toBugReportTimestamp(new Date()), mOriginalBug);
+ MetaBugReport bug = BugStorageUtils.update(mContext,
+ mOriginalBug.toBuilder().setAudioFileName(audioFileName).build());
+ try (OutputStream out = BugStorageUtils.openAudioMessageFileToWrite(mContext, bug);
+ InputStream input = new FileInputStream(mAudioFile)) {
+ ByteStreams.copy(input, out);
+ } catch (IOException e) {
+ BugStorageUtils.setBugReportStatus(mContext, bug,
+ com.google.android.car.bugreport.Status.STATUS_WRITE_FAILED,
+ "Failed to write audio to bug report");
+ Log.e(TAG, "Failed to write audio to bug report", e);
+ return null;
+ }
+ if (mConfig.getAutoUpload()) {
+ BugStorageUtils.setBugReportStatus(mContext, bug,
+ com.google.android.car.bugreport.Status.STATUS_UPLOAD_PENDING, "");
+ } else {
+ BugStorageUtils.setBugReportStatus(mContext, bug,
+ com.google.android.car.bugreport.Status.STATUS_PENDING_USER_ACTION, "");
+ BugReportService.showBugReportFinishedNotification(mContext, bug);
+ }
+ mAudioFile.delete();
+ return null;
+ }
+ }
}
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportInfoActivity.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportInfoActivity.java
index 0469bb1..343a52d 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportInfoActivity.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportInfoActivity.java
@@ -21,9 +21,13 @@
import android.app.NotificationManager;
import android.content.ContentResolver;
import android.content.Intent;
+import android.content.res.AssetFileDescriptor;
+import android.database.ContentObserver;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.UserHandle;
import android.provider.DocumentsContract;
import android.util.Log;
import android.view.View;
@@ -33,10 +37,16 @@
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.io.ByteStreams;
+
import java.io.File;
+import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -47,105 +57,26 @@
public class BugReportInfoActivity extends Activity {
public static final String TAG = BugReportInfoActivity.class.getSimpleName();
+ /** Used for moving bug reports to a new location (e.g. USB drive). */
private static final int SELECT_DIRECTORY_REQUEST_CODE = 1;
+ /** Used to start {@link BugReportActivity} to add audio message. */
+ private static final int ADD_AUDIO_MESSAGE_REQUEST_CODE = 2;
+
private RecyclerView mRecyclerView;
- private RecyclerView.Adapter mAdapter;
+ private BugInfoAdapter mBugInfoAdapter;
private RecyclerView.LayoutManager mLayoutManager;
private NotificationManager mNotificationManager;
private MetaBugReport mLastSelectedBugReport;
-
- private static final class AsyncMoveFilesTask extends AsyncTask<Void, Void, Boolean> {
- private final BugReportInfoActivity mActivity;
- private final MetaBugReport mBugReport;
- private final Uri mDestinationDirUri;
-
- AsyncMoveFilesTask(BugReportInfoActivity activity, MetaBugReport bugReport,
- Uri destinationDir) {
- mActivity = activity;
- mBugReport = bugReport;
- mDestinationDirUri = destinationDir;
- }
-
- @Override
- protected Boolean doInBackground(Void... params) {
- Uri sourceUri = BugStorageProvider.buildUriWithBugId(mBugReport.getId());
- ContentResolver resolver = mActivity.getContentResolver();
- String documentId = DocumentsContract.getTreeDocumentId(mDestinationDirUri);
- Uri parentDocumentUri =
- DocumentsContract.buildDocumentUriUsingTree(mDestinationDirUri, documentId);
- String mimeType = resolver.getType(sourceUri);
- try {
- Uri newFileUri = DocumentsContract.createDocument(resolver, parentDocumentUri,
- mimeType,
- new File(mBugReport.getFilePath()).toPath().getFileName().toString());
- if (newFileUri == null) {
- Log.e(TAG, "Unable to create a new file.");
- return false;
- }
- try (InputStream input = resolver.openInputStream(sourceUri);
- OutputStream output = resolver.openOutputStream(newFileUri)) {
- byte[] buffer = new byte[4096];
- int len;
- while ((len = input.read(buffer)) > 0) {
- output.write(buffer, 0, len);
- }
- }
- BugStorageUtils.setBugReportStatus(
- mActivity, mBugReport,
- com.google.android.car.bugreport.Status.STATUS_MOVE_SUCCESSFUL, "");
- } catch (IOException e) {
- Log.e(TAG, "Failed to create the bug report in the location.", e);
- return false;
- }
- return true;
- }
-
- @Override
- protected void onPostExecute(Boolean moveSuccessful) {
- if (!moveSuccessful) {
- BugStorageUtils.setBugReportStatus(
- mActivity, mBugReport,
- com.google.android.car.bugreport.Status.STATUS_MOVE_FAILED, "");
- }
- // Refresh the UI to reflect the new status.
- new BugReportInfoTask(mActivity).execute();
- }
- }
-
- private static final class BugReportInfoTask extends
- AsyncTask<Void, Void, List<MetaBugReport>> {
- private final WeakReference<BugReportInfoActivity> mBugReportInfoActivityWeakReference;
-
- BugReportInfoTask(BugReportInfoActivity activity) {
- mBugReportInfoActivityWeakReference = new WeakReference<>(activity);
- }
-
- @Override
- protected List<MetaBugReport> doInBackground(Void... voids) {
- BugReportInfoActivity activity = mBugReportInfoActivityWeakReference.get();
- if (activity == null) {
- Log.w(TAG, "Activity is gone, cancelling BugReportInfoTask.");
- return new ArrayList<>();
- }
- return BugStorageUtils.getAllBugReportsDescending(activity);
- }
-
- @Override
- protected void onPostExecute(List<MetaBugReport> result) {
- BugReportInfoActivity activity = mBugReportInfoActivityWeakReference.get();
- if (activity == null) {
- Log.w(TAG, "Activity is gone, cancelling onPostExecute.");
- return;
- }
- activity.mAdapter = new BugInfoAdapter(result, activity::onBugReportItemClicked);
- activity.mRecyclerView.setAdapter(activity.mAdapter);
- activity.mRecyclerView.getAdapter().notifyDataSetChanged();
- }
- }
+ private BugInfoAdapter.BugInfoViewHolder mLastSelectedBugInfoViewHolder;
+ private BugStorageObserver mBugStorageObserver;
+ private Config mConfig;
+ private boolean mAudioRecordingStarted;
@Override
protected void onCreate(Bundle savedInstanceState) {
+ Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled.");
+
super.onCreate(savedInstanceState);
setContentView(R.layout.bug_report_info_activity);
@@ -159,9 +90,13 @@
mRecyclerView.addItemDecoration(new DividerItemDecoration(mRecyclerView.getContext(),
DividerItemDecoration.VERTICAL));
- // specify an adapter (see also next example)
- mAdapter = new BugInfoAdapter(new ArrayList<>(), this::onBugReportItemClicked);
- mRecyclerView.setAdapter(mAdapter);
+ mConfig = new Config();
+ mConfig.start();
+
+ mBugInfoAdapter = new BugInfoAdapter(this::onBugReportItemClicked, mConfig);
+ mRecyclerView.setAdapter(mBugInfoAdapter);
+
+ mBugStorageObserver = new BugStorageObserver(this, new Handler());
findViewById(R.id.quit_button).setOnClickListener(this::onQuitButtonClick);
findViewById(R.id.start_bug_report_button).setOnClickListener(
@@ -175,7 +110,16 @@
@Override
protected void onStart() {
super.onStart();
- new BugReportInfoTask(this).execute();
+ new BugReportsLoaderAsyncTask(this).execute();
+ // As BugStorageProvider is running under user0, we register using USER_ALL.
+ getContentResolver().registerContentObserver(BugStorageProvider.BUGREPORT_CONTENT_URI, true,
+ mBugStorageObserver, UserHandle.USER_ALL);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getContentResolver().unregisterContentObserver(mBugStorageObserver);
}
/**
@@ -186,17 +130,26 @@
mNotificationManager.cancel(BugReportService.BUGREPORT_FINISHED_NOTIF_ID);
}
- private void onBugReportItemClicked(int buttonType, MetaBugReport bugReport) {
+ private void onBugReportItemClicked(
+ int buttonType, MetaBugReport bugReport, BugInfoAdapter.BugInfoViewHolder holder) {
if (buttonType == BugInfoAdapter.BUTTON_TYPE_UPLOAD) {
- Log.i(TAG, "Uploading " + bugReport.getFilePath());
+ Log.i(TAG, "Uploading " + bugReport.getTimestamp());
BugStorageUtils.setBugReportStatus(this, bugReport, Status.STATUS_UPLOAD_PENDING, "");
// Refresh the UI to reflect the new status.
- new BugReportInfoTask(this).execute();
+ new BugReportsLoaderAsyncTask(this).execute();
} else if (buttonType == BugInfoAdapter.BUTTON_TYPE_MOVE) {
- Log.i(TAG, "Moving " + bugReport.getFilePath());
+ Log.i(TAG, "Moving " + bugReport.getTimestamp());
mLastSelectedBugReport = bugReport;
+ mLastSelectedBugInfoViewHolder = holder;
startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE),
SELECT_DIRECTORY_REQUEST_CODE);
+ } else if (buttonType == BugInfoAdapter.BUTTON_TYPE_ADD_AUDIO) {
+ // Check mAudioRecordingStarted to prevent double click to BUTTON_TYPE_ADD_AUDIO.
+ if (!mAudioRecordingStarted) {
+ mAudioRecordingStarted = true;
+ startActivityForResult(BugReportActivity.buildAddAudioIntent(this, bugReport),
+ ADD_AUDIO_MESSAGE_REQUEST_CODE);
+ }
} else {
throw new IllegalStateException("unreachable");
}
@@ -211,11 +164,20 @@
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
Uri destDirUri = data.getData();
getContentResolver().takePersistableUriPermission(destDirUri, takeFlags);
- if (mLastSelectedBugReport == null) {
+ if (mLastSelectedBugReport == null || mLastSelectedBugInfoViewHolder == null) {
Log.w(TAG, "No bug report is selected.");
return;
}
- new AsyncMoveFilesTask(this, mLastSelectedBugReport, destDirUri).execute();
+ MetaBugReport updatedBugReport = BugStorageUtils.setBugReportStatus(this,
+ mLastSelectedBugReport, Status.STATUS_MOVE_IN_PROGRESS, "");
+ mBugInfoAdapter.updateBugReportInDataSet(
+ updatedBugReport, mLastSelectedBugInfoViewHolder.getAdapterPosition());
+ new AsyncMoveFilesTask(
+ this,
+ mBugInfoAdapter,
+ updatedBugReport,
+ mLastSelectedBugInfoViewHolder,
+ destDirUri).execute();
}
}
@@ -230,4 +192,161 @@
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
+
+ /**
+ * Print the Provider's state into the given stream. This gets invoked if
+ * you run "adb shell dumpsys activity BugReportInfoActivity".
+ *
+ * @param prefix Desired prefix to prepend at each line of output.
+ * @param fd The raw file descriptor that the dump is being sent to.
+ * @param writer The PrintWriter to which you should dump your state. This will be
+ * closed for you after you return.
+ * @param args additional arguments to the dump request.
+ */
+ public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+ super.dump(prefix, fd, writer, args);
+ mConfig.dump(prefix, writer);
+ }
+
+ /**
+ * Moves bugreport zip to USB drive and updates RecyclerView.
+ *
+ * <p>It merges bugreport zip file and audio file into one final zip file and moves it.
+ */
+ private static final class AsyncMoveFilesTask extends AsyncTask<Void, Void, MetaBugReport> {
+ private final BugReportInfoActivity mActivity;
+ private final MetaBugReport mBugReport;
+ private final Uri mDestinationDirUri;
+ /** RecyclerView.Adapter that contains all the bug reports. */
+ private final BugInfoAdapter mBugInfoAdapter;
+ /** ViewHolder for {@link #mBugReport}. */
+ private final BugInfoAdapter.BugInfoViewHolder mBugViewHolder;
+ private final ContentResolver mResolver;
+
+ AsyncMoveFilesTask(BugReportInfoActivity activity, BugInfoAdapter bugInfoAdapter,
+ MetaBugReport bugReport, BugInfoAdapter.BugInfoViewHolder holder,
+ Uri destinationDir) {
+ mActivity = activity;
+ mBugInfoAdapter = bugInfoAdapter;
+ mBugReport = bugReport;
+ mBugViewHolder = holder;
+ mDestinationDirUri = destinationDir;
+ mResolver = mActivity.getContentResolver();
+ }
+
+ /** Moves the bugreport to the USB drive and returns the updated {@link MetaBugReport}. */
+ @Override
+ protected MetaBugReport doInBackground(Void... params) {
+ try {
+ return copyFilesToUsb();
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to copy bugreport "
+ + mBugReport.getTimestamp() + " to USB", e);
+ return BugStorageUtils.setBugReportStatus(
+ mActivity, mBugReport,
+ com.google.android.car.bugreport.Status.STATUS_MOVE_FAILED, e);
+ }
+ }
+
+ private MetaBugReport copyFilesToUsb() throws IOException {
+ String documentId = DocumentsContract.getTreeDocumentId(mDestinationDirUri);
+ Uri parentDocumentUri =
+ DocumentsContract.buildDocumentUriUsingTree(mDestinationDirUri, documentId);
+ if (!Strings.isNullOrEmpty(mBugReport.getFilePath())) {
+ // There are still old bugreports with deprecated filePath.
+ Uri sourceUri = BugStorageProvider.buildUriWithSegment(
+ mBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_FILE);
+ copyFileToUsb(
+ new File(mBugReport.getFilePath()).getName(), sourceUri, parentDocumentUri);
+ } else {
+ Uri sourceBugReport = BugStorageProvider.buildUriWithSegment(
+ mBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_BUGREPORT_FILE);
+ copyFileToUsb(
+ mBugReport.getBugReportFileName(), sourceBugReport, parentDocumentUri);
+ Uri sourceAudio = BugStorageProvider.buildUriWithSegment(
+ mBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_AUDIO_FILE);
+ copyFileToUsb(mBugReport.getAudioFileName(), sourceAudio, parentDocumentUri);
+ }
+ Log.d(TAG, "Deleting local bug report files.");
+ BugStorageUtils.deleteBugReportFiles(mActivity, mBugReport.getId());
+ return BugStorageUtils.setBugReportStatus(mActivity, mBugReport,
+ com.google.android.car.bugreport.Status.STATUS_MOVE_SUCCESSFUL,
+ "Moved to: " + mDestinationDirUri.getPath());
+ }
+
+ private void copyFileToUsb(String filename, Uri sourceUri, Uri parentDocumentUri)
+ throws IOException {
+ String mimeType = mResolver.getType(sourceUri);
+ Uri newFileUri = DocumentsContract.createDocument(
+ mResolver, parentDocumentUri, mimeType, filename);
+ if (newFileUri == null) {
+ throw new IOException("Unable to create a file " + filename + " in USB");
+ }
+ try (InputStream input = mResolver.openInputStream(sourceUri);
+ AssetFileDescriptor fd = mResolver.openAssetFileDescriptor(newFileUri, "w")) {
+ OutputStream output = fd.createOutputStream();
+ ByteStreams.copy(input, output);
+ // Force sync the written data from memory to the disk.
+ fd.getFileDescriptor().sync();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(MetaBugReport updatedBugReport) {
+ // Refresh the UI to reflect the new status.
+ mBugInfoAdapter.updateBugReportInDataSet(
+ updatedBugReport, mBugViewHolder.getAdapterPosition());
+ }
+ }
+
+ /** Asynchronously loads bugreports from {@link BugStorageProvider}. */
+ private static final class BugReportsLoaderAsyncTask extends
+ AsyncTask<Void, Void, List<MetaBugReport>> {
+ private final WeakReference<BugReportInfoActivity> mBugReportInfoActivityWeakReference;
+
+ BugReportsLoaderAsyncTask(BugReportInfoActivity activity) {
+ mBugReportInfoActivityWeakReference = new WeakReference<>(activity);
+ }
+
+ @Override
+ protected List<MetaBugReport> doInBackground(Void... voids) {
+ BugReportInfoActivity activity = mBugReportInfoActivityWeakReference.get();
+ if (activity == null) {
+ Log.w(TAG, "Activity is gone, cancelling BugReportsLoaderAsyncTask.");
+ return new ArrayList<>();
+ }
+ return BugStorageUtils.getAllBugReportsDescending(activity);
+ }
+
+ @Override
+ protected void onPostExecute(List<MetaBugReport> result) {
+ BugReportInfoActivity activity = mBugReportInfoActivityWeakReference.get();
+ if (activity == null) {
+ Log.w(TAG, "Activity is gone, cancelling onPostExecute.");
+ return;
+ }
+ activity.mBugInfoAdapter.setDataset(result);
+ }
+ }
+
+ /** Observer for {@link BugStorageProvider}. */
+ private static class BugStorageObserver extends ContentObserver {
+ private final BugReportInfoActivity mInfoActivity;
+
+ /**
+ * Creates a content observer.
+ *
+ * @param activity A {@link BugReportInfoActivity} instance.
+ * @param handler The handler to run {@link #onChange} on, or null if none.
+ */
+ BugStorageObserver(BugReportInfoActivity activity, Handler handler) {
+ super(handler);
+ mInfoActivity = activity;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ new BugReportsLoaderAsyncTask(mInfoActivity).execute();
+ }
+ }
}
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 f6fc651..3f89660 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugReportService.java
@@ -27,7 +27,12 @@
import android.car.Car;
import android.car.CarBugreportManager;
import android.car.CarNotConnectedException;
+import android.content.Context;
import android.content.Intent;
+import android.media.AudioManager;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -38,33 +43,32 @@
import android.util.Log;
import android.widget.Toast;
+import com.google.common.base.Preconditions;
+import com.google.common.io.ByteStreams;
import com.google.common.util.concurrent.AtomicDouble;
-import libcore.io.IoUtils;
-
import java.io.BufferedOutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.io.OutputStream;
-import java.util.Enumeration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
/**
* Service that captures screenshot and bug report using dumpstate and bluetooth snoop logs.
*
- * <p>After collecting all the logs it updates the {@link MetaBugReport} using {@link
- * BugStorageProvider}, which in turn schedules bug report to upload.
+ * <p>After collecting all the logs it sets the {@link MetaBugReport} status to
+ * {@link Status#STATUS_AUDIO_PENDING} or {@link Status#STATUS_PENDING_USER_ACTION} depending
+ * on {@link MetaBugReport#getType}.
+ *
+ * <p>If the service is started with action {@link #ACTION_START_SILENT}, it will start
+ * bugreporting without showing dialog and recording audio message, see
+ * {@link MetaBugReport#TYPE_SILENT}.
*/
public class BugReportService extends Service {
private static final String TAG = BugReportService.class.getSimpleName();
@@ -74,6 +78,10 @@
*/
static final String EXTRA_META_BUG_REPORT = "meta_bug_report";
+ /** Starts silent (no audio message recording) bugreporting. */
+ private static final String ACTION_START_SILENT =
+ "com.google.android.car.bugreport.action.START_SILENT";
+
// Wait a short time before starting to capture the bugreport and the screen, so that
// bugreport activity can detach from the view tree.
// It is ugly to have a timeout, but it is ok here because such a delay should not really
@@ -81,6 +89,12 @@
// this, the best option is probably to wait for onDetach events from view tree.
private static final int ACTIVITY_FINISH_DELAY_MILLIS = 1000;
+ /**
+ * Wait a short time before showing "bugreport started" toast message, because the service
+ * will take a screenshot of the screen.
+ */
+ private static final int BUGREPORT_STARTED_TOAST_DELAY_MILLIS = 2000;
+
private static final String BT_SNOOP_LOG_LOCATION = "/data/misc/bluetooth/logs/btsnoop_hci.log";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -90,9 +104,10 @@
/** Notifications on this channel will pop-up. */
private static final String STATUS_CHANNEL_ID = "BUGREPORT_STATUS_CHANNEL";
+ /** Persistent notification is shown when bugreport is in progress or waiting for audio. */
private static final int BUGREPORT_IN_PROGRESS_NOTIF_ID = 1;
- /** The notification is shown when bugreport is collected. */
+ /** Dismissible notification is shown when bugreport is collected. */
static final int BUGREPORT_FINISHED_NOTIF_ID = 2;
private static final String OUTPUT_ZIP_FILE = "output_file.zip";
@@ -119,9 +134,16 @@
private Car mCar;
private CarBugreportManager mBugreportManager;
private CarBugreportManager.CarBugreportManagerCallback mCallback;
+ private Config mConfig;
/** A handler on the main thread. */
private Handler mHandler;
+ /**
+ * A handler to the main thread to show toast messages, it will be cleared when the service
+ * finishes. We need to clear it otherwise when bugreport fails, it will show "bugreport start"
+ * toast, which will confuse users.
+ */
+ private Handler mHandlerToast;
/** A listener that's notified when bugreport progress changes. */
interface BugReportProgressListener {
@@ -141,7 +163,7 @@
}
}
- /** A handler on a main thread. */
+ /** A handler on the main thread. */
private class BugReportHandler extends Handler {
@Override
public void handleMessage(Message message) {
@@ -161,6 +183,8 @@
@Override
public void onCreate() {
+ Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled.");
+
mNotificationManager = getSystemService(NotificationManager.class);
mNotificationManager.createNotificationChannel(new NotificationChannel(
PROGRESS_CHANNEL_ID,
@@ -172,34 +196,58 @@
NotificationManager.IMPORTANCE_HIGH));
mSingleThreadExecutor = Executors.newSingleThreadScheduledExecutor();
mHandler = new BugReportHandler();
+ mHandlerToast = 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) {
- Log.w(TAG, "Couldn't get CarBugreportManager", e);
+ throw new IllegalStateException("Failed to get CarBugreportManager.", e);
}
}
@Override
+ public void onDestroy() {
+ if (DEBUG) {
+ Log.d(TAG, "Service destroyed");
+ }
+ mCar.disconnect();
+ }
+
+ @Override
public int onStartCommand(final Intent intent, int flags, int startId) {
- if (mIsCollectingBugReport.get()) {
+ if (mIsCollectingBugReport.getAndSet(true)) {
Log.w(TAG, "bug report is already being collected, ignoring");
Toast.makeText(this, R.string.toast_bug_report_in_progress, Toast.LENGTH_SHORT).show();
return START_NOT_STICKY;
}
+
Log.i(TAG, String.format("Will start collecting bug report, version=%s",
getPackageVersion(this)));
- mIsCollectingBugReport.set(true);
+
+ if (ACTION_START_SILENT.equals(intent.getAction())) {
+ Log.i(TAG, "Starting a silent bugreport.");
+ mMetaBugReport = BugReportActivity.createBugReport(this, MetaBugReport.TYPE_SILENT);
+ } else {
+ Bundle extras = intent.getExtras();
+ mMetaBugReport = extras.getParcelable(EXTRA_META_BUG_REPORT);
+ }
+
mBugReportProgress.set(0);
startForeground(BUGREPORT_IN_PROGRESS_NOTIF_ID, buildProgressNotification());
showProgressNotification();
- Bundle extras = intent.getExtras();
- mMetaBugReport = extras.getParcelable(EXTRA_META_BUG_REPORT);
-
collectBugReport();
+ // Show a short lived "bugreport started" toast message after a short delay.
+ mHandlerToast.postDelayed(() -> {
+ Toast.makeText(this,
+ getText(R.string.toast_bug_report_started), Toast.LENGTH_LONG).show();
+ }, BUGREPORT_STARTED_TOAST_DELAY_MILLIS);
+
// If the service process gets killed due to heavy memory pressure, do not restart.
return START_NOT_STICKY;
}
@@ -213,6 +261,9 @@
}
private Notification buildProgressNotification() {
+ Intent intent = new Intent(getApplicationContext(), BugReportInfoActivity.class);
+ PendingIntent startBugReportInfoActivity =
+ PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
return new Notification.Builder(this, PROGRESS_CHANNEL_ID)
.setContentTitle(getText(R.string.notification_bugreport_in_progress))
.setSubText(String.format("%.1f%%", mBugReportProgress.get()))
@@ -220,6 +271,7 @@
.setCategory(Notification.CATEGORY_STATUS)
.setOngoing(true)
.setProgress((int) MAX_PROGRESS_VALUE, (int) mBugReportProgress.get(), false)
+ .setContentIntent(startBugReportInfoActivity)
.build();
}
@@ -266,9 +318,14 @@
Log.i(TAG, "Grabbing bt snoop log");
File result = FileUtils.getFileWithSuffix(this, mMetaBugReport.getTimestamp(),
"-btsnoop.bin.log");
- try {
- copyBinaryStream(new FileInputStream(new File(BT_SNOOP_LOG_LOCATION)),
- new FileOutputStream(result));
+ File snoopFile = new File(BT_SNOOP_LOG_LOCATION);
+ if (!snoopFile.exists()) {
+ Log.w(TAG, BT_SNOOP_LOG_LOCATION + " not found, skipping");
+ return;
+ }
+ try (FileInputStream input = new FileInputStream(snoopFile);
+ FileOutputStream output = new FileOutputStream(result)) {
+ ByteStreams.copy(input, output);
} catch (IOException e) {
// this regularly happens when snooplog is not enabled so do not log as an error
Log.i(TAG, "Failed to grab bt snooplog, continuing to take bug report.", e);
@@ -307,14 +364,21 @@
mCallback = new CarBugreportManager.CarBugreportManagerCallback() {
@Override
public void onError(int errorCode) {
- Log.e(TAG, "Bugreport failed " + errorCode);
- showToast(R.string.toast_status_failed);
- // TODO(b/133520419): show this error on Info page or add to zip file.
- scheduleZipTask();
+ 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);
}
@Override
@@ -325,86 +389,102 @@
@Override
public void onFinished() {
- Log.i(TAG, "Bugreport finished");
- scheduleZipTask();
+ Log.d(TAG, "CarBugreportManager finished");
mBugReportProgress.set(MAX_PROGRESS_VALUE);
sendProgressEventToHandler(MAX_PROGRESS_VALUE);
+ mSingleThreadExecutor.submit(BugReportService.this::zipDirectoryAndUpdateStatus);
}
};
mBugreportManager.requestBugreport(outFd, extraOutFd, mCallback);
}
- private void scheduleZipTask() {
- mSingleThreadExecutor.submit(this::zipDirectoryAndScheduleForUpload);
- }
-
/**
* Shows a clickable bugreport finished notification. When clicked it opens
* {@link BugReportInfoActivity}.
*/
- private void showBugReportFinishedNotification() {
- Intent intent = new Intent(getApplicationContext(), BugReportInfoActivity.class);
+ static void showBugReportFinishedNotification(Context context, MetaBugReport bug) {
+ Intent intent = new Intent(context, BugReportInfoActivity.class);
PendingIntent startBugReportInfoActivity =
- PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
+ PendingIntent.getActivity(context, 0, intent, 0);
Notification notification = new Notification
- .Builder(getApplicationContext(), STATUS_CHANNEL_ID)
- .setContentTitle(getText(R.string.notification_bugreport_finished_title))
- .setContentText(getText(JobSchedulingUtils.uploadByDefault()
- ? R.string.notification_bugreport_auto_upload_finished_text
- : R.string.notification_bugreport_manual_upload_finished_text))
+ .Builder(context, STATUS_CHANNEL_ID)
+ .setContentTitle(context.getText(R.string.notification_bugreport_finished_title))
+ .setContentText(bug.getTitle())
.setCategory(Notification.CATEGORY_STATUS)
.setSmallIcon(R.drawable.ic_upload)
.setContentIntent(startBugReportInfoActivity)
.build();
- mNotificationManager.notify(BUGREPORT_FINISHED_NOTIF_ID, notification);
+ context.getSystemService(NotificationManager.class)
+ .notify(BUGREPORT_FINISHED_NOTIF_ID, notification);
}
- private void zipDirectoryAndScheduleForUpload() {
+ /**
+ * Zips the temp directory, writes to the system user's {@link FileUtils#getPendingDir} and
+ * updates the bug report status.
+ *
+ * <p>For {@link MetaBugReport#TYPE_INTERACTIVE}: Sets status to either STATUS_UPLOAD_PENDING or
+ * STATUS_PENDING_USER_ACTION and shows a regular notification.
+ *
+ * <p>For {@link MetaBugReport#TYPE_SILENT}: Sets status to STATUS_AUDIO_PENDING and shows
+ * a dialog to record audio message.
+ */
+ private void zipDirectoryAndUpdateStatus() {
try {
- // When OutputStream from openBugReportFile is closed, BugStorageProvider automatically
- // schedules an upload job.
- zipDirectoryToOutputStream(
- FileUtils.createTempDir(this, mMetaBugReport.getTimestamp()),
- BugStorageUtils.openBugReportFile(this, mMetaBugReport));
- showBugReportFinishedNotification();
+ // All the generated zip files, images and audio messages are located in this dir.
+ // This is located under the current user.
+ String bugreportFileName = FileUtils.getZipFileName(mMetaBugReport);
+ Log.d(TAG, "Zipping bugreport into " + bugreportFileName);
+ mMetaBugReport = BugStorageUtils.update(this,
+ mMetaBugReport.toBuilder().setBugReportFileName(bugreportFileName).build());
+ 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,
MESSAGE_FAILURE_ZIP);
showToast(R.string.toast_status_failed);
+ return;
}
- mIsCollectingBugReport.set(false);
- showToast(R.string.toast_status_finished);
- mHandler.post(() -> stopForeground(true));
+ if (mMetaBugReport.getType() == MetaBugReport.TYPE_SILENT) {
+ BugStorageUtils.setBugReportStatus(BugReportService.this,
+ mMetaBugReport, Status.STATUS_AUDIO_PENDING, /* message= */ "");
+ playNotificationSound();
+ startActivity(BugReportActivity.buildAddAudioIntent(this, mMetaBugReport));
+ } else {
+ // NOTE: If bugreport type is INTERACTIVE, it will already contain an audio message.
+ Status status = mConfig.getAutoUpload()
+ ? Status.STATUS_UPLOAD_PENDING : Status.STATUS_PENDING_USER_ACTION;
+ BugStorageUtils.setBugReportStatus(BugReportService.this,
+ mMetaBugReport, status, /* message= */ "");
+ showBugReportFinishedNotification(this, mMetaBugReport);
+ }
+ mHandler.post(() -> {
+ mNotificationManager.cancel(BUGREPORT_IN_PROGRESS_NOTIF_ID);
+ stopForeground(true);
+ });
+ mHandlerToast.removeCallbacksAndMessages(null);
}
- @Override
- public void onDestroy() {
- if (DEBUG) {
- Log.d(TAG, "Service destroyed");
+ private void playNotificationSound() {
+ Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
+ Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), notification);
+ if (ringtone == null) {
+ Log.w(TAG, "No notification ringtone found.");
+ return;
}
- }
-
- private static void copyBinaryStream(InputStream in, OutputStream out) throws IOException {
- OutputStream writer = null;
- InputStream reader = null;
- try {
- writer = new DataOutputStream(out);
- reader = new DataInputStream(in);
- rawCopyStream(writer, reader);
- } finally {
- IoUtils.closeQuietly(reader);
- IoUtils.closeQuietly(writer);
+ float volume = ringtone.getVolume();
+ // Use volume from audio manager, otherwise default ringtone volume can be too loud.
+ AudioManager audioManager = getSystemService(AudioManager.class);
+ if (audioManager != null) {
+ int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION);
+ int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION);
+ volume = (currentVolume + 0.0f) / maxVolume;
}
- }
-
- // does not close the reader or writer.
- private static void rawCopyStream(OutputStream writer, InputStream reader) throws IOException {
- int read;
- byte[] buf = new byte[8192];
- while ((read = reader.read(buf, 0, buf.length)) > 0) {
- writer.write(buf, 0, read);
- }
+ Log.v(TAG, "Using volume " + volume);
+ ringtone.setVolume(volume);
+ ringtone.play();
}
/**
@@ -425,59 +505,23 @@
Log.v(TAG, "zipping directory " + dirToZip.getAbsolutePath());
File[] listFiles = dirToZip.listFiles();
- ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(outStream));
- try {
+ try (ZipOutputStream zipStream = new ZipOutputStream(new BufferedOutputStream(outStream))) {
for (File file : listFiles) {
if (file.isDirectory()) {
continue;
}
- if (file.length() == 0) {
- // If there were issues with reading from dumpstate socket, the dumpstate zip
- // file still might be available in
- // /data/user_de/0/com.android.shell/files/bugreports/.
- Log.w(TAG, "File " + file.getName() + " is empty, skipping.");
- return;
- }
String filename = file.getName();
-
// only for the zipped output file, we add individual entries to zip file.
if (filename.equals(OUTPUT_ZIP_FILE) || filename.equals(EXTRA_OUTPUT_ZIP_FILE)) {
- extractZippedFileToOutputStream(file, zipStream);
+ ZipUtils.extractZippedFileToZipStream(file, zipStream);
} else {
- try (FileInputStream reader = new FileInputStream(file)) {
- addFileToOutputStream(filename, reader, zipStream);
- }
+ ZipUtils.addFileToZipStream(file, zipStream);
}
}
} finally {
- zipStream.close();
outStream.close();
}
// Zipping successful, now cleanup the temp dir.
FileUtils.deleteDirectory(dirToZip);
}
-
- private void extractZippedFileToOutputStream(File file, ZipOutputStream zipStream)
- throws IOException {
- ZipFile zipFile = new ZipFile(file);
- Enumeration<? extends ZipEntry> entries = zipFile.entries();
- while (entries.hasMoreElements()) {
- ZipEntry entry = entries.nextElement();
- try (InputStream stream = zipFile.getInputStream(entry)) {
- addFileToOutputStream(entry.getName(), stream, zipStream);
- }
- }
- }
-
- private void addFileToOutputStream(
- String filename, InputStream reader, ZipOutputStream zipStream) {
- ZipEntry entry = new ZipEntry(filename);
- try {
- zipStream.putNextEntry(entry);
- rawCopyStream(zipStream, reader);
- zipStream.closeEntry();
- } catch (IOException e) {
- Log.w(TAG, "Failed to add file " + filename + " to the zip.", e);
- }
- }
}
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
index 7a4b3de..d9e271e 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageProvider.java
@@ -17,6 +17,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.StringDef;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
@@ -26,13 +27,19 @@
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.CancellationSignal;
-import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.util.Log;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+
import java.io.File;
+import java.io.FileDescriptor;
import java.io.FileNotFoundException;
-import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.function.Function;
/**
@@ -40,32 +47,76 @@
* In Android Automotive user 0 runs as the system and all the time, while other users won't once
* their session ends. This content provider enables bug reports to be uploaded even after
* user session ends.
+ *
+ * <p>A bugreport constists of two files: bugreport zip file and audio file. Audio file is added
+ * later through notification. {@link SimpleUploaderAsyncTask} merges two files into one zip file
+ * before uploading.
+ *
+ * <p>All files are stored under system user's {@link FileUtils#getPendingDir}.
*/
public class BugStorageProvider extends ContentProvider {
private static final String TAG = BugStorageProvider.class.getSimpleName();
private static final String AUTHORITY = "com.google.android.car.bugreport";
private static final String BUG_REPORTS_TABLE = "bugreports";
- static final Uri BUGREPORT_CONTENT_URI =
- Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE);
- static final String COLUMN_ID = "_ID";
- static final String COLUMN_USERNAME = "username";
- static final String COLUMN_TITLE = "title";
- static final String COLUMN_TIMESTAMP = "timestamp";
- static final String COLUMN_DESCRIPTION = "description";
- static final String COLUMN_FILEPATH = "filepath";
- static final String COLUMN_STATUS = "status";
- static final String COLUMN_STATUS_MESSAGE = "message";
+ /** Deletes files associated with a bug report. */
+ static final String URL_SEGMENT_DELETE_FILES = "deleteZipFile";
+ /** Destructively deletes a bug report. */
+ static final String URL_SEGMENT_COMPLETE_DELETE = "completeDelete";
+ /** Opens bugreport file of a bug report, uses column {@link #COLUMN_BUGREPORT_FILENAME}. */
+ static final String URL_SEGMENT_OPEN_BUGREPORT_FILE = "openBugReportFile";
+ /** Opens audio file of a bug report, uses column {@link #URL_MATCHED_OPEN_AUDIO_FILE}. */
+ static final String URL_SEGMENT_OPEN_AUDIO_FILE = "openAudioFile";
+ /**
+ * Opens final bugreport zip file, uses column {@link #COLUMN_FILEPATH}.
+ *
+ * <p>NOTE: This is the old way of storing final zipped bugreport. In
+ * {@code BugStorageProvider#AUDIO_VERSION} {@link #COLUMN_FILEPATH} is dropped. But there are
+ * still some devices with this field set.
+ */
+ static final String URL_SEGMENT_OPEN_FILE = "openFile";
// URL Matcher IDs.
private static final int URL_MATCHED_BUG_REPORTS_URI = 1;
private static final int URL_MATCHED_BUG_REPORT_ID_URI = 2;
+ private static final int URL_MATCHED_DELETE_FILES = 3;
+ private static final int URL_MATCHED_COMPLETE_DELETE = 4;
+ private static final int URL_MATCHED_OPEN_BUGREPORT_FILE = 5;
+ private static final int URL_MATCHED_OPEN_AUDIO_FILE = 6;
+ private static final int URL_MATCHED_OPEN_FILE = 7;
- private Handler mHandler;
+ @StringDef({
+ URL_SEGMENT_DELETE_FILES,
+ URL_SEGMENT_COMPLETE_DELETE,
+ URL_SEGMENT_OPEN_BUGREPORT_FILE,
+ URL_SEGMENT_OPEN_AUDIO_FILE,
+ URL_SEGMENT_OPEN_FILE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface UriActionSegments {}
+
+ static final Uri BUGREPORT_CONTENT_URI =
+ Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE);
+
+ /** See {@link MetaBugReport} for column descriptions. */
+ static final String COLUMN_ID = "_ID";
+ static final String COLUMN_USERNAME = "username";
+ static final String COLUMN_TITLE = "title";
+ static final String COLUMN_TIMESTAMP = "timestamp";
+ /** not used anymore */
+ static final String COLUMN_DESCRIPTION = "description";
+ /** not used anymore, but some devices still might have bugreports with this field set. */
+ static final String COLUMN_FILEPATH = "filepath";
+ static final String COLUMN_STATUS = "status";
+ static final String COLUMN_STATUS_MESSAGE = "message";
+ static final String COLUMN_TYPE = "type";
+ static final String COLUMN_BUGREPORT_FILENAME = "bugreport_filename";
+ static final String COLUMN_AUDIO_FILENAME = "audio_filename";
private DatabaseHelper mDatabaseHelper;
private final UriMatcher mUriMatcher;
+ private Config mConfig;
/**
* A helper class to work with sqlite database.
@@ -78,9 +129,13 @@
/**
* All changes in database versions should be recorded here.
* 1: Initial version.
+ * 2: Add integer column details_needed.
+ * 3: Add string column audio_filename and bugreport_filename.
*/
private static final int INITIAL_VERSION = 1;
- private static final int DATABASE_VERSION = INITIAL_VERSION;
+ private static final int TYPE_VERSION = 2;
+ private static final int AUDIO_VERSION = 3;
+ private static final int DATABASE_VERSION = AUDIO_VERSION;
private static final String CREATE_TABLE = "CREATE TABLE " + BUG_REPORTS_TABLE + " ("
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
@@ -90,7 +145,10 @@
+ COLUMN_DESCRIPTION + " TEXT NULL,"
+ COLUMN_FILEPATH + " TEXT DEFAULT NULL,"
+ COLUMN_STATUS + " INTEGER DEFAULT " + Status.STATUS_WRITE_PENDING.getValue() + ","
- + COLUMN_STATUS_MESSAGE + " TEXT NULL"
+ + COLUMN_STATUS_MESSAGE + " TEXT NULL,"
+ + COLUMN_TYPE + " INTEGER DEFAULT " + MetaBugReport.TYPE_INTERACTIVE + ","
+ + COLUMN_BUGREPORT_FILENAME + " TEXT DEFAULT NULL,"
+ + COLUMN_AUDIO_FILENAME + " TEXT DEFAULT NULL"
+ ");";
DatabaseHelper(Context context) {
@@ -105,24 +163,56 @@
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading from " + oldVersion + " to " + newVersion);
+ if (oldVersion < TYPE_VERSION) {
+ db.execSQL("ALTER TABLE " + BUG_REPORTS_TABLE + " ADD COLUMN "
+ + COLUMN_TYPE + " INTEGER DEFAULT " + MetaBugReport.TYPE_INTERACTIVE);
+ }
+ if (oldVersion < AUDIO_VERSION) {
+ db.execSQL("ALTER TABLE " + BUG_REPORTS_TABLE + " ADD COLUMN "
+ + COLUMN_BUGREPORT_FILENAME + " TEXT DEFAULT NULL");
+ db.execSQL("ALTER TABLE " + BUG_REPORTS_TABLE + " ADD COLUMN "
+ + COLUMN_AUDIO_FILENAME + " TEXT DEFAULT NULL");
+ }
}
}
- /** Builds {@link Uri} that points to a bugreport entry with provided bugreport id. */
- static Uri buildUriWithBugId(int bugReportId) {
- return Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE + "/" + bugReportId);
+ /**
+ * Builds an {@link Uri} that points to the single bug report and performs an action
+ * defined by given URI segment.
+ */
+ static Uri buildUriWithSegment(int bugReportId, @UriActionSegments String segment) {
+ return Uri.parse("content://" + AUTHORITY + "/" + BUG_REPORTS_TABLE + "/"
+ + segment + "/" + bugReportId);
}
public BugStorageProvider() {
mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mUriMatcher.addURI(AUTHORITY, BUG_REPORTS_TABLE, URL_MATCHED_BUG_REPORTS_URI);
mUriMatcher.addURI(AUTHORITY, BUG_REPORTS_TABLE + "/#", URL_MATCHED_BUG_REPORT_ID_URI);
+ mUriMatcher.addURI(
+ AUTHORITY, BUG_REPORTS_TABLE + "/" + URL_SEGMENT_DELETE_FILES + "/#",
+ URL_MATCHED_DELETE_FILES);
+ mUriMatcher.addURI(
+ AUTHORITY, BUG_REPORTS_TABLE + "/" + URL_SEGMENT_COMPLETE_DELETE + "/#",
+ URL_MATCHED_COMPLETE_DELETE);
+ mUriMatcher.addURI(
+ AUTHORITY, BUG_REPORTS_TABLE + "/" + URL_SEGMENT_OPEN_BUGREPORT_FILE + "/#",
+ URL_MATCHED_OPEN_BUGREPORT_FILE);
+ mUriMatcher.addURI(
+ AUTHORITY, BUG_REPORTS_TABLE + "/" + URL_SEGMENT_OPEN_AUDIO_FILE + "/#",
+ URL_MATCHED_OPEN_AUDIO_FILE);
+ mUriMatcher.addURI(
+ AUTHORITY, BUG_REPORTS_TABLE + "/" + URL_SEGMENT_OPEN_FILE + "/#",
+ URL_MATCHED_OPEN_FILE);
}
@Override
public boolean onCreate() {
+ Preconditions.checkState(Config.isBugReportEnabled(), "BugReport is disabled.");
+
mDatabaseHelper = new DatabaseHelper(getContext());
- mHandler = new Handler();
+ mConfig = new Config();
+ mConfig.start();
return true;
}
@@ -181,10 +271,6 @@
switch (mUriMatcher.match(uri)) {
case URL_MATCHED_BUG_REPORTS_URI:
table = BUG_REPORTS_TABLE;
- String filepath = FileUtils.getZipFile(getContext(),
- (String) values.get(COLUMN_TIMESTAMP),
- (String) values.get(COLUMN_USERNAME)).getPath();
- values.put(COLUMN_FILEPATH, filepath);
break;
default:
throw new IllegalArgumentException("unknown uri" + uri);
@@ -203,31 +289,46 @@
@Nullable
@Override
public String getType(@NonNull Uri uri) {
- if (mUriMatcher.match(uri) != URL_MATCHED_BUG_REPORT_ID_URI) {
- throw new IllegalArgumentException("unknown uri:" + uri);
+ switch (mUriMatcher.match(uri)) {
+ case URL_MATCHED_OPEN_BUGREPORT_FILE:
+ case URL_MATCHED_OPEN_FILE:
+ return "application/zip";
+ case URL_MATCHED_OPEN_AUDIO_FILE:
+ return "audio/3gpp";
+ default:
+ throw new IllegalArgumentException("unknown uri:" + uri);
}
- // We only store zip files in this provider.
- return "application/zip";
}
@Override
public int delete(
@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+ SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
switch (mUriMatcher.match(uri)) {
- // returns the bugreport that match the id.
- case URL_MATCHED_BUG_REPORT_ID_URI:
+ case URL_MATCHED_DELETE_FILES:
if (selection != null || selectionArgs != null) {
throw new IllegalArgumentException("selection is not allowed for "
- + URL_MATCHED_BUG_REPORT_ID_URI);
+ + URL_MATCHED_DELETE_FILES);
+ }
+ if (deleteFilesFor(getBugReportFromUri(uri))) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ return 1;
+ }
+ return 0;
+ case URL_MATCHED_COMPLETE_DELETE:
+ if (selection != null || selectionArgs != null) {
+ throw new IllegalArgumentException("selection is not allowed for "
+ + URL_MATCHED_COMPLETE_DELETE);
}
selection = COLUMN_ID + " = ?";
selectionArgs = new String[]{uri.getLastPathSegment()};
- break;
+ // Ignore the results of zip file deletion, possibly it wasn't even created.
+ deleteFilesFor(getBugReportFromUri(uri));
+ getContext().getContentResolver().notifyChange(uri, null);
+ return db.delete(BUG_REPORTS_TABLE, selection, selectionArgs);
default:
throw new IllegalArgumentException("Unknown URL " + uri);
}
- SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
- return db.delete(BUG_REPORTS_TABLE, selection, selectionArgs);
}
@Override
@@ -263,77 +364,72 @@
}
/**
- * This is called when the OutputStream is requested by
- * {@link BugStorageUtils#openBugReportFile}.
+ * This is called when a file is opened.
*
- * It expects the file to be a zip file and schedules an upload under the primary user.
+ * <p>See {@link BugStorageUtils#openBugReportFileToWrite},
+ * {@link BugStorageUtils#openAudioMessageFileToWrite}.
*/
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
throws FileNotFoundException {
- if (mUriMatcher.match(uri) != URL_MATCHED_BUG_REPORT_ID_URI) {
- throw new IllegalArgumentException("unknown uri:" + uri);
+ Function<MetaBugReport, String> fileNameExtractor;
+ switch (mUriMatcher.match(uri)) {
+ case URL_MATCHED_OPEN_BUGREPORT_FILE:
+ fileNameExtractor = MetaBugReport::getBugReportFileName;
+ break;
+ case URL_MATCHED_OPEN_AUDIO_FILE:
+ fileNameExtractor = MetaBugReport::getAudioFileName;
+ break;
+ case URL_MATCHED_OPEN_FILE:
+ File file = new File(getBugReportFromUri(uri).getFilePath());
+ Log.v(TAG, "Opening file " + file + " with mode " + mode);
+ return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
+ default:
+ throw new IllegalArgumentException("unknown uri:" + uri);
}
-
- Cursor c = query(uri, new String[]{COLUMN_FILEPATH}, null, null, null);
- int count = (c != null) ? c.getCount() : 0;
- if (count != 1) {
- // If there is not exactly one result, throw an appropriate
- // exception.
- if (c != null) {
- c.close();
- }
- if (count == 0) {
- throw new FileNotFoundException("No entry for " + uri);
- }
- throw new FileNotFoundException("Multiple items at " + uri);
- }
-
- c.moveToFirst();
- int i = c.getColumnIndex(COLUMN_FILEPATH);
- String path = (i >= 0 ? c.getString(i) : null);
- c.close();
- if (path == null) {
- throw new FileNotFoundException("Column for path not found.");
- }
-
+ // URI contains bugreport ID as the last segment, see the matched urls.
+ MetaBugReport bugReport = getBugReportFromUri(uri);
+ File file = new File(
+ FileUtils.getPendingDir(getContext()), fileNameExtractor.apply(bugReport));
+ Log.v(TAG, "Opening file " + file + " with mode " + mode);
int modeBits = ParcelFileDescriptor.parseMode(mode);
- try {
- return ParcelFileDescriptor.open(new File(path), modeBits, mHandler, e -> {
- if (mode.equals("r")) {
- Log.i(TAG, "File " + path + " opened in read-only mode.");
- return;
- } else if (!mode.equals("w")) {
- Log.e(TAG, "Only read-only or write-only mode supported; mode=" + mode);
- return;
- }
- Log.i(TAG, "File " + path + " opened in write-only mode.");
- Status status;
- if (e == null) {
- // success writing the file. Update the field to indicate bugreport
- // is ready for upload
- status = JobSchedulingUtils.uploadByDefault() ? Status.STATUS_UPLOAD_PENDING
- : Status.STATUS_PENDING_USER_ACTION;
- } else {
- // We log it and ignore it
- Log.e(TAG, "Bug report file write failed ", e);
- status = Status.STATUS_WRITE_FAILED;
- }
- SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
- ContentValues values = new ContentValues();
- values.put(COLUMN_STATUS, status.getValue());
- db.update(BUG_REPORTS_TABLE, values, COLUMN_ID + "=?",
- new String[]{ uri.getLastPathSegment() });
- if (status == Status.STATUS_UPLOAD_PENDING) {
- JobSchedulingUtils.scheduleUploadJob(BugStorageProvider.this.getContext());
- }
- Log.i(TAG, "Finished adding bugreport " + path + " " + uri);
- });
- } catch (IOException e) {
- // An IOException (for example not being able to open the file, will crash us.
- // That is ok.
- throw new RuntimeException(e);
+ return ParcelFileDescriptor.open(file, modeBits);
+ }
+
+ private MetaBugReport getBugReportFromUri(@NonNull Uri uri) {
+ int bugreportId = Integer.parseInt(uri.getLastPathSegment());
+ return BugStorageUtils.findBugReport(getContext(), bugreportId)
+ .orElseThrow(() -> new IllegalArgumentException("No record found for " + uri));
+ }
+
+ /**
+ * Print the Provider's state into the given stream. This gets invoked if
+ * you run "dumpsys activity provider com.google.android.car.bugreport/.BugStorageProvider".
+ *
+ * @param fd The raw file descriptor that the dump is being sent to.
+ * @param writer The PrintWriter to which you should dump your state. This will be
+ * closed for you after you return.
+ * @param args additional arguments to the dump request.
+ */
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ writer.println("BugStorageProvider:");
+ mConfig.dump(/* prefix= */ " ", writer);
+ }
+
+ private boolean deleteFilesFor(MetaBugReport bugReport) {
+ if (!Strings.isNullOrEmpty(bugReport.getFilePath())) {
+ // Old bugreports have only filePath.
+ return new File(bugReport.getFilePath()).delete();
}
+ File pendingDir = FileUtils.getPendingDir(getContext());
+ boolean result = true;
+ if (!Strings.isNullOrEmpty(bugReport.getAudioFileName())) {
+ result = new File(pendingDir, bugReport.getAudioFileName()).delete();
+ }
+ if (!Strings.isNullOrEmpty(bugReport.getBugReportFileName())) {
+ result = result && new File(pendingDir, bugReport.getBugReportFileName()).delete();
+ }
+ return result;
}
}
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java
index cbb1a3c..a009129 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/BugStorageUtils.java
@@ -15,12 +15,15 @@
*/
package com.google.android.car.bugreport;
+import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_AUDIO_FILENAME;
+import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_BUGREPORT_FILENAME;
import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_FILEPATH;
import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_ID;
import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_STATUS;
import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_STATUS_MESSAGE;
import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_TIMESTAMP;
import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_TITLE;
+import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_TYPE;
import static com.google.android.car.bugreport.BugStorageProvider.COLUMN_USERNAME;
import android.annotation.NonNull;
@@ -30,18 +33,21 @@
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
-import android.text.TextUtils;
import android.util.Log;
import com.google.api.client.auth.oauth2.TokenResponseException;
+import com.google.common.base.Strings;
import java.io.FileNotFoundException;
+import java.io.InputStream;
import java.io.OutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
+import java.util.Optional;
/**
* A class that hides details when communicating with the bug storage provider.
@@ -68,6 +74,7 @@
* @param title - title of the bug report.
* @param timestamp - timestamp when the bug report was initiated.
* @param username - current user name. Note, it's a user name, not an account name.
+ * @param type - bug report type, {@link MetaBugReport.BugReportType}.
* @return an instance of {@link MetaBugReport} that was created in a database.
*/
@NonNull
@@ -75,68 +82,95 @@
@NonNull Context context,
@NonNull String title,
@NonNull String timestamp,
- @NonNull String username) {
+ @NonNull String username,
+ @MetaBugReport.BugReportType int type) {
// insert bug report username and title
ContentValues values = new ContentValues();
values.put(COLUMN_TITLE, title);
values.put(COLUMN_TIMESTAMP, timestamp);
values.put(COLUMN_USERNAME, username);
+ values.put(COLUMN_TYPE, type);
ContentResolver r = context.getContentResolver();
Uri uri = r.insert(BugStorageProvider.BUGREPORT_CONTENT_URI, values);
-
- Cursor c = r.query(uri, new String[]{COLUMN_ID}, null, null, null);
- int count = (c == null) ? 0 : c.getCount();
- if (count != 1) {
- throw new RuntimeException("Could not create a bug report entry.");
- }
- c.moveToFirst();
- int id = getInt(c, COLUMN_ID);
- c.close();
- return new MetaBugReport.Builder(id, timestamp)
- .setTitle(title)
- .setUserName(username)
- .build();
+ return findBugReport(context, Integer.parseInt(uri.getLastPathSegment())).get();
}
- /**
- * Returns a file stream to write the zipped file to. The content provider listens for file
- * descriptor to be closed, and as soon as it is closed, {@link BugStorageProvider} schedules
- * it for upload.
- *
- * @param context - an application context.
- * @param metaBugReport - a bug report.
- * @return a file descriptor where a zip content should be written.
- */
+ /** Returns an output stream to write the zipped file to. */
@NonNull
- static OutputStream openBugReportFile(
+ static OutputStream openBugReportFileToWrite(
@NonNull Context context, @NonNull MetaBugReport metaBugReport)
throws FileNotFoundException {
ContentResolver r = context.getContentResolver();
+ return r.openOutputStream(BugStorageProvider.buildUriWithSegment(
+ metaBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_BUGREPORT_FILE));
+ }
- // Write the file. When file is closed, bug report record status
- // will automatically be made ready for uploading.
- return r.openOutputStream(BugStorageProvider.buildUriWithBugId(metaBugReport.getId()));
+ /** Returns an output stream to write the audio message file to. */
+ static OutputStream openAudioMessageFileToWrite(
+ @NonNull Context context, @NonNull MetaBugReport metaBugReport)
+ throws FileNotFoundException {
+ ContentResolver r = context.getContentResolver();
+ return r.openOutputStream(BugStorageProvider.buildUriWithSegment(
+ metaBugReport.getId(), BugStorageProvider.URL_SEGMENT_OPEN_AUDIO_FILE));
}
/**
- * Deletes {@link MetaBugReport} record from a local database. Returns true if the record was
- * deleted.
+ * Returns an input stream to read the final zip file from.
+ *
+ * <p>NOTE: This is the old way of storing final zipped bugreport. See
+ * {@link BugStorageProvider#URL_SEGMENT_OPEN_FILE} for more info.
+ */
+ static InputStream openFileToRead(Context context, MetaBugReport bug)
+ throws FileNotFoundException {
+ return context.getContentResolver().openInputStream(
+ BugStorageProvider.buildUriWithSegment(
+ bug.getId(), BugStorageProvider.URL_SEGMENT_OPEN_FILE));
+ }
+
+ /** Returns an input stream to read the bug report zip file from. */
+ static InputStream openBugReportFileToRead(Context context, MetaBugReport bug)
+ throws FileNotFoundException {
+ return context.getContentResolver().openInputStream(
+ BugStorageProvider.buildUriWithSegment(
+ bug.getId(), BugStorageProvider.URL_SEGMENT_OPEN_BUGREPORT_FILE));
+ }
+
+ /** Returns an input stream to read the audio file from. */
+ static InputStream openAudioFileToRead(Context context, MetaBugReport bug)
+ throws FileNotFoundException {
+ return context.getContentResolver().openInputStream(
+ BugStorageProvider.buildUriWithSegment(
+ bug.getId(), BugStorageProvider.URL_SEGMENT_OPEN_AUDIO_FILE));
+ }
+
+ /**
+ * Deletes {@link MetaBugReport} record from a local database and deletes the associated file.
+ *
+ * <p>WARNING: destructive operation.
*
* @param context - an application context.
* @param bugReportId - a bug report id.
* @return true if the record was deleted.
*/
- static boolean deleteBugReport(@NonNull Context context, int bugReportId) {
+ static boolean completeDeleteBugReport(@NonNull Context context, int bugReportId) {
ContentResolver r = context.getContentResolver();
- return r.delete(BugStorageProvider.buildUriWithBugId(bugReportId), null, null) == 1;
+ return r.delete(BugStorageProvider.buildUriWithSegment(
+ bugReportId, BugStorageProvider.URL_SEGMENT_COMPLETE_DELETE), null, null) == 1;
+ }
+
+ /** Deletes all files for given bugreport id; doesn't delete sqlite3 record. */
+ static boolean deleteBugReportFiles(@NonNull Context context, int bugReportId) {
+ ContentResolver r = context.getContentResolver();
+ return r.delete(BugStorageProvider.buildUriWithSegment(
+ bugReportId, BugStorageProvider.URL_SEGMENT_DELETE_FILES), null, null) == 1;
}
/**
- * Returns bugreports that are waiting to be uploaded.
+ * Returns all the bugreports that are waiting to be uploaded.
*/
@NonNull
- public static List<MetaBugReport> getPendingBugReports(@NonNull Context context) {
+ public static List<MetaBugReport> getUploadPendingBugReports(@NonNull Context context) {
String selection = COLUMN_STATUS + "=?";
String[] selectionArgs = new String[]{
Integer.toString(Status.STATUS_UPLOAD_PENDING.getValue())};
@@ -152,17 +186,32 @@
return getBugreports(context, null, null, COLUMN_ID + " DESC");
}
- private static List<MetaBugReport> getBugreports(Context context, String selection,
- String[] selectionArgs, String order) {
+ /** Returns {@link MetaBugReport} for given bugreport id. */
+ static Optional<MetaBugReport> findBugReport(Context context, int bugreportId) {
+ String selection = COLUMN_ID + " = ?";
+ String[] selectionArgs = new String[]{Integer.toString(bugreportId)};
+ List<MetaBugReport> bugs = BugStorageUtils.getBugreports(
+ context, selection, selectionArgs, null);
+ if (bugs.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(bugs.get(0));
+ }
+
+ private static List<MetaBugReport> getBugreports(
+ Context context, String selection, String[] selectionArgs, String order) {
ArrayList<MetaBugReport> bugReports = new ArrayList<>();
String[] projection = {
COLUMN_ID,
COLUMN_USERNAME,
COLUMN_TITLE,
COLUMN_TIMESTAMP,
+ COLUMN_BUGREPORT_FILENAME,
+ COLUMN_AUDIO_FILENAME,
COLUMN_FILEPATH,
COLUMN_STATUS,
- COLUMN_STATUS_MESSAGE};
+ COLUMN_STATUS_MESSAGE,
+ COLUMN_TYPE};
ContentResolver r = context.getContentResolver();
Cursor c = r.query(BugStorageProvider.BUGREPORT_CONTENT_URI, projection,
selection, selectionArgs, order);
@@ -171,13 +220,17 @@
if (count > 0) c.moveToFirst();
for (int i = 0; i < count; i++) {
- MetaBugReport meta = new MetaBugReport.Builder(getInt(c, COLUMN_ID),
- getString(c, COLUMN_TIMESTAMP))
+ MetaBugReport meta = MetaBugReport.builder()
+ .setId(getInt(c, COLUMN_ID))
+ .setTimestamp(getString(c, COLUMN_TIMESTAMP))
.setUserName(getString(c, COLUMN_USERNAME))
.setTitle(getString(c, COLUMN_TITLE))
- .setFilepath(getString(c, COLUMN_FILEPATH))
+ .setBugReportFileName(getString(c, COLUMN_BUGREPORT_FILENAME))
+ .setAudioFileName(getString(c, COLUMN_AUDIO_FILENAME))
+ .setFilePath(getString(c, COLUMN_FILEPATH))
.setStatus(getInt(c, COLUMN_STATUS))
.setStatusMessage(getString(c, COLUMN_STATUS_MESSAGE))
+ .setType(getInt(c, COLUMN_TYPE))
.build();
bugReports.add(meta);
c.moveToNext();
@@ -207,7 +260,7 @@
Log.w(TAG, "Column " + colName + " not found.");
return "";
}
- return c.getString(colIndex);
+ return Strings.nullToEmpty(c.getString(colIndex));
}
/**
@@ -219,13 +272,6 @@
}
/**
- * Sets bugreport status to upload failed.
- */
- public static void setUploadFailed(Context context, MetaBugReport bugReport, Exception e) {
- setBugReportStatus(context, bugReport, Status.STATUS_UPLOAD_FAILED, getRootCauseMessage(e));
- }
-
- /**
* Sets bugreport status pending, and update the message to last exception message.
*
* <p>Used when a transient error has occurred.
@@ -244,6 +290,22 @@
setBugReportStatus(context, bugReport, Status.STATUS_UPLOAD_PENDING, msg);
}
+ /**
+ * Sets {@link MetaBugReport} status {@link Status#STATUS_EXPIRED}.
+ * Deletes the associated zip file from disk.
+ *
+ * @return true if succeeded.
+ */
+ static boolean expireBugReport(@NonNull Context context,
+ @NonNull MetaBugReport metaBugReport, @NonNull Instant expiredAt) {
+ metaBugReport = setBugReportStatus(
+ context, metaBugReport, Status.STATUS_EXPIRED, "Expired on " + expiredAt);
+ if (metaBugReport.getStatus() != Status.STATUS_EXPIRED.getValue()) {
+ return false;
+ }
+ return deleteBugReportFiles(context, metaBugReport.getId());
+ }
+
/** Gets the root cause of the error. */
@NonNull
private static String getRootCauseMessage(@Nullable Throwable t) {
@@ -260,18 +322,52 @@
return t.getMessage();
}
- /** Updates bug report record status. */
- static void setBugReportStatus(
+ /**
+ * Updates bug report record status.
+ *
+ * <p>NOTE: When status is set to STATUS_UPLOAD_PENDING, BugStorageProvider automatically
+ * schedules the bugreport to be uploaded.
+ *
+ * @return Updated {@link MetaBugReport}.
+ */
+ static MetaBugReport setBugReportStatus(
Context context, MetaBugReport bugReport, Status status, String message) {
- // update status
+ return update(context, bugReport.toBuilder()
+ .setStatus(status.getValue())
+ .setStatusMessage(message)
+ .build());
+ }
+
+ /**
+ * Updates bug report record status.
+ *
+ * <p>NOTE: When status is set to STATUS_UPLOAD_PENDING, BugStorageProvider automatically
+ * schedules the bugreport to be uploaded.
+ *
+ * @return Updated {@link MetaBugReport}.
+ */
+ static MetaBugReport setBugReportStatus(
+ Context context, MetaBugReport bugReport, Status status, Exception e) {
+ return setBugReportStatus(context, bugReport, status, getRootCauseMessage(e));
+ }
+
+ /**
+ * Updates the bugreport and returns the updated version.
+ *
+ * <p>NOTE: doesn't update all the fields.
+ */
+ static MetaBugReport update(Context context, MetaBugReport bugReport) {
+ // Update only necessary fields.
ContentValues values = new ContentValues();
- values.put(COLUMN_STATUS, status.getValue());
- if (!TextUtils.isEmpty(message)) {
- values.put(COLUMN_STATUS_MESSAGE, message);
- }
+ values.put(COLUMN_BUGREPORT_FILENAME, bugReport.getBugReportFileName());
+ values.put(COLUMN_AUDIO_FILENAME, bugReport.getAudioFileName());
+ values.put(COLUMN_STATUS, bugReport.getStatus());
+ values.put(COLUMN_STATUS_MESSAGE, bugReport.getStatusMessage());
String where = COLUMN_ID + "=" + bugReport.getId();
- context.getContentResolver().update(BugStorageProvider.BUGREPORT_CONTENT_URI, values,
- where, null);
+ context.getContentResolver().update(
+ BugStorageProvider.BUGREPORT_CONTENT_URI, values, where, null);
+ return findBugReport(context, bugReport.getId()).orElseThrow(
+ () -> new IllegalArgumentException("Bug " + bugReport.getId() + " not found"));
}
private static String currentTimestamp() {
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/Config.java b/tests/BugReportApp/src/com/google/android/car/bugreport/Config.java
new file mode 100644
index 0000000..70f65bf
--- /dev/null
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/Config.java
@@ -0,0 +1,166 @@
+/*
+ * 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.app.ActivityThread;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.provider.DeviceConfig;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+
+/**
+ * Contains config for BugReport App.
+ *
+ * <p>The config is kept synchronized with {@code car} namespace. It's not defined in
+ * {@link DeviceConfig}.
+ *
+ * <ul>To get/set the flags via adb:
+ * <li>{@code adb shell device_config get car bugreport_upload_destination}
+ * <li>{@code adb shell device_config put car bugreport_upload_destination gcs}
+ * <li>{@code adb shell device_config delete car bugreport_upload_destination}
+ * </ul>
+ */
+final class Config {
+ private static final String TAG = Config.class.getSimpleName();
+
+ private static final String HAWK = "hawk";
+
+ /**
+ * Namespace for all Android Automotive related features.
+ *
+ * <p>In the future it will move to {@code DeviceConfig#NAMESPACE_CAR}.
+ */
+ private static final String NAMESPACE_CAR = "car";
+
+ /**
+ * A string flag, can be one of {@code null} or {@link #UPLOAD_DESTINATION_GCS}.
+ */
+ private static final String KEY_BUGREPORT_UPLOAD_DESTINATION = "bugreport_upload_destination";
+
+ /**
+ * A value for {@link #KEY_BUGREPORT_UPLOAD_DESTINATION}.
+ *
+ * Upload bugreports to GCS. Only works in {@code userdebug} or {@code eng} builds.
+ */
+ private static final String UPLOAD_DESTINATION_GCS = "gcs";
+
+ /**
+ * A system property to force enable the app bypassing the {@code userdebug/eng} build check.
+ */
+ private static final String PROP_FORCE_ENABLE = "android.car.bugreport.force_enable";
+
+ /**
+ * Temporary flag to retain the old behavior.
+ *
+ * Default is {@code true}.
+ *
+ * TODO(b/143183993): Disable auto-upload to GCS after testing DeviceConfig.
+ */
+ private static final String ENABLE_AUTO_UPLOAD = "android.car.bugreport.enableautoupload";
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private String mUploadDestination = null;
+
+ void start() {
+ DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_CAR,
+ ActivityThread.currentApplication().getMainExecutor(), this::onPropertiesChanged);
+ updateConstants();
+ }
+
+ private void onPropertiesChanged(DeviceConfig.Properties properties) {
+ if (properties.getKeyset().contains(KEY_BUGREPORT_UPLOAD_DESTINATION)) {
+ updateConstants();
+ }
+ }
+
+ /** Returns true if bugreport app is enabled for this device. */
+ static boolean isBugReportEnabled() {
+ return Build.IS_DEBUGGABLE || SystemProperties.getBoolean(PROP_FORCE_ENABLE, false);
+ }
+
+ /** If new bugreports should be scheduled for uploading. */
+ boolean getAutoUpload() {
+ if (isTempForceAutoUploadGcsEnabled()) {
+ Log.d(TAG, "Enabling auto-upload because ENABLE_AUTO_UPLOAD is true");
+ return true;
+ }
+ // TODO(b/144851443): Enable auto-upload only if upload destination is Gcs until
+ // we create a way to allow implementing OEMs custom upload logic.
+ return isUploadDestinationGcs();
+ }
+
+ /**
+ * Returns {@link true} if bugreport upload destination is GCS.
+ */
+ boolean isUploadDestinationGcs() {
+ if (isTempForceAutoUploadGcsEnabled()) {
+ Log.d(TAG, "Setting upload dest to GCS ENABLE_AUTO_UPLOAD is true");
+ return true;
+ }
+ // TODO(b/146214182): Enable uploading to GCS if the device is hawk.
+ if (HAWK.equals(Build.DEVICE) && Build.IS_DEBUGGABLE) {
+ return true;
+ }
+ // NOTE: enable it only for userdebug builds, unless it's force enabled using a system
+ // property.
+ return UPLOAD_DESTINATION_GCS.equals(getUploadDestination()) && Build.IS_DEBUGGABLE;
+ }
+
+ private static boolean isTempForceAutoUploadGcsEnabled() {
+ return SystemProperties.getBoolean(ENABLE_AUTO_UPLOAD, /* def= */ true);
+ }
+
+ /**
+ * Returns value of a flag {@link #KEY_BUGREPORT_UPLOAD_DESTINATION}.
+ */
+ private String getUploadDestination() {
+ synchronized (mLock) {
+ return mUploadDestination;
+ }
+ }
+
+ private void updateConstants() {
+ synchronized (mLock) {
+ mUploadDestination = DeviceConfig.getString(NAMESPACE_CAR,
+ KEY_BUGREPORT_UPLOAD_DESTINATION, /* defaultValue= */ null);
+ }
+ }
+
+ void dump(String prefix, PrintWriter pw) {
+ pw.println(prefix + "car.bugreport.Config:");
+
+ pw.print(prefix + " ");
+ pw.print("getAutoUpload");
+ pw.print("=");
+ pw.println(getAutoUpload() ? "true" : "false");
+
+ pw.print(prefix + " ");
+ pw.print("getUploadDestination");
+ pw.print("=");
+ pw.println(getUploadDestination());
+
+ pw.print(prefix + " ");
+ pw.print("isUploadDestinationGcs");
+ pw.print("=");
+ pw.println(isUploadDestinationGcs());
+ }
+}
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/FileUtils.java b/tests/BugReportApp/src/com/google/android/car/bugreport/FileUtils.java
index 47d05c6..b30035e 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/FileUtils.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/FileUtils.java
@@ -17,6 +17,8 @@
import android.content.Context;
+import com.google.common.base.Preconditions;
+
import java.io.File;
/**
@@ -34,14 +36,16 @@
*/
public class FileUtils {
private static final String PREFIX = "bugreport-";
- // bug reports waiting to be uploaded
+ /** A directory under the system user; contains bugreport zip files and audio files. */
private static final String PENDING_DIR = "bug_reports_pending";
- // temporary directory, used for zipping files
+ // Temporary directory under the current user, used for zipping files.
private static final String TEMP_DIR = "bug_reports_temp";
private static final String FS = "@";
- private static File getPendingDir(Context context) {
+ static File getPendingDir(Context context) {
+ Preconditions.checkArgument(context.getUser().isSystem(),
+ "Must be called from the system user.");
File dir = new File(context.getDataDir(), PENDING_DIR);
dir.mkdirs();
return dir;
@@ -62,15 +66,38 @@
* single file.
*/
static File getTempDir(Context context, String timestamp) {
+ Preconditions.checkArgument(!context.getUser().isSystem(),
+ "Must be called from the current user.");
return new File(context.getDataDir(), TEMP_DIR + "/" + timestamp);
}
/**
- * Returns zip file directory with the given timestamp and ldap
+ * Constructs a bugreport zip file name.
+ *
+ * <p>Add lookup code to the filename to allow matching audio file and bugreport file in USB.
*/
- static File getZipFile(Context context, String timestamp, String ldap) {
- File zipdir = getPendingDir(context);
- return new File(zipdir, PREFIX + ldap + FS + timestamp + ".zip");
+ static String getZipFileName(MetaBugReport bug) {
+ String lookupCode = extractLookupCode(bug);
+ return PREFIX + bug.getUserName() + FS + bug.getTimestamp() + "-" + lookupCode + ".zip";
+ }
+
+ /**
+ * Constructs a audio message file name.
+ *
+ * <p>Add lookup code to the filename to allow matching audio file and bugreport file in USB.
+ *
+ * @param timestamp - current timestamp, when audio was created.
+ * @param bug - a bug report.
+ */
+ static String getAudioFileName(String timestamp, MetaBugReport bug) {
+ String lookupCode = extractLookupCode(bug);
+ return PREFIX + bug.getUserName() + FS + timestamp + "-" + lookupCode + "-message.3gp";
+ }
+
+ private static String extractLookupCode(MetaBugReport bug) {
+ Preconditions.checkArgument(bug.getTitle().startsWith("["),
+ "Invalid bugreport title, doesn't contain lookup code. ");
+ return bug.getTitle().substring(1, BugReportActivity.LOOKUP_STRING_LENGTH + 1);
}
/**
@@ -99,7 +126,6 @@
return new File(getTempDir(context, timestamp), name);
}
-
/**
* Deletes a directory and its contents recursively
*
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java b/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
index 7a6de23..eac8b9a 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/JobSchedulingUtils.java
@@ -19,7 +19,6 @@
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
-import android.os.SystemProperties;
import android.util.Log;
/**
@@ -32,15 +31,7 @@
private static final int RETRY_DELAY_IN_MS = 5_000;
/**
- * The system property to disable auto-upload when bug reports are collected. When auto-upload
- * is disabled, the app waits for user action on collected bug reports: user can either
- * upload to Google Cloud or copy to flash drive.
- */
- private static final String PROP_DISABLE_AUTO_UPLOAD =
- "android.car.bugreport.disableautoupload";
-
- /**
- * Schedules an upload job under the current user.
+ * Schedules {@link UploadJob} under the current user.
*
* <p>Make sure this method is called under the primary user.
*
@@ -72,15 +63,4 @@
.setBackoffCriteria(RETRY_DELAY_IN_MS, JobInfo.BACKOFF_POLICY_LINEAR)
.build());
}
-
- /**
- * Returns true if collected bugreports should be uploaded automatically.
- *
- * <p>If it returns false, the app maps to an alternative workflow that requires user action
- * after bugreport is successfully written. A user then has an option to choose whether to
- * upload the bugreport or copy it to an external drive.
- */
- static boolean uploadByDefault() {
- return !SystemProperties.getBoolean(PROP_DISABLE_AUTO_UPLOAD, false);
- }
}
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java b/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java
index d309303..fcdb5b7 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/MetaBugReport.java
@@ -15,77 +15,95 @@
*/
package com.google.android.car.bugreport;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
import android.os.Parcel;
import android.os.Parcelable;
-/** Represents the information that a bugreport can contain. */
-public final class MetaBugReport implements Parcelable {
- private final int mId;
- private final String mTimestamp;
- private final String mTitle;
- private final String mUsername;
- private final String mFilePath;
- private final int mStatus;
- private final String mStatusMessage;
+import com.google.auto.value.AutoValue;
- private MetaBugReport(Builder builder) {
- mId = builder.mId;
- mTimestamp = builder.mTimestamp;
- mTitle = builder.mTitle;
- mUsername = builder.mUsername;
- mFilePath = builder.mFilePath;
- mStatus = builder.mStatus;
- mStatusMessage = builder.mStatusMessage;
- }
+import java.lang.annotation.Retention;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/** Represents the information that a bugreport can contain. */
+@AutoValue
+abstract class MetaBugReport implements Parcelable {
+
+ private static final DateFormat BUG_REPORT_TIMESTAMP_DATE_FORMAT =
+ new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
+
+ /** The app records audio message when initiated. Can change audio state. */
+ static final int TYPE_INTERACTIVE = 0;
+
+ /**
+ * The app doesn't show dialog and doesn't record audio when initiated. It allows user to
+ * add audio message when bugreport is collected.
+ */
+ static final int TYPE_SILENT = 1;
+
+ /** Annotation for bug report types. */
+ @Retention(SOURCE)
+ @IntDef({TYPE_INTERACTIVE, TYPE_SILENT})
+ @interface BugReportType {};
/**
* @return Id of the bug report. Bug report id monotonically increases and is unique.
*/
- public int getId() {
- return mId;
- }
+ public abstract int getId();
/**
* @return Username (LDAP) that created this bugreport
*/
- public String getUsername() {
- return mUsername == null ? "" : mUsername;
- }
+ public abstract String getUserName();
/**
* @return Title of the bug.
*/
- public String getTitle() {
- return mTitle == null ? "" : mTitle;
- }
+ public abstract String getTitle();
/**
* @return Timestamp when the bug report is initialized.
*/
- public String getTimestamp() {
- return mTimestamp == null ? "" : mTimestamp;
- }
+ public abstract String getTimestamp();
/**
- * @return path to the zip file
+ * @return path to the zip file stored under the system user.
+ *
+ * <p>NOTE: This is the old way of storing final zipped bugreport. See
+ * {@link BugStorageProvider#URL_SEGMENT_OPEN_FILE} for more info.
*/
- public String getFilePath() {
- return mFilePath == null ? "" : mFilePath;
- }
+ public abstract String getFilePath();
/**
- * @return Status of the bug upload.
+ * @return filename of the bug report zip file stored under the system user.
*/
- public int getStatus() {
- return mStatus;
- }
+ public abstract String getBugReportFileName();
+
+ /**
+ * @return filename of the audio message file stored under the system user.
+ */
+ public abstract String getAudioFileName();
+
+ /**
+ * @return {@link Status} of the bug upload.
+ */
+ public abstract int getStatus();
/**
* @return StatusMessage of the bug upload.
*/
- public String getStatusMessage() {
- return mStatusMessage == null ? "" : mStatusMessage;
- }
+ public abstract String getStatusMessage();
+
+ /**
+ * @return {@link BugReportType}.
+ */
+ public abstract int getType();
+
+ /** @return {@link Builder} from the meta bug report. */
+ public abstract Builder toBuilder();
@Override
public int describeContents() {
@@ -94,13 +112,33 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(mId);
- dest.writeString(mTimestamp);
- dest.writeString(mTitle);
- dest.writeString(mUsername);
- dest.writeString(mFilePath);
- dest.writeInt(mStatus);
- dest.writeString(mStatusMessage);
+ dest.writeInt(getId());
+ dest.writeString(getTimestamp());
+ dest.writeString(getTitle());
+ dest.writeString(getUserName());
+ dest.writeString(getFilePath());
+ dest.writeString(getBugReportFileName());
+ dest.writeString(getAudioFileName());
+ dest.writeInt(getStatus());
+ dest.writeString(getStatusMessage());
+ dest.writeInt(getType());
+ }
+
+ /** Converts {@link Date} to bugreport timestamp. */
+ static String toBugReportTimestamp(Date date) {
+ return BUG_REPORT_TIMESTAMP_DATE_FORMAT.format(date);
+ }
+
+ /** Creates a {@link Builder} with default, non-null values. */
+ static Builder builder() {
+ return new AutoValue_MetaBugReport.Builder()
+ .setTimestamp("")
+ .setFilePath("")
+ .setBugReportFileName("")
+ .setAudioFileName("")
+ .setStatusMessage("")
+ .setTitle("")
+ .setUserName("");
}
/** A creator that's used by Parcelable. */
@@ -112,14 +150,22 @@
String title = in.readString();
String username = in.readString();
String filePath = in.readString();
+ String bugReportFileName = in.readString();
+ String audioFileName = in.readString();
int status = in.readInt();
String statusMessage = in.readString();
- return new Builder(id, timestamp)
+ int type = in.readInt();
+ return MetaBugReport.builder()
+ .setId(id)
+ .setTimestamp(timestamp)
.setTitle(title)
.setUserName(username)
- .setFilepath(filePath)
+ .setFilePath(filePath)
+ .setBugReportFileName(bugReportFileName)
+ .setAudioFileName(audioFileName)
.setStatus(status)
.setStatusMessage(statusMessage)
+ .setType(type)
.build();
}
@@ -129,59 +175,38 @@
};
/** Builder for MetaBugReport. */
- public static class Builder {
- private final int mId;
- private final String mTimestamp;
- private String mTitle;
- private String mUsername;
- private String mFilePath;
- private int mStatus;
- private String mStatusMessage;
+ @AutoValue.Builder
+ abstract static class Builder {
+ /** Sets id. */
+ public abstract Builder setId(int id);
- /**
- * Initializes MetaBugReport.Builder.
- *
- * @param id - mandatory bugreport id
- * @param timestamp - mandatory timestamp when bugreport initialized.
- */
- public Builder(int id, String timestamp) {
- mId = id;
- mTimestamp = timestamp;
- }
+ /** Sets timestamp. */
+ public abstract Builder setTimestamp(String timestamp);
/** Sets title. */
- public Builder setTitle(String title) {
- mTitle = title;
- return this;
- }
+ public abstract Builder setTitle(String title);
/** Sets username. */
- public Builder setUserName(String username) {
- mUsername = username;
- return this;
- }
+ public abstract Builder setUserName(String username);
/** Sets filepath. */
- public Builder setFilepath(String filePath) {
- mFilePath = filePath;
- return this;
- }
+ public abstract Builder setFilePath(String filePath);
- /** Sets status. */
- public Builder setStatus(int status) {
- mStatus = status;
- return this;
- }
+ /** Sets bugReportFileName. */
+ public abstract Builder setBugReportFileName(String bugReportFileName);
+
+ /** Sets audioFileName. */
+ public abstract Builder setAudioFileName(String audioFileName);
+
+ /** Sets {@link Status}. */
+ public abstract Builder setStatus(int status);
/** Sets statusmessage. */
- public Builder setStatusMessage(String statusMessage) {
- mStatusMessage = statusMessage;
- return this;
- }
+ public abstract Builder setStatusMessage(String statusMessage);
- /** Returns a {@link MetaBugReport}. */
- public MetaBugReport build() {
- return new MetaBugReport(this);
- }
+ /** Sets the {@link BugReportType}. */
+ public abstract Builder setType(@BugReportType int type);
+
+ public abstract MetaBugReport build();
}
}
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/SimpleUploaderAsyncTask.java b/tests/BugReportApp/src/com/google/android/car/bugreport/SimpleUploaderAsyncTask.java
index 779750c..32dc804 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/SimpleUploaderAsyncTask.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/SimpleUploaderAsyncTask.java
@@ -29,18 +29,24 @@
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.storage.Storage;
import com.google.api.services.storage.model.StorageObject;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
+import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.zip.ZipOutputStream;
/**
- * Uploads a file to GCS using a simple (no-multipart / no-resume) upload policy.
+ * Uploads a bugreport files to GCS using a simple (no-multipart / no-resume) upload policy.
+ *
+ * <p>It merges bugreport zip file and audio file into one final zip file and uploads it.
*
* <p>Please see {@code res/values/configs.xml} and {@code res/raw/gcs_credentials.json} for the
* configuration.
@@ -113,14 +119,51 @@
Storage storage = new Storage.Builder(httpTransport, jsonFactory, credential)
.setApplicationName("Bugreportupload/1.0").build();
- File bugReportFile = new File(bugReport.getFilePath());
- String fileName = bugReportFile.getName();
- try (FileInputStream inputStream = new FileInputStream(bugReportFile)) {
- StorageObject object = uploadSimple(storage, bugReport, fileName, inputStream);
- Log.v(TAG, "finished uploading object " + object.getName() + " file " + fileName);
+ File tmpBugReportFile = zipBugReportFiles(bugReport);
+ Log.d(TAG, "Uploading file " + tmpBugReportFile);
+ try {
+ // Upload filename is bugreport filename, although, now it contains the audio message.
+ String fileName = bugReport.getBugReportFileName();
+ try (FileInputStream inputStream = new FileInputStream(tmpBugReportFile)) {
+ StorageObject object = uploadSimple(storage, bugReport, fileName, inputStream);
+ Log.v(TAG, "finished uploading object " + object.getName() + " file " + fileName);
+ }
+ File pendingDir = FileUtils.getPendingDir(mContext);
+ // Delete only after successful upload; the files are needed for retry.
+ if (!Strings.isNullOrEmpty(bugReport.getAudioFileName())) {
+ Log.v(TAG, "Deleting file " + bugReport.getAudioFileName());
+ new File(pendingDir, bugReport.getAudioFileName()).delete();
+ }
+ if (!Strings.isNullOrEmpty(bugReport.getBugReportFileName())) {
+ Log.v(TAG, "Deleting file " + bugReport.getBugReportFileName());
+ new File(pendingDir, bugReport.getBugReportFileName()).delete();
+ }
+ } finally {
+ // Delete the temp file if it's not a MetaBugReport#getFilePath, because it's needed
+ // for retry.
+ if (Strings.isNullOrEmpty(bugReport.getFilePath())) {
+ Log.v(TAG, "Deleting file " + tmpBugReportFile);
+ tmpBugReportFile.delete();
+ }
}
- Log.v(TAG, "Deleting file " + fileName);
- bugReportFile.delete();
+ }
+
+ private File zipBugReportFiles(MetaBugReport bugReport) throws IOException {
+ if (!Strings.isNullOrEmpty(bugReport.getFilePath())) {
+ // Old bugreports still have this field.
+ return new File(bugReport.getFilePath());
+ }
+ File finalZipFile =
+ File.createTempFile("bugreport", ".zip", mContext.getCacheDir());
+ File pendingDir = FileUtils.getPendingDir(mContext);
+ try (ZipOutputStream zipStream = new ZipOutputStream(
+ new BufferedOutputStream(new FileOutputStream(finalZipFile)))) {
+ ZipUtils.extractZippedFileToZipStream(
+ new File(pendingDir, bugReport.getBugReportFileName()), zipStream);
+ ZipUtils.addFileToZipStream(
+ new File(pendingDir, bugReport.getAudioFileName()), zipStream);
+ }
+ return finalZipFile;
}
@Override
@@ -131,7 +174,7 @@
/** Returns true is there are more files to upload. */
@Override
protected Boolean doInBackground(Void... voids) {
- List<MetaBugReport> bugReports = BugStorageUtils.getPendingBugReports(mContext);
+ List<MetaBugReport> bugReports = BugStorageUtils.getUploadPendingBugReports(mContext);
for (MetaBugReport bugReport : bugReports) {
try {
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/StartUpBootReceiver.java b/tests/BugReportApp/src/com/google/android/car/bugreport/StartUpBootReceiver.java
index abe729a..7e89d2d 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/StartUpBootReceiver.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/StartUpBootReceiver.java
@@ -33,6 +33,9 @@
@Override
public void onReceive(Context context, Intent intent) {
+ if (!Config.isBugReportEnabled()) {
+ return;
+ }
// Run it only once for the system user (u0) and ignore for other users.
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
if (!userManager.isSystemUser()) {
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java b/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java
index 9142b91..380944e 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/Status.java
@@ -42,7 +42,16 @@
STATUS_MOVE_SUCCESSFUL(7),
// Bugreport move has failed.
- STATUS_MOVE_FAILED(8);
+ STATUS_MOVE_FAILED(8),
+
+ // Bugreport is moving to USB drive.
+ STATUS_MOVE_IN_PROGRESS(9),
+
+ // Bugreport is expired. Associated file is deleted from the disk.
+ STATUS_EXPIRED(10),
+
+ // Bugreport needs audio message.
+ STATUS_AUDIO_PENDING(11);
private final int mValue;
@@ -76,6 +85,12 @@
return "Move successful";
case 8:
return "Move failed";
+ case 9:
+ return "Move in progress";
+ case 10:
+ return "Expired";
+ case 11:
+ return "Audio message pending";
}
return "unknown";
}
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/UploadJob.java b/tests/BugReportApp/src/com/google/android/car/bugreport/UploadJob.java
index 67cc560..b2c17e9 100644
--- a/tests/BugReportApp/src/com/google/android/car/bugreport/UploadJob.java
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/UploadJob.java
@@ -27,6 +27,9 @@
@Override
public boolean onStartJob(final JobParameters jobParameters) {
+ if (!Config.isBugReportEnabled()) {
+ return false;
+ }
Log.v(TAG, "Starting upload job");
mUploader = new SimpleUploaderAsyncTask(
this, reschedule -> jobFinished(jobParameters, reschedule));
diff --git a/tests/BugReportApp/src/com/google/android/car/bugreport/ZipUtils.java b/tests/BugReportApp/src/com/google/android/car/bugreport/ZipUtils.java
new file mode 100644
index 0000000..e33b706
--- /dev/null
+++ b/tests/BugReportApp/src/com/google/android/car/bugreport/ZipUtils.java
@@ -0,0 +1,87 @@
+/*
+ * 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.util.Log;
+
+import com.google.common.io.ByteStreams;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/** Zip utility functions. */
+final class ZipUtils {
+ private static final String TAG = ZipUtils.class.getSimpleName();
+
+ /** Extracts the contents of a zip file to the zip output stream. */
+ static void extractZippedFileToZipStream(File file, ZipOutputStream zipStream) {
+ if (!file.exists()) {
+ Log.w(TAG, "File " + file + " not found");
+ return;
+ }
+ if (file.length() == 0) {
+ // If there were issues with reading from dumpstate socket, the dumpstate zip
+ // file still might be available in
+ // /data/user_de/0/com.android.shell/files/bugreports/.
+ Log.w(TAG, "Zip file " + file.getName() + " is empty, skipping.");
+ return;
+ }
+ try (ZipFile zipFile = new ZipFile(file)) {
+ Enumeration<? extends ZipEntry> entries = zipFile.entries();
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = entries.nextElement();
+ try (InputStream stream = zipFile.getInputStream(entry)) {
+ writeInputStreamToZipStream(entry.getName(), stream, zipStream);
+ }
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to add " + file + " to zip", e);
+ }
+ }
+
+ /** Adds a file to the zip output stream. */
+ static void addFileToZipStream(File file, ZipOutputStream zipStream) {
+ if (!file.exists()) {
+ Log.w(TAG, "File " + file + " not found");
+ return;
+ }
+ if (file.length() == 0) {
+ Log.w(TAG, "File " + file.getName() + " is empty, skipping.");
+ return;
+ }
+ try (FileInputStream audioInput = new FileInputStream(file)) {
+ writeInputStreamToZipStream(file.getName(), audioInput, zipStream);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to add " + file + "to the final zip");
+ }
+ }
+
+ private static void writeInputStreamToZipStream(
+ String filename, InputStream input, ZipOutputStream zipStream) throws IOException {
+ ZipEntry entry = new ZipEntry(filename);
+ zipStream.putNextEntry(entry);
+ ByteStreams.copy(input, zipStream);
+ zipStream.closeEntry();
+ }
+
+ private ZipUtils() {}
+}
diff --git a/tests/BugReportApp/tests/Android.mk b/tests/BugReportApp/tests/Android.mk
new file mode 100644
index 0000000..2a6ab88
--- /dev/null
+++ b/tests/BugReportApp/tests/Android.mk
@@ -0,0 +1,40 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := BugReportAppTest
+LOCAL_INSTRUMENTATION_FOR := BugReportApp
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_CERTIFICATE := platform
+LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_JAVA_LIBRARIES := \
+ android.test.base \
+ android.test.mock \
+ android.test.runner
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-test \
+ truth-prebuilt
+
+include $(BUILD_PACKAGE)
+
+include $(CLEAR_VARS)
diff --git a/tests/BugReportApp/tests/AndroidManifest.xml b/tests/BugReportApp/tests/AndroidManifest.xml
new file mode 100644
index 0000000..e6a8537
--- /dev/null
+++ b/tests/BugReportApp/tests/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.car.bugreport.tests" >
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:label="BugReportAppTest"
+ android:targetPackage="com.google.android.car.bugreport" />
+</manifest>
diff --git a/tests/BugReportApp/tests/src/com/google/android/car/bugreport/BugStorageUtilsTest.java b/tests/BugReportApp/tests/src/com/google/android/car/bugreport/BugStorageUtilsTest.java
new file mode 100644
index 0000000..747cac4
--- /dev/null
+++ b/tests/BugReportApp/tests/src/com/google/android/car/bugreport/BugStorageUtilsTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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 static com.google.android.car.bugreport.MetaBugReport.TYPE_INTERACTIVE;
+import static com.google.android.car.bugreport.Status.STATUS_PENDING_USER_ACTION;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.time.Instant;
+import java.util.Date;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class BugStorageUtilsTest {
+ private static final String TIMESTAMP_TODAY = MetaBugReport.toBugReportTimestamp(new Date());
+ private static final String BUGREPORT_ZIP_FILE_NAME = "bugreport@ASD.zip";
+ private static final int BUGREPORT_ZIP_FILE_CONTENT = 1;
+
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getContext();
+ }
+
+ @Test
+ public void test_createBugReport_createsAndReturnsMetaBugReport() throws Exception {
+ MetaBugReport bug = createBugReportWithStatus(TIMESTAMP_TODAY,
+ STATUS_PENDING_USER_ACTION, TYPE_INTERACTIVE, /* createBugReportFile= */ true);
+
+ assertThat(BugStorageUtils.findBugReport(mContext, bug.getId()).get()).isEqualTo(bug);
+ }
+
+ @Test
+ public void test_expireBugReport_marksBugReportDeletedAndDeletesZip() throws Exception {
+ MetaBugReport bug = createBugReportWithStatus(TIMESTAMP_TODAY,
+ STATUS_PENDING_USER_ACTION, TYPE_INTERACTIVE, /* createBugReportFile= */ true);
+ try (InputStream in = BugStorageUtils.openBugReportFileToRead(mContext, bug)) {
+ assertThat(in).isNotNull();
+ }
+ Instant now = Instant.now();
+
+ boolean deleteResult = BugStorageUtils.expireBugReport(mContext, bug, now);
+
+ assertThat(deleteResult).isTrue();
+ assertThat(BugStorageUtils.findBugReport(mContext, bug.getId()).get())
+ .isEqualTo(bug.toBuilder()
+ .setStatus(Status.STATUS_EXPIRED.getValue())
+ .setStatusMessage("Expired on " + now).build());
+ assertThrows(FileNotFoundException.class, () ->
+ BugStorageUtils.openBugReportFileToRead(mContext, bug));
+ }
+
+ @Test
+ public void test_completeDeleteBugReport_removesBugReportRecordFromDb() throws Exception {
+ MetaBugReport bug = createBugReportWithStatus(TIMESTAMP_TODAY,
+ STATUS_PENDING_USER_ACTION, TYPE_INTERACTIVE, /* createBugReportFile= */ true);
+ try (InputStream in = BugStorageUtils.openBugReportFileToRead(mContext, bug)) {
+ assertThat(in).isNotNull();
+ }
+
+ boolean deleteResult = BugStorageUtils.completeDeleteBugReport(mContext, bug.getId());
+
+ assertThat(deleteResult).isTrue();
+ assertThat(BugStorageUtils.findBugReport(mContext, bug.getId()).isPresent()).isFalse();
+ assertThrows(IllegalArgumentException.class, () ->
+ BugStorageUtils.openBugReportFileToRead(mContext, bug));
+ }
+
+ private MetaBugReport createBugReportWithStatus(
+ String timestamp, Status status, int type, boolean createBugReportFile)
+ throws IOException {
+ MetaBugReport bugReport = BugStorageUtils.createBugReport(
+ mContext, "sample title", timestamp, "driver", type);
+ if (createBugReportFile) {
+ bugReport = BugStorageUtils.update(mContext,
+ bugReport.toBuilder().setBugReportFileName(BUGREPORT_ZIP_FILE_NAME).build());
+ try (OutputStream out = BugStorageUtils.openBugReportFileToWrite(mContext, bugReport)) {
+ out.write(BUGREPORT_ZIP_FILE_CONTENT);
+ }
+ }
+ return BugStorageUtils.setBugReportStatus(mContext, bugReport, status, "");
+ }
+
+ private static void assertThrows(Class<? extends Throwable> exceptionClass,
+ ExceptionRunnable r) {
+ try {
+ r.run();
+ } catch (Throwable e) {
+ assertTrue("Expected exception type " + exceptionClass.getName() + " but got "
+ + e.getClass().getName(), exceptionClass.isAssignableFrom(e.getClass()));
+ return;
+ }
+ fail("Expected exception type " + exceptionClass.getName()
+ + ", but no exception was thrown");
+ }
+
+ private interface ExceptionRunnable {
+ void run() throws Exception;
+ }
+}
diff --git a/tests/BugReportApp/utils/bugreport_app_tester.py b/tests/BugReportApp/utils/bugreport_app_tester.py
index 656f2d7..baf8ada 100755
--- a/tests/BugReportApp/utils/bugreport_app_tester.py
+++ b/tests/BugReportApp/utils/bugreport_app_tester.py
@@ -63,6 +63,10 @@
STATUS_UPLOAD_SUCCESS = 3
STATUS_UPLOAD_FAILED = 4
STATUS_USER_CANCELLED = 5
+STATUS_PENDING_USER_ACTION = 6
+STATUS_MOVE_SUCCESSFUL = 7
+STATUS_MOVE_FAILED = 8
+STATUS_MOVE_IN_PROGRESS = 9
DUMPSTATE_DEADLINE_SEC = 300 # 10 minutes.
UPLOAD_DEADLINE_SEC = 180 # 3 minutes.
@@ -120,6 +124,14 @@
return 'UPLOAD_FAILED'
elif status == STATUS_USER_CANCELLED:
return 'USER_CANCELLED'
+ elif status == STATUS_PENDING_USER_ACTION:
+ return 'PENDING_USER_ACTION'
+ elif status == STATUS_MOVE_SUCCESSFUL:
+ return 'MOVE_SUCCESSFUL'
+ elif status == STATUS_MOVE_FAILED:
+ return 'MOVE_FAILED'
+ elif status == STATUS_MOVE_IN_PROGRESS:
+ return 'MOVE_IN_PROGRESS'
return 'UNKNOWN_STATUS'
@@ -337,7 +349,7 @@
_bugreport_status_to_str(meta_bugreport.status))
def _wait_for_bugreport_to_complete(self, bugreport_id):
- """Waits until status changes to UPLOAD_PENDING.
+ """Waits until status changes to WRITE_PENDING.
It means dumpstate (bugreport) is completed (or failed).
@@ -356,13 +368,17 @@
print('\nDumpstate (bugreport) completed (or failed).')
def _wait_for_bugreport_to_upload(self, bugreport_id):
- """Waits bugreport to be uploaded and returns None if succeeds."""
+ """Waits bugreport to be uploaded and returns None if succeeds.
+
+ NOTE: If "android.car.bugreport.disableautoupload" system property is set,
+ the App will not upload.
+ """
print('\nWaiting for the bug report to be uploaded.')
err_msg = self._wait_for_bugreport_status_to_change_to(
STATUS_UPLOAD_SUCCESS,
UPLOAD_DEADLINE_SEC,
bugreport_id,
- allowed_statuses=[STATUS_UPLOAD_PENDING])
+ allowed_statuses=[STATUS_UPLOAD_PENDING, STATUS_PENDING_USER_ACTION])
if err_msg:
print('Failed to upload: %s' % err_msg)
return err_msg
diff --git a/tests/CarCtsDummyLauncher/src/com/android/car/dummylauncher/LauncherActivity.java b/tests/CarCtsDummyLauncher/src/com/android/car/dummylauncher/LauncherActivity.java
index 20fcfc0..71b3c2b 100644
--- a/tests/CarCtsDummyLauncher/src/com/android/car/dummylauncher/LauncherActivity.java
+++ b/tests/CarCtsDummyLauncher/src/com/android/car/dummylauncher/LauncherActivity.java
@@ -33,6 +33,7 @@
View view = getLayoutInflater().inflate(R.layout.launcher_activity, null);
setContentView(view);
+ reportFullyDrawn();
}
}
diff --git a/tests/CarDeveloperOptions/AndroidManifest.xml b/tests/CarDeveloperOptions/AndroidManifest.xml
index 5975572..046b386 100644
--- a/tests/CarDeveloperOptions/AndroidManifest.xml
+++ b/tests/CarDeveloperOptions/AndroidManifest.xml
@@ -21,7 +21,6 @@
<original-package android:name="com.android.car.developeroptions" />
<uses-permission android:name="android.permission.REQUEST_NETWORK_SCORES" />
- <uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
diff --git a/tests/CarDeveloperOptions/res/xml/development_settings.xml b/tests/CarDeveloperOptions/res/xml/development_settings.xml
index 9ed1285..a946526 100644
--- a/tests/CarDeveloperOptions/res/xml/development_settings.xml
+++ b/tests/CarDeveloperOptions/res/xml/development_settings.xml
@@ -30,11 +30,6 @@
android:summary="@string/summary_placeholder"
android:fragment="com.android.car.developeroptions.applications.ProcessStatsSummary" />
- <com.android.car.developeroptions.BugreportPreference
- android:key="bugreport"
- android:title="@*android:string/bugreport_title"
- android:dialogTitle="@*android:string/bugreport_title" />
-
<Preference
android:key="system_server_heap_dump"
android:title="@string/capture_system_heap_dump_title" />
@@ -151,11 +146,6 @@
android:summary="@string/enable_terminal_summary" />
<SwitchPreference
- android:key="bugreport_in_power"
- android:title="@string/bugreport_in_power"
- android:summary="@string/bugreport_in_power_summary" />
-
- <SwitchPreference
android:key="automatic_system_server_heap_dumps"
android:title="@string/automatic_system_heap_dump_title"
android:summary="@string/automatic_system_heap_dump_summary" />
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/BugreportPreference.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/BugreportPreference.java
deleted file mode 100644
index 6acef70..0000000
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/BugreportPreference.java
+++ /dev/null
@@ -1,106 +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.android.car.developeroptions;
-
-import android.app.ActivityManager;
-import android.app.settings.SettingsEnums;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.os.RemoteException;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.View;
-import android.widget.CheckedTextView;
-import android.widget.TextView;
-
-import androidx.appcompat.app.AlertDialog.Builder;
-
-import com.android.car.developeroptions.overlay.FeatureFactory;
-import com.android.settingslib.CustomDialogPreferenceCompat;
-
-public class BugreportPreference extends CustomDialogPreferenceCompat {
-
- private static final String TAG = "BugreportPreference";
-
- private CheckedTextView mInteractiveTitle;
- private TextView mInteractiveSummary;
- private CheckedTextView mFullTitle;
- private TextView mFullSummary;
-
- public BugreportPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onPrepareDialogBuilder(Builder builder, DialogInterface.OnClickListener listener) {
- super.onPrepareDialogBuilder(builder, listener);
-
- final View dialogView = View.inflate(getContext(), R.layout.bugreport_options_dialog, null);
- mInteractiveTitle = (CheckedTextView) dialogView.findViewById(R.id.bugreport_option_interactive_title);
- mInteractiveSummary = (TextView) dialogView.findViewById(R.id.bugreport_option_interactive_summary);
- mFullTitle = (CheckedTextView) dialogView.findViewById(R.id.bugreport_option_full_title);
- mFullSummary = (TextView) dialogView.findViewById(R.id.bugreport_option_full_summary);
- final View.OnClickListener l = new View.OnClickListener() {
-
- @Override
- public void onClick(View v) {
- if (v == mFullTitle || v == mFullSummary) {
- mInteractiveTitle.setChecked(false);
- mFullTitle.setChecked(true);
- }
- if (v == mInteractiveTitle || v == mInteractiveSummary) {
- mInteractiveTitle.setChecked(true);
- mFullTitle.setChecked(false);
- }
- }
- };
- mInteractiveTitle.setOnClickListener(l);
- mFullTitle.setOnClickListener(l);
- mInteractiveSummary.setOnClickListener(l);
- mFullSummary.setOnClickListener(l);
-
- builder.setPositiveButton(com.android.internal.R.string.report, listener);
- builder.setView(dialogView);
- }
-
- @Override
- protected void onClick(DialogInterface dialog, int which) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
-
- final Context context = getContext();
- if (mFullTitle.isChecked()) {
- Log.v(TAG, "Taking full bugreport right away");
- FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context,
- SettingsEnums.ACTION_BUGREPORT_FROM_SETTINGS_FULL);
- takeBugreport(ActivityManager.BUGREPORT_OPTION_FULL);
- } else {
- Log.v(TAG, "Taking interactive bugreport right away");
- FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context,
- SettingsEnums.ACTION_BUGREPORT_FROM_SETTINGS_INTERACTIVE);
- takeBugreport(ActivityManager.BUGREPORT_OPTION_INTERACTIVE);
- }
- }
- }
-
- private void takeBugreport(int bugreportType) {
- try {
- ActivityManager.getService().requestBugReport(bugreportType);
- } catch (RemoteException e) {
- Log.e(TAG, "error taking bugreport (bugreportType=" + bugreportType + ")", e);
- }
- }
-}
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/BugReportInPowerPreferenceController.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/BugReportInPowerPreferenceController.java
deleted file mode 100644
index 1f22eb2..0000000
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/BugReportInPowerPreferenceController.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.android.car.developeroptions.development;
-
-import android.content.Context;
-import android.os.UserManager;
-import android.provider.Settings;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.preference.Preference;
-import androidx.preference.SwitchPreference;
-
-import com.android.car.developeroptions.core.PreferenceControllerMixin;
-import com.android.settingslib.development.DeveloperOptionsPreferenceController;
-
-public class BugReportInPowerPreferenceController extends
- DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener,
- PreferenceControllerMixin {
-
- private static final String KEY_BUGREPORT_IN_POWER = "bugreport_in_power";
-
- @VisibleForTesting
- static int SETTING_VALUE_ON = 1;
- @VisibleForTesting
- static int SETTING_VALUE_OFF = 0;
-
- private final UserManager mUserManager;
-
- public BugReportInPowerPreferenceController(Context context) {
- super(context);
- mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
- }
-
- @Override
- public boolean isAvailable() {
- return !mUserManager.hasUserRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES);
- }
-
- @Override
- public String getPreferenceKey() {
- return KEY_BUGREPORT_IN_POWER;
- }
-
- @Override
- public boolean onPreferenceChange(Preference preference, Object newValue) {
- final boolean isEnabled = (Boolean) newValue;
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Global.BUGREPORT_IN_POWER_MENU,
- isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);
- return true;
- }
-
- @Override
- public void updateState(Preference preference) {
- final int mode = Settings.Secure.getInt(mContext.getContentResolver(),
- Settings.Global.BUGREPORT_IN_POWER_MENU, SETTING_VALUE_OFF);
- ((SwitchPreference) mPreference).setChecked(mode != SETTING_VALUE_OFF);
- }
-
- @Override
- protected void onDeveloperOptionsSwitchDisabled() {
- super.onDeveloperOptionsSwitchDisabled();
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Global.BUGREPORT_IN_POWER_MENU, SETTING_VALUE_OFF);
- ((SwitchPreference) mPreference).setChecked(false);
- }
-}
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/BugReportPreferenceController.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/BugReportPreferenceController.java
deleted file mode 100644
index 28fb9b5..0000000
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/BugReportPreferenceController.java
+++ /dev/null
@@ -1,47 +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.android.car.developeroptions.development;
-
-import android.content.Context;
-import android.os.UserManager;
-
-import com.android.car.developeroptions.core.PreferenceControllerMixin;
-import com.android.settingslib.development.DeveloperOptionsPreferenceController;
-
-public class BugReportPreferenceController extends DeveloperOptionsPreferenceController implements
- PreferenceControllerMixin {
-
- private static final String KEY_BUGREPORT = "bugreport";
-
- private final UserManager mUserManager;
-
- public BugReportPreferenceController(Context context) {
- super(context);
-
- mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
- }
-
- @Override
- public boolean isAvailable() {
- return !mUserManager.hasUserRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES);
- }
-
- @Override
- public String getPreferenceKey() {
- return KEY_BUGREPORT;
- }
-}
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/DevelopmentSettingsDashboardFragment.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/DevelopmentSettingsDashboardFragment.java
index 95c8b6a..3253d21 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/DevelopmentSettingsDashboardFragment.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/development/DevelopmentSettingsDashboardFragment.java
@@ -403,7 +403,6 @@
BluetoothA2dpConfigStore bluetoothA2dpConfigStore) {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
controllers.add(new MemoryUsagePreferenceController(context));
- controllers.add(new BugReportPreferenceController(context));
controllers.add(new SystemServerHeapDumpPreferenceController(context));
controllers.add(new LocalBackupPasswordPreferenceController(context));
controllers.add(new StayAwakePreferenceController(context, lifecycle));
@@ -418,7 +417,6 @@
controllers.add(new AdbPreferenceController(context, fragment));
controllers.add(new ClearAdbKeysPreferenceController(context, fragment));
controllers.add(new LocalTerminalPreferenceController(context));
- controllers.add(new BugReportInPowerPreferenceController(context));
controllers.add(new AutomaticSystemServerHeapDumpPreferenceController(context));
controllers.add(new MockLocationAppPreferenceController(context, fragment));
controllers.add(new DebugViewAttributesPreferenceController(context));
diff --git a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/wifi/NetworkRequestDialogFragment.java b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/wifi/NetworkRequestDialogFragment.java
index 6309b42..6c32f5c 100644
--- a/tests/CarDeveloperOptions/src/com/android/car/developeroptions/wifi/NetworkRequestDialogFragment.java
+++ b/tests/CarDeveloperOptions/src/com/android/car/developeroptions/wifi/NetworkRequestDialogFragment.java
@@ -310,7 +310,7 @@
mHandler.sendEmptyMessageDelayed(MESSAGE_STOP_SCAN_WIFI_LIST, DELAY_TIME_STOP_SCAN_MS);
if (mFilterWifiTracker == null) {
- mFilterWifiTracker = new FilterWifiTracker(getActivity(), getSettingsLifecycle());
+ mFilterWifiTracker = new FilterWifiTracker(getContext(), getSettingsLifecycle());
}
mFilterWifiTracker.onResume();
}
@@ -473,11 +473,13 @@
private final class FilterWifiTracker {
private final List<String> mAccessPointKeys;
private final WifiTracker mWifiTracker;
+ private final Context mContext;
public FilterWifiTracker(Context context, Lifecycle lifecycle) {
mWifiTracker = WifiTrackerFactory.create(context, mWifiListener,
lifecycle, /* includeSaved */ true, /* includeScans */ true);
mAccessPointKeys = new ArrayList<>();
+ mContext = context;
}
/**
@@ -486,7 +488,7 @@
*/
public void updateKeys(List<ScanResult> scanResults) {
for (ScanResult scanResult : scanResults) {
- final String key = AccessPoint.getKey(scanResult);
+ final String key = AccessPoint.getKey(mContext, scanResult);
if (!mAccessPointKeys.contains(key)) {
mAccessPointKeys.add(key);
}
diff --git a/tests/CarTrustAgentClientApp/Android.mk b/tests/CarTrustAgentClientApp/Android.mk
deleted file mode 100644
index 3504ff7..0000000
--- a/tests/CarTrustAgentClientApp/Android.mk
+++ /dev/null
@@ -1,25 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_PACKAGE_NAME := CarTrustAgentClient
-
-LOCAL_USE_AAPT2 := true
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_STATIC_ANDROID_LIBRARIES := \
- androidx.appcompat_appcompat \
- androidx-constraintlayout_constraintlayout \
- androidx.legacy_legacy-support-v4
-
-LOCAL_CERTIFICATE := platform
-LOCAL_MODULE_TAGS := optional
-LOCAL_MIN_SDK_VERSION := 23
-LOCAL_SDK_VERSION := current
-
-LOCAL_PROGUARD_ENABLED := disabled
-
-LOCAL_DEX_PREOPT := false
-
-include $(BUILD_PACKAGE)
diff --git a/tests/CarTrustAgentClientApp/AndroidManifest.xml b/tests/CarTrustAgentClientApp/AndroidManifest.xml
deleted file mode 100644
index e76485f..0000000
--- a/tests/CarTrustAgentClientApp/AndroidManifest.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2018 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.
- -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.car.trust.client">
-
- <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23"/>
-
- <!-- Need Bluetooth LE -->
- <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
-
- <uses-permission android:name="android.permission.BLUETOOTH" />
- <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
-
- <!-- Needed to unlock user -->
- <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
- <uses-permission android:name="android.permission.MANAGE_USERS" />
- <uses-permission android:name="android.permission.CONTROL_KEYGUARD" />
- <uses-permission android:name="android.permission.PROVIDE_TRUST_AGENT" />
- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
-
- <application
- android:label="@string/app_name"
- android:theme="@style/Theme.AppCompat">
-
- <activity
- android:name=".PhoneEnrolmentActivity"
- android:label="@string/app_name"
- android:exported="true"
- android:screenOrientation="portrait"
- android:launchMode="singleInstance">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
- </application>
-</manifest>
diff --git a/tests/CarTrustAgentClientApp/README.txt b/tests/CarTrustAgentClientApp/README.txt
deleted file mode 100644
index bf6c444..0000000
--- a/tests/CarTrustAgentClientApp/README.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-IMPORTANT NOTE: This is a reference app to smart unlock paired HU during development.
-Consider moving the functionality to a more proper place.
diff --git a/tests/CarTrustAgentClientApp/res/layout/phone_enrolment_activity.xml b/tests/CarTrustAgentClientApp/res/layout/phone_enrolment_activity.xml
deleted file mode 100644
index 7237dfa..0000000
--- a/tests/CarTrustAgentClientApp/res/layout/phone_enrolment_activity.xml
+++ /dev/null
@@ -1,70 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2018 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.
- -->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:weightSum="1">
- <ScrollView
- android:id="@+id/scroll"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:scrollbars="vertical"
- android:layout_weight="0.80">
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:id="@+id/output"/>
- </ScrollView>
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="0.10"
- android:orientation="horizontal">
- <Button
- android:id="@+id/enroll_scan"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="2"
- android:text="@string/enroll_scan"/>
- <Button
- android:id="@+id/enroll_button"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="3"
- android:text="@string/enroll_button"/>
- </LinearLayout>
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="0.10"
- android:orientation="horizontal">
- <Button
- android:id="@+id/unlock_scan"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="2"
- android:text="@string/unlock_scan"/>
- <Button
- android:id="@+id/unlock_button"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="3"
- android:text="@string/unlock_button"/>
- </LinearLayout>
-</LinearLayout>
diff --git a/tests/CarTrustAgentClientApp/res/values/strings.xml b/tests/CarTrustAgentClientApp/res/values/strings.xml
deleted file mode 100644
index 6e33a81..0000000
--- a/tests/CarTrustAgentClientApp/res/values/strings.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2018 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.
- -->
-<resources>
- <string name="app_name" translatable="false">CarTrustAgentClient</string>
-
- <!-- service/characteristics uuid for unlocking a device -->
- <string name="unlock_service_uuid" translatable="false">5e2a68a1-27be-43f9-8d1e-4546976fabd7</string>
- <string name="unlock_escrow_token_uiid" translatable="false">5e2a68a2-27be-43f9-8d1e-4546976fabd7</string>
- <string name="unlock_handle_uiid" translatable="false">5e2a68a3-27be-43f9-8d1e-4546976fabd7</string>
-
- <!-- service/characteristics uuid for adding new escrow token -->
- <string name="enrollment_service_uuid" translatable="false">5e2a68a4-27be-43f9-8d1e-4546976fabd7</string>
- <string name="enrollment_handle_uuid" translatable="false">5e2a68a5-27be-43f9-8d1e-4546976fabd7</string>
- <string name="enrollment_token_uuid" translatable="false">5e2a68a6-27be-43f9-8d1e-4546976fabd7</string>
-
- <string name="pref_key_token_handle" translatable="false">token-handle-key</string>
- <string name="pref_key_escrow_token" translatable="false">escrow-token-key</string>
-
- <string name="enroll_button" translatable="false">Enroll new token</string>
- <string name="enroll_scan" translatable="false">Scan to enroll</string>
- <string name="unlock_button" translatable="false">Unlock</string>
- <string name="unlock_scan" translatable="false">Scan to unlock</string>
-</resources>
diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/BluetoothUtils.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/BluetoothUtils.java
deleted file mode 100644
index 77ed7bb..0000000
--- a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/BluetoothUtils.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2018 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.android.car.trust.client;
-
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattService;
-import android.content.Context;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-
-import java.util.UUID;
-
-/**
- * A utility class holding methods related to Bluetooth.
- */
-public class BluetoothUtils {
- private BluetoothUtils() {}
-
- /**
- * Returns a characteristic off the given {@link BluetoothGattService} mapped to the jUUID
- * specified. If the given service has multiple characteristics of the same UUID, then the
- * first instance is returned.
- *
- * @param uuidRes The unique identifier for the characteristic.
- * @param service The {@link BluetoothGattService} that contains the characteristic.
- * @param context The current {@link Context}.
- * @return A {@link BluetoothGattCharacteristic} with a UUID matching {@code uuidRes} or
- * {@code null} if none exists.
- *
- * @see BluetoothGattService#getCharacteristic(UUID)
- */
- @Nullable
- public static BluetoothGattCharacteristic getCharacteristic(@StringRes int uuidRes,
- BluetoothGattService service, Context context) {
- return service.getCharacteristic(UUID.fromString(context.getString(uuidRes)));
- }
-}
diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentActivity.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentActivity.java
deleted file mode 100644
index fd29624..0000000
--- a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentActivity.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2018 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.android.car.trust.client;
-
-import android.Manifest;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-
-import androidx.fragment.app.FragmentActivity;
-
-/**
- * Activity to allow the user to add an escrow token to a remote device. <p/>
- *
- * For this to work properly, the correct permissions must be set in the system config. In AOSP,
- * this config is in frameworks/base/core/res/res/values/config.xml <p/>
- *
- * The config must set config_allowEscrowTokenForTrustAgent to true. For the desired car
- * experience, the config should also set config_strongAuthRequiredOnBoot to false.
- */
-public class PhoneEnrolmentActivity extends FragmentActivity {
-
- private static final int FINE_LOCATION_REQUEST_CODE = 42;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.phone_enrolment_activity);
-
- PhoneEnrolmentController enrolmentController = new PhoneEnrolmentController(this);
- enrolmentController.bind(findViewById(R.id.output), findViewById(R.id.enroll_scan),
- findViewById(R.id.enroll_button));
-
- PhoneUnlockController unlockController = new PhoneUnlockController(this);
- unlockController.bind(findViewById(R.id.output), findViewById(R.id.unlock_scan),
- findViewById(R.id.unlock_button));
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
- != PackageManager.PERMISSION_GRANTED) {
- requestPermissions(
- new String[] { android.Manifest.permission.ACCESS_FINE_LOCATION },
- FINE_LOCATION_REQUEST_CODE);
- }
- }
-}
diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentController.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentController.java
deleted file mode 100644
index 1d3f672..0000000
--- a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneEnrolmentController.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright (C) 2018 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.android.car.trust.client;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattService;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Handler;
-import android.os.ParcelUuid;
-import android.preference.PreferenceManager;
-import android.util.Base64;
-import android.util.Log;
-import android.widget.Button;
-import android.widget.TextView;
-
-import java.nio.ByteBuffer;
-import java.util.Random;
-import java.util.UUID;
-
-/**
- * A controller that sets up a {@link SimpleBleClient} to connect to the BLE enrollment service.
- * It also binds the UI components to control the enrollment process.
- */
-public class PhoneEnrolmentController {
- private static final String TAG = "PhoneEnrollmentCltr";
-
- private final String mTokenHandleKey;
- private final String mEscrowTokenKey;
-
- private final ParcelUuid mEnrolmentServiceUuid;
-
- private final SimpleBleClient mClient;
- private final Context mContext;
- private final Handler mHandler;
-
- // BLE characteristics associated with the enrollment/add escrow token service.
- private BluetoothGattCharacteristic mEnrolmentTokenHandle;
- private BluetoothGattCharacteristic mEnrolmentEscrowToken;
-
- private TextView mTextView;
- private Button mEnrolButton;
-
- public PhoneEnrolmentController(Context context) {
- mContext = context;
-
- mTokenHandleKey = context.getString(R.string.pref_key_token_handle);
- mEscrowTokenKey = context.getString(R.string.pref_key_escrow_token);
-
- mClient = new SimpleBleClient(context);
- mEnrolmentServiceUuid = new ParcelUuid(
- UUID.fromString(mContext.getString(R.string.enrollment_service_uuid)));
- mClient.addCallback(mCallback /* callback */);
-
- mHandler = new Handler(mContext.getMainLooper());
- }
-
- /**
- * Binds the views to the actions that can be performed by this controller.
- *
- * @param textView A text view used to display results from various BLE actions
- * @param scanButton Button used to start scanning for available BLE devices.
- * @param enrolButton Button used to send new escrow token to remote device.
- */
- public void bind(TextView textView, Button scanButton, Button enrolButton) {
- mTextView = textView;
- mEnrolButton = enrolButton;
-
- scanButton.setOnClickListener(v -> mClient.start(mEnrolmentServiceUuid));
-
- mEnrolButton.setEnabled(false);
- mEnrolButton.setAlpha(0.3f);
- mEnrolButton.setOnClickListener(v -> {
- appendOutputText("Sending new escrow token to remote device");
-
- byte[] token = generateEscrowToken();
- sendEnrolmentRequest(token);
-
- // WARNING: Store the token so it can be used later for unlocking. This token
- // should NEVER be stored on the device that is being unlocked. It should
- // always be securely stored on a remote device that will trigger the unlock.
- storeToken(token);
- });
- }
-
- /**
- * @return A random byte array that is used as the escrow token for remote device unlock.
- */
- private byte[] generateEscrowToken() {
- Random random = new Random();
- ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
- buffer.putLong(0, random.nextLong());
- return buffer.array();
- }
-
- private void sendEnrolmentRequest(byte[] token) {
- mEnrolmentEscrowToken.setValue(token);
- mClient.writeCharacteristic(mEnrolmentEscrowToken);
- storeToken(token);
- }
-
- private void storeHandle(long handle) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
- prefs.edit().putLong(mTokenHandleKey, handle).apply();
- }
-
- private void storeToken(byte[] token) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
- String byteArray = Base64.encodeToString(token, Base64.DEFAULT);
- prefs.edit().putString(mEscrowTokenKey, byteArray).apply();
- }
-
- private void appendOutputText(final String text) {
- mHandler.post(() -> mTextView.append("\n" + text));
- }
-
- private final SimpleBleClient.ClientCallback mCallback = new SimpleBleClient.ClientCallback() {
- @Override
- public void onDeviceConnected(BluetoothDevice device) {
- appendOutputText("Device connected: " + device.getName()
- + " addr: " + device.getAddress());
- }
-
- @Override
- public void onDeviceDisconnected() {
- appendOutputText("Device disconnected");
- }
-
- @Override
- public void onCharacteristicChanged(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCharacteristicChanged: "
- + convertToLong(characteristic.getValue()));
- }
-
- if (characteristic.getUuid().equals(mEnrolmentTokenHandle.getUuid())) {
- // Store the new token handle that the BLE server is sending us. This required
- // to unlock the device.
- long handle = convertToLong(characteristic.getValue());
- storeHandle(handle);
- appendOutputText("Token handle received: " + handle);
- }
- }
-
- @Override
- public void onServiceDiscovered(BluetoothGattService service) {
- if (!service.getUuid().equals(mEnrolmentServiceUuid.getUuid())) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Service UUID: " + service.getUuid()
- + " does not match Enrolment UUID " + mEnrolmentServiceUuid.getUuid());
- }
- return;
- }
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Enrolment Service # characteristics: "
- + service.getCharacteristics().size());
- }
-
- mEnrolmentEscrowToken = BluetoothUtils.getCharacteristic(
- R.string.enrollment_token_uuid, service, mContext);
- mEnrolmentTokenHandle = BluetoothUtils.getCharacteristic(
- R.string.enrollment_handle_uuid, service, mContext);
- mClient.setCharacteristicNotification(mEnrolmentTokenHandle, true /* enable */);
- appendOutputText("Enrolment BLE client successfully connected");
-
- mHandler.post(() -> {
- // Services are now set up, allow users to enrol new escrow tokens.
- mEnrolButton.setEnabled(true);
- mEnrolButton.setAlpha(1.0f);
- });
- }
-
- private long convertToLong(byte[] bytes) {
- ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
- buffer.put(bytes);
- buffer.flip();
- return buffer.getLong();
- }
- };
-}
diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneUnlockController.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneUnlockController.java
deleted file mode 100644
index 1296529..0000000
--- a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/PhoneUnlockController.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2018 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.android.car.trust.client;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattService;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Handler;
-import android.os.ParcelUuid;
-import android.preference.PreferenceManager;
-import android.util.Base64;
-import android.util.Log;
-import android.widget.Button;
-import android.widget.TextView;
-
-import java.nio.ByteBuffer;
-import java.util.UUID;
-
-/**
- * A controller that sets up a {@link SimpleBleClient} to connect to the BLE unlock service.
- */
-public class PhoneUnlockController {
- private static final String TAG = "PhoneUnlockController";
-
- private final String mTokenHandleKey;
- private final String mEscrowTokenKey;
-
- // BLE characteristics associated with the enrolment/add escrow token service.
- private BluetoothGattCharacteristic mUnlockTokenHandle;
- private BluetoothGattCharacteristic mUnlockEscrowToken;
-
- private final ParcelUuid mUnlockServiceUuid;
-
- private final SimpleBleClient mClient;
- private final Context mContext;
- private final Handler mHandler;
-
- private TextView mTextView;
- private Button mUnlockButton;
-
- public PhoneUnlockController(Context context) {
- mContext = context;
-
- mTokenHandleKey = context.getString(R.string.pref_key_token_handle);
- mEscrowTokenKey = context.getString(R.string.pref_key_escrow_token);
-
- mClient = new SimpleBleClient(context);
- mUnlockServiceUuid = new ParcelUuid(
- UUID.fromString(mContext.getString(R.string.unlock_service_uuid)));
- mClient.addCallback(mCallback /* callback */);
-
- mHandler = new Handler(mContext.getMainLooper());
- }
-
- /**
- * Binds the views to the actions that can be performed by this controller.
- *
- * @param textView A text view used to display results from various BLE actions
- * @param scanButton Button used to start scanning for available BLE devices.
- * @param enrolButton Button used to send new escrow token to remote device.
- */
- public void bind(TextView textView, Button scanButton, Button enrolButton) {
- mTextView = textView;
- mUnlockButton = enrolButton;
-
- scanButton.setOnClickListener(v -> mClient.start(mUnlockServiceUuid));
-
- mUnlockButton.setEnabled(false);
- mUnlockButton.setAlpha(0.3f);
- mUnlockButton.setOnClickListener(v -> {
- appendOutputText("Sending unlock token and handle to remote device");
- sendUnlockRequest();
- });
- }
-
- private void sendUnlockRequest() {
- // Retrieve stored token and handle and write to remote device.
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
- long handle = prefs.getLong(mTokenHandleKey, -1);
- byte[] token = Base64.decode(prefs.getString(mEscrowTokenKey, null), Base64.DEFAULT);
-
- mUnlockEscrowToken.setValue(token);
- mUnlockTokenHandle.setValue(convertToBytes(handle));
-
- mClient.writeCharacteristic(mUnlockEscrowToken);
- mClient.writeCharacteristic(mUnlockTokenHandle);
- }
-
- private void appendOutputText(String text) {
- mHandler.post(() -> mTextView.append("\n" + text));
- }
-
- private static byte[] convertToBytes(long l) {
- ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE);
- buffer.putLong(0, l);
- return buffer.array();
- }
-
- private final SimpleBleClient.ClientCallback mCallback = new SimpleBleClient.ClientCallback() {
- @Override
- public void onDeviceConnected(BluetoothDevice device) {
- appendOutputText("Device connected: " + device.getName()
- + " addr: " + device.getAddress());
- }
-
- @Override
- public void onDeviceDisconnected() {
- appendOutputText("Device disconnected");
- }
-
- @Override
- public void onCharacteristicChanged(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic) {
- // Not expecting any characteristics changes for the unlocking client.
- }
-
- @Override
- public void onServiceDiscovered(BluetoothGattService service) {
- if (!service.getUuid().equals(mUnlockServiceUuid.getUuid())) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Service UUID: " + service.getUuid()
- + " does not match Enrolment UUID " + mUnlockServiceUuid.getUuid());
- }
- return;
- }
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Unlock Service # characteristics: "
- + service.getCharacteristics().size());
- }
-
- mUnlockEscrowToken = BluetoothUtils.getCharacteristic(
- R.string.unlock_escrow_token_uiid, service, mContext);
- mUnlockTokenHandle = BluetoothUtils.getCharacteristic(
- R.string.unlock_handle_uiid, service, mContext);
- appendOutputText("Unlock BLE client successfully connected");
-
- mHandler.post(() -> {
- // Services are now set up, allow users to enrol new escrow tokens.
- mUnlockButton.setEnabled(true);
- mUnlockButton.setAlpha(1.0f);
- });
- }
- };
-}
diff --git a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/SimpleBleClient.java b/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/SimpleBleClient.java
deleted file mode 100644
index 3cce775..0000000
--- a/tests/CarTrustAgentClientApp/src/com/android/car/trust/client/SimpleBleClient.java
+++ /dev/null
@@ -1,405 +0,0 @@
-/*
- * Copyright (C) 2018 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.android.car.trust.client;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCallback;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattService;
-import android.bluetooth.BluetoothManager;
-import android.bluetooth.BluetoothProfile;
-import android.bluetooth.le.BluetoothLeScanner;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.content.Context;
-import android.os.Handler;
-import android.os.ParcelUuid;
-import android.util.Log;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Queue;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.TimeUnit;
-
-/**
- * A simple client that supports the scanning and connecting to available BLE devices. Should be
- * used along with {@link SimpleBleServer}.
- */
-public class SimpleBleClient {
- private static final String TAG = "SimpleBleClient";
- private static final long SCAN_TIME_MS = TimeUnit.SECONDS.toMillis(10);
-
- private final Queue<BleAction> mBleActionQueue = new ConcurrentLinkedQueue<BleAction>();
- private final List<ClientCallback> mCallbacks = new ArrayList<>();
- private final Context mContext;
- private final BluetoothLeScanner mScanner;
-
- private BluetoothGatt mBtGatt;
- private ParcelUuid mServiceUuid;
-
- public SimpleBleClient(Context context) {
- mContext = context;
- BluetoothManager btManager = (BluetoothManager) mContext.getSystemService(
- Context.BLUETOOTH_SERVICE);
- mScanner = btManager.getAdapter().getBluetoothLeScanner();
- }
-
- /**
- * Start scanning for a BLE devices with the specified service uuid.
- *
- * @param parcelUuid {@link ParcelUuid} used to identify the device that should be used for
- * this client. This uuid should be the same as the one that is set in the
- * {@link android.bluetooth.le.AdvertiseData.Builder} by the advertising
- * device.
- */
- public void start(ParcelUuid parcelUuid) {
- mServiceUuid = parcelUuid;
-
- // We only want to scan for devices that have the correct uuid set in its advertise data.
- List<ScanFilter> filters = new ArrayList<ScanFilter>();
- ScanFilter.Builder serviceFilter = new ScanFilter.Builder();
- serviceFilter.setServiceUuid(mServiceUuid);
- filters.add(serviceFilter.build());
-
- ScanSettings.Builder settings = new ScanSettings.Builder();
- settings.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Start scanning for uuid: " + mServiceUuid.getUuid());
- }
-
- mScanner.startScan(filters, settings.build(), mScanCallback);
-
- Handler handler = new Handler();
- handler.postDelayed(new Runnable() {
- @Override
- public void run() {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Stopping Scanner");
- }
- mScanner.stopScan(mScanCallback);
- }
- }, SCAN_TIME_MS);
- }
-
- private boolean hasServiceUuid(ScanResult result) {
- if (result.getScanRecord() == null
- || result.getScanRecord().getServiceUuids() == null
- || result.getScanRecord().getServiceUuids().size() == 0) {
- return false;
- }
- return true;
- }
-
- /**
- * Writes to a {@link BluetoothGattCharacteristic} if possible, or queues the action until
- * other actions are complete.
- *
- * @param characteristic {@link BluetoothGattCharacteristic} to be written
- */
- public void writeCharacteristic(BluetoothGattCharacteristic characteristic) {
- processAction(new BleAction(characteristic, BleAction.ACTION_WRITE));
- }
-
- /**
- * Reads a {@link BluetoothGattCharacteristic} if possible, or queues the read action until
- * other actions are complete.
- *
- * @param characteristic {@link BluetoothGattCharacteristic} to be read.
- */
- public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
- processAction(new BleAction(characteristic, BleAction.ACTION_READ));
- }
-
- /**
- * Enable or disable notification for specified {@link BluetoothGattCharacteristic}.
- *
- * @param characteristic The {@link BluetoothGattCharacteristic} for which to enable
- * notifications.
- * @param enabled True if notifications should be enabled, false otherwise.
- */
- public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
- boolean enabled) {
- mBtGatt.setCharacteristicNotification(characteristic, enabled);
- }
-
- /**
- * Add a {@link ClientCallback} to listen for updates from BLE components
- */
- public void addCallback(ClientCallback callback) {
- mCallbacks.add(callback);
- }
-
- public void removeCallback(ClientCallback callback) {
- mCallbacks.remove(callback);
- }
-
- private void processAction(BleAction action) {
- // Only execute actions if the queue is empty.
- if (mBleActionQueue.size() > 0) {
- mBleActionQueue.add(action);
- return;
- }
-
- mBleActionQueue.add(action);
- executeAction(mBleActionQueue.peek());
- }
-
- private void processNextAction() {
- mBleActionQueue.poll();
- executeAction(mBleActionQueue.peek());
- }
-
- private void executeAction(@Nullable BleAction action) {
- if (action == null) {
- return;
- }
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Executing BLE Action type: " + action.getAction());
- }
-
- switch (action.getAction()) {
- case BleAction.ACTION_WRITE:
- mBtGatt.writeCharacteristic(action.getCharacteristic());
- break;
- case BleAction.ACTION_READ:
- mBtGatt.readCharacteristic(action.getCharacteristic());
- break;
- default:
- Log.e(TAG, "Encountered unknown BlueAction: " + action.getAction());
- }
- }
-
- private String getStatus(int status) {
- switch (status) {
- case BluetoothGatt.GATT_FAILURE:
- return "Failure";
- case BluetoothGatt.GATT_SUCCESS:
- return "GATT_SUCCESS";
- case BluetoothGatt.GATT_READ_NOT_PERMITTED:
- return "GATT_READ_NOT_PERMITTED";
- case BluetoothGatt.GATT_WRITE_NOT_PERMITTED:
- return "GATT_WRITE_NOT_PERMITTED";
- case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
- return "GATT_INSUFFICIENT_AUTHENTICATION";
- case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED:
- return "GATT_REQUEST_NOT_SUPPORTED";
- case BluetoothGatt.GATT_INVALID_OFFSET:
- return "GATT_INVALID_OFFSET";
- case BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH:
- return "GATT_INVALID_ATTRIBUTE_LENGTH";
- case BluetoothGatt.GATT_CONNECTION_CONGESTED:
- return "GATT_CONNECTION_CONGESTED";
- default:
- return "unknown";
- }
- }
-
- private ScanCallback mScanCallback = new ScanCallback() {
- @Override
- public void onScanResult(int callbackType, ScanResult result) {
- BluetoothDevice device = result.getDevice();
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Scan result found: " + result.getScanRecord().getServiceUuids());
- }
-
- if (!hasServiceUuid(result)) {
- return;
- }
-
- for (ParcelUuid uuid : result.getScanRecord().getServiceUuids()) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Scan result UUID: " + uuid);
- }
-
- if (uuid.equals(mServiceUuid)) {
- // This client only supports connecting to one service.
- // Once we find one, stop scanning and open a GATT connection to the device.
- mScanner.stopScan(mScanCallback);
- mBtGatt = device.connectGatt(mContext, /* autoConnect= */ false, mGattCallback);
- return;
- }
- }
- }
-
- @Override
- public void onBatchScanResults(List<ScanResult> results) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- for (ScanResult r : results) {
- Log.d(TAG, "Batch scanResult: " + r.getDevice().getName()
- + " " + r.getDevice().getAddress());
- }
- }
- }
-
- @Override
- public void onScanFailed(int errorCode) {
- Log.e(TAG, "Scan failed: " + errorCode);
- }
- };
-
- private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
- @Override
- public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
- super.onConnectionStateChange(gatt, status, newState);
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Gatt connection status: " + getStatus(status)
- + " newState: " + newState);
- }
-
- switch (newState) {
- case BluetoothProfile.STATE_CONNECTED:
- mBtGatt.discoverServices();
- for (ClientCallback callback : mCallbacks) {
- callback.onDeviceConnected(gatt.getDevice());
- }
- break;
-
- case BluetoothProfile.STATE_DISCONNECTED:
- for (ClientCallback callback : mCallbacks) {
- callback.onDeviceDisconnected();
- }
- break;
-
- default:
- // Do nothing.
- }
- }
-
- @Override
- public void onServicesDiscovered(BluetoothGatt gatt, int status) {
- super.onServicesDiscovered(gatt, status);
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onServicesDiscovered: " + status);
- }
-
- List<BluetoothGattService> services = gatt.getServices();
- if (services == null || services.size() <= 0) {
- return;
- }
-
- // Notify clients of newly discovered services.
- for (BluetoothGattService service : mBtGatt.getServices()) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Found service: " + service.getUuid() + " notifying clients");
- }
-
- for (ClientCallback callback : mCallbacks) {
- callback.onServiceDiscovered(service);
- }
- }
- }
-
- @Override
- public void onCharacteristicWrite(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic, int status) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCharacteristicWrite: " + status);
- }
-
- processNextAction();
- }
-
- @Override
- public void onCharacteristicRead(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic, int status) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onCharacteristicRead:" + new String(characteristic.getValue()));
- }
-
- processNextAction();
- }
-
- @Override
- public void onCharacteristicChanged(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic) {
- for (ClientCallback callback : mCallbacks) {
- callback.onCharacteristicChanged(gatt, characteristic);
- }
- processNextAction();
- }
- };
-
- /**
- * Wrapper class to allow queuing of BLE actions. The BLE stack allows only one action to be
- * executed at a time.
- */
- private static class BleAction {
- public static final int ACTION_WRITE = 0;
- public static final int ACTION_READ = 1;
-
- @IntDef({ ACTION_WRITE, ACTION_READ })
- public @interface ActionType {}
-
- private final int mAction;
- private final BluetoothGattCharacteristic mCharacteristic;
-
- BleAction(BluetoothGattCharacteristic characteristic, @ActionType int action) {
- mAction = action;
- mCharacteristic = characteristic;
- }
-
- @ActionType
- public int getAction() {
- return mAction;
- }
-
- public BluetoothGattCharacteristic getCharacteristic() {
- return mCharacteristic;
- }
- }
-
- /**
- * Callback for classes that wish to be notified of BLE updates.
- */
- public interface ClientCallback {
- /**
- * Called when a device that has a matching service UUID is found.
- **/
- void onDeviceConnected(BluetoothDevice device);
-
- /** Called when the currently connected device has been disconnected. */
- void onDeviceDisconnected();
-
- /**
- * Called when a characteristic has been changed.
- *
- * @param gatt The GATT client the characteristic is associated with.
- * @param characteristic The characteristic that has been changed.
- */
- void onCharacteristicChanged(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic);
-
- /**
- * Called for each {@link BluetoothGattService} that is discovered on the
- * {@link BluetoothDevice} after a matching scan result and connection.
- *
- * @param service {@link BluetoothGattService} that has been discovered.
- */
- void onServiceDiscovered(BluetoothGattService service);
- }
-}
diff --git a/tests/CarVoiceServiceTriggerApp/Android.mk b/tests/CarVoiceServiceTriggerApp/Android.mk
new file mode 100644
index 0000000..b492e1d
--- /dev/null
+++ b/tests/CarVoiceServiceTriggerApp/Android.mk
@@ -0,0 +1,50 @@
+# Copyright (C) 2015 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.
+#
+#
+
+ifneq ($(TARGET_BUILD_PDK),true)
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_USE_AAPT2 := true
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := CarVoiceTriggerApp
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_CERTIFICATE := platform
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
+
+include $(CLEAR_VARS)
+
+include $(BUILD_MULTI_PREBUILT)
+
+include $(CLEAR_VARS)
+
+endif #TARGET_BUILD_PDK
diff --git a/tests/CarVoiceServiceTriggerApp/AndroidManifest.xml b/tests/CarVoiceServiceTriggerApp/AndroidManifest.xml
new file mode 100644
index 0000000..0e43fa7
--- /dev/null
+++ b/tests/CarVoiceServiceTriggerApp/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.voicetrigger">
+
+ <uses-permission android:name="android.permission.ACCESS_VOICE_INTERACTION_SERVICE"/>
+
+ <application android:label="@string/app_title">
+ <receiver android:name=".VoiceTriggerReceiver" android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.VOICE_ASSIST"/>
+ </intent-filter>
+ </receiver>
+ </application>
+</manifest>
+
diff --git a/tests/CarVoiceServiceTriggerApp/res/values/strings.xml b/tests/CarVoiceServiceTriggerApp/res/values/strings.xml
new file mode 100644
index 0000000..3edaf54
--- /dev/null
+++ b/tests/CarVoiceServiceTriggerApp/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_title" translatable="false">Voice Service Trigger</string>
+</resources>
diff --git a/tests/CarVoiceServiceTriggerApp/src/com/android/voicetrigger/VoiceTriggerReceiver.java b/tests/CarVoiceServiceTriggerApp/src/com/android/voicetrigger/VoiceTriggerReceiver.java
new file mode 100644
index 0000000..f022fb1
--- /dev/null
+++ b/tests/CarVoiceServiceTriggerApp/src/com/android/voicetrigger/VoiceTriggerReceiver.java
@@ -0,0 +1,42 @@
+/*
+ * 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.android.voicetrigger;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.service.voice.VoiceInteractionSession;
+
+import com.android.internal.app.AssistUtils;
+
+/**
+ * The exported {@link BroadcastReceiver} which receives an Intent to trigger the current active
+ * voice service. The voice service will be triggered as if the assistant button in the system UI
+ * is clicked.
+ *
+ * Run adb shell am broadcast -a android.intent.action.VOICE_ASSIST -n
+ * com.android.voicetrigger/.VoiceTriggerReceiver to use.
+ */
+public class VoiceTriggerReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ AssistUtils assistUtils = new AssistUtils(context);
+ assistUtils.showSessionForActiveService(new Bundle(),
+ VoiceInteractionSession.SHOW_SOURCE_AUTOMOTIVE_SYSTEM_UI, null, null);
+ }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index 8997e32..5612807 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -116,5 +116,26 @@
android:grantUriPermissions="true"
android:exported="true" />
+ <activity android:name=".AlwaysCrashingActivity"
+ android:label="@string/always_crashing_activity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".NoCrashActivity"
+ android:label="@string/no_crash_activity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".EmptyActivity"
+ android:label="@string/empty_activity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+
</application>
</manifest>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/empty_activity.xml b/tests/EmbeddedKitchenSinkApp/res/layout/empty_activity.xml
new file mode 100644
index 0000000..5312fee
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/empty_activity.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <TextView
+ android:id="@+id/empty_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/empty_activity"
+ android:layout_weight="1" />
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 6575ce1..0d56369 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -332,4 +332,9 @@
<string name="usernotice" translatable="false">This screen is for showing initial user notice and is not for product. Plz change config_userNoticeUiService in CarService before shipping.</string>
<string name="dismiss_now" translatable="false">Dismiss for now</string>
<string name="dismiss_forever" translatable="false">Do not show again</string>
+
+ <!-- [AlwaysCrashing|NoCrash|Empty]Activity -->
+ <string name="always_crashing_activity" translatable="false">Always Crash Activity</string>
+ <string name="no_crash_activity" translatable="false">No Crash Activity</string>
+ <string name="empty_activity" translatable="false">Empty Activity</string>
</resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/AlwaysCrashingActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/AlwaysCrashingActivity.java
new file mode 100644
index 0000000..05529b2
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/AlwaysCrashingActivity.java
@@ -0,0 +1,30 @@
+/*
+ * 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.kitchensink;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/**
+ * Activity for testing purpose. This one always crashes inside onCreate.
+ */
+public class AlwaysCrashingActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ throw new RuntimeException("Intended crash for testing");
+ }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/EmptyActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/EmptyActivity.java
new file mode 100644
index 0000000..aad25cb
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/EmptyActivity.java
@@ -0,0 +1,29 @@
+/*
+ * 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.kitchensink;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class EmptyActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.empty_activity);
+ }
+
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/NoCrashActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/NoCrashActivity.java
new file mode 100644
index 0000000..f10e1db
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/NoCrashActivity.java
@@ -0,0 +1,31 @@
+/*
+ * 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.kitchensink;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.TextView;
+
+public class NoCrashActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.empty_activity);
+ TextView text = findViewById(R.id.empty_text);
+ text.setText(R.string.no_crash_activity);
+ }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
index b03e320..84b6bcf 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/audio/AudioTestFragment.java
@@ -21,9 +21,7 @@
import android.car.CarAppFocusManager.OnAppFocusChangedListener;
import android.car.CarAppFocusManager.OnAppFocusOwnershipCallback;
import android.car.media.CarAudioManager;
-import android.content.ComponentName;
import android.content.Context;
-import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.hardware.display.DisplayManager;
@@ -34,7 +32,6 @@
import android.media.HwAudioSource;
import android.os.Bundle;
import android.os.Handler;
-import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import android.view.Display;
@@ -124,41 +121,39 @@
private void connectCar() {
mContext = getContext();
mHandler = new Handler(Looper.getMainLooper());
- mCar = Car.createCar(mContext, new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- mAppFocusManager =
- (CarAppFocusManager) mCar.getCarManager(Car.APP_FOCUS_SERVICE);
- OnAppFocusChangedListener listener = new OnAppFocusChangedListener() {
- @Override
- public void onAppFocusChanged(int appType, boolean active) {
+ mCar = Car.createCar(mContext, /* handler= */ null,
+ Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, (car, ready) -> {
+ if (!ready) {
+ return;
}
- };
- mAppFocusManager.addFocusListener(listener,
- CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
- mAppFocusManager.addFocusListener(listener,
- CarAppFocusManager.APP_FOCUS_TYPE_VOICE_COMMAND);
+ mAppFocusManager =
+ (CarAppFocusManager) car.getCarManager(Car.APP_FOCUS_SERVICE);
+ OnAppFocusChangedListener listener = new OnAppFocusChangedListener() {
+ @Override
+ public void onAppFocusChanged(int appType, boolean active) {
+ }
+ };
+ mAppFocusManager.addFocusListener(listener,
+ CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
+ mAppFocusManager.addFocusListener(listener,
+ CarAppFocusManager.APP_FOCUS_TYPE_VOICE_COMMAND);
- mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
+ mCarAudioManager = (CarAudioManager) car.getCarManager(Car.AUDIO_SERVICE);
- //take care of zone selection
- int[] zoneList = mCarAudioManager.getAudioZoneIds();
- Integer[] zoneArray = Arrays.stream(zoneList).boxed().toArray(Integer[]::new);
- mZoneAdapter = new ArrayAdapter<>(mContext,
- android.R.layout.simple_spinner_item, zoneArray);
- mZoneAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- mZoneSpinner.setAdapter(mZoneAdapter);
- mZoneSpinner.setEnabled(true);
+ //take care of zone selection
+ int[] zoneList = mCarAudioManager.getAudioZoneIds();
+ Integer[] zoneArray = Arrays.stream(zoneList).boxed().toArray(Integer[]::new);
+ mZoneAdapter = new ArrayAdapter<>(mContext,
+ android.R.layout.simple_spinner_item, zoneArray);
+ mZoneAdapter.setDropDownViewResource(
+ android.R.layout.simple_spinner_dropdown_item);
+ mZoneSpinner.setAdapter(mZoneAdapter);
+ mZoneSpinner.setEnabled(true);
- if (mCarAudioManager.isDynamicRoutingEnabled()) {
- setUpDisplayPlayer();
- }
- }
- @Override
- public void onServiceDisconnected(ComponentName name) {
- }
- });
- mCar.connect();
+ if (mCarAudioManager.isDynamicRoutingEnabled()) {
+ setUpDisplayPlayer();
+ }
+ });
}
private void initializePlayers() {
@@ -217,9 +212,16 @@
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
Log.i(TAG, "onCreateView");
+ View view = inflater.inflate(R.layout.audio, container, false);
+ //Zone Spinner
+ setUpZoneSpinnerView(view);
+
+ //Display layout
+ setUpDisplayLayoutView(view);
+
connectCar();
initializePlayers();
- View view = inflater.inflate(R.layout.audio, container, false);
+
mAudioManager = (AudioManager) mContext.getSystemService(
Context.AUDIO_SERVICE);
mAudioFocusHandler = new FocusHandler(
@@ -331,35 +333,6 @@
}
});
- //Zone Spinner
- mZoneSpinner = view.findViewById(R.id.zone_spinner);
- mZoneSpinner.setEnabled(false);
- mZoneSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
- handleZoneSelection();
- }
-
- @Override
- public void onNothingSelected(AdapterView<?> parent) {
- }
- });
-
-
- mDisplayLayout = view.findViewById(R.id.audio_display_layout);
-
- mDisplaySpinner = view.findViewById(R.id.display_spinner);
- mDisplaySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
- @Override
- public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
- handleDisplaySelection();
- }
-
- @Override
- public void onNothingSelected(AdapterView<?> parent) {
- }
- });
-
// Manage buttons for audio player for displays
view.findViewById(R.id.button_display_media_play_start).setOnClickListener(v -> {
startDisplayAudio();
@@ -375,6 +348,37 @@
return view;
}
+ private void setUpDisplayLayoutView(View view) {
+ mDisplayLayout = view.findViewById(R.id.audio_display_layout);
+
+ mDisplaySpinner = view.findViewById(R.id.display_spinner);
+ mDisplaySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+ handleDisplaySelection();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ });
+ }
+
+ private void setUpZoneSpinnerView(View view) {
+ mZoneSpinner = view.findViewById(R.id.zone_spinner);
+ mZoneSpinner.setEnabled(false);
+ mZoneSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+ handleZoneSelection();
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+ });
+ }
+
public void handleZoneSelection() {
int position = mZoneSpinner.getSelectedItemPosition();
int zone = mZoneAdapter.getItem(position);
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
index 7657c38..8886913 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
@@ -17,6 +17,7 @@
import android.annotation.Nullable;
import android.car.Car;
+import android.car.Car.CarServiceLifecycleListener;
import android.car.CarAppFocusManager;
import android.car.CarNotConnectedException;
import android.car.cluster.navigation.NavigationState;
@@ -33,11 +34,8 @@
import android.car.cluster.navigation.NavigationState.Step;
import android.car.cluster.navigation.NavigationState.Timestamp;
import android.car.navigation.CarNavigationStatusManager;
-import android.content.ComponentName;
-import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Bundle;
-import android.os.IBinder;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -73,19 +71,19 @@
private NavigationStateProto[] mNavStateData;
private Button mTurnByTurnButton;
- private ServiceConnection mCarServiceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- Log.d(TAG, "Connected to Car Service");
- mCarNavigationStatusManager = (CarNavigationStatusManager) mCarApi
- .getCarManager(Car.CAR_NAVIGATION_SERVICE);
- mCarAppFocusManager = (CarAppFocusManager) mCarApi
- .getCarManager(Car.APP_FOCUS_SERVICE);
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
+ private CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
+ if (!ready) {
Log.d(TAG, "Disconnect from Car Service");
+ return;
+ }
+ Log.d(TAG, "Connected to Car Service");
+ try {
+ mCarNavigationStatusManager = (CarNavigationStatusManager) car.getCarManager(
+ Car.CAR_NAVIGATION_SERVICE);
+ mCarAppFocusManager = (CarAppFocusManager) car.getCarManager(
+ Car.APP_FOCUS_SERVICE);
+ } catch (CarNotConnectedException e) {
+ Log.e(TAG, "Car is not connected!", e);
}
};
@@ -117,13 +115,8 @@
private void initCarApi() {
- if (mCarApi != null && mCarApi.isConnected()) {
- mCarApi.disconnect();
- mCarApi = null;
- }
-
- mCarApi = Car.createCar(getContext(), mCarServiceConnection);
- mCarApi.connect();
+ mCarApi = Car.createCar(getContext(), /* handler= */ null,
+ Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener);
}
@NonNull
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
index 73e3798..df9aa7b 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
@@ -16,14 +16,12 @@
package com.google.android.car.kitchensink.volume;
import android.car.Car;
+import android.car.Car.CarServiceLifecycleListener;
import android.car.media.CarAudioManager;
-import android.content.ComponentName;
import android.content.Context;
-import android.content.ServiceConnection;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
-import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import android.util.SparseIntArray;
@@ -112,20 +110,15 @@
public boolean mHasFocus;
}
- private final ServiceConnection mCarConnectionCallback =
- new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder binder) {
- Log.d(TAG, "Connected to Car Service");
- mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
- initVolumeInfo();
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- Log.d(TAG, "Disconnect from Car Service");
- }
- };
+ private CarServiceLifecycleListener mCarServiceLifecycleListener = (car, ready) -> {
+ if (!ready) {
+ Log.d(TAG, "Disconnect from Car Service");
+ return;
+ }
+ Log.d(TAG, "Connected to Car Service");
+ mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
+ initVolumeInfo();
+ };
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -161,8 +154,8 @@
mBalance = v.findViewById(R.id.balance_bar);
mBalance.setOnSeekBarChangeListener(seekListener);
- mCar = Car.createCar(getActivity(), mCarConnectionCallback);
- mCar.connect();
+ mCar = Car.createCar(getActivity(), /* handler= */ null,
+ Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener);
return v;
}
@@ -210,12 +203,4 @@
}
mAdapter.refreshVolumes(mVolumeInfos);
}
-
- @Override
- public void onDestroy() {
- if (mCar != null) {
- mCar.disconnect();
- }
- super.onDestroy();
- }
}
diff --git a/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java b/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
index aadb6f9..0aa89f9 100644
--- a/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
+++ b/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
@@ -27,14 +27,18 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
+import android.car.VehiclePropertyIds;
import android.car.drivingstate.CarDrivingStateEvent;
import android.car.drivingstate.CarUxRestrictions;
import android.car.drivingstate.CarUxRestrictionsConfiguration;
import android.car.drivingstate.CarUxRestrictionsConfiguration.Builder;
+import android.car.drivingstate.ICarDrivingStateChangeListener;
import android.car.hardware.CarPropertyValue;
+import android.car.hardware.property.CarPropertyEvent;
import android.content.Context;
import android.content.res.Resources;
import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.os.RemoteException;
import android.os.SystemClock;
import android.util.JsonReader;
import android.util.JsonWriter;
@@ -46,6 +50,7 @@
import com.android.car.systeminterface.SystemInterface;
import org.junit.After;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -62,6 +67,7 @@
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
@RunWith(AndroidJUnit4.class)
@MediumTest
@@ -220,6 +226,198 @@
assertTrue(restrictions.toString(), expected.isSameRestrictions(restrictions));
}
+ // This test only involves calling a few methods and should finish very quickly. If it doesn't
+ // finish in 20s, we probably encountered a deadlock.
+ @Test(timeout = 20000)
+ public void testInitService_NoDeadlockWithCarDrivingStateService()
+ throws Exception {
+
+ CarDrivingStateService drivingStateService = new CarDrivingStateService(mSpyContext,
+ mMockCarPropertyService);
+ CarUxRestrictionsManagerService uxRestrictionsService = new CarUxRestrictionsManagerService(
+ mSpyContext, drivingStateService, mMockCarPropertyService);
+
+ CountDownLatch dispatchingStartedSignal = new CountDownLatch(1);
+ CountDownLatch initCompleteSignal = new CountDownLatch(1);
+
+ // A deadlock can exist when the dispatching of a listener is synchronized. For instance,
+ // the CarUxRestrictionsManagerService#init() method registers a callback like this one. The
+ // deadlock risk occurs if:
+ // 1. CarUxRestrictionsManagerService has registered a listener with CarDrivingStateService
+ // 2. A synchronized method of CarUxRestrictionsManagerService starts to run
+ // 3. While the method from (2) is running, a property event occurs on a different thread
+ // that triggers a drive state event in CarDrivingStateService. If CarDrivingStateService
+ // handles the property event in a synchronized method, then CarDrivingStateService is
+ // locked. The listener from (1) will wait until the lock on
+ // CarUxRestrictionsManagerService is released.
+ // 4. The synchronized method from (2) attempts to access CarDrivingStateService. For
+ // example, the implementation below attempts to read the restriction mode.
+ //
+ // In the above steps, both CarUxRestrictionsManagerService and CarDrivingStateService are
+ // locked and waiting on each other, hence the deadlock.
+ drivingStateService.registerDrivingStateChangeListener(
+ new ICarDrivingStateChangeListener.Stub() {
+ @Override
+ public void onDrivingStateChanged(CarDrivingStateEvent event)
+ throws RemoteException {
+ // EVENT 2 [new thread]: this callback is called from within
+ // handlePropertyEvent(), which might (but shouldn't) lock
+ // CarDrivingStateService
+
+ // Notify that the dispatching process has started
+ dispatchingStartedSignal.countDown();
+
+ try {
+ // EVENT 3b [new thread]: Wait until init() has finished. If these
+ // threads don't have lock dependencies, there is no reason there
+ // would be an issue with waiting.
+ //
+ // In the real world, this wait could represent a long-running
+ // task, or hitting the below line that attempts to access the
+ // CarUxRestrictionsManagerService (which might be locked while init
+ // () is running).
+ //
+ // If there is a deadlock while waiting for init to complete, we will
+ // never progress past this line.
+ initCompleteSignal.await();
+ } catch (InterruptedException e) {
+ Assert.fail("onDrivingStateChanged thread interrupted");
+ }
+
+ // Attempt to access CarUxRestrictionsManagerService. If
+ // CarUxRestrictionsManagerService is locked because it is doing its own
+ // work, then this will wait.
+ //
+ // This line won't execute in the deadlock flow. However, it is an example
+ // of a real-world piece of code that would serve the same role as the above
+ uxRestrictionsService.getCurrentUxRestrictions();
+ }
+ });
+
+ // EVENT 1 [new thread]: handlePropertyEvent() is called, which locks CarDrivingStateService
+ // Ideally CarPropertyService would trigger the change event, but since that is mocked
+ // we manually trigger the event. This event is what eventually triggers the dispatch to
+ // ICarDrivingStateChangeListener that was defined above.
+ Runnable propertyChangeEventRunnable =
+ () -> drivingStateService.handlePropertyEvent(
+ new CarPropertyEvent(CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE,
+ new CarPropertyValue<>(
+ VehiclePropertyIds.PERF_VEHICLE_SPEED, 0, 100f)));
+ Thread thread = new Thread(propertyChangeEventRunnable);
+ thread.start();
+
+ // Wait until propertyChangeEventRunnable has triggered and the
+ // ICarDrivingStateChangeListener callback declared above started to run.
+ dispatchingStartedSignal.await();
+
+ // EVENT 3a [main thread]: init() is called, which locks CarUxRestrictionsManagerService
+ // If init() is synchronized, thereby locking CarUxRestrictionsManagerService, and it
+ // internally attempts to access CarDrivingStateService, and if CarDrivingStateService has
+ // been locked because of the above listener, then both classes are locked and waiting on
+ // each other, so we would encounter a deadlock.
+ uxRestrictionsService.init();
+
+ // If there is a deadlock in init(), then this will never be called
+ initCompleteSignal.countDown();
+
+ // wait for thread to join to leave in a deterministic state
+ try {
+ thread.join(5000);
+ } catch (InterruptedException e) {
+ Assert.fail("Thread failed to join");
+ }
+ }
+
+ // This test only involves calling a few methods and should finish very quickly. If it doesn't
+ // finish in 20s, we probably encountered a deadlock.
+ @Test(timeout = 20000)
+ public void testSetUxRChangeBroadcastEnabled_NoDeadlockWithCarDrivingStateService()
+ throws Exception {
+
+ CarDrivingStateService drivingStateService = new CarDrivingStateService(mSpyContext,
+ mMockCarPropertyService);
+ CarUxRestrictionsManagerService uxRestrictionService = new CarUxRestrictionsManagerService(
+ mSpyContext, drivingStateService, mMockCarPropertyService);
+
+ CountDownLatch dispatchingStartedSignal = new CountDownLatch(1);
+ CountDownLatch initCompleteSignal = new CountDownLatch(1);
+
+ // See testInitService_NoDeadlockWithCarDrivingStateService for details on why a deadlock
+ // may occur. This test could fail for the same reason, except the callback we register here
+ // is purely to introduce a delay, and the deadlock actually happens inside the callback
+ // that CarUxRestrictionsManagerService#init() registers internally.
+ drivingStateService.registerDrivingStateChangeListener(
+ new ICarDrivingStateChangeListener.Stub() {
+ @Override
+ public void onDrivingStateChanged(CarDrivingStateEvent event)
+ throws RemoteException {
+ // EVENT 2 [new thread]: this callback is called from within
+ // handlePropertyEvent(), which might (but shouldn't) lock
+ // CarDrivingStateService
+
+ // Notify that the dispatching process has started
+ dispatchingStartedSignal.countDown();
+
+ try {
+ // EVENT 3b [new thread]: Wait until init() has finished. If these
+ // threads don't have lock dependencies, there is no reason there
+ // would be an issue with waiting.
+ //
+ // In the real world, this wait could represent a long-running
+ // task, or hitting the line inside
+ // CarUxRestrictionsManagerService#init()'s internal registration
+ // that attempts to access the CarUxRestrictionsManagerService (which
+ // might be locked while init() is running).
+ //
+ // If there is a deadlock while waiting for init to complete, we will
+ // never progress past this line.
+ initCompleteSignal.await();
+ } catch (InterruptedException e) {
+ Assert.fail("onDrivingStateChanged thread interrupted");
+ }
+ }
+ });
+
+ // The init() method internally registers a callback to CarDrivingStateService
+ uxRestrictionService.init();
+
+ // EVENT 1 [new thread]: handlePropertyEvent() is called, which locks CarDrivingStateService
+ // Ideally CarPropertyService would trigger the change event, but since that is mocked
+ // we manually trigger the event. This event eventually triggers the dispatch to
+ // ICarDrivingStateChangeListener that was defined above and a dispatch to the registration
+ // that CarUxRestrictionsManagerService internally made to CarDrivingStateService in
+ // CarUxRestrictionsManagerService#init().
+ Runnable propertyChangeEventRunnable =
+ () -> drivingStateService.handlePropertyEvent(
+ new CarPropertyEvent(CarPropertyEvent.PROPERTY_EVENT_PROPERTY_CHANGE,
+ new CarPropertyValue<>(
+ VehiclePropertyIds.PERF_VEHICLE_SPEED, 0, 100f)));
+ Thread thread = new Thread(propertyChangeEventRunnable);
+ thread.start();
+
+ // Wait until propertyChangeEventRunnable has triggered and the
+ // ICarDrivingStateChangeListener callback declared above started to run.
+ dispatchingStartedSignal.await();
+
+ // EVENT 3a [main thread]: a synchronized method is called, which locks
+ // CarUxRestrictionsManagerService
+ //
+ // Any synchronized method that internally accesses CarDrivingStateService could encounter a
+ // deadlock if the above setup locks CarDrivingStateService.
+ uxRestrictionService.setUxRChangeBroadcastEnabled(true);
+
+ // If there is a deadlock in init(), then this will never be called
+ initCompleteSignal.countDown();
+
+ // wait for thread to join to leave in a deterministic state
+ try {
+ thread.join(5000);
+ } catch (InterruptedException e) {
+ Assert.fail("Thread failed to join");
+ }
+ }
+
+
private CarUxRestrictionsConfiguration createEmptyConfig() {
return createEmptyConfig(null);
}
diff --git a/tests/carservice_test/src/com/android/car/MockedCarTestBase.java b/tests/carservice_test/src/com/android/car/MockedCarTestBase.java
index 894c402..69eba19 100644
--- a/tests/carservice_test/src/com/android/car/MockedCarTestBase.java
+++ b/tests/carservice_test/src/com/android/car/MockedCarTestBase.java
@@ -56,6 +56,7 @@
import com.android.car.vehiclehal.test.MockedVehicleHal.StaticPropertyHandler;
import com.android.car.vehiclehal.test.MockedVehicleHal.VehicleHalPropertyHandler;
import com.android.car.vehiclehal.test.VehiclePropConfigBuilder;
+import com.android.car.vms.VmsClientManager;
import org.junit.After;
import org.junit.Before;
@@ -185,6 +186,10 @@
return (CarPackageManagerService) mCarImpl.getCarService(Car.PACKAGE_SERVICE);
}
+ public VmsClientManager getVmsClientManager() {
+ return (VmsClientManager) mCarImpl.getCarInternalService(ICarImpl.INTERNAL_VMS_MANAGER);
+ }
+
protected Context getCarServiceContext() {
return getContext();
}
@@ -261,11 +266,10 @@
if (mRealCarServiceReleased) {
return; // We just want to release it once.
}
-
mRealCarServiceReleased = true; // To make sure it was called once.
Object waitForConnection = new Object();
- android.car.Car car = android.car.Car.createCar(context, new ServiceConnection() {
+ Car car = android.car.Car.createCar(context, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (waitForConnection) {
@@ -287,10 +291,10 @@
if (car.isConnected()) {
Log.i(TAG, "Connected to real car service");
CarTestManagerBinderWrapper binderWrapper =
- (CarTestManagerBinderWrapper) car.getCarManager(android.car.Car.TEST_SERVICE);
+ (CarTestManagerBinderWrapper) car.getCarManager(Car.TEST_SERVICE);
assertNotNull(binderWrapper);
- CarTestManager mgr = new CarTestManager(binderWrapper.binder);
+ CarTestManager mgr = new CarTestManager(car, binderWrapper.binder);
mgr.stopCarService(mCarServiceToken);
}
}
diff --git a/tests/carservice_test/src/com/android/car/MockedVmsTestBase.java b/tests/carservice_test/src/com/android/car/MockedVmsTestBase.java
index 450b9e9..ccc2e6c 100644
--- a/tests/carservice_test/src/com/android/car/MockedVmsTestBase.java
+++ b/tests/carservice_test/src/com/android/car/MockedVmsTestBase.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import android.app.ActivityManager;
import android.car.Car;
import android.car.VehicleAreaType;
import android.car.vms.VmsAvailableLayers;
@@ -25,7 +26,6 @@
import android.car.vms.VmsPublisherClientService;
import android.car.vms.VmsSubscriberManager;
import android.car.vms.VmsSubscriptionState;
-import android.content.Intent;
import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
import android.hardware.automotive.vehicle.V2_0.VehiclePropertyAccess;
@@ -34,7 +34,6 @@
import android.hardware.automotive.vehicle.V2_0.VmsBaseMessageIntegerValuesIndex;
import android.hardware.automotive.vehicle.V2_0.VmsMessageType;
import android.hardware.automotive.vehicle.V2_0.VmsStartSessionMessageIntegerValuesIndex;
-import android.os.UserHandle;
import android.util.Log;
import android.util.Pair;
@@ -83,8 +82,7 @@
@Before
public void setUpVms() throws Exception {
// Trigger VmsClientManager to bind to the MockPublisherClient
- getContext().sendBroadcastAsUser(new Intent(Intent.ACTION_USER_UNLOCKED), UserHandle.ALL);
-
+ getVmsClientManager().mUserCallback.onSwitchUser(ActivityManager.getCurrentUser());
mVmsSubscriberManager = (VmsSubscriberManager) getCar().getCarManager(
Car.VMS_SUBSCRIBER_SERVICE);
mSubscriberClient = new MockSubscriberClient();
diff --git a/tests/carservice_test/src/com/android/car/VmsPublisherClientPermissionTest.java b/tests/carservice_test/src/com/android/car/VmsPublisherClientPermissionTest.java
index 0e3adde..0bb9d4f 100644
--- a/tests/carservice_test/src/com/android/car/VmsPublisherClientPermissionTest.java
+++ b/tests/carservice_test/src/com/android/car/VmsPublisherClientPermissionTest.java
@@ -21,10 +21,9 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import android.app.ActivityManager;
import android.car.vms.VmsPublisherClientService;
import android.car.vms.VmsSubscriptionState;
-import android.content.Intent;
-import android.os.UserHandle;
import org.junit.Before;
import org.junit.Test;
@@ -98,7 +97,7 @@
@Before
public void triggerClientBinding() {
- getContext().sendBroadcastAsUser(new Intent(Intent.ACTION_USER_UNLOCKED), UserHandle.ALL);
+ getVmsClientManager().mUserCallback.onSwitchUser(ActivityManager.getCurrentUser());
}
@Test
diff --git a/tests/carservice_unit_test/AndroidManifest.xml b/tests/carservice_unit_test/AndroidManifest.xml
index e5e31bf..5ed59ef 100644
--- a/tests/carservice_unit_test/AndroidManifest.xml
+++ b/tests/carservice_unit_test/AndroidManifest.xml
@@ -21,6 +21,7 @@
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.car.carservice_unittest"
android:label="Unit Tests for Car APIs"/>
+ <uses-permission android:name="android.car.permission.CAR_ENROLL_TRUST" />
<application android:label="CarServiceUnitTest"
android:debuggable="true">
diff --git a/tests/carservice_unit_test/src/android/car/CarTest.java b/tests/carservice_unit_test/src/android/car/CarTest.java
index 9ac8d75..d1f5ac6 100644
--- a/tests/carservice_unit_test/src/android/car/CarTest.java
+++ b/tests/carservice_unit_test/src/android/car/CarTest.java
@@ -152,7 +152,9 @@
@Test
public void testCreateCarSuccessWithCarServiceRunning() {
expectService(mService);
- assertThat(Car.createCar(mContext)).isNotNull();
+ Car car = Car.createCar(mContext);
+ assertThat(car).isNotNull();
+ car.disconnect();
}
@Test
@@ -171,14 +173,7 @@
Car car = Car.createCar(mContext);
assertThat(car).isNotNull();
assertServiceBoundOnce();
-
- // Just call these to guarantee that nothing crashes when service is connected /
- // disconnected.
- runOnMainSyncSafe(() -> {
- car.getServiceConnectionListener().onServiceConnected(new ComponentName("", ""),
- mService);
- car.getServiceConnectionListener().onServiceDisconnected(new ComponentName("", ""));
- });
+ car.disconnect();
}
@Test
diff --git a/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
index fa82f90..056fe9c 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarPowerManagementServiceTest.java
@@ -142,6 +142,86 @@
mSystemStateInterface.waitForShutdown(WAIT_TIMEOUT_MS);
}
+ public void testSuspend() throws Exception {
+ final int wakeupTime = 100;
+ initTest(wakeupTime);
+
+ // Start in the ON state
+ mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
+ assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
+ // Request suspend
+ mPowerHal.setCurrentPowerState(
+ new PowerState(
+ VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+ VehicleApPowerStateShutdownParam.CAN_SLEEP));
+ // Verify suspend
+ assertStateReceivedForShutdownOrSleepWithPostpone(
+ PowerHalService.SET_DEEP_SLEEP_ENTRY, WAIT_TIMEOUT_LONG_MS, wakeupTime);
+ }
+
+ public void testShutdownOnSuspend() throws Exception {
+ final int wakeupTime = 100;
+ initTest(wakeupTime);
+
+ // Start in the ON state
+ mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
+ assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
+ // Tell it to shutdown
+ mService.requestShutdownOnNextSuspend();
+ // Request suspend
+ mPowerHal.setCurrentPowerState(
+ new PowerState(
+ VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+ VehicleApPowerStateShutdownParam.CAN_SLEEP));
+ // Verify shutdown
+ assertStateReceivedForShutdownOrSleepWithPostpone(
+ PowerHalService.SET_SHUTDOWN_START, WAIT_TIMEOUT_LONG_MS, wakeupTime);
+ mPowerSignalListener.waitForShutdown(WAIT_TIMEOUT_MS);
+ // Send the finished signal
+ mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.FINISHED, 0));
+ mSystemStateInterface.waitForShutdown(WAIT_TIMEOUT_MS);
+ // Cancel the shutdown
+ mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.CANCEL_SHUTDOWN, 0));
+ assertStateReceivedForShutdownOrSleepWithPostpone(
+ PowerHalService.SET_SHUTDOWN_CANCELLED, WAIT_TIMEOUT_LONG_MS, 0);
+
+ // Request suspend again
+ mPowerHal.setCurrentPowerState(
+ new PowerState(
+ VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+ VehicleApPowerStateShutdownParam.CAN_SLEEP));
+ // Verify suspend
+ assertStateReceivedForShutdownOrSleepWithPostpone(
+ PowerHalService.SET_DEEP_SLEEP_ENTRY, WAIT_TIMEOUT_LONG_MS, wakeupTime);
+ }
+
+ public void testShutdownCancel() throws Exception {
+ final int wakeupTime = 100;
+ initTest(wakeupTime);
+
+ // Start in the ON state
+ mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
+ assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
+ // Start shutting down
+ mPowerHal.setCurrentPowerState(
+ new PowerState(
+ VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+ VehicleApPowerStateShutdownParam.SHUTDOWN_IMMEDIATELY));
+ assertStateReceivedForShutdownOrSleepWithPostpone(
+ PowerHalService.SET_SHUTDOWN_START, WAIT_TIMEOUT_LONG_MS, 0);
+ // Cancel the shutdown
+ mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.CANCEL_SHUTDOWN, 0));
+ assertStateReceivedForShutdownOrSleepWithPostpone(
+ PowerHalService.SET_SHUTDOWN_CANCELLED, WAIT_TIMEOUT_LONG_MS, 0);
+ // Go to suspend
+ mPowerHal.setCurrentPowerState(
+ new PowerState(
+ VehicleApPowerStateReq.SHUTDOWN_PREPARE,
+ VehicleApPowerStateShutdownParam.CAN_SLEEP));
+ assertStateReceivedForShutdownOrSleepWithPostpone(
+ PowerHalService.SET_DEEP_SLEEP_ENTRY, WAIT_TIMEOUT_LONG_MS, wakeupTime);
+ }
+
public void testShutdownWithProcessing() throws Exception {
final int wakeupTime = 100;
initTest(wakeupTime);
@@ -197,7 +277,7 @@
// second processing after wakeup
assertFalse(mDisplayInterface.getDisplayState());
// do not skip user switching part.
- mService.clearIsBooting();
+ mService.clearIsBootingOrResuming();
mPowerHal.setCurrentPowerState(new PowerState(VehicleApPowerStateReq.ON, 0));
assertTrue(mDisplayInterface.waitForDisplayStateChange(WAIT_TIMEOUT_MS));
// user switching should have been requested.
diff --git a/tests/carservice_unit_test/src/com/android/car/MockedPowerHalService.java b/tests/carservice_unit_test/src/com/android/car/MockedPowerHalService.java
index 7359a03..770cc85 100644
--- a/tests/carservice_unit_test/src/com/android/car/MockedPowerHalService.java
+++ b/tests/carservice_unit_test/src/com/android/car/MockedPowerHalService.java
@@ -87,6 +87,12 @@
doSendState(SET_SHUTDOWN_START, wakeupTimeSec);
}
+ @Override
+ public void sendShutdownCancel() {
+ Log.i(TAG, "sendShutdownCancel");
+ doSendState(SET_SHUTDOWN_CANCELLED, 0);
+ }
+
public synchronized int[] waitForSend(long timeoutMs) throws Exception {
if (mSentStates.size() == 0) {
wait(timeoutMs);
diff --git a/tests/carservice_unit_test/src/com/android/car/VmsPublisherServiceTest.java b/tests/carservice_unit_test/src/com/android/car/VmsPublisherServiceTest.java
index 866bc8a..2bd09df 100644
--- a/tests/carservice_unit_test/src/com/android/car/VmsPublisherServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/VmsPublisherServiceTest.java
@@ -16,8 +16,6 @@
package com.android.car;
-import static com.google.common.truth.Truth.assertThat;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
@@ -45,25 +43,24 @@
import androidx.test.filters.SmallTest;
+import com.android.car.stats.CarStatsService;
+import com.android.car.stats.VmsClientLogger;
import com.android.car.vms.VmsBrokerService;
import com.android.car.vms.VmsClientManager;
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
-import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
-import java.io.ByteArrayOutputStream;
-import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
-import java.util.List;
@SmallTest
public class VmsPublisherServiceTest {
@@ -72,20 +69,22 @@
private static final VmsLayersOffering OFFERING = new VmsLayersOffering(Collections.emptySet(),
54321);
private static final VmsLayer LAYER = new VmsLayer(1, 2, 3);
- private static final VmsLayer LAYER2 = new VmsLayer(2, 2, 8);
- private static final VmsLayer LAYER3 = new VmsLayer(3, 2, 8);
- private static final VmsLayer LAYER4 = new VmsLayer(4, 2, 8);
private static final int PUBLISHER_ID = 54321;
private static final byte[] PAYLOAD = new byte[]{1, 2, 3, 4};
- private static final byte[] PAYLOAD2 = new byte[]{1, 2, 3, 4, 5, 6};
- private static final byte[] PAYLOAD3 = new byte[]{10, 12, 93, 4, 5, 6, 1, 1, 1};
+
+ private static final int PUBLISHER_UID = 10100;
+ private static final int SUBSCRIBER_UID = 10101;
+ private static final int SUBSCRIBER_UID2 = 10102;
+ private static final int NO_SUBSCRIBERS_UID = -1;
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock
private Context mContext;
@Mock
+ private CarStatsService mStatsService;
+ @Mock
private VmsBrokerService mBrokerService;
@Captor
private ArgumentCaptor<VmsBrokerService.PublisherListener> mProxyCaptor;
@@ -93,13 +92,18 @@
private VmsClientManager mClientManager;
@Mock
+ private VmsClientLogger mPublisherLog;
+ @Mock
+ private VmsClientLogger mSubscriberLog;
+ @Mock
+ private VmsClientLogger mSubscriberLog2;
+ @Mock
+ private VmsClientLogger mNoSubscribersLog;
+
+ @Mock
private IVmsSubscriberClient mSubscriberClient;
@Mock
private IVmsSubscriberClient mSubscriberClient2;
- @Mock
- private IVmsSubscriberClient mThrowingSubscriberClient;
- @Mock
- private IVmsSubscriberClient mThrowingSubscriberClient2;
private VmsPublisherService mPublisherService;
private MockPublisherClient mPublisherClient;
@@ -107,14 +111,27 @@
@Before
public void setUp() {
- mPublisherService = new VmsPublisherService(mContext, mBrokerService, mClientManager);
+ mPublisherService = new VmsPublisherService(mContext, mStatsService, mBrokerService,
+ mClientManager, () -> PUBLISHER_UID);
verify(mClientManager).setPublisherService(mPublisherService);
+ when(mClientManager.getSubscriberUid(mSubscriberClient)).thenReturn(SUBSCRIBER_UID);
+ when(mClientManager.getSubscriberUid(mSubscriberClient2)).thenReturn(SUBSCRIBER_UID2);
+
+ when(mStatsService.getVmsClientLogger(PUBLISHER_UID)).thenReturn(mPublisherLog);
+ when(mStatsService.getVmsClientLogger(SUBSCRIBER_UID)).thenReturn(mSubscriberLog);
+ when(mStatsService.getVmsClientLogger(SUBSCRIBER_UID2)).thenReturn(mSubscriberLog2);
+ when(mStatsService.getVmsClientLogger(NO_SUBSCRIBERS_UID)).thenReturn(mNoSubscribersLog);
+
mPublisherClient = new MockPublisherClient();
mPublisherClient2 = new MockPublisherClient();
when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER, PUBLISHER_ID))
.thenReturn(new HashSet<>(Arrays.asList(mSubscriberClient, mSubscriberClient2)));
+ }
+ @After
+ public void tearDown() {
+ verifyNoMoreInteractions(mPublisherLog, mSubscriberLog, mSubscriberLog2, mNoSubscribersLog);
}
@Test
@@ -190,6 +207,10 @@
PAYLOAD);
verify(mSubscriberClient).onVmsMessageReceived(LAYER, PAYLOAD);
verify(mSubscriberClient2).onVmsMessageReceived(LAYER, PAYLOAD);
+
+ verify(mPublisherLog).logPacketSent(LAYER, PAYLOAD.length);
+ verify(mSubscriberLog).logPacketReceived(LAYER, PAYLOAD.length);
+ verify(mSubscriberLog2).logPacketReceived(LAYER, PAYLOAD.length);
}
@Test
@@ -202,6 +223,19 @@
}
@Test
+ public void testPublish_NoSubscribers() throws Exception {
+ mPublisherService.onClientConnected("SomeClient", mPublisherClient);
+ when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER, PUBLISHER_ID))
+ .thenReturn(Collections.emptySet());
+
+ mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
+ PAYLOAD);
+
+ verify(mPublisherLog).logPacketSent(LAYER, PAYLOAD.length);
+ verify(mNoSubscribersLog).logPacketDropped(LAYER, PAYLOAD.length);
+ }
+
+ @Test
public void testPublish_ClientError() throws Exception {
mPublisherService.onClientConnected("SomeClient", mPublisherClient);
doThrow(new RemoteException()).when(mSubscriberClient).onVmsMessageReceived(LAYER, PAYLOAD);
@@ -210,6 +244,10 @@
PAYLOAD);
verify(mSubscriberClient).onVmsMessageReceived(LAYER, PAYLOAD);
verify(mSubscriberClient2).onVmsMessageReceived(LAYER, PAYLOAD);
+
+ verify(mPublisherLog).logPacketSent(LAYER, PAYLOAD.length);
+ verify(mSubscriberLog).logPacketDropped(LAYER, PAYLOAD.length);
+ verify(mSubscriberLog2).logPacketReceived(LAYER, PAYLOAD.length);
}
@Test(expected = SecurityException.class)
@@ -301,341 +339,6 @@
}
@Test
- public void testDump_getPacketCount() throws Exception {
- mPublisherService.onClientConnected("SomeClient", mPublisherClient);
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- PrintWriter printWriter = new PrintWriter(outputStream);
-
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
- PAYLOAD);
- mPublisherService.dump(printWriter);
-
- printWriter.flush();
- String dumpString = outputStream.toString();
- String expectedPacketCountString = String.format(VmsPublisherService.PACKET_COUNT_FORMAT,
- LAYER, 1L);
- String expectedPacketSizeString = String.format(VmsPublisherService.PACKET_SIZE_FORMAT,
- LAYER, PAYLOAD.length);
- assertThat(dumpString.contains(expectedPacketCountString)).isTrue();
- assertThat(dumpString.contains(expectedPacketSizeString)).isTrue();
- }
-
- @Test
- public void testDump_getPacketCounts() throws Exception {
- mPublisherService.onClientConnected("SomeClient", mPublisherClient);
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- PrintWriter printWriter = new PrintWriter(outputStream);
-
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
- PAYLOAD);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER2, PUBLISHER_ID,
- PAYLOAD);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
- PAYLOAD2);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
- PAYLOAD);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
- PAYLOAD);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
- PAYLOAD3);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
- PAYLOAD3);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER2, PUBLISHER_ID,
- PAYLOAD3);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
- PAYLOAD3);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
- PAYLOAD);
- mPublisherService.dump(printWriter);
-
- printWriter.flush();
- String dumpString = outputStream.toString();
-
- // LAYER called 6 times with PAYLOAD 2 times, PAYLOAD2 1 time, PAYLOAD3 3 times
- String expectedPacketCountString1 = String.format(VmsPublisherService.PACKET_COUNT_FORMAT,
- LAYER, 6L);
- String expectedPacketSizeString1 = String.format(VmsPublisherService.PACKET_SIZE_FORMAT,
- LAYER, 2 * PAYLOAD.length + PAYLOAD2.length + 3 * PAYLOAD3.length);
-
- // LAYER2 called 2 times with PAYLOAD 1 time, PAYLOAD2 0 time, PAYLOAD3 1 times
- String expectedPacketCountString2 = String.format(VmsPublisherService.PACKET_COUNT_FORMAT,
- LAYER2, 2L);
- String expectedPacketSizeString2 = String.format(VmsPublisherService.PACKET_SIZE_FORMAT,
- LAYER2, PAYLOAD.length + PAYLOAD3.length);
-
- // LAYER3 called 2 times with PAYLOAD 2 times, PAYLOAD2 0 time, PAYLOAD3 0 times
- String expectedPacketCountString3 = String.format(VmsPublisherService.PACKET_COUNT_FORMAT,
- LAYER3, 2L);
- String expectedPacketSizeString3 = String.format(VmsPublisherService.PACKET_SIZE_FORMAT,
- LAYER3, 2 * PAYLOAD.length);
-
- assertThat(dumpString.contains(expectedPacketCountString1)).isTrue();
- assertThat(dumpString.contains(expectedPacketSizeString1)).isTrue();
- assertThat(dumpString.contains(expectedPacketCountString2)).isTrue();
- assertThat(dumpString.contains(expectedPacketSizeString2)).isTrue();
- assertThat(dumpString.contains(expectedPacketCountString3)).isTrue();
- assertThat(dumpString.contains(expectedPacketSizeString3)).isTrue();
- }
-
- @Test
- public void testDumpNoListeners_getPacketFailureCount() throws Exception {
- mPublisherService.onClientConnected("SomeClient", mPublisherClient);
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- PrintWriter printWriter = new PrintWriter(outputStream);
-
- // Layer 2 has no listeners and should therefore result in a packet failure to be recorded.
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER2, PUBLISHER_ID,
- PAYLOAD);
- mPublisherService.dump(printWriter);
-
- printWriter.flush();
- String dumpString = outputStream.toString();
-
- String expectedPacketFailureString = String.format(
- VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT,
- LAYER2, "SomeClient", "", 1L);
- String expectedPacketFailureSizeString = String.format(
- VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT,
- LAYER2, "SomeClient", "", PAYLOAD.length);
-
- assertThat(dumpString.contains(expectedPacketFailureString)).isTrue();
- assertThat(dumpString.contains(expectedPacketFailureSizeString)).isTrue();
- }
-
- @Test
- public void testDumpNoListeners_getPacketFailureCounts() throws Exception {
- // LAYER2 and LAYER3 both have no listeners
- when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER2, PUBLISHER_ID))
- .thenReturn(new HashSet<>());
- when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER3, PUBLISHER_ID))
- .thenReturn(new HashSet<>());
-
- mPublisherService.onClientConnected("SomeClient", mPublisherClient);
- mPublisherService.onClientConnected("SomeClient2", mPublisherClient2);
-
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- PrintWriter printWriter = new PrintWriter(outputStream);
-
- // Layer 2 has no listeners and should therefore result in a packet failure to be recorded.
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER2, PUBLISHER_ID,
- PAYLOAD);
- mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER3, PUBLISHER_ID,
- PAYLOAD);
-
- mPublisherService.dump(printWriter);
-
- printWriter.flush();
- String dumpString = outputStream.toString();
-
- String expectedPacketFailureString = String.format(
- VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT,
- LAYER2, "SomeClient", "", 1L);
- String expectedPacketFailureString2 = String.format(
- VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT,
- LAYER3, "SomeClient2", "", 1L);
- String expectedPacketFailureSizeString = String.format(
- VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT,
- LAYER2, "SomeClient", "", PAYLOAD.length);
- String expectedPacketFailureSizeString2 = String.format(
- VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT,
- LAYER3, "SomeClient2", "", PAYLOAD.length);
-
- assertThat(dumpString.contains(expectedPacketFailureString)).isTrue();
- assertThat(dumpString.contains(expectedPacketFailureSizeString)).isTrue();
- assertThat(dumpString.contains(expectedPacketFailureString2)).isTrue();
- assertThat(dumpString.contains(expectedPacketFailureSizeString2)).isTrue();
- }
-
- @Test
- public void testDumpRemoteException_getPacketFailureCount() throws Exception {
- // The listener on LAYER3 will throw on LAYER3 and PAYLOAD
- Mockito.doThrow(new RemoteException()).when(mThrowingSubscriberClient).onVmsMessageReceived(
- LAYER3, PAYLOAD);
- when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER3, PUBLISHER_ID))
- .thenReturn(new HashSet<>(Arrays.asList(mThrowingSubscriberClient)));
- when(mClientManager.getPackageName(mThrowingSubscriberClient)).thenReturn("Thrower");
-
- mPublisherService.onClientConnected("SomeClient", mPublisherClient);
-
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- PrintWriter printWriter = new PrintWriter(outputStream);
-
- // Layer 2 has no listeners and should therefore result in a packet failure to be recorded.
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
- PAYLOAD);
-
- mPublisherService.dump(printWriter);
-
- printWriter.flush();
- String dumpString = outputStream.toString();
-
- String expectedPacketFailureString = String.format(
- VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT,
- LAYER3, "SomeClient", "Thrower", 1L);
- String expectedPacketFailureSizeString = String.format(
- VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT,
- LAYER3, "SomeClient", "Thrower", PAYLOAD.length);
-
- assertThat(dumpString.contains(expectedPacketFailureString)).isTrue();
- assertThat(dumpString.contains(expectedPacketFailureSizeString)).isTrue();
- }
-
- @Test
- public void testDumpRemoteException_getPacketFailureCounts() throws Exception {
- // The listeners will throw on LAYER3 or LAYER4 and PAYLOAD
- Mockito.doThrow(new RemoteException()).when(mThrowingSubscriberClient).onVmsMessageReceived(
- LAYER3, PAYLOAD);
- Mockito.doThrow(new RemoteException()).when(mThrowingSubscriberClient).onVmsMessageReceived(
- LAYER4, PAYLOAD);
- Mockito.doThrow(new RemoteException()).when(
- mThrowingSubscriberClient2).onVmsMessageReceived(LAYER3, PAYLOAD);
- Mockito.doThrow(new RemoteException()).when(
- mThrowingSubscriberClient2).onVmsMessageReceived(LAYER4, PAYLOAD);
-
- when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER3, PUBLISHER_ID))
- .thenReturn(new HashSet<>(
- Arrays.asList(mThrowingSubscriberClient, mThrowingSubscriberClient2)));
- when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER4, PUBLISHER_ID))
- .thenReturn(new HashSet<>(
- Arrays.asList(mThrowingSubscriberClient, mThrowingSubscriberClient2)));
-
- when(mClientManager.getPackageName(mThrowingSubscriberClient)).thenReturn("Thrower");
- when(mClientManager.getPackageName(mThrowingSubscriberClient2)).thenReturn("Thrower2");
-
- mPublisherService.onClientConnected("SomeClient", mPublisherClient);
- mPublisherService.onClientConnected("SomeClient2", mPublisherClient2);
-
- // Layer 2 has no listeners and should therefore result in a packet failure to be recorded.
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
- PAYLOAD);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
- PAYLOAD);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER4, PUBLISHER_ID,
- PAYLOAD);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER4, PUBLISHER_ID,
- PAYLOAD);
-
- mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER3, PUBLISHER_ID,
- PAYLOAD);
- mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER3, PUBLISHER_ID,
- PAYLOAD);
- mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER4, PUBLISHER_ID,
- PAYLOAD);
- mPublisherClient2.mPublisherService.publish(mPublisherClient2.mToken, LAYER4, PUBLISHER_ID,
- PAYLOAD);
-
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- PrintWriter printWriter = new PrintWriter(outputStream);
- mPublisherService.dump(printWriter);
-
- printWriter.flush();
- String dumpString = outputStream.toString();
-
- List<String> expectedStrings = Arrays.asList(
- String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3, "SomeClient",
- "Thrower", 2L),
- String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3, "SomeClient",
- "Thrower2", 2L),
- String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4, "SomeClient",
- "Thrower", 2L),
- String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4, "SomeClient",
- "Thrower2", 2L),
- String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3,
- "SomeClient2",
- "Thrower", 2L),
- String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3,
- "SomeClient2",
- "Thrower2", 2L),
- String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4,
- "SomeClient2",
- "Thrower", 2L),
- String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4,
- "SomeClient2",
- "Thrower2", 2L),
-
- String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient",
- "Thrower", 2 * PAYLOAD.length),
- String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient",
- "Thrower2", 2 * PAYLOAD.length),
- String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient",
- "Thrower", 2 * PAYLOAD.length),
- String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient",
- "Thrower2", 2 * PAYLOAD.length),
- String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient2",
- "Thrower", 2 * PAYLOAD.length),
- String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient2",
- "Thrower2", 2 * PAYLOAD.length),
- String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient2",
- "Thrower", 2 * PAYLOAD.length),
- String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient2",
- "Thrower2", 2 * PAYLOAD.length));
-
- for (String expected : expectedStrings) {
- assertThat(dumpString.contains(expected)).isTrue();
- }
- }
-
- @Test
- public void testDump_getAllMetrics() throws Exception {
-
- // LAYER3 has no subscribers
- when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER3, PUBLISHER_ID))
- .thenReturn(new HashSet<>(Arrays.asList()));
-
- // LAYER4 has a subscriber that will always throw
- Mockito.doThrow(new RemoteException()).when(mThrowingSubscriberClient).onVmsMessageReceived(
- LAYER4, PAYLOAD);
-
- when(mBrokerService.getSubscribersForLayerFromPublisher(LAYER4, PUBLISHER_ID))
- .thenReturn(new HashSet<>(
- Arrays.asList(mThrowingSubscriberClient)));
-
- when(mClientManager.getPackageName(mThrowingSubscriberClient)).thenReturn("Thrower");
-
- mPublisherService.onClientConnected("SomeClient", mPublisherClient);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
- PAYLOAD);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER, PUBLISHER_ID,
- PAYLOAD2);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER3, PUBLISHER_ID,
- PAYLOAD3);
- mPublisherClient.mPublisherService.publish(mPublisherClient.mToken, LAYER4, PUBLISHER_ID,
- PAYLOAD);
-
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- PrintWriter printWriter = new PrintWriter(outputStream);
- mPublisherService.dump(printWriter);
-
- printWriter.flush();
- String dumpString = outputStream.toString();
-
- List<String> expectedStrings = Arrays.asList(
- String.format(VmsPublisherService.PACKET_COUNT_FORMAT, LAYER, 2),
- String.format(VmsPublisherService.PACKET_COUNT_FORMAT, LAYER3, 1),
- String.format(VmsPublisherService.PACKET_COUNT_FORMAT, LAYER4, 1),
- String.format(VmsPublisherService.PACKET_SIZE_FORMAT, LAYER,
- PAYLOAD.length + PAYLOAD2.length),
- String.format(VmsPublisherService.PACKET_SIZE_FORMAT, LAYER3, PAYLOAD3.length),
- String.format(VmsPublisherService.PACKET_SIZE_FORMAT, LAYER4, PAYLOAD.length),
- String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER3, "SomeClient",
- "",
- 1),
- String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER3, "SomeClient",
- "",
- PAYLOAD3.length),
- String.format(VmsPublisherService.PACKET_FAILURE_COUNT_FORMAT, LAYER4, "SomeClient",
- "Thrower", 1),
- String.format(VmsPublisherService.PACKET_FAILURE_SIZE_FORMAT, LAYER4, "SomeClient",
- "Thrower", PAYLOAD.length)
- );
-
- for (String expected : expectedStrings) {
- assertThat(dumpString.contains(expected)).isTrue();
- }
- }
-
-
- @Test
public void testRelease() {
mPublisherService.release();
}
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/PropertyHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/PropertyHalServiceTest.java
new file mode 100644
index 0000000..6f7fe18
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/hal/PropertyHalServiceTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.android.car.hal;
+
+import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidJUnit4.class)
+public class PropertyHalServiceTest {
+ @Rule
+ public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ @Mock
+ private VehicleHal mVehicleHal;
+
+ private PropertyHalService mPropertyHalService;
+ private static final int[] UNITS_PROPERTY_ID = {
+ VehicleProperty.DISTANCE_DISPLAY_UNITS,
+ VehicleProperty.FUEL_CONSUMPTION_UNITS_DISTANCE_OVER_VOLUME,
+ VehicleProperty.FUEL_VOLUME_DISPLAY_UNITS,
+ VehicleProperty.TIRE_PRESSURE_DISPLAY_UNITS,
+ VehicleProperty.EV_BATTERY_DISPLAY_UNITS,
+ VehicleProperty.VEHICLE_SPEED_DISPLAY_UNITS};
+
+ @Before
+ public void setUp() {
+ mPropertyHalService = new PropertyHalService(mVehicleHal);
+ mPropertyHalService.init();
+ }
+
+ @After
+ public void tearDown() {
+ mPropertyHalService.release();
+ mPropertyHalService = null;
+ }
+
+ @Test
+ public void checkDisplayUnitsProperty() {
+ for (int propId : UNITS_PROPERTY_ID) {
+ Assert.assertTrue(mPropertyHalService.isDisplayUnitsProperty(propId));
+ }
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/VmsHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/VmsHalServiceTest.java
index 7571867..093ab9b 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/VmsHalServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/VmsHalServiceTest.java
@@ -17,7 +17,9 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
@@ -44,8 +46,6 @@
import android.os.Binder;
import android.os.IBinder;
-import androidx.test.filters.RequiresDevice;
-
import com.android.car.R;
import com.android.car.test.utils.TemporaryFile;
import com.android.car.vms.VmsClientManager;
@@ -103,8 +103,13 @@
@Before
public void setUp() throws Exception {
+ initHalService(true);
+ }
+
+ private void initHalService(boolean propagatePropertyException) throws Exception {
when(mContext.getResources()).thenReturn(mResources);
- mHalService = new VmsHalService(mContext, mVehicleHal, () -> (long) CORE_ID);
+ mHalService = new VmsHalService(mContext, mVehicleHal, () -> (long) CORE_ID,
+ propagatePropertyException);
mHalService.setClientManager(mClientManager);
mHalService.setVmsSubscriberService(mSubscriberService);
@@ -158,6 +163,8 @@
0, // Sequence number
0)); // # of associated layers
+
+ waitForHandlerCompletion();
initOrder.verifyNoMoreInteractions();
reset(mClientManager, mSubscriberService, mVehicleHal);
}
@@ -165,7 +172,7 @@
@Test
public void testCoreId_IntegerOverflow() throws Exception {
mHalService = new VmsHalService(mContext, mVehicleHal,
- () -> (long) Integer.MAX_VALUE + CORE_ID);
+ () -> (long) Integer.MAX_VALUE + CORE_ID, true);
VehiclePropConfig propConfig = new VehiclePropConfig();
propConfig.prop = VehicleProperty.VEHICLE_MAP_SERVICE;
@@ -587,7 +594,6 @@
* </ul>
*/
@Test
- @RequiresDevice
public void testHandleStartSessionEvent() throws Exception {
when(mSubscriberService.getAvailableLayers()).thenReturn(
new VmsAvailableLayers(Collections.emptySet(), 5));
@@ -605,13 +611,18 @@
);
sendHalMessage(request);
+
InOrder inOrder = Mockito.inOrder(mClientManager, mVehicleHal);
inOrder.verify(mClientManager).onHalDisconnected();
inOrder.verify(mVehicleHal).set(response);
+ inOrder.verify(mClientManager).onHalConnected(mPublisherClient, mSubscriberClient);
+
+ waitForHandlerCompletion();
inOrder.verify(mVehicleHal).set(createHalMessage(
VmsMessageType.AVAILABILITY_CHANGE, // Message type
5, // Sequence number
0)); // # of associated layers
+
}
/**
@@ -962,6 +973,33 @@
}
@Test
+ public void testPropertySetExceptionNotPropagated_CoreStartSession() throws Exception {
+ doThrow(new RuntimeException()).when(mVehicleHal).set(any());
+ initHalService(false);
+
+ mHalService.init();
+ waitForHandlerCompletion();
+ }
+
+ @Test
+ public void testPropertySetExceptionNotPropagated_ClientStartSession() throws Exception {
+ initHalService(false);
+
+ when(mSubscriberService.getAvailableLayers()).thenReturn(
+ new VmsAvailableLayers(Collections.emptySet(), 0));
+ doThrow(new RuntimeException()).when(mVehicleHal).set(any());
+
+ VehiclePropValue request = createHalMessage(
+ VmsMessageType.START_SESSION, // Message type
+ -1, // Core ID (unknown)
+ CLIENT_ID // Client ID
+ );
+
+ sendHalMessage(request);
+ waitForHandlerCompletion();
+ }
+
+ @Test
public void testDumpMetrics_DefaultConfig() {
mHalService.dumpMetrics(new FileDescriptor());
verifyZeroInteractions(mVehicleHal);
diff --git a/tests/carservice_unit_test/src/com/android/car/stats/CarStatsServiceTest.java b/tests/carservice_unit_test/src/com/android/car/stats/CarStatsServiceTest.java
new file mode 100644
index 0000000..9bdcaa6
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/stats/CarStatsServiceTest.java
@@ -0,0 +1,357 @@
+/*
+ * 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.android.car.stats;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import android.car.vms.VmsLayer;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.car.stats.VmsClientLogger.ConnectionState;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+@SmallTest
+@RunWith(JUnit4.class)
+public class CarStatsServiceTest {
+ private static final int CLIENT_UID = 10101;
+ private static final int CLIENT_UID2 = 10102;
+ private static final String CLIENT_PACKAGE = "test.package";
+ private static final String CLIENT_PACKAGE2 = "test.package2";
+ private static final VmsLayer LAYER = new VmsLayer(1, 2, 3);
+ private static final VmsLayer LAYER2 = new VmsLayer(2, 3, 4);
+ private static final VmsLayer LAYER3 = new VmsLayer(3, 4, 5);
+
+ @Rule
+ public MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Mock
+ private Context mContext;
+ @Mock
+ private PackageManager mPackageManager;
+
+ private CarStatsService mCarStatsService;
+ private StringWriter mDumpsysOutput;
+ private PrintWriter mDumpsysWriter;
+
+ @Before
+ public void setUp() {
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ when(mPackageManager.getNameForUid(CLIENT_UID)).thenReturn(CLIENT_PACKAGE);
+ when(mPackageManager.getNameForUid(CLIENT_UID2)).thenReturn(CLIENT_PACKAGE2);
+
+ mCarStatsService = new CarStatsService(mContext);
+ mDumpsysOutput = new StringWriter();
+ mDumpsysWriter = new PrintWriter(mDumpsysOutput);
+ }
+
+ @Test
+ public void testEmptyStats() {
+ mCarStatsService.dump(null, mDumpsysWriter, new String[0]);
+ assertEquals(
+ "uid,packageName,attempts,connected,disconnected,terminated,errors\n"
+ + "\nuid,layerType,layerChannel,layerVersion,"
+ + "txBytes,txPackets,rxBytes,rxPackets,droppedBytes,droppedPackets\n",
+ mDumpsysOutput.toString());
+ }
+
+ @Test
+ public void testLogConnectionState_Connecting() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logConnectionState(ConnectionState.CONNECTING);
+ validateConnectionStats("10101,test.package,1,0,0,0,0");
+ }
+
+ @Test
+ public void testLogConnectionState_Connected() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logConnectionState(ConnectionState.CONNECTED);
+ validateConnectionStats("10101,test.package,0,1,0,0,0");
+ }
+
+ @Test
+ public void testLogConnectionState_Disconnected() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logConnectionState(ConnectionState.DISCONNECTED);
+ validateConnectionStats("10101,test.package,0,0,1,0,0");
+ }
+
+ @Test
+ public void testLogConnectionState_Terminated() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logConnectionState(ConnectionState.TERMINATED);
+ validateConnectionStats("10101,test.package,0,0,0,1,0");
+ }
+
+ @Test
+ public void testLogConnectionState_ConnectionError() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logConnectionState(ConnectionState.CONNECTION_ERROR);
+ validateConnectionStats("10101,test.package,0,0,0,0,1");
+ }
+
+ @Test
+ public void testLogConnectionState_UnknownUID() {
+ mCarStatsService.getVmsClientLogger(-1)
+ .logConnectionState(ConnectionState.CONNECTING);
+ testEmptyStats();
+ }
+
+ @Test
+ public void testLogConnectionState_MultipleClients_MultipleStates() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logConnectionState(ConnectionState.CONNECTING);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logConnectionState(ConnectionState.CONNECTED);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logConnectionState(ConnectionState.DISCONNECTED);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logConnectionState(ConnectionState.CONNECTED);
+
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logConnectionState(ConnectionState.CONNECTING);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logConnectionState(ConnectionState.CONNECTED);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logConnectionState(ConnectionState.TERMINATED);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logConnectionState(ConnectionState.CONNECTING);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logConnectionState(ConnectionState.CONNECTION_ERROR);
+ validateConnectionStats(
+ "10101,test.package,1,2,1,0,0\n"
+ + "10102,test.package2,2,1,0,1,1");
+ }
+
+ @Test
+ public void testLogPacketSent() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketSent(LAYER, 5);
+ validateClientStats("10101,1,2,3,5,1,0,0,0,0");
+ }
+
+ @Test
+ public void testLogPacketSent_MultiplePackets() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketSent(LAYER, 3);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketSent(LAYER, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketSent(LAYER, 1);
+
+ validateClientStats("10101,1,2,3,6,3,0,0,0,0");
+ }
+
+ @Test
+ public void testLogPacketSent_MultipleLayers() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketSent(LAYER, 3);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketSent(LAYER2, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketSent(LAYER3, 1);
+
+ validateClientStats(
+ "10101,1,2,3,3,1,0,0,0,0\n"
+ + "10101,2,3,4,2,1,0,0,0,0\n"
+ + "10101,3,4,5,1,1,0,0,0,0");
+ }
+
+ @Test
+ public void testLogPacketSent_MultipleClients() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketSent(LAYER, 3);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logPacketSent(LAYER, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logPacketSent(LAYER2, 1);
+
+ validateDumpsys(
+ "10101,test.package,0,0,0,0,0\n"
+ + "10102,test.package2,0,0,0,0,0\n",
+ "10101,1,2,3,3,1,0,0,0,0\n"
+ + "10102,1,2,3,2,1,0,0,0,0\n"
+ + "10102,2,3,4,1,1,0,0,0,0\n");
+ }
+
+ @Test
+ public void testLogPacketReceived() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketReceived(LAYER, 5);
+ validateClientStats("10101,1,2,3,0,0,5,1,0,0");
+ }
+
+ @Test
+ public void testLogPacketReceived_MultiplePackets() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketReceived(LAYER, 3);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketReceived(LAYER, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketReceived(LAYER, 1);
+
+ validateClientStats("10101,1,2,3,0,0,6,3,0,0");
+ }
+
+ @Test
+ public void testLogPacketReceived_MultipleLayers() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketReceived(LAYER, 3);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketReceived(LAYER2, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketReceived(LAYER3, 1);
+
+ validateClientStats(
+ "10101,1,2,3,0,0,3,1,0,0\n"
+ + "10101,2,3,4,0,0,2,1,0,0\n"
+ + "10101,3,4,5,0,0,1,1,0,0");
+ }
+
+ @Test
+ public void testLogPacketReceived_MultipleClients() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketReceived(LAYER, 3);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logPacketReceived(LAYER, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logPacketReceived(LAYER2, 1);
+
+ validateDumpsys(
+ "10101,test.package,0,0,0,0,0\n"
+ + "10102,test.package2,0,0,0,0,0\n",
+ "10101,1,2,3,0,0,3,1,0,0\n"
+ + "10102,1,2,3,0,0,2,1,0,0\n"
+ + "10102,2,3,4,0,0,1,1,0,0\n");
+ }
+
+ @Test
+ public void testLogPacketDropped() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketDropped(LAYER, 5);
+ validateClientStats("10101,1,2,3,0,0,0,0,5,1");
+ }
+
+ @Test
+ public void testLogPacketDropped_MultiplePackets() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketDropped(LAYER, 3);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketDropped(LAYER, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketDropped(LAYER, 1);
+
+ validateClientStats("10101,1,2,3,0,0,0,0,6,3");
+ }
+
+ @Test
+ public void testLogPacketDropped_MultipleLayers() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketDropped(LAYER, 3);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketDropped(LAYER2, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketDropped(LAYER3, 1);
+
+ validateClientStats(
+ "10101,1,2,3,0,0,0,0,3,1\n"
+ + "10101,2,3,4,0,0,0,0,2,1\n"
+ + "10101,3,4,5,0,0,0,0,1,1");
+ }
+
+ @Test
+ public void testLogPacketDropped_MultipleClients() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketDropped(LAYER, 3);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logPacketDropped(LAYER, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logPacketDropped(LAYER2, 1);
+
+ validateDumpsys(
+ "10101,test.package,0,0,0,0,0\n"
+ + "10102,test.package2,0,0,0,0,0\n",
+ "10101,1,2,3,0,0,0,0,3,1\n"
+ + "10102,1,2,3,0,0,0,0,2,1\n"
+ + "10102,2,3,4,0,0,0,0,1,1\n");
+ }
+
+ @Test
+ public void testLogPackets_MultipleClients_MultipleLayers_MultipleOperations() {
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketSent(LAYER, 3);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketReceived(LAYER, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID)
+ .logPacketDropped(LAYER, 1);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logPacketReceived(LAYER, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logPacketReceived(LAYER, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logPacketReceived(LAYER, 2);
+ mCarStatsService.getVmsClientLogger(CLIENT_UID2)
+ .logPacketSent(LAYER2, 2);
+ mCarStatsService.getVmsClientLogger(-1)
+ .logPacketDropped(LAYER2, 12);
+
+
+ validateDumpsys(
+ "10101,test.package,0,0,0,0,0\n"
+ + "10102,test.package2,0,0,0,0,0\n",
+ "-1,2,3,4,0,0,0,0,12,1\n"
+ + "10101,1,2,3,3,1,2,1,1,1\n"
+ + "10102,1,2,3,0,0,6,3,0,0\n"
+ + "10102,2,3,4,2,1,0,0,0,0\n");
+ }
+
+
+ private void validateConnectionStats(String vmsConnectionStats) {
+ validateDumpsys(vmsConnectionStats + "\n", "");
+ }
+
+ private void validateClientStats(String vmsClientStats) {
+ validateDumpsys(
+ "10101,test.package,0,0,0,0,0\n",
+ vmsClientStats + "\n");
+ }
+
+ private void validateDumpsys(String vmsConnectionStats, String vmsClientStats) {
+ mCarStatsService.dump(null, mDumpsysWriter, new String[0]);
+ assertEquals(
+ "uid,packageName,attempts,connected,disconnected,terminated,errors\n"
+ + vmsConnectionStats
+ + "\n"
+ + "uid,layerType,layerChannel,layerVersion,"
+ + "txBytes,txPackets,rxBytes,rxPackets,droppedBytes,droppedPackets\n"
+ + vmsClientStats,
+ mDumpsysOutput.toString());
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java b/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
index b90dac4..e1c9d9a 100644
--- a/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/vms/VmsClientManagerTest.java
@@ -25,7 +25,6 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atMost;
@@ -38,7 +37,6 @@
import static org.mockito.Mockito.when;
import android.car.Car;
-import android.car.userlib.CarUserManagerHelper;
import android.car.vms.IVmsPublisherClient;
import android.car.vms.IVmsPublisherService;
import android.car.vms.IVmsSubscriberClient;
@@ -48,14 +46,15 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
+import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
@@ -63,6 +62,9 @@
import com.android.car.VmsPublisherService;
import com.android.car.hal.VmsHalService;
+import com.android.car.stats.CarStatsService;
+import com.android.car.stats.VmsClientLogger;
+import com.android.car.stats.VmsClientLogger.ConnectionState;
import com.android.car.user.CarUserService;
import org.junit.After;
@@ -90,13 +92,18 @@
private static final String USER_CLIENT_NAME =
"com.google.android.apps.vms.test/com.google.android.apps.vms.test.VmsUserClient U=10";
private static final int USER_ID_U11 = 11;
- private static final String USER_CLIENT_NAME_U11 =
- "com.google.android.apps.vms.test/com.google.android.apps.vms.test.VmsUserClient U=11";
private static final String TEST_PACKAGE = "test.package1";
private static final String HAL_CLIENT_NAME = "HalClient";
private static final String UNKNOWN_PACKAGE = "UnknownPackage";
+ private static final int TEST_APP_ID = 12345;
+ private static final int TEST_SYSTEM_UID = 12345;
+ private static final int TEST_USER_UID = 1012345;
+ private static final int TEST_USER_UID_U11 = 1112345;
+
+ private static final long MILLIS_BEFORE_REBIND = 100;
+
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock
@@ -105,13 +112,12 @@
private PackageManager mPackageManager;
@Mock
private Resources mResources;
-
+ @Mock
+ private CarStatsService mStatsService;
@Mock
private UserManager mUserManager;
@Mock
private CarUserService mUserService;
- @Mock
- private CarUserManagerHelper mUserManagerHelper;
@Mock
private VmsBrokerService mBrokerService;
@@ -120,6 +126,12 @@
private VmsHalService mHal;
@Mock
+ private Handler mHandler;
+
+ @Captor
+ private ArgumentCaptor<Runnable> mRebindCaptor;
+
+ @Mock
private VmsPublisherService mPublisherService;
@Mock
@@ -138,21 +150,48 @@
@Captor
private ArgumentCaptor<ServiceConnection> mConnectionCaptor;
+ @Mock
+ private VmsClientLogger mSystemClientLog;
+ @Mock
+ private VmsClientLogger mUserClientLog;
+ @Mock
+ private VmsClientLogger mUserClientLog2;
+ @Mock
+ private VmsClientLogger mHalClientLog;
+
private VmsClientManager mClientManager;
private int mForegroundUserId;
private int mCallingAppUid;
+ private ServiceInfo mSystemServiceInfo;
+ private ServiceInfo mUserServiceInfo;
+
@Before
public void setUp() throws Exception {
resetContext();
- ServiceInfo serviceInfo = new ServiceInfo();
- serviceInfo.permission = Car.PERMISSION_BIND_VMS_CLIENT;
- when(mPackageManager.getServiceInfo(any(), anyInt())).thenReturn(serviceInfo);
+ mSystemServiceInfo = new ServiceInfo();
+ mSystemServiceInfo.permission = Car.PERMISSION_BIND_VMS_CLIENT;
+ mSystemServiceInfo.applicationInfo = new ApplicationInfo();
+ mSystemServiceInfo.applicationInfo.uid = TEST_APP_ID;
+ when(mPackageManager.getServiceInfo(eq(SYSTEM_CLIENT_COMPONENT), anyInt()))
+ .thenReturn(mSystemServiceInfo);
+ when(mStatsService.getVmsClientLogger(TEST_SYSTEM_UID)).thenReturn(mSystemClientLog);
+
+ mUserServiceInfo = new ServiceInfo();
+ mUserServiceInfo.permission = Car.PERMISSION_BIND_VMS_CLIENT;
+ mUserServiceInfo.applicationInfo = new ApplicationInfo();
+ mUserServiceInfo.applicationInfo.uid = TEST_APP_ID;
+ when(mPackageManager.getServiceInfo(eq(USER_CLIENT_COMPONENT), anyInt()))
+ .thenReturn(mUserServiceInfo);
+ when(mStatsService.getVmsClientLogger(TEST_USER_UID)).thenReturn(mUserClientLog);
+ when(mStatsService.getVmsClientLogger(TEST_USER_UID_U11)).thenReturn(mUserClientLog2);
+
+ when(mStatsService.getVmsClientLogger(Process.myUid())).thenReturn(mHalClientLog);
when(mResources.getInteger(
com.android.car.R.integer.millisecondsBeforeRebindToVmsPublisher)).thenReturn(
- 5);
+ (int) MILLIS_BEFORE_REBIND);
when(mResources.getStringArray(
com.android.car.R.array.vmsPublisherSystemClients)).thenReturn(
new String[]{ SYSTEM_CLIENT });
@@ -161,17 +200,15 @@
new String[]{ USER_CLIENT });
when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager);
- when(mUserManagerHelper.getCurrentForegroundUserId())
- .thenAnswer(invocation -> mForegroundUserId);
- mForegroundUserId = USER_ID;
- mCallingAppUid = UserHandle.getUid(USER_ID, 0);
-
- mClientManager = new VmsClientManager(mContext, mBrokerService, mUserService,
- mUserManagerHelper, mHal, () -> mCallingAppUid);
+ mClientManager = new VmsClientManager(mContext, mStatsService, mUserService,
+ mBrokerService, mHal, mHandler, () -> mCallingAppUid);
verify(mHal).setClientManager(mClientManager);
mClientManager.setPublisherService(mPublisherService);
+ notifyUserSwitched(USER_ID, false);
+ mCallingAppUid = UserHandle.getUid(USER_ID, 0);
+
when(mSubscriberClient1.asBinder()).thenReturn(mSubscriberBinder1);
when(mSubscriberClient2.asBinder()).thenReturn(mSubscriberBinder2);
@@ -179,12 +216,12 @@
}
@After
- public void tearDown() throws Exception {
- Thread.sleep(10); // Time to allow for delayed rebinds to settle
+ public void tearDown() {
verify(mContext, atLeast(0)).getSystemService(eq(Context.USER_SERVICE));
verify(mContext, atLeast(0)).getResources();
verify(mContext, atLeast(0)).getPackageManager();
- verifyNoMoreInteractions(mContext, mBrokerService, mHal, mPublisherService);
+ verifyNoMoreInteractions(mContext, mBrokerService, mHal, mPublisherService, mHandler);
+ verifyNoMoreInteractions(mSystemClientLog, mUserClientLog, mUserClientLog2, mHalClientLog);
}
@Test
@@ -193,15 +230,8 @@
// Verify registration of system user unlock listener
verify(mUserService).runOnUser0Unlock(mClientManager.mSystemUserUnlockedListener);
-
- // Verify registration of user switch receiver
- ArgumentCaptor<IntentFilter> userFilterCaptor = ArgumentCaptor.forClass(IntentFilter.class);
- verify(mContext).registerReceiverAsUser(eq(mClientManager.mUserSwitchReceiver),
- eq(UserHandle.ALL), userFilterCaptor.capture(), isNull(), isNull());
- IntentFilter userEventFilter = userFilterCaptor.getValue();
- assertEquals(2, userEventFilter.countActions());
- assertTrue(userEventFilter.hasAction(Intent.ACTION_USER_SWITCHED));
- assertTrue(userEventFilter.hasAction(Intent.ACTION_USER_UNLOCKED));
+ // Verify user callback is added
+ verify(mUserService).addUserCallback(eq(mClientManager.mUserCallback));
}
@Test
@@ -209,7 +239,7 @@
mClientManager.release();
// Verify user switch receiver is unregistered
- verify(mContext).unregisterReceiver(mClientManager.mUserSwitchReceiver);
+ verify(mUserService).removeUserCallback(mClientManager.mUserCallback);
}
@Test
@@ -233,14 +263,12 @@
@Test
public void testSystemUserUnlocked_WrongPermission() throws Exception {
- ServiceInfo serviceInfo = new ServiceInfo();
- serviceInfo.permission = Car.PERMISSION_VMS_PUBLISHER;
- when(mPackageManager.getServiceInfo(eq(SYSTEM_CLIENT_COMPONENT), anyInt()))
- .thenReturn(serviceInfo);
+ mSystemServiceInfo.permission = Car.PERMISSION_VMS_PUBLISHER;
notifySystemUserUnlocked();
// Process will not be bound
verifySystemBind(0);
+ verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
}
@Test
@@ -251,6 +279,7 @@
// Failure state will trigger another attempt on event
verifySystemBind(2);
+ verify(mSystemClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
}
@Test
@@ -262,6 +291,7 @@
// Failure state will trigger another attempt on event
verifySystemBind(2);
+ verify(mSystemClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
}
@Test
@@ -282,6 +312,14 @@
}
@Test
+ public void testUserUnlocked_OtherUserUnlocked() {
+ notifyUserUnlocked(USER_ID_U11, true);
+
+ // Process will not be bound
+ verifyUserBind(0);
+ }
+
+ @Test
public void testUserUnlocked_ClientNotFound() throws Exception {
when(mPackageManager.getServiceInfo(eq(USER_CLIENT_COMPONENT), anyInt()))
.thenThrow(new PackageManager.NameNotFoundException());
@@ -293,14 +331,12 @@
@Test
public void testUserUnlocked_WrongPermission() throws Exception {
- ServiceInfo serviceInfo = new ServiceInfo();
- serviceInfo.permission = Car.PERMISSION_VMS_PUBLISHER;
- when(mPackageManager.getServiceInfo(eq(USER_CLIENT_COMPONENT), anyInt()))
- .thenReturn(serviceInfo);
+ mUserServiceInfo.permission = Car.PERMISSION_VMS_PUBLISHER;
notifyUserUnlocked(USER_ID, true);
// Process will not be bound
verifyUserBind(0);
+ verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
}
@Test
@@ -312,6 +348,7 @@
// Failure state will trigger another attempt
verifyUserBind(2);
+ verify(mUserClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
}
@Test
@@ -323,6 +360,7 @@
// Failure state will trigger another attempt
verifyUserBind(2);
+ verify(mUserClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
}
@Test
@@ -334,6 +372,7 @@
// Failure state will trigger another attempt
verifyUserBind(2);
+ verify(mUserClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
}
@Test
@@ -342,6 +381,7 @@
.thenReturn(false);
notifySystemUserUnlocked();
verifySystemBind(1);
+ verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
resetContext();
when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), eq(UserHandle.SYSTEM)))
@@ -357,6 +397,7 @@
.thenReturn(false);
notifySystemUserUnlocked();
verifySystemBind(1);
+ verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
resetContext();
when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), eq(UserHandle.SYSTEM)))
@@ -365,6 +406,7 @@
notifyUserUnlocked(USER_ID, true);
verifySystemBind(2); // Failure state will trigger another attempt
+ verify(mSystemClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
verifyUserBind(1);
}
@@ -374,6 +416,7 @@
.thenThrow(new SecurityException());
notifySystemUserUnlocked();
verifySystemBind(1);
+ verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTION_ERROR);
resetContext();
when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), eq(UserHandle.SYSTEM)))
@@ -382,6 +425,7 @@
notifyUserUnlocked(USER_ID, true);
verifySystemBind(2); // Failure state will trigger another attempt
+ verify(mSystemClientLog, times(2)).logConnectionState(ConnectionState.CONNECTION_ERROR);
verifyUserBind(1);
}
@@ -424,6 +468,7 @@
public void testOnSystemServiceConnected() {
IBinder binder = bindSystemClient();
verifyOnClientConnected(SYSTEM_CLIENT_NAME, binder);
+ verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
}
private IBinder bindSystemClient() {
@@ -441,6 +486,7 @@
public void testOnUserServiceConnected() {
IBinder binder = bindUserClient();
verifyOnClientConnected(USER_CLIENT_NAME, binder);
+ verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
}
private IBinder bindUserClient() {
@@ -462,12 +508,14 @@
ServiceConnection connection = mConnectionCaptor.getValue();
connection.onServiceConnected(null, createPublisherBinder());
+ verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
reset(mPublisherService);
connection.onServiceDisconnected(null);
verify(mPublisherService).onClientDisconnected(eq(SYSTEM_CLIENT_NAME));
+ verify(mSystemClientLog).logConnectionState(ConnectionState.DISCONNECTED);
- Thread.sleep(10);
+ verifyAndRunRebindTask();
verify(mContext).unbindService(connection);
verifySystemBind(1);
}
@@ -482,14 +530,19 @@
IBinder binder = createPublisherBinder();
connection.onServiceConnected(null, binder);
verifyOnClientConnected(SYSTEM_CLIENT_NAME, binder);
- reset(mPublisherService);
+ verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
+ reset(mPublisherService, mSystemClientLog);
connection.onServiceDisconnected(null);
verify(mPublisherService).onClientDisconnected(eq(SYSTEM_CLIENT_NAME));
+ verify(mSystemClientLog).logConnectionState(ConnectionState.DISCONNECTED);
binder = createPublisherBinder();
connection.onServiceConnected(null, binder);
verifyOnClientConnected(SYSTEM_CLIENT_NAME, binder);
+ verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
+
+ verifyAndRunRebindTask();
// No more interactions (verified by tearDown)
}
@@ -502,12 +555,14 @@
ServiceConnection connection = mConnectionCaptor.getValue();
connection.onServiceConnected(null, createPublisherBinder());
+ verify(mSystemClientLog).logConnectionState(ConnectionState.CONNECTED);
reset(mPublisherService);
connection.onBindingDied(null);
verify(mPublisherService).onClientDisconnected(eq(SYSTEM_CLIENT_NAME));
+ verify(mSystemClientLog).logConnectionState(ConnectionState.DISCONNECTED);
- Thread.sleep(10);
+ verifyAndRunRebindTask();
verify(mContext).unbindService(connection);
verifySystemBind(1);
}
@@ -523,7 +578,7 @@
verifyZeroInteractions(mPublisherService);
- Thread.sleep(10);
+ verifyAndRunRebindTask();
verify(mContext).unbindService(connection);
verifySystemBind(1);
}
@@ -536,12 +591,14 @@
ServiceConnection connection = mConnectionCaptor.getValue();
connection.onServiceConnected(null, createPublisherBinder());
+ verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
reset(mPublisherService);
connection.onServiceDisconnected(null);
verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+ verify(mUserClientLog).logConnectionState(ConnectionState.DISCONNECTED);
- Thread.sleep(10);
+ verifyAndRunRebindTask();
verify(mContext).unbindService(connection);
verifyUserBind(1);
}
@@ -556,14 +613,19 @@
IBinder binder = createPublisherBinder();
connection.onServiceConnected(null, binder);
verifyOnClientConnected(USER_CLIENT_NAME, binder);
- reset(mPublisherService);
+ verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
+ reset(mPublisherService, mUserClientLog);
connection.onServiceDisconnected(null);
verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+ verify(mUserClientLog).logConnectionState(ConnectionState.DISCONNECTED);
binder = createPublisherBinder();
connection.onServiceConnected(null, binder);
verifyOnClientConnected(USER_CLIENT_NAME, binder);
+ verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
+
+ verifyAndRunRebindTask();
// No more interactions (verified by tearDown)
}
@@ -575,12 +637,14 @@
ServiceConnection connection = mConnectionCaptor.getValue();
connection.onServiceConnected(null, createPublisherBinder());
+ verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
reset(mPublisherService);
connection.onBindingDied(null);
verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+ verify(mUserClientLog).logConnectionState(ConnectionState.DISCONNECTED);
- Thread.sleep(10);
+ verifyAndRunRebindTask();
verify(mContext).unbindService(connection);
verifyUserBind(1);
}
@@ -596,7 +660,7 @@
verifyZeroInteractions(mPublisherService);
- Thread.sleep(10);
+ verifyAndRunRebindTask();
verify(mContext).unbindService(connection);
verifyUserBind(1);
}
@@ -605,15 +669,18 @@
public void testOnUserSwitched_UserChange() {
notifyUserUnlocked(USER_ID, true);
verifyUserBind(1);
+ resetContext();
+
ServiceConnection connection = mConnectionCaptor.getValue();
connection.onServiceConnected(null, createPublisherBinder());
- resetContext();
+ verify(mUserClientLog).logConnectionState(ConnectionState.CONNECTED);
reset(mPublisherService);
notifyUserSwitched(USER_ID_U11, true);
verify(mContext).unbindService(connection);
verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+ verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
verifyUserBind(1);
}
@@ -630,6 +697,7 @@
verify(mContext).unbindService(connection);
verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+ verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
verifyUserBind(0);
}
@@ -646,6 +714,7 @@
verify(mContext).unbindService(connection);
verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+ verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
verifyUserBind(0);
}
@@ -684,10 +753,12 @@
resetContext();
reset(mPublisherService);
+ notifyUserSwitched(USER_ID_U11, false);
notifyUserUnlocked(USER_ID_U11, true);
verify(mContext).unbindService(connection);
verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+ verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
verifyUserBind(1);
}
@@ -702,10 +773,12 @@
resetContext();
reset(mPublisherService);
+ notifyUserSwitched(USER_ID_U11, false);
notifyUserUnlocked(UserHandle.USER_SYSTEM, true);
verify(mContext).unbindService(connection);
verify(mPublisherService).onClientDisconnected(eq(USER_CLIENT_NAME));
+ verify(mUserClientLog).logConnectionState(ConnectionState.TERMINATED);
// User processes will not be bound for system user
verifyUserBind(0);
}
@@ -717,6 +790,7 @@
ServiceConnection connection = mConnectionCaptor.getValue();
resetContext();
+ notifyUserSwitched(USER_ID_U11, false);
notifyUserUnlocked(USER_ID_U11, true);
verify(mContext).unbindService(connection);
@@ -727,7 +801,9 @@
public void testAddSubscriber() {
mClientManager.addSubscriber(mSubscriberClient1);
assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+ assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient1));
assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+ assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient2));
}
@Test
@@ -737,7 +813,9 @@
mClientManager.addSubscriber(mSubscriberClient1);
assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+ assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient1));
assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+ assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient2));
}
@Test
@@ -751,6 +829,7 @@
// expected
}
assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+ assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
}
@Test
@@ -759,7 +838,9 @@
mClientManager.addSubscriber(mSubscriberClient1);
verify(mPackageManager, atMost(1)).getNameForUid(anyInt());
assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+ assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient1));
assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+ assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient2));
}
@Test
@@ -768,11 +849,14 @@
mClientManager.addSubscriber(mSubscriberClient2);
verify(mPackageManager, atMost(2)).getNameForUid(anyInt());
assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+ assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient1));
assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+ assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient2));
}
@Test
public void testAddSubscriber_MultipleClients_ForegroundAndSystemUsers_SamePackage() {
+ int clientUid1 = mCallingAppUid;
mClientManager.addSubscriber(mSubscriberClient1);
mCallingAppUid = UserHandle.getUid(UserHandle.USER_SYSTEM, 0);
@@ -781,12 +865,15 @@
verify(mPackageManager, atMost(2)).getNameForUid(anyInt());
assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+ assertEquals(clientUid1, mClientManager.getSubscriberUid(mSubscriberClient1));
assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+ assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient2));
}
@Test
public void testAddSubscriber_MultipleClients_MultiplePackages() {
+ int clientUid1 = mCallingAppUid;
mClientManager.addSubscriber(mSubscriberClient1);
mCallingAppUid = UserHandle.getUid(mForegroundUserId, 1);
@@ -795,7 +882,9 @@
verify(mPackageManager, times(2)).getNameForUid(anyInt());
assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+ assertEquals(clientUid1, mClientManager.getSubscriberUid(mSubscriberClient1));
assertEquals("test.package2", mClientManager.getPackageName(mSubscriberClient2));
+ assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient2));
}
@Test
@@ -804,12 +893,14 @@
mClientManager.removeSubscriber(mSubscriberClient1);
verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+ assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
}
@Test
public void testRemoveSubscriber_NotRegistered() {
mClientManager.removeSubscriber(mSubscriberClient1);
assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+ assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
}
@Test
@@ -821,17 +912,17 @@
verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+ assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
}
@Test
public void testOnUserSwitch_RemoveSubscriber() {
mClientManager.addSubscriber(mSubscriberClient1);
- mForegroundUserId = USER_ID_U11;
- mClientManager.mUserSwitchReceiver.onReceive(mContext, new Intent());
-
+ notifyUserSwitched(USER_ID_U11, false);
verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+ assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
assertTrue(mClientManager.getAllSubscribers().isEmpty());
}
@@ -839,15 +930,17 @@
public void testOnUserSwitch_RemoveSubscriber_AddNewSubscriber() {
mClientManager.addSubscriber(mSubscriberClient1);
- mForegroundUserId = USER_ID_U11;
- mClientManager.mUserSwitchReceiver.onReceive(mContext, new Intent());
+ notifyUserSwitched(USER_ID_U11, false);
verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
mCallingAppUid = UserHandle.getUid(USER_ID_U11, 0);
when(mPackageManager.getNameForUid(mCallingAppUid)).thenReturn(TEST_PACKAGE);
mClientManager.addSubscriber(mSubscriberClient2);
+ assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(mSubscriberClient1));
+ assertEquals(-1, mClientManager.getSubscriberUid(mSubscriberClient1));
assertEquals(TEST_PACKAGE, mClientManager.getPackageName(mSubscriberClient2));
+ assertEquals(mCallingAppUid, mClientManager.getSubscriberUid(mSubscriberClient2));
assertFalse(mClientManager.getAllSubscribers().contains(mSubscriberClient1));
assertTrue(mClientManager.getAllSubscribers().contains(mSubscriberClient2));
}
@@ -861,8 +954,7 @@
mClientManager.addSubscriber(mSubscriberClient2);
- mForegroundUserId = USER_ID_U11;
- mClientManager.mUserSwitchReceiver.onReceive(mContext, new Intent());
+ notifyUserSwitched(USER_ID_U11, false);
verify(mBrokerService).removeDeadSubscriber(mSubscriberClient1);
verify(mBrokerService, never()).removeDeadSubscriber(mSubscriberClient2);
@@ -875,10 +967,10 @@
IVmsPublisherClient publisherClient = createPublisherClient();
IVmsSubscriberClient subscriberClient = createSubscriberClient();
mClientManager.onHalConnected(publisherClient, subscriberClient);
+ verify(mHalClientLog).logConnectionState(ConnectionState.CONNECTED);
reset(mPublisherService);
- mForegroundUserId = USER_ID_U11;
- mClientManager.mUserSwitchReceiver.onReceive(mContext, new Intent());
+ notifyUserSwitched(USER_ID_U11, false);
verify(mBrokerService, never()).removeDeadSubscriber(subscriberClient);
assertEquals(HAL_CLIENT_NAME, mClientManager.getPackageName(subscriberClient));
@@ -890,6 +982,7 @@
IVmsSubscriberClient subscriberClient = createSubscriberClient();
mClientManager.onHalConnected(publisherClient, subscriberClient);
verify(mPublisherService).onClientConnected(eq(HAL_CLIENT_NAME), same(publisherClient));
+ verify(mHalClientLog).logConnectionState(ConnectionState.CONNECTED);
assertTrue(mClientManager.getAllSubscribers().contains(subscriberClient));
assertEquals(HAL_CLIENT_NAME, mClientManager.getPackageName(subscriberClient));
}
@@ -902,6 +995,7 @@
mClientManager.onHalConnected(publisherClient, subscriberClient);
verify(mPublisherService).onClientConnected(eq(HAL_CLIENT_NAME), same(publisherClient));
+ verify(mHalClientLog).logConnectionState(ConnectionState.CONNECTED);
assertTrue(mClientManager.getAllSubscribers().contains(subscriberClient));
assertEquals(HAL_CLIENT_NAME, mClientManager.getPackageName(subscriberClient));
}
@@ -911,11 +1005,13 @@
IVmsPublisherClient publisherClient = createPublisherClient();
IVmsSubscriberClient subscriberClient = createSubscriberClient();
mClientManager.onHalConnected(publisherClient, subscriberClient);
+ verify(mHalClientLog).logConnectionState(ConnectionState.CONNECTED);
reset(mPublisherService);
mClientManager.onHalDisconnected();
verify(mPublisherService).onClientDisconnected(eq(HAL_CLIENT_NAME));
verify(mBrokerService).removeDeadSubscriber(eq(subscriberClient));
+ verify(mHalClientLog).logConnectionState(ConnectionState.DISCONNECTED);
assertFalse(mClientManager.getAllSubscribers().contains(subscriberClient));
assertEquals(UNKNOWN_PACKAGE, mClientManager.getPackageName(subscriberClient));
}
@@ -928,7 +1024,7 @@
}
private void resetContext() {
- reset(mContext);
+ reset(mContext, mSystemClientLog, mUserClientLog);
when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mContext.bindServiceAsUser(any(), any(), anyInt(), any(), any())).thenReturn(true);
when(mContext.getResources()).thenReturn(mResources);
@@ -939,30 +1035,29 @@
}
private void notifyUserSwitched(int foregroundUserId, boolean isForegroundUserUnlocked) {
- notifyUserAction(foregroundUserId, isForegroundUserUnlocked, Intent.ACTION_USER_SWITCHED);
+ when(mUserManager.isUserUnlockingOrUnlocked(foregroundUserId))
+ .thenReturn(isForegroundUserUnlocked);
+ mForegroundUserId = foregroundUserId; // Member variable used by verifyUserBind()
+ mClientManager.mUserCallback.onSwitchUser(foregroundUserId);
}
private void notifyUserUnlocked(int foregroundUserId, boolean isForegroundUserUnlocked) {
- notifyUserAction(foregroundUserId, isForegroundUserUnlocked, Intent.ACTION_USER_UNLOCKED);
- }
-
- // Sets the current foreground user + unlock state and dispatches the specified intent action
- private void notifyUserAction(int foregroundUserId, boolean isForegroundUserUnlocked,
- String action) {
- mForegroundUserId = foregroundUserId; // Member variable used by verifyUserBind()
- when(mUserManagerHelper.getCurrentForegroundUserId()).thenReturn(foregroundUserId);
-
- reset(mUserManager);
- when(mUserManager.isUserUnlocked(foregroundUserId)).thenReturn(isForegroundUserUnlocked);
-
- mClientManager.mUserSwitchReceiver.onReceive(mContext, new Intent(action));
+ when(mUserManager.isUserUnlockingOrUnlocked(foregroundUserId))
+ .thenReturn(isForegroundUserUnlocked);
+ mClientManager.mUserCallback.onUserLockChanged(foregroundUserId, isForegroundUserUnlocked);
}
private void verifySystemBind(int times) {
+ verify(mSystemClientLog, times(times)).logConnectionState(ConnectionState.CONNECTING);
verifyBind(times, SYSTEM_CLIENT_COMPONENT, UserHandle.SYSTEM);
}
private void verifyUserBind(int times) {
+ if (mForegroundUserId == USER_ID) {
+ verify(mUserClientLog, times(times)).logConnectionState(ConnectionState.CONNECTING);
+ } else if (mForegroundUserId == USER_ID_U11) {
+ verify(mUserClientLog2, times(times)).logConnectionState(ConnectionState.CONNECTING);
+ }
verifyBind(times, USER_CLIENT_COMPONENT, UserHandle.of(mForegroundUserId));
}
@@ -975,6 +1070,11 @@
eq(Context.BIND_AUTO_CREATE), any(Handler.class), eq(user));
}
+ private void verifyAndRunRebindTask() {
+ verify(mHandler).postDelayed(mRebindCaptor.capture(), eq(MILLIS_BEFORE_REBIND));
+ mRebindCaptor.getValue().run();
+ }
+
private void verifyOnClientConnected(String publisherName, IBinder binder) {
ArgumentCaptor<IVmsPublisherClient> clientCaptor =
ArgumentCaptor.forClass(IVmsPublisherClient.class);
diff --git a/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh b/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh
new file mode 100755
index 0000000..8beb1f4
--- /dev/null
+++ b/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+if [ -z "$ANDROID_PRODUCT_OUT" ]; then
+ echo "ANDROID_PRODUCT_OUT not set"
+ exit
+fi
+DISP_ID=1
+if [[ $# -eq 1 ]]; then
+ echo "$1"
+ DISP_ID=$1
+fi
+echo "Use display:$DISP_ID"
+
+adb root
+# Check always crashing one
+echo "Start AlwaysCrashingActivity in fixed mode"
+adb shell dumpsys car_service start-fixed-activity-mode $DISP_ID com.google.android.car.kitchensink com.google.android.car.kitchensink.AlwaysCrashingActivity
+sleep 1
+read -p "AlwaysCrashingAvtivity should not be tried any more. Press Enter"
+# package update
+echo "Will try package update:"
+adb install -r -g $ANDROID_PRODUCT_OUT/system/priv-app/EmbeddedKitchenSinkApp/EmbeddedKitchenSinkApp.apk
+read -p "AlwaysCrashingAvtivity should have been retried. Press Enter"
+# suspend-resume
+echo "Check retry for suspend - resume"
+adb shell setprop android.car.garagemodeduration 1
+adb shell dumpsys car_service suspend
+adb shell dumpsys car_service resume
+read -p "AlwaysCrashingAvtivity should have been retried. Press Enter"
+# starting other Activity
+echo "Switch to no crash Activity"
+adb shell dumpsys car_service start-fixed-activity-mode $DISP_ID com.google.android.car.kitchensink com.google.android.car.kitchensink.NoCrashActivity
+read -p "NoCrashAvtivity should have been shown. Press Enter"
+# stating other non-fixed Activity
+adb shell am start-activity --display $DISP_ID -n com.google.android.car.kitchensink/.EmptyActivity
+read -p "NoCrashAvtivity should be shown after showing EmptyActivity. Press Enter"
+# package update
+echo "Will try package update:"
+adb install -r -g $ANDROID_PRODUCT_OUT/system/priv-app/EmbeddedKitchenSinkApp/EmbeddedKitchenSinkApp.apk
+read -p "NoCrashActivity should be shown. Press Enter"
+# stop the mode
+echo "Stop fixed activity mode"
+adb shell dumpsys car_service stop-fixed-activity-mode $DISP_ID
+adb shell am start-activity --display $DISP_ID -n com.google.android.car.kitchensink/.EmptyActivity
+read -p "EmptyActivity should be shown. Press Enter"
diff --git a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
index 0745945..72332d9 100644
--- a/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
+++ b/user/car-user-lib/src/android/car/userlib/CarUserManagerHelper.java
@@ -238,10 +238,8 @@
// If an override user is present and a real user, return it
if (bootUserOverride != BOOT_USER_NOT_FOUND
&& allUsers.contains(bootUserOverride)) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Boot user id override found for initial user, user id: "
- + bootUserOverride);
- }
+ Log.i(TAG, "Boot user id override found for initial user, user id: "
+ + bootUserOverride);
return bootUserOverride;
}
@@ -249,19 +247,15 @@
int lastActiveUser = getLastActiveUser();
if (lastActiveUser != UserHandle.USER_SYSTEM
&& allUsers.contains(lastActiveUser)) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Last active user loaded for initial user, user id: "
- + lastActiveUser);
- }
+ Log.i(TAG, "Last active user loaded for initial user, user id: "
+ + lastActiveUser);
return lastActiveUser;
}
// If all else fails, return the smallest user id
int returnId = Collections.min(allUsers);
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Saved ids were invalid. Returning smallest user id, user id: "
- + returnId);
- }
+ Log.i(TAG, "Saved ids were invalid. Returning smallest user id, user id: "
+ + returnId);
return returnId;
}