Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7749835f authored by Ludovic Barman's avatar Ludovic Barman
Browse files

Coarse locations based on population density.

At a high-level, this adds the option to coarsen (fudge) locations based on the population density at the device location. This feature is gated behind a flag.

In details:

Adds a new LocationFudgerCache to the existing LocationFudger. This cache is responsible for querying and storing values from the population density provider created in the other commits of this topic.
The cache is necessary because of the asynchronous nature of the provider, whereas the LocationFudger requires an immediate answer.
The cache is a simple fixed-size LIFO cache that stores S2CellIds, and returns the S2 level when a query corresponds to a cell in the cache.
If the cache doesn't hold a corresponding value, it returns a default value.

Changes the LocationFudger to use this cache and coarsen the location to the indicated S2 level.

Changes the LocationProviderManager and LocationManagerService to pass the new LocationFudgerCache to LocationFudger.

Tests:
- atest FrameworksMockingServicesTests:LocationManagerServiceTest
- atest FrameworksMockingServicesTests:LocationProviderManagerTest
- atest FrameworksMockingServicesTests:LocationFudgerTest
- atest FrameworksMockingServicesTests:LocationFudgerCacheTest

Test: manual atest on Pixel 7 pro (see above)
Bug: 376198890
Flag: android.location.flags.density_based_coarse_locations
Change-Id: I66d1d80fdb75e82781d09e787e8a2c17af092747
parent 4f699aab
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -108,6 +108,7 @@ import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.location.eventlog.LocationEventLog;
import com.android.server.location.fudger.LocationFudgerCache;
import com.android.server.location.geofence.GeofenceManager;
import com.android.server.location.geofence.GeofenceProxy;
import com.android.server.location.gnss.GnssConfiguration;
@@ -263,6 +264,9 @@ public class LocationManagerService extends ILocationManager.Stub implements

    private @Nullable ProxyPopulationDensityProvider mPopulationDensityProvider = null;

    // A cache for population density lookups. Used if density-based coarse locations are enabled.
    private @Nullable LocationFudgerCache mLocationFudgerCache = null;

    private final Object mDeprecatedGnssBatchingLock = new Object();
    @GuardedBy("mDeprecatedGnssBatchingLock")
    private @Nullable ILocationListener mDeprecatedGnssBatchingListener;
@@ -539,6 +543,9 @@ public class LocationManagerService extends ILocationManager.Stub implements
                Log.e(TAG, "no population density provider found");
            }
        }
        if (mPopulationDensityProvider != null && Flags.densityBasedCoarseLocations()) {
            setLocationFudgerCache(new LocationFudgerCache(mPopulationDensityProvider));
        }

        // bind to hardware activity recognition
        HardwareActivityRecognitionProxy hardwareActivityRecognitionProxy =
+53 −10
Original line number Diff line number Diff line
@@ -16,13 +16,16 @@

package com.android.server.location.fudger;

import android.annotation.FlaggedApi;
import android.annotation.Nullable;
import android.location.Location;
import android.location.LocationResult;
import android.location.flags.Flags;
import android.os.SystemClock;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.location.geometry.S2CellIdUtils;

import java.security.SecureRandom;
import java.time.Clock;
@@ -83,6 +86,9 @@ public class LocationFudger {
    @GuardedBy("this")
    @Nullable private LocationResult mCachedCoarseLocationResult;

    @GuardedBy("this")
    @Nullable private LocationFudgerCache mLocationFudgerCache = null;

    public LocationFudger(float accuracyM) {
        this(accuracyM, SystemClock.elapsedRealtimeClock(), new SecureRandom());
    }
@@ -96,6 +102,16 @@ public class LocationFudger {
        resetOffsets();
    }

    /**
     * Provides the optional {@link LocationFudgerCache} for coarsening based on population density.
     */
    @FlaggedApi(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS)
    public void setLocationFudgerCache(LocationFudgerCache cache) {
        synchronized (this) {
            mLocationFudgerCache = cache;
        }
    }

    /**
     * Resets the random offsets completely.
     */
@@ -162,16 +178,34 @@ public class LocationFudger {
        longitude += wrapLongitude(metersToDegreesLongitude(mLongitudeOffsetM, latitude));
        latitude += wrapLatitude(metersToDegreesLatitude(mLatitudeOffsetM));

        // We copy a reference to the cache, so even if mLocationFudgerCache is concurrently set
        // to null, we can continue executing the condition below.
        LocationFudgerCache cacheCopy = null;
        synchronized (this) {
            cacheCopy = mLocationFudgerCache;
        }

        // 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.
        if (Flags.densityBasedCoarseLocations() && cacheCopy != null
                && cacheCopy.hasDefaultValue()) {
            int level = cacheCopy.getCoarseningLevel(latitude, longitude);
            double[] center = snapToCenterOfS2Cell(latitude, longitude, level);
            latitude = center[S2CellIdUtils.LAT_INDEX];
            longitude = center[S2CellIdUtils.LNG_INDEX];
        } 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).
            // 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
            // 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);
        }

        coarse.setLatitude(latitude);
        coarse.setLongitude(longitude);
@@ -185,6 +219,15 @@ public class LocationFudger {
        return coarse;
    }

    @VisibleForTesting
    protected double[] snapToCenterOfS2Cell(double latDegrees, double lngDegrees, int level) {
        long leafCell = S2CellIdUtils.fromLatLngDegrees(latDegrees, lngDegrees);
        long coarsenedCell = S2CellIdUtils.getParent(leafCell, level);
        double[] center = new double[] {0.0, 0.0};
        S2CellIdUtils.toLatLngDegrees(coarsenedCell, center);
        return center;
    }

    /**
     * Update the random offsets over time.
     *
+199 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.fudger;

import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.location.flags.Flags;
import android.location.provider.IS2CellIdsCallback;
import android.location.provider.IS2LevelCallback;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.location.geometry.S2CellIdUtils;
import com.android.server.location.provider.proxy.ProxyPopulationDensityProvider;

import java.util.Objects;

/**
 * A cache for returning the coarsening level to be used. The coarsening level depends on the user
 * location. If the cache contains the requested latitude/longitude, the s2 level of the cached
 * cell id is returned. If not, a default value is returned.
 * This class has a {@link ProxyPopulationDensityProvider} used to refresh the cache.
 * This cache exists because {@link ProxyPopulationDensityProvider} must be queried asynchronously,
 * whereas a synchronous answer is needed.
 * The cache is first-in, first-out, and has a fixed size. Cache entries are valid until evicted by
 * another value.
 */
@FlaggedApi(Flags.FLAG_POPULATION_DENSITY_PROVIDER)
public class LocationFudgerCache {

    // The maximum number of S2 cell ids stored in the cache.
    // Each cell id is a long, so the memory requirement is 8*MAX_CACHE_SIZE bytes.
    protected static final int MAX_CACHE_SIZE = 20;

    private final Object mLock = new Object();

    // mCache is a circular buffer of size MAX_CACHE_SIZE. The next position to be written to is
    // mPosInCache. Initially, the cache is filled with INVALID_CELL_IDs.
    @GuardedBy("mLock")
    private final long[] mCache = new long[MAX_CACHE_SIZE];

    @GuardedBy("mLock")
    private int mPosInCache = 0;

    @GuardedBy("mLock")
    private int mCacheSize = 0;

    // The S2 level to coarsen to, if the cache doesn't contain a better answer.
    // Updated concurrently by callbacks.
    @GuardedBy("mLock")
    private Integer mDefaultCoarseningLevel = null;

    // The provider that asynchronously provides what is stored in the cache.
    private final ProxyPopulationDensityProvider mPopulationDensityProvider;

    private static String sTAG = "LocationFudgerCache";

    public LocationFudgerCache(@NonNull ProxyPopulationDensityProvider provider) {
        mPopulationDensityProvider = Objects.requireNonNull(provider);

        asyncFetchDefaultCoarseningLevel();
    }

    /** Returns true if the cache has successfully received a default value from the provider. */
    public boolean hasDefaultValue() {
        synchronized (mLock) {
            return (mDefaultCoarseningLevel != null);
        }
    }

    /**
     * Returns the S2 level to which the provided location should be coarsened.
     * The answer comes from the cache if available, otherwise the default value is returned.
     */
    public int getCoarseningLevel(double latitudeDegrees, double longitudeDegrees) {
        // If we still haven't received the default level from the provider, try fetching it again.
        // The answer wouldn't come in time, but it will be used for the following queries.
        if (!hasDefaultValue()) {
            asyncFetchDefaultCoarseningLevel();
        }
        Long s2CellId = readCacheForLatLng(latitudeDegrees, longitudeDegrees);
        if (s2CellId == null) {
            // Asynchronously queries the density from the provider. The answer won't come in time,
            // but it will update the cache for the following queries.
            refreshCache(latitudeDegrees, longitudeDegrees);

            return getDefaultCoarseningLevel();
        }
        return S2CellIdUtils.getLevel(s2CellId);
    }

    /**
     * If the cache contains the current location, returns the corresponding S2 cell id.
     * Otherwise, returns null.
     */
    @Nullable
    private Long readCacheForLatLng(double latDegrees, double lngDegrees) {
        synchronized (mLock) {
            for (int i = 0; i < mCacheSize; i++) {
                if (S2CellIdUtils.containsLatLngDegrees(mCache[i], latDegrees, lngDegrees)) {
                    return mCache[i];
                }
            }
        }
        return null;
    }

    /** Adds the provided s2 cell id to the cache. This might evict other values from the cache. */
    public void addToCache(long s2CellId) {
        addToCache(new long[] {s2CellId});
    }

    /**
     * Adds the provided s2 cell ids to the cache. This might evict other values from the cache.
     * If more than MAX_CACHE_SIZE elements are provided, only the first elements are copied.
     * The first element of the input is added last into the FIFO cache, so it gets evicted last.
     */
    public void addToCache(long[] s2CellIds) {
        synchronized (mLock) {
            // Only copy up to MAX_CACHE_SIZE elements
            int end = Math.min(s2CellIds.length, MAX_CACHE_SIZE);
            mCacheSize = Math.min(mCacheSize + end, MAX_CACHE_SIZE);

            // Add in reverse so the first cell of s2CellIds is the last evicted
            for (int i = end - 1; i >= 0; i--) {
                mCache[mPosInCache] = s2CellIds[i];
                mPosInCache = (mPosInCache + 1) % MAX_CACHE_SIZE;
            }
        }
    }

    /**
     * Queries the population density provider for the default coarsening level (to be used if the
     * cache doesn't contain a better answer), and updates mDefaultCoarseningLevel with the answer.
     */
    private void asyncFetchDefaultCoarseningLevel() {
        IS2LevelCallback callback = new IS2LevelCallback.Stub() {
            @Override
            public void onResult(int s2level) {
                synchronized (mLock) {
                    mDefaultCoarseningLevel = Integer.valueOf(s2level);
                }
            }

            @Override
            public void onError() {
                Log.e(sTAG, "could not get default population density");
            }
        };
        mPopulationDensityProvider.getDefaultCoarseningLevel(callback);
    }

    /**
     *  Queries the population density provider and store the result in the cache.
     */
    private void refreshCache(double latitude, double longitude) {
        IS2CellIdsCallback callback = new IS2CellIdsCallback.Stub() {
            @Override
            public void onResult(long[] s2CellIds) {
                addToCache(s2CellIds);
            }

            @Override
            public void onError() {
                Log.e(sTAG, "could not get population density");
            }
        };
        mPopulationDensityProvider.getCoarsenedS2Cell(latitude, longitude, callback);
    }

    /**
     * Returns the default S2 level to coarsen to. This should be used if the cache
     * does not provide a better answer.
     */
    private int getDefaultCoarseningLevel() {
        synchronized (mLock) {
            // The minimum valid level is 0.
            if (mDefaultCoarseningLevel == null) {
                return 0;
            }
            return mDefaultCoarseningLevel;
        }
    }
}
+14 −0
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ import static com.android.server.location.eventlog.LocationEventLog.EVENT_LOG;
import static java.lang.Math.max;
import static java.lang.Math.min;

import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -105,6 +106,7 @@ import com.android.server.LocalServices;
import com.android.server.location.LocationPermissions;
import com.android.server.location.LocationPermissions.PermissionLevel;
import com.android.server.location.fudger.LocationFudger;
import com.android.server.location.fudger.LocationFudgerCache;
import com.android.server.location.injector.AlarmHelper;
import com.android.server.location.injector.AppForegroundHelper;
import com.android.server.location.injector.AppForegroundHelper.AppForegroundListener;
@@ -1662,6 +1664,18 @@ public class LocationProviderManager extends
        }
    }

    /**
     * Provides the optional {@link LocationFudgerCache} for coarsening based on population density.
     */
    @FlaggedApi(Flags.FLAG_DENSITY_BASED_COARSE_LOCATIONS)
    public void setLocationFudgerCache(LocationFudgerCache cache) {
        if (!Flags.densityBasedCoarseLocations()) {
            return;
        }

        mLocationFudger.setLocationFudgerCache(cache);
    }

    /**
     * Returns true if this provider is visible to the current caller (whether called from a binder
     * thread or not). If a provider isn't visible, then all APIs return the same data they would if
+3 −0
Original line number Diff line number Diff line
@@ -23,6 +23,8 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;

@@ -45,6 +47,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.server.LocalServices;
import com.android.server.location.fudger.LocationFudgerCache;
import com.android.server.location.injector.FakeUserInfoHelper;
import com.android.server.location.injector.TestInjector;
import com.android.server.location.provider.AbstractLocationProvider;
Loading