From 5c36bc424d610d0226694782d29d80f428fd7ca3 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart 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 +- gradle/libs.versions.toml | 2 +- 6 files changed, 149 insertions(+), 115 deletions(-) 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 = fakeLocationStateUseCase.currentLocation + private val _singleEvents = MutableSharedFlow() 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() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5b7a35..e85838d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ junit = { group = "junit", name = "junit", version = "4.13.1" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version = "2.9.1" } -maplibre = { group = "org.maplibre.gl", name = "android-sdk", version = "10.0.2" } +maplibre = { group = "org.maplibre.gl", name = "android-sdk", version = "10.2.0" } mockk = { group = "io.mockk", name = "mockk", version = "1.10.5" } mpandroidcharts = { group = "com.github.PhilJay", name = "MPAndroidChart", version = "v3.1.0" } retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } -- cgit v1.2.1