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 | |
parent | 298dff2a877680e928b37e3a1336dc7d7aa52dfb (diff) |
Implemented route mode
Diffstat (limited to 'app/src/main')
10 files changed, 373 insertions, 77 deletions
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<Boolean> = 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<LocationMode> = MutableStateFlow(LocationMode.REAL_LOCATION) + override var route: List<FakeLocationCoordinate> + get() { + return Gson().fromJson<List<FakeLocationCoordinate>>(sharedPref.getString(KEY_LOCATION_ROUTE, "[]"), object : TypeToken<List<FakeLocationCoordinate>>() {}.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>( - 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<FakeLocationState> = _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<Float, Float>, isSpecificLocation: Boolean = false) { + fun useFakeLocation(location: Pair<Float,Float>) { 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<Float, Float>, - 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<FakeLocationCoordinate>? = 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<FakeLocationCoordinate>) { + _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<Location?>(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<MainFeatures> { 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<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() } } 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" /> + + <foundation.e.advancedprivacy.common.RightRadioButton + android:id="@+id/radio_use_route" + android:layout_height="52dp" + android:layout_width="match_parent" + android:text="@string/location_use_route" + android:textSize="14sp" + /> </RadioGroup> <com.google.android.material.textfield.TextInputLayout @@ -141,6 +149,38 @@ /> </com.google.android.material.textfield.TextInputLayout> + <Button + android:id="@+id/button_location_route_path_select" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/location_route_path_select" /> + + <TextView + android:id="@+id/location_route_path" + android:layout_gravity="center_horizontal" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:text="@string/location_route_path" + android:lineSpacingExtra="5sp" + /> + + <CheckBox android:id="@+id/checkbox_route_loop" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/location_route_loop" /> + + <Button + android:id="@+id/button_location_route_start" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/location_route_start" /> + + <Button + android:id="@+id/button_location_route_stop" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/location_route_stop" /> + <FrameLayout android:layout_marginTop="16dp" android:layout_height="220dp" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb64b9d..2379ab9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,6 +61,7 @@ <string name="dashboard_location_subtitle_off" weblate_ctx="home-2">Real geolocation</string> <string name="dashboard_location_subtitle_specific">Specific fake geolocation</string> <string name="dashboard_location_subtitle_random">Random fake geolocation</string> + <string name="dashboard_location_subtitle_route">Walking fake route</string> <string name="dashboard_internet_activity_privacy_title" weblate_ctx="home-2 ">Manage my Internet address</string> <string name="dashboard_internet_activity_privacy_subtitle_off" weblate_ctx="home-2">Real IP address exposed</string> <string name="dashboard_internet_activity_privacy_subtitle_on">Real IP address hidden</string> @@ -84,14 +85,22 @@ <string name="location_title" weblate_ctx="location-1">Manage my location</string> <string name="location_info" weblate_ctx="location-1">Your location can reveal a lot about yourself or your activities.\n\nManage my location enables you to use a fake location instead of your real position. This way, your real location isn\'t shared with applications that might be snooping too much.</string> <string name="location_altitude">Altitude [m]</string> - <string name="location_speed">Speed [km/h]</string> + <string name="location_speed">Speed [m/s]</string> <string name="location_jitter">Jitter [m]</string> <string name="location_use_real_location" weblate_ctx="location-1">Use my real location</string> <string name="location_use_random_location" weblate_ctx="location-1">Use a random plausible location</string> <string name="location_use_specific_location" weblate_ctx="location-1">Use a specific location</string> + <string name="location_use_route" weblate_ctx="location-1">Walk specific route</string> <string name="location_hint_longitude" weblate_ctx="location-2">Longitude</string> <string name="location_hint_latitude" weblate_ctx="location-2">Latitude</string> + <string name="location_error_bounds">Value out of bounds</string> <string name="location_input_error">Invalid coordinates</string> + <string name="location_route_path_select">Choose route...</string> + <string name="location_route_path_select_picker">Select route JSON</string> + <string name="location_route_path">No route selected</string> + <string name="location_route_loop">Loop route</string> + <string name="location_route_start">Start route</string> + <string name="location_route_stop">Stop route</string> <!-- Trackers --> <string name="trackers_title" weblate_ctx="trackers-1">Manage apps\' trackers</string> |