374 lines
13 KiB
Kotlin
374 lines
13 KiB
Kotlin
/*
|
|
* MapFragment.kt
|
|
* Implements the MapFragment fragment
|
|
* A MapFragment displays a map using osmdroid as well as the controls to start / stop a recording
|
|
*
|
|
* This file is part of
|
|
* TRACKBOOK - Movement Recorder for Android
|
|
*
|
|
* Copyright (c) 2016-20 - 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 YesNoDialog
|
|
import android.Manifest
|
|
import android.content.*
|
|
import android.content.pm.PackageManager
|
|
import android.location.Location
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.Handler
|
|
import android.os.IBinder
|
|
import android.view.LayoutInflater
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.fragment.app.Fragment
|
|
import androidx.navigation.fragment.findNavController
|
|
import androidx.preference.PreferenceManager
|
|
import kotlinx.coroutines.GlobalScope
|
|
import kotlinx.coroutines.launch
|
|
import org.y20k.trackbook.core.Track
|
|
import org.y20k.trackbook.core.TracklistElement
|
|
import org.y20k.trackbook.helpers.*
|
|
import org.y20k.trackbook.ui.MapFragmentLayoutHolder
|
|
|
|
|
|
/*
|
|
* MapFragment class
|
|
*/
|
|
class MapFragment : Fragment(), YesNoDialog.YesNoDialogListener, MapOverlay.MarkerListener {
|
|
|
|
/* Define log tag */
|
|
private val TAG: String = LogHelper.makeLogTag(MapFragment::class.java)
|
|
|
|
|
|
/* Main class variables */
|
|
private var bound: Boolean = false
|
|
private val handler: Handler = Handler()
|
|
private var trackingState: Int = Keys.STATE_TRACKING_NOT
|
|
private var gpsProviderActive: Boolean = false
|
|
private var networkProviderActive: Boolean = false
|
|
private var track: Track = Track()
|
|
private lateinit var currentBestLocation: Location
|
|
private lateinit var layout: MapFragmentLayoutHolder
|
|
private lateinit var trackerService: TrackerService
|
|
|
|
|
|
/* Overrides onCreate from Fragment */
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
// get current best location
|
|
currentBestLocation = LocationHelper.getLastKnownLocation(activity as Context)
|
|
// get saved tracking state
|
|
trackingState = PreferencesHelper.loadTrackingState(activity as Context)
|
|
}
|
|
|
|
|
|
/* Overrides onStop from Fragment */
|
|
override fun onCreateView(
|
|
inflater: LayoutInflater,
|
|
container: ViewGroup?,
|
|
savedInstanceState: Bundle?
|
|
): View? {
|
|
// initialize layout
|
|
layout = MapFragmentLayoutHolder(
|
|
activity as Context,
|
|
this as MapOverlay.MarkerListener,
|
|
inflater,
|
|
container,
|
|
currentBestLocation,
|
|
trackingState
|
|
)
|
|
|
|
// set up buttons
|
|
layout.currentLocationButton.setOnClickListener {
|
|
layout.centerMap(currentBestLocation, animated = true)
|
|
}
|
|
layout.recordingButton.setOnClickListener {
|
|
handleTrackingManagementMenu()
|
|
}
|
|
layout.saveButton.setOnClickListener {
|
|
saveTrack()
|
|
}
|
|
layout.clearButton.setOnClickListener {
|
|
trackerService.clearTrack()
|
|
}
|
|
layout.resumeButton.setOnClickListener {
|
|
// start service via intent so that it keeps running after unbind
|
|
startTrackerService()
|
|
trackerService.resumeTracking()
|
|
}
|
|
|
|
return layout.rootView
|
|
}
|
|
|
|
|
|
/* Overrides onStart from Fragment */
|
|
override fun onStart() {
|
|
super.onStart()
|
|
// request location permission if denied
|
|
if (ContextCompat.checkSelfPermission(
|
|
activity as Context,
|
|
Manifest.permission.ACCESS_FINE_LOCATION
|
|
) == PackageManager.PERMISSION_DENIED
|
|
) {
|
|
this.requestPermissions(
|
|
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
|
|
Keys.REQUEST_CODE_FOREGROUND
|
|
)
|
|
}
|
|
// bind to TrackerService
|
|
activity?.bindService(
|
|
Intent(activity, TrackerService::class.java),
|
|
connection,
|
|
Context.BIND_AUTO_CREATE
|
|
)
|
|
}
|
|
|
|
|
|
/* Overrides onResume from Fragment */
|
|
override fun onResume() {
|
|
super.onResume()
|
|
// if (bound) {
|
|
// trackerService.addGpsLocationListener()
|
|
// trackerService.addNetworkLocationListener()
|
|
// }
|
|
}
|
|
|
|
|
|
/* Overrides onPause from Fragment */
|
|
override fun onPause() {
|
|
super.onPause()
|
|
layout.saveState(currentBestLocation)
|
|
if (bound && trackingState != Keys.STATE_TRACKING_ACTIVE) {
|
|
trackerService.removeGpsLocationListener()
|
|
trackerService.removeNetworkLocationListener()
|
|
}
|
|
}
|
|
|
|
|
|
/* Overrides onStop from Fragment */
|
|
override fun onStop() {
|
|
super.onStop()
|
|
// unbind from TrackerService
|
|
activity?.unbindService(connection)
|
|
}
|
|
|
|
|
|
/* Overrides onRequestPermissionsResult from Fragment */
|
|
override fun onRequestPermissionsResult(
|
|
requestCode: Int,
|
|
permissions: Array<String>,
|
|
grantResults: IntArray
|
|
) {
|
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
when (requestCode) {
|
|
Keys.REQUEST_CODE_FOREGROUND -> {
|
|
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
|
|
// permission was granted - re-bind service
|
|
activity?.unbindService(connection)
|
|
activity?.bindService(
|
|
Intent(activity, TrackerService::class.java),
|
|
connection,
|
|
Context.BIND_AUTO_CREATE
|
|
)
|
|
LogHelper.i(TAG, "Request result: Location permission has been granted.")
|
|
} else {
|
|
// permission denied - unbind service
|
|
activity?.unbindService(connection)
|
|
}
|
|
layout.toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Overrides onYesNoDialog from YesNoDialogListener */
|
|
override fun onYesNoDialog(
|
|
type: Int,
|
|
dialogResult: Boolean,
|
|
payload: Int,
|
|
payloadString: String
|
|
) {
|
|
super.onYesNoDialog(type, dialogResult, payload, payloadString)
|
|
when (type) {
|
|
Keys.DIALOG_EMPTY_RECORDING -> {
|
|
when (dialogResult) {
|
|
// user tapped resume
|
|
true -> {
|
|
trackerService.resumeTracking()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Overrides onMarkerTapped from MarkerListener */
|
|
override fun onMarkerTapped(latitude: Double, longitude: Double) {
|
|
super.onMarkerTapped(latitude, longitude)
|
|
if (bound) {
|
|
track = TrackHelper.toggleStarred(activity as Context, track, latitude, longitude)
|
|
layout.overlayCurrentTrack(track, trackingState)
|
|
trackerService.track = track
|
|
}
|
|
}
|
|
|
|
|
|
/* Start tracker service */
|
|
private fun startTrackerService() {
|
|
val intent = Intent(activity, TrackerService::class.java)
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
// ... start service in foreground to prevent it being killed on Oreo
|
|
activity?.startForegroundService(intent)
|
|
} else {
|
|
activity?.startService(intent)
|
|
}
|
|
}
|
|
|
|
|
|
/* Starts / pauses tracking and toggles the recording sub menu_bottom_navigation */
|
|
private fun handleTrackingManagementMenu() {
|
|
when (trackingState) {
|
|
Keys.STATE_TRACKING_STOPPED -> layout.toggleRecordingButtonSubMenu()
|
|
Keys.STATE_TRACKING_ACTIVE -> trackerService.stopTracking()
|
|
Keys.STATE_TRACKING_NOT -> {
|
|
// start service via intent so that it keeps running after unbind
|
|
startTrackerService()
|
|
trackerService.startTracking()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Saves track - shows dialog, if recording is still empty */
|
|
private fun saveTrack() {
|
|
if (track.wayPoints.isEmpty()) {
|
|
YesNoDialog(this as YesNoDialog.YesNoDialogListener).show(
|
|
activity as Context,
|
|
type = Keys.DIALOG_EMPTY_RECORDING,
|
|
title = R.string.dialog_error_empty_recording_title,
|
|
message = R.string.dialog_error_empty_recording_message,
|
|
yesButton = R.string.dialog_error_empty_recording_action_resume
|
|
)
|
|
} else {
|
|
GlobalScope.launch {
|
|
// step 1: create and store filenames for json and gpx files
|
|
track.trackUriString =
|
|
FileHelper.getTrackFileUri(activity as Context, track).toString()
|
|
track.gpxUriString = FileHelper.getGpxFileUri(activity as Context, track).toString()
|
|
// step 2: save track
|
|
FileHelper.saveTrackSuspended(track, saveGpxToo = true)
|
|
// step 3: save tracklist - suspended
|
|
FileHelper.addTrackAndSaveTracklistSuspended(activity as Context, track)
|
|
// step 3: clear track
|
|
trackerService.clearTrack()
|
|
// step 4: open track in TrackFragement
|
|
openTrack(track.toTracklistElement(activity as Context))
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Opens a track in TrackFragment */
|
|
private fun openTrack(tracklistElement: TracklistElement) {
|
|
val bundle = Bundle()
|
|
bundle.putString(Keys.ARG_TRACK_TITLE, tracklistElement.name)
|
|
bundle.putString(Keys.ARG_TRACK_FILE_URI, tracklistElement.trackUriString)
|
|
bundle.putString(Keys.ARG_GPX_FILE_URI, tracklistElement.gpxUriString)
|
|
bundle.putLong(Keys.ARG_TRACK_ID, TrackHelper.getTrackId(tracklistElement))
|
|
findNavController().navigate(R.id.fragment_track, bundle)
|
|
}
|
|
|
|
|
|
/*
|
|
* Defines the listener for changes in shared preferences
|
|
*/
|
|
private val sharedPreferenceChangeListener =
|
|
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
|
when (key) {
|
|
Keys.PREF_TRACKING_STATE -> {
|
|
if (activity != null) {
|
|
trackingState = PreferencesHelper.loadTrackingState(activity as Context)
|
|
layout.updateRecordingButton(trackingState)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
* End of declaration
|
|
*/
|
|
|
|
|
|
/*
|
|
* Defines callbacks for service binding, passed to bindService()
|
|
*/
|
|
private val connection = object : ServiceConnection {
|
|
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
|
bound = true
|
|
// get reference to tracker service
|
|
val binder = service as TrackerService.LocalBinder
|
|
trackerService = binder.service
|
|
// get state of tracking and update button if necessary
|
|
trackingState = trackerService.trackingState
|
|
layout.updateRecordingButton(trackingState)
|
|
// register listener for changes in shared preferences
|
|
PreferenceManager.getDefaultSharedPreferences(activity as Context)
|
|
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
|
|
// start listening for location updates
|
|
handler.removeCallbacks(periodicLocationRequestRunnable)
|
|
handler.postDelayed(periodicLocationRequestRunnable, 0)
|
|
}
|
|
|
|
override fun onServiceDisconnected(arg0: ComponentName) {
|
|
bound = false
|
|
// unregister listener for changes in shared preferences
|
|
PreferenceManager.getDefaultSharedPreferences(activity as Context)
|
|
.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener)
|
|
// stop receiving location updates
|
|
handler.removeCallbacks(periodicLocationRequestRunnable)
|
|
}
|
|
}
|
|
/*
|
|
* End of declaration
|
|
*/
|
|
|
|
|
|
/*
|
|
* Runnable: Periodically requests location
|
|
*/
|
|
private val periodicLocationRequestRunnable: Runnable = object : Runnable {
|
|
override fun run() {
|
|
// pull current state from service
|
|
currentBestLocation = trackerService.currentBestLocation
|
|
track = trackerService.track
|
|
gpsProviderActive = trackerService.gpsProviderActive
|
|
networkProviderActive = trackerService.networkProviderActive
|
|
trackingState = trackerService.trackingState
|
|
// update location and track
|
|
layout.markCurrentPosition(currentBestLocation, trackingState)
|
|
layout.overlayCurrentTrack(track, trackingState)
|
|
// center map, if it had not been dragged/zoomed before
|
|
if (!layout.userInteraction) {
|
|
layout.centerMap(currentBestLocation, true)
|
|
}
|
|
// show error snackbar if necessary
|
|
layout.toggleLocationErrorBar(gpsProviderActive, networkProviderActive)
|
|
// use the handler to start runnable again after specified delay
|
|
handler.postDelayed(this, Keys.REQUEST_CURRENT_LOCATION_INTERVAL)
|
|
}
|
|
}
|
|
/*
|
|
* End of declaration
|
|
*/
|
|
|
|
}
|