Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.WorkManager
import com.bringyour.network.location.MockLocationController
import com.bringyour.network.location.MockLocationFeeder
import com.bringyour.network.ui.shared.models.ProvideNetworkMode
import com.bringyour.sdk.DeviceLocal
import com.bringyour.sdk.LocalState
Expand Down Expand Up @@ -167,6 +168,9 @@ class MainApplication : Application() {
@Inject
lateinit var mockLocationController: MockLocationController

@Inject
lateinit var mockLocationFeeder: MockLocationFeeder

var vpnRequestStart: Boolean = false
private set

Expand Down Expand Up @@ -471,6 +475,7 @@ class MainApplication : Application() {
// from the feature UI) so a previous process's leftovers are cleared
// even when the user never opens the provider locations sheet.
mockLocationController.start()
mockLocationFeeder.start()

networkSpaceManagerProvider.init(filesDir.absolutePath)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.bringyour.network.location

import com.bringyour.network.DeviceManager
import com.bringyour.sdk.DeviceLocal
import com.bringyour.sdk.Sub
import javax.inject.Inject
import javax.inject.Singleton

/**
* Feeds MockLocationController with tunnel lifecycle and exit provider location updates
* from the SDK DeviceLocal instance across the entire application lifecycle.
*/
@Singleton
class MockLocationFeeder @Inject constructor(
private val deviceManager: DeviceManager,
private val controller: MockLocationController,
) {
private var removeDeviceChangeListener: (() -> Unit)? = null
private var connectSub: Sub? = null
private var providerSub: Sub? = null

fun start() {
if (removeDeviceChangeListener != null) return
removeDeviceChangeListener = deviceManager.addDeviceChangeListener { device ->
attach(device)
}
attach(deviceManager.device)
}

private fun attach(device: DeviceLocal?) {
connectSub?.close()
connectSub = null
providerSub?.close()
providerSub = null

if (device != null) {
connectSub = device.addConnectChangeListener { enabled ->
controller.onTunnelChanged(enabled)
}
providerSub = device.addConnectedProviderLocationChangeListener {
pushTarget(device)
}
controller.onTunnelChanged(device.connectEnabled)
pushTarget(device)
} else {
controller.onTunnelChanged(false)
controller.onTargetChanged(null)
}
}

private fun pushTarget(device: DeviceLocal) {
controller.onTunnelChanged(device.connectEnabled)
val locations = device.connectedProviderLocations
var target: MockLocationTarget? = null
if (locations != null) {
for (i in 0 until locations.len()) {
val location = locations.get(i)
val lat: Double
val lon: Double
when {
location.hasCityCoordinates -> {
lat = location.cityLat
lon = location.cityLon
}
location.hasRegionCoordinates -> {
lat = location.regionLat
lon = location.regionLon
}
else -> continue
}
val label = listOf(location.city, location.region, location.country)
.filter { it.isNotEmpty() }
.take(2)
.joinToString(", ")
target = MockLocationTarget(
clientId = location.clientId?.idStr ?: "",
label = label,
lat = lat,
lon = lon,
)
break
}
}
controller.onTargetChanged(target)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import com.bringyour.network.location.openAboutPhone
import com.bringyour.network.location.openDeveloperOptions
import com.bringyour.network.location.openLocationSettings
import com.bringyour.network.ui.components.URButton
import com.bringyour.network.ui.components.URSwitch
import com.bringyour.network.ui.theme.Black
import com.bringyour.network.ui.theme.Green
import com.bringyour.network.ui.theme.MainTintedBackgroundBase
Expand Down Expand Up @@ -159,11 +160,32 @@ fun MockLocationGuideScreen(
Spacer(modifier = Modifier.height(24.dp))

if (state.setupComplete) {
Text(
stringResource(id = R.string.mock_location_ready),
style = MaterialTheme.typography.bodyMedium,
color = Green,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
val statusText = when {
state.status == MockLocationStatus.ACTIVE && state.target != null ->
stringResource(id = R.string.mock_location_active, state.target.label)
state.enabled ->
stringResource(id = R.string.mock_location_waiting_for_provider)
else ->
stringResource(id = R.string.mock_location_ready)
}

Text(
statusText,
style = MaterialTheme.typography.bodyMedium,
color = Green,
modifier = Modifier.weight(1f),
)
Spacer(modifier = Modifier.width(8.dp))
URSwitch(
checked = state.enabled,
toggle = { viewModel.setEnabled(!state.enabled) },
)
}
Spacer(modifier = Modifier.height(24.dp))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,109 +1,29 @@
package com.bringyour.network.ui.connect.providerlocations

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.bringyour.network.DeviceManager
import com.bringyour.network.location.MockLocationController
import com.bringyour.network.location.MockLocationState
import com.bringyour.network.location.MockLocationTarget
import com.bringyour.sdk.DeviceLocal
import com.bringyour.sdk.Sub
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

/**
* Feeds the mock location controller from the connected provider window: the
* target is the oldest connected provider that has coordinates. The controller
* owns all Android location state; this only translates sdk events.
* UI bridge for the mock location controller: exposes state and UI actions.
* SDK event feeds are managed at process lifetime by MockLocationFeeder.
*/
@HiltViewModel
class MockLocationViewModel @Inject constructor(
private val deviceManager: DeviceManager,
private val controller: MockLocationController,
) : ViewModel() {

val state: StateFlow<MockLocationState> = controller.state

private var sub: Sub? = null
private var removeDeviceChangeListener: (() -> Unit)? = null

init {
removeDeviceChangeListener = deviceManager.addDeviceChangeListener { device ->
attach(device)
}
attach(deviceManager.device)
}

private fun attach(device: DeviceLocal?) {
sub?.close()
sub = device?.addConnectedProviderLocationChangeListener {
viewModelScope.launch { pushTarget() }
}
viewModelScope.launch {
controller.onTunnelChanged(device?.connectEnabled == true)
pushTarget()
}
}

private fun pushTarget() {
val device = deviceManager.device
controller.onTunnelChanged(device?.connectEnabled == true)

val locations = device?.connectedProviderLocations
var target: MockLocationTarget? = null
if (locations != null) {
// Read from the DEVICE, not the provider-locations view controller:
// the device getter is the raw window, still sorted oldest
// connected first, while the controller reorders it west to east
// for the list and the globe. Take the first one that actually has
// coordinates.
for (i in 0 until locations.len()) {
val location = locations.get(i)
val lat: Double
val lon: Double
when {
location.hasCityCoordinates -> {
lat = location.cityLat
lon = location.cityLon
}
location.hasRegionCoordinates -> {
lat = location.regionLat
lon = location.regionLon
}
else -> continue
}
val label = listOf(location.city, location.region, location.country)
.filter { it.isNotEmpty() }
.take(2)
.joinToString(", ")
target = MockLocationTarget(
clientId = location.clientId?.idStr ?: "",
label = label,
lat = lat,
lon = lon,
)
break
}
}
controller.onTargetChanged(target)
}

fun setEnabled(enabled: Boolean) {
controller.setEnabled(enabled)
pushTarget()
}

fun refreshEligibility() {
controller.refreshEligibility()
}

override fun onCleared() {
super.onCleared()
sub?.close()
sub = null
removeDeviceChangeListener?.invoke()
removeDeviceChangeListener = null
}
}

Loading
Loading