Drone3D-Team/Drone3D

View on GitHub
app/src/main/java/ch/epfl/sdp/drone3d/ui/map/MissionInProgressActivity.kt

Summary

Maintainability
A
0 mins
Test Coverage
B
81%
/*
 * Copyright (C) 2021  Drone3D-Team
 * The license can be found in LICENSE at root of the repository
 */

package ch.epfl.sdp.drone3d.ui.map

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.View.*
import android.widget.TextView
import android.widget.Toast
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import ch.epfl.sdp.drone3d.R
import ch.epfl.sdp.drone3d.map.MapboxDroneDrawer
import ch.epfl.sdp.drone3d.map.MapboxHomeDrawer
import ch.epfl.sdp.drone3d.map.MapboxMissionDrawer
import ch.epfl.sdp.drone3d.map.MapboxUserDrawer
import ch.epfl.sdp.drone3d.model.weather.WeatherReport
import ch.epfl.sdp.drone3d.service.api.drone.DroneData
import ch.epfl.sdp.drone3d.service.api.drone.DroneData.DroneStatus
import ch.epfl.sdp.drone3d.service.api.drone.DroneService
import ch.epfl.sdp.drone3d.service.api.location.LocationService
import ch.epfl.sdp.drone3d.service.api.weather.WeatherService
import ch.epfl.sdp.drone3d.ui.ToastHandler
import ch.epfl.sdp.drone3d.ui.mission.MissionPicturesActivity
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.source.rtsp.RtspDefaultClient
import com.google.android.exoplayer2.source.rtsp.RtspMediaSource
import com.google.android.exoplayer2.source.rtsp.core.Client
import com.google.android.exoplayer2.ui.PlayerView
import com.google.android.material.button.MaterialButton
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.maps.MapView
import com.mapbox.mapboxsdk.maps.MapboxMap
import com.mapbox.mapboxsdk.maps.Style
import dagger.hilt.android.AndroidEntryPoint
import io.reactivex.disposables.CompositeDisposable
import timber.log.Timber
import javax.inject.Inject
import kotlin.math.abs

/**
 * This class show the drone on a map while it is doing a mission, as well as what the drone is currently
 * seeing.
 *
 * This class is heavily inspired by the class MapActivity.kt from the Fly2Find project. It has a few
 * adaptations :
 *
 * - deletion of some values we don't use in our implementation
 * - removal of the creation of a mission, it is done by another activity in our project
 * - addition of the use of the camera of the drone, and a video feed on the layout of this activity
 * - a few minor adaptations to make the class compatible with our project
 */
@AndroidEntryPoint
class MissionInProgressActivity : BaseMapActivity() {

    companion object {
        private const val DEFAULT_ZOOM: Double = 17.0
        private const val ZOOM_TOLERANCE: Double = 2.0

        // Constants used to update the user location on the map
        internal const val MIN_TIME_DELTA: Long = 1000
        internal const val MIN_DISTANCE_DELTA: Float = 1.0F

        // maximum distance allowed between the user and the drone, in meters
        private const val MAX_DIST_TO_USER: Double = 1000.0
    }

    @Inject
    lateinit var droneService: DroneService
    @Inject
    lateinit var locationService: LocationService
    @Inject
    lateinit var weatherService: WeatherService

    private val disposables = CompositeDisposable()
    private lateinit var mapboxMap: MapboxMap

    private lateinit var userDrawer: MapboxUserDrawer

    // used to keep track of the subscription tu the user location
    private var subscriptionTracker: Int? = null
    private lateinit var missionDrawer: MapboxMissionDrawer
    private lateinit var droneDrawer: MapboxDroneDrawer
    private lateinit var homeDrawer: MapboxHomeDrawer

    private lateinit var player: ExoPlayer
    private lateinit var rtspFactory: Client.Factory<*>

    private val observedData: MutableSet<LiveData<*>> = mutableSetOf()

    private lateinit var backToHomeButton: MaterialButton
    private lateinit var backToUserButton: MaterialButton

    private lateinit var weatherReport: LiveData<WeatherReport>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        initMapView(
            savedInstanceState,
            R.layout.activity_mission_in_progress,
            R.id.map_in_mission_view
        )

        mapView.getMapAsync { mapboxMap ->
            this.mapboxMap = mapboxMap
            mapboxMap.setStyle(Style.MAPBOX_STREETS) { style ->
                setupMapboxMap(mapView, style)
            }
        }

        backToHomeButton = findViewById(R.id.backToHomeButton)
        backToUserButton = findViewById(R.id.backToUserButton)

        weatherReport = if (droneService.getData().getPosition().value != null) {
            weatherService.getWeatherReport(droneService.getData().getPosition().value!!)
        } else {
            MutableLiveData()
        }

        player = SimpleExoPlayer.Builder(this).build()
        rtspFactory = RtspDefaultClient.factory(player)
            .setFlags(Client.FLAG_ENABLE_RTCP_SUPPORT)
            .setNatMethod(Client.RTSP_NAT_DUMMY)

        findViewById<PlayerView>(R.id.camera_mission_view).apply {
            this.player = player
            this.visibility = INVISIBLE
        }

        setupObservers()

        startMission()
    }

    private fun setupMapboxMap(mapView: MapView, style: Style) {
        userDrawer = MapboxUserDrawer(mapView, mapboxMap, style)
        homeDrawer = MapboxHomeDrawer(mapView, mapboxMap, style)
        droneDrawer = MapboxDroneDrawer(mapView, mapboxMap, style)
        missionDrawer = MapboxMissionDrawer(mapView, mapboxMap, style)

        centerCameraOnDrone()

        Transformations.map(droneService.getData().getMission()) { mission ->
            mission?.let {
                it.map { item ->
                    LatLng(item.latitudeDeg, item.longitudeDeg)
                }
            }
        }.observe(this, { path ->
            if (path != null) missionDrawer.showMission(path, false)
        })
    }

    /**
     * Centers the camera on the drone
     */
    private fun centerCameraOnDrone() {
        val currentZoom = mapboxMap.cameraPosition.zoom
        droneService.getData().getPosition().value?.let {
            mapboxMap.moveCamera(
                CameraUpdateFactory.newLatLngZoom(
                    it,
                    if (abs(currentZoom - DEFAULT_ZOOM) < ZOOM_TOLERANCE) currentZoom else DEFAULT_ZOOM
                )
            )
        }
    }

    /**
     * Launch the mission
     */
    private fun startMission() {
        try {
            disposables.add(
                droneService.getExecutor().executeMission(this)
                    .subscribe(
                        {
                            val intent = Intent(this, MissionPicturesActivity::class.java)
                            startActivity(intent)
                            finish()
                        },
                        {
                            showError(it)
                            finish()
                        })
            )
        } catch (e: Exception) {
            showError(e)
            finish()
        }
    }


    private fun setupObservers() {
        val droneData = droneService.getData()

        if (locationService.isLocationEnabled()) {
            subscriptionTracker = locationService.subscribeToLocationUpdates({ newLatLng: LatLng ->
                if (::userDrawer.isInitialized) userDrawer.showUser(newLatLng)
            }, MIN_TIME_DELTA, MIN_DISTANCE_DELTA)
        }

        createObserver(droneData.getPosition()) {
            it?.let { newLatLng -> if (::droneDrawer.isInitialized) droneDrawer.showDrone(newLatLng) }
        }

        createObserver(droneData.getHomeLocation()) {
            it?.let { home -> if (::homeDrawer.isInitialized) homeDrawer.showHome(LatLng(home.latitudeDeg, home.longitudeDeg)) }
        }

        createDroneStatusObserver(droneData)
        createTextObserver(droneData.getSpeed(), R.id.speedLive, R.string.live_speed) { it }
        createTextObserver(droneData.getRelativeAltitude(), R.id.altitudeLive, R.string.live_altitude) { it }
        createTextObserver(droneData.getBatteryLevel(), R.id.batteryLive, R.string.live_battery) { it * 100 }
        createPositionObserver(droneData)
        createConnectionObserver(droneData)
        if (!droneService.isSimulation()) {
            createDroneKeeperObserver(droneData)
        }
        createObserver(droneData.getVideoStreamUri()) { streamUri ->
            if (streamUri != null) {
                val mediaSource = RtspMediaSource.Factory(rtspFactory)
                    .createMediaSource(Uri.parse(streamUri.replace("rtspt://", "rtsp://")))

                player.setMediaSource(mediaSource)
                player.prepare()
                player.play()
                findViewById<View>(R.id.camera_mission_view).visibility = VISIBLE
            } else {
                findViewById<View>(R.id.camera_mission_view).visibility = INVISIBLE
            }
        }
    }

    /**
     * Stop the mission and bring back the drone to its home
     */
    fun backToHome(@Suppress("UNUSED_PARAMETER") view: View) {
        val completable = droneService.getExecutor().returnToHomeLocationAndLand(this)

        disposables.add(
            completable.subscribe({
                ToastHandler.showToastAsync(this, "The drone is coming back to its launch location")
            }, {
                showError(it)
            })
        )
    }

    /**
     * Stop the mission and bring back the drone to the user
     */
    fun backToUser(@Suppress("UNUSED_PARAMETER") view: View) {
        val completable = droneService.getExecutor().returnToUserLocationAndLand(this)

        disposables.add(
            completable.subscribe({
                ToastHandler.showToastAsync(this, "The drone is coming back to you")
            }, {
                showError(it)
            })
        )
    }

    private fun showError(ex: Throwable) {
        ToastHandler.showToastAsync(
            this,
            R.string.drone_mission_error,
            Toast.LENGTH_LONG,
            ex.message
        )
        Timber.e(ex)
    }

    override fun onDestroy() {
        super.onDestroy()

        if (this::droneDrawer.isInitialized) droneDrawer.onDestroy()
        if (this::missionDrawer.isInitialized) missionDrawer.onDestroy()
        if (this::homeDrawer.isInitialized) homeDrawer.onDestroy()
        if (this::userDrawer.isInitialized) userDrawer.onDestroy()

        if (subscriptionTracker != null) locationService.unsubscribeFromLocationUpdates(subscriptionTracker!!)

        observedData.forEach { data -> data.removeObservers(this) }
        observedData.clear()

        disposables.dispose()

        player.release()
    }

    private fun createPositionObserver(droneData: DroneData) {
        createObserver(droneData.getPosition()) {
            it?.let {
                findViewById<TextView>(R.id.distanceUserLive).apply {
                    text = if (locationService.isLocationEnabled())
                        if (locationService.getCurrentLocation() == null)
                            getString(R.string.user_location_null)
                        else {
                            val distanceUser: Double = it.distanceTo(locationService.getCurrentLocation()!!)
                            getString(R.string.live_distance_user, distanceUser)
                        }
                    else
                        getString(R.string.user_location_deactivated)
                }
            }
        }
    }

    private fun createDroneKeeperObserver(droneData: DroneData) {
        // Create an observer assuring that the drone stays within [MAX_DIST_TO_USER] meters of the user
        // and stay visible for the user
        createObserver(droneData.getPosition()) {
            it?.let {
                if (locationService.isLocationEnabled() && locationService.getCurrentLocation() != null) {
                    val distanceUser: Double = it.distanceTo(locationService.getCurrentLocation()!!)
                    val maxDistance: Double =
                        if (weatherReport.value != null && MAX_DIST_TO_USER > weatherReport.value!!.visibility) {
                            weatherReport.value!!.visibility.toDouble()
                        } else {
                            MAX_DIST_TO_USER
                        }

                    if (!droneService.isSimulation() && distanceUser > maxDistance && !droneData.isMissionPaused().value!!) {
                        droneService.getExecutor().pauseMission(this)
                        ToastHandler.showToastAsync(this, R.string.drone_too_far, Toast.LENGTH_SHORT)
                    } else if (distanceUser <= maxDistance && droneData.isMissionPaused().value!!) {
                        droneService.getExecutor().resumeMission(this)
                        ToastHandler.showToastAsync(this, R.string.drone_close_again, Toast.LENGTH_SHORT)
                    }
                }
            }
        }
    }

    private fun createDroneStatusObserver(droneData: DroneData) {
        createObserver(droneData.getDroneStatus()) { status ->
            val visibility = if (status == DroneStatus.EXECUTING_MISSION) VISIBLE else GONE

            backToHomeButton.visibility = visibility
            backToUserButton.visibility = visibility

            findViewById<TextView>(R.id.statusLive).apply {
                text = getString(R.string.live_status, status.name)
            }
        }
    }

    private fun createConnectionObserver(droneData: DroneData) {
        createObserver(droneData.isConnected()) {
            if (it == null || !it) {
                ToastHandler.showToastAsync(this, R.string.lost_connection_message, Toast.LENGTH_SHORT)
                finish()
            }
        }
    }

    private fun <T> createTextObserver(data: LiveData<T>, viewId: Int, txtId: Int, arg: (T) -> Any) {
        createObserver(data) {
            it?.let { findViewById<TextView>(viewId).text = getString(txtId, arg(it)) }
        }
    }

    private fun <T> createObserver(data: LiveData<T>, observer: Observer<T>) {
        data.observe(this, observer)
        observedData.add(data)
    }
}