From 5db0bdfdf62ae0915b587399a0ff4ce53bca813b Mon Sep 17 00:00:00 2001 From: Leonard Kugis Date: Tue, 2 Jan 2024 17:53:12 +0100 Subject: Implemented route mode --- app/build.gradle | 3 +- .../data/repositories/LocalStateRepository.kt | 46 +++-- .../domain/usecases/FakeLocationStateUseCase.kt | 211 +++++++++++++++------ .../domain/usecases/GetQuickPrivacyStateUseCase.kt | 9 +- .../domain/usecases/ShowFeaturesWarningUseCase.kt | 3 - .../features/dashboard/DashboardFragment.kt | 1 + .../features/location/FakeLocationFragment.kt | 84 ++++++++ .../features/location/FakeLocationState.kt | 4 + .../features/location/FakeLocationViewModel.kt | 41 +++- app/src/main/res/layout/fragment_fake_location.xml | 40 ++++ app/src/main/res/values/strings.xml | 11 +- .../domain/entities/FakeLocationCoordinate.kt | 28 +++ .../domain/entities/LocationMode.kt | 2 +- .../domain/repositories/LocalStateRepository.kt | 10 +- fakelocation/build.gradle | 3 +- .../domain/usecases/FakeLocationModule.kt | 9 + .../fakelocation/services/FakeLocationService.kt | 108 +++++++++-- 17 files changed, 518 insertions(+), 95 deletions(-) create mode 100644 core/src/main/java/foundation/e/advancedprivacy/domain/entities/FakeLocationCoordinate.kt diff --git a/app/build.gradle b/app/build.gradle index 816af83..7c7875b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -175,7 +175,8 @@ dependencies { libs.mpandroidcharts, libs.eos.telemetry, - libs.timber + libs.timber, + libs.google.gson ) debugImplementation libs.leakcanary diff --git a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt index 540d502..9643899 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/data/repositories/LocalStateRepository.kt @@ -30,6 +30,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import foundation.e.advancedprivacy.domain.entities.FakeLocationCoordinate class LocalStateRepositoryImpl(context: Context) : LocalStateRepository { companion object { @@ -39,7 +42,9 @@ class LocalStateRepositoryImpl(context: Context) : LocalStateRepository { private const val KEY_FAKE_ALTITUDE = "fakeAltitude" private const val KEY_FAKE_SPEED = "fakeSpeed" private const val KEY_FAKE_JITTER = "fakeJitter" - private const val KEY_FAKE_LOCATION = "fakeLocation" + private const val KEY_LOCATION_MODE = "locationMode" + private const val KEY_LOCATION_ROUTE = "locationRoute" + private const val KEY_LOCATION_ROUTE_LOOP = "locationRouteLoop" private const val KEY_FAKE_LATITUDE = "fakeLatitude" private const val KEY_FAKE_LONGITUDE = "fakeLongitude" private const val KEY_FIRST_BOOT = "firstBoot" @@ -61,15 +66,6 @@ class LocalStateRepositoryImpl(context: Context) : LocalStateRepository { override val areAllTrackersBlocked: MutableStateFlow = MutableStateFlow(false) - private val _fakeLocationEnabled = MutableStateFlow(sharedPref.getBoolean(KEY_FAKE_LOCATION, false)) - - override val fakeLocationEnabled = _fakeLocationEnabled.asStateFlow() - - override fun setFakeLocationEnabled(enabled: Boolean) { - set(KEY_FAKE_LOCATION, enabled) - _fakeLocationEnabled.update { enabled } - } - override var fakeAltitude: Float get() = sharedPref.getFloat(KEY_FAKE_ALTITUDE, 3.0f) set(value) { @@ -108,7 +104,35 @@ class LocalStateRepositoryImpl(context: Context) : LocalStateRepository { .apply() } - override val locationMode: MutableStateFlow = MutableStateFlow(LocationMode.REAL_LOCATION) + override var route: List + get() { + return Gson().fromJson>(sharedPref.getString(KEY_LOCATION_ROUTE, "[]"), object : TypeToken>() {}.type) + } + set(value) { + sharedPref.edit() + .putString(KEY_LOCATION_ROUTE, Gson().toJson(value)) + .apply() + } + + override var routeLoopEnabled: Boolean + get() = sharedPref.getBoolean(KEY_LOCATION_ROUTE_LOOP, false) + set(value) { + sharedPref.edit() + .putBoolean(KEY_LOCATION_ROUTE_LOOP, value) + .apply() + } + + private val _locationMode = MutableStateFlow(LocationMode.valueOf(sharedPref.getString(KEY_LOCATION_MODE, LocationMode.REAL_LOCATION.toString()) ?: "REAL_LOCATION")) + + override val locationMode = _locationMode.asStateFlow() + + override fun setLocationMode(mode: LocationMode) { + sharedPref.edit() + .putString(KEY_LOCATION_MODE, mode.toString()) + .apply() + //set(KEY_LOCATION_MODE, mode.toString()) + _locationMode.update { mode } + } private val _ipScramblingSetting = MutableStateFlow(sharedPref.getBoolean(KEY_IP_SCRAMBLING, false)) 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 114b5ca..27a2104 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 @@ -34,6 +34,7 @@ import foundation.e.advancedprivacy.dummy.CityDataSource import foundation.e.advancedprivacy.externalinterfaces.permissions.IPermissionsPrivacyModule import foundation.e.advancedprivacy.fakelocation.domain.usecases.FakeLocationModule import foundation.e.advancedprivacy.features.location.FakeLocationState +import foundation.e.advancedprivacy.domain.entities.FakeLocationCoordinate import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -52,15 +53,22 @@ class FakeLocationStateUseCase( coroutineScope: CoroutineScope ) { private val _configuredLocationMode = MutableStateFlow( - FakeLocationState(LocationMode.REAL_LOCATION, null, null, null, null, false, null, null, false) + FakeLocationState(LocationMode.REAL_LOCATION, null, null, null, null, null, null, false, null, false, false) ) val configuredLocationMode: StateFlow = _configuredLocationMode init { coroutineScope.launch { - localStateRepository.fakeLocationEnabled.collect { - applySettings(it, it) + localStateRepository.locationMode.collect { + if(it == LocationMode.REAL_LOCATION) + useRealLocation() + if(it == LocationMode.RANDOM_LOCATION) + useRandomLocation() + if(it == LocationMode.SPECIFIC_LOCATION) + useFakeLocation(localStateRepository.fakeLocation) + if(it == LocationMode.ROUTE) + useRoute(localStateRepository.route) } } } @@ -73,81 +81,178 @@ class FakeLocationStateUseCase( permissionsModule.toggleDangerousPermission(appDesc, android.Manifest.permission.ACCESS_FINE_LOCATION, true) } - private fun applySettings(isEnabled: Boolean, isSpecificLocation: Boolean = false) { - _configuredLocationMode.value = computeLocationMode(isEnabled, localStateRepository.fakeAltitude, localStateRepository.fakeSpeed, localStateRepository.fakeJitter, localStateRepository.fakeLocation, isSpecificLocation) + // private fun applySettings(isEnabled: Boolean, isSpecificLocation: Boolean = false) { + // _configuredLocationMode.value = computeLocationMode(isEnabled, + // localStateRepository.locationMode, + // localStateRepository.fakeAltitude, + // localStateRepository.fakeSpeed, + // localStateRepository.fakeJitter, + // localStateRepository.fakeLocation, + // localStateRepository.routeLoopEnabled, + // isSpecificLocation) - if (isEnabled && hasAcquireMockLocationPermission()) { - fakeLocationModule.startFakeLocation() - fakeLocationModule.setFakeLocation(localStateRepository.fakeAltitude.toDouble(), - localStateRepository.fakeSpeed, - localStateRepository.fakeJitter, - localStateRepository.fakeLocation.first.toDouble(), - localStateRepository.fakeLocation.second.toDouble()) - localStateRepository.locationMode.value = configuredLocationMode.value.mode - } else { - fakeLocationModule.stopFakeLocation() - localStateRepository.locationMode.value = LocationMode.REAL_LOCATION - } - } + // if (isEnabled && hasAcquireMockLocationPermission()) { + // fakeLocationModule.startFakeLocation() + // localStateRepository.setLocationMode(configuredLocationMode.value.mode) + // fakeLocationModule.setFakeLocation(localStateRepository.fakeAltitude.toDouble(), + // localStateRepository.fakeSpeed, + // localStateRepository.fakeJitter, + // localStateRepository.fakeLocation.first.toDouble(), + // localStateRepository.fakeLocation.second.toDouble()) + // } else { + // fakeLocationModule.stopFakeLocation() + // localStateRepository.setLocationMode(LocationMode.REAL_LOCATION) + // } + // } private fun hasAcquireMockLocationPermission(): Boolean { return (permissionsModule.getAppOpMode(appDesc, AppOpsManager.OPSTR_MOCK_LOCATION) == AppOpModes.ALLOWED) || permissionsModule.setAppOpMode(appDesc, AppOpsManager.OPSTR_MOCK_LOCATION, AppOpModes.ALLOWED) } + fun setRouteLoopEnabled(isEnabled: Boolean) { + _configuredLocationMode.value = FakeLocationState( + LocationMode.ROUTE, + null, + null, + null, + null, + null, + null, + false, + localStateRepository.route, + isEnabled, + false + ) + + localStateRepository.routeLoopEnabled = isEnabled + } + fun setFakeLocationParameters(altitude: Float, speed: Float, jitter: Float) { + _configuredLocationMode.value = FakeLocationState( + LocationMode.SPECIFIC_LOCATION, + null, + altitude, + speed, + jitter, + localStateRepository.fakeLocation.first, + localStateRepository.fakeLocation.second, + false, + null, + false, + false + ) + localStateRepository.fakeAltitude = altitude localStateRepository.fakeSpeed = speed localStateRepository.fakeJitter = jitter - applySettings(localStateRepository.fakeLocationEnabled.value, localStateRepository.fakeLocationEnabled.value) } - fun setSpecificLocation(latitude: Float, longitude: Float) { - setFakeLocation(latitude to longitude, true) + fun useRealLocation() { + _configuredLocationMode.value = FakeLocationState( + LocationMode.REAL_LOCATION, + null, + null, + null, + null, + null, + null, + false, + null, + false, + false + ) + + fakeLocationModule.stopFakeLocation() + localStateRepository.setLocationMode(LocationMode.REAL_LOCATION) } - fun setRandomLocation() { + fun useRandomLocation() { val randomIndex = Random.nextInt(citiesRepository.citiesLocationsList.size) val location = citiesRepository.citiesLocationsList[randomIndex] - setFakeLocation(location) + useFakeLocation(location) } - private fun setFakeLocation(location: Pair, isSpecificLocation: Boolean = false) { + fun useFakeLocation(location: Pair) { localStateRepository.fakeLocation = location - localStateRepository.setFakeLocationEnabled(true) - applySettings(true, isSpecificLocation) - } - - fun stopFakeLocation() { - localStateRepository.setFakeLocationEnabled(false) - applySettings(false, false) - } - - private fun computeLocationMode( - isFakeLocationEnabled: Boolean, - altitude: Float, - speed: Float, - jitter: Float, - fakeLocation: Pair, - isSpecificLocation: Boolean = false, - ): FakeLocationState { - return FakeLocationState( - when { - !isFakeLocationEnabled -> LocationMode.REAL_LOCATION - (fakeLocation in citiesRepository.citiesLocationsList && !isSpecificLocation) -> - LocationMode.RANDOM_LOCATION - else -> LocationMode.SPECIFIC_LOCATION - }, + + _configuredLocationMode.value = FakeLocationState( + LocationMode.SPECIFIC_LOCATION, null, - altitude, - speed, - jitter, + localStateRepository.fakeAltitude, + localStateRepository.fakeSpeed, + localStateRepository.fakeJitter, + location.first, + location.second, + false, + null, + false, + false + ) + + if (hasAcquireMockLocationPermission()) { + fakeLocationModule.startFakeLocation() + localStateRepository.setLocationMode(LocationMode.SPECIFIC_LOCATION) + fakeLocationModule.setFakeLocation(localStateRepository.fakeAltitude.toDouble(), + localStateRepository.fakeSpeed, + localStateRepository.fakeJitter, + localStateRepository.fakeLocation.first.toDouble(), + localStateRepository.fakeLocation.second.toDouble()) + } else { + fakeLocationModule.stopFakeLocation() + localStateRepository.setLocationMode(LocationMode.REAL_LOCATION) + } + } + + fun useRoute(route: List? = null) { + _configuredLocationMode.value = FakeLocationState( + LocationMode.ROUTE, + null, + null, + null, + null, + null, + null, + false, + route, false, - fakeLocation.first, - fakeLocation.second, false ) + + localStateRepository.setLocationMode(LocationMode.ROUTE) + } + + fun setRoute(route: List) { + _configuredLocationMode.value = FakeLocationState( + LocationMode.ROUTE, + null, + null, + null, + null, + null, + null, + false, + route, + false, + false + ) + } + + fun routeStart() { + if (hasAcquireMockLocationPermission()) { + fakeLocationModule.routeStart(localStateRepository.route, localStateRepository.routeLoopEnabled) + } else { + useRealLocation() + } + } + + fun routeStop() { + if (hasAcquireMockLocationPermission()) { + fakeLocationModule.routeStop() + } else { + useRealLocation() + } } val currentLocation = MutableStateFlow(null) diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt index 3c37da9..c58f4f7 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/GetQuickPrivacyStateUseCase.kt @@ -83,11 +83,12 @@ class GetQuickPrivacyStateUseCase( } } + @Suppress("UNUSED_PARAMETER") fun toggleLocation(enabled: Boolean?) { - val value = enabled ?: !localStateRepository.fakeLocationEnabled.value - if (value != localStateRepository.fakeLocationEnabled.value) { - localStateRepository.setFakeLocationEnabled(value) - } + // val value = enabled ?: !localStateRepository.fakeLocationEnabled.value + // if (value != localStateRepository.fakeLocationEnabled.value) { + // localStateRepository.setFakeLocationEnabled(value) + // } } fun toggleIpScrambling(enabled: Boolean?) { diff --git a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/ShowFeaturesWarningUseCase.kt b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/ShowFeaturesWarningUseCase.kt index 56b398a..177eab0 100644 --- a/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/ShowFeaturesWarningUseCase.kt +++ b/app/src/main/java/foundation/e/advancedprivacy/domain/usecases/ShowFeaturesWarningUseCase.kt @@ -37,9 +37,6 @@ class ShowFeaturesWarningUseCase( fun showWarning(): Flow { return merge( - localStateRepository.fakeLocationEnabled.drop(1).dropWhile { !it } - .filter { it && !localStateRepository.hideWarningLocation } - .map { FakeLocation }, localStateRepository.startVpnDisclaimer.filter { (it is IpScrambling && !localStateRepository.hideWarningIpScrambling) || (it is TrackersControl && !localStateRepository.hideWarningTrackers) 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? = 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>() {}.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? = 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() private val mockLocationParametersInputFlow = MutableSharedFlow() + private val setRouteLoopEnabledInputFlow = MutableSharedFlow() + private val setRouteInputFlow = MutableSharedFlow() @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(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 + ) : Action() + data class SetRouteLoopEnabledAction( + val isEnabled: Boolean + ) : Action() + object RouteStartAction : Action() + object RouteStopAction : Action() } } diff --git a/app/src/main/res/layout/fragment_fake_location.xml b/app/src/main/res/layout/fragment_fake_location.xml index 841ee56..d9f8a08 100644 --- a/app/src/main/res/layout/fragment_fake_location.xml +++ b/app/src/main/res/layout/fragment_fake_location.xml @@ -82,6 +82,14 @@ android:text="@string/location_use_specific_location" android:textSize="14sp" /> + + +