gc.david.dfm.ui.activity.MainActivity.java Source code

Java tutorial

Introduction

Here is the source code for gc.david.dfm.ui.activity.MainActivity.java

Source

/*
 * Copyright (c) 2018 David Aguiar Gonzalez
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package gc.david.dfm.ui.activity;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.NavigationView;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnInfoWindowClickListener;
import com.google.android.gms.maps.GoogleMap.OnMapClickListener;
import com.google.android.gms.maps.GoogleMap.OnMapLongClickListener;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.PolylineOptions;
import com.jjoe64.graphview.GraphView;
import com.jjoe64.graphview.GraphViewSeries;
import com.jjoe64.graphview.GraphViewSeries.GraphViewSeriesStyle;
import com.jjoe64.graphview.GraphViewStyle;
import com.jjoe64.graphview.LineGraphView;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.inject.Inject;
import javax.inject.Named;

import butterknife.BindView;
import butterknife.OnClick;
import dagger.Lazy;
import gc.david.dfm.ConnectionManager;
import gc.david.dfm.DFMApplication;
import gc.david.dfm.DFMPreferences;
import gc.david.dfm.PreferencesProvider;
import gc.david.dfm.R;
import gc.david.dfm.Utils;
import gc.david.dfm.adapter.MarkerInfoWindowAdapter;
import gc.david.dfm.address.domain.GetAddressUseCase;
import gc.david.dfm.address.presentation.Address;
import gc.david.dfm.address.presentation.AddressPresenter;
import gc.david.dfm.dagger.DaggerMainComponent;
import gc.david.dfm.dagger.MainModule;
import gc.david.dfm.dagger.RootModule;
import gc.david.dfm.dagger.StorageModule;
import gc.david.dfm.deviceinfo.DeviceInfo;
import gc.david.dfm.deviceinfo.PackageManager;
import gc.david.dfm.distance.domain.GetPositionListUseCase;
import gc.david.dfm.distance.domain.LoadDistancesUseCase;
import gc.david.dfm.elevation.domain.ElevationUseCase;
import gc.david.dfm.elevation.presentation.Elevation;
import gc.david.dfm.elevation.presentation.ElevationPresenter;
import gc.david.dfm.feedback.Feedback;
import gc.david.dfm.feedback.FeedbackPresenter;
import gc.david.dfm.logger.DFMLogger;
import gc.david.dfm.map.Haversine;
import gc.david.dfm.map.LocationUtils;
import gc.david.dfm.model.Distance;
import gc.david.dfm.model.Position;
import gc.david.dfm.service.GeofencingService;
import gc.david.dfm.ui.animation.AnimatorUtil;
import gc.david.dfm.ui.dialog.AddressSuggestionsDialogFragment;
import gc.david.dfm.ui.dialog.DistanceSelectionDialogFragment;
import gc.david.dfm.ui.dialog.ErrorDialogFragment;

import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.M;
import static butterknife.ButterKnife.bind;
import static gc.david.dfm.Utils.isReleaseBuild;
import static gc.david.dfm.Utils.showAlertDialog;
import static gc.david.dfm.Utils.toastIt;

public class MainActivity extends AppCompatActivity
        implements GoogleApiClient.OnConnectionFailedListener, OnMapReadyCallback, OnMapLongClickListener,
        OnMapClickListener, OnInfoWindowClickListener, Elevation.View, Address.View {

    private static final String TAG = MainActivity.class.getSimpleName();
    private static final String[] PERMISSIONS = { ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION };
    private static final int PERMISSIONS_REQUEST_CODE = 2;

    @BindView(R.id.elevationchart)
    protected RelativeLayout rlElevationChart;
    @BindView(R.id.closeChart)
    protected ImageView ivCloseElevationChart;
    @BindView(R.id.tbMain)
    protected Toolbar tbMain;
    @BindView(R.id.drawer_layout)
    protected DrawerLayout drawerLayout;
    @BindView(R.id.nvDrawer)
    protected NavigationView nvDrawer;
    @BindView(R.id.main_activity_showchart_floatingactionbutton)
    protected FloatingActionButton fabShowChart;
    @BindView(R.id.main_activity_mylocation_floatingactionbutton)
    protected FloatingActionButton fabMyLocation;

    @Inject
    protected Context appContext;
    @Inject
    protected Lazy<PackageManager> packageManager;
    @Inject
    protected Lazy<DeviceInfo> deviceInfo;
    @Inject
    protected ElevationUseCase elevationUseCase;
    @Inject
    protected ConnectionManager connectionManager;
    @Inject
    protected PreferencesProvider preferencesProvider;
    @Inject
    @Named("CoordinatesByName")
    protected GetAddressUseCase getAddressCoordinatesByNameUseCase;
    @Inject
    @Named("NameByCoordinates")
    protected GetAddressUseCase getAddressNameByCoordinatesUseCase;
    @Inject
    protected LoadDistancesUseCase loadDistancesUseCase;
    @Inject
    protected GetPositionListUseCase getPositionListUseCase;

    private final BroadcastReceiver locationReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final double latitude = intent.getDoubleExtra(GeofencingService.GEOFENCE_RECEIVER_LATITUDE_KEY, 0D);
            final double longitude = intent.getDoubleExtra(GeofencingService.GEOFENCE_RECEIVER_LONGITUDE_KEY, 0D);
            final Location location = new Location("");
            location.setLatitude(latitude);
            location.setLongitude(longitude);
            onLocationChanged(location);
        }
    };

    private GoogleMap googleMap = null;
    private Location currentLocation = null;
    // Moves to current position if app has just started
    private boolean appHasJustStarted = true;
    private String distanceMeasuredAsText = "";
    private MenuItem searchMenuItem = null;
    // Show position if we come from other app (p.e. Whatsapp)
    private boolean mustShowPositionWhenComingFromOutside = false;
    private LatLng sendDestinationPosition = null;
    private GraphView graphView = null;
    private final List<LatLng> coordinates = new ArrayList<>();
    private boolean calculatingDistance;
    private ProgressDialog progressDialog;

    private Elevation.Presenter elevationPresenter;
    private Address.Presenter addressPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        DFMLogger.logMessage(TAG, "onCreate savedInstanceState=" + Utils.dumpBundleToString(savedInstanceState));

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DaggerMainComponent.builder().rootModule(new RootModule((DFMApplication) getApplication()))
                .storageModule(new StorageModule()).mainModule(new MainModule()).build().inject(this);
        bind(this);

        setSupportActionBar(tbMain);

        final ActionBar supportActionBar = getSupportActionBar();
        if (supportActionBar != null) {
            supportActionBar.setDisplayHomeAsUpEnabled(true);
            supportActionBar.setHomeButtonEnabled(true);
            final Drawable upArrow = ContextCompat.getDrawable(appContext, R.drawable.ic_menu_white_24dp);
            supportActionBar.setHomeAsUpIndicator(upArrow);
        }

        elevationPresenter = new ElevationPresenter(this, elevationUseCase, connectionManager, preferencesProvider);
        addressPresenter = new AddressPresenter(this, getAddressCoordinatesByNameUseCase,
                getAddressNameByCoordinatesUseCase, connectionManager);

        final SupportMapFragment supportMapFragment = ((SupportMapFragment) getSupportFragmentManager()
                .findFragmentById(R.id.map));
        supportMapFragment.getMapAsync(this);

        if (!connectionManager.isOnline()) {
            showConnectionProblemsDialog();
        }

        handleIntents(getIntent());

        nvDrawer.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
                drawerLayout.closeDrawers();
                switch (menuItem.getItemId()) {
                case R.id.menu_current_position:
                    menuItem.setChecked(true);
                    onStartingPointSelected();
                    if (SDK_INT >= M && !isLocationPermissionGranted()) {
                        Snackbar.make(drawerLayout, "This feature needs location permissions.",
                                Snackbar.LENGTH_INDEFINITE).setAction("Settings", new View.OnClickListener() {
                                    @Override
                                    public void onClick(View v) {
                                        final Intent intent = new Intent(
                                                android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                                        intent.setData(Uri.parse("package:" + getPackageName()));
                                        startActivity(intent);
                                    }
                                }).show();
                    }
                    return true;
                case R.id.menu_any_position:
                    menuItem.setChecked(true);
                    onStartingPointSelected();
                    return true;
                case R.id.menu_rate_app:
                    showRateDialog();
                    return true;
                case R.id.menu_settings:
                    SettingsActivity.open(MainActivity.this);
                    return true;
                case R.id.menu_help_feedback:
                    HelpAndFeedbackActivity.open(MainActivity.this);
                    return true;
                case R.id.menu_about:
                    AboutActivity.open(MainActivity.this);
                    return true;
                }
                return false;
            }
        });
    }

    @Override
    public void onMapReady(final GoogleMap map) {
        googleMap = map;

        googleMap.getUiSettings().setMyLocationButtonEnabled(false);

        googleMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);

        googleMap.setOnMapLongClickListener(this);
        googleMap.setOnMapClickListener(this);
        googleMap.setOnInfoWindowClickListener(this);
        googleMap.setInfoWindowAdapter(new MarkerInfoWindowAdapter(this));

        onStartingPointSelected();

        if (SDK_INT >= M && !isLocationPermissionGranted()) {
            requestPermissions(PERMISSIONS, PERMISSIONS_REQUEST_CODE);
        } else {
            toastIt(R.string.toast_loading_position, appContext);
            googleMap.setMyLocationEnabled(true);
            fabMyLocation.setVisibility(View.VISIBLE);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
            @NonNull int[] grantResults) {
        if (requestCode == PERMISSIONS_REQUEST_CODE) {
            // not necessary to check both permissions, they fall under location group
            if (grantResults[0] == PERMISSION_GRANTED) {
                DFMLogger.logMessage(TAG, "onRequestPermissionsResult GRANTED");

                toastIt(R.string.toast_loading_position, appContext);
                googleMap.setMyLocationEnabled(true);
                fabMyLocation.setVisibility(View.VISIBLE);

                registerReceiver(locationReceiver, new IntentFilter(GeofencingService.GEOFENCE_RECEIVER_ACTION));
                startService(new Intent(this, GeofencingService.class));
            } else {
                DFMLogger.logMessage(TAG, "onRequestPermissionsResult DENIED");
                fabMyLocation.setVisibility(View.GONE);
                nvDrawer.getMenu().findItem(R.id.menu_any_position).setChecked(true);
                onStartingPointSelected();
            }
        }
    }

    @Override
    public void onMapLongClick(LatLng point) {
        DFMLogger.logMessage(TAG, "onMapLongClick");

        calculatingDistance = true;

        if (getSelectedDistanceMode() == DistanceMode.DISTANCE_FROM_ANY_POINT) {
            if (coordinates.isEmpty()) {
                toastIt(R.string.toast_first_point_needed, appContext);
            } else {
                coordinates.add(point);
                drawAndShowMultipleDistances(coordinates, "", false);
            }
        } else if (currentLocation != null) { // Without current location, we cannot calculate any distance
            if (getSelectedDistanceMode() == DistanceMode.DISTANCE_FROM_CURRENT_POINT && coordinates.isEmpty()) {
                coordinates.add(new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude()));
            }
            coordinates.add(point);
            drawAndShowMultipleDistances(coordinates, "", false);
        }

        calculatingDistance = false;
    }

    @Override
    public void onMapClick(LatLng point) {
        DFMLogger.logMessage(TAG, "onMapClick");

        if (getSelectedDistanceMode() == DistanceMode.DISTANCE_FROM_ANY_POINT) {
            if (!calculatingDistance) {
                coordinates.clear();
            }

            calculatingDistance = true;

            if (coordinates.isEmpty()) {
                googleMap.clear();
            }
            coordinates.add(point);
            googleMap.addMarker(new MarkerOptions().position(point));
        } else {
            // Without current location, we cannot calculate any distance
            if (currentLocation != null) {
                if (!calculatingDistance) {
                    coordinates.clear();
                }
                calculatingDistance = true;

                if (coordinates.isEmpty()) {
                    googleMap.clear();
                    coordinates.add(new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude()));
                }
                coordinates.add(point);
                googleMap.addMarker(new MarkerOptions().position(point));
            }
        }
    }

    @Override
    public void onInfoWindowClick(Marker marker) {
        DFMLogger.logMessage(TAG, "onInfoWindowClick");

        ShowInfoActivity.open(this, coordinates, distanceMeasuredAsText);
    }

    private void onStartingPointSelected() {
        if (getSelectedDistanceMode() == DistanceMode.DISTANCE_FROM_CURRENT_POINT) {
            DFMLogger.logMessage(TAG, "onStartingPointSelected Distance from current point");
        } else {
            DFMLogger.logMessage(TAG, "onStartingPointSelected Distance from any point");
        }

        calculatingDistance = false;

        coordinates.clear();

        if (googleMap != null) {
            googleMap.clear();
        }

        elevationPresenter.onReset();
    }

    private DistanceMode getSelectedDistanceMode() {
        return nvDrawer.getMenu().findItem(R.id.menu_current_position).isChecked()
                ? DistanceMode.DISTANCE_FROM_CURRENT_POINT
                : DistanceMode.DISTANCE_FROM_ANY_POINT;
    }

    @Override
    protected void onNewIntent(Intent intent) {
        DFMLogger.logMessage(TAG, "onNewIntent " + Utils.dumpIntentToString(intent));

        setIntent(intent);
        handleIntents(intent);
    }

    private void handleIntents(final Intent intent) {
        if (intent != null) {
            if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
                handleSearchIntent(intent);
            } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
                handleViewPositionIntent(intent);
            }
        }
    }

    private void handleSearchIntent(final Intent intent) {
        DFMLogger.logMessage(TAG, "handleSearchIntent");

        // Para controlar instancias nicas, no queremos que cada vez que
        // busquemos nos inicie una nueva instancia de la aplicacin
        final String query = intent.getStringExtra(SearchManager.QUERY);
        if (currentLocation != null) {
            addressPresenter.searchPositionByName(query);
            searchMenuItem.collapseActionView();
        }
        if (searchMenuItem != null) {
            searchMenuItem.collapseActionView();
        }
    }

    private void handleViewPositionIntent(final Intent intent) {
        final Uri uri = intent.getData();
        DFMLogger.logMessage(TAG, "handleViewPositionIntent uri=" + uri.toString());

        final String uriScheme = uri.getScheme();
        if (uriScheme.equals("geo")) {
            handleGeoSchemeIntent(uri);
        } else if ((uriScheme.equals("http") || uriScheme.equals("https"))
                && (uri.getHost().equals("maps.google.com"))) { // Manage maps.google.com?q=latitude,longitude
            handleMapsHostIntent(uri);
        } else {
            final Exception exception = new Exception("Imposible tratar la query " + uri.toString());
            DFMLogger.logException(exception);
            toastIt("Unable to parse address", this);
        }
    }

    private void handleGeoSchemeIntent(final Uri uri) {
        final String schemeSpecificPart = uri.getSchemeSpecificPart();
        final Matcher matcher = getMatcherForUri(schemeSpecificPart);
        if (matcher.find()) {
            if (matcher.group(1).equals("0") && matcher.group(2).equals("0")) {
                if (matcher.find()) { // Manage geo:0,0?q=lat,lng(label)
                    setDestinationPosition(matcher);
                } else { // Manage geo:0,0?q=my+street+address
                    String destination = Uri.decode(uri.getQuery()).replace('+', ' ');
                    destination = destination.replace("q=", "");

                    // TODO check this ugly workaround
                    addressPresenter.searchPositionByName(destination);
                    searchMenuItem.collapseActionView();
                    mustShowPositionWhenComingFromOutside = true;
                }
            } else { // Manage geo:latitude,longitude or geo:latitude,longitude?z=zoom
                setDestinationPosition(matcher);
            }
        } else {
            final NoSuchFieldException noSuchFieldException = new NoSuchFieldException(
                    "Error al obtener las coordenadas. Matcher = " + matcher.toString());
            DFMLogger.logException(noSuchFieldException);
            toastIt("Unable to parse address", this);
        }
    }

    private void handleMapsHostIntent(final Uri uri) {
        final String queryParameter = uri.getQueryParameter("q");
        if (queryParameter != null) {
            final Matcher matcher = getMatcherForUri(queryParameter);
            if (matcher.find()) {
                setDestinationPosition(matcher);
            } else {
                final NoSuchFieldException noSuchFieldException = new NoSuchFieldException(
                        "Error al obtener las coordenadas. Matcher = " + matcher.toString());
                DFMLogger.logException(noSuchFieldException);
                toastIt("Unable to parse address", this);
            }
        } else {
            final NoSuchFieldException noSuchFieldException = new NoSuchFieldException("Query sin parmetro q.");
            DFMLogger.logException(noSuchFieldException);
            toastIt("Unable to parse address", this);
        }
    }

    private void setDestinationPosition(final Matcher matcher) {
        DFMLogger.logMessage(TAG, "setDestinationPosition");

        sendDestinationPosition = new LatLng(Double.valueOf(matcher.group(1)), Double.valueOf(matcher.group(2)));
        mustShowPositionWhenComingFromOutside = true;
    }

    private Matcher getMatcherForUri(final String schemeSpecificPart) {
        DFMLogger.logMessage(TAG, "getMatcherForUri scheme=" + schemeSpecificPart);

        final String regex = "(\\-?\\d+\\.*\\d*),(\\-?\\d+\\.*\\d*)";
        final Pattern pattern = Pattern.compile(regex);
        return pattern.matcher(schemeSpecificPart);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);

        searchMenuItem = menu.findItem(R.id.action_search);
        final SearchView searchView = (SearchView) searchMenuItem.getActionView();
        final SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
        // Indicamos que la activity actual sea la buscadora
        searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
        searchView.setSubmitButtonEnabled(false);
        searchView.setQueryRefinementEnabled(true);
        searchView.setIconifiedByDefault(true);

        // TODO: 16.01.17 move this to presenter
        final MenuItem loadItem = menu.findItem(R.id.action_load);
        loadDistancesUseCase.execute(new LoadDistancesUseCase.Callback() {
            @Override
            public void onDistanceListLoaded(final List<Distance> distanceList) {
                if (distanceList.isEmpty()) {
                    loadItem.setVisible(false);
                }
            }

            @Override
            public void onError() {
                loadItem.setVisible(false);
            }
        });

        menu.findItem(R.id.action_crash).setVisible(!isReleaseBuild());

        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case android.R.id.home:
            DFMLogger.logMessage(TAG, "onOptionsItemSelected home");
            drawerLayout.openDrawer(GravityCompat.START);
            return true;
        case R.id.action_search:
            DFMLogger.logMessage(TAG, "onOptionsItemSelected search");
            return true;
        case R.id.action_load:
            DFMLogger.logMessage(TAG, "onOptionsItemSelected load distances from ddbb");
            loadDistancesFromDB();
            return true;
        case R.id.action_crash:
            DFMLogger.logMessage(TAG, "onOptionsItemSelected crash");
            throw new RuntimeException("User forced crash");
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onBackPressed() {
        if (drawerLayout != null && drawerLayout.isDrawerOpen(GravityCompat.START)) {
            drawerLayout.closeDrawer(GravityCompat.START);
        } else {
            super.onBackPressed();
        }
    }

    private void loadDistancesFromDB() {
        // TODO: 16.01.17 move this to presenter
        loadDistancesUseCase.execute(new LoadDistancesUseCase.Callback() {
            @Override
            public void onDistanceListLoaded(final List<Distance> distanceList) {
                if (!distanceList.isEmpty()) {
                    final DistanceSelectionDialogFragment distanceSelectionDialogFragment = new DistanceSelectionDialogFragment();
                    distanceSelectionDialogFragment.setDistanceList(distanceList);
                    distanceSelectionDialogFragment.setOnDialogActionListener(
                            new DistanceSelectionDialogFragment.OnDialogActionListener() {
                                @Override
                                public void onItemClick(int position) {
                                    final Distance distance = distanceList.get(position);
                                    getPositionListUseCase.execute(distance.getId(),
                                            new GetPositionListUseCase.Callback() {
                                                @Override
                                                public void onPositionListLoaded(
                                                        final List<Position> positionList) {
                                                    coordinates.clear();
                                                    coordinates.addAll(
                                                            Utils.convertPositionListToLatLngList(positionList));

                                                    drawAndShowMultipleDistances(coordinates,
                                                            distance.getName() + "\n", true);
                                                }

                                                @Override
                                                public void onError() {
                                                    DFMLogger.logException(
                                                            new Exception("Unable to get position by id."));
                                                }
                                            });
                                }
                            });
                    distanceSelectionDialogFragment.show(getSupportFragmentManager(), null);
                }
            }

            @Override
            public void onError() {
                DFMLogger.logException(new Exception("Unable to load distances."));
            }
        });
    }

    private void showRateDialog() {
        DFMLogger.logMessage(TAG, "showRateDialog");

        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.dialog_rate_app_title).setMessage(R.string.dialog_rate_app_message)
                .setPositiveButton(getString(R.string.dialog_rate_app_positive_button),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.dismiss();
                                openPlayStoreAppPage();
                            }
                        })
                .setNegativeButton(getString(R.string.dialog_rate_app_negative_button),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.dismiss();
                                openFeedbackActivity();
                            }
                        })
                .create().show();
    }

    private void openPlayStoreAppPage() {
        DFMLogger.logMessage(TAG, "openPlayStoreAppPage");

        try {
            startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=gc.david.dfm")));
        } catch (ActivityNotFoundException e) {
            DFMLogger.logException(new Exception("Unable to open Play Store, rooted device?"));
        }
    }

    private void openFeedbackActivity() {
        DFMLogger.logMessage(TAG, "openFeedbackActivity");

        new FeedbackPresenter(new Feedback.View() {
            @Override
            public void showError() {
                toastIt(R.string.toast_send_feedback_error, appContext);
            }

            @Override
            public void showEmailClient(final Intent intent) {
                startActivity(intent);
            }

            @Override
            public Context context() {
                return appContext;
            }
        }, packageManager.get(), deviceInfo.get()).start();
    }

    /**
     * Called when the Activity is no longer visible at all. Stop updates and
     * disconnect.
     */
    @Override
    public void onStop() {
        DFMLogger.logMessage(TAG, "onStop");

        super.onStop();
        try {
            unregisterReceiver(locationReceiver);
        } catch (IllegalArgumentException exception) {
            DFMLogger.logMessage(TAG, "onStop receiver not registered, do nothing");
        }
        stopService(new Intent(this, GeofencingService.class));
    }

    /**
     * Called when the Activity is restarted, even before it becomes visible.
     */
    @Override
    public void onStart() {
        DFMLogger.logMessage(TAG, "onStart");

        super.onStart();
        if (isLocationPermissionGranted()) {
            registerReceiver(locationReceiver, new IntentFilter(GeofencingService.GEOFENCE_RECEIVER_ACTION));
            startService(new Intent(this, GeofencingService.class));
            if (googleMap != null) {
                googleMap.setMyLocationEnabled(true);
            }
            fabMyLocation.setVisibility(View.VISIBLE);
        } else {
            fabMyLocation.setVisibility(View.GONE);
        }
    }

    private boolean isLocationPermissionGranted() {
        return ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED
                && ContextCompat.checkSelfPermission(this, ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED;
    }

    /**
     * Called when the system detects that this Activity is now visible.
     */
    @Override
    public void onResume() {
        DFMLogger.logMessage(TAG, "onResume");

        super.onResume();
        invalidateOptionsMenu();
        checkPlayServices();
    }

    @Override
    public void onDestroy() {
        DFMLogger.logMessage(TAG, "onDestroy");

        elevationPresenter.onReset();
        super.onDestroy();
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
        DFMLogger.logMessage(TAG,
                String.format(Locale.getDefault(), "onActivityResult requestCode=%d, resultCode=%d, intent=%s",
                        requestCode, resultCode, Utils.dumpIntentToString(intent)));

        if (requestCode == LocationUtils.CONNECTION_FAILURE_RESOLUTION_REQUEST) {
            if (resultCode == Activity.RESULT_OK) {
                // Log the result
                // Log.d(LocationUtils.APPTAG, getString(R.string.resolved));

                // Display the result
                // mConnectionState.setText(R.string.connected);
                // mConnectionStatus.setText(R.string.resolved);
            } else { // If any other result was returned by Google Play services
                // Log the result
                // Log.d(LocationUtils.APPTAG,
                // getString(R.string.no_resolution));

                // Display the result
                // mConnectionState.setText(R.string.disconnected);
                // mConnectionStatus.setText(R.string.no_resolution);
            }
        }
    }

    private boolean checkPlayServices() {
        final GoogleApiAvailability googleApiAvailabilityInstance = GoogleApiAvailability.getInstance();
        final int resultCode = googleApiAvailabilityInstance.isGooglePlayServicesAvailable(appContext);

        if (resultCode == ConnectionResult.SUCCESS) {
            DFMLogger.logMessage(TAG, "checkPlayServices success");

            return true;
        } else {
            if (googleApiAvailabilityInstance.isUserResolvableError(resultCode)) {
                DFMLogger.logMessage(TAG, "checkPlayServices isUserRecoverableError");

                final int RQS_GooglePlayServices = 1;
                googleApiAvailabilityInstance.getErrorDialog(this, resultCode, RQS_GooglePlayServices).show();
            } else {
                DFMLogger.logMessage(TAG, "checkPlayServices device not supported, finishing");

                finish();
            }
            return false;
        }
    }

    /**
     * Called by Location Services if the attempt to Location Services fails.
     */
    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        DFMLogger.logMessage(TAG, "onConnectionFailed");

        /*
         * Google Play services can resolve some errors it detects. If the error
        * has a resolution, try sending an Intent to start a Google Play
        * services activity that can resolve error.
        */
        if (connectionResult.hasResolution()) {
            DFMLogger.logMessage(TAG, "onConnectionFailed connection has resolution");

            try {
                // Start an Activity that tries to resolve the error
                connectionResult.startResolutionForResult(this,
                        LocationUtils.CONNECTION_FAILURE_RESOLUTION_REQUEST);
                /*
                 * Thrown if Google Play services cancelled the original
                * PendingIntent
                */
            } catch (IntentSender.SendIntentException e) {
                // Log the error
                e.printStackTrace();
                DFMLogger.logException(e);
            }
        } else {
            DFMLogger.logMessage(TAG, "onConnectionFailed connection does not have resolution");
            // If no resolution is available, display a dialog to the user with
            // the error.
            showErrorDialog(connectionResult.getErrorCode());
        }
    }

    public void onLocationChanged(final Location location) {
        DFMLogger.logMessage(TAG, "onLocationChanged");

        if (currentLocation != null) {
            currentLocation.set(location);
        } else {
            currentLocation = new Location(location);
        }

        if (appHasJustStarted) {
            DFMLogger.logMessage(TAG, "onLocationChanged appHasJustStarted");

            if (mustShowPositionWhenComingFromOutside) {
                DFMLogger.logMessage(TAG, "onLocationChanged mustShowPositionWhenComingFromOutside");

                if (currentLocation != null && sendDestinationPosition != null) {
                    addressPresenter.searchPositionByCoordinates(sendDestinationPosition);

                    mustShowPositionWhenComingFromOutside = false;
                }
            } else {
                DFMLogger.logMessage(TAG, "onLocationChanged NOT mustShowPositionWhenComingFromOutside");

                final LatLng latlng = new LatLng(location.getLatitude(), location.getLongitude());
                // 17 is a good zoom level for this action
                googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latlng, 17));
            }
            appHasJustStarted = false;
        }
    }

    /**
     * Shows a dialog returned by Google Play services for the connection error
     * code
     *
     * @param errorCode An error code returned from onConnectionFailed
     */
    private void showErrorDialog(final int errorCode) {
        DFMLogger.logMessage(TAG, "showErrorDialog errorCode=" + errorCode);

        final Dialog errorDialog = GoogleApiAvailability.getInstance().getErrorDialog(this, errorCode,
                LocationUtils.CONNECTION_FAILURE_RESOLUTION_REQUEST);

        // If Google Play services can provide an error dialog
        if (errorDialog != null) {
            final ErrorDialogFragment errorFragment = new ErrorDialogFragment();
            errorFragment.setDialog(errorDialog);
            errorFragment.show(getSupportFragmentManager(), "Geofence detection");
        }
    }

    private void drawAndShowMultipleDistances(final List<LatLng> coordinates, final String message,
            final boolean isLoadingFromDB) {
        DFMLogger.logMessage(TAG, "drawAndShowMultipleDistances");

        googleMap.clear();

        distanceMeasuredAsText = calculateDistance(coordinates);

        addMarkers(coordinates, distanceMeasuredAsText, message, isLoadingFromDB);

        addLines(coordinates, isLoadingFromDB);

        moveCameraZoom(coordinates);

        elevationPresenter.buildChart(coordinates);
    }

    private void addMarkers(final List<LatLng> coordinates, final String distance, final String message,
            final boolean isLoadingFromDB) {
        for (int i = 0; i < coordinates.size(); i++) {
            if ((i == 0 && (isLoadingFromDB || getSelectedDistanceMode() == DistanceMode.DISTANCE_FROM_ANY_POINT))
                    || (i == coordinates.size() - 1)) {
                final LatLng coordinate = coordinates.get(i);
                final Marker marker = addMarker(coordinate);

                if (i == coordinates.size() - 1) {
                    marker.setTitle(message + distance);
                    marker.showInfoWindow();
                }
            }
        }
    }

    private Marker addMarker(final LatLng coordinate) {
        return googleMap.addMarker(new MarkerOptions().position(coordinate));
    }

    private void addLines(final List<LatLng> coordinates, final boolean isLoadingFromDB) {
        for (int i = 0; i < coordinates.size() - 1; i++) {
            addLine(coordinates.get(i), coordinates.get(i + 1), isLoadingFromDB);
        }
    }

    private void addLine(final LatLng start, final LatLng end, final boolean isLoadingFromDB) {
        final PolylineOptions lineOptions = new PolylineOptions().add(start).add(end);
        lineOptions.width(getResources().getDimension(R.dimen.map_line_width));
        lineOptions.color(isLoadingFromDB ? Color.YELLOW : Color.GREEN);
        googleMap.addPolyline(lineOptions);
    }

    private String calculateDistance(final List<LatLng> coordinates) {
        double distanceInMetres = Utils.calculateDistanceInMetres(coordinates);

        return Haversine.normalizeDistance(distanceInMetres, getAmericanOrEuropeanLocale());
    }

    private void moveCameraZoom(final List<LatLng> coordinatesList) {
        final String centre = DFMPreferences.getAnimationPreference(getBaseContext());
        switch (centre) {
        case DFMPreferences.ANIMATION_CENTRE_VALUE:
            final LatLngBounds.Builder latLngBoundsBuilder = new LatLngBounds.Builder();
            for (LatLng latLng : coordinatesList) {
                latLngBoundsBuilder.include(latLng);
            }
            googleMap.animateCamera(CameraUpdateFactory.newLatLngBounds(latLngBoundsBuilder.build(), 100));
            break;
        case DFMPreferences.ANIMATION_DESTINATION_VALUE:
            final LatLng lastCoordinates = coordinatesList.get(coordinatesList.size() - 1);
            googleMap.animateCamera(
                    CameraUpdateFactory.newLatLng(new LatLng(lastCoordinates.latitude, lastCoordinates.longitude)));
            break;
        case DFMPreferences.NO_ANIMATION_DESTINATION_VALUE:
            // nothing
            break;
        }
    }

    private void fixMapPadding() {
        DFMLogger.logMessage(TAG,
                String.format("fixMapPadding elevationChartShown %s", rlElevationChart.isShown()));
        googleMap.setPadding(0, rlElevationChart.isShown() ? rlElevationChart.getHeight() : 0, 0, 0);
    }

    @Override
    public void setPresenter(final Elevation.Presenter presenter) {
        this.elevationPresenter = presenter;
    }

    @Override
    public void hideChart() {
        rlElevationChart.setVisibility(View.INVISIBLE);
        fabShowChart.setVisibility(View.INVISIBLE);
        fixMapPadding();
    }

    @Override
    public void showChart() {
        rlElevationChart.setVisibility(View.VISIBLE);
        fixMapPadding();
    }

    @Override
    public void buildChart(final List<Double> elevationList) {
        final Locale locale = getAmericanOrEuropeanLocale();

        // Creates the series and adds data to it
        final GraphViewSeries series = buildGraphViewSeries(elevationList, locale);

        if (graphView == null) {
            graphView = new LineGraphView(appContext,
                    getString(R.string.elevation_chart_title, Haversine.getAltitudeUnitByLocale(locale)));
            final GraphViewStyle graphViewStyle = graphView.getGraphViewStyle();
            graphViewStyle.setGridColor(Color.TRANSPARENT);
            graphViewStyle.setNumHorizontalLabels(1); // Not working with zero?
            graphViewStyle.setTextSize(getResources().getDimension(R.dimen.elevation_chart_text_size));
            graphViewStyle.setVerticalLabelsWidth(
                    getResources().getDimensionPixelSize(R.dimen.elevation_chart_vertical_label_width));
            rlElevationChart.addView(graphView);

            ivCloseElevationChart.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    elevationPresenter.onCloseChart();
                }
            });
        }
        graphView.removeAllSeries();
        graphView.addSeries(series);

        elevationPresenter.onChartBuilt();
    }

    @NonNull
    private GraphViewSeries buildGraphViewSeries(final List<Double> elevationList, final Locale locale) {
        final GraphViewSeriesStyle style = new GraphViewSeriesStyle(
                ContextCompat.getColor(getApplicationContext(), R.color.elevation_chart_line),
                getResources().getDimensionPixelSize(R.dimen.elevation_chart_line_size));
        final GraphViewSeries series = new GraphViewSeries(null, style, new GraphView.GraphViewData[] {});

        for (int w = 0; w < elevationList.size(); w++) {
            series.appendData(
                    new GraphView.GraphViewData(w,
                            Haversine.normalizeAltitudeByLocale(elevationList.get(w), locale)),
                    false, elevationList.size());
        }
        return series;
    }

    @Override
    public void animateHideChart() {
        AnimatorUtil.replaceViews(rlElevationChart, fabShowChart);
    }

    @Override
    public void animateShowChart() {
        AnimatorUtil.replaceViews(fabShowChart, rlElevationChart);
    }

    @Override
    public boolean isMinimiseButtonShown() {
        return fabShowChart.isShown();
    }

    @Override
    public void logError(final String errorMessage) {
        DFMLogger.logException(new Exception(errorMessage));
    }

    @OnClick(R.id.main_activity_showchart_floatingactionbutton)
    void onShowChartClick() {
        elevationPresenter.onOpenChart();
    }

    @OnClick(R.id.main_activity_mylocation_floatingactionbutton)
    void onMyLocationClick() {
        if (currentLocation != null) {
            googleMap.animateCamera(CameraUpdateFactory
                    .newLatLng(new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude())));
        }
    }

    @Override
    public void setPresenter(final Address.Presenter presenter) {
        this.addressPresenter = presenter;
    }

    @Override
    public void showConnectionProblemsDialog() {
        DFMLogger.logMessage(TAG, "showConnectionProblemsDialog");

        showAlertDialog(android.provider.Settings.ACTION_SETTINGS, R.string.dialog_connection_problems_title,
                R.string.dialog_connection_problems_message, R.string.dialog_connection_problems_positive_button,
                R.string.dialog_connection_problems_negative_button, this);
    }

    @Override
    public void showProgressDialog() {
        progressDialog = new ProgressDialog(this);
        progressDialog.setTitle(R.string.progressdialog_search_position_title);
        progressDialog.setMessage(getString(R.string.progressdialog_search_position_message));
        progressDialog.setCancelable(false);
        progressDialog.setIndeterminate(true);
        progressDialog.show();
    }

    @Override
    public void hideProgressDialog() {
        if (progressDialog != null && progressDialog.isShowing()) {
            progressDialog.dismiss();
        }
    }

    @Override
    public void showCallError(final String errorMessage) {
        logError(errorMessage);
        toastIt(R.string.toast_no_find_address, appContext);
    }

    @Override
    public void showNoMatchesMessage() {
        toastIt(R.string.toast_no_results, appContext);
    }

    @Override
    public void showAddressSelectionDialog(final List<gc.david.dfm.address.domain.model.Address> addressList) {
        final AddressSuggestionsDialogFragment addressSuggestionsDialogFragment = new AddressSuggestionsDialogFragment();
        addressSuggestionsDialogFragment.setAddressList(addressList);
        addressSuggestionsDialogFragment
                .setOnDialogActionListener(new AddressSuggestionsDialogFragment.OnDialogActionListener() {
                    @Override
                    public void onItemClick(final int position) {
                        addressPresenter.selectAddressInDialog(addressList.get(position));
                    }
                });
        addressSuggestionsDialogFragment.show(getSupportFragmentManager(), null);
    }

    @Override
    public void showPositionByName(final gc.david.dfm.address.domain.model.Address address) {
        DFMLogger.logMessage(TAG, "showPositionByName " + getSelectedDistanceMode());

        final LatLng addressCoordinates = address.getCoordinates();
        if (getSelectedDistanceMode() == DistanceMode.DISTANCE_FROM_ANY_POINT) {
            coordinates.add(addressCoordinates);
            if (coordinates.isEmpty()) {
                DFMLogger.logMessage(TAG, "showPositionByName empty coordinates list");

                // add marker
                final Marker marker = addMarker(addressCoordinates);
                marker.setTitle(address.getFormattedAddress());
                marker.showInfoWindow();
                // moveCamera
                googleMap.animateCamera(CameraUpdateFactory
                        .newLatLng(new LatLng(addressCoordinates.latitude, addressCoordinates.longitude)));
                distanceMeasuredAsText = "";
                // That means we are looking for a first position, so we want to calculate a distance starting
                // from here
                calculatingDistance = true;
            } else {
                drawAndShowMultipleDistances(coordinates, address.getFormattedAddress() + "\n", false);
            }
        } else {
            if (!appHasJustStarted) {
                DFMLogger.logMessage(TAG, "showPositionByName appHasJustStarted");

                if (coordinates.isEmpty()) {
                    DFMLogger.logMessage(TAG, "showPositionByName empty coordinates list");

                    coordinates.add(new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude()));
                }
                coordinates.add(addressCoordinates);
                drawAndShowMultipleDistances(this.coordinates, address.getFormattedAddress() + "\n", false);
            } else {
                DFMLogger.logMessage(TAG, "showPositionByName NOT appHasJustStarted");

                // Coming from View Action Intent
                sendDestinationPosition = addressCoordinates;
            }
        }
    }

    @Override
    public void showPositionByCoordinates(final gc.david.dfm.address.domain.model.Address address) {
        drawAndShowMultipleDistances(
                Arrays.asList(new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude()),
                        address.getCoordinates()),
                address.getFormattedAddress() + "\n", false);
    }

    private enum DistanceMode {
        DISTANCE_FROM_CURRENT_POINT, DISTANCE_FROM_ANY_POINT
    }

    private Locale getAmericanOrEuropeanLocale() {
        final String defaultUnit = DFMPreferences.getMeasureUnitPreference(getBaseContext());
        return DFMPreferences.MEASURE_AMERICAN_UNIT_VALUE.equals(defaultUnit) ? Locale.US : Locale.FRANCE;
    }
}