Added UserHalService.HalCallback.STATUS_CONCURRENT_OPERATION.

This status is called when a UserHalService method is called before
the HAL returned from a previous call to the same method.

Also fixed mPendingCallbacks thread-safety on UserHalService.

Test: atest CarServiceUnitTest:com.android.car.hal.UserHalServiceTest#testGetUserInfo_secondCallFailWhilePending
Test: atest CarServiceUnitTest:com.android.car.hal.UserHalServiceTest

Bug: 146207078
Bug: 150399261

Change-Id: I4970ca32f64b5166ec6eabfd039afb18d50facf1
diff --git a/service/src/com/android/car/hal/UserHalHelper.java b/service/src/com/android/car/hal/UserHalHelper.java
index 18f9a58..d5311dc 100644
--- a/service/src/com/android/car/hal/UserHalHelper.java
+++ b/service/src/com/android/car/hal/UserHalHelper.java
@@ -37,11 +37,13 @@
             case HalCallback.STATUS_OK:
                 return "OK";
             case HalCallback.STATUS_HAL_SET_TIMEOUT:
-                return "STATUS_HAL_SET_TIMEOUT";
+                return "HAL_SET_TIMEOUT";
             case HalCallback.STATUS_HAL_RESPONSE_TIMEOUT:
-                return "STATUS_HAL_RESPONSE_TIMEOUT";
+                return "HAL_RESPONSE_TIMEOUT";
             case HalCallback.STATUS_WRONG_HAL_RESPONSE:
-                return "STATUS_WRONG_HAL_RESPONSE";
+                return "WRONG_HAL_RESPONSE";
+            case HalCallback.STATUS_CONCURRENT_OPERATION:
+                return "CONCURRENT_OPERATION";
             default:
                 return "UNKNOWN-" + status;
         }
diff --git a/service/src/com/android/car/hal/UserHalService.java b/service/src/com/android/car/hal/UserHalService.java
index 72d1fb1..336f28d 100644
--- a/service/src/com/android/car/hal/UserHalService.java
+++ b/service/src/com/android/car/hal/UserHalService.java
@@ -38,8 +38,8 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseIntArray;
 
-import com.android.car.CarLog;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
 
@@ -50,6 +50,8 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Service used to integrate the OEM's custom user management with Android's.
@@ -58,11 +60,20 @@
 
     private static final String UNSUPPORTED_MSG = "Vehicle HAL does not support user management";
 
-    private static final String TAG = CarLog.TAG_USER;
+    private static final String TAG = UserHalService.class.getSimpleName();
 
     // TODO(b/146207078): STOPSHIP - change to false before R is launched
     private static final boolean DBG = true;
 
+    private static final int REQUEST_TYPE_GET_INITIAL_INFO = 1;
+
+    /** @hide */
+    @IntDef(prefix = { "REQUEST_TYPE_" }, value = {
+            REQUEST_TYPE_GET_INITIAL_INFO
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface RequestType{}
+
     private final Object mLock = new Object();
 
     private final VehicleHal mHal;
@@ -83,11 +94,17 @@
     private int mNextRequestId = 1;
 
     /**
-     * Map of callback by request id.
+     * Map of callbacks by request id.
      */
     @GuardedBy("mHandler")
     private SparseArray<Pair<Class<?>, HalCallback<?>>> mPendingCallbacks = new SparseArray<>();
 
+    /**
+     * Map of request ids by {@link RequestType}.
+     */
+    @GuardedBy("mLock")
+    private SparseIntArray mPendingRequests = new SparseIntArray();
+
     public UserHalService(VehicleHal hal) {
         mHal = hal;
     }
@@ -173,13 +190,15 @@
         int STATUS_HAL_SET_TIMEOUT = 2;
         int STATUS_HAL_RESPONSE_TIMEOUT = 3;
         int STATUS_WRONG_HAL_RESPONSE = 4;
+        int STATUS_CONCURRENT_OPERATION = 5;
 
         /** @hide */
         @IntDef(prefix = { "STATUS_" }, value = {
                 STATUS_OK,
                 STATUS_HAL_SET_TIMEOUT,
                 STATUS_HAL_RESPONSE_TIMEOUT,
-                STATUS_WRONG_HAL_RESPONSE
+                STATUS_WRONG_HAL_RESPONSE,
+                STATUS_CONCURRENT_OPERATION
         })
         @Retention(RetentionPolicy.SOURCE)
         @interface HalCallbackStatus{}
@@ -203,6 +222,7 @@
         }
     }
 
+    @GuardedBy("mLock")
     private void checkSupportedLocked() {
         Preconditions.checkState(isSupported(), UNSUPPORTED_MSG);
     }
@@ -232,7 +252,8 @@
         int requestId;
         synchronized (mLock) {
             checkSupportedLocked();
-            requestId = mNextRequestId++;
+            if (hasPendingRequestLocked(REQUEST_TYPE_GET_INITIAL_INFO, callback)) return;
+            requestId = addPendingRequestLocked(REQUEST_TYPE_GET_INITIAL_INFO);
             // TODO(b/146207078): use helper method to convert request to prop value
             propRequest.value.int32Values.add(requestId);
             propRequest.value.int32Values.add(requestType);
@@ -245,9 +266,10 @@
                 propRequest.value.int32Values.add(userInfo.flags);
             }
             setTimestamp(propRequest);
-            if (DBG) Log.d(TAG, "adding pending callback for request " + requestId);
-            mPendingCallbacks.put(requestId, new Pair<>(InitialUserInfoResponse.class, callback));
         }
+        mHandler.sendMessage(obtainMessage(
+                UserHalService::handleAddPendingRequest, this, requestId,
+                InitialUserInfoResponse.class, callback));
 
         mHandler.sendMessageDelayed(obtainMessage(
                 UserHalService::handleCheckIfRequestTimedOut, this, requestId).setWhat(requestId),
@@ -261,6 +283,42 @@
         }
     }
 
+    private void handleAddPendingRequest(int requestId, @NonNull Class<?> responseClass,
+            @NonNull HalCallback<?> callback) {
+        if (DBG) {
+            Log.d(TAG, "adding pending callback (of type " + responseClass.getName()
+                    + ") for request " + requestId);
+        }
+        mPendingCallbacks.put(requestId, new Pair<>(responseClass, callback));
+    }
+
+    /**
+     * Checks if there is a pending request of type {@code requestType}, calling {@code callback}
+     * with {@link HalCallback#STATUS_CONCURRENT_OPERATION} when there is.
+     */
+    private boolean hasPendingRequestLocked(@RequestType int requestType,
+            @NonNull HalCallback<?> callback) {
+        int index = mPendingRequests.indexOfKey(requestType);
+        if (index < 0) return false;
+
+        int requestId = mPendingRequests.valueAt(index);
+        Log.w(TAG, "Already have pending request of type " + requestTypeToString(requestType)
+                + ": id=" + requestId);
+
+        callback.onResponse(HalCallback.STATUS_CONCURRENT_OPERATION, null);
+        return true;
+    }
+
+    /**
+     * Adds a new pending request to the queue, returning its request id.
+     */
+    @GuardedBy("mLock")
+    private int addPendingRequestLocked(@RequestType int requestType) {
+        int requestId = mNextRequestId++;
+        mPendingRequests.put(requestType, requestId);
+        return requestId;
+    }
+
     /**
      * Removes the pending request and its timeout callback.
      */
@@ -334,7 +392,7 @@
         return callback;
     }
 
-    private void setTimestamp(VehiclePropValue propRequest) {
+    private void setTimestamp(@NonNull VehiclePropValue propRequest) {
         propRequest.timestamp = SystemClock.elapsedRealtime();
     }
 
@@ -353,11 +411,50 @@
                 writer.printf("%s%s\n", indent, mProperties.valueAt(i));
             }
             writer.printf("next request id: %d\n", mNextRequestId);
-            if (mPendingCallbacks.size() == 0) {
-                writer.println("no pending callbacks");
+            int size = mPendingRequests.size();
+            if (size == 0) {
+                writer.println("no pending requests");
             } else {
-                writer.printf("pending callbacks: %s\n", mPendingCallbacks);
+                writer.printf("%d pending requests\n", size);
+                for (int i = 0; i < size; i++) {
+                    String type = requestTypeToString(mPendingRequests.keyAt(i));
+                    int requestId = mPendingRequests.valueAt(i);
+                    writer.printf("%s#%d: %s=req_id(%d)\n", indent, i, type, requestId);
+                }
             }
         }
+
+        CountDownLatch latch = new CountDownLatch(1);
+        mHandler.sendMessage(obtainMessage(UserHalService::handleDump, this, writer, latch));
+        try {
+            if (!latch.await(500, TimeUnit.SECONDS)) {
+                writer.println("\nTIMED OUT");
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            writer.println("\nINTERRUPTED");
+        }
+    }
+
+    /**
+     * Dumps the state that's guarded by {@code mHandler}.
+     */
+    private void handleDump(@NonNull PrintWriter writer, @NonNull CountDownLatch latch) {
+        if (mPendingCallbacks.size() == 0) {
+            writer.println("no pending callbacks");
+        } else {
+            writer.printf("pending callbacks: %s\n", mPendingCallbacks);
+        }
+        latch.countDown();
+    }
+
+    @NonNull
+    private static String requestTypeToString(@RequestType int type) {
+        switch (type) {
+            case REQUEST_TYPE_GET_INITIAL_INFO:
+                return "TYPE_GET_INITIAL_INFO";
+            default:
+                return "UNKNOWN-" + type;
+        }
     }
 }
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java
index 9e3c4e3..d5f4ccc 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/UserHalServiceTest.java
@@ -196,6 +196,26 @@
     }
 
     @Test
+    public void testGetUserInfo_secondCallFailWhilePending() throws Exception {
+        GenericHalCallback<InitialUserInfoResponse> callback1 = new GenericHalCallback<>(
+                INITIAL_USER_CALLBACK_TIMEOUT_TIMEOUT);
+        GenericHalCallback<InitialUserInfoResponse> callback2 = new GenericHalCallback<>(
+                INITIAL_USER_CALLBACK_TIMEOUT_TIMEOUT);
+        mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS, mUsersInfo,
+                callback1);
+        mUserHalService.getInitialUserInfo(COLD_BOOT, INITIAL_USER_TIMEOUT_MS, mUsersInfo,
+                callback2);
+
+        callback1.assertCalled();
+        assertCallbackStatus(callback1, HalCallback.STATUS_HAL_RESPONSE_TIMEOUT);
+        assertThat(callback1.response).isNull();
+
+        callback2.assertCalled();
+        assertCallbackStatus(callback2, HalCallback.STATUS_CONCURRENT_OPERATION);
+        assertThat(callback1.response).isNull();
+    }
+
+    @Test
     public void testGetUserInfo_halReplyWithWrongRequestId() throws Exception {
         // TODO(b/146207078): use helper method to convert prop value to proper req
         VehiclePropValue propResponse = new VehiclePropValue();