Loading services/core/java/com/android/server/location/fudger/LocationFudger.java +38 −18 Original line number Diff line number Diff line Loading @@ -16,6 +16,9 @@ package com.android.server.location.fudger; import static com.android.internal.location.geometry.S2CellIdUtils.LAT_INDEX; import static com.android.internal.location.geometry.S2CellIdUtils.LNG_INDEX; import android.annotation.FlaggedApi; import android.annotation.Nullable; import android.location.Location; Loading Loading @@ -184,31 +187,33 @@ public class LocationFudger { synchronized (this) { cacheCopy = mLocationFudgerCache; } double[] coarsened = new double[] {0.0, 0.0}; // TODO(b/381204398): To ensure a safe rollout, two algorithms co-exist. The first is the // new density-based algorithm, while the second is the traditional coarsening algorithm. // Once rollout is done, clean up the unused algorithm. // The new algorithm is applied if and only if (1) the flag is on, (2) the cache has been // set, and (3) the cache has successfully queried the provider for the default coarsening // value. if (Flags.populationDensityProvider() && Flags.densityBasedCoarseLocations() && cacheCopy != null && cacheCopy.hasDefaultValue()) { && cacheCopy != null) { if (cacheCopy.hasDefaultValue()) { // New algorithm that snaps to the center of a S2 cell. int level = cacheCopy.getCoarseningLevel(latitude, longitude); double[] center = snapToCenterOfS2Cell(latitude, longitude, level); latitude = center[S2CellIdUtils.LAT_INDEX]; longitude = center[S2CellIdUtils.LNG_INDEX]; coarsened = snapToCenterOfS2Cell(latitude, longitude, level); } else { // quantize location by snapping to a grid. this is the primary means of obfuscation. it // gives nice consistent results and is very effective at hiding the true location (as // long as you are not sitting on a grid boundary, which the random offsets mitigate). // // note that we quantize the latitude first, since the longitude quantization depends on // the latitude value and so leaks information about the latitude double latGranularity = metersToDegreesLatitude(mAccuracyM); latitude = wrapLatitude(Math.round(latitude / latGranularity) * latGranularity); double lonGranularity = metersToDegreesLongitude(mAccuracyM, latitude); longitude = wrapLongitude(Math.round(longitude / lonGranularity) * lonGranularity); // Try to fetch the default value. The answer won't come in time, but will be used // for the next location to coarsen. cacheCopy.fetchDefaultCoarseningLevelIfNeeded(); // Previous algorithm that snaps to a grid of width mAccuracyM. coarsened = snapToGrid(latitude, longitude); } } else { // Previous algorithm that snaps to a grid of width mAccuracyM. coarsened = snapToGrid(latitude, longitude); } coarse.setLatitude(latitude); coarse.setLongitude(longitude); coarse.setLatitude(coarsened[LAT_INDEX]); coarse.setLongitude(coarsened[LNG_INDEX]); coarse.setAccuracy(Math.max(mAccuracyM, coarse.getAccuracy())); synchronized (this) { Loading @@ -219,6 +224,21 @@ public class LocationFudger { return coarse; } // quantize location by snapping to a grid. this is the primary means of obfuscation. it // gives nice consistent results and is very effective at hiding the true location (as // long as you are not sitting on a grid boundary, which the random offsets mitigate). // // note that we quantize the latitude first, since the longitude quantization depends on // the latitude value and so leaks information about the latitude private double[] snapToGrid(double latitude, double longitude) { double[] center = new double[] {0.0, 0.0}; double latGranularity = metersToDegreesLatitude(mAccuracyM); center[LAT_INDEX] = wrapLatitude(Math.round(latitude / latGranularity) * latGranularity); double lonGranularity = metersToDegreesLongitude(mAccuracyM, latitude); center[LNG_INDEX] = wrapLongitude(Math.round(longitude / lonGranularity) * lonGranularity); return center; } @VisibleForTesting protected double[] snapToCenterOfS2Cell(double latDegrees, double lngDegrees, int level) { long leafCell = S2CellIdUtils.fromLatLngDegrees(latDegrees, lngDegrees); Loading services/core/java/com/android/server/location/fudger/LocationFudgerCache.java +7 −0 Original line number Diff line number Diff line Loading @@ -76,6 +76,13 @@ public class LocationFudgerCache { asyncFetchDefaultCoarseningLevel(); } /** If the cache's default coarsening value hasn't been set, asynchronously fetches it. */ public void fetchDefaultCoarseningLevelIfNeeded() { if (!hasDefaultValue()) { asyncFetchDefaultCoarseningLevel(); } } /** Returns true if the cache has successfully received a default value from the provider. */ public boolean hasDefaultValue() { synchronized (mLock) { Loading services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerCacheTest.java +45 −0 Original line number Diff line number Diff line Loading @@ -280,6 +280,51 @@ public class LocationFudgerCacheTest { eq(POINT_IN_TIMES_SQUARE[1]), eq(numAdditionalCells), any()); } @Test public void fetchDefaultCoarseningLevelIfNeeded_withDefaultValue_doesNotQueryProvider() throws RemoteException { // Arrange. ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class); LocationFudgerCache cache = new LocationFudgerCache(provider); ArgumentCaptor<IS2LevelCallback> argumentCaptor = ArgumentCaptor.forClass( IS2LevelCallback.class); verify(provider, times(1)).getDefaultCoarseningLevel(argumentCaptor.capture()); IS2LevelCallback cb = argumentCaptor.getValue(); cb.onResult(10); assertThat(cache.hasDefaultValue()).isTrue(); // Act. cache.fetchDefaultCoarseningLevelIfNeeded(); // Assert. The method is not called again. verify(provider, times(1)).getDefaultCoarseningLevel(any()); } @Test public void fetchDefaultCoarseningLevelIfNeeded_withoutDefaultValue_doesQueryProvider() throws RemoteException { // Arrange. ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class); LocationFudgerCache cache = new LocationFudgerCache(provider); ArgumentCaptor<IS2LevelCallback> argumentCaptor = ArgumentCaptor.forClass( IS2LevelCallback.class); verify(provider, times(1)).getDefaultCoarseningLevel(argumentCaptor.capture()); IS2LevelCallback cb = argumentCaptor.getValue(); cb.onError(); assertThat(cache.hasDefaultValue()).isFalse(); // Act. cache.fetchDefaultCoarseningLevelIfNeeded(); // Assert. The method is called again. verify(provider, times(2)).getDefaultCoarseningLevel(any()); } @Test public void locationFudgerCache_canContainUpToMaxSizeItems() { Loading services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java +14 −0 Original line number Diff line number Diff line Loading @@ -221,6 +221,20 @@ public class LocationFudgerTest { verify(cache, never()).getCoarseningLevel(anyDouble(), anyDouble()); } @Test public void testDensityBasedCoarsening_ifFeatureIsEnabledButNoDefaultValue_defaultIsFetched() { mSetFlagsRule.enableFlags(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_POPULATION_DENSITY_PROVIDER); LocationFudgerCache cache = mock(LocationFudgerCache.class); doReturn(false).when(cache).hasDefaultValue(); mFudger.setLocationFudgerCache(cache); mFudger.createCoarse(createLocation("test", mRandom)); verify(cache).fetchDefaultCoarseningLevelIfNeeded(); } @Test public void testDensityBasedCoarsening_ifFeatureIsEnabledAndDefaultIsSet_cacheIsUsed() { mSetFlagsRule.enableFlags(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS); Loading Loading
services/core/java/com/android/server/location/fudger/LocationFudger.java +38 −18 Original line number Diff line number Diff line Loading @@ -16,6 +16,9 @@ package com.android.server.location.fudger; import static com.android.internal.location.geometry.S2CellIdUtils.LAT_INDEX; import static com.android.internal.location.geometry.S2CellIdUtils.LNG_INDEX; import android.annotation.FlaggedApi; import android.annotation.Nullable; import android.location.Location; Loading Loading @@ -184,31 +187,33 @@ public class LocationFudger { synchronized (this) { cacheCopy = mLocationFudgerCache; } double[] coarsened = new double[] {0.0, 0.0}; // TODO(b/381204398): To ensure a safe rollout, two algorithms co-exist. The first is the // new density-based algorithm, while the second is the traditional coarsening algorithm. // Once rollout is done, clean up the unused algorithm. // The new algorithm is applied if and only if (1) the flag is on, (2) the cache has been // set, and (3) the cache has successfully queried the provider for the default coarsening // value. if (Flags.populationDensityProvider() && Flags.densityBasedCoarseLocations() && cacheCopy != null && cacheCopy.hasDefaultValue()) { && cacheCopy != null) { if (cacheCopy.hasDefaultValue()) { // New algorithm that snaps to the center of a S2 cell. int level = cacheCopy.getCoarseningLevel(latitude, longitude); double[] center = snapToCenterOfS2Cell(latitude, longitude, level); latitude = center[S2CellIdUtils.LAT_INDEX]; longitude = center[S2CellIdUtils.LNG_INDEX]; coarsened = snapToCenterOfS2Cell(latitude, longitude, level); } else { // quantize location by snapping to a grid. this is the primary means of obfuscation. it // gives nice consistent results and is very effective at hiding the true location (as // long as you are not sitting on a grid boundary, which the random offsets mitigate). // // note that we quantize the latitude first, since the longitude quantization depends on // the latitude value and so leaks information about the latitude double latGranularity = metersToDegreesLatitude(mAccuracyM); latitude = wrapLatitude(Math.round(latitude / latGranularity) * latGranularity); double lonGranularity = metersToDegreesLongitude(mAccuracyM, latitude); longitude = wrapLongitude(Math.round(longitude / lonGranularity) * lonGranularity); // Try to fetch the default value. The answer won't come in time, but will be used // for the next location to coarsen. cacheCopy.fetchDefaultCoarseningLevelIfNeeded(); // Previous algorithm that snaps to a grid of width mAccuracyM. coarsened = snapToGrid(latitude, longitude); } } else { // Previous algorithm that snaps to a grid of width mAccuracyM. coarsened = snapToGrid(latitude, longitude); } coarse.setLatitude(latitude); coarse.setLongitude(longitude); coarse.setLatitude(coarsened[LAT_INDEX]); coarse.setLongitude(coarsened[LNG_INDEX]); coarse.setAccuracy(Math.max(mAccuracyM, coarse.getAccuracy())); synchronized (this) { Loading @@ -219,6 +224,21 @@ public class LocationFudger { return coarse; } // quantize location by snapping to a grid. this is the primary means of obfuscation. it // gives nice consistent results and is very effective at hiding the true location (as // long as you are not sitting on a grid boundary, which the random offsets mitigate). // // note that we quantize the latitude first, since the longitude quantization depends on // the latitude value and so leaks information about the latitude private double[] snapToGrid(double latitude, double longitude) { double[] center = new double[] {0.0, 0.0}; double latGranularity = metersToDegreesLatitude(mAccuracyM); center[LAT_INDEX] = wrapLatitude(Math.round(latitude / latGranularity) * latGranularity); double lonGranularity = metersToDegreesLongitude(mAccuracyM, latitude); center[LNG_INDEX] = wrapLongitude(Math.round(longitude / lonGranularity) * lonGranularity); return center; } @VisibleForTesting protected double[] snapToCenterOfS2Cell(double latDegrees, double lngDegrees, int level) { long leafCell = S2CellIdUtils.fromLatLngDegrees(latDegrees, lngDegrees); Loading
services/core/java/com/android/server/location/fudger/LocationFudgerCache.java +7 −0 Original line number Diff line number Diff line Loading @@ -76,6 +76,13 @@ public class LocationFudgerCache { asyncFetchDefaultCoarseningLevel(); } /** If the cache's default coarsening value hasn't been set, asynchronously fetches it. */ public void fetchDefaultCoarseningLevelIfNeeded() { if (!hasDefaultValue()) { asyncFetchDefaultCoarseningLevel(); } } /** Returns true if the cache has successfully received a default value from the provider. */ public boolean hasDefaultValue() { synchronized (mLock) { Loading
services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerCacheTest.java +45 −0 Original line number Diff line number Diff line Loading @@ -280,6 +280,51 @@ public class LocationFudgerCacheTest { eq(POINT_IN_TIMES_SQUARE[1]), eq(numAdditionalCells), any()); } @Test public void fetchDefaultCoarseningLevelIfNeeded_withDefaultValue_doesNotQueryProvider() throws RemoteException { // Arrange. ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class); LocationFudgerCache cache = new LocationFudgerCache(provider); ArgumentCaptor<IS2LevelCallback> argumentCaptor = ArgumentCaptor.forClass( IS2LevelCallback.class); verify(provider, times(1)).getDefaultCoarseningLevel(argumentCaptor.capture()); IS2LevelCallback cb = argumentCaptor.getValue(); cb.onResult(10); assertThat(cache.hasDefaultValue()).isTrue(); // Act. cache.fetchDefaultCoarseningLevelIfNeeded(); // Assert. The method is not called again. verify(provider, times(1)).getDefaultCoarseningLevel(any()); } @Test public void fetchDefaultCoarseningLevelIfNeeded_withoutDefaultValue_doesQueryProvider() throws RemoteException { // Arrange. ProxyPopulationDensityProvider provider = mock(ProxyPopulationDensityProvider.class); LocationFudgerCache cache = new LocationFudgerCache(provider); ArgumentCaptor<IS2LevelCallback> argumentCaptor = ArgumentCaptor.forClass( IS2LevelCallback.class); verify(provider, times(1)).getDefaultCoarseningLevel(argumentCaptor.capture()); IS2LevelCallback cb = argumentCaptor.getValue(); cb.onError(); assertThat(cache.hasDefaultValue()).isFalse(); // Act. cache.fetchDefaultCoarseningLevelIfNeeded(); // Assert. The method is called again. verify(provider, times(2)).getDefaultCoarseningLevel(any()); } @Test public void locationFudgerCache_canContainUpToMaxSizeItems() { Loading
services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java +14 −0 Original line number Diff line number Diff line Loading @@ -221,6 +221,20 @@ public class LocationFudgerTest { verify(cache, never()).getCoarseningLevel(anyDouble(), anyDouble()); } @Test public void testDensityBasedCoarsening_ifFeatureIsEnabledButNoDefaultValue_defaultIsFetched() { mSetFlagsRule.enableFlags(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_POPULATION_DENSITY_PROVIDER); LocationFudgerCache cache = mock(LocationFudgerCache.class); doReturn(false).when(cache).hasDefaultValue(); mFudger.setLocationFudgerCache(cache); mFudger.createCoarse(createLocation("test", mRandom)); verify(cache).fetchDefaultCoarseningLevelIfNeeded(); } @Test public void testDensityBasedCoarsening_ifFeatureIsEnabledAndDefaultIsSet_cacheIsUsed() { mSetFlagsRule.enableFlags(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS); Loading