package com.airbnb.android.airmapview; import android.annotation.SuppressLint; import android.graphics.Bitmap; import android.graphics.Point; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.webkit.GeolocationPermissions; import android.webkit.JavascriptInterface; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import com.airbnb.android.airmapview.listeners.InfoWindowCreator; import com.airbnb.android.airmapview.listeners.OnCameraChangeListener; import com.airbnb.android.airmapview.listeners.OnInfoWindowClickListener; import com.airbnb.android.airmapview.listeners.OnLatLngScreenLocationCallback; import com.airbnb.android.airmapview.listeners.OnMapBoundsCallback; import com.airbnb.android.airmapview.listeners.OnMapClickListener; import com.airbnb.android.airmapview.listeners.OnMapLoadedListener; import com.airbnb.android.airmapview.listeners.OnMapMarkerClickListener; import com.airbnb.android.airmapview.listeners.OnMapMarkerDragListener; import com.airbnb.android.airmapview.listeners.OnSnapshotReadyListener; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.HashMap; import java.util.Locale; import java.util.Map; public abstract class WebViewMapFragment extends Fragment implements AirMapInterface { private static final String TAG = WebViewMapFragment.class.getSimpleName(); protected WebView webView; private ViewGroup mLayout; private OnMapClickListener onMapClickListener; private OnCameraChangeListener onCameraChangeListener; private OnMapLoadedListener onMapLoadedListener; private OnMapMarkerClickListener onMapMarkerClickListener; private OnMapMarkerDragListener onMapMarkerDragListener; private OnInfoWindowClickListener onInfoWindowClickListener; private InfoWindowCreator infoWindowCreator; private OnMapBoundsCallback onMapBoundsCallback; private OnLatLngScreenLocationCallback onLatLngScreenLocationCallback; private LatLng center; private int zoom; private boolean loaded; private boolean ignoreNextMapMove; private View infoWindowView; private final Map<Long, AirMapMarker<?>> markers = new HashMap<>(); private boolean trackUserLocation = false; public WebViewMapFragment setArguments(AirMapType mapType) { setArguments(mapType.toBundle()); return this; } public class GeoWebChromeClient extends WebChromeClient { @Override public void onGeolocationPermissionsShowPrompt( String origin, GeolocationPermissions.Callback callback) { // Always grant permission since the app itself requires location // permission and the user has therefore already granted it callback.invoke(origin, true, false); } } @SuppressLint({ "SetJavaScriptEnabled", "AddJavascriptInterface" }) @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_webview, container, false); webView = (WebView) view.findViewById(R.id.webview); mLayout = (ViewGroup) view; WebSettings webViewSettings = webView.getSettings(); webViewSettings.setSupportZoom(true); webViewSettings.setBuiltInZoomControls(false); webViewSettings.setJavaScriptEnabled(true); webViewSettings.setGeolocationEnabled(true); webViewSettings.setAllowFileAccess(false); webViewSettings.setAllowContentAccess(false); webView.setWebChromeClient(new GeoWebChromeClient()); AirMapType mapType = AirMapType.fromBundle(getArguments()); webView.loadDataWithBaseURL(mapType.getDomain(), mapType.getMapData(getResources()), "text/html", "base64", null); webView.addJavascriptInterface(new MapsJavaScriptInterface(), "AirMapView"); return view; } public int getZoom() { return zoom; } public LatLng getCenter() { return center; } public void setCenter(LatLng latLng) { webView.loadUrl(String.format(Locale.US, "javascript:centerMap(%1$f, %2$f);", latLng.latitude, latLng.longitude)); } public void animateCenter(LatLng latLng) { setCenter(latLng); } public void setZoom(int zoom) { webView.loadUrl(String.format(Locale.US, "javascript:setZoom(%1$d);", zoom)); } public void drawCircle(LatLng latLng, int radius) { drawCircle(latLng, radius, CIRCLE_BORDER_COLOR); } @Override public void drawCircle(LatLng latLng, int radius, int borderColor) { drawCircle(latLng, radius, borderColor, CIRCLE_BORDER_WIDTH); } @Override public void drawCircle(LatLng latLng, int radius, int borderColor, int borderWidth) { drawCircle(latLng, radius, borderColor, borderWidth, CIRCLE_FILL_COLOR); } @Override public void drawCircle(LatLng latLng, int radius, int borderColor, int borderWidth, int fillColor) { webView.loadUrl( String.format(Locale.US, "javascript:addCircle(%1$f, %2$f, %3$d, %4$d, %5$d, %6$d);", latLng.latitude, latLng.longitude, radius, borderColor, borderWidth, fillColor)); } public void highlightMarker(long markerId) { if (markerId != -1) { webView.loadUrl(String.format(Locale.US, "javascript:highlightMarker(%1$d);", markerId)); } } public void unhighlightMarker(long markerId) { if (markerId != -1) { webView.loadUrl(String.format(Locale.US, "javascript:unhighlightMarker(%1$d);", markerId)); } } @Override public boolean isInitialized() { return webView != null && loaded; } @Override public void addMarker(AirMapMarker<?> marker) { LatLng latLng = marker.getLatLng(); markers.put(marker.getId(), marker); webView.loadUrl( String.format(Locale.US, "javascript:addMarkerWithId(%1$f, %2$f, %3$d, '%4$s', '%5$s', %6$b);", latLng.latitude, latLng.longitude, marker.getId(), marker.getTitle(), marker.getSnippet(), marker.getMarkerOptions().isDraggable())); } @Override public void moveMarker(AirMapMarker<?> marker, LatLng to) { marker.setLatLng(to); webView.loadUrl( String.format(Locale.US, "javascript:moveMarker(%1$f, %2$f, '%3$s');", to.latitude, to.longitude, marker.getId())); } @Override public void removeMarker(AirMapMarker<?> marker) { markers.remove(marker.getId()); webView.loadUrl(String.format(Locale.US, "javascript:removeMarker(%1$d);", marker.getId())); } public void clearMarkers() { markers.clear(); webView.loadUrl("javascript:clearMarkers();"); } public void setOnCameraChangeListener(OnCameraChangeListener listener) { onCameraChangeListener = listener; } public void setOnMapLoadedListener(OnMapLoadedListener listener) { onMapLoadedListener = listener; if (loaded) { onMapLoadedListener.onMapLoaded(); } } @Override public void setCenterZoom(LatLng latLng, int zoom) { setCenter(latLng); setZoom(zoom); } @Override public void animateCenterZoom(LatLng latLng, int zoom) { setCenterZoom(latLng, zoom); } public void setOnMarkerClickListener(OnMapMarkerClickListener listener) { onMapMarkerClickListener = listener; } @Override public void setOnMarkerDragListener(OnMapMarkerDragListener listener) { onMapMarkerDragListener = listener; } @Override public void setPadding(int left, int top, int right, int bottom) { // no-op } @Override public void setMyLocationEnabled(boolean trackUserLocationEnabled) { trackUserLocation = trackUserLocationEnabled; if (trackUserLocationEnabled) { RuntimePermissionUtils.checkLocationPermissions(getActivity(), this); } else { webView.loadUrl("javascript:stopTrackingUserLocation();"); } } @Override public void onLocationPermissionsGranted() { webView.loadUrl("javascript:startTrackingUserLocation();"); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); RuntimePermissionUtils.onRequestPermissionsResult(this, requestCode, grantResults); } @Override public boolean isMyLocationEnabled() { return trackUserLocation; } @Override public void setMapToolbarEnabled(boolean enabled) { // no-op } @Override public <T> void addPolyline(AirMapPolyline<T> polyline) { try { JSONArray array = new JSONArray(); for (LatLng point : polyline.getPoints()) { JSONObject json = new JSONObject(); json.put("lat", point.latitude); json.put("lng", point.longitude); array.put(json); } webView.loadUrl(String.format( "javascript:addPolyline(" + array.toString() + ", %1$d, %2$d, %3$d);", polyline.getId(), polyline.getStrokeWidth(), polyline.getStrokeColor())); } catch (JSONException e) { Log.e(TAG, "error constructing polyline JSON", e); } } @Override public <T> void removePolyline(AirMapPolyline<T> polyline) { webView.loadUrl(String.format(Locale.US, "javascript:removePolyline(%1$d);", polyline.getId())); } @Override public <T> void addPolygon(AirMapPolygon<T> polygon) { try { JSONArray array = new JSONArray(); for (LatLng point : polygon.getPolygonOptions().getPoints()) { JSONObject json = new JSONObject(); json.put("lat", point.latitude); json.put("lng", point.longitude); array.put(json); } webView.loadUrl(String.format(Locale.US, "javascript:addPolygon(" + array.toString() + ", %1$d, %2$d, %3$d, %4$d);", polygon.getId(), (int) polygon.getPolygonOptions().getStrokeWidth(), polygon.getPolygonOptions().getStrokeColor(), polygon.getPolygonOptions().getFillColor())); } catch (JSONException e) { Log.e(TAG, "error constructing polyline JSON", e); } } @Override public <T> void removePolygon(AirMapPolygon<T> polygon) { webView.loadUrl(String.format(Locale.US, "javascript:removePolygon(%1$d);", polygon.getId())); } @Override public void setOnMapClickListener(final OnMapClickListener listener) { onMapClickListener = listener; } public void setOnInfoWindowClickListener(OnInfoWindowClickListener listener) { onInfoWindowClickListener = listener; } @Override public void setInfoWindowCreator(GoogleMap.InfoWindowAdapter adapter, InfoWindowCreator creator) { infoWindowCreator = creator; } @Override public void getMapScreenBounds(OnMapBoundsCallback callback) { onMapBoundsCallback = callback; webView.loadUrl("javascript:getBounds();"); } @Override public void getScreenLocation(LatLng latLng, OnLatLngScreenLocationCallback callback) { onLatLngScreenLocationCallback = callback; webView.loadUrl(String.format(Locale.US, "javascript:getScreenLocation(%1$f, %2$f);", latLng.latitude, latLng.longitude)); } @Override public void setCenter(LatLngBounds bounds, int boundsPadding) { webView.loadUrl(String.format(Locale.US, "javascript:setBounds(%1$f, %2$f, %3$f, %4$f);", bounds.northeast.latitude, bounds.northeast.longitude, bounds.southwest.latitude, bounds.southwest.longitude)); } protected boolean isChinaMode() { return false; } private class MapsJavaScriptInterface { private final Handler handler = new Handler(Looper.getMainLooper()); @JavascriptInterface public boolean isChinaMode() { return WebViewMapFragment.this.isChinaMode(); } @JavascriptInterface public void onMapLoaded() { handler.post(new Runnable() { @Override public void run() { if (!loaded) { loaded = true; if (onMapLoadedListener != null) { onMapLoadedListener.onMapLoaded(); } } } }); } @JavascriptInterface public void mapClick(final double lat, final double lng) { handler.post(new Runnable() { @Override public void run() { if (onMapClickListener != null) { onMapClickListener.onMapClick(new LatLng(lat, lng)); } if (infoWindowView != null) { mLayout.removeView(infoWindowView); } } }); } @JavascriptInterface public void getBoundsCallback( double neLat, double neLng, double swLat, double swLng) { final LatLngBounds bounds = new LatLngBounds(new LatLng(swLat, swLng), new LatLng(neLat, neLng)); handler.post(new Runnable() { @Override public void run() { onMapBoundsCallback.onMapBoundsReady(bounds); } }); } @JavascriptInterface public void getLatLngScreenLocationCallback(int x, int y) { final Point point = new Point(x, y); handler.post(new Runnable() { @Override public void run() { onLatLngScreenLocationCallback.onLatLngScreenLocationReady(point); } }); } @JavascriptInterface public void mapMove(double lat, double lng, int zoom) { center = new LatLng(lat, lng); WebViewMapFragment.this.zoom = zoom; handler.post(new Runnable() { @Override public void run() { if (onCameraChangeListener != null) { onCameraChangeListener.onCameraChanged(center, WebViewMapFragment.this.zoom); } if (ignoreNextMapMove) { ignoreNextMapMove = false; return; } if (infoWindowView != null) { mLayout.removeView(infoWindowView); } } }); } @JavascriptInterface public void markerClick(long markerId) { final AirMapMarker<?> airMapMarker = markers.get(markerId); handler.post(new Runnable() { @Override public void run() { if (onMapMarkerClickListener != null) { onMapMarkerClickListener.onMapMarkerClick(airMapMarker); } if (infoWindowView != null) { mLayout.removeView(infoWindowView); } // TODO convert to custom dialog fragment if (infoWindowCreator != null) { infoWindowView = infoWindowCreator.createInfoWindow(airMapMarker); if (infoWindowView != null) { mLayout.addView(infoWindowView); infoWindowView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(@NonNull View v) { if (onInfoWindowClickListener != null) { onInfoWindowClickListener.onInfoWindowClick(airMapMarker); } } }); } } else { webView.loadUrl( String.format(Locale.US, "javascript:showDefaultInfoWindow(%1$d);", airMapMarker.getId())); } ignoreNextMapMove = true; } }); } @JavascriptInterface public void markerDragStart(final long markerId, final double lat, final double lng) { handler.post(new Runnable() { @Override public void run() { if (onMapMarkerDragListener != null) { onMapMarkerDragListener.onMapMarkerDragStart(markerId, new LatLng(lat, lng)); } } }); } @JavascriptInterface public void markerDrag(final long markerId, final double lat, final double lng) { handler.post(new Runnable() { @Override public void run() { if (onMapMarkerDragListener != null) { onMapMarkerDragListener.onMapMarkerDrag(markerId, new LatLng(lat, lng)); } } }); } @JavascriptInterface public void markerDragEnd(final long markerId, final double lat, final double lng) { handler.post(new Runnable() { @Override public void run() { if (onMapMarkerDragListener != null) { onMapMarkerDragListener.onMapMarkerDragEnd(markerId, new LatLng(lat, lng)); } } }); } @JavascriptInterface public void defaultInfoWindowClick(long markerId) { final AirMapMarker<?> airMapMarker = markers.get(markerId); handler.post(new Runnable() { @Override public void run() { if (onInfoWindowClickListener != null) { onInfoWindowClickListener.onInfoWindowClick(airMapMarker); } } }); } } @Override public void setGeoJsonLayer(AirMapGeoJsonLayer layer) { // clear any existing layer clearGeoJsonLayer(); webView.loadUrl(String.format(Locale.US, "javascript:addGeoJsonLayer(%1$s, %2$f, %3$d, %4$d);", layer.geoJson, layer.strokeWidth, layer.strokeColor, layer.fillColor)); } @Override public void clearGeoJsonLayer() { webView.loadUrl("javascript:removeGeoJsonLayer();"); } @Override public void getSnapshot(OnSnapshotReadyListener listener) { boolean isDrawingCacheEnabled = webView.isDrawingCacheEnabled(); webView.setDrawingCacheEnabled(true); // copy to a new bitmap, otherwise the bitmap will be // destroyed when the drawing cache is destroyed // webView.getDrawingCache can return null if drawing cache is disabled or if the size is 0 Bitmap bitmap = webView.getDrawingCache(); Bitmap newBitmap = null; if (bitmap != null) { newBitmap = bitmap.copy(Bitmap.Config.RGB_565, false); } webView.destroyDrawingCache(); webView.setDrawingCacheEnabled(isDrawingCacheEnabled); listener.onSnapshotReady(newBitmap); } }