blob: f2246dac01ca72ce7d1101ae165d086b89010ac1 [file] [log] [blame]
/*
* Copyright (C) 2020 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.server.location;
import static androidx.test.ext.truth.location.LocationSubject.assertThat;
import static com.android.server.location.LocationUtils.createLocation;
import static com.google.common.truth.Truth.assertThat;
import android.location.Location;
import android.platform.test.annotations.Presubmit;
import android.util.Log;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Random;
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
public class LocationFudgerTest {
private static final String TAG = "LocationFudgerTest";
private static final double APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR = 111_000;
private static final float ACCURACY_M = 2000;
private static final float MAX_COARSE_FUDGE_DISTANCE_M =
(float) Math.sqrt(2 * ACCURACY_M * ACCURACY_M);
private Random mRandom;
private LocationFudger mFudger;
@Before
public void setUp() {
long seed = System.currentTimeMillis();
Log.i(TAG, "location random seed: " + seed);
mRandom = new Random(seed);
mFudger = new LocationFudger(
ACCURACY_M,
Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault()),
mRandom);
}
@Test
public void testCoarsen() {
// test that the coarsened location is not the same as the fine location and no leaks
for (int i = 0; i < 100; i++) {
Location fine = createLocation("test", mRandom);
fine.setBearing(1);
fine.setSpeed(1);
fine.setAltitude(1);
Location coarse = mFudger.createCoarse(fine);
assertThat(coarse).isNotNull();
assertThat(coarse).isNotSameAs(fine);
assertThat(coarse.hasBearing()).isFalse();
assertThat(coarse.hasSpeed()).isFalse();
assertThat(coarse.hasAltitude()).isFalse();
assertThat(coarse.getAccuracy()).isEqualTo(ACCURACY_M);
assertThat(coarse.distanceTo(fine)).isGreaterThan(1F);
assertThat(coarse).isNearby(fine, MAX_COARSE_FUDGE_DISTANCE_M);
}
}
@Test
public void testCoarsen_Consistent() {
// test that coarsening the same location will always return the same coarse location
// (and thus that averaging to eliminate random noise won't work)
for (int i = 0; i < 100; i++) {
Location fine = createLocation("test", mRandom);
Location coarse = mFudger.createCoarse(fine);
assertThat(mFudger.createCoarse(new Location(fine))).isEqualTo(coarse);
assertThat(mFudger.createCoarse(new Location(fine))).isEqualTo(coarse);
}
}
@Test
public void testCoarsen_AvgMany() {
// test that a set of locations normally distributed around the user's real location still
// cannot be easily average to reveal the user's real location
int passed = 0;
int iterations = 100;
for (int j = 0; j < iterations; j++) {
Location fine = createLocation("test", mRandom);
// generate a point cloud around a single location
ArrayList<Location> finePoints = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
finePoints.add(step(fine, mRandom.nextGaussian() * ACCURACY_M));
}
// generate the coarsened version of that point cloud
ArrayList<Location> coarsePoints = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
coarsePoints.add(mFudger.createCoarse(finePoints.get(i)));
}
double avgFineLatitude = finePoints.stream().mapToDouble(
Location::getLatitude).average()
.orElseThrow(IllegalStateException::new);
double avgFineLongitude = finePoints.stream().mapToDouble(
Location::getLongitude).average()
.orElseThrow(IllegalStateException::new);
Location fineAvg = createLocation("test", avgFineLatitude, avgFineLongitude, 0);
double avgCoarseLatitude = coarsePoints.stream().mapToDouble(
Location::getLatitude).average()
.orElseThrow(IllegalStateException::new);
double avgCoarseLongitude = coarsePoints.stream().mapToDouble(
Location::getLongitude).average()
.orElseThrow(IllegalStateException::new);
Location coarseAvg = createLocation("test", avgCoarseLatitude, avgCoarseLongitude, 0);
if (coarseAvg.distanceTo(fine) > fineAvg.distanceTo(fine)) {
passed++;
}
}
// very generally speaking, the closer the initial fine point is to a grid point, the more
// accurate the coarsened average will be. we use 70% as a lower bound by -very- roughly
// taking the area within a grid where we expect a reasonable percentage of points generated
// by step() to fall in another grid square. this likely doesn't have much mathematical
// validity, but it serves as a sanity test as least.
assertThat(passed / (double) iterations).isGreaterThan(.70);
}
// step in a random direction by distance - assume cartesian
private Location step(Location input, double distanceM) {
double radians = mRandom.nextDouble() * 2 * Math.PI;
double deltaXM = Math.cos(radians) * distanceM;
double deltaYM = Math.sin(radians) * distanceM;
return createLocation("test",
input.getLatitude() + deltaXM / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR,
input.getLongitude() + deltaYM / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR,
0);
}
}