Skip to content

Commit

Permalink
Full serverless locations / Updates (#13)
Browse files Browse the repository at this point in the history
* show warm and cold locations

* fix location selector

* include estimate in proto and types for ts and rust

* UI show warm and cold icons with colors

* MdCircle

* update android to show warm and cold icons

* on location conflict ignore

* check token from backend; have separate daemon offline check

* make warm cold icon size 1em

* handle unauthorized on Android

* update desktop app version to 0.5.0

* update android app version to u2 / 5

* fix device handler is_authenticated

* fix error handling for unauthenticated

* spelling fix

* cleanup unused imports
  • Loading branch information
64bit committed Dec 6, 2023
1 parent ec363af commit 62ad924
Show file tree
Hide file tree
Showing 39 changed files with 385 additions and 132 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions upvpn-android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ android {
applicationId = "app.upvpn.upvpn"
minSdk = 24
targetSdk = 34
versionCode = 4
versionName = "u1-beta"
versionCode = 5
versionName = "u2"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ class DefaultVPNRepository(

private val tag = "DefaultVPNRepository"

private var locations: List<Location> = listOf()

private fun createDevice(): Device {
val keyPair = KeyPair()
val device = Device(
Expand Down Expand Up @@ -97,46 +95,52 @@ class DefaultVPNRepository(
return addDeviceResponse.map { it.token }
}

private suspend fun postSignOutCleanup() {
vpnDatabase.withTransaction {
val device = vpnDatabase.deviceDao().getDevice()
val user = vpnDatabase.userDao().getUser()
device?.let { vpnDatabase.deviceDao().delete(it) }
user?.let { vpnDatabase.userDao().delete(it) }
}
}

override suspend fun signOut(): Result<Unit, String> {
val signedOut = vpnApiService.signOut().toResult().mapError { e -> e.message }

signedOut.onSuccess {
vpnDatabase.withTransaction {
val device = vpnDatabase.deviceDao().getDevice()
val user = vpnDatabase.userDao().getUser()
device?.let { vpnDatabase.deviceDao().delete(it) }
user?.let { vpnDatabase.userDao().delete(it) }
return signedOut.fold(
success = {
postSignOutCleanup()
Ok(Unit)
},
failure = {
if (it == "unauthorized") {
postSignOutCleanup()
Ok(Unit)
} else {
Err(it)
}
}
}

return signedOut
)
}

override suspend fun getLocations(): Result<List<Location>, String> {
var result = if (locations.isEmpty()) {
val fromDatabase = vpnDatabase.locationDao().getLocations()
if (fromDatabase.isEmpty()) {
val apiResult = vpnApiService.getLocations().toResult().mapError { e -> e.message }

apiResult.fold(
success = { newLocations ->
Log.i(tag, "received ${newLocations.size} locations from API")
locations = newLocations
val dbLocations = locations.map { it.toDbLocation() }
vpnDatabase.locationDao().insert(dbLocations)
Ok(locations)
},
failure = { error ->
Log.i(tag, "failed to get locations from API $error")
Err(error)
}
)
} else {
Ok(fromDatabase.map { it.toModelLocation() })

val apiResult = vpnApiService.getLocations().toResult().mapError { e -> e.message }

var result = apiResult.fold(
success = { newLocations ->
Log.i(tag, "received ${newLocations.size} locations from API")
val newLocationCodes = newLocations.map { it.code }
vpnDatabase.locationDao().deleteNotIn(newLocationCodes);
val dbLocations = newLocations.map { it.toDbLocation() }
vpnDatabase.locationDao().insert(dbLocations)
Ok(newLocations)
},
failure = { error ->
Log.i(tag, "failed to get locations from API $error")
Err(error)
}
} else {
Ok(locations)
}
)

return result
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ interface LocationDao {
@Query("SELECT * FROM location")
suspend fun getLocations(): List<Location>

@Insert(onConflict = OnConflictStrategy.REPLACE)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(locations: List<Location>)

@Query("DELETE FROM location WHERE code NOT IN (:notInLocationCodes)")
suspend fun deleteNotIn(notInLocationCodes: List<String>);

@Query("UPDATE location SET lastAccess = :lastAccess WHERE code = :code")
suspend fun updateLastAccess(code: String, lastAccess: Long)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package app.upvpn.upvpn.model

import androidx.compose.ui.graphics.Color

fun List<Location>.toCountries(): List<Country> =
this.sortedWith(compareByDescending<Location> { it.country }.thenBy { it.city })
.groupBy { it.country }
Expand Down Expand Up @@ -46,8 +48,36 @@ fun Location.displayText(): String {
else -> "${city.uppercase()}, ${stateCode.uppercase()}"
}
}

else -> {
city.uppercase()
}
}
}

val LOCATION_WARM_COLOR = Color(22, 163, 74, 255)
val LOCATION_COLD_COLOR = Color(56, 189, 248, 255)

fun Location.warmOrColdColor(): Color {
return when (this.estimate) {
null -> Color.Unspecified
else -> {
when (this.estimate <= 10) {
true -> LOCATION_WARM_COLOR
else -> LOCATION_COLD_COLOR
}
}
}
}

fun Location.warmOrColdDescription(): String {
return when (this.estimate) {
null -> ""
else -> {
when (this.estimate <= 10) {
true -> "Warm"
else -> "Cold"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ data class Location(
val city: String,
val cityCode: String,
val state: String? = null,
val stateCode: String? = null
val stateCode: String? = null,
val estimate: Short? = null
) : Parcelable

@Serializable
Expand Down
10 changes: 9 additions & 1 deletion upvpn-android/app/src/main/kotlin/app/upvpn/upvpn/ui/VPNApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,21 @@ fun VPNApp(
val vpnNotifications = homeVM.vpnNotificationState.collectAsStateWithLifecycle()

LaunchedEffect(key1 = vpnNotifications.value) {
var unauthorized = false;
for (notification in vpnNotifications.value) {
showSnackBar(notification.msg)
homeVM.ackVpnNotification(notification)
if (notification.msg == "unauthorized") {
unauthorized = true;
}
}

if (unauthorized) {
(vm::onSignOutClick)()
}
}

if(BuildConfig.DEBUG) {
if (BuildConfig.DEBUG) {
Log.d("VPNApp", "CURRENT SCREEN: $currentVPNScreen")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package app.upvpn.upvpn.ui.components

import androidx.compose.foundation.border
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
Expand All @@ -17,6 +21,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import app.upvpn.upvpn.model.LOCATION_COLD_COLOR
import app.upvpn.upvpn.model.LOCATION_WARM_COLOR
import app.upvpn.upvpn.model.Location


Expand Down Expand Up @@ -45,6 +51,23 @@ fun LocationComponent(

Text(text = location.city, modifier = Modifier.weight(1f))

location?.estimate?.let {
if (it <= 10) {
Icon(
imageVector = Icons.Rounded.Circle,
contentDescription = "Warm",
modifier = Modifier.size(15.dp),
tint = LOCATION_WARM_COLOR
)
} else {
Icon(
imageVector = Icons.Rounded.Circle, contentDescription = "Cold",
modifier = Modifier.size(15.dp),
tint = LOCATION_COLD_COLOR
)
}
}

RadioButton(
enabled = isVpnSessionActivityInProgress.not(),
selected = isSelectedLocation(location),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ChevronRight
import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
Expand All @@ -24,9 +26,11 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import app.upvpn.upvpn.model.Location
import app.upvpn.upvpn.model.displayText
import app.upvpn.upvpn.model.warmOrColdColor
import app.upvpn.upvpn.model.warmOrColdDescription
import app.upvpn.upvpn.ui.state.LocationState
import app.upvpn.upvpn.ui.state.VpnUiState
import app.upvpn.upvpn.ui.state.locationSelectorEnabled
import app.upvpn.upvpn.ui.state.isVpnSessionActivityInProgress

@Composable
fun LocationSelector(
Expand Down Expand Up @@ -54,19 +58,23 @@ fun LocationSelector(
val found =
locationState.locations.firstOrNull { it.city.contains("ashburn") }
if (found != null) {
Triple(found.displayText(), Icons.Rounded.ChevronRight, openLocationScreen)
Triple(
found.displayText(),
Icons.Rounded.Circle,
openLocationScreen
)
} else {
Triple(
locationState.locations.first().displayText(),
Icons.Rounded.ChevronRight,
Icons.Rounded.Circle,
openLocationScreen
)
}
}
} else {
Triple(
selectedLocation.displayText(),
Icons.Rounded.ChevronRight,
Icons.Rounded.Circle,
openLocationScreen
)
}
Expand All @@ -83,7 +91,6 @@ fun LocationSelector(
disabledContainerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onSurface
),
enabled = vpnUiState.locationSelectorEnabled() && locationState.locationSelectorEnabled(),
modifier = modifier
.fillMaxWidth(0.8f)
.height(50.dp)
Expand All @@ -98,16 +105,28 @@ fun LocationSelector(
.fillMaxHeight(0.6f)
.aspectRatio(1f)
)
}
if (displayText.contains("No Locations").not()) {
CountryIcon(countryCode = selectedLocation?.countryCode ?: "US")
}
Text(text = displayText, overflow = TextOverflow.Visible)
if (isLoading.not()) {
Icon(
imageVector = icon,
contentDescription = "Arrow"
)
} else {
if (displayText.contains("No Locations")) {
Text(text = displayText, overflow = TextOverflow.Visible)
Icon(imageVector = icon, contentDescription = "Reload")
} else {
CountryIcon(countryCode = selectedLocation?.countryCode ?: "US")
Text(text = displayText, overflow = TextOverflow.Visible)
if (vpnUiState.isVpnSessionActivityInProgress().not()) {
Icon(
imageVector = icon,
contentDescription = selectedLocation?.warmOrColdDescription()
?: "Warm or Cold",
modifier = Modifier.size(15.dp),
tint = selectedLocation?.warmOrColdColor() ?: Color.Transparent
)
} else {
Icon(
imageVector = Icons.Rounded.ChevronRight,
contentDescription = "Arrow"
)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,6 @@ fun VpnUiState.vpnDisplayText(): String {

fun VpnUiState.isVpnSessionActivityInProgress(): Boolean = (this is VpnUiState.Disconnected).not()

fun VpnUiState.locationSelectorEnabled(): Boolean {
return when (this) {
is VpnUiState.Disconnected -> true
else -> false
}
}

fun LocationState.locationSelectorEnabled(): Boolean {
return when (this) {
is LocationState.Loading -> false
else -> true
}
}

fun VpnUiState.switchEnabled(): Boolean {
return when (this) {
Expand Down
Loading

0 comments on commit 62ad924

Please sign in to comment.