Skip to content

Commit 92cecf8

Browse files
authored
fix: prevent crash on min/max zoom changes and fix zoom on navigation (#572)
1 parent 8292ae1 commit 92cecf8

File tree

19 files changed

+431
-49
lines changed

19 files changed

+431
-49
lines changed

android/src/main/java/com/google/android/react/navsdk/MapViewController.java

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@
1515

1616
import android.annotation.SuppressLint;
1717
import android.app.Activity;
18+
import android.util.Log;
1819
import androidx.core.util.Supplier;
1920
import com.facebook.react.bridge.UiThreadUtil;
2021
import com.google.android.gms.maps.CameraUpdateFactory;
2122
import com.google.android.gms.maps.GoogleMap;
23+
import com.google.android.gms.maps.GoogleMap.CameraPerspective;
2224
import com.google.android.gms.maps.model.BitmapDescriptor;
2325
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
2426
import com.google.android.gms.maps.model.CameraPosition;
2527
import com.google.android.gms.maps.model.Circle;
2628
import com.google.android.gms.maps.model.CircleOptions;
29+
import com.google.android.gms.maps.model.FollowMyLocationOptions;
2730
import com.google.android.gms.maps.model.GroundOverlay;
2831
import com.google.android.gms.maps.model.GroundOverlayOptions;
2932
import com.google.android.gms.maps.model.LatLng;
@@ -42,6 +45,7 @@
4245
import java.util.Map;
4346

4447
public class MapViewController implements INavigationViewControllerProperties {
48+
private static final String TAG = "MapViewController";
4549
private GoogleMap mGoogleMap;
4650
private Supplier<Activity> activitySupplier;
4751
private INavigationViewCallback mNavigationViewCallback;
@@ -883,23 +887,35 @@ public void setMinZoomLevel(float minZoomLevel) {
883887
return;
884888
}
885889

886-
// Get the effective max zoom for comparison
887-
float maxZoom =
890+
minZoomLevelPreference = minZoomLevel;
891+
892+
// Reset both preferences first so the new min/max pair is always applied
893+
// atomically. Without this, Fabric can deliver minZoomLevel and maxZoomLevel
894+
// prop updates in any order, causing a transient state where min > max.
895+
mGoogleMap.resetMinMaxZoomPreference();
896+
897+
float effectiveMin = (minZoomLevel < 0.0f) ? mGoogleMap.getMinZoomLevel() : minZoomLevel;
898+
float effectiveMax =
888899
(maxZoomLevelPreference != null && maxZoomLevelPreference >= 0.0f)
889900
? maxZoomLevelPreference
890901
: mGoogleMap.getMaxZoomLevel();
891902

892-
// Validate that min is not greater than max (unless using -1 sentinel)
893-
if (minZoomLevel >= 0.0f && minZoomLevel > maxZoom) {
894-
throw new IllegalArgumentException(
895-
"Minimum zoom level cannot be greater than maximum zoom level");
903+
if (effectiveMin > effectiveMax) {
904+
Log.w(
905+
TAG,
906+
"minZoomLevel ("
907+
+ effectiveMin
908+
+ ") is greater than maxZoomLevel ("
909+
+ effectiveMax
910+
+ "). Ignoring zoom level constraints.");
911+
return;
896912
}
897913

898-
minZoomLevelPreference = minZoomLevel;
899-
900-
// Use map's current minZoomLevel if -1 is provided
901-
float effectiveMin = (minZoomLevel < 0.0f) ? mGoogleMap.getMinZoomLevel() : minZoomLevel;
902914
mGoogleMap.setMinZoomPreference(effectiveMin);
915+
916+
if (maxZoomLevelPreference != null) {
917+
mGoogleMap.setMaxZoomPreference(effectiveMax);
918+
}
903919
}
904920

905921
@Override
@@ -908,23 +924,35 @@ public void setMaxZoomLevel(float maxZoomLevel) {
908924
return;
909925
}
910926

911-
// Get the effective min zoom for comparison
912-
float minZoom =
927+
maxZoomLevelPreference = maxZoomLevel;
928+
929+
// Reset both preferences first so the new min/max pair is always applied
930+
// atomically. Without this, Fabric can deliver minZoomLevel and maxZoomLevel
931+
// prop updates in any order, causing a transient state where min > max.
932+
mGoogleMap.resetMinMaxZoomPreference();
933+
934+
float effectiveMax = (maxZoomLevel < 0.0f) ? mGoogleMap.getMaxZoomLevel() : maxZoomLevel;
935+
float effectiveMin =
913936
(minZoomLevelPreference != null && minZoomLevelPreference >= 0.0f)
914937
? minZoomLevelPreference
915938
: mGoogleMap.getMinZoomLevel();
916939

917-
// Validate that max is not less than min (unless using -1 sentinel)
918-
if (maxZoomLevel >= 0.0f && maxZoomLevel < minZoom) {
919-
throw new IllegalArgumentException(
920-
"Maximum zoom level cannot be less than minimum zoom level");
940+
if (effectiveMin > effectiveMax) {
941+
Log.w(
942+
TAG,
943+
"minZoomLevel ("
944+
+ effectiveMin
945+
+ ") is greater than maxZoomLevel ("
946+
+ effectiveMax
947+
+ "). Ignoring zoom level constraints.");
948+
return;
921949
}
922950

923-
maxZoomLevelPreference = maxZoomLevel;
924-
925-
// Use map's current maxZoomLevel if -1 is provided
926-
float effectiveMax = (maxZoomLevel < 0.0f) ? mGoogleMap.getMaxZoomLevel() : maxZoomLevel;
927951
mGoogleMap.setMaxZoomPreference(effectiveMax);
952+
953+
if (minZoomLevelPreference != null) {
954+
mGoogleMap.setMinZoomPreference(effectiveMin);
955+
}
928956
}
929957

930958
public void setZoomGesturesEnabled(boolean enabled) {
@@ -1005,16 +1033,27 @@ public void resetMinMaxZoomLevel() {
10051033
return;
10061034
}
10071035

1036+
minZoomLevelPreference = null;
1037+
maxZoomLevelPreference = null;
10081038
mGoogleMap.resetMinMaxZoomPreference();
10091039
}
10101040

10111041
@SuppressLint("MissingPermission")
1012-
public void setFollowingPerspective(int jsValue) {
1042+
public void setFollowingPerspective(int jsValue, Float zoomLevel) {
10131043
if (mGoogleMap == null) {
10141044
return;
10151045
}
10161046

1017-
mGoogleMap.followMyLocation(EnumTranslationUtil.getCameraPerspectiveFromJsValue(jsValue));
1047+
@CameraPerspective
1048+
int perspective = EnumTranslationUtil.getCameraPerspectiveFromJsValue(jsValue);
1049+
1050+
if (zoomLevel != null) {
1051+
FollowMyLocationOptions options =
1052+
FollowMyLocationOptions.builder().setZoomLevel(zoomLevel).build();
1053+
mGoogleMap.followMyLocation(perspective, options);
1054+
} else {
1055+
mGoogleMap.followMyLocation(perspective);
1056+
}
10181057
}
10191058

10201059
public void setPadding(int top, int left, int bottom, int right) {

android/src/main/java/com/google/android/react/navsdk/NavAutoModule.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,14 +393,15 @@ public void setMyLocationEnabled(boolean enabled) {
393393
}
394394

395395
@Override
396-
public void setFollowingPerspective(double perspective) {
396+
public void setFollowingPerspective(double perspective, Double zoomLevel) {
397397
int jsValue = (int) perspective;
398398
UiThreadUtil.runOnUiThread(
399399
() -> {
400400
if (mMapViewController == null) {
401401
return;
402402
}
403-
mMapViewController.setFollowingPerspective(jsValue);
403+
mMapViewController.setFollowingPerspective(
404+
jsValue, zoomLevel == null ? null : zoomLevel.floatValue());
404405
});
405406
}
406407

android/src/main/java/com/google/android/react/navsdk/NavViewModule.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,8 @@ public void setNavigationUIEnabled(String nativeID, boolean enabled, final Promi
405405
}
406406

407407
@Override
408-
public void setFollowingPerspective(String nativeID, double perspective, final Promise promise) {
408+
public void setFollowingPerspective(
409+
String nativeID, double perspective, Double zoomLevel, final Promise promise) {
409410
UiThreadUtil.runOnUiThread(
410411
() -> {
411412
IMapViewFragment fragment = mNavViewManager.getFragmentByNativeId(nativeID);
@@ -415,7 +416,10 @@ public void setFollowingPerspective(String nativeID, double perspective, final P
415416
}
416417

417418
if (fragment instanceof INavViewFragment) {
418-
fragment.getMapController().setFollowingPerspective((int) perspective);
419+
fragment
420+
.getMapController()
421+
.setFollowingPerspective(
422+
(int) perspective, zoomLevel == null ? null : zoomLevel.floatValue());
419423
promise.resolve(null);
420424
} else {
421425
promise.reject(JsErrors.NOT_NAV_VIEW_ERROR_CODE, JsErrors.NOT_NAV_VIEW_ERROR_MESSAGE);

example/e2e/map.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,11 @@ describe('Map view tests', () => {
8989
await expectNoErrors();
9090
await expectSuccess();
9191
});
92+
93+
it('MT10 - test min and max zoom level constraints', async () => {
94+
await selectTestByName('testMinMaxZoomLevels');
95+
await waitForTestToFinish();
96+
await expectNoErrors();
97+
await expectSuccess();
98+
});
9299
});

example/e2e/navigation.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,12 @@ describe('Navigation tests', () => {
9090
await expectNoErrors();
9191
await expectSuccess();
9292
});
93+
94+
it('NT09 - test setFollowingPerspective with zoom level options', async () => {
95+
await selectTestByName('testSetFollowingPerspective');
96+
await agreeToTermsAndConditions();
97+
await waitForTestToFinish();
98+
await expectNoErrors();
99+
await expectSuccess();
100+
});
93101
});

example/src/screens/IntegrationTestsScreen.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import {
5555
testStartGuidanceWithoutDestinations,
5656
testRouteTokenOptionsValidation,
5757
testMapStyle,
58+
testMinMaxZoomLevels,
59+
testSetFollowingPerspective,
5860
NO_ERRORS_DETECTED_LABEL,
5961
} from './integration_tests/integration_test';
6062

@@ -127,6 +129,12 @@ const IntegrationTestsScreen = () => {
127129
boolean | undefined
128130
>(undefined);
129131
const [mapStyle, setMapStyle] = useState<string | undefined>(undefined);
132+
const [minZoomLevel, setMinZoomLevel] = useState<number | undefined>(
133+
undefined
134+
);
135+
const [maxZoomLevel, setMaxZoomLevel] = useState<number | undefined>(
136+
undefined
137+
);
130138

131139
const onMapReady = useCallback(async () => {
132140
try {
@@ -236,6 +244,8 @@ const IntegrationTestsScreen = () => {
236244
setZoomControlsEnabled,
237245
setMapToolbarEnabled,
238246
setMapStyle,
247+
setMinZoomLevel,
248+
setMaxZoomLevel,
239249
};
240250
};
241251

@@ -305,6 +315,12 @@ const IntegrationTestsScreen = () => {
305315
case 'testMapStyle':
306316
await testMapStyle(getTestTools());
307317
break;
318+
case 'testMinMaxZoomLevels':
319+
await testMinMaxZoomLevels(getTestTools());
320+
break;
321+
case 'testSetFollowingPerspective':
322+
await testSetFollowingPerspective(getTestTools());
323+
break;
308324
default:
309325
resetTestState();
310326
break;
@@ -347,6 +363,8 @@ const IntegrationTestsScreen = () => {
347363
zoomControlsEnabled={zoomControlsEnabled}
348364
mapToolbarEnabled={mapToolbarEnabled}
349365
mapStyle={mapStyle}
366+
minZoomLevel={minZoomLevel}
367+
maxZoomLevel={maxZoomLevel}
350368
/>
351369
</View>
352370
<View style={{ flex: 4 }}>
@@ -518,6 +536,20 @@ const IntegrationTestsScreen = () => {
518536
}}
519537
testID="testMapStyle"
520538
/>
539+
<ExampleAppButton
540+
title="testMinMaxZoomLevels"
541+
onPress={() => {
542+
runTest('testMinMaxZoomLevels');
543+
}}
544+
testID="testMinMaxZoomLevels"
545+
/>
546+
<ExampleAppButton
547+
title="testSetFollowingPerspective"
548+
onPress={() => {
549+
runTest('testSetFollowingPerspective');
550+
}}
551+
testID="testSetFollowingPerspective"
552+
/>
521553
</OverlayModal>
522554
</View>
523555
);

0 commit comments

Comments
 (0)