diff options
author | Leonard Kugis <leonard@kug.is> | 2024-01-02 17:53:12 +0100 |
---|---|---|
committer | Leonard Kugis <leonard@kug.is> | 2024-01-02 17:53:12 +0100 |
commit | 5db0bdfdf62ae0915b587399a0ff4ce53bca813b (patch) | |
tree | 538a18ce0adbf6e600ee77a48e51d8c67649b0c6 /app/src/main/java/foundation/e/advancedprivacy/features | |
parent | 298dff2a877680e928b37e3a1336dc7d7aa52dfb (diff) | |
download | advanced-privacy-5db0bdfdf62ae0915b587399a0ff4ce53bca813b.tar.gz |
Implemented route mode
Diffstat (limited to 'app/src/main/java/foundation/e/advancedprivacy/features')
4 files changed, 125 insertions, 5 deletions
diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt index b7ff5e0..559e13f 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/dashboard/DashboardFragment.kt @@ -247,6 +247,7 @@ class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { LocationMode.REAL_LOCATION -> R.string.dashboard_location_subtitle_off LocationMode.SPECIFIC_LOCATION -> R.string.dashboard_location_subtitle_specific LocationMode.RANDOM_LOCATION -> R.string.dashboard_location_subtitle_random + LocationMode.ROUTE -> R.string.dashboard_location_subtitle_route } ) 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 7b456d1..b70ae36 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 @@ -35,6 +35,10 @@ import androidx.core.widget.addTextChangedListener import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import android.app.Activity +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import androidx.documentfile.provider.DocumentFile 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 @@ -60,6 +64,10 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import foundation.e.advancedprivacy.domain.entities.FakeLocationCoordinate +import java.io.File class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) { @@ -206,6 +214,25 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) } } + private fun validateBounds(inputLayout: TextInputLayout, minValue: Float, maxValue: Float): Boolean { + return try { + val value = inputLayout.editText?.text?.toString()?.toFloat()!! + + if (value > maxValue || value < minValue) { + 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_error_bounds) + false + } + } + private fun validateCoordinate( inputLayout: TextInputLayout, maxValue: Float @@ -261,16 +288,19 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) @Suppress("UNUSED_PARAMETER") private fun onAltitudeTextChanged(editable: Editable?) { + if(!validateBounds(binding.textlayoutAltitude, -100000.0f, 100000.0f)) return updateMockLocationParameters() } @Suppress("UNUSED_PARAMETER") private fun onSpeedTextChanged(editable: Editable?) { + if(!validateBounds(binding.textlayoutSpeed, 0.0f, 299792458.0f)) return updateMockLocationParameters() } @Suppress("UNUSED_PARAMETER") private fun onJitterTextChanged(editable: Editable?) { + if(!validateBounds(binding.textlayoutJitter, 0.0f, 10000000.0f)) return updateMockLocationParameters() } @@ -305,6 +335,35 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) } } + private var route: List<FakeLocationCoordinate>? = null + + private val filePickerLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { uri -> + if(uri.path != null) { + var routeFile = File(uri.path ?: ".") + //val filePath = selectedFile?.uri?.path ?: "Path not found" + //binding.locationRoutePath.text = "Path: $filePath" + route = Gson().fromJson(routeFile.readText(Charsets.UTF_8), object : TypeToken<List<FakeLocationCoordinate>>() {}.type) + var route_buf = route + route_buf?.let { + viewModel.submitAction(Action.SetRoute(route_buf)) + } + } + } + } + } + + private fun openFilePicker() { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + } + + filePickerLauncher.launch(intent) + } + @SuppressLint("ClickableViewAccessibility") private fun bindClickListeners() { binding.radioUseRealLocation.setOnClickListener { @@ -320,6 +379,9 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) ) } } + binding.radioUseRoute.setOnClickListener { + viewModel.submitAction(Action.UseRoute) + } binding.edittextAltitude.addTextChangedListener(afterTextChanged = ::onAltitudeTextChanged) binding.edittextSpeed.addTextChangedListener(afterTextChanged = ::onSpeedTextChanged) @@ -331,6 +393,11 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) binding.edittextJitter.onFocusChangeListener = latLonOnFocusChangeListener binding.edittextLatitude.onFocusChangeListener = latLonOnFocusChangeListener binding.edittextLongitude.onFocusChangeListener = latLonOnFocusChangeListener + + binding.buttonLocationRoutePathSelect.setOnClickListener { openFilePicker() } + binding.checkboxRouteLoop.setOnCheckedChangeListener { _, isChecked -> viewModel.submitAction(Action.SetRouteLoopEnabledAction(isChecked)) } + binding.buttonLocationRouteStart.setOnClickListener { viewModel.submitAction(Action.RouteStartAction) } + binding.buttonLocationRouteStop.setOnClickListener { viewModel.submitAction(Action.RouteStopAction) } } @SuppressLint("MissingPermission") @@ -341,12 +408,23 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) binding.radioUseRealLocation.isChecked = state.mode == LocationMode.REAL_LOCATION + binding.radioUseRoute.isChecked = state.mode == LocationMode.ROUTE + binding.mapView.isEnabled = (state.mode == LocationMode.SPECIFIC_LOCATION) binding.textlayoutAltitude.isVisible = state.mode == LocationMode.SPECIFIC_LOCATION binding.textlayoutSpeed.isVisible = state.mode == LocationMode.SPECIFIC_LOCATION binding.textlayoutJitter.isVisible = state.mode == LocationMode.SPECIFIC_LOCATION + binding.buttonLocationRoutePathSelect.isVisible = state.mode == LocationMode.ROUTE + binding.locationRoutePath.isVisible = state.mode == LocationMode.ROUTE + binding.checkboxRouteLoop.isVisible = state.mode == LocationMode.ROUTE + binding.buttonLocationRouteStart.isVisible = state.mode == LocationMode.ROUTE + binding.buttonLocationRouteStop.isVisible = state.mode == LocationMode.ROUTE + + if(binding.checkboxRouteLoop.isVisible) + binding.checkboxRouteLoop.isChecked = state.loopRoute + if(!binding.edittextAltitude.isFocused) binding.edittextAltitude.setText(state.altitude?.toString()) @@ -379,6 +457,12 @@ class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) binding.edittextLatitude.setText(state.specificLatitude?.toString()) binding.edittextLongitude.setText(state.specificLongitude?.toString()) } + + if(route == null) { + binding.locationRoutePath.text = "No valid route selected" + } else { + binding.locationRoutePath.text = "Route valid" + } } @SuppressLint("MissingPermission") diff --git a/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationState.kt b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationState.kt index cc16b1b..56acdfd 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationState.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/features/location/FakeLocationState.kt @@ -20,6 +20,7 @@ package foundation.e.advancedprivacy.features.location import android.location.Location import foundation.e.advancedprivacy.domain.entities.LocationMode +import foundation.e.advancedprivacy.domain.entities.FakeLocationCoordinate data class FakeLocationState( val mode: LocationMode = LocationMode.REAL_LOCATION, @@ -30,4 +31,7 @@ data class FakeLocationState( val specificLatitude: Float? = null, val specificLongitude: Float? = null, val forceRefresh: Boolean = false, + val route: List<FakeLocationCoordinate>? = null, + val loopRoute: Boolean = false, + val routeStarted: Boolean = false, ) 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 143612f..c88c638 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 @@ -36,12 +36,16 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.time.Duration.Companion.milliseconds +import foundation.e.advancedprivacy.domain.entities.FakeLocationCoordinate class FakeLocationViewModel( private val fakeLocationStateUseCase: FakeLocationStateUseCase ) : ViewModel() { companion object { private val SET_SPECIFIC_LOCATION_DELAY = 200.milliseconds + private val SET_MOCK_LOCATION_PARAMETERS_DELAY = 1000.milliseconds + private val SET_ROUTE_LOOP_ENABLED_DELAY = 1000.milliseconds + private val SET_ROUTE_DELAY = 1000.milliseconds } private val _state = MutableStateFlow(FakeLocationState()) @@ -54,6 +58,8 @@ class FakeLocationViewModel( private val specificLocationInputFlow = MutableSharedFlow<Action.SetSpecificLocationAction>() private val mockLocationParametersInputFlow = MutableSharedFlow<Action.UpdateMockLocationParameters>() + private val setRouteLoopEnabledInputFlow = MutableSharedFlow<Action.SetRouteLoopEnabledAction>() + private val setRouteInputFlow = MutableSharedFlow<Action.SetRoute>() @OptIn(FlowPreview::class) suspend fun doOnStartedState() = withContext(Dispatchers.Main) { @@ -73,12 +79,20 @@ class FakeLocationViewModel( }, specificLocationInputFlow .debounce(SET_SPECIFIC_LOCATION_DELAY).map { action -> - fakeLocationStateUseCase.setSpecificLocation(action.latitude, action.longitude) + fakeLocationStateUseCase.useFakeLocation(Pair<Float,Float>(action.latitude, action.longitude)) }, mockLocationParametersInputFlow - .debounce(SET_SPECIFIC_LOCATION_DELAY).map { action -> + .debounce(SET_MOCK_LOCATION_PARAMETERS_DELAY).map { action -> fakeLocationStateUseCase.setFakeLocationParameters(action.altitude, action.speed, action.jitter) }, + setRouteLoopEnabledInputFlow + .debounce(SET_ROUTE_LOOP_ENABLED_DELAY).map { action -> + fakeLocationStateUseCase.setRouteLoopEnabled(action.isEnabled) + }, + setRouteInputFlow + .debounce(SET_ROUTE_DELAY).map { action -> + fakeLocationStateUseCase.setRoute(action.route) + }, ).collect {} } } @@ -88,10 +102,14 @@ class FakeLocationViewModel( is Action.StartListeningLocation -> actionStartListeningLocation() is Action.StopListeningLocation -> fakeLocationStateUseCase.stopListeningLocation() is Action.SetSpecificLocationAction -> setSpecificLocation(action) - is Action.UseRandomLocationAction -> fakeLocationStateUseCase.setRandomLocation() - is Action.UseRealLocationAction -> - fakeLocationStateUseCase.stopFakeLocation() + is Action.UseRandomLocationAction -> fakeLocationStateUseCase.useRandomLocation() + is Action.UseRealLocationAction -> fakeLocationStateUseCase.useRealLocation() + is Action.UseRoute -> fakeLocationStateUseCase.useRoute() is Action.UpdateMockLocationParameters -> updateMockLocationParameters(action) + is Action.SetRoute -> setRouteInputFlow.emit(action) + is Action.SetRouteLoopEnabledAction -> setRouteLoopEnabled(action) + is Action.RouteStartAction -> fakeLocationStateUseCase.routeStart() + is Action.RouteStopAction -> fakeLocationStateUseCase.routeStop() } } @@ -110,6 +128,10 @@ class FakeLocationViewModel( mockLocationParametersInputFlow.emit(action) } + private suspend fun setRouteLoopEnabled(action: Action.SetRouteLoopEnabledAction) { + setRouteLoopEnabledInputFlow.emit(action) + } + sealed class SingleEvent { object RequestLocationPermission : SingleEvent() data class ErrorEvent(val error: String) : SingleEvent() @@ -120,6 +142,7 @@ class FakeLocationViewModel( object StopListeningLocation : Action() object UseRealLocationAction : Action() object UseRandomLocationAction : Action() + object UseRoute : Action() data class UpdateMockLocationParameters( val altitude: Float, val speed: Float, @@ -129,5 +152,13 @@ class FakeLocationViewModel( val latitude: Float, val longitude: Float ) : Action() + data class SetRoute( + val route: List<FakeLocationCoordinate> + ) : Action() + data class SetRouteLoopEnabledAction( + val isEnabled: Boolean + ) : Action() + object RouteStartAction : Action() + object RouteStopAction : Action() } } |