From fb8aa31c415e1ae85344e9a2c91f60e1b4c76938 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 28 Feb 2026 21:37:22 +0200 Subject: [PATCH] Fix GeofenceManager expiry purge concurrent modification --- .../codename1/location/GeofenceManager.java | 5 ++-- .../location/GeofenceManagerTest.java | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CodenameOne/src/com/codename1/location/GeofenceManager.java b/CodenameOne/src/com/codename1/location/GeofenceManager.java index 70595a5c81..f9473b124c 100644 --- a/CodenameOne/src/com/codename1/location/GeofenceManager.java +++ b/CodenameOne/src/com/codename1/location/GeofenceManager.java @@ -170,9 +170,10 @@ private synchronized void purgeExpired() { boolean saveFences = false; boolean saveActive = false; boolean saveActiveFences = false; - for (Map.Entry time : times.entrySet()) { + for (Iterator> it = times.entrySet().iterator(); it.hasNext();) { + Map.Entry time = it.next(); if (time.getValue() > 0L && time.getValue() < now) { - times.remove(time.getKey()); + it.remove(); if (!saveFences && fences.containsKey(time.getKey())) { saveFences = true; } diff --git a/maven/core-unittests/src/test/java/com/codename1/location/GeofenceManagerTest.java b/maven/core-unittests/src/test/java/com/codename1/location/GeofenceManagerTest.java index 6df6916ed8..a54b9f3464 100644 --- a/maven/core-unittests/src/test/java/com/codename1/location/GeofenceManagerTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/location/GeofenceManagerTest.java @@ -126,6 +126,22 @@ void isBubbleRecognizesBubbleId() { assertFalse(manager.isBubble("not-bubble")); } + @FormTest + void updatePurgesExpiredGeofencesWithoutConcurrentModification() throws Exception { + Geofence first = createGeofence("expired-1", 0.0, 0.0, 50, -1L); + Geofence second = createGeofence("expired-2", 0.0, 0.0, 50, -1L); + manager.add(first, second); + Map expirations = new HashMap(); + long expiredAt = System.currentTimeMillis() - 1000L; + expirations.put(first.getId(), expiredAt); + expirations.put(second.getId(), expiredAt); + setPrivateField("expiryTimes", expirations); + + assertDoesNotThrow(() -> manager.update(1000)); + assertFalse(manager.asMap().containsKey(first.getId())); + assertFalse(manager.asMap().containsKey(second.getId())); + } + @FormTest public void testGeofenceManagerListener() { // Instantiate the listener @@ -147,6 +163,13 @@ public void testGeofenceManagerListener() { assertTrue(MyGeofenceListener.calledEnter, "onEntered should delegate to registered listener"); } + + private void setPrivateField(String name, Object value) throws Exception { + Field field = GeofenceManager.class.getDeclaredField(name); + field.setAccessible(true); + field.set(manager, value); + } + private Geofence createGeofence(String id, double lat, double lng, int radius, long expiration) { Location location = new Location(lat, lng); return new Geofence(id, location, radius, expiration);