package org.azavea.otm.ui; import android.app.Activity; import android.app.Fragment; import android.app.ProgressDialog; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.BitmapFactory; import android.location.Geocoder; import android.location.Location; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.SearchView; import android.widget.TextView; import android.widget.Toast; import com.atlassian.fugue.Either; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.MapView; import com.google.android.gms.maps.MapsInitializer; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.android.gms.maps.model.TileOverlay; import com.google.android.gms.maps.model.TileOverlayOptions; import com.joelapenna.foursquared.widget.SegmentedButton; import com.loopj.android.http.BinaryHttpResponseHandler; import com.loopj.android.http.JsonHttpResponseHandler; import org.azavea.helpers.GoogleMapsListeners; import org.azavea.helpers.Logger; import org.azavea.map.FilterableTMSTileProvider; import org.azavea.map.TMSTileProvider; import org.azavea.otm.App; import org.azavea.otm.R; import org.azavea.otm.data.Geometry; import org.azavea.otm.data.InstanceInfo; import org.azavea.otm.data.Plot; import org.azavea.otm.data.PlotContainer; import org.azavea.otm.map.FallbackGeocoder; import org.azavea.otm.rest.RequestGenerator; import org.azavea.otm.rest.handlers.ContainerRestHandler; import org.azavea.otm.rest.handlers.LoggingJsonHttpResponseHandler; import org.jdeferred.Deferred; import org.jdeferred.DeferredManager; import org.jdeferred.Promise; import org.jdeferred.android.AndroidDeferredManager; import org.jdeferred.impl.DeferredObject; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.net.MalformedURLException; import java.util.Arrays; import java.util.Collection; import java.util.Locale; import java.util.concurrent.TimeoutException; import cz.msebera.android.httpclient.Header; public class MainMapFragment extends Fragment implements GoogleApiClient.ConnectionCallbacks { private static final int STREET_ZOOM_LEVEL = 17; private static final int FILTER_INTENT = 1; private static final int INFO_INTENT = 2; private static final int ADD_INTENT = 3; // modes for the add tree marker feature private static final int STEP1 = 1; private static final int STEP2 = 2; private static final int CANCEL = 3; private static final int STEP3 = 4; private Menu menu; private SearchView searchView; private TextView plotSpeciesView; private TextView plotAddressView; private ImageView plotImageView; private RelativeLayout plotPopup; private Plot currentPlot; // The Plot we're currently showing a pop-up for, if any private Marker plotMarker; private MapView mapView; private TextView filterDisplay; private int treeAddMode = CANCEL; private GoogleApiClient mGoogleApiClient; private Deferred<Location, Throwable, Void> mLocationDeferred; // the map setup can last the entire life of the fragment instance private Deferred<GoogleMap, Throwable, Void> mMapSetupDeferred = new DeferredObject<>(); private Promise<InstanceInfo, Throwable, Void> mInstanceLoadPromise; FilterableTMSTileProvider filterTileProvider; TMSTileProvider boundaryTileProvider; TMSTileProvider canopyTileProvider; TileOverlay filterTileOverlay; TileOverlay canopyTileOverlay; TileOverlay boundaryTileOverlay; public void onBackPressed() { hidePopup(); removePlotMarker(); mMapSetupDeferred.promise().done(map -> setTreeAddMode(CANCEL, map)); searchView.setIconified(true); } public boolean shouldHandleBackPress() { return treeAddMode != CANCEL || currentPlot != null || !searchView.isIconified(); } /** * **************************************************** * Overrides for the Fragment base class * ***************************************************** */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.setHasOptionsMenu(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); return inflater.inflate(R.layout.main_map, container, false); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { this.menu = menu; inflater.inflate(R.menu.main_menu, menu); setupSearchView(menu); super.onCreateOptionsMenu(menu, inflater); } @Override public boolean onOptionsItemSelected(MenuItem item) { final int id = item.getItemId(); if (id == R.id.filterButton) { Intent filter = new Intent(getActivity(), FilterDisplay.class); startActivityForResult(filter, FILTER_INTENT); return true; } else if (id == R.id.addTreeButton) { mMapSetupDeferred.promise().done(this::handleAddTree); return true; } else { return false; } } // Called after onCreateView, so getView() is guaranteed to be non-null. // // Unlike activities, only our own app is responsible for fragment life cycle. // We never detach this fragment from an activity and later reattach it, // so onActivityCreated only gets called once in the life cycle of the fragment. // // If we ever do start detaching and re-attaching it, or re-intenting its parent `TabLayout`, // more guards must be put in place. @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // In the highly unlikely event that tile provider urls are malformed, fail fast. if (!setupTileProviders(mMapSetupDeferred)) { return; } Deferred<InstanceInfo, Throwable, Void> instanceLoadDeferred = new DeferredObject<>(); mInstanceLoadPromise = instanceLoadDeferred.promise(); mLocationDeferred = new DeferredObject<>(); final Promise<Location, Throwable, Void> locationPromise = mLocationDeferred.promise(); final Promise<GoogleMap, Throwable, Void> mapSetupPromise = mMapSetupDeferred.promise(); final DeferredManager dm = new AndroidDeferredManager(); final int INSTANCE = 0; final ProgressDialog dialog = ProgressDialog.show(getActivity(), "", "Loading Map Info...", true); if (mGoogleApiClient == null) { mGoogleApiClient = new GoogleApiClient.Builder(this.getActivity()) .addApi(LocationServices.API) .addConnectionCallbacks(this) .build(); } App.getAppInstance().ensureInstanceLoaded(result -> { if (result != null && result.getData().getBoolean("success")) { instanceLoadDeferred.resolve(App.getCurrentInstance()); } else { instanceLoadDeferred.reject(new Throwable("Instance load failed")); return false; } return true; }); // Functionality dependent on the GoogleMap but not on the treeMap InstanceInfo. mapSetupPromise.then(map -> { setUpMap(getView(), map); setupControls(); }); // Functionality dependent on both. dm.when(mInstanceLoadPromise, mapSetupPromise).then(results -> { // Will execute immediately, since we got here thanks to the promise being resolved. // Simply a way of retrieving a correctly typed map. mapSetupPromise.done(map -> { setupViewHandlers(getView(), map); setProviderDisplayParameters(); InstanceInfo treeMap = InstanceInfo.class.cast(results.get(INSTANCE).getResult()); initLocation(treeMap, locationPromise, map).always((state, resolved, rejected) -> dialog.dismiss()); }); }, failure -> { dialog.dismiss(); Throwable e = Throwable.class.cast(failure.getReject()); Logger.error("Unable to setup map", e); Toast.makeText(App.getAppInstance(), R.string.map_failed, Toast.LENGTH_SHORT).show(); }); // Since Google Play services is required in order to obtain a GoogleMap, // check whether it is correctly installed and give the user a chance to rectify it. // // If it is not, the MapHelper will show a dialog which will redirect to an activity in // another app (play store or system settings) to rectify the situation. // A user can return to this Activity and Fragment after following the prompt and correctly // installing/updating/enabling the Google Play services. // The MapHelper does not tell whether the situation has been rectified, // so a future attempt to fetch a GoogleMap still may fail. Activity activity = getActivity(); MapsInitializer.initialize(activity); MapHelper.checkGooglePlay(activity); mapView = (MapView) getView().findViewById(R.id.map); mapView.onCreate(savedInstanceState); // If the above MapHelper check did not rectify an incorrect Google Play installation, // the same prompt may be shown during getMapAsync. mapView.getMapAsync(map -> { if (map != null) { mMapSetupDeferred.resolve(map); } else { Logger.warning("Map was null, missing google play support?"); mMapSetupDeferred.reject(new Exception("Map was null, missing google play support?")); } }); } // Inevitably called after onActivityCreated. Means the fragment is visible. @Override public void onStart() { super.onStart(); // Can happen in the extremely unlikely event that tile providers setup failed, // or in the equally unlikely event that Google Play services is not setup correctly // and the user failed to fix it when prompted by checkGooglePlay. if (mMapSetupDeferred.promise().isRejected()) { return; } // mInstanceLoadPromise could be rejected if ensureInstanceLoaded failed fast if (mInstanceLoadPromise.isRejected()) { return; } mapView.onStart(); // Even though the deferred is only resolved once, // a done handler gets called every time .done is called thereafter. mMapSetupDeferred.promise().done(map -> map.setMyLocationEnabled(true)); // Should result in onConnect being called. mGoogleApiClient.connect(); } // Usually called after onStart. Means the fragment got the focus. @Override public void onResume() { super.onResume(); mapView.onResume(); } @Override public void onStop() { super.onStop(); // Even though the deferred is only resolved once, // a done handler gets called every time .done is called thereafter. mMapSetupDeferred.promise().done(map -> { map.setMyLocationEnabled(false); }); mapView.onStop(); mGoogleApiClient.disconnect(); } @Override // Shown when the user has added a tree, set a filter, or clicked on a tree // in response to startActivityForResult public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); mMapSetupDeferred.promise().done(map -> { switch (requestCode) { case FILTER_INTENT: if (resultCode == Activity.RESULT_OK) { Collection<Either<JSONObject, JSONArray>> activeFilters = App.getFilterManager().getActiveFilters(); setFilterDisplay(App.getFilterManager().getActiveFilterDisplay()); filterTileProvider.setParameters(activeFilters); reloadTiles(map); } break; case INFO_INTENT: if (resultCode == TreeDisplay.RESULT_PLOT_EDITED) { showPlotFromIntent(data, map); } else if (resultCode == TreeDisplay.RESULT_PLOT_DELETED) { hidePopup(); reloadTiles(map); // TODO: Do we need to refresh the map tile? } break; case ADD_INTENT: if (resultCode == Activity.RESULT_OK) { reloadTiles(map); showPlotFromIntent(data, map); setTreeAddMode(CANCEL, map); } } }); } // If you use a MapView directly, you need to forward it events @Override public void onDestroy() { super.onDestroy(); if (mapView != null) { mapView.onDestroy(); } } @Override public void onLowMemory() { super.onLowMemory(); if (mapView != null) { mapView.onLowMemory(); } } @Override public void onPause() { super.onPause(); if (mapView != null) { mapView.onPause(); } } @Override public void onSaveInstanceState(Bundle outState) { if (mapView != null) { mapView.onSaveInstanceState(outState); } super.onSaveInstanceState(outState); } /** * *************************************************************** * Overrides for the GoogleApiClient.ConnectionCallbacks interface * *************************************************************** */ @Override public void onConnected(Bundle bundle) { int WAIT_DURATION = 5000; // 5 seconds LocationRequest request = LocationRequest.create() .setNumUpdates(1) .setExpirationDuration(WAIT_DURATION) .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); LocationServices.FusedLocationApi .requestLocationUpdates(mGoogleApiClient, request, loc -> { if (mLocationDeferred.isPending()) { mLocationDeferred.resolve(loc); } }); new Handler().postDelayed(() -> { if (mLocationDeferred.isPending()) { mLocationDeferred.reject(new TimeoutException("Location request timed out")); } }, WAIT_DURATION); } @Override public void onConnectionSuspended(int i) { // Implement the interface } /********************************* * Private methods *********************************/ private void showPlotFromIntent(Intent data, GoogleMap map) { try { // The plot was updated, so update the pop-up with any new data String plotJSON = data.getExtras().getString("plot"); Plot updatedPlot = new Plot(new JSONObject(plotJSON)); showPopup(updatedPlot, map); } catch (JSONException e) { Logger.error("Unable to deserialze updated plot for map popup", e); hidePopup(); } } private void showPopupOnMap(LatLng point, GoogleMap map) { Log.d("TREE_CLICK", "(" + point.latitude + "," + point.longitude + ")"); final ProgressDialog dialog = ProgressDialog.show(getActivity(), "", "Loading. Please wait...", true); dialog.show(); new RequestGenerator().getPlotsNearLocation( point.latitude, point.longitude, null, new ContainerRestHandler<PlotContainer>(new PlotContainer()) { @Override public void failure(Throwable e, String message) { dialog.hide(); Log.e("TREE_CLICK", "Error retrieving plots on map touch event: ", e); } @Override public void dataReceived(PlotContainer response) { try { Plot plot = response.getFirst(); if (plot != null) { Log.d("TREE_CLICK", "plot: " + plot.getTitle()); showPopup(plot, map); } else { Log.d("TREE_CLICK", "null plot"); hidePopup(); } } catch (JSONException e) { Logger.error("Error retrieving plot info on map touch event: ", e); } finally { dialog.hide(); } } } ); } private boolean setupTileProviders(Deferred<GoogleMap, Throwable, Void> mapDeferred) { final SharedPreferences prefs = App.getSharedPreferences(); final String baseTileUrl = prefs.getString("tiler_url", null); final String boundaryFeature = prefs.getString("boundary_feature", null); final String plotFeature = prefs.getString("plot_feature", null); try { boundaryTileProvider = new TMSTileProvider(baseTileUrl, boundaryFeature); canopyTileProvider = new TMSTileProvider(baseTileUrl, plotFeature); if (filterTileProvider == null) { filterTileProvider = new FilterableTMSTileProvider(baseTileUrl, plotFeature); } } catch (MalformedURLException e) { e.printStackTrace(); Logger.error("Unable to setup map tile provider, baseTileUrl: " + baseTileUrl, e); Toast.makeText(App.getAppInstance(), R.string.map_failed, Toast.LENGTH_SHORT).show(); mapDeferred.reject(e); return false; } return true; } // Sets up the filterDisplay, plotPopup, and plot related views. // Called from onActivityCreated, so getView() is guaranteed to return non-null. private void setupControls() { final View view = getView(); filterDisplay = (TextView) view.findViewById(R.id.filterDisplay); plotPopup = (RelativeLayout) view.findViewById(R.id.plotPopup); plotSpeciesView = (TextView) view.findViewById(R.id.plotSpecies); plotAddressView = (TextView) view.findViewById(R.id.plotAddress); plotImageView = (ImageView) view.findViewById(R.id.plotImage); } private void setProviderDisplayParameters() { String[] displayFilters = App.getAppInstance().getResources().getStringArray(R.array.display_filters); canopyTileProvider.setDisplayParameters(Arrays.asList(displayFilters)); filterTileProvider.setDisplayParameters(Arrays.asList(displayFilters)); } private void reloadTiles(GoogleMap map) { map.clear(); setupMapOverlays(map); setupCanopyOverlay(map); } private void hideMenuItems() { if (menu != null) { menu.setGroupVisible(R.id.main_map_menu_group, false); } } private void showMenuItems() { if (menu != null) { menu.setGroupVisible(R.id.main_map_menu_group, true); } } private void setUpMap(View view, GoogleMap map) { map.setMyLocationEnabled(true); map.getUiSettings().setZoomControlsEnabled(false); map.setOnMarkerDragListener(new GoogleMapsListeners.NoopDragListener()); setupMapOverlays(map); // Set up the default click listener map.setOnMapClickListener(point -> { showPopupOnMap(point, map); }); SegmentedButton buttons = (SegmentedButton) view.findViewById(R.id.basemap_controls); MapHelper.setUpBasemapControls(buttons, map); } private void setupMapOverlays(GoogleMap map) { try { boundaryTileOverlay = map.addTileOverlay( new TileOverlayOptions().tileProvider(boundaryTileProvider).zIndex(0)); filterTileOverlay = map.addTileOverlay(new TileOverlayOptions().tileProvider(filterTileProvider)); } catch (Exception e) { Logger.error("Error Setting Up Basemap", e); Toast.makeText(getActivity(), "Error Setting Up Base Map", Toast.LENGTH_LONG).show(); } } private void setupCanopyOverlay(GoogleMap map) { try { // Canopy layer shows all trees, is always on, but is 'dimmed' while a filter is active canopyTileOverlay = map.addTileOverlay( new TileOverlayOptions() .tileProvider(canopyTileProvider) .zIndex(50) .transparency(0.7f)); } catch (Exception e) { Logger.error("Error setting up transparent canopy layer", e); Toast.makeText(getActivity(), "Error Setting Up Basemap", Toast.LENGTH_LONG).show(); } } private Promise<Location, Throwable, Void> initLocation( InstanceInfo treeMap, Promise<Location, Throwable, Void> locationPromise, GoogleMap map) { SharedPreferences prefs = App.getSharedPreferences(); int startingZoomLevel = Integer.parseInt(prefs.getString("starting_zoom_level", "12")); map.moveCamera(CameraUpdateFactory.newLatLngZoom(App.getCurrentInstance().getStartPos(), startingZoomLevel)); locationPromise.then(loc -> { LatLng latlng = new LatLng(loc.getLatitude(), loc.getLongitude()); if (treeMap.getExtent().contains(latlng)) { map.moveCamera(CameraUpdateFactory.newLatLngZoom(latlng, STREET_ZOOM_LEVEL)); } }); return locationPromise; } private void setupViewHandlers(View view, GoogleMap map) { view.findViewById(R.id.plotImage).setOnClickListener(v -> { if (currentPlot != null) { currentPlot.getTreePhoto(MapHelper.getPhotoDetailHandler(getActivity(), currentPlot)); } }); view.findViewById(R.id.treeAddNext).setOnClickListener(v -> setTreeAddMode(STEP3, map)); view.findViewById(R.id.plotPopup).setOnClickListener(v -> { // Show TreeInfoDisplay with current plot Intent viewPlot = new Intent(getActivity(), TreeInfoDisplay.class); viewPlot.putExtra("plot", currentPlot.getData().toString()); if (App.getLoginManager().isLoggedIn()) { viewPlot.putExtra("user", App.getLoginManager().loggedInUser.getData().toString()); } startActivityForResult(viewPlot, INFO_INTENT); }); } private void handleAddTree(GoogleMap map) { if (!App.getLoginManager().isLoggedIn()) { startActivity(new Intent(getActivity(), LoginActivity.class)); } else if (!App.getCurrentInstance().canAddTree()) { Toast.makeText(getActivity(), getString(R.string.perms_add_tree_fail), Toast.LENGTH_SHORT).show(); } else { setTreeAddMode(CANCEL, map); setTreeAddMode(STEP1, map); } } private void showPopup(Plot plot, GoogleMap map) { //set default text plotSpeciesView.setText(getString(R.string.species_missing)); plotAddressView.setText(getString(R.string.address_missing)); plotImageView.setImageResource(R.drawable.missing_tree_photo); try { String addr = plot.getAddress(); if (!TextUtils.isEmpty(addr)) { plotAddressView.setText(addr); } String speciesName = plot.getTitle(); plotSpeciesView.setText(speciesName); showImageOnPlotPopup(plot); LatLng position = zoomToPlot(plot, map); removePlotMarker(); plotMarker = map.addMarker(new MarkerOptions() .position(position) .title("") .icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_mapmarker))); } catch (JSONException e) { Logger.error("Could not show tree popup", e); } currentPlot = plot; plotPopup.setVisibility(View.VISIBLE); } private LatLng zoomToPlot(Plot plot, GoogleMap map) throws JSONException { LatLng position = new LatLng(plot.getGeometry().getY(), plot.getGeometry().getX()); if (map.getCameraPosition().zoom >= STREET_ZOOM_LEVEL) { map.animateCamera(CameraUpdateFactory.newLatLng(position)); } else { map.animateCamera(CameraUpdateFactory.newLatLngZoom(position, STREET_ZOOM_LEVEL)); } return position; } private void hidePopup() { RelativeLayout plotPopup = (RelativeLayout) getActivity().findViewById(R.id.plotPopup); plotPopup.setVisibility(View.INVISIBLE); currentPlot = null; } private void removePlotMarker() { if (plotMarker != null) { plotMarker.remove(); plotMarker = null; } } private void showImageOnPlotPopup(Plot plot) { plot.getTreeThumbnail(new BinaryHttpResponseHandler(Plot.IMAGE_TYPES) { @Override public void onSuccess(int statusCode, Header[] headers, byte[] imageData) { ImageView plotImage = (ImageView) getActivity().findViewById(R.id.plotImage); plotImage.setImageBitmap(BitmapFactory.decodeByteArray(imageData, 0, imageData.length)); } @Override public void onFailure(int statusCode, Header[] headers, byte[] imageData, Throwable e) { // Log the error, but not important enough to bother the user Logger.warning("Could not retreive tree image", e); } }); } private void setFilterDisplay(String activeFilterDisplay) { if (activeFilterDisplay.equals("")) { filterDisplay.setVisibility(View.GONE); } else { filterDisplay.setText(getString(R.string.filter_display_label) + " " + activeFilterDisplay); filterDisplay.setVisibility(View.VISIBLE); } } /* tree add modes: * CANCEL : not adding a tree * STEP1 : "Tap to add a tree" * STEP2 : "Long press to move the tree into position, then click next" * STEP3 : Create tree and redirect to tree detail page. */ private void setTreeAddMode(int step, GoogleMap map) { this.treeAddMode = step; View step1 = getActivity().findViewById(R.id.addTreeStep1); View step2 = getActivity().findViewById(R.id.addTreeStep2); switch (step) { case CANCEL: step1.setVisibility(View.GONE); step2.setVisibility(View.GONE); map.setOnMapClickListener(point -> showPopupOnMap(point, map)); showMenuItems(); break; case STEP1: hidePopup(); removePlotMarker(); hideMenuItems(); step2.setVisibility(View.GONE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { step1.setTranslationY(step1.getHeight()); step1.setVisibility(View.VISIBLE); step1.animate().translationY(0); } else { step1.setVisibility(View.VISIBLE); } map.setOnMapClickListener(point -> { Log.d("TREE_CLICK", "(" + point.latitude + "," + point.longitude + ")"); plotMarker = map.addMarker(new MarkerOptions() .position(point) .title("New Tree") .icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_mapmarker)) ); plotMarker.setDraggable(true); setTreeAddMode(STEP2, map); }); break; case STEP2: hidePopup(); hideMenuItems(); step1.setVisibility(View.GONE); step2.setVisibility(View.VISIBLE); map.setOnMapClickListener(null); break; case STEP3: Intent editPlotIntent = new Intent(getActivity(), TreeEditDisplay.class); Plot newPlot; try { newPlot = getPlotForNewTree(); String plotString = newPlot.getData().toString(); editPlotIntent.putExtra("plot", plotString); editPlotIntent.putExtra("new_tree", "1"); startActivityForResult(editPlotIntent, ADD_INTENT); } catch (Exception e) { Logger.error("Error creating tree", e); setTreeAddMode(CANCEL, map); Toast.makeText(getActivity(), "Error creating new tree", Toast.LENGTH_LONG).show(); } } } private Plot getPlotForNewTree() throws JSONException { Plot newPlot = new Plot(); Geometry newGeometry = new Geometry(); double lat = plotMarker.getPosition().latitude; double lon = plotMarker.getPosition().longitude; newGeometry.setY(lat); newGeometry.setX(lon); // We always get coordinates in lat/lon newGeometry.setSrid(4326); newPlot.setGeometry(newGeometry); newPlot.setAddressFromGeocoder(new Geocoder(getActivity(), Locale.getDefault())); return newPlot; } private void setupSearchView(Menu menu) { searchView = (SearchView) menu.findItem(R.id.search_button).getActionView(); searchView.setQueryHint(getString(R.string.search_field_hint)); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String s) { doLocationSearch(s); searchView.clearFocus(); return true; } @Override public boolean onQueryTextChange(String s) { return false; } }); } private void moveMapAndFinishGeocode(LatLng pos) { mMapSetupDeferred.promise().done(map -> map.moveCamera(CameraUpdateFactory.newLatLngZoom(pos, STREET_ZOOM_LEVEL)) ); } private void alertGeocodeError() { Toast.makeText(getActivity(), "Location search error.", Toast.LENGTH_SHORT).show(); } JsonHttpResponseHandler handleGoogleGeocodeResponse = new LoggingJsonHttpResponseHandler() { public void onSuccess(int statusCode, Header[] headers, JSONObject data) { LatLng pos = FallbackGeocoder.decodeGoogleJsonResponse(data); if (pos == null) { alertGeocodeError(); } else { moveMapAndFinishGeocode(pos); } } @Override public void failure(Throwable arg0, String arg1) { alertGeocodeError(); } }; /* Read the location search field, geocode it, and zoom to the location. */ private void doLocationSearch(CharSequence query) { String address = query.toString(); if (address.equals("")) { Toast.makeText(getActivity(), "Enter an address in the search field to search.", Toast.LENGTH_SHORT).show(); return; } FallbackGeocoder geocoder = new FallbackGeocoder(getActivity(), App.getCurrentInstance()); LatLng pos = geocoder.androidGeocode(address); if (pos == null) { geocoder.httpGeocode(address, handleGoogleGeocodeResponse); } else { moveMapAndFinishGeocode(pos); } } }