[google_maps_flutter] Add methods to programmatically show/hide marker's infowindow (#2181)

diff --git a/AUTHORS b/AUTHORS
index b693b0b..ffc4e5a 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -49,3 +49,4 @@
 Quentin Le Guennec <quentin@tengio.com>
 Koushik Ravikumar <koushik@tengio.com>
 Nissim Dsilva <nissim@tengio.com>
+Giancarlo Rocha <giancarloiff@gmail.com>
diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md
index a51d924..9e87ada 100644
--- a/packages/google_maps_flutter/CHANGELOG.md
+++ b/packages/google_maps_flutter/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.5.23
+
+* Add methods to programmatically control markers info windows.
+
 ## 0.5.22+3
 
 * Fix polygon and circle stroke width according to device density
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
index fe3960f..2b96e3c 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java
@@ -303,6 +303,24 @@
           result.success(null);
           break;
         }
+      case "markers#showInfoWindow":
+        {
+          Object markerId = call.argument("markerId");
+          markersController.showMarkerInfoWindow((String) markerId, result);
+          break;
+        }
+      case "markers#hideInfoWindow":
+        {
+          Object markerId = call.argument("markerId");
+          markersController.hideMarkerInfoWindow((String) markerId, result);
+          break;
+        }
+      case "markers#isInfoWindowShown":
+        {
+          Object markerId = call.argument("markerId");
+          markersController.isInfoWindowShown((String) markerId, result);
+          break;
+        }
       case "polygons#update":
         {
           Object polygonsToAdd = call.argument("polygonsToAdd");
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java
index 6f3089b..412daee 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java
@@ -93,4 +93,16 @@
   boolean consumeTapEvents() {
     return consumeTapEvents;
   }
+
+  public void showInfoWindow() {
+    marker.showInfoWindow();
+  }
+
+  public void hideInfoWindow() {
+    marker.hideInfoWindow();
+  }
+
+  public boolean isInfoWindowShown() {
+    return marker.isInfoWindowShown();
+  }
 }
diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java
index 1f86346..70feb97 100644
--- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java
+++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java
@@ -63,6 +63,35 @@
     }
   }
 
+  void showMarkerInfoWindow(String markerId, MethodChannel.Result result) {
+    MarkerController markerController = markerIdToController.get(markerId);
+    if (markerController != null) {
+      markerController.showInfoWindow();
+      result.success(null);
+    } else {
+      result.error("Invalid markerId", "showInfoWindow called with invalid markerId", null);
+    }
+  }
+
+  void hideMarkerInfoWindow(String markerId, MethodChannel.Result result) {
+    MarkerController markerController = markerIdToController.get(markerId);
+    if (markerController != null) {
+      markerController.hideInfoWindow();
+      result.success(null);
+    } else {
+      result.error("Invalid markerId", "hideInfoWindow called with invalid markerId", null);
+    }
+  }
+
+  void isInfoWindowShown(String markerId, MethodChannel.Result result) {
+    MarkerController markerController = markerIdToController.get(markerId);
+    if (markerController != null) {
+      result.success(markerController.isInfoWindowShown());
+    } else {
+      result.error("Invalid markerId", "isInfoWindowShown called with invalid markerId", null);
+    }
+  }
+
   boolean onMarkerTap(String googleMarkerId) {
     String markerId = googleMapsMarkerIdToDartMarkerId.get(googleMarkerId);
     if (markerId == null) {
diff --git a/packages/google_maps_flutter/example/test_driver/google_maps_e2e.dart b/packages/google_maps_flutter/example/test_driver/google_maps_e2e.dart
index 474f605..2697e38 100644
--- a/packages/google_maps_flutter/example/test_driver/google_maps_e2e.dart
+++ b/packages/google_maps_flutter/example/test_driver/google_maps_e2e.dart
@@ -753,4 +753,39 @@
     final LatLngBounds bounds2 = await controller.getVisibleRegion();
     expect(bounds1, bounds2);
   });
+
+  testWidgets('testToggleInfoWindow', (WidgetTester tester) async {
+    final Marker marker = Marker(
+        markerId: MarkerId("marker"),
+        infoWindow: InfoWindow(title: "InfoWindow"));
+    final Set<Marker> markers = <Marker>{marker};
+
+    Completer<GoogleMapController> controllerCompleter =
+        Completer<GoogleMapController>();
+
+    await tester.pumpWidget(Directionality(
+      textDirection: TextDirection.ltr,
+      child: GoogleMap(
+        initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)),
+        markers: markers,
+        onMapCreated: (GoogleMapController googleMapController) {
+          controllerCompleter.complete(googleMapController);
+        },
+      ),
+    ));
+
+    GoogleMapController controller = await controllerCompleter.future;
+
+    bool iwVisibleStatus =
+        await controller.isMarkerInfoWindowShown(marker.markerId);
+    expect(iwVisibleStatus, false);
+
+    await controller.showMarkerInfoWindow(marker.markerId);
+    iwVisibleStatus = await controller.isMarkerInfoWindowShown(marker.markerId);
+    expect(iwVisibleStatus, true);
+
+    await controller.hideMarkerInfoWindow(marker.markerId);
+    iwVisibleStatus = await controller.isMarkerInfoWindowShown(marker.markerId);
+    expect(iwVisibleStatus, false);
+  });
 }
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
index 37e0ad0..d694f6f 100644
--- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m
@@ -185,6 +185,33 @@
       [_markersController removeMarkerIds:markerIdsToRemove];
     }
     result(nil);
+  } else if ([call.method isEqualToString:@"markers#showInfoWindow"]) {
+    id markerId = call.arguments[@"markerId"];
+    if ([markerId isKindOfClass:[NSString class]]) {
+      [_markersController showMarkerInfoWindow:markerId result:result];
+    } else {
+      result([FlutterError errorWithCode:@"Invalid markerId"
+                                 message:@"showInfoWindow called with invalid markerId"
+                                 details:nil]);
+    }
+  } else if ([call.method isEqualToString:@"markers#hideInfoWindow"]) {
+    id markerId = call.arguments[@"markerId"];
+    if ([markerId isKindOfClass:[NSString class]]) {
+      [_markersController hideMarkerInfoWindow:markerId result:result];
+    } else {
+      result([FlutterError errorWithCode:@"Invalid markerId"
+                                 message:@"hideInfoWindow called with invalid markerId"
+                                 details:nil]);
+    }
+  } else if ([call.method isEqualToString:@"markers#isInfoWindowShown"]) {
+    id markerId = call.arguments[@"markerId"];
+    if ([markerId isKindOfClass:[NSString class]]) {
+      [_markersController isMarkerInfoWindowShown:markerId result:result];
+    } else {
+      result([FlutterError errorWithCode:@"Invalid markerId"
+                                 message:@"isInfoWindowShown called with invalid markerId"
+                                 details:nil]);
+    }
   } else if ([call.method isEqualToString:@"polygons#update"]) {
     id polygonsToAdd = call.arguments[@"polygonsToAdd"];
     if ([polygonsToAdd isKindOfClass:[NSArray class]]) {
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h
index 24d4253..593d2ff 100644
--- a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h
@@ -30,6 +30,9 @@
 - (instancetype)initMarkerWithPosition:(CLLocationCoordinate2D)position
                               markerId:(NSString*)markerId
                                mapView:(GMSMapView*)mapView;
+- (void)showInfoWindow;
+- (void)hideInfoWindow;
+- (BOOL)isInfoWindowShown;
 - (BOOL)consumeTapEvents;
 - (void)removeMarker;
 @end
@@ -44,6 +47,9 @@
 - (BOOL)onMarkerTap:(NSString*)markerId;
 - (void)onMarkerDragEnd:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate;
 - (void)onInfoWindowTap:(NSString*)markerId;
+- (void)showMarkerInfoWindow:(NSString*)markerId result:(FlutterResult)result;
+- (void)hideMarkerInfoWindow:(NSString*)markerId result:(FlutterResult)result;
+- (void)isMarkerInfoWindowShown:(NSString*)markerId result:(FlutterResult)result;
 @end
 
 NS_ASSUME_NONNULL_END
\ No newline at end of file
diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m
index 76a420f..cd51b2f 100644
--- a/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m
+++ b/packages/google_maps_flutter/ios/Classes/GoogleMapMarkerController.m
@@ -26,6 +26,17 @@
   }
   return self;
 }
+- (void)showInfoWindow {
+  _mapView.selectedMarker = _marker;
+}
+- (void)hideInfoWindow {
+  if (_mapView.selectedMarker == _marker) {
+    _mapView.selectedMarker = nil;
+  }
+}
+- (BOOL)isInfoWindowShown {
+  return _mapView.selectedMarker == _marker;
+}
 - (BOOL)consumeTapEvents {
   return _consumeTapEvents;
 }
@@ -300,6 +311,38 @@
     [_methodChannel invokeMethod:@"infoWindow#onTap" arguments:@{@"markerId" : markerId}];
   }
 }
+- (void)showMarkerInfoWindow:(NSString*)markerId result:(FlutterResult)result {
+  FLTGoogleMapMarkerController* controller = _markerIdToController[markerId];
+  if (controller) {
+    [controller showInfoWindow];
+    result(nil);
+  } else {
+    result([FlutterError errorWithCode:@"Invalid markerId"
+                               message:@"showInfoWindow called with invalid markerId"
+                               details:nil]);
+  }
+}
+- (void)hideMarkerInfoWindow:(NSString*)markerId result:(FlutterResult)result {
+  FLTGoogleMapMarkerController* controller = _markerIdToController[markerId];
+  if (controller) {
+    [controller hideInfoWindow];
+    result(nil);
+  } else {
+    result([FlutterError errorWithCode:@"Invalid markerId"
+                               message:@"hideInfoWindow called with invalid markerId"
+                               details:nil]);
+  }
+}
+- (void)isMarkerInfoWindowShown:(NSString*)markerId result:(FlutterResult)result {
+  FLTGoogleMapMarkerController* controller = _markerIdToController[markerId];
+  if (controller) {
+    result(@([controller isInfoWindowShown]));
+  } else {
+    result([FlutterError errorWithCode:@"Invalid markerId"
+                               message:@"isInfoWindowShown called with invalid markerId"
+                               details:nil]);
+  }
+}
 
 + (CLLocationCoordinate2D)getPosition:(NSDictionary*)marker {
   NSArray* position = marker[@"position"];
diff --git a/packages/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/lib/src/controller.dart
index c6c64a5..d451e3b 100644
--- a/packages/google_maps_flutter/lib/src/controller.dart
+++ b/packages/google_maps_flutter/lib/src/controller.dart
@@ -236,4 +236,46 @@
         'map#getLatLng', screenCoordinate._toJson());
     return LatLng(latLng[0], latLng[1]);
   }
+
+  /// Programmatically show the Info Window for a [Marker].
+  ///
+  /// The `markerId` must match one of the markers on the map.
+  /// An invalid `markerId` triggers an "Invalid markerId" error.
+  ///
+  /// * See also:
+  ///   * [hideMarkerInfoWindow] to hide the Info Window.
+  ///   * [isMarkerInfoWindowShown] to check if the Info Window is showing.
+  Future<void> showMarkerInfoWindow(MarkerId markerId) async {
+    assert(markerId != null);
+    await channel.invokeMethod<void>(
+        'markers#showInfoWindow', <String, String>{'markerId': markerId.value});
+  }
+
+  /// Programmatically hide the Info Window for a [Marker].
+  ///
+  /// The `markerId` must match one of the markers on the map.
+  /// An invalid `markerId` triggers an "Invalid markerId" error.
+  ///
+  /// * See also:
+  ///   * [showMarkerInfoWindow] to show the Info Window.
+  ///   * [isMarkerInfoWindowShown] to check if the Info Window is showing.
+  Future<void> hideMarkerInfoWindow(MarkerId markerId) async {
+    assert(markerId != null);
+    await channel.invokeMethod<void>(
+        'markers#hideInfoWindow', <String, String>{'markerId': markerId.value});
+  }
+
+  /// Returns `true` when the [InfoWindow] is showing, `false` otherwise.
+  ///
+  /// The `markerId` must match one of the markers on the map.
+  /// An invalid `markerId` triggers an "Invalid markerId" error.
+  ///
+  /// * See also:
+  ///   * [showMarkerInfoWindow] to show the Info Window.
+  ///   * [hideMarkerInfoWindow] to hide the Info Window.
+  Future<bool> isMarkerInfoWindowShown(MarkerId markerId) async {
+    assert(markerId != null);
+    return await channel.invokeMethod<bool>('markers#isInfoWindowShown',
+        <String, String>{'markerId': markerId.value});
+  }
 }
diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml
index 31d0f82..d3e751e 100644
--- a/packages/google_maps_flutter/pubspec.yaml
+++ b/packages/google_maps_flutter/pubspec.yaml
@@ -1,7 +1,7 @@
 name: google_maps_flutter
 description: A Flutter plugin for integrating Google Maps in iOS and Android applications.
 homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter
-version: 0.5.22+3
+version: 0.5.23
 
 dependencies:
   flutter: