/** * 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 - 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 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); LogHelper.v(LOG_TAG, "MainActivityMapFragment onCreate called."); // 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); // mTrackerServiceRunning = savedInstanceState.getBoolean(INSTANCE_TRACKING_STATE, false); } // 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; } // restore track StorageHelper storageHelper = new StorageHelper(mActivity, FILETYPE_TEMP); if (storageHelper.tempFileExists()) { // load track from temp file if it exists LoadTempTrackAsyncHelper loadTempTrackAsyncHelper = new LoadTempTrackAsyncHelper(); loadTempTrackAsyncHelper.execute(); LogHelper.v(LOG_TAG, "MapFragment: getting track from temp file."); } else if (savedInstanceState != null) { // load track from saved instance mTrack = savedInstanceState.getParcelable(INSTANCE_TRACK_MAIN_MAP); if (mTrack != null) { drawTrackOverlay(mTrack); LogHelper.v(LOG_TAG, "MapFragment: got track from saved instance."); } } // 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; // handle incoming intent (from notification) // handleIncomingIntent(); // todo remove // load state of tracker service - see if anything changed loadTrackerServiceState(mActivity); // center map on current position - if TrackerService is running if (mTrackerServiceRunning) { mController.setCenter(convertToGeoPoint(mCurrentBestLocation)); } // draw track on map - if available if (mTrack != null) { drawTrackOverlay(mTrack); } // show/hide the location off notification bar toggleLocationOffBar(); // start preliminary tracking - if no TrackerService is running // TODO check if this still works in tabbed ui 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); // // save state of track visibility // saveTrackVisibilityState(mActivity); } @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) { LogHelper.v(LOG_TAG, "MapFragment: onSaveInstanceState called. mTrack object is Null: " + (mTrack == null)); 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; // got a new track (from notification) Intent intent = mActivity.getIntent(); if (intent != null && intent.hasExtra(EXTRA_TRACK)) { mTrack = intent.getParcelableExtra(EXTRA_TRACK); LogHelper.v(LOG_TAG, "MapFragment: getting track from intent."); } // turn on/off tracking for MainActivity Fragment - prevent double tracking if (mTrackerServiceRunning) { stopPreliminaryTracking(); } else if (!mLocalTrackerRunning && mFragmentVisible) { startPreliminaryTracking(); } if (mTrack != null) { drawTrackOverlay(mTrack); } // 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; StorageHelper storageHelper = new StorageHelper(mActivity, FILETYPE_TEMP); storageHelper.deleteTempFile(); } // // save track state // saveTrackVisibilityState(mActivity); } // public void updateTrackingState(boolean trackerServiceRunning, Track track) { // mTrackerServiceRunning = trackerServiceRunning; // mTrack = track; // if (mTrackerServiceRunning && mTrack != null) { // drawTrackOverlay(mTrack); // // remove the blue "my location" dot // mMapView.getOverlays().remove(mMyLocationOverlay); // } // } // /* Handles new incoming intents */ // private void handleIncomingIntent() { // LogHelper.v(LOG_TAG, "Map Fragment received intent."); // Intent intent = mActivity.getIntent(); // String intentAction = intent.getAction(); // switch (intentAction) { // case ACTION_SHOW_MAP: //// if (intent.hasExtra(EXTRA_TRACK)) { //// LogHelper.v(LOG_TAG, "MapFragment: Intent received drawing map."); //// mTrack = intent.getParcelableExtra(EXTRA_TRACK); //// drawTrackOverlay(mTrack); //// } //// //// if (intent.hasExtra(EXTRA_TRACKING_STATE)) { //// mTrackerServiceRunning = intent.getBooleanExtra(EXTRA_TRACKING_STATE, false); //// mMapView.getOverlays().remove(mMyLocationOverlay); //// } // // // if (intent.hasExtra(EXTRA_TRACKING_STATE)) { // // store tracker service state // mTrackerServiceRunning = intent.getBooleanExtra(EXTRA_TRACKING_STATE, false); // // // get most current track (from notification) - if recording is active // if (mTrackerServiceRunning && intent.hasExtra(EXTRA_TRACK)) { // LogHelper.v(LOG_TAG, "MapFragment: Intent received drawing map."); // mTrack = intent.getParcelableExtra(EXTRA_TRACK); // drawTrackOverlay(mTrack); // // remove the blue "my location" dot // mMapView.getOverlays().remove(mMyLocationOverlay); // } // } // // // // prevent multiple reactions to intent // intent.setAction(ACTION_DEFAULT); // // break; // // default: // LogHelper.v(LOG_TAG, "Doing nothing. Type of ACTION: " + intentAction); // break; // } // } /* 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, "MapFragment: drawing new 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)) { // 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); LogHelper.v(LOG_TAG, "MapFragment: Track update received - drawing map."); } } }; } /* 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); } } // /* Saves state of track visibility to SharedPreferences */ // private void saveTrackVisibilityState(Context context) { // SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); // SharedPreferences.Editor editor = settings.edit(); // editor.putBoolean(PREFS_TRACK_VISIBLE, (mTrackOverlay != null)); // editor.apply(); // LogHelper.v(LOG_TAG, "Saving state: track visibility = " + (mTrackOverlay != null)); // } // /* Saves state of map */ // private void saveMaoState(Context context) { // SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); // SharedPreferences.Editor editor = settings.edit(); // editor.putInt(PREFS_ZOOM_LEVEL, mMapView.getZoomLevel()); // editor.apply(); // } // /* Loads app state from preferences */ // private void loadMapState(Context context) { // SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); // int zoom = settings.getInt(PREFS_ZOOM_LEVEL, 16); // } /* 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 StorageHelper storageHelper = new StorageHelper(mActivity, FILETYPE_TRACK); storageHelper.saveTrack(mTrack); 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 { StorageHelper storageHelper; @Override protected Void doInBackground(Void... voids) { LogHelper.v(LOG_TAG, "Loading temporary track object in background."); // load track object storageHelper = new StorageHelper(mActivity, FILETYPE_TEMP); mTrack = storageHelper.loadTrack(); 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 storageHelper.deleteTempFile(); } } }