[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();