Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 2.20.0

* Adds support for tapping points of interest on the map.
* Updates minimum supported SDK version to Flutter 3.41/Dart 3.11.

## 2.19.13
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import com.google.android.gms.maps.model.MapCapabilities;
import com.google.android.gms.maps.model.MapStyleOptions;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.PointOfInterest;
import com.google.android.gms.maps.model.Polygon;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.gms.maps.model.TileOverlay;
Expand Down Expand Up @@ -385,6 +386,14 @@ public void onCircleClick(Circle circle) {
circlesController.onCircleTap(circle.getId());
}

@Override
public void onPoiClick(PointOfInterest pointOfInterest) {
if (pointOfInterest.placeId != null) {
flutterApi.onPointOfInterestTap(
pointOfInterest.placeId, (Result<Unit> result) -> Unit.INSTANCE);
}
}
Comment on lines +389 to +395

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent potential NullPointerExceptions, it is safer to perform a null check on pointOfInterest before accessing its properties, especially since it is a parameter received from an external SDK and Java does not have compile-time null safety.

Suggested change
@Override
public void onPoiClick(PointOfInterest pointOfInterest) {
if (pointOfInterest.placeId != null) {
flutterApi.onPointOfInterestTap(
pointOfInterest.placeId, (Result<Unit> result) -> Unit.INSTANCE);
}
}
@Override
public void onPoiClick(PointOfInterest pointOfInterest) {
if (pointOfInterest != null && pointOfInterest.placeId != null) {
flutterApi.onPointOfInterestTap(
pointOfInterest.placeId, (Result<Unit> result) -> Unit.INSTANCE);
}
}

Comment on lines +389 to +395

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent a potential NullPointerException, we should defensively check if pointOfInterest is null before accessing its properties.

Suggested change
@Override
public void onPoiClick(PointOfInterest pointOfInterest) {
if (pointOfInterest.placeId != null) {
flutterApi.onPointOfInterestTap(
pointOfInterest.placeId, (Result<Unit> result) -> Unit.INSTANCE);
}
}
@Override
public void onPoiClick(PointOfInterest pointOfInterest) {
if (pointOfInterest != null && pointOfInterest.placeId != null) {
flutterApi.onPointOfInterestTap(
pointOfInterest.placeId, (Result<Unit> result) -> Unit.INSTANCE);
}
}


@Override
public void onGroundOverlayClick(@NonNull GroundOverlay groundOverlay) {
groundOverlaysController.onGroundOverlayTap(groundOverlay.getId());
Expand Down Expand Up @@ -421,6 +430,7 @@ private void setGoogleMapListener(@Nullable GoogleMapListener listener) {
googleMap.setOnPolygonClickListener(listener);
googleMap.setOnPolylineClickListener(listener);
googleMap.setOnCircleClickListener(listener);
googleMap.setOnPoiClickListener(listener);
googleMap.setOnMapClickListener(listener);
googleMap.setOnMapLongClickListener(listener);
googleMap.setOnGroundOverlayClickListener(listener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface GoogleMapListener
GoogleMap.OnPolygonClickListener,
GoogleMap.OnPolylineClickListener,
GoogleMap.OnCircleClickListener,
GoogleMap.OnPoiClickListener,
GoogleMap.OnMapClickListener,
GoogleMap.OnMapLongClickListener,
GoogleMap.OnMarkerDragListener,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4247,6 +4247,25 @@ class MapsCallbackApi(
}
}
}
/** Called when a point of interest is tapped. */
fun onPointOfInterestTap(placeIdArg: String, callback: (Result<Unit>) -> Unit) {
val separatedMessageChannelSuffix =
if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName =
"dev.flutter.pigeon.google_maps_flutter_android.MapsCallbackApi.onPointOfInterestTap$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(placeIdArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(MessagesPigeonUtils.createConnectionError(channelName)))
}
}
}
/** Called when a marker cluster is tapped. */
fun onClusterTap(clusterArg: PlatformCluster, callback: (Result<Unit>) -> Unit) {
val separatedMessageChannelSuffix =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MapCapabilities;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.PointOfInterest;
import com.google.maps.android.clustering.ClusterManager;
import io.flutter.plugin.common.BinaryMessenger;
import java.util.ArrayList;
Expand Down Expand Up @@ -258,6 +259,28 @@ public void OnClusterItemInfoWindowClickCallsMarkersController() {
verify(mockMarkersController, times(1)).onClusterItemInfoWindowTap(markerBuilder.markerId());
}

@Test
public void OnPoiClickCallsFlutterApi() {
GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies();
googleMapController.onMapReady(mockGoogleMap);

PointOfInterest pointOfInterest =
new PointOfInterest(new LatLng(0, 0), "place-123", "Test Place");
googleMapController.onPoiClick(pointOfInterest);

verify(flutterApi, times(1)).onPointOfInterestTap(eq("place-123"), any());
}

@Test
public void OnPoiClickNullPlaceIdDoesNotCallFlutterApi() {
GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies();
googleMapController.onMapReady(mockGoogleMap);

googleMapController.onPoiClick(new PointOfInterest(new LatLng(0, 0), null, "Test Place"));

verify(flutterApi, times(0)).onPointOfInterestTap(any(), any());
}
Comment on lines +274 to +282

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a unit test to verify that onPoiClick safely handles a null PointOfInterest parameter without throwing a NullPointerException.

  @Test
  public void OnPoiClickNullPlaceIdDoesNotCallFlutterApi() {
    GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies();
    googleMapController.onMapReady(mockGoogleMap);

    googleMapController.onPoiClick(new PointOfInterest(new LatLng(0, 0), null, "Test Place"));

    verify(flutterApi, times(0)).onPointOfInterestTap(any(), any());
  }

  @Test
  public void OnPoiClickNullPoiDoesNotCallFlutterApi() {
    GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies();
    googleMapController.onMapReady(mockGoogleMap);

    googleMapController.onPoiClick(null);

    verify(flutterApi, times(0)).onPointOfInterestTap(any(), any());
  }


@Test
public void SetInitialHeatmaps() {
GoogleMapController googleMapController = getGoogleMapControllerWithMockedDependencies();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
google_maps_flutter_platform_interface: ^2.13.0
google_maps_flutter_platform_interface: ^2.17.0

dev_dependencies:
build_runner: ^2.1.10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform {
return mapEventStreamController.stream.whereType<CircleTapEvent>();
}

@override
Stream<PointOfInterestTapEvent> onPointOfInterestTap({required int mapId}) {
return mapEventStreamController.stream.whereType<PointOfInterestTapEvent>();
}

@override
Stream<GroundOverlayTapEvent> onGroundOverlayTap({required int mapId}) {
return mapEventStreamController.stream.whereType<GroundOverlayTapEvent>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform {
return _events(mapId).whereType<CircleTapEvent>();
}

@override
Stream<PointOfInterestTapEvent> onPointOfInterestTap({required int mapId}) {
return _events(mapId).whereType<PointOfInterestTapEvent>();
}

@override
Stream<GroundOverlayTapEvent> onGroundOverlayTap({required int mapId}) {
return _events(mapId).whereType<GroundOverlayTapEvent>();
Expand Down Expand Up @@ -1079,6 +1084,11 @@ class HostMapMessageHandler implements MapsCallbackApi {
streamController.add(CircleTapEvent(mapId, CircleId(circleId)));
}

@override
void onPointOfInterestTap(String placeId) {
streamController.add(PointOfInterestTapEvent(mapId, PointOfInterestId(placeId)));
}

@override
void onClusterTap(PlatformCluster cluster) {
streamController.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3656,6 +3656,9 @@ abstract class MapsCallbackApi {
/// Called when a circle is tapped.
void onCircleTap(String circleId);

/// Called when a point of interest is tapped.
void onPointOfInterestTap(String placeId);

/// Called when a marker cluster is tapped.
void onClusterTap(PlatformCluster cluster);

Expand Down Expand Up @@ -3951,6 +3954,31 @@ abstract class MapsCallbackApi {
});
}
}
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.google_maps_flutter_android.MapsCallbackApi.onPointOfInterestTap$messageChannelSuffix',
pigeonChannelCodec,
binaryMessenger: binaryMessenger,
);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
final List<Object?> args = message! as List<Object?>;
final String arg_placeId = args[0]! as String;
try {
api.onPointOfInterestTap(arg_placeId);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(
error: PlatformException(code: 'error', message: e.toString()),
);
}
});
}
}
{
final pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.google_maps_flutter_android.MapsCallbackApi.onClusterTap$messageChannelSuffix',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,9 @@ abstract class MapsCallbackApi {
/// Called when a circle is tapped.
void onCircleTap(String circleId);

/// Called when a point of interest is tapped.
void onPointOfInterestTap(String placeId);

/// Called when a marker cluster is tapped.
void onClusterTap(PlatformCluster cluster);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: google_maps_flutter_android
description: Android implementation of the google_maps_flutter plugin.
repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
version: 2.19.13
version: 2.20.0

environment:
sdk: ^3.12.0
Expand All @@ -21,7 +21,7 @@ dependencies:
flutter:
sdk: flutter
flutter_plugin_android_lifecycle: ^2.0.1
google_maps_flutter_platform_interface: ^2.13.0
google_maps_flutter_platform_interface: ^2.17.0
meta: ^1.10.0
stream_transform: ^2.0.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,20 @@ void main() {
expect((await stream.next).value.value, equals(objectId));
});

test('points of interest send tap events to correct stream', () async {
const mapId = 1;
const placeId = 'place-123';

final maps = GoogleMapsFlutterAndroid();
final HostMapMessageHandler callbackHandler = maps.ensureHandlerInitialized(mapId);

final stream = StreamQueue<PointOfInterestTapEvent>(maps.onPointOfInterestTap(mapId: mapId));

callbackHandler.onPointOfInterestTap(placeId);

expect((await stream.next).value.value, equals(placeId));
});

test('clusters send tap events to correct stream', () async {
const mapId = 1;
const managerId = 'manager-id';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 2.19.0

* Adds support for tapping points of interest on the map.
* Updates minimum supported SDK version to Flutter 3.41/Dart 3.11.

## 2.18.13
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ class StubBinaryMessenger: NSObject, FlutterBinaryMessenger {
}
}

/// Records POI tap callbacks for unit tests.
class MockMapEventHandler: TestMapEventHandler {
var lastTappedPointOfInterestPlaceIdentifier: String?
private var pointOfInterestTapContinuation: CheckedContinuation<Void, Never>?

func waitForPointOfInterestTap() async {
if lastTappedPointOfInterestPlaceIdentifier != nil {
return
}
await withCheckedContinuation { continuation in
pointOfInterestTapContinuation = continuation
}
}

override func didTapPointOfInterest(withPlaceIdentifier placeIdArg: String) async throws {
lastTappedPointOfInterestPlaceIdentifier = placeIdArg
pointOfInterestTapContinuation?.resume()
pointOfInterestTapContinuation = nil
}
}

class StubPluginRegistrar: NSObject, FlutterPluginRegistrar {
var viewController: UIViewController? { nil }
func publish(_ value: NSObject) {}
Expand Down Expand Up @@ -167,6 +188,37 @@ class StubPluginRegistrar: NSObject, FlutterPluginRegistrar {
#expect(mockTransactionWrapper.animationDuration == Double(durationMilliseconds) / 1000)
}

@Test func didTapPOIForwardsPlaceIdentifierToCallbackApi() async {
let frame = CGRect(x: 0, y: 0, width: 100, height: 100)
let mapViewOptions = GMSMapViewOptions()
mapViewOptions.frame = frame
mapViewOptions.camera = GMSCameraPosition(latitude: 0, longitude: 0, zoom: 0)

let mapView = PartiallyMockedMapView(options: mapViewOptions)

let mapEventHandler = MockMapEventHandler()
let controller = GoogleMapController(
mapView: mapView,
viewIdentifier: 0,
creationParameters: emptyCreationParameters(),
assetProvider: TestAssetProvider(),
binaryMessenger: StubBinaryMessenger(),
callbackHandler: mapEventHandler,
tileProvider: mapEventHandler
)

async let poiTapReceived: Void = mapEventHandler.waitForPointOfInterestTap()
controller.mapView(
mapView,
didTapPOIWithPlaceID: "place-123",
name: "Test Place",
location: CLLocationCoordinate2DMake(0, 0)
)
await poiTapReceived

#expect(mapEventHandler.lastTappedPointOfInterestPlaceIdentifier == "place-123")
}

@Test func inspectorAPICameraPosition() throws {
let frame = CGRect(x: 0, y: 0, width: 100, height: 100)
let mapViewOptions = GMSMapViewOptions()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import Foundation

@testable import google_maps_flutter_ios_sdk10

/// Fake implementation of MapEventDelegate for unit tests.
class TestMapEventHandler: MapEventDelegate {
/// Fake implementation of MapEventDelegate (and TileProviderDelegate) for unit tests.
class TestMapEventHandler: MapEventDelegate, TileProviderDelegate {
func didStartCameraMove() async throws {}

func didMoveCamera(to cameraPositionArg: PlatformCameraPosition) async throws {}
Expand Down Expand Up @@ -39,6 +39,8 @@ class TestMapEventHandler: MapEventDelegate {

func didTapCircle(withIdentifier circleIdArg: String) async throws {}

func didTapPointOfInterest(withPlaceIdentifier placeIdArg: String) async throws {}

func didTapCluster(_ clusterArg: PlatformCluster) async throws {}

func didTapPolygon(withIdentifier polygonIdArg: String) async throws {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
google_maps_flutter_platform_interface: ^2.14.1
google_maps_flutter_platform_interface: ^2.17.0

dev_dependencies:
flutter_test:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform {
return mapEventStreamController.stream.whereType<CircleTapEvent>();
}

@override
Stream<PointOfInterestTapEvent> onPointOfInterestTap({required int mapId}) {
return mapEventStreamController.stream.whereType<PointOfInterestTapEvent>();
}

@override
Stream<MapTapEvent> onTap({required int mapId}) {
return mapEventStreamController.stream.whereType<MapTapEvent>();
Expand Down
Loading
Loading