Add deadlock test for UXRE and driving state srvc

This implementation doesn't use any Thread.sleep()
calls to avoid test flakiness.

Also specify a faster than normal timeout since a test
failure represents a deadlock.

Bug: 142804287
Test: atest CarUxRestrictionsManagerServiceTest
Change-Id: I6a0dc23d4cc82cc3ef80ade8df14a98afab3b611
Merged-In: I6a0dc23d4cc82cc3ef80ade8df14a98afab3b611
(cherry picked from commit 6e923007414a37d199434d46f88ff1bd283a7dcf)
diff --git a/service/src/com/android/car/CarDrivingStateService.java b/service/src/com/android/car/CarDrivingStateService.java
index 584228d..59a3a8c 100644
--- a/service/src/com/android/car/CarDrivingStateService.java
+++ b/service/src/com/android/car/CarDrivingStateService.java
@@ -35,6 +35,8 @@
 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;
@@ -316,7 +318,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;
         }
diff --git a/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java b/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
index aadb6f9..4ca9d04 100644
--- a/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
+++ b/tests/carservice_test/src/com/android/car/CarUxRestrictionsManagerServiceTest.java
@@ -27,25 +27,31 @@
 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;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.MediumTest;
+import androidx.test.filters.Suppress;
 import androidx.test.runner.AndroidJUnit4;
 
 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 +68,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 +227,202 @@
         assertTrue(restrictions.toString(), expected.isSameRestrictions(restrictions));
     }
 
+    // TODO(b/142804287): Suppress this test until bug is fixed.
+    @Suppress
+    // 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");
+        }
+    }
+
+    // TODO(b/142804287): Suppress this test until bug is fixed.
+    @Suppress
+    // 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);
     }