Change Car.createCar to wait for car service if not found
- Add explicit binding and retry instead of giving up immediately.
- Try only up to 5 secs and return null after that.
- This can block main thread but car service not available for that long
is a problem anyway.
- Added unit test to verify the functionality when car service is not started.
Bug: 139687557
Test: check boot up
Change-Id: I272db5f8be37d1650ac43b91ed9d8d7bd064f08b
Merged-In: I272db5f8be37d1650ac43b91ed9d8d7bd064f08b
(cherry picked from commit f9745398b4aa19e011c59b640e3b504c14f784ff)
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index 3d4b2f8..1912803 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -56,6 +56,7 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -67,6 +68,13 @@
* Calling this API on a device with no such feature will lead to an exception.
*/
public final class Car {
+
+ /**
+ * Binder service name of car service registered to service manager.
+ *
+ * @hide
+ */
+ public static final String CAR_SERVICE_BINDER_SERVICE_NAME = "car_service";
/**
* Service name for {@link CarSensorManager}, to be used in {@link #getCarManager(String)}.
*
@@ -602,6 +610,9 @@
private static final long CAR_SERVICE_BIND_RETRY_INTERVAL_MS = 500;
private static final long CAR_SERVICE_BIND_MAX_RETRY = 20;
+ private static final long CAR_SERVICE_BINDER_POLLING_INTERVAL_MS = 50;
+ private static final long CAR_SERVICE_BINDER_POLLING_MAX_RETRY = 100;
+
private final Context mContext;
@GuardedBy("this")
private ICar mService;
@@ -636,7 +647,9 @@
mService = ICar.Stub.asInterface(service);
mConnectionState = STATE_CONNECTED;
}
- mServiceConnectionListenerClient.onServiceConnected(name, service);
+ if (mServiceConnectionListenerClient != null) {
+ mServiceConnectionListenerClient.onServiceConnected(name, service);
+ }
}
public void onServiceDisconnected(ComponentName name) {
@@ -647,7 +660,9 @@
}
// unbind explicitly and set connectionState to STATE_DISCONNECTED here.
disconnect();
- mServiceConnectionListenerClient.onServiceDisconnected(name);
+ if (mServiceConnectionListenerClient != null) {
+ mServiceConnectionListenerClient.onServiceDisconnected(name);
+ }
}
};
@@ -723,11 +738,54 @@
*/
@Nullable
public static Car createCar(Context context, @Nullable Handler handler) {
- IBinder service = ServiceManager.getService("car_service");
- if (service == null) {
- return null;
+ Car car = null;
+ IBinder service = null;
+ boolean started = false;
+ int retryCount = 0;
+ while (true) {
+ service = ServiceManager.getService(CAR_SERVICE_BINDER_SERVICE_NAME);
+ if (car == null) {
+ // service can be still null. The constructor is safe for null service.
+ car = new Car(context, ICar.Stub.asInterface(service), handler);
+ }
+ if (service != null) {
+ if (!started) { // specialization for most common case.
+ return car;
+ }
+ break;
+ }
+ if (!started) {
+ car.startCarService();
+ started = true;
+ }
+ retryCount++;
+ if (retryCount > CAR_SERVICE_BINDER_POLLING_MAX_RETRY) {
+ Log.e(CarLibLog.TAG_CAR, "cannot get car_service, waited for car service (ms):"
+ + CAR_SERVICE_BINDER_POLLING_INTERVAL_MS
+ * CAR_SERVICE_BINDER_POLLING_MAX_RETRY,
+ new RuntimeException());
+ return null;
+ }
+ try {
+ Thread.sleep(CAR_SERVICE_BINDER_POLLING_INTERVAL_MS);
+ } catch (InterruptedException e) {
+ Log.e(CarLibLog.TAG_CAR, "interrupted while waiting for car_service",
+ new RuntimeException());
+ return null;
+ }
}
- return new Car(context, ICar.Stub.asInterface(service), handler);
+ // Can be accessed from mServiceConnectionListener in main thread.
+ synchronized (car) {
+ if (car.mService == null) {
+ car.mService = ICar.Stub.asInterface(service);
+ Log.w(CarLibLog.TAG_CAR,
+ "waited for car_service (ms):"
+ + CAR_SERVICE_BINDER_POLLING_INTERVAL_MS * retryCount,
+ new RuntimeException());
+ }
+ car.mConnectionState = STATE_CONNECTED;
+ }
+ return car;
}
private Car(Context context, ServiceConnection serviceConnectionListener,
@@ -737,6 +795,7 @@
mMainThreadEventHandler = determineMainThreadEventHandler(mEventHandler);
mService = null;
+ mConnectionState = STATE_DISCONNECTED;
mOwnsService = true;
mServiceConnectionListenerClient = serviceConnectionListener;
}
@@ -746,14 +805,19 @@
* Car constructor when ICar binder is already available.
* @hide
*/
- public Car(Context context, ICar service, @Nullable Handler handler) {
+ public Car(Context context, @Nullable ICar service, @Nullable Handler handler) {
mContext = context;
mEventHandler = determineEventHandler(handler);
mMainThreadEventHandler = determineMainThreadEventHandler(mEventHandler);
mService = service;
- mOwnsService = false;
- mConnectionState = STATE_CONNECTED;
+ if (service != null) {
+ mConnectionState = STATE_CONNECTED;
+ mOwnsService = false;
+ } else {
+ mConnectionState = STATE_DISCONNECTED;
+ mOwnsService = true;
+ }
mServiceConnectionListenerClient = null;
}
@@ -833,6 +897,12 @@
}
}
+ /** @hide */
+ @VisibleForTesting
+ public ServiceConnection getServiceConnectionListener() {
+ return mServiceConnectionListener;
+ }
+
/**
* Get car specific service as in {@link Context#getSystemService(String)}. Returned
* {@link Object} should be type-casted to the desired service.
diff --git a/tests/carservice_unit_test/Android.mk b/tests/carservice_unit_test/Android.mk
index 6b715fc..ec984f8 100644
--- a/tests/carservice_unit_test/Android.mk
+++ b/tests/carservice_unit_test/Android.mk
@@ -48,17 +48,19 @@
LOCAL_STATIC_JAVA_LIBRARIES := \
androidx.test.core \
+ androidx.test.ext.junit \
androidx.test.rules \
car-frameworks-service \
car-service-lib-for-test \
com.android.car.test.utils \
- junit \
- mockito-target-inline-minus-junit4 \
frameworks-base-testutils \
+ mockito-target-extended \
+ testng \
truth-prebuilt
# mockito-target-inline dependency
LOCAL_JNI_SHARED_LIBRARIES := \
libdexmakerjvmtiagent \
+ libstaticjvmtiagent \
include $(BUILD_PACKAGE)
diff --git a/tests/carservice_unit_test/src/android/car/CarTest.java b/tests/carservice_unit_test/src/android/car/CarTest.java
new file mode 100644
index 0000000..91a1ddb
--- /dev/null
+++ b/tests/carservice_unit_test/src/android/car/CarTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.ServiceManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.quality.Strictness;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Unit test for Car API.
+ */
+@RunWith(AndroidJUnit4.class)
+public class CarTest {
+ private static final String TAG = CarTest.class.getSimpleName();
+
+ private MockitoSession mMockingSession;
+
+ @Mock
+ private Context mContext;
+
+ // It is tricky to mock this. So create dummy version instead.
+ private ICar.Stub mService = new ICar.Stub() {
+ @Override
+ public void setCarServiceHelper(android.os.IBinder helper) {
+ }
+
+ @Override
+ public void setUserLockStatus(int userHandle, int unlocked) {
+ }
+
+ @Override
+ public void onSwitchUser(int userHandle) {
+ }
+
+ @Override
+ public android.os.IBinder getCarService(java.lang.String serviceName) {
+ return null;
+ }
+
+ @Override
+ public int getCarConnectionType() {
+ return 0;
+ }
+ };
+
+ @Before
+ public void setUp() {
+ mMockingSession = mockitoSession()
+ .initMocks(this)
+ .mockStatic(ServiceManager.class)
+ .strictness(Strictness.LENIENT)
+ .startMocking();
+ }
+
+ @After
+ public void tearDown() {
+ mMockingSession.finishMocking();
+ }
+
+ private void expectService(@Nullable IBinder service) {
+ doReturn(service).when(
+ () -> ServiceManager.getService(Car.CAR_SERVICE_BINDER_SERVICE_NAME));
+ }
+
+ @Test
+ public void testCreateCarSuccessWithCarServiceRunning() {
+ expectService(mService);
+ assertThat(Car.createCar(mContext)).isNotNull();
+ }
+
+ @Test
+ public void testCreateCarReturnNull() {
+ // car service is not running yet and bindService does not bring the service yet.
+ // createCar should timeout and give up.
+ expectService(null);
+ assertThat(Car.createCar(mContext)).isNull();
+ }
+
+ @Test
+ public void testCreateCarOkWhenCarServiceIsStarted() {
+ // Car service is not running yet and binsService call should start it.
+ when(mContext.bindServiceAsUser(anyObject(), anyObject(), anyInt(),
+ anyObject())).thenReturn(true);
+ final int returnNonNullAfterThisCall = 10;
+ doAnswer(new Answer() {
+
+ private int mCallCount = 0;
+
+ @Override
+ public Object answer(InvocationOnMock invocation) {
+ mCallCount++;
+ if (mCallCount > returnNonNullAfterThisCall) {
+ return mService;
+ } else {
+ return null;
+ }
+ }
+ }).when(() -> ServiceManager.getService(Car.CAR_SERVICE_BINDER_SERVICE_NAME));
+ Car car = Car.createCar(mContext);
+ assertThat(car).isNotNull();
+ verify(mContext, times(1)).bindServiceAsUser(anyObject(), anyObject(),
+ anyInt(), anyObject());
+
+ // Just call these to guarantee that nothing crashes when service is connected /
+ // disconnected.
+ car.getServiceConnectionListener().onServiceConnected(new ComponentName("", ""), mService);
+ car.getServiceConnectionListener().onServiceDisconnected(new ComponentName("", ""));
+ }
+}