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("", ""));
+    }
+}