[RESTRICT AUTOMERGE] Create TimeHalService in CarServices.

This Service registers a BroadcastReceiver for Intent.ACTION_TIME_CHANGED, and writes an update to
VHAL Property EPOCH_TIME, if supported by the VHAL.

Bug: 202377994
Bug: 157504928
Test: atest CarServiceUnitTest:TimeHalServiceTest
Change-Id: Ide99a1c1d8847cef33d7ea13d46ac2073229aff3
diff --git a/car-test-lib/Android.bp b/car-test-lib/Android.bp
index bb72590..bad376e 100644
--- a/car-test-lib/Android.bp
+++ b/car-test-lib/Android.bp
@@ -48,5 +48,6 @@
         "android.hardware.automotive.vehicle-V2.0-java",
         "mockito-target-extended",
         "compatibility-device-util-axt",
+        "android.test.mock",
     ],
 }
diff --git a/car-test-lib/src/android/car/test/util/BroadcastingFakeContext.java b/car-test-lib/src/android/car/test/util/BroadcastingFakeContext.java
new file mode 100644
index 0000000..014e283
--- /dev/null
+++ b/car-test-lib/src/android/car/test/util/BroadcastingFakeContext.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2021 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.test.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.test.mock.MockContext;
+
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * A fake implementation for {@link android.content.Context}, that helps broadcast {@link Intent}s
+ * to registered {@link BroadcastReceiver} instances.
+ */
+// TODO(b/202420937): Add unit tests for this class.
+public final class BroadcastingFakeContext extends MockContext {
+    private BroadcastReceiver mReceiver;
+    private IntentFilter mIntentFilter;
+    private Handler mHandler;
+
+    @Override
+    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+        mReceiver = receiver;
+        mIntentFilter = filter;
+
+        return null;
+    }
+
+    @Override
+    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+            String broadcastPermission, Handler scheduler) {
+        mReceiver = receiver;
+        mIntentFilter = filter;
+        mHandler = scheduler;
+
+        return null;
+    }
+
+    @Override
+    public void sendBroadcast(Intent intent) {
+        if (mHandler == null) {
+            mReceiver.onReceive(this, intent);
+            return;
+        }
+
+        CountDownLatch latch = new CountDownLatch(1);
+        mHandler.getLooper().getQueue().addIdleHandler(() -> {
+            latch.countDown();
+            return false;
+        });
+
+        mHandler.post(() -> mReceiver.onReceive(this, intent));
+
+        // wait until the queue is idle
+        try {
+            latch.await();
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException(
+                    "Interrupted while waiting for Broadcast Intent to be received");
+        }
+    }
+
+    @Override
+    public void unregisterReceiver(BroadcastReceiver receiver) {
+        if (receiver == mReceiver) {
+            mReceiver = null;
+            mIntentFilter = null;
+            mHandler = null;
+        }
+    }
+
+    public void verifyReceiverNotRegistered() {
+        assertThat(mIntentFilter).isNull();
+        assertThat(mReceiver).isNull();
+        assertThat(mHandler).isNull();
+    }
+
+    public void verifyReceiverRegistered(String expectedAction) {
+        assertThat(mIntentFilter.actionsIterator()).isNotNull();
+        ArrayList<String> actions = Lists.newArrayList(mIntentFilter.actionsIterator());
+        assertWithMessage("IntentFilter actions").that(actions).contains(expectedAction);
+        assertWithMessage("Registered BroadcastReceiver").that(mReceiver).isNotNull();
+    }
+}
diff --git a/service/src/com/android/car/CarLog.java b/service/src/com/android/car/CarLog.java
index 4585eda..3cec3bb 100644
--- a/service/src/com/android/car/CarLog.java
+++ b/service/src/com/android/car/CarLog.java
@@ -43,6 +43,7 @@
     public static final String TAG_SERVICE = "CAR.SERVICE";
     public static final String TAG_STORAGE = "CAR.STORAGE";
     public static final String TAG_TELEMETRY = "CAR.TELEMETRY";
+    public static final String TAG_TIME = "CAR.TIME";
     public static final String TAG_WATCHDOG = "CAR.WATCHDOG";
 
     /**
diff --git a/service/src/com/android/car/CarServiceUtils.java b/service/src/com/android/car/CarServiceUtils.java
index 2dddeeb..c4f77cf 100644
--- a/service/src/com/android/car/CarServiceUtils.java
+++ b/service/src/com/android/car/CarServiceUtils.java
@@ -154,8 +154,17 @@
     }
 
     public static float[] toFloatArray(List<Float> list) {
-        final int size = list.size();
-        final float[] array = new float[size];
+        int size = list.size();
+        float[] array = new float[size];
+        for (int i = 0; i < size; ++i) {
+            array[i] = list.get(i);
+        }
+        return array;
+    }
+
+    public static long[] toLongArray(List<Long> list) {
+        int size = list.size();
+        long[] array = new long[size];
         for (int i = 0; i < size; ++i) {
             array[i] = list.get(i);
         }
@@ -163,8 +172,8 @@
     }
 
     public static int[] toIntArray(List<Integer> list) {
-        final int size = list.size();
-        final int[] array = new int[size];
+        int size = list.size();
+        int[] array = new int[size];
         for (int i = 0; i < size; ++i) {
             array[i] = list.get(i);
         }
@@ -172,8 +181,8 @@
     }
 
     public static byte[] toByteArray(List<Byte> list) {
-        final int size = list.size();
-        final byte[] array = new byte[size];
+        int size = list.size();
+        byte[] array = new byte[size];
         for (int i = 0; i < size; ++i) {
             array[i] = list.get(i);
         }
diff --git a/service/src/com/android/car/hal/TimeHalService.java b/service/src/com/android/car/hal/TimeHalService.java
new file mode 100644
index 0000000..a166850
--- /dev/null
+++ b/service/src/com/android/car/hal/TimeHalService.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2021 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 static android.hardware.automotive.vehicle.V2_0.VehicleProperty.EPOCH_TIME;
+
+import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.automotive.vehicle.V2_0.VehicleArea;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropertyStatus;
+import android.util.IndentingPrintWriter;
+
+import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
+
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.List;
+
+/** Writes the Android System time to EPOCH_TIME in the VHAL, if supported. */
+public final class TimeHalService extends HalServiceBase {
+
+    private static final int[] SUPPORTED_PROPERTIES = new int[]{EPOCH_TIME};
+
+    private final Context mContext;
+
+    private final VehicleHal mHal;
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_TIME_CHANGED.equals(intent.getAction())) {
+                updateProperty(System.currentTimeMillis());
+            }
+        }
+    };
+
+    private boolean mReceiverRegistered;
+
+    @Nullable
+    private Instant mLastAndroidTimeReported;
+
+    private boolean mAndroidTimeSupported;
+
+    TimeHalService(Context context, VehicleHal hal) {
+        mContext = requireNonNull(context);
+        mHal = requireNonNull(hal);
+    }
+
+    @Override
+    public void init() {
+        if (!mAndroidTimeSupported) {
+            return;
+        }
+
+        updateProperty(System.currentTimeMillis());
+
+        IntentFilter filter = new IntentFilter(Intent.ACTION_TIME_CHANGED);
+        mContext.registerReceiver(mReceiver, filter);
+        mReceiverRegistered = true;
+    }
+
+    @Override
+    public void release() {
+        if (mReceiverRegistered) {
+            mContext.unregisterReceiver(mReceiver);
+            mReceiverRegistered = false;
+        }
+
+        mAndroidTimeSupported = false;
+        mLastAndroidTimeReported = null;
+    }
+
+    @Override
+    public int[] getAllSupportedProperties() {
+        return SUPPORTED_PROPERTIES;
+    }
+
+    @Override
+    public void takeProperties(Collection<VehiclePropConfig> properties) {
+        for (VehiclePropConfig property : properties) {
+            switch (property.prop) {
+                case EPOCH_TIME:
+                    mAndroidTimeSupported = true;
+                    return;
+            }
+        }
+    }
+
+    @Override
+    public void onHalEvents(List<VehiclePropValue> values) {
+    }
+
+    public boolean isAndroidTimeSupported() {
+        return mAndroidTimeSupported;
+    }
+
+    private void updateProperty(long timeMillis) {
+        VehiclePropValue propValue = new VehiclePropValue();
+        propValue.prop = EPOCH_TIME;
+        propValue.areaId = VehicleArea.GLOBAL;
+        propValue.status = VehiclePropertyStatus.AVAILABLE;
+        propValue.timestamp = timeMillis;
+        propValue.value.int64Values.add(timeMillis);
+
+        mHal.set(propValue);
+        mLastAndroidTimeReported = Instant.ofEpochMilli(timeMillis);
+    }
+
+    @Override
+    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
+    public void dump(PrintWriter printWriter) {
+        IndentingPrintWriter writer = new IndentingPrintWriter(printWriter);
+        writer.println("*ExternalTime HAL*");
+        writer.increaseIndent();
+        writer.printf(
+                "mLastAndroidTimeReported: %d millis",
+                mLastAndroidTimeReported.toEpochMilli());
+        writer.decreaseIndent();
+        writer.flush();
+    }
+}
diff --git a/service/src/com/android/car/hal/VehicleHal.java b/service/src/com/android/car/hal/VehicleHal.java
index 726f34f..6b67b8d 100644
--- a/service/src/com/android/car/hal/VehicleHal.java
+++ b/service/src/com/android/car/hal/VehicleHal.java
@@ -19,6 +19,7 @@
 import static com.android.car.CarServiceUtils.toByteArray;
 import static com.android.car.CarServiceUtils.toFloatArray;
 import static com.android.car.CarServiceUtils.toIntArray;
+import static com.android.car.CarServiceUtils.toLongArray;
 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
 
 import static java.lang.Integer.toHexString;
@@ -97,6 +98,7 @@
     private final DiagnosticHalService mDiagnosticHal;
     private final ClusterHalService mClusterHalService;
     private final EvsHalService mEvsHal;
+    private final TimeHalService mTimeHalService;
 
     private final Object mLock = new Object();
 
@@ -136,6 +138,8 @@
         mDiagnosticHal = new DiagnosticHalService(this);
         mClusterHalService = new ClusterHalService(this);
         mEvsHal = new EvsHalService(this);
+        mTimeHalService = new TimeHalService(context, this);
+        //TODO(b/202396546): Dedupe this assignment with the other one in constructor below
         mAllServices.addAll(Arrays.asList(mPowerHal,
                 mInputHal,
                 mDiagnosticHal,
@@ -143,6 +147,7 @@
                 mUserHal,
                 mClusterHalService,
                 mEvsHal,
+                mTimeHalService,
                 mPropertyHal)); // mPropertyHal should be the last.
         mHalClient = new HalClient(vehicle, mHandlerThread.getLooper(),
                 /* callback= */ this);
@@ -160,6 +165,7 @@
             UserHalService userHal,
             DiagnosticHalService diagnosticHal,
             ClusterHalService clusterHalService,
+            TimeHalService timeHalService,
             HalClient halClient,
             HandlerThread handlerThread) {
         mHandlerThread = handlerThread;
@@ -172,12 +178,14 @@
         mDiagnosticHal = diagnosticHal;
         mClusterHalService = clusterHalService;
         mEvsHal = new EvsHalService(this);
+        mTimeHalService = timeHalService;
         mAllServices.addAll(Arrays.asList(mPowerHal,
                 mInputHal,
                 mDiagnosticHal,
                 mVmsHal,
                 mUserHal,
                 mEvsHal,
+                mTimeHalService,
                 mPropertyHal));
         mHalClient = halClient;
     }
@@ -322,6 +330,10 @@
         return mEvsHal;
     }
 
+    public TimeHalService getTimeHalService() {
+        return mTimeHalService;
+    }
+
     private void assertServiceOwnerLocked(HalServiceBase service, int property) {
         if (service != mPropertyHandlers.get(property)) {
             throw new IllegalArgumentException("Property 0x" + toHexString(property)
@@ -517,18 +529,25 @@
         VehiclePropValue propValue;
         propValue = mHalClient.getValue(requestedPropValue);
 
-        if (clazz == Integer.class || clazz == int.class) {
+        if (clazz == Long.class || clazz == long.class) {
+            return (T) propValue.value.int64Values.get(0);
+        } else if (clazz == Integer.class || clazz == int.class) {
             return (T) propValue.value.int32Values.get(0);
         } else if (clazz == Boolean.class || clazz == boolean.class) {
             return (T) Boolean.valueOf(propValue.value.int32Values.get(0) == 1);
         } else if (clazz == Float.class || clazz == float.class) {
             return (T) propValue.value.floatValues.get(0);
+        } else if (clazz == Long[].class) {
+            Long[] longArray = new Long[propValue.value.int64Values.size()];
+            return (T) propValue.value.int32Values.toArray(longArray);
         } else if (clazz == Integer[].class) {
             Integer[] intArray = new Integer[propValue.value.int32Values.size()];
             return (T) propValue.value.int32Values.toArray(intArray);
         } else if (clazz == Float[].class) {
             Float[] floatArray = new Float[propValue.value.floatValues.size()];
             return (T) propValue.value.floatValues.toArray(floatArray);
+        } else if (clazz == long[].class) {
+            return (T) toLongArray(propValue.value.int64Values);
         } else if (clazz == int[].class) {
             return (T) toIntArray(propValue.value.int32Values);
         } else if (clazz == float[].class) {
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/MockedPowerHalService.java b/tests/carservice_unit_test/src/com/android/car/hal/MockedPowerHalService.java
index 2ab14dc..fd15435 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/MockedPowerHalService.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/MockedPowerHalService.java
@@ -49,6 +49,7 @@
                 mock(UserHalService.class),
                 mock(DiagnosticHalService.class),
                 mock(ClusterHalService.class),
+                mock(TimeHalService.class),
                 mock(HalClient.class),
                 CarServiceUtils.getHandlerThread(VehicleHal.class.getSimpleName()));
         return mockedVehicleHal;
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/TimeHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/TimeHalServiceTest.java
new file mode 100644
index 0000000..adc5153
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/hal/TimeHalServiceTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2021 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 static android.hardware.automotive.vehicle.V2_0.VehicleProperty.EPOCH_TIME;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.verify;
+
+import android.car.test.util.BroadcastingFakeContext;
+import android.car.test.util.VehicleHalTestingHelper;
+import android.content.Intent;
+import android.hardware.automotive.vehicle.V2_0.VehicleArea;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropertyStatus;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Collections;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class TimeHalServiceTest {
+
+    private static final VehiclePropConfig ANDROID_TIME_PROP =
+            VehicleHalTestingHelper.newConfig(EPOCH_TIME);
+
+    @Mock private VehicleHal mVehicleHal;
+    private BroadcastingFakeContext mFakeContext;
+
+    private TimeHalService mTimeHalService;
+
+    @Before
+    public void setUp() {
+        mFakeContext = new BroadcastingFakeContext();
+        mTimeHalService = new TimeHalService(mFakeContext, mVehicleHal);
+    }
+
+    @Test
+    public void testInitDoesNothing() {
+        mTimeHalService.takeProperties(Collections.emptyList());
+
+        mTimeHalService.init();
+
+        mFakeContext.verifyReceiverNotRegistered();
+    }
+
+    @Test
+    public void testInitRegistersBroadcastReceiver() {
+        mTimeHalService.takeProperties(Collections.singletonList(ANDROID_TIME_PROP));
+
+        mTimeHalService.init();
+
+        assertThat(mTimeHalService.isAndroidTimeSupported()).isTrue();
+        mFakeContext.verifyReceiverRegistered(Intent.ACTION_TIME_CHANGED);
+    }
+
+    @Test
+    public void testInitSendsAndroidTimeUpdate() {
+        mTimeHalService.takeProperties(Collections.singletonList(ANDROID_TIME_PROP));
+        long sysTimeMillis = System.currentTimeMillis();
+
+        mTimeHalService.init();
+
+        assertThat(mTimeHalService.isAndroidTimeSupported()).isTrue();
+        ArgumentCaptor<VehiclePropValue> captor = ArgumentCaptor.forClass(VehiclePropValue.class);
+        verify(mVehicleHal).set(captor.capture());
+        VehiclePropValue propValue = captor.getValue();
+        assertThat(propValue.prop).isEqualTo(EPOCH_TIME);
+        assertThat(propValue.areaId).isEqualTo(VehicleArea.GLOBAL);
+        assertThat(propValue.status).isEqualTo(VehiclePropertyStatus.AVAILABLE);
+        assertThat(propValue.timestamp).isAtLeast(sysTimeMillis);
+        assertThat(propValue.value.int64Values).hasSize(1);
+        assertThat(propValue.value.int64Values.get(0)).isAtLeast(sysTimeMillis);
+    }
+
+    @Test
+    public void testReleaseUnregistersBroadcastReceiver() {
+        mTimeHalService.takeProperties(Collections.singletonList(ANDROID_TIME_PROP));
+        mTimeHalService.init();
+        clearInvocations(mVehicleHal);
+
+        mTimeHalService.release();
+
+        mFakeContext.verifyReceiverNotRegistered();
+        assertThat(mTimeHalService.isAndroidTimeSupported()).isFalse();
+    }
+
+    @Test
+    public void testSendsAndroidTimeUpdateWhenBroadcast() {
+        mTimeHalService.takeProperties(Collections.singletonList(ANDROID_TIME_PROP));
+        mTimeHalService.init();
+        clearInvocations(mVehicleHal);
+        long sysTimeMillis = System.currentTimeMillis();
+
+        mFakeContext.sendBroadcast(new Intent(Intent.ACTION_TIME_CHANGED));
+
+        ArgumentCaptor<VehiclePropValue> captor = ArgumentCaptor.forClass(VehiclePropValue.class);
+        verify(mVehicleHal).set(captor.capture());
+        VehiclePropValue propValue = captor.getValue();
+        assertThat(propValue.prop).isEqualTo(EPOCH_TIME);
+        assertThat(propValue.areaId).isEqualTo(VehicleArea.GLOBAL);
+        assertThat(propValue.status).isEqualTo(VehiclePropertyStatus.AVAILABLE);
+        assertThat(propValue.timestamp).isAtLeast(sysTimeMillis);
+        assertThat(propValue.value.int64Values).hasSize(1);
+        assertThat(propValue.value.int64Values.get(0)).isAtLeast(sysTimeMillis);
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/VehicleHalTest.java b/tests/carservice_unit_test/src/com/android/car/hal/VehicleHalTest.java
index 4aa9a81..2a39046 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/VehicleHalTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/VehicleHalTest.java
@@ -71,6 +71,7 @@
     @Mock private UserHalService mUserHalService;
     @Mock private DiagnosticHalService mDiagnosticHalService;
     @Mock private ClusterHalService mClusterHalService;
+    @Mock private TimeHalService mTimeHalService;
     @Mock private HalClient mHalClient;
 
     private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
@@ -86,7 +87,8 @@
     public void setUp() throws Exception {
         mVehicleHal = new VehicleHal(mPowerHalService,
                 mPropertyHalService, mInputHalService, mVmsHalService, mUserHalService,
-                mDiagnosticHalService, mClusterHalService, mHalClient, mHandlerThread);
+                mDiagnosticHalService, mClusterHalService, mTimeHalService, mHalClient,
+                mHandlerThread);
 
         mConfigs.clear();