/** * MainActivityMapFragment.java * Implements the map fragment used in the map tab of the main activity * This fragment displays a map using osmdroid * * This file is part of * TRACKBOOK - Movement Recorder for Android * * Copyright (c) 2016-17 - Y20K.org * Licensed under the MIT-License * http://opensource.org/licenses/MIT * * Trackbook uses osmdroid - OpenStreetMap-Tools for Android * https://github.com/osmdroid/osmdroid */ package org.y20k.trackbook; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.database.ContentObserver; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v4.content.LocalBroadcastManager; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import org.osmdroid.api.IMapController; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; import org.osmdroid.util.GeoPoint; import org.osmdroid.views.MapView; import org.osmdroid.views.overlay.ItemizedIconOverlay; import org.osmdroid.views.overlay.compass.CompassOverlay; import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider; import org.y20k.trackbook.core.Track; import org.y20k.trackbook.helpers.LocationHelper; import org.y20k.trackbook.helpers.LogHelper; import org.y20k.trackbook.helpers.MapHelper; import org.y20k.trackbook.helpers.StorageHelper; import org.y20k.trackbook.helpers.TrackbookKeys; import java.util.List; /** * MainActivityMapFragment class */ public class MainActivityMapFragment extends Fragment implements TrackbookKeys { /* Define log tag */ private static final String LOG_TAG = MainActivityMapFragment.class.getSimpleName(); /* Main class variables */ private Activity mActivity; private Track mTrack; private boolean mFirstStart; private Snackbar mLocationOffBar; private BroadcastReceiver mTrackUpdatedReceiver; private SettingsContentObserver mSettingsContentObserver; private MapView mMapView; private IMapController mController; private StorageHelper mStorageHelper; private LocationManager mLocationManager; private LocationListener mGPSListener; private LocationListener mNetworkListener; private ItemizedIconOverlay mMyLocationOverlay; private ItemizedIconOverlay mTrackOverlay; private Location mCurrentBestLocation; private boolean mTrackerServiceRunning; private boolean mLocalTrackerRunning; private boolean mLocationSystemSetting; private boolean mFragmentVisible; /* Constructor (default) */ public MainActivityMapFragment() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // get activity mActivity = getActivity(); // action bar has options menu setHasOptionsMenu(true); // restore first start state and tracking state mFirstStart = true; mTrackerServiceRunning = false; loadTrackerServiceState(mActivity); if (savedInstanceState != null) { mFirstStart = savedInstanceState.getBoolean(INSTANCE_FIRST_START, true); } // create storage helper mStorageHelper = new StorageHelper(mActivity); // acquire reference to Location Manager mLocationManager = (LocationManager) mActivity.getSystemService(Context.LOCATION_SERVICE); // CASE 1: get saved location if possible if (savedInstanceState != null) { Location savedLocation = savedInstanceState.getParcelable(INSTANCE_CURRENT_LOCATION); // check if saved location is still current if (LocationHelper.isNewLocation(savedLocation)) { mCurrentBestLocation = savedLocation; } else { mCurrentBestLocation = null; } } // CASE 2: get last known location if no saved location or saved location is too old if (mCurrentBestLocation == null && mLocationManager.getProviders(true).size() > 0) { mCurrentBestLocation = LocationHelper.determineLastKnownLocation(mLocationManager); } // CASE 3: location services are available but unable to get location - this should not happen if (mCurrentBestLocation == null) { mCurrentBestLocation = new Location(LocationManager.NETWORK_PROVIDER); mCurrentBestLocation.setLatitude(DEFAULT_LATITUDE); mCurrentBestLocation.setLongitude(DEFAULT_LONGITUDE); } // get state of location system setting mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(mActivity); // create content observer for changes in System Settings mSettingsContentObserver = new SettingsContentObserver( new Handler()); // register broadcast receiver for new WayPoints mTrackUpdatedReceiver = createTrackUpdatedReceiver(); IntentFilter trackUpdatedIntentFilter = new IntentFilter(ACTION_TRACK_UPDATED); LocalBroadcastManager.getInstance(mActivity).registerReceiver(mTrackUpdatedReceiver, trackUpdatedIntentFilter); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // create basic map mMapView = new MapView(inflater.getContext()); // get map controller mController = mMapView.getController(); // basic map setup mMapView.setTileSource(TileSourceFactory.MAPNIK); mMapView.setTilesScaledToDpi(true); // add multi-touch capability mMapView.setMultiTouchControls(true); // add compass to map CompassOverlay compassOverlay = new CompassOverlay(mActivity, new InternalCompassOrientationProvider(mActivity), mMapView); compassOverlay.enableCompass(); mMapView.getOverlays().add(compassOverlay); // initiate map state if (savedInstanceState != null) { // restore saved instance of map GeoPoint position = new GeoPoint(savedInstanceState.getDouble(INSTANCE_LATITUDE_MAIN_MAP, DEFAULT_LATITUDE), savedInstanceState.getDouble(INSTANCE_LONGITUDE_MAIN_MAP, DEFAULT_LONGITUDE)); mController.setCenter(position); mController.setZoom(savedInstanceState.getInt(INSTANCE_ZOOM_LEVEL_MAIN_MAP, 16)); // restore current location mCurrentBestLocation = savedInstanceState.getParcelable(INSTANCE_CURRENT_LOCATION); } else if (mCurrentBestLocation != null) { // fallback or first run: set map to current position GeoPoint position = convertToGeoPoint(mCurrentBestLocation); mController.setCenter(position); mController.setZoom(16); } // inform user that new/better location is on its way if (mFirstStart && !mTrackerServiceRunning) { Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_acquiring_location), Toast.LENGTH_LONG).show(); mFirstStart = false; } // load track from saved instance if (savedInstanceState != null) { mTrack = savedInstanceState.getParcelable(INSTANCE_TRACK_MAIN_MAP); } // mark user's location on map if (mCurrentBestLocation != null && !mTrackerServiceRunning) { mMyLocationOverlay = MapHelper.createMyLocationOverlay(mActivity, mCurrentBestLocation, LocationHelper.isNewLocation(mCurrentBestLocation)); mMapView.getOverlays().add(mMyLocationOverlay); } return mMapView; } @Override public void onResume() { super.onResume(); // set visibility mFragmentVisible = true; // load state of tracker service - see if anything changed loadTrackerServiceState(mActivity); // CASE 1: recording active if (mTrackerServiceRunning) { // request an updated track recording from service Intent i = new Intent(); i.setAction(ACTION_TRACK_REQUEST); LocalBroadcastManager.getInstance(mActivity).sendBroadcast(i); } // CASE 2: recording stopped - temp file exists else if (mStorageHelper.tempFileExists()) { // load track from temp file if it exists LoadTempTrackAsyncHelper loadTempTrackAsyncHelper = new LoadTempTrackAsyncHelper(); loadTempTrackAsyncHelper.execute(); // CASE 3: not recording and no temp file } else if (mTrack != null) { // just draw existing track data (from saved instance) drawTrackOverlay(mTrack); } // show/hide the location off notification bar toggleLocationOffBar(); // start preliminary tracking - if no TrackerService is running if (!mTrackerServiceRunning && mFragmentVisible) { startPreliminaryTracking(); } // register content observer for changes in System Settings mActivity.getContentResolver().registerContentObserver(android.provider.Settings.Secure.CONTENT_URI, true, mSettingsContentObserver ); } @Override public void onPause() { super.onPause(); // set visibility mFragmentVisible = false; // disable preliminary location listeners stopPreliminaryTracking(); // disable content observer for changes in System Settings mActivity.getContentResolver().unregisterContentObserver(mSettingsContentObserver); } @Override public void onDestroyView(){ super.onDestroyView(); // deactivate map mMapView.onDetach(); } @Override public void onDestroy() { LogHelper.v(LOG_TAG, "onDestroy called."); // reset first start state mFirstStart = true; // disable broadcast receivers LocalBroadcastManager.getInstance(mActivity).unregisterReceiver(mTrackUpdatedReceiver); super.onDestroy(); } @Override public boolean onOptionsItemSelected(MenuItem item) { // handle action bar options menu selection switch (item.getItemId()) { // CASE MY LOCATION case R.id.action_bar_my_location: // do nothing if location setting is off if (toggleLocationOffBar()) { stopPreliminaryTracking(); return false; } // get current position GeoPoint position; if (mTrackerServiceRunning && mTrack != null) { // get current Location from tracker service mCurrentBestLocation = mTrack.getWayPointLocation(mTrack.getSize() - 1); } else if (mCurrentBestLocation == null) { // app does not have any location fix mCurrentBestLocation = LocationHelper.determineLastKnownLocation(mLocationManager); } // check if really got a position if (mCurrentBestLocation != null) { position = convertToGeoPoint(mCurrentBestLocation); // center map on current position mController.setCenter(position); // mark user's new location on map and remove last marker updateMyLocationMarker(); // inform user about location quality String locationInfo; long locationAge = (SystemClock.elapsedRealtimeNanos() - mCurrentBestLocation.getElapsedRealtimeNanos()) / 1000000; String locationAgeString = LocationHelper.convertToReadableTime(locationAge, false); if (locationAgeString == null) { locationAgeString = mActivity.getString(R.string.toast_message_last_location_age_one_hour); } locationInfo = " " + locationAgeString + " | " + mCurrentBestLocation.getProvider(); Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_last_location) + locationInfo, Toast.LENGTH_LONG).show(); return true; } else { Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_location_services_not_ready), Toast.LENGTH_LONG).show(); return false; } // CASE DEFAULT default: return super.onOptionsItemSelected(item); } } @Override public void onSaveInstanceState(Bundle outState) { outState.putBoolean(INSTANCE_FIRST_START, mFirstStart); outState.putBoolean(INSTANCE_TRACKING_STATE, mTrackerServiceRunning); outState.putParcelable(INSTANCE_CURRENT_LOCATION, mCurrentBestLocation); outState.putDouble(INSTANCE_LATITUDE_MAIN_MAP, mMapView.getMapCenter().getLatitude()); outState.putDouble(INSTANCE_LONGITUDE_MAIN_MAP, mMapView.getMapCenter().getLongitude()); outState.putInt(INSTANCE_ZOOM_LEVEL_MAIN_MAP, mMapView.getZoomLevel()); outState.putParcelable(INSTANCE_TRACK_MAIN_MAP, mTrack); super.onSaveInstanceState(outState); } /* Setter for tracking state */ public void setTrackingState(boolean trackingState) { mTrackerServiceRunning = trackingState; // turn on/off tracking for MainActivity Fragment - prevent double tracking if (mTrackerServiceRunning) { stopPreliminaryTracking(); } else if (!mLocalTrackerRunning && mFragmentVisible) { startPreliminaryTracking(); } if (mTrack != null) { drawTrackOverlay(mTrack); // TODO check if redundant } // update marker updateMyLocationMarker(); LogHelper.v(LOG_TAG, "TrackingState: " + trackingState); } /* Getter for current best location */ public Location getCurrentBestLocation() { return mCurrentBestLocation; } /* Removes track crumbs from map */ public void clearMap(boolean saveTrack) { // clear map if (mTrackOverlay != null) { mMapView.getOverlays().remove(mTrackOverlay); mTrackOverlay = null; } if (saveTrack) { // save track object if requested SaveTrackAsyncHelper saveTrackAsyncHelper = new SaveTrackAsyncHelper(); saveTrackAsyncHelper.execute(); Toast.makeText(mActivity, mActivity.getString(R.string.toast_message_save_track), Toast.LENGTH_LONG).show(); } else { // clear track object and delete temp file mTrack = null; mStorageHelper.deleteTempFile(); } } /* Start preliminary tracking for map */ private void startPreliminaryTracking() { if (mLocationSystemSetting && !mLocalTrackerRunning) { // create location listeners List locationProviders = mLocationManager.getAllProviders(); if (locationProviders.contains(LocationManager.GPS_PROVIDER)) { mGPSListener = createLocationListener(); mLocalTrackerRunning = true; } if (locationProviders.contains(LocationManager.NETWORK_PROVIDER)) { mNetworkListener = createLocationListener(); mLocalTrackerRunning = true; } // register listeners LocationHelper.registerLocationListeners(mLocationManager, mGPSListener, mNetworkListener); LogHelper.v(LOG_TAG, "Starting preliminary tracking."); } } /* Removes gps and network location listeners */ private void stopPreliminaryTracking() { if (mLocalTrackerRunning) { mLocalTrackerRunning = false; // remove listeners LocationHelper.removeLocationListeners(mLocationManager, mGPSListener, mNetworkListener); LogHelper.v(LOG_TAG, "Stopping preliminary tracking."); } } /* Creates listener for changes in location status */ private LocationListener createLocationListener() { return new LocationListener() { public void onLocationChanged(Location location) { // check if the new location is better if (mCurrentBestLocation == null || LocationHelper.isBetterLocation(location, mCurrentBestLocation)) { // save location mCurrentBestLocation = location; // mark user's new location on map and remove last marker updateMyLocationMarker(); } } public void onStatusChanged(String provider, int status, Bundle extras) { LogHelper.v(LOG_TAG, "Location provider status change: " + provider + " | " + status); } public void onProviderEnabled(String provider) { LogHelper.v(LOG_TAG, "Location provider enabled: " + provider); } public void onProviderDisabled(String provider) { LogHelper.v(LOG_TAG, "Location provider disabled: " + provider); } }; } /* Updates marker for current user location */ private void updateMyLocationMarker() { mMapView.getOverlays().remove(mMyLocationOverlay); // only update while not tracking if (!mTrackerServiceRunning) { mMyLocationOverlay = MapHelper.createMyLocationOverlay(mActivity, mCurrentBestLocation, LocationHelper.isNewLocation(mCurrentBestLocation)); mMapView.getOverlays().add(mMyLocationOverlay); } } /* Draws track onto overlay */ private void drawTrackOverlay(Track track) { mMapView.getOverlays().remove(mTrackOverlay); mTrackOverlay = null; if (track != null) { LogHelper.v(LOG_TAG, "Drawing track overlay."); mTrackOverlay = MapHelper.createTrackOverlay(mActivity, track, mTrackerServiceRunning); mMapView.getOverlays().add(mTrackOverlay); } } /* Toggles snackbar indicating that location setting is off */ private boolean toggleLocationOffBar() { // create snackbar indicator for location setting off if (mLocationOffBar == null) { mLocationOffBar = Snackbar.make(mMapView, R.string.snackbar_message_location_offline, Snackbar.LENGTH_INDEFINITE).setAction("Action", null); } // get state of location system setting mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(mActivity); // show snackbar if necessary if (!mLocationSystemSetting) { // show snackbar mLocationOffBar.show(); return true; } else { // hide snackbar mLocationOffBar.dismiss(); return false; } } /* Creates receiver for new WayPoints */ private BroadcastReceiver createTrackUpdatedReceiver() { return new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.hasExtra(EXTRA_TRACK) && intent.hasExtra(EXTRA_LAST_LOCATION)) { LogHelper.v(LOG_TAG, "Track update received."); // draw track on map mTrack = intent.getParcelableExtra(EXTRA_TRACK); drawTrackOverlay(mTrack); // center map over last location mCurrentBestLocation = intent.getParcelableExtra(EXTRA_LAST_LOCATION); mController.setCenter(convertToGeoPoint(mCurrentBestLocation)); // clear intent intent.setAction(ACTION_DEFAULT); } } }; } /* Converts Location to GeoPoint */ private GeoPoint convertToGeoPoint (Location location) { if (location != null) { return new GeoPoint(location.getLatitude(), location.getLongitude()); } else { return new GeoPoint(DEFAULT_LATITUDE, DEFAULT_LONGITUDE); } } /* Loads state tracker service from preferences */ private void loadTrackerServiceState(Context context) { SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); int fabState = settings.getInt(PREFS_FAB_STATE, FAB_STATE_DEFAULT); mTrackerServiceRunning = fabState == FAB_STATE_RECORDING; } /** * Inner class: SettingsContentObserver is a custom ContentObserver for changes in Android Settings */ private class SettingsContentObserver extends ContentObserver { SettingsContentObserver(Handler handler) { super(handler); } @Override public boolean deliverSelfNotifications() { return super.deliverSelfNotifications(); } @Override public void onChange(boolean selfChange) { super.onChange(selfChange); LogHelper.v(LOG_TAG, "System Setting change detected."); // check if location setting was changed boolean previousLocationSystemSetting = mLocationSystemSetting; mLocationSystemSetting = LocationHelper.checkLocationSystemSetting(mActivity); if (previousLocationSystemSetting != mLocationSystemSetting) { LogHelper.v(LOG_TAG, "Location Setting change detected."); toggleLocationOffBar(); } // start / stop preliminary tracking if (!mLocationSystemSetting) { stopPreliminaryTracking(); } else if (!mTrackerServiceRunning && mFragmentVisible) { startPreliminaryTracking(); } } } /** * Inner class: Saves track to external storage using AsyncTask */ private class SaveTrackAsyncHelper extends AsyncTask { @Override protected Void doInBackground(Void... voids) { LogHelper.v(LOG_TAG, "Saving track object in background."); // save track object mStorageHelper.saveTrack(mTrack, FILE_TYPE_TRACK); return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); // clear track object LogHelper.v(LOG_TAG, "Saving finished."); mTrack = null; // notify track fragment that save is finished Intent i = new Intent(); i.setAction(ACTION_TRACK_SAVE); i.putExtra(EXTRA_SAVE_FINISHED, true); LocalBroadcastManager.getInstance(mActivity).sendBroadcast(i); } } /** * Inner class: Loads track from external storage using AsyncTask */ private class LoadTempTrackAsyncHelper extends AsyncTask { @Override protected Void doInBackground(Void... voids) { LogHelper.v(LOG_TAG, "Loading temporary track object in background."); // load track object mTrack = mStorageHelper.loadTrack(FILE_TYPE_TEMP); return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); LogHelper.v(LOG_TAG, "Loading finished."); // draw track on map if (mTrack != null) { drawTrackOverlay(mTrack); } // delete temp file mStorageHelper.deleteTempFile(); } } }