From 5c36bc424d610d0226694782d29d80f428fd7ca3 Mon Sep 17 00:00:00 2001
From: Guillaume Jacquart <guillaume.jacquart@hoodbrains.com>
Date: Mon, 31 Jul 2023 14:48:56 +0000
Subject: 5842: fix back to REAL location

---
 .../e/advancedprivacy/DependencyContainer.kt       |   1 -
 .../domain/usecases/FakeLocationStateUseCase.kt    |  30 ++-
 .../features/location/FakeLocationFragment.kt      | 208 +++++++++++++--------
 .../features/location/FakeLocationMapView.kt       |   5 +-
 .../features/location/FakeLocationViewModel.kt     |  18 +-
 5 files changed, 148 insertions(+), 114 deletions(-)

(limited to 'app/src/main/java')

diff --git a/app/src/main/java/foundation/e/advancedprivacy/DependencyContainer.kt b/app/src/main/java/foundation/e/advancedprivacy/DependencyContainer.kt
index 820a868..f6f2038 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/DependencyContainer.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/DependencyContainer.kt
@@ -190,7 +190,6 @@ class ViewModelsFactory(
                 )
             FakeLocationViewModel::class.java ->
                 FakeLocationViewModel(
-                    getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase,
                     fakeLocationStateUseCase = fakeLocationStateUseCase
                 )
             InternetPrivacyViewModel::class.java ->
diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt
index 5a16308..8831fff 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/FakeLocationStateUseCase.kt
@@ -25,7 +25,6 @@ import android.location.Location
 import android.location.LocationListener
 import android.location.LocationManager
 import android.os.Bundle
-import android.util.Log
 import foundation.e.advancedprivacy.data.repositories.LocalStateRepository
 import foundation.e.advancedprivacy.domain.entities.LocationMode
 import foundation.e.advancedprivacy.dummy.CityDataSource
@@ -38,6 +37,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
+import timber.log.Timber
 import kotlin.random.Random
 
 class FakeLocationStateUseCase(
@@ -134,15 +134,7 @@ class FakeLocationStateUseCase(
     private var localListener = object : LocationListener {
 
         override fun onLocationChanged(location: Location) {
-            currentLocation.update { previous ->
-                if ((previous?.time ?: 0) + 1800 < location.time ||
-                    (previous?.accuracy ?: Float.MAX_VALUE) > location.accuracy
-                ) {
-                    location
-                } else {
-                    previous
-                }
-            }
+            currentLocation.update { location }
         }
 
         @Deprecated("Deprecated since API 29, never called.")
@@ -198,13 +190,19 @@ class FakeLocationStateUseCase(
                 )
             }
 
-            networkProvider?.let { locationManager.getLastKnownLocation(it) }
-                ?: gpsProvider?.let { locationManager.getLastKnownLocation(it) }
-                    ?.let {
-                        localListener.onLocationChanged(it)
-                    }
+            var lastKnownLocation = networkProvider?.let {
+                locationManager.getLastKnownLocation(it)
+            }
+
+            if (lastKnownLocation == null) {
+                lastKnownLocation = gpsProvider?.let {
+                    locationManager.getLastKnownLocation(it)
+                }
+            }
+
+            lastKnownLocation?.let { localListener.onLocationChanged(it) }
         } catch (se: SecurityException) {
-            Log.e(TAG, "Missing permission", se)
+            Timber.e("Missing permission", se)
         }
     }
 }
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationFragment.kt
index 09409f2..7d18930 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationFragment.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationFragment.kt
@@ -25,6 +25,7 @@ import android.location.Location
 import android.os.Bundle
 import android.text.Editable
 import android.view.View
+import android.view.inputmethod.InputMethodManager
 import android.widget.Toast
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.annotation.NonNull
@@ -34,10 +35,10 @@ import androidx.fragment.app.viewModels
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
-import com.google.android.material.textfield.TextInputEditText
 import com.google.android.material.textfield.TextInputLayout
 import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM
 import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE
+import com.mapbox.android.gestures.MoveGestureDetector
 import com.mapbox.mapboxsdk.Mapbox
 import com.mapbox.mapboxsdk.WellKnownTileServer
 import com.mapbox.mapboxsdk.camera.CameraPosition
@@ -58,8 +59,8 @@ import foundation.e.advancedprivacy.domain.entities.LocationMode
 import foundation.e.advancedprivacy.features.location.FakeLocationViewModel.Action
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.launch
+import timber.log.Timber
 
 class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) {
 
@@ -81,6 +82,8 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location)
 
     private var inputJob: Job? = null
 
+    private var updateLocationJob: Job? = null
+
     private val locationPermissionRequest = registerForActivityResult(
         ActivityResultContracts.RequestMultiplePermissions()
     ) { permissions ->
@@ -92,7 +95,6 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location)
     }
 
     companion object {
-        private const val DEBOUNCE_PERIOD = 1000L
         private const val MAP_STYLE = "mapbox://styles/mapbox/outdoors-v12"
     }
 
@@ -115,18 +117,8 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location)
             mapboxMap.uiSettings.isRotateGesturesEnabled = false
             mapboxMap.setStyle(MAP_STYLE) { style ->
                 enableLocationPlugin(style)
-                mapboxMap.addOnCameraMoveListener {
-                    if (binding.mapView.isEnabled) {
-                        mapboxMap.cameraPosition.target?.let {
-                            viewModel.submitAction(
-                                Action.SetSpecificLocationAction(
-                                    it.latitude.toFloat(),
-                                    it.longitude.toFloat()
-                                )
-                            )
-                        }
-                    }
-                }
+
+                mapboxMap.addOnMoveListener(onMoveListener)
 
                 mapboxMap.cameraPosition = CameraPosition.Builder().zoom(8.0).build()
 
@@ -134,18 +126,44 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location)
                 bindClickListeners()
 
                 render(viewModel.state.value)
-                viewLifecycleOwner.lifecycleScope.launch {
-                    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                        viewModel.singleEvents.collect { event ->
-                            if (event is FakeLocationViewModel.SingleEvent.LocationUpdatedEvent) {
-                                updateLocation(event.location, event.mode)
-                            }
-                        }
+                startUpdateLocationJob()
+            }
+        }
+
+        startListening()
+    }
+
+    private val onMoveListener = object : MapboxMap.OnMoveListener {
+        private val cameraIdleListener: MapboxMap.OnCameraIdleListener =
+            object : MapboxMap.OnCameraIdleListener {
+                override fun onCameraIdle() {
+                    mapboxMap?.cameraPosition?.target?.let {
+                        viewModel.submitAction(
+                            Action.SetSpecificLocationAction(
+                                it.latitude.toFloat(),
+                                it.longitude.toFloat()
+                            )
+                        )
+                        startUpdateLocationJob()
                     }
+                    mapboxMap?.removeOnCameraIdleListener(this)
                 }
             }
+
+        override fun onMoveBegin(detector: MoveGestureDetector) {
+            updateLocationJob?.cancel()
+            updateLocationJob = null
+            mapboxMap?.removeOnCameraIdleListener(cameraIdleListener)
         }
 
+        override fun onMove(detector: MoveGestureDetector) {}
+
+        override fun onMoveEnd(detector: MoveGestureDetector) {
+            mapboxMap?.addOnCameraIdleListener(cameraIdleListener)
+        }
+    }
+
+    private fun startListening() {
         viewLifecycleOwner.lifecycleScope.launch {
             viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                 render(viewModel.state.value)
@@ -169,9 +187,6 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location)
                                 )
                             )
                         }
-                        is FakeLocationViewModel.SingleEvent.LocationUpdatedEvent -> {
-                            // Nothing here, another collect linked to mapbox view.
-                        }
                     }
                 }
             }
@@ -184,49 +199,90 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location)
         }
     }
 
-    private fun getCoordinatesAfterTextChanged(
-        inputLayout: TextInputLayout,
-        editText: TextInputEditText,
-        isLat: Boolean
-    ) = { editable: Editable? ->
-        inputJob?.cancel()
-        if (editable != null && editable.isNotEmpty() && editText.isEnabled) {
-            inputJob = lifecycleScope.launch {
-                delay(DEBOUNCE_PERIOD)
-                ensureActive()
-                try {
-                    val value = editable.toString().toFloat()
-                    val maxValue = if (isLat) 90f else 180f
-
-                    if (value > maxValue || value < -maxValue) {
-                        throw NumberFormatException("value $value is out of bounds")
-                    }
-                    inputLayout.error = null
-
-                    inputLayout.setEndIconDrawable(R.drawable.ic_valid)
-                    inputLayout.endIconMode = END_ICON_CUSTOM
-
-                    // Here, value is valid, try to send the values
-                    try {
-                        val lat = binding.edittextLatitude.text.toString().toFloat()
-                        val lon = binding.edittextLongitude.text.toString().toFloat()
-                        if (lat <= 90f && lat >= -90f && lon <= 180f && lon >= -180f) {
-                            mapboxMap?.moveCamera(
-                                CameraUpdateFactory.newLatLng(
-                                    LatLng(lat.toDouble(), lon.toDouble())
-                                )
-                            )
-                        }
-                    } catch (e: NumberFormatException) {
-                    }
-                } catch (e: NumberFormatException) {
-                    inputLayout.endIconMode = END_ICON_NONE
-                    inputLayout.error = getString(R.string.location_input_error)
+    private fun startUpdateLocationJob() {
+        updateLocationJob?.cancel()
+        updateLocationJob = viewLifecycleOwner.lifecycleScope.launch {
+            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // Without this delay, onResume, map apply the updateLocation and then
+                // move to an old fake location.
+                delay(1000)
+                viewModel.currentLocation.collect { location ->
+                    updateLocation(location, viewModel.state.value.mode)
                 }
             }
         }
     }
 
+    private fun validateCoordinate(
+        inputLayout: TextInputLayout,
+        maxValue: Float
+    ): Boolean {
+        return try {
+            val value = inputLayout.editText?.text?.toString()?.toFloat()!!
+
+            if (value > maxValue || value < -maxValue) {
+                throw NumberFormatException("value $value is out of bounds")
+            }
+            inputLayout.error = null
+
+            inputLayout.setEndIconDrawable(R.drawable.ic_valid)
+            inputLayout.endIconMode = END_ICON_CUSTOM
+            true
+        } catch (e: Exception) {
+            inputLayout.endIconMode = END_ICON_NONE
+            inputLayout.error = getString(R.string.location_input_error)
+            false
+        }
+    }
+
+    private fun updateSpecificCoordinates() {
+        try {
+            val lat = binding.edittextLatitude.text.toString().toFloat()
+            val lon = binding.edittextLongitude.text.toString().toFloat()
+            if (lat <= 90f && lat >= -90f && lon <= 180f && lon >= -180f) {
+                viewModel.submitAction(
+                    Action.SetSpecificLocationAction(
+                        lat,
+                        lon
+                    )
+                )
+            }
+        } catch (e: NumberFormatException) {
+            Timber.e("Unfiltered wrong lat lon format")
+        }
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    private fun onLatTextChanged(editable: Editable?) {
+        if (!binding.edittextLatitude.isFocused ||
+            !validateCoordinate(binding.textlayoutLatitude, 90f)
+        ) return
+
+        updateSpecificCoordinates()
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    private fun onLonTextChanged(editable: Editable?) {
+        if (!binding.edittextLongitude.isFocused ||
+            !validateCoordinate(binding.textlayoutLongitude, 180f)
+        ) return
+
+        updateSpecificCoordinates()
+    }
+
+    private val isEditingLatLon get() = binding.edittextLongitude.isFocused || binding.edittextLatitude.isFocused
+
+    private val latLonOnFocusChangeListener = object : View.OnFocusChangeListener {
+        override fun onFocusChange(v: View?, hasFocus: Boolean) {
+            if (!isEditingLatLon) {
+                (context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager)?.hideSoftInputFromWindow(
+                    v?.windowToken,
+                    0
+                )
+            }
+        }
+    }
+
     @SuppressLint("ClickableViewAccessibility")
     private fun bindClickListeners() {
         binding.radioUseRealLocation.setOnClickListener {
@@ -242,21 +298,11 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location)
                 )
             }
         }
-        binding.edittextLatitude.addTextChangedListener(
-            afterTextChanged = getCoordinatesAfterTextChanged(
-                binding.textlayoutLatitude,
-                binding.edittextLatitude,
-                true
-            )
-        )
 
-        binding.edittextLongitude.addTextChangedListener(
-            afterTextChanged = getCoordinatesAfterTextChanged(
-                binding.textlayoutLongitude,
-                binding.edittextLongitude,
-                false
-            )
-        )
+        binding.edittextLatitude.addTextChangedListener(afterTextChanged = ::onLatTextChanged)
+        binding.edittextLongitude.addTextChangedListener(afterTextChanged = ::onLonTextChanged)
+        binding.edittextLatitude.onFocusChangeListener = latLonOnFocusChangeListener
+        binding.edittextLongitude.onFocusChangeListener = latLonOnFocusChangeListener
     }
 
     @SuppressLint("MissingPermission")
@@ -275,7 +321,6 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location)
             binding.mapLoader.isVisible = false
             binding.mapOverlay.isVisible = state.mode != LocationMode.SPECIFIC_LOCATION
             binding.centeredMarker.isVisible = true
-
             mapboxMap?.moveCamera(
                 CameraUpdateFactory.newLatLng(
                     LatLng(
@@ -289,8 +334,10 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location)
         binding.textlayoutLatitude.isVisible = (state.mode == LocationMode.SPECIFIC_LOCATION)
         binding.textlayoutLongitude.isVisible = (state.mode == LocationMode.SPECIFIC_LOCATION)
 
-        binding.edittextLatitude.setText(state.specificLatitude?.toString())
-        binding.edittextLongitude.setText(state.specificLongitude?.toString())
+        if (!isEditingLatLon) {
+            binding.edittextLatitude.setText(state.specificLatitude?.toString())
+            binding.edittextLongitude.setText(state.specificLongitude?.toString())
+        }
     }
 
     @SuppressLint("MissingPermission")
@@ -306,7 +353,6 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location)
                 val update = CameraUpdateFactory.newLatLng(
                     LatLng(location.latitude, location.longitude)
                 )
-
                 if (isFirstLaunch) {
                     mapboxMap?.moveCamera(update)
                     isFirstLaunch = false
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationMapView.kt b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationMapView.kt
index fbb5b6c..f80c25e 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationMapView.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationMapView.kt
@@ -38,7 +38,10 @@ class FakeLocationMapView @JvmOverloads constructor(
     @SuppressLint("ClickableViewAccessibility")
     override fun onTouchEvent(event: MotionEvent?): Boolean {
         when (event?.action) {
-            MotionEvent.ACTION_DOWN -> parent.requestDisallowInterceptTouchEvent(true)
+            MotionEvent.ACTION_DOWN -> {
+                parent.requestDisallowInterceptTouchEvent(true)
+                requestFocus()
+            }
             MotionEvent.ACTION_UP -> parent.requestDisallowInterceptTouchEvent(false)
         }
         super.onTouchEvent(event)
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationViewModel.kt b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationViewModel.kt
index 87b64c5..deca4c1 100644
--- a/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationViewModel.kt
+++ b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationViewModel.kt
@@ -20,13 +20,12 @@ package foundation.e.advancedprivacy.features.location
 import android.location.Location
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
-import foundation.e.advancedprivacy.domain.entities.LocationMode
 import foundation.e.advancedprivacy.domain.usecases.FakeLocationStateUseCase
-import foundation.e.advancedprivacy.domain.usecases.GetQuickPrivacyStateUseCase
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.debounce
@@ -38,7 +37,6 @@ import kotlinx.coroutines.withContext
 import kotlin.time.Duration.Companion.milliseconds
 
 class FakeLocationViewModel(
-    private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase,
     private val fakeLocationStateUseCase: FakeLocationStateUseCase
 ) : ViewModel() {
     companion object {
@@ -48,6 +46,8 @@ class FakeLocationViewModel(
     private val _state = MutableStateFlow(FakeLocationState())
     val state = _state.asStateFlow()
 
+    val currentLocation: StateFlow<Location?> = fakeLocationStateUseCase.currentLocation
+
     private val _singleEvents = MutableSharedFlow<SingleEvent>()
     val singleEvents = _singleEvents.asSharedFlow()
 
@@ -72,17 +72,6 @@ class FakeLocationViewModel(
                     }
             ).collect {}
         }
-
-        launch {
-            fakeLocationStateUseCase.currentLocation.collect { location ->
-                _singleEvents.emit(
-                    SingleEvent.LocationUpdatedEvent(
-                        mode = _state.value.mode,
-                        location = location
-                    )
-                )
-            }
-        }
     }
 
     fun submitAction(action: Action) = viewModelScope.launch {
@@ -108,7 +97,6 @@ class FakeLocationViewModel(
     }
 
     sealed class SingleEvent {
-        data class LocationUpdatedEvent(val mode: LocationMode, val location: Location?) : SingleEvent()
         object RequestLocationPermission : SingleEvent()
         data class ErrorEvent(val error: String) : SingleEvent()
     }
-- 
cgit v1.2.1