blob: b071e9947ddf9bdd47d673ab6b07602f83a877f5 [file] [log] [blame]
/*
* Copyright (C) 2018 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;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.car.hardware.CarPropertyValue;
import android.car.hardware.CarSensorEvent;
import android.car.hardware.CarSensorManager;
import android.car.hardware.property.CarPropertyEvent;
import android.car.hardware.property.ICarPropertyEventListener;
import android.car.user.CarUserManagerHelper;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationManager;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import com.android.internal.util.ArrayUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
/**
* This class contains unit tests for the {@link CarLocationService}.
* It tests that {@link LocationManager}'s last known location is stored in and loaded from a JSON
* file upon appropriate system events.
*
* The following mocks are used:
* 1. {@link Context} provides files and a mocked {@link LocationManager}.
* 2. {@link LocationManager} provides dummy {@link Location}s.
* 3. {@link CarPropertyService} registers a listener for ignition state events.
* 3. {@link CarPowerManagementService} registers a handler for power events.
* 4. {@link CarUserManagerHelper} tells whether or not the system user is headless.
*/
@RunWith(AndroidJUnit4.class)
public class CarLocationServiceTest {
private static String TAG = "CarLocationServiceTest";
private static String TEST_FILENAME = "location_cache_test.json";
private CarLocationService mCarLocationService;
private Context mContext;
private CountDownLatch mLatch;
@Mock private Context mMockContext;
@Mock private LocationManager mMockLocationManager;
@Mock private CarPropertyService mMockCarPropertyService;
@Mock private CarPowerManagementService mMockCarPowerManagementService;
@Mock private CarUserManagerHelper mMockCarUserManagerHelper;
/**
* Initialize all of the objects with the @Mock annotation.
*/
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext = InstrumentationRegistry.getTargetContext();
mLatch = new CountDownLatch(1);
mCarLocationService = new CarLocationService(mMockContext, mMockCarPowerManagementService,
mMockCarPropertyService, mMockCarUserManagerHelper) {
@Override
void asyncOperation(Runnable operation) {
super.asyncOperation(() -> {
operation.run();
mLatch.countDown();
});
}
};
}
@After
public void tearDown() throws Exception {
if (mCarLocationService != null) {
mCarLocationService.release();
}
mContext.deleteFile(TEST_FILENAME);
}
/**
* Test that the {@link CarLocationService} has the permissions necessary to call the
* {@link LocationManager} injectLocation API.
*
* Note that this test will never fail even if the relevant permissions are removed from the
* manifest since {@link CarService} runs in a system process.
*/
@Test
public void testCarLocationServiceShouldHavePermissions() {
int fineLocationCheck =
mContext.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION);
int locationHardwareCheck =
mContext.checkSelfPermission(android.Manifest.permission.LOCATION_HARDWARE);
assertEquals(PackageManager.PERMISSION_GRANTED, fineLocationCheck);
assertEquals(PackageManager.PERMISSION_GRANTED, locationHardwareCheck);
}
/**
* Test that the {@link CarLocationService} registers to receive the locked boot completed
* intent and ignition sensor events upon initialization.
*/
@Test
public void testRegistersToReceiveEvents() {
ArgumentCaptor<IntentFilter> argument = ArgumentCaptor.forClass(IntentFilter.class);
mCarLocationService.init();
verify(mMockCarPowerManagementService).registerPowerEventProcessingHandler(
mCarLocationService);
verify(mMockContext).registerReceiver(eq(mCarLocationService), argument.capture());
IntentFilter intentFilter = argument.getValue();
assertEquals(4, intentFilter.countActions());
String[] actions = {intentFilter.getAction(0), intentFilter.getAction(1),
intentFilter.getAction(2), intentFilter.getAction(3)};
assertTrue(ArrayUtils.contains(actions, Intent.ACTION_LOCKED_BOOT_COMPLETED));
assertTrue(ArrayUtils.contains(actions, LocationManager.MODE_CHANGED_ACTION));
assertTrue(ArrayUtils.contains(actions, LocationManager.GPS_ENABLED_CHANGE_ACTION));
assertTrue(ArrayUtils.contains(actions, Intent.ACTION_USER_SWITCHED));
verify(mMockCarPropertyService).registerListener(
eq(CarSensorManager.SENSOR_TYPE_IGNITION_STATE), eq(0.0f), any());
}
/**
* Test that the {@link CarLocationService} unregisters its event receivers.
*/
@Test
public void testUnregistersEventReceivers() {
mCarLocationService.release();
verify(mMockContext).unregisterReceiver(mCarLocationService);
verify(mMockCarPropertyService).unregisterListener(
eq(CarSensorManager.SENSOR_TYPE_IGNITION_STATE), any());
}
/**
* Test that the {@link CarLocationService} parses a location from a JSON serialization and then
* injects it into the {@link LocationManager} upon boot complete if the system user is not
* headless.
*/
@Test
public void testLoadsLocationOnLockedBootComplete() throws IOException, InterruptedException {
long currentTime = System.currentTimeMillis();
long elapsedTime = SystemClock.elapsedRealtimeNanos();
long pastTime = currentTime - 60000;
writeCacheFile("{\"provider\": \"gps\", \"latitude\": 16.7666, \"longitude\": 3.0026,"
+ "\"accuracy\":12.3, \"captureTime\": " + pastTime + "}");
ArgumentCaptor<Location> argument = ArgumentCaptor.forClass(Location.class);
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.injectLocation(argument.capture())).thenReturn(true);
when(mMockContext.getFileStreamPath("location_cache.json"))
.thenReturn(mContext.getFileStreamPath(TEST_FILENAME));
when(mMockCarUserManagerHelper.isHeadlessSystemUser()).thenReturn(false);
mCarLocationService.onReceive(mMockContext,
new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED));
mLatch.await();
Location location = argument.getValue();
assertEquals("gps", location.getProvider());
assertEquals(16.7666, location.getLatitude());
assertEquals(3.0026, location.getLongitude());
assertEquals(12.3f, location.getAccuracy());
assertTrue(location.getTime() >= currentTime);
assertTrue(location.getElapsedRealtimeNanos() >= elapsedTime);
}
/**
* Test that the {@link CarLocationService} parses a location from a JSON seialization and then
* injects it into the {@link LocationManager} upon user switch if the system user is headless.
*/
@Test
public void testLoadsLocationWithHeadlessSystemUser() throws IOException, InterruptedException {
long currentTime = System.currentTimeMillis();
long elapsedTime = SystemClock.elapsedRealtimeNanos();
long pastTime = currentTime - 60000;
writeCacheFile("{\"provider\": \"gps\", \"latitude\": 16.7666, \"longitude\": 3.0026,"
+ "\"accuracy\":12.3, \"captureTime\": " + pastTime + "}");
ArgumentCaptor<Location> argument = ArgumentCaptor.forClass(Location.class);
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.injectLocation(argument.capture())).thenReturn(true);
when(mMockContext.getFileStreamPath("location_cache.json"))
.thenReturn(mContext.getFileStreamPath(TEST_FILENAME));
when(mMockCarUserManagerHelper.isHeadlessSystemUser()).thenReturn(true);
Intent userSwitchedIntent = new Intent(Intent.ACTION_USER_SWITCHED);
userSwitchedIntent.putExtra(Intent.EXTRA_USER_HANDLE, 11);
mCarLocationService.onReceive(mMockContext, userSwitchedIntent);
mLatch.await();
Location location = argument.getValue();
assertEquals("gps", location.getProvider());
assertEquals(16.7666, location.getLatitude());
assertEquals(3.0026, location.getLongitude());
assertEquals(12.3f, location.getAccuracy());
assertTrue(location.getTime() >= currentTime);
assertTrue(location.getElapsedRealtimeNanos() >= elapsedTime);
}
/**
* Test that the {@link CarLocationService} does not inject a location if there is no location
* cache file.
*/
@Test
public void testDoesNotLoadLocationWhenNoFileExists()
throws FileNotFoundException, InterruptedException {
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER))
.thenReturn(null);
when(mMockContext.getFileStreamPath("location_cache.json"))
.thenReturn(mContext.getFileStreamPath(TEST_FILENAME));
mCarLocationService.onReceive(mMockContext,
new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED));
mLatch.await();
verify(mMockLocationManager, never()).injectLocation(any());
}
/**
* Test that the {@link CarLocationService} handles an incomplete JSON file gracefully.
*/
@Test
public void testDoesNotLoadLocationFromIncompleteFile() throws IOException,
InterruptedException {
writeCacheFile("{\"provider\": \"gps\", \"latitude\": 16.7666, \"longitude\": 3.0026,");
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER))
.thenReturn(null);
when(mMockContext.getFileStreamPath("location_cache.json"))
.thenReturn(mContext.getFileStreamPath(TEST_FILENAME));
mCarLocationService.onReceive(mMockContext,
new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED));
mLatch.await();
verify(mMockLocationManager, never()).injectLocation(any());
}
/**
* Test that the {@link CarLocationService} handles a corrupt JSON file gracefully.
*/
@Test
public void testDoesNotLoadLocationFromCorruptFile() throws IOException, InterruptedException {
writeCacheFile("{\"provider\":\"latitude\":16.7666,\"longitude\": \"accuracy\":1.0}");
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER))
.thenReturn(null);
when(mMockContext.getFileStreamPath("location_cache.json"))
.thenReturn(mContext.getFileStreamPath(TEST_FILENAME));
mCarLocationService.onReceive(mMockContext,
new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED));
mLatch.await();
verify(mMockLocationManager, never()).injectLocation(any());
}
/**
* Test that the {@link CarLocationService} does not inject a location that is missing
* accuracy.
*/
@Test
public void testDoesNotLoadIncompleteLocation() throws IOException, InterruptedException {
writeCacheFile("{\"provider\": \"gps\", \"latitude\": 16.7666, \"longitude\": 3.0026}");
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER))
.thenReturn(null);
when(mMockContext.getFileStreamPath("location_cache.json"))
.thenReturn(mContext.getFileStreamPath(TEST_FILENAME));
mCarLocationService.onReceive(mMockContext,
new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED));
mLatch.await();
verify(mMockLocationManager, never()).injectLocation(any());
}
/**
* Test that the {@link CarLocationService} does not inject a location that is older than
* thirty days.
*/
@Test
public void testDoesNotLoadOldLocation() throws IOException, InterruptedException {
long thirtyThreeDaysMs = 33 * 24 * 60 * 60 * 1000L;
long oldTime = System.currentTimeMillis() - thirtyThreeDaysMs;
writeCacheFile("{\"provider\": \"gps\", \"latitude\": 16.7666, \"longitude\": 3.0026,"
+ "\"accuracy\":12.3, \"captureTime\": " + oldTime + "}");
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER))
.thenReturn(null);
when(mMockContext.getFileStreamPath("location_cache.json"))
.thenReturn(mContext.getFileStreamPath(TEST_FILENAME));
mCarLocationService.onReceive(mMockContext,
new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED));
mLatch.await();
verify(mMockLocationManager, never()).injectLocation(any());
}
/**
* Test that the {@link CarLocationService} stores the {@link LocationManager}'s last known
* location in a JSON file upon ignition-off events.
*/
@Test
public void testStoresLocationUponIgnitionOff()
throws IOException, RemoteException, InterruptedException {
long currentTime = System.currentTimeMillis();
long elapsedTime = SystemClock.elapsedRealtimeNanos();
Location timbuktu = new Location(LocationManager.GPS_PROVIDER);
timbuktu.setLatitude(16.7666);
timbuktu.setLongitude(3.0026);
timbuktu.setAccuracy(13.75f);
timbuktu.setTime(currentTime);
timbuktu.setElapsedRealtimeNanos(elapsedTime);
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER))
.thenReturn(timbuktu);
when(mMockContext.getFileStreamPath("location_cache.json"))
.thenReturn(mContext.getFileStreamPath(TEST_FILENAME));
sendIgnitionOffEvent();
mLatch.await();
verify(mMockLocationManager).getLastKnownLocation(LocationManager.GPS_PROVIDER);
String actualContents = readCacheFile();
long oneDayMs = 24 * 60 * 60 * 1000;
long granularCurrentTime = (currentTime / oneDayMs) * oneDayMs;
String expectedContents = "{\"provider\":\"gps\",\"latitude\":16.7666,\"longitude\":"
+ "3.0026,\"accuracy\":13.75,\"captureTime\":" + granularCurrentTime + "}";
assertEquals(expectedContents, actualContents);
}
/**
* Test that the {@link CarLocationService} stores the {@link LocationManager}'s last known
* location upon prepare-shutdown events.
*/
@Test
public void testStoresLocationUponPrepareShutdown()
throws IOException, RemoteException, InterruptedException {
long currentTime = System.currentTimeMillis();
long elapsedTime = SystemClock.elapsedRealtimeNanos();
Location timbuktu = new Location(LocationManager.GPS_PROVIDER);
timbuktu.setLatitude(16.7666);
timbuktu.setLongitude(3.0026);
timbuktu.setAccuracy(13.75f);
timbuktu.setTime(currentTime);
timbuktu.setElapsedRealtimeNanos(elapsedTime);
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER))
.thenReturn(timbuktu);
when(mMockContext.getFileStreamPath("location_cache.json"))
.thenReturn(mContext.getFileStreamPath(TEST_FILENAME));
mCarLocationService.onPrepareShutdown(true);
mLatch.await();
verify(mMockLocationManager).getLastKnownLocation(LocationManager.GPS_PROVIDER);
String actualContents = readCacheFile();
long oneDayMs = 24 * 60 * 60 * 1000;
long granularCurrentTime = (currentTime / oneDayMs) * oneDayMs;
String expectedContents = "{\"provider\":\"gps\",\"latitude\":16.7666,\"longitude\":"
+ "3.0026,\"accuracy\":13.75,\"captureTime\":" + granularCurrentTime + "}";
assertEquals(expectedContents, actualContents);
}
/**
* Test that the {@link CarLocationService} does not store null locations.
*/
@Test
public void testDoesNotStoreNullLocation() throws Exception {
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER))
.thenReturn(null);
when(mMockContext.getFileStreamPath("location_cache.json"))
.thenReturn(mContext.getFileStreamPath(TEST_FILENAME));
sendIgnitionOffEvent();
mLatch.await();
verify(mMockLocationManager).getLastKnownLocation(LocationManager.GPS_PROVIDER);
verify(mMockContext).deleteFile("location_cache.json");
}
/**
* Test that the {@link CarLocationService} deletes location_cache.json when location is
* disabled.
*/
@Test
public void testDeletesCacheFileWhenLocationIsDisabled() throws Exception {
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.isLocationEnabled()).thenReturn(false);
mCarLocationService.init();
mCarLocationService.onReceive(mMockContext,
new Intent(LocationManager.MODE_CHANGED_ACTION));
mLatch.await();
verify(mMockLocationManager, times(1)).isLocationEnabled();
verify(mMockContext).deleteFile("location_cache.json");
}
/**
* Test that the {@link CarLocationService} deletes location_cache.json when the GPS location
* provider is disabled.
*/
@Test
public void testDeletesCacheFileWhenTheGPSProviderIsDisabled() throws Exception {
when(mMockContext.getSystemService(Context.LOCATION_SERVICE))
.thenReturn(mMockLocationManager);
when(mMockLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)).thenReturn(
false);
mCarLocationService.init();
mCarLocationService.onReceive(mMockContext,
new Intent(LocationManager.GPS_ENABLED_CHANGE_ACTION));
mLatch.await();
verify(mMockLocationManager, times(1))
.isProviderEnabled(LocationManager.GPS_PROVIDER);
verify(mMockContext).deleteFile("location_cache.json");
}
private void writeCacheFile(String json) throws IOException {
FileOutputStream fos = mContext.openFileOutput(TEST_FILENAME, Context.MODE_PRIVATE);
fos.write(json.getBytes());
fos.close();
}
private String readCacheFile() throws IOException {
FileInputStream fis = mContext.openFileInput(TEST_FILENAME);
String json = new BufferedReader(new InputStreamReader(fis)).lines()
.parallel().collect(Collectors.joining("\n"));
fis.close();
return json;
}
private void sendIgnitionOffEvent() throws RemoteException {
mCarLocationService.init();
ArgumentCaptor<ICarPropertyEventListener> argument =
ArgumentCaptor.forClass(ICarPropertyEventListener.class);
verify(mMockCarPropertyService).registerListener(
eq(CarSensorManager.SENSOR_TYPE_IGNITION_STATE), eq(0.0f), argument.capture());
ICarPropertyEventListener carPropertyEventListener = argument.getValue();
int intValues = CarSensorEvent.IGNITION_STATE_OFF;
CarPropertyValue ignitionOff = new CarPropertyValue(
CarSensorManager.SENSOR_TYPE_IGNITION_STATE, 0, 0,
System.currentTimeMillis(), intValues);
CarPropertyEvent event = new CarPropertyEvent(0, ignitionOff);
List<CarPropertyEvent> events = new ArrayList<>(Arrays.asList(event));
carPropertyEventListener.onEvent(events);
}
}