From 51f556b782f336a82c2162385f83dcdec1568b99 Mon Sep 17 00:00:00 2001 From: soupslurpr <92235850+soupslurpr@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:07:58 -0700 Subject: [PATCH 1/2] test synchronous population density queries Exercise success, error, timeout, null, duplicate, and cross-thread callback paths through the synchronous Binder bridge. Verify coordinates are forwarded and only the first result cell is returned. --- .../PopulationDensityProviderBaseTest.java | 267 +++++++++++++----- 1 file changed, 193 insertions(+), 74 deletions(-) diff --git a/tests/location/location_fine/src/android/location/cts/fine/PopulationDensityProviderBaseTest.java b/tests/location/location_fine/src/android/location/cts/fine/PopulationDensityProviderBaseTest.java index 706753b58038..59bf9901cade 100644 --- a/tests/location/location_fine/src/android/location/cts/fine/PopulationDensityProviderBaseTest.java +++ b/tests/location/location_fine/src/android/location/cts/fine/PopulationDensityProviderBaseTest.java @@ -16,20 +16,16 @@ package android.location.cts.fine; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; +import static org.junit.Assert.assertThrows; import android.content.Context; -import android.location.flags.Flags; import android.location.provider.IPopulationDensityProvider; -import android.location.provider.IS2CellIdsCallback; -import android.location.provider.IS2LevelCallback; import android.location.provider.PopulationDensityProviderBase; import android.os.OutcomeReceiver; +import android.os.RemoteException; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -40,118 +36,240 @@ import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + @RunWith(AndroidJUnit4.class) public class PopulationDensityProviderBaseTest { private static final String TAG = "PopulationDensityProviderBaseTest"; - private static final double TEST_LATITUDE = 10.0; - private static final double TEST_LONGITUDE = 80.0; - private static final int TEST_NUM_ADDITIONAL_CELLS = 1; - private static final long[] TEST_S2_CELL_IDS_RESULT = {1000, 1001}; - private static final Integer TEST_S2_LEVEL_RESULT = 12; + private static final long TEST_WORKER_TIMEOUT_SECONDS = 10; + private static final double TEST_LATITUDE = 51.5; + private static final double TEST_LONGITUDE = -0.12; + private static final long TEST_EXPECTED_CELL_ID = 0x42L; + private static final long TEST_IGNORED_CELL_ID = 0x24L; + private static final long[] TEST_CELL_IDS = {TEST_EXPECTED_CELL_ID}; private Context mContext; @Before - public void setUp() throws Exception { + public void setUp() { mContext = ApplicationProvider.getApplicationContext(); } @ApiTest(apis = "android.location.provider.PopulationDensityProviderBase#getBinder") @Test - public void testGetBinder() { - MyProvider mProvider = new MyProvider(mContext, TAG); + public void testGetBinder_returnsBinder() { + TestProvider provider = + new TestProvider( + mContext, + (latitude, longitude, numAdditionalCells, callback) -> + callback.onResult(TEST_CELL_IDS)); + + assertNotNull(provider.getBinder()); + } - assertNotNull(mProvider.getBinder()); + @ApiTest(apis = "android.location.provider.PopulationDensityProviderBase#onGetCoarsenedS2Cells") + @Test + public void testGetCoarsenedS2CellId_forwardsQueryAndReturnsFirstCell() throws Exception { + AtomicReference queriedArguments = new AtomicReference<>(); + TestProvider provider = + new TestProvider( + mContext, + (latitude, longitude, numAdditionalCells, callback) -> { + queriedArguments.set( + new QueryArguments(latitude, longitude, numAdditionalCells)); + callback.onResult(TEST_CELL_IDS); + }); + + long s2CellId = query(provider); + QueryArguments arguments = queriedArguments.get(); + + assertThat(s2CellId).isEqualTo(TEST_EXPECTED_CELL_ID); + assertThat(arguments.mLatitude).isEqualTo(TEST_LATITUDE); + assertThat(arguments.mLongitude).isEqualTo(TEST_LONGITUDE); + assertThat(arguments.mNumAdditionalCells).isEqualTo(0); } - @ApiTest(apis = "android.location.provider.PopulationDensityProviderBase#onGetCoarsenedS2Cell") + @ApiTest(apis = "android.location.provider.PopulationDensityProviderBase#onGetCoarsenedS2Cells") @Test - public void testGetCoarsenedS2Cell_isCorrectlyCalled() throws Exception { - MyProvider mProvider = new MyProvider(mContext, TAG); - IS2CellIdsCallback mMock = mock(IS2CellIdsCallback.class); + public void testGetCoarsenedS2CellId_providerReportsError_throws() { + TestProvider provider = + new TestProvider( + mContext, + (latitude, longitude, numAdditionalCells, callback) -> + callback.onError(new RuntimeException("provider failure"))); - mProvider - .asProvider() - .getCoarsenedS2Cells( - TEST_LATITUDE, TEST_LONGITUDE, TEST_NUM_ADDITIONAL_CELLS, mMock); + IllegalStateException exception = + assertThrows(IllegalStateException.class, () -> query(provider)); - verify(mMock).onResult(TEST_S2_CELL_IDS_RESULT); - verify(mMock, never()).onError(); + assertThat(exception).hasCauseThat().isInstanceOf(RuntimeException.class); } - @ApiTest(apis = "android.location.provider.PopulationDensityProviderBase#onGetCoarsenedS2Cell") + @ApiTest(apis = "android.location.provider.PopulationDensityProviderBase#onGetCoarsenedS2Cells") @Test - public void testErrorCallback_isCorrectlyCalled() throws Exception { - MyFaultyProvider mProvider = new MyFaultyProvider(mContext, TAG); - IS2CellIdsCallback mMock = mock(IS2CellIdsCallback.class); + public void testGetCoarsenedS2CellId_providerReturnsNoCells_throws() { + TestProvider provider = + new TestProvider( + mContext, + (latitude, longitude, numAdditionalCells, callback) -> + callback.onResult(new long[0])); - mProvider - .asProvider() - .getCoarsenedS2Cells( - TEST_LATITUDE, TEST_LONGITUDE, TEST_NUM_ADDITIONAL_CELLS, mMock); + IllegalStateException exception = + assertThrows(IllegalStateException.class, () -> query(provider)); - verify(mMock, never()).onResult(any(long[].class)); - verify(mMock).onError(); + assertThat(exception).hasMessageThat().contains("returned no S2 cells"); } - @ApiTest(apis = - "android.location.provider.PopulationDensityProviderBase#onGetDefaultCoarseningLevel") + @ApiTest(apis = "android.location.provider.PopulationDensityProviderBase#onGetCoarsenedS2Cells") @Test - public void testGetDefaultCoarseningLevel_isCorrectlyCalled() throws Exception { - MyProvider mProvider = new MyProvider(mContext, TAG); - IS2LevelCallback mMock = mock(IS2LevelCallback.class); + public void testGetCoarsenedS2CellId_providerReturnsNullCells_throws() { + TestProvider provider = + new TestProvider( + mContext, + (latitude, longitude, numAdditionalCells, callback) -> + callback.onResult(null)); - mProvider.asProvider().getDefaultCoarseningLevel(mMock); + IllegalStateException exception = + assertThrows(IllegalStateException.class, () -> query(provider)); - verify(mMock).onResult(TEST_S2_LEVEL_RESULT); - verify(mMock, never()).onError(); + assertThat(exception).hasMessageThat().contains("returned no S2 cells"); + } + + @ApiTest(apis = "android.location.provider.PopulationDensityProviderBase#onGetCoarsenedS2Cells") + @Test + public void testGetCoarsenedS2CellId_providerReportsNullError_throws() { + TestProvider provider = + new TestProvider( + mContext, + (latitude, longitude, numAdditionalCells, callback) -> + callback.onError(null)); + + IllegalStateException exception = + assertThrows(IllegalStateException.class, () -> query(provider)); + + assertThat(exception).hasMessageThat().contains("reported an error"); + assertThat(exception).hasCauseThat().isInstanceOf(IllegalStateException.class); } - @ApiTest(apis = - "android.location.provider.PopulationDensityProviderBase#onGetDefaultCoarseningLevel") + @ApiTest(apis = "android.location.provider.PopulationDensityProviderBase#onGetCoarsenedS2Cells") @Test - public void testErrorCallback2_isCorrectlyCalled() throws Exception { - MyFaultyProvider mProvider = new MyFaultyProvider(mContext, TAG); - IS2LevelCallback mMock = mock(IS2LevelCallback.class); + public void testGetCoarsenedS2CellId_providerDoesNotRespond_throws() { + TestProvider provider = + new TestProvider( + mContext, (latitude, longitude, numAdditionalCells, callback) -> {}); - mProvider.asProvider().getDefaultCoarseningLevel(mMock); + IllegalStateException exception = + assertThrows(IllegalStateException.class, () -> query(provider)); - verify(mMock, never()).onResult(anyInt()); - verify(mMock).onError(); + assertThat(exception).hasMessageThat().contains("did not respond within"); } - private static class MyProvider extends PopulationDensityProviderBase { - MyProvider(Context context, String tag) { - super(context, tag); + @ApiTest(apis = "android.location.provider.PopulationDensityProviderBase#onGetCoarsenedS2Cells") + @Test + public void testGetCoarsenedS2CellId_callbackOnWorkerThread_returnsCell() throws Exception { + CountDownLatch responseWorkerReady = new CountDownLatch(1); + SynchronousQueue> callbackQueue = + new SynchronousQueue<>(); + FutureTask responseWorkerTask = + new FutureTask<>( + () -> { + responseWorkerReady.countDown(); + callbackQueue.take().onResult(TEST_CELL_IDS); + return null; + }); + Thread responseWorkerThread = + new Thread(responseWorkerTask, "PopulationDensityCtsResponse"); + responseWorkerThread.start(); + + try { + assertThat(responseWorkerReady.await(TEST_WORKER_TIMEOUT_SECONDS, TimeUnit.SECONDS)) + .isTrue(); + TestProvider provider = + new TestProvider( + mContext, + (latitude, longitude, numAdditionalCells, callback) -> + deliverResultFromWorker( + callbackQueue, responseWorkerTask, callback)); + + assertThat(query(provider)).isEqualTo(TEST_EXPECTED_CELL_ID); + } finally { + responseWorkerTask.cancel(true); + responseWorkerThread.join(TimeUnit.SECONDS.toMillis(TEST_WORKER_TIMEOUT_SECONDS)); + assertThat(responseWorkerThread.isAlive()).isFalse(); } + } + + @ApiTest(apis = "android.location.provider.PopulationDensityProviderBase#onGetCoarsenedS2Cells") + @Test + public void testGetCoarsenedS2CellId_providerRespondsTwice_returnsFirstCell() throws Exception { + TestProvider provider = + new TestProvider( + mContext, + (latitude, longitude, numAdditionalCells, callback) -> { + callback.onResult(new long[] {TEST_EXPECTED_CELL_ID}); + callback.onResult(new long[] {TEST_IGNORED_CELL_ID}); + }); + + assertThat(query(provider)).isEqualTo(TEST_EXPECTED_CELL_ID); + } - public IPopulationDensityProvider asProvider() { - return IPopulationDensityProvider.Stub.asInterface(getBinder()); + private static void deliverResultFromWorker( + SynchronousQueue> callbackQueue, + FutureTask responseWorkerTask, + OutcomeReceiver callback) { + try { + if (!callbackQueue.offer(callback, TEST_WORKER_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + throw new AssertionError("timed out handing callback to response worker"); + } + responseWorkerTask.get(TEST_WORKER_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new AssertionError("interrupted awaiting response worker", exception); + } catch (ExecutionException exception) { + throw new AssertionError("response worker failed", exception.getCause()); + } catch (TimeoutException exception) { + throw new AssertionError("timed out awaiting response worker", exception); } + } - @Override - public void onGetCoarsenedS2Cells( + private static long query(PopulationDensityProviderBase provider) throws RemoteException { + return IPopulationDensityProvider.Stub.asInterface(provider.getBinder()) + .getCoarsenedS2CellId(TEST_LATITUDE, TEST_LONGITUDE); + } + + private interface CoarsenedCellsResponder { + void respond( double latitude, double longitude, int numAdditionalCells, - OutcomeReceiver callback) { - callback.onResult(TEST_S2_CELL_IDS_RESULT); - } + OutcomeReceiver callback); + } - @Override - public void onGetDefaultCoarseningLevel(OutcomeReceiver callback) { - callback.onResult(TEST_S2_LEVEL_RESULT); + private static final class QueryArguments { + private final double mLatitude; + private final double mLongitude; + private final int mNumAdditionalCells; + + QueryArguments(double latitude, double longitude, int numAdditionalCells) { + mLatitude = latitude; + mLongitude = longitude; + mNumAdditionalCells = numAdditionalCells; } } - private static class MyFaultyProvider extends PopulationDensityProviderBase { - MyFaultyProvider(Context context, String tag) { - super(context, tag); - } + private static final class TestProvider extends PopulationDensityProviderBase { + + private final CoarsenedCellsResponder mResponder; - public IPopulationDensityProvider asProvider() { - return IPopulationDensityProvider.Stub.asInterface(getBinder()); + TestProvider(Context context, CoarsenedCellsResponder responder) { + super(context, TAG); + mResponder = responder; } @Override @@ -160,12 +278,13 @@ public void onGetCoarsenedS2Cells( double longitude, int numAdditionalCells, OutcomeReceiver callback) { - callback.onError(new RuntimeException()); + mResponder.respond(latitude, longitude, numAdditionalCells, callback); } @Override public void onGetDefaultCoarseningLevel(OutcomeReceiver callback) { - callback.onError(new RuntimeException()); + callback.onError(new UnsupportedOperationException( + "default coarsening level is not supported")); } } } From 9fcb4ffad7395e2e3f47faf2a5d16f9b4b37c04f Mon Sep 17 00:00:00 2001 From: soupslurpr <92235850+soupslurpr@users.noreply.github.com> Date: Sat, 18 Jul 2026 01:17:54 -0700 Subject: [PATCH 2/2] adapt coarse location CTS to S2 accuracy --- .../cts/coarse/LocationManagerCoarseTest.java | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/tests/location/location_coarse/src/android/location/cts/coarse/LocationManagerCoarseTest.java b/tests/location/location_coarse/src/android/location/cts/coarse/LocationManagerCoarseTest.java index 05b7557ffd17..d71fcb76e777 100644 --- a/tests/location/location_coarse/src/android/location/cts/coarse/LocationManagerCoarseTest.java +++ b/tests/location/location_coarse/src/android/location/cts/coarse/LocationManagerCoarseTest.java @@ -50,7 +50,6 @@ import android.os.SystemClock; import android.platform.test.annotations.AppModeFull; import android.provider.Settings; -import android.util.Log; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -69,11 +68,10 @@ @RunWith(AndroidJUnit4.class) public class LocationManagerCoarseTest { - private static final String TAG = "LocationManagerCoarseTest"; - private static final long TIMEOUT_MS = 5000; + private static final long RANDOM_SEED = 0; - private static final float MIN_COARSE_FUDGE_DISTANCE_M = 2000f; + private static final float MIN_COARSE_LOCATION_OFFSET_SCALE_METERS = 2000f; private static final String TEST_PROVIDER = "test_provider"; @@ -81,26 +79,22 @@ public class LocationManagerCoarseTest { private Context mContext; private LocationManager mManager; - private float mMaxCoarseFudgeDistanceM; + private float mCoarseLocationOffsetScaleMeters; @Before public void setUp() throws Exception { LocationUtils.registerMockLocationProvider(InstrumentationRegistry.getInstrumentation(), true); - long seed = System.currentTimeMillis(); - Log.i(TAG, "location random seed: " + seed); - - mRandom = new Random(seed); + mRandom = new Random(RANDOM_SEED); mContext = ApplicationProvider.getApplicationContext(); mManager = mContext.getSystemService(LocationManager.class); - float coarseLocationAccuracyM = Settings.Secure.getFloat( - mContext.getContentResolver(), - LOCATION_COARSE_ACCURACY_M, - MIN_COARSE_FUDGE_DISTANCE_M); - mMaxCoarseFudgeDistanceM = (float) Math.sqrt( - 2 * coarseLocationAccuracyM * coarseLocationAccuracyM); + mCoarseLocationOffsetScaleMeters = + Settings.Secure.getFloat( + mContext.getContentResolver(), + LOCATION_COARSE_ACCURACY_M, + MIN_COARSE_LOCATION_OFFSET_SCALE_METERS); assertNotNull(mManager); @@ -135,11 +129,12 @@ public void tearDown() throws Exception { } @Test - public void testMinCoarseLocationDistance() { + public void testMinCoarseLocationOffsetScale() { assertThat(Settings.Secure.getFloat( mContext.getContentResolver(), LOCATION_COARSE_ACCURACY_M, - MIN_COARSE_FUDGE_DISTANCE_M)).isAtLeast(MIN_COARSE_FUDGE_DISTANCE_M); + MIN_COARSE_LOCATION_OFFSET_SCALE_METERS)) + .isAtLeast(MIN_COARSE_LOCATION_OFFSET_SCALE_METERS); } @Test @@ -147,8 +142,7 @@ public void testGetLastKnownLocation() { Location loc = createLocation(TEST_PROVIDER, mRandom); mManager.setTestProviderLocation(TEST_PROVIDER, loc); - assertThat(mManager.getLastKnownLocation(TEST_PROVIDER)).isNearby(loc, - mMaxCoarseFudgeDistanceM); + assertCoarseLocationIsNearby(mManager.getLastKnownLocation(TEST_PROVIDER), loc); } @Test @@ -158,6 +152,7 @@ public void testGetLastKnownLocation_FastInterval() { mManager.setTestProviderLocation(TEST_PROVIDER, loc1); Location coarseLocation = mManager.getLastKnownLocation(TEST_PROVIDER); + assertNotNull(coarseLocation); mManager.setTestProviderLocation(TEST_PROVIDER, loc2); assertThat(mManager.getLastKnownLocation(TEST_PROVIDER)).isEqualTo(coarseLocation); } @@ -179,7 +174,7 @@ public void testRequestLocationUpdates() throws Exception { Runnable::run, capture); mManager.setTestProviderLocation(TEST_PROVIDER, loc); - assertThat(capture.getNextLocation(TIMEOUT_MS)).isNearby(loc, mMaxCoarseFudgeDistanceM); + assertCoarseLocationIsNearby(capture.getNextLocation(TIMEOUT_MS), loc); mManager.setTestProviderLocation(TEST_PROVIDER, createLocation(TEST_PROVIDER, mRandom)); assertThat(capture.getNextLocation(TIMEOUT_MS)).isNull(); } @@ -203,7 +198,7 @@ public void testRequestLocationUpdates_Passive() throws Exception { Runnable::run, capture); mManager.setTestProviderLocation(TEST_PROVIDER, loc); - assertThat(capture.getNextLocation(TIMEOUT_MS)).isNearby(loc, mMaxCoarseFudgeDistanceM); + assertCoarseLocationIsNearby(capture.getNextLocation(TIMEOUT_MS), loc); mManager.setTestProviderLocation(TEST_PROVIDER, createLocation(TEST_PROVIDER, mRandom)); assertThat(capture.getNextLocation(TIMEOUT_MS)).isNull(); } @@ -221,10 +216,19 @@ public void testRequestLocationUpdates_PendingIntent() throws Exception { try (LocationPendingIntentCapture capture = new LocationPendingIntentCapture(mContext)) { mManager.requestLocationUpdates(TEST_PROVIDER, 0, 0, capture.getPendingIntent()); mManager.setTestProviderLocation(TEST_PROVIDER, loc); - assertThat(capture.getNextLocation(TIMEOUT_MS)).isNearby(loc, mMaxCoarseFudgeDistanceM); + assertCoarseLocationIsNearby(capture.getNextLocation(TIMEOUT_MS), loc); } } + private void assertCoarseLocationIsNearby(Location coarseLocation, Location fineLocation) { + assertNotNull(coarseLocation); + float cellEdgeMeters = coarseLocation.getAccuracy(); + float approximateCellDiagonalMeters = (float) Math.hypot(cellEdgeMeters, cellEdgeMeters); + float distanceAllowanceMeters = + approximateCellDiagonalMeters + mCoarseLocationOffsetScaleMeters; + assertThat(coarseLocation).isNearby(fineLocation, distanceAllowanceMeters); + } + @Test public void testGetProviders() { List providers = mManager.getProviders(false);