Drone3D-Team/Drone3D

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

Summary

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

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

import android.app.AlertDialog
import android.graphics.LightingColorFilter
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.EditText
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import ch.epfl.sdp.drone3d.R
import ch.epfl.sdp.drone3d.map.gps.LocationComponentManager
import ch.epfl.sdp.drone3d.map.offline.OfflineMapSaver
import ch.epfl.sdp.drone3d.map.offline.OfflineMapSaverImpl
import ch.epfl.sdp.drone3d.service.api.location.LocationService
import ch.epfl.sdp.drone3d.ui.ToastHandler
import ch.epfl.sdp.drone3d.ui.map.BaseMapActivity
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.mapbox.mapboxsdk.Mapbox
import com.mapbox.mapboxsdk.maps.MapboxMap
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.offline.OfflineRegion
import com.mapbox.mapboxsdk.offline.OfflineRegionError
import com.mapbox.mapboxsdk.offline.OfflineRegionStatus
import com.mapbox.mapboxsdk.plugins.annotation.LineManager
import com.mapbox.mapboxsdk.plugins.annotation.LineOptions
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import java.lang.System.currentTimeMillis
import javax.inject.Inject
import kotlin.math.min


/**
 * Activity which allow a user to select regions on the map to download/remove when he's online so
 * that he can use them in offline mode.
 */
@AndroidEntryPoint
class ManageOfflineMapActivity : BaseMapActivity(), OnMapReadyCallback {

    companion object {
        private const val DOWNLOAD_STATUS_TIME_DELAY = 1000
        private const val BACKGROUND_COLOR_MULTIPLIER = -0x1000000
    }

    // Location
    @Inject
    lateinit var locationService: LocationService

    private lateinit var lineManager: LineManager
    private lateinit var offlineMapSaver: OfflineMapSaver
    private lateinit var mapboxMap: MapboxMap
    private lateinit var downloadButton: FloatingActionButton
    private var timeOfLastDownloadToast = 0L

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Mapbox.getInstance(this, getString(R.string.mapbox_access_token))
        super.initMapView(savedInstanceState, R.layout.activity_manage_offline_map, R.id.mapView)

        mapView.contentDescription = getString(R.string.map_not_ready)
        mapView.getMapAsync(this)

        downloadButton = findViewById(R.id.buttonToSaveOfflineMap)
        //We must wait for the map to be available to download
        downloadButton.isEnabled = false
    }

    override fun onMapReady(mapboxMap: MapboxMap) {
        this.mapboxMap = mapboxMap
        // Used to detect when the map is ready in tests
        mapView.contentDescription = getString(R.string.map_ready)

        mapboxMap.setStyle(Style.MAPBOX_STREETS) { style ->
            //configureLocationOptions
            LocationComponentManager.enableLocationComponent(this, mapboxMap, locationService)

            lineManager = LineManager(mapView, mapboxMap, style)
            offlineMapSaver = OfflineMapSaverImpl(this@ManageOfflineMapActivity, style.uri)
            bindOfflineRegionsToRecycler()
            downloadButton.isEnabled = true
            bindTileCount()
        }
    }

    /**
     * Download the offlineRegion delimited with the current view of the map and call it [regionName]
     */
    private fun downloadOfflineMap(regionName: String) {
        val bounds = mapboxMap.projection.visibleRegion.latLngBounds
        val zoom = mapboxMap.cameraPosition.zoom

        offlineMapSaver.downloadRegion(regionName, bounds, zoom, object : OfflineRegion.OfflineRegionObserver {
            override fun onStatusChanged(status: OfflineRegionStatus) {

                if (status.isComplete) {
                    ToastHandler.showToast(applicationContext, getString(R.string.download_succeeded, regionName))
                } else if (currentTimeMillis() - timeOfLastDownloadToast > DOWNLOAD_STATUS_TIME_DELAY) {
                    timeOfLastDownloadToast = currentTimeMillis()
                    val percentage =
                        if (status.requiredResourceCount >= 0) 100.0 * status.completedResourceCount / status.requiredResourceCount else 0.0
                    ToastHandler.showToast(
                        applicationContext,
                        getString(R.string.download_progress, "%.2f".format(percentage) + "%")
                    )
                }
            }

            override fun onError(error: OfflineRegionError) {
                Timber.e("DownloadError $error")
            }

            override fun mapboxTileCountLimitExceeded(limit: Long) {
                Timber.e("mapboxTileCountLimitExceeded")
                ToastHandler.showToast(applicationContext, R.string.tile_limit_exceeded)
            }
        })
    }

    /**
     * Get the recyclerView, create an adapter and bind it to the offlineRegions by displaying them.
     */
    private fun bindOfflineRegionsToRecycler() {
        val savedRegionsRecycler = findViewById<RecyclerView>(R.id.saved_regions)
        val offlineRegions = offlineMapSaver.getOfflineRegions()
        val adapter = OfflineRegionViewAdapter(offlineMapSaver, lineManager, mapboxMap)
        savedRegionsRecycler.adapter = adapter

        offlineRegions.observe(this, {
            it.let {
                adapter.submitList(it.sortedWith { r0, r1 ->
                    OfflineMapSaverImpl.getMetadata(r0).name.compareTo(
                        OfflineMapSaverImpl.getMetadata(
                            r1
                        ).name
                    )
                })
            }
        })

        offlineRegions.observe(this, {
            it.forEach { offlineRegion -> (display(offlineRegion)) }
        })
    }

    /**
     * Add observer to the tileCount so that the progress bar and the text are updated on change.
     */
    private fun bindTileCount() {
        val tilesUsedTextView = findViewById<TextView>(R.id.tiles_used)
        val tilesBar = findViewById<ProgressBar>(R.id.tile_count_bar)

        val maxTileCount = offlineMapSaver.getMaxTileCount()
        val actualTileCount = offlineMapSaver.getTotalTileCount()

        tilesBar.max = maxTileCount.toInt()

        actualTileCount.observe(this, {
            it.let {
                val builder = StringBuilder()
                builder.append(min(it, maxTileCount)).append("/").append(maxTileCount)
                tilesUsedTextView.text = builder.toString()

                tilesBar.setProgress(min(it, maxTileCount).toInt(), true)
            }
        })
    }

    /**
     * Show a dialog which let the user enter the name of the region he wants to download.
     */
    fun showDialog(@Suppress("UNUSED_PARAMETER") view: View) {

        val viewInflated: View = LayoutInflater.from(this)
            .inflate(R.layout.enter_offline_region_name_dialog, parent as ViewGroup?, false)
        val inputText = viewInflated.findViewById<View>(R.id.input_text) as EditText

        val builder: AlertDialog.Builder = AlertDialog.Builder(this)

        val dialog: AlertDialog = builder.setView(viewInflated)
            .setTitle("New offline region")
            .setPositiveButton(R.string.download, null) //Set to null, will be overridden to add check that not empty
            .setNegativeButton(R.string.cancel
            ) { dialog, _ -> dialog.cancel() }
            .create()

        dialog.setOnShowListener {
            val button: Button = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
            button.contentDescription = "Positive button" //Used for testing

            button.setOnClickListener {

                val regionName = inputText.text.toString()

                if (regionName == "") {

                    //Close the keyboard if it's open so that we can see the toast
                    val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
                    if (imm.isAcceptingText) {
                        imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0)
                    }

                    ToastHandler.showToast(applicationContext, R.string.empty)
                } else {
                    downloadOfflineMap(regionName)
                    dialog.dismiss()
                }
            }
        }

        dialog.show()

        //Set backgroundColor for the dialog
        dialog.window?.decorView?.background?.colorFilter =
            LightingColorFilter(BACKGROUND_COLOR_MULTIPLIER, ContextCompat.getColor(this, R.color.white))
    }

    /**
     * Display the [offlineRegion] on the map by putting a square surrounding the region on the map
     */
    private fun display(offlineRegion: OfflineRegion) {
        val bounds = OfflineMapSaverImpl.getMetadata(offlineRegion).bounds
        lineManager.create(
            LineOptions().withLatLngs(
                listOf(bounds.northEast, bounds.northWest, bounds.southWest, bounds.southEast, bounds.northEast)
            )
        )
    }

}