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