diff options
17 files changed, 518 insertions, 95 deletions
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<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> diff --git a/core/src/main/java/foundation/e/advancedprivacy/domain/entities/FakeLocationCoordinate.kt b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/FakeLocationCoordinate.kt new file mode 100644 index 0000000..a7992a9 --- /dev/null +++ b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/FakeLocationCoordinate.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 Leonard Kugis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + + package foundation.e.advancedprivacy.domain.entities + + data class FakeLocationCoordinate( + val latitude: Float, + val longitude: Float, + val altitude: Float, + val speed: Float, + val jitter: Float, + val bearing: Float, + ) +
\ No newline at end of file diff --git a/core/src/main/java/foundation/e/advancedprivacy/domain/entities/LocationMode.kt b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/LocationMode.kt index 3642fcc..a82e7d0 100644 --- a/core/src/main/java/foundation/e/advancedprivacy/domain/entities/LocationMode.kt +++ b/core/src/main/java/foundation/e/advancedprivacy/domain/entities/LocationMode.kt @@ -19,5 +19,5 @@ package foundation.e.advancedprivacy.domain.entities enum class LocationMode { - REAL_LOCATION, RANDOM_LOCATION, SPECIFIC_LOCATION + REAL_LOCATION, RANDOM_LOCATION, SPECIFIC_LOCATION, ROUTE } diff --git a/core/src/main/java/foundation/e/advancedprivacy/domain/repositories/LocalStateRepository.kt b/core/src/main/java/foundation/e/advancedprivacy/domain/repositories/LocalStateRepository.kt index 641f6da..34bc096 100644 --- a/core/src/main/java/foundation/e/advancedprivacy/domain/repositories/LocalStateRepository.kt +++ b/core/src/main/java/foundation/e/advancedprivacy/domain/repositories/LocalStateRepository.kt @@ -21,6 +21,7 @@ import foundation.e.advancedprivacy.domain.entities.ApplicationDescription import foundation.e.advancedprivacy.domain.entities.FeatureState import foundation.e.advancedprivacy.domain.entities.LocationMode import foundation.e.advancedprivacy.domain.entities.MainFeatures +import foundation.e.advancedprivacy.domain.entities.FakeLocationCoordinate import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow @@ -31,16 +32,17 @@ interface LocalStateRepository { val areAllTrackersBlocked: MutableStateFlow<Boolean> - val fakeLocationEnabled: StateFlow<Boolean> - fun setFakeLocationEnabled(enabled: Boolean) - var fakeAltitude: Float var fakeSpeed: Float var fakeJitter: Float var fakeLocation: Pair<Float, Float> - val locationMode: MutableStateFlow<LocationMode> + var routeLoopEnabled: Boolean + var route: List<FakeLocationCoordinate> + + val locationMode: StateFlow<LocationMode> + fun setLocationMode(mode: LocationMode) fun setIpScramblingSetting(enabled: Boolean) val ipScramblingSetting: StateFlow<Boolean> diff --git a/fakelocation/build.gradle b/fakelocation/build.gradle index 02f6de5..58099d3 100644 --- a/fakelocation/build.gradle +++ b/fakelocation/build.gradle @@ -49,7 +49,8 @@ android { dependencies { implementation( libs.bundles.koin, - libs.bundles.kotlin.android.coroutines + libs.bundles.kotlin.android.coroutines, + libs.google.gson ) implementation project(':core') diff --git a/fakelocation/src/main/java/foundation/e/advancedprivacy/fakelocation/domain/usecases/FakeLocationModule.kt b/fakelocation/src/main/java/foundation/e/advancedprivacy/fakelocation/domain/usecases/FakeLocationModule.kt index 89d5628..7424f38 100644 --- a/fakelocation/src/main/java/foundation/e/advancedprivacy/fakelocation/domain/usecases/FakeLocationModule.kt +++ b/fakelocation/src/main/java/foundation/e/advancedprivacy/fakelocation/domain/usecases/FakeLocationModule.kt @@ -29,6 +29,7 @@ import android.os.Build import android.os.SystemClock import android.util.Log import foundation.e.advancedprivacy.fakelocation.services.FakeLocationService +import foundation.e.advancedprivacy.domain.entities.FakeLocationCoordinate /** * Implementation of the functionality of fake location. @@ -86,6 +87,14 @@ class FakeLocationModule(private val context: Context) { } } + fun routeStart(route: List<FakeLocationCoordinate>, loopEnabled: Boolean) { + context.startService(FakeLocationService.buildRouteIntent(context, route, loopEnabled)) + } + + fun routeStop() { + context.stopService(FakeLocationService.buildStopIntent(context)) + } + fun setFakeLocation(altitude: Double, speed: Float, jitter: Float, latitude: Double, longitude: Double) { context.startService(FakeLocationService.buildFakeLocationIntent(context, altitude, speed, jitter, latitude, longitude)) } diff --git a/fakelocation/src/main/java/foundation/e/advancedprivacy/fakelocation/services/FakeLocationService.kt b/fakelocation/src/main/java/foundation/e/advancedprivacy/fakelocation/services/FakeLocationService.kt index 2d85e6c..c388afc 100644 --- a/fakelocation/src/main/java/foundation/e/advancedprivacy/fakelocation/services/FakeLocationService.kt +++ b/fakelocation/src/main/java/foundation/e/advancedprivacy/fakelocation/services/FakeLocationService.kt @@ -25,13 +25,18 @@ import android.os.CountDownTimer import android.os.IBinder import android.util.Log import foundation.e.advancedprivacy.fakelocation.domain.usecases.FakeLocationModule +import foundation.e.advancedprivacy.domain.entities.FakeLocationCoordinate +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import kotlin.math.sqrt import kotlin.random.Random class FakeLocationService : Service() { enum class Actions { - START_FAKE_LOCATION + START_FAKE_LOCATION, + START_ROUTE } companion object { @@ -43,6 +48,8 @@ class FakeLocationService : Service() { private const val PARAM_JITTER = "PARAM_JITTER" private const val PARAM_LATITUDE = "PARAM_LATITUDE" private const val PARAM_LONGITUDE = "PARAM_LONGITUDE" + private const val PARAM_ROUTE = "PARAM_ROUTE" + private const val PARAM_ROUTE_LOOP = "PARAM_ROUTE_LOOP" fun buildFakeLocationIntent(context: Context, altitude: Double, speed: Float, jitter: Float, latitude: Double, longitude: Double): Intent { return Intent(context, FakeLocationService::class.java).apply { @@ -55,12 +62,22 @@ class FakeLocationService : Service() { } } + fun buildRouteIntent(context: Context, route: List<FakeLocationCoordinate>, loopRoute: Boolean): Intent { + return Intent(context, FakeLocationService::class.java).apply { + action = Actions.START_ROUTE.name + putExtra(PARAM_ROUTE, Gson().toJson(route)) + putExtra(PARAM_ROUTE_LOOP, loopRoute) + } + } + fun buildStopIntent(context: Context) = Intent(context, FakeLocationService::class.java) } private lateinit var fakeLocationModule: FakeLocationModule - private var countDownTimer: CountDownTimer? = null + private var cdtFakeLocation: CountDownTimer? = null + private var cdtRoute: CountDownTimer? = null + private var routeTime: Float = 0f private var altitude: Double? = null private var speed: Float? = null @@ -68,6 +85,10 @@ class FakeLocationService : Service() { private var fakeLocation: Pair<Double, Double>? = null + private var route: List<FakeLocationCoordinate>? = null + private var loopRoute: Boolean = false + private var routeReversed: Boolean = false + override fun onCreate() { super.onCreate() fakeLocationModule = FakeLocationModule(applicationContext) @@ -84,7 +105,12 @@ class FakeLocationService : Service() { it.getDoubleExtra(PARAM_LATITUDE, 0.0), it.getDoubleExtra(PARAM_LONGITUDE, 0.0) ) - initTimer() + initTimerFakeLocation() + } + Actions.START_ROUTE -> { + route = Gson().fromJson(it.getStringExtra(PARAM_ROUTE) ?: "[]", object : TypeToken<List<FakeLocationCoordinate>>() {}.type) + loopRoute = it.getBooleanExtra(PARAM_ROUTE_LOOP, false) + initTimerRoute() } else -> {} } @@ -94,13 +120,14 @@ class FakeLocationService : Service() { } override fun onDestroy() { - countDownTimer?.cancel() + cdtFakeLocation?.cancel() + cdtRoute?.cancel() super.onDestroy() } - private fun initTimer() { - countDownTimer?.cancel() - countDownTimer = object : CountDownTimer(PERIOD_UPDATES_SERIE, PERIOD_LOCATION_UPDATE) { + private fun initTimerFakeLocation() { + cdtFakeLocation?.cancel() + cdtFakeLocation = object : CountDownTimer(PERIOD_UPDATES_SERIE, PERIOD_LOCATION_UPDATE) { override fun onTick(millisUntilFinished: Long) { var altitude_buf: Double = altitude ?: return var speed_buf: Float = speed ?: return @@ -109,11 +136,11 @@ class FakeLocationService : Service() { if(fakeLocation != null && altitude != null && speed != null && jitter != null) { try { fakeLocationModule.setTestProviderLocation( - altitude_buf + ((Random.nextFloat() * jitter_buf) - (jitter_buf * 0.5f)), + (altitude_buf + ((Random.nextFloat() * jitter_buf) - (jitter_buf * 0.5f))).toDouble(), speed_buf + ((Random.nextFloat() * jitter_buf) - (jitter_buf * 0.5f)), jitter_buf, - fakeLocation_buf.first + (((Random.nextFloat() * jitter_buf) - (jitter_buf * 0.5f)) / 111139.0f), - fakeLocation_buf.second + (((Random.nextFloat() * jitter_buf) - (jitter_buf * 0.5f)) / 111139.0f) + (fakeLocation_buf.first + (((Random.nextFloat() * jitter_buf) - (jitter_buf * 0.5f)) / 111139.0f)).toDouble(), + (fakeLocation_buf.second + (((Random.nextFloat() * jitter_buf) - (jitter_buf * 0.5f)) / 111139.0f)).toDouble() ) } catch (e: Exception) { Log.d("FakeLocationService", "setting fake location", e) @@ -122,7 +149,66 @@ class FakeLocationService : Service() { } override fun onFinish() { - initTimer() + initTimerFakeLocation() + } + }.start() + } + + private fun calculateRouteSegment(route: List<FakeLocationCoordinate>, routeTime: Float): Pair<FakeLocationCoordinate,Pair<Float,Float>>? { + if(route.size < 2) + return null + var prev = route.first() + var timeCurrent: Float = 0f + do { + var route_current = if(routeReversed) route.reversed() else route + for(coord in route_current) { + var direction = Pair<Float,Float>((coord.latitude - prev.latitude) * 111139.0f, (coord.longitude - prev.longitude) * 111139.0f) + if(!(coord.latitude == prev.latitude && coord.longitude == prev.longitude)) { + var distance_target = sqrt((direction.first * direction.first) + (direction.second * direction.second)) + var direction_unit = Pair<Float,Float>(direction.first / distance_target, direction.second / distance_target) + var location_meters = Pair<Float,Float>(direction_unit.first * (routeTime - timeCurrent) * prev.speed, + direction_unit.second * (routeTime - timeCurrent) * prev.speed) + var distance_current = sqrt((location_meters.first * location_meters.first) + (location_meters.second - location_meters.second)) + var location = Pair<Float,Float>(prev.latitude + (location_meters.first / 111139.0f), prev.longitude + (location_meters.second / 111139.0f)) + if(distance_current < distance_target) + return Pair<FakeLocationCoordinate,Pair<Float,Float>>(prev, location) + timeCurrent += distance_target / prev.speed + prev = coord + } + } + if(loopRoute) + routeReversed = !routeReversed + } while(loopRoute) + return null + } + + private fun initTimerRoute() { + cdtRoute?.cancel() + routeTime = 0f + cdtRoute = object : CountDownTimer(PERIOD_UPDATES_SERIE, PERIOD_LOCATION_UPDATE) { + override fun onTick(millisUntilFinished: Long) { + var route_buf: List<FakeLocationCoordinate> = route ?: return + var coord = calculateRouteSegment(route_buf, routeTime) + if(coord == null) { + // done with route + return + } + try { + fakeLocationModule.setTestProviderLocation( + (coord.first.altitude + ((Random.nextFloat() * coord.first.jitter) - (coord.first.jitter * 0.5f))).toDouble(), + coord.first.speed + ((Random.nextFloat() * coord.first.jitter) - (coord.first.jitter * 0.5f)), + coord.first.jitter, + (coord.second.first + (((Random.nextFloat() * coord.first.jitter) - (coord.first.jitter * 0.5f)) / 111139.0f)).toDouble(), + (coord.second.second + (((Random.nextFloat() * coord.first.jitter) - (coord.first.jitter * 0.5f)) / 111139.0f)).toDouble() + ) + } catch (e: Exception) { + Log.d("FakeLocationService", "setting fake location", e) + } + routeTime += (PERIOD_LOCATION_UPDATE / 1000f) + } + + override fun onFinish() { + initTimerRoute() } }.start() } |