name.gumartinm.weather.information.activity.MapActivity.java Source code

Java tutorial

Introduction

Here is the source code for name.gumartinm.weather.information.activity.MapActivity.java

Source

/**
 * Copyright 2014 Gustavo Martin Morcuende
 *
 * 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 name.gumartinm.weather.information.activity;

import android.app.ActionBar;
import android.content.Context;
import android.location.Criteria;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnMapLongClickListener;
import com.google.android.gms.maps.MapFragment;
import com.google.android.gms.maps.OnMapReadyCallback;
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 name.gumartinm.weather.information.R;
import name.gumartinm.weather.information.fragment.map.MapButtonsFragment;
import name.gumartinm.weather.information.fragment.map.MapProgressFragment;
import name.gumartinm.weather.information.model.DatabaseQueries;
import name.gumartinm.weather.information.model.WeatherLocation;

public class MapActivity extends FragmentActivity implements LocationListener, MapProgressFragment.TaskCallbacks {
    private static final String PROGRESS_FRAGMENT_TAG = "PROGRESS_FRAGMENT";
    private static final String BUTTONS_FRAGMENT_TAG = "BUTTONS_FRAGMENT";
    private WeatherLocation mRestoreUI;

    // Google Play Services Map
    private GoogleMap mMap;
    private Marker mMarker;
    // Async map loading.
    private ButtonsUpdate mButtonsUpdate;
    private MapUpdate mMapUpdate;

    // The Android rotate screen mess and callback coming from external API (nobody knows how
    // it was implemented, otherwise I could avoid this code)
    private MapActivityOnMapReadyCallback mMapActivityOnMapReadyCallback;

    private LocationManager mLocationManager;

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(R.layout.weather_map);

        // Acquire a reference to the system Location Manager
        this.mLocationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);

        // Google Play Services Map
        final MapFragment mapFragment = (MapFragment) this.getFragmentManager()
                .findFragmentById(R.id.weather_map_fragment_map);
        this.mMapActivityOnMapReadyCallback = new MapActivityOnMapReadyCallback(this);
        mapFragment.getMapAsync(this.mMapActivityOnMapReadyCallback);
    }

    @Override
    protected void onRestoreInstanceState(final Bundle savedInstanceState) {
        // Instead of restoring the state during onCreate() you may choose to
        // implement onRestoreInstanceState(), which the system calls after the
        // onStart() method. The system calls onRestoreInstanceState() only if
        // there is a saved state to restore, so you do not need to check whether
        // the Bundle is null:
        super.onRestoreInstanceState(savedInstanceState);

        // Restore UI state
        this.mRestoreUI = (WeatherLocation) savedInstanceState.getSerializable("WeatherLocation");
    }

    @Override
    public void onResume() {
        super.onResume();

        final ActionBar actionBar = this.getActionBar();
        actionBar.setTitle(this.getString(R.string.weather_map_mark_location));

        WeatherLocation weatherLocation;
        if (this.mRestoreUI != null) {
            // Restore UI state
            weatherLocation = this.mRestoreUI;
            // just once
            this.mRestoreUI = null;
        } else if (this.mMarker != null) {
            final TextView city = (TextView) this.findViewById(R.id.weather_map_city);
            final TextView country = (TextView) this.findViewById(R.id.weather_map_country);
            final String cityString = city.getText().toString();
            final String countryString = country.getText().toString();

            final LatLng point = this.mMarker.getPosition();
            double latitude = point.latitude;
            double longitude = point.longitude;

            weatherLocation = new WeatherLocation().setCity(cityString).setCountry(countryString)
                    .setLatitude(latitude).setLongitude(longitude);
        } else {
            final DatabaseQueries query = new DatabaseQueries(this.getApplicationContext());
            weatherLocation = query.queryDataBase();
        }

        if (weatherLocation != null) {
            this.updateUI(weatherLocation);
        }
    }

    /**
     * I am not using fragment transactions in the right way. But I do not know other way for doing what I am doing.
     * 
     * {@link http://stackoverflow.com/questions/16265733/failure-delivering-result-onactivityforresult}
     */
    @Override
    public void onPostResume() {
        super.onPostResume();

        final FragmentManager fm = getSupportFragmentManager();
        final Fragment progressFragment = fm.findFragmentByTag(PROGRESS_FRAGMENT_TAG);
        if (progressFragment == null) {
            this.addButtonsFragment();
        } else {
            this.removeProgressFragment();
            final Bundle bundle = progressFragment.getArguments();
            double latitude = bundle.getDouble("latitude");
            double longitude = bundle.getDouble("longitude");
            this.addProgressFragment(latitude, longitude);
        }
    }

    @Override
    public void onSaveInstanceState(final Bundle savedInstanceState) {
        // Save UI state
        // Save Google Maps Marker
        if (this.mMarker != null) {
            final TextView city = (TextView) this.findViewById(R.id.weather_map_city);
            final TextView country = (TextView) this.findViewById(R.id.weather_map_country);
            final String cityString = city.getText().toString();
            final String countryString = country.getText().toString();

            final LatLng point = this.mMarker.getPosition();
            double latitude = point.latitude;
            double longitude = point.longitude;

            final WeatherLocation location = new WeatherLocation().setCity(cityString).setCountry(countryString)
                    .setLatitude(latitude).setLongitude(longitude);
            savedInstanceState.putSerializable("WeatherLocation", location);
        }

        super.onSaveInstanceState(savedInstanceState);
    }

    @Override
    public void onPause() {
        super.onPause();

        this.mLocationManager.removeUpdates(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // The Android rotate screen mess.
        if (this.mMapActivityOnMapReadyCallback != null) {
            this.mMapActivityOnMapReadyCallback.onDestroy();
        }
        if (this.mButtonsUpdate != null) {
            this.mButtonsUpdate.onDestroy();
        }
        if (this.mMapUpdate != null) {
            this.mMapUpdate.onDestroy();
        }
    }

    public void onClickSaveLocation(final View v) {
        if (this.mMarker != null) {
            final LatLng position = this.mMarker.getPosition();

            final TextView city = (TextView) this.findViewById(R.id.weather_map_city);
            final TextView country = (TextView) this.findViewById(R.id.weather_map_country);
            final String cityString = city.getText().toString();
            final String countryString = country.getText().toString();

            final DatabaseQueries query = new DatabaseQueries(this.getApplicationContext());
            final WeatherLocation weatherLocation = query.queryDataBase();
            if (weatherLocation != null) {
                weatherLocation.setCity(cityString).setCountry(countryString).setLatitude(position.latitude)
                        .setLongitude(position.longitude).setLastCurrentUIUpdate(null).setLastForecastUIUpdate(null)
                        .setIsNew(true);
                query.updateDataBase(weatherLocation);
            } else {
                final WeatherLocation location = new WeatherLocation().setCity(cityString).setCountry(countryString)
                        .setIsSelected(true).setLatitude(position.latitude).setLongitude(position.longitude)
                        .setIsNew(true);
                query.insertIntoDataBase(location);
            }
        }
    }

    public void onClickGetLocation(final View v) {
        // TODO: Somehow I should show a progress dialog.
        // If Google Play Services is available
        if (this.mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
            // TODO: Hopefully there will be results even if location did not change...   
            final Criteria criteria = new Criteria();
            criteria.setAccuracy(Criteria.ACCURACY_FINE);
            criteria.setAltitudeRequired(false);
            criteria.setBearingAccuracy(Criteria.NO_REQUIREMENT);
            criteria.setBearingRequired(false);
            criteria.setCostAllowed(false);
            criteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH);
            criteria.setPowerRequirement(Criteria.POWER_MEDIUM);
            criteria.setSpeedAccuracy(Criteria.NO_REQUIREMENT);
            criteria.setSpeedRequired(false);
            criteria.setVerticalAccuracy(Criteria.ACCURACY_HIGH);

            this.mLocationManager.requestSingleUpdate(criteria, this, null);
        } else {
            Toast.makeText(this, this.getString(R.string.weather_map_not_enabled_location), Toast.LENGTH_LONG)
                    .show();
        }
        // Trying to use the synchronous calls. Problems: mGoogleApiClient read/store from different threads.
        // new GetLocationTask(this).execute();
    }

    private void updateUI(final WeatherLocation weatherLocation) {

        final TextView city = (TextView) this.findViewById(R.id.weather_map_city);
        final TextView country = (TextView) this.findViewById(R.id.weather_map_country);
        city.setText(weatherLocation.getCity());
        country.setText(weatherLocation.getCountry());

        this.mMapUpdate = new MapUpdate(this, weatherLocation);
        this.mMapUpdate.doUpdate();
    }

    private class MapActivityOnMapLongClickListener implements OnMapLongClickListener {
        private final Context localContext;

        private MapActivityOnMapLongClickListener(final Context context) {
            this.localContext = context;
        }

        @Override
        public void onMapLongClick(final LatLng point) {
            final MapActivity activity = (MapActivity) this.localContext;
            activity.getAddressAndUpdateUI(point.latitude, point.longitude);
        }

    }

    /**
     * Getting the address of the current location, using reverse geocoding only works if
     * a geocoding service is available.
     *
     */
    private void getAddressAndUpdateUI(final double latitude, final double longitude) {
        // In Gingerbread and later, use Geocoder.isPresent() to see if a geocoder is available.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD && Geocoder.isPresent()) {
            this.removeButtonsFragment();
            this.removeProgressFragment();
            this.addProgressFragment(latitude, longitude);
        } else {
            this.removeProgressFragment();
            this.addButtonsFragment();
            // No geocoder is present. Issue an error message.
            Toast.makeText(this, this.getString(R.string.weather_map_no_geocoder_available), Toast.LENGTH_LONG)
                    .show();

            // Default values
            final String city = this.getString(R.string.city_not_found);
            final String country = this.getString(R.string.country_not_found);
            final WeatherLocation weatherLocation = new WeatherLocation().setLatitude(latitude)
                    .setLongitude(longitude).setCity(city).setCountry(country);

            updateUI(weatherLocation);
        }
    }

    /*****************************************************************************************************
     *
     *                      MapProgressFragment.TaskCallbacks
     *
     *****************************************************************************************************/
    @Override
    public void onPostExecute(WeatherLocation weatherLocation) {

        this.updateUI(weatherLocation);
        this.removeProgressFragment();

        this.addButtonsFragment();
    }

    /*****************************************************************************************************
     *
     *                      MapProgressFragment
     * I am not using fragment transactions in the right way. But I do not know other way for doing what I am doing.
      * Android sucks.
      *
      * "Avoid performing transactions inside asynchronous callback methods." :(
      * see: http://stackoverflow.com/questions/16265733/failure-delivering-result-onactivityforresult
      * see: http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html
      * How do you do what I am doing in a different way without using fragments?
     *****************************************************************************************************/

    private void addProgressFragment(final double latitude, final double longitude) {
        final Fragment progressFragment = new MapProgressFragment();
        progressFragment.setRetainInstance(true);
        final Bundle args = new Bundle();
        args.putDouble("latitude", latitude);
        args.putDouble("longitude", longitude);
        progressFragment.setArguments(args);

        final FragmentManager fm = this.getSupportFragmentManager();
        final FragmentTransaction fragmentTransaction = fm.beginTransaction();
        fragmentTransaction.setCustomAnimations(R.anim.weather_map_enter_progress,
                R.anim.weather_map_exit_progress);
        fragmentTransaction.add(R.id.weather_map_buttons_container, progressFragment, PROGRESS_FRAGMENT_TAG)
                .commit();
        fm.executePendingTransactions();
    }

    private void removeProgressFragment() {
        final FragmentManager fm = this.getSupportFragmentManager();
        final Fragment progressFragment = fm.findFragmentByTag(PROGRESS_FRAGMENT_TAG);
        if (progressFragment != null) {
            final FragmentTransaction fragmentTransaction = fm.beginTransaction();
            fragmentTransaction.remove(progressFragment).commit();
            fm.executePendingTransactions();
        }
    }

    private void addButtonsFragment() {
        final FragmentManager fm = this.getSupportFragmentManager();
        Fragment buttonsFragment = fm.findFragmentByTag(BUTTONS_FRAGMENT_TAG);
        if (buttonsFragment == null) {
            buttonsFragment = new MapButtonsFragment();
            buttonsFragment.setRetainInstance(true);
            final FragmentTransaction fragmentTransaction = fm.beginTransaction();
            fragmentTransaction.setCustomAnimations(R.anim.weather_map_enter_progress,
                    R.anim.weather_map_exit_progress);
            fragmentTransaction.add(R.id.weather_map_buttons_container, buttonsFragment, BUTTONS_FRAGMENT_TAG)
                    .commit();
            fm.executePendingTransactions();
        }

        if (this.mButtonsUpdate == null) {
            this.mButtonsUpdate = new ButtonsUpdate(this);
        }
        this.mButtonsUpdate.doUpdate();
    }

    private void removeButtonsFragment() {
        final FragmentManager fm = this.getSupportFragmentManager();
        final Fragment buttonsFragment = fm.findFragmentByTag(BUTTONS_FRAGMENT_TAG);
        if (buttonsFragment != null) {
            final FragmentTransaction fragmentTransaction = fm.beginTransaction();
            fragmentTransaction.remove(buttonsFragment).commit();
            fm.executePendingTransactions();
        }
    }

    /*****************************************************************************************************
     *
     *                      android.location.LocationListener
     *
     *****************************************************************************************************/

    @Override
    public void onLocationChanged(final Location location) {
        // It was called from onClickGetLocation (UI thread) This method will run in the same thread (the UI thread)

        // Display the current location in the UI
        // TODO: May location not be null?
        this.getAddressAndUpdateUI(location.getLatitude(), location.getLongitude());
    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
    }

    @Override
    public void onProviderEnabled(String provider) {
    }

    @Override
    public void onProviderDisabled(String provider) {
    }

    private class MapActivityOnMapReadyCallback implements OnMapReadyCallback {
        private MapActivity mapActivity;

        private MapActivityOnMapReadyCallback(final MapActivity mapActivity) {
            this.mapActivity = mapActivity;
        }

        @Override
        public void onMapReady(final GoogleMap googleMap) {
            if (this.mapActivity != null) {
                this.mapActivity.mMap = googleMap;
                this.mapActivity.mMap.setMyLocationEnabled(false);
                this.mapActivity.mMap.getUiSettings().setMyLocationButtonEnabled(false);
                this.mapActivity.mMap.getUiSettings().setZoomControlsEnabled(true);
                this.mapActivity.mMap.getUiSettings().setCompassEnabled(true);
                this.mapActivity.mMap
                        .setOnMapLongClickListener(new MapActivityOnMapLongClickListener(this.mapActivity));

                if (this.mapActivity.mMapUpdate != null) {
                    this.mapActivity.mMapUpdate.doUpdate();
                }
                if (this.mapActivity.mButtonsUpdate != null) {
                    this.mapActivity.mButtonsUpdate.doUpdate();
                }
            }
        }

        /**
         * The Android rotate screen mess. Required because the Activity.isDestroyed method exists
         * just from API level 17.
         */
        public void onDestroy() {
            this.mapActivity = null;
        }
    }

    private class MapUpdate {
        private MapActivity mapActivity;
        private final WeatherLocation weatherLocation;

        private MapUpdate(MapActivity mapActivity, final WeatherLocation weatherLocation) {
            this.mapActivity = mapActivity;
            this.weatherLocation = weatherLocation;
        }

        private void doUpdate() {
            if (this.mapActivity.mMap == null) {
                // Do nothing.
                return;
            }

            final LatLng point = new LatLng(this.weatherLocation.getLatitude(),
                    this.weatherLocation.getLongitude());
            if (this.mapActivity.mMarker != null) {
                // Just one marker on map
                this.mapActivity.mMarker.remove();
            }
            this.mapActivity.mMarker = this.mapActivity.mMap
                    .addMarker(new MarkerOptions().position(point).draggable(true));
            this.mapActivity.mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(point, 5));
            this.mapActivity.mMap.animateCamera(CameraUpdateFactory.zoomIn());
            this.mapActivity.mMap.animateCamera(CameraUpdateFactory.zoomTo(8), 2000, null);
        }

        /**
         * The Android rotate screen mess. Required because the Activity.isDestroyed method exists
         * just from API level 17.
         */
        private void onDestroy() {
            this.mapActivity = null;
        }
    }

    private class ButtonsUpdate {
        private MapActivity mapActivity;

        private ButtonsUpdate(final MapActivity mapActivity) {
            this.mapActivity = mapActivity;
        }

        private void doUpdate() {
            if (this.mapActivity == null) {
                // Do nothing
                return;
            }

            if (this.mapActivity.mMap == null) {
                final Button getLocationButton = (Button) this.mapActivity
                        .findViewById(R.id.weather_map_button_getlocation);
                final Button saveLocationButton = (Button) this.mapActivity
                        .findViewById(R.id.weather_map_button_savelocation);
                if (getLocationButton != null) {
                    getLocationButton.setEnabled(false);
                }
                if (saveLocationButton != null) {
                    saveLocationButton.setEnabled(false);
                }
            } else {
                if (this.mapActivity.mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
                    final Button getLocationButton = (Button) this.mapActivity
                            .findViewById(R.id.weather_map_button_getlocation);
                    if (getLocationButton != null) {
                        getLocationButton.setEnabled(true);
                    }
                }
                final Button saveLocationButton = (Button) this.mapActivity
                        .findViewById(R.id.weather_map_button_savelocation);
                if (saveLocationButton != null) {
                    saveLocationButton.setEnabled(true);
                }
            }
        }

        /**
         * The Android rotate screen mess. Required because the Activity.isDestroyed method exists
         * just from API level 17.
         */
        private void onDestroy() {
            this.mapActivity = null;
        }
    }
}