mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(android): Resources Table UI view (#1954)
* Implements UI for resources received on `onUpdateResources` callback. * Subsequent call to `onUpdateResources` calculates diff and updates the updates rows only. --------- Co-authored-by: Jamil Bou Kheir <jamilbk@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
skip = ./website/.next,./website/pnpm-lock.yaml,./rust/target,Cargo.lock,./website/docs/reference/api/*.mdx,./erl_crash.dump,./apps/*/erl_crash.dump,./cover,./vendor,*.json,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./_build
|
||||
ignore-words-list = crate,keypair,keypairs,iif,statics,wee,anull,commitish,inout
|
||||
ignore-words-list = optin,crate,keypair,keypairs,iif,statics,wee,anull,commitish,inout
|
||||
|
||||
@@ -114,14 +114,11 @@ dependencies {
|
||||
|
||||
// Moshi
|
||||
implementation "com.squareup.moshi:moshi-kotlin:1.12.0"
|
||||
implementation 'com.squareup.moshi:moshi:1.12.0'
|
||||
|
||||
// Gson
|
||||
implementation "com.google.code.gson:gson:2.9.0"
|
||||
|
||||
// Deep Link
|
||||
implementation "com.airbnb:deeplinkdispatch:6.1.0"
|
||||
kapt "com.airbnb:deeplinkdispatch-processor:6.1.0"
|
||||
|
||||
// Security
|
||||
implementation "androidx.security:security-crypto:1.1.0-alpha05"
|
||||
|
||||
|
||||
@@ -6,13 +6,15 @@ import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import dev.firezone.android.features.session.backend.SessionManager
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.firezone.android.core.domain.preference.GetConfigUseCase
|
||||
import dev.firezone.android.core.domain.preference.SaveIsConnectedUseCase
|
||||
import dev.firezone.android.tunnel.TunnelManager
|
||||
|
||||
internal const val ENCRYPTED_SHARED_PREFERENCES = "encryptedSharedPreferences"
|
||||
|
||||
@@ -39,8 +41,10 @@ object AppModule {
|
||||
)
|
||||
|
||||
@Provides
|
||||
internal fun provideSessionManager(
|
||||
internal fun provideTunnelManager(
|
||||
@ApplicationContext appContext: Context,
|
||||
getConfigUseCase: GetConfigUseCase,
|
||||
saveIsConnectedUseCase: SaveIsConnectedUseCase,
|
||||
): SessionManager = SessionManager(getConfigUseCase, saveIsConnectedUseCase)
|
||||
moshi: Moshi,
|
||||
): TunnelManager = TunnelManager(appContext, getConfigUseCase, saveIsConnectedUseCase, moshi)
|
||||
}
|
||||
|
||||
@@ -7,15 +7,9 @@ import android.util.Log
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.firezone.android.BuildConfig
|
||||
import dev.firezone.android.R
|
||||
import dev.firezone.android.core.presentation.MainActivity
|
||||
import dev.firezone.android.databinding.ActivityAppLinkHandlerBinding
|
||||
import dev.firezone.android.features.session.backend.SessionManager
|
||||
import dev.firezone.android.features.splash.ui.SplashFragmentDirections
|
||||
import dev.firezone.android.tunnel.TunnelManager
|
||||
import dev.firezone.android.tunnel.TunnelSession
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AppLinkHandlerActivity : AppCompatActivity(R.layout.activity_app_link_handler) {
|
||||
|
||||
@@ -7,13 +7,8 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.firezone.android.BuildConfig
|
||||
import dev.firezone.android.core.domain.preference.SaveTokenUseCase
|
||||
import dev.firezone.android.core.domain.preference.ValidateCsrfTokenUseCase
|
||||
import dev.firezone.android.features.session.backend.SessionManager
|
||||
import dev.firezone.android.tunnel.TunnelLogger
|
||||
import dev.firezone.android.tunnel.TunnelManager
|
||||
import dev.firezone.android.tunnel.TunnelSession
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
@@ -24,7 +19,6 @@ internal class AppLinkViewModel @Inject constructor(
|
||||
private val validateCsrfTokenUseCase: ValidateCsrfTokenUseCase,
|
||||
private val saveTokenUseCase: SaveTokenUseCase,
|
||||
) : ViewModel() {
|
||||
private val callback: TunnelManager = TunnelManager()
|
||||
private val actionMutableLiveData = MutableLiveData<ViewAction>()
|
||||
val actionLiveData: LiveData<ViewAction> = actionMutableLiveData
|
||||
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package dev.firezone.android.features.session.backend
|
||||
|
||||
import android.net.VpnService
|
||||
import android.util.Log
|
||||
import android.provider.Settings
|
||||
import dev.firezone.android.BuildConfig
|
||||
import dev.firezone.android.core.domain.preference.GetConfigUseCase
|
||||
import dev.firezone.android.core.domain.preference.SaveIsConnectedUseCase
|
||||
import dev.firezone.android.tunnel.TunnelCallbacks
|
||||
import dev.firezone.android.tunnel.TunnelLogger
|
||||
import dev.firezone.android.tunnel.TunnelSession
|
||||
import dev.firezone.android.tunnel.TunnelManager
|
||||
import dev.firezone.android.tunnel.TunnelService
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SessionManager @Inject constructor(
|
||||
private val getConfigUseCase: GetConfigUseCase,
|
||||
private val saveIsConnectedUseCase: SaveIsConnectedUseCase,
|
||||
) {
|
||||
private val callback: TunnelManager = TunnelManager()
|
||||
|
||||
fun connect() {
|
||||
try {
|
||||
val config = getConfigUseCase.sync()
|
||||
|
||||
Log.d("Connlib", "accountId: ${config.accountId}")
|
||||
Log.d("Connlib", "token: ${config.token}")
|
||||
|
||||
if (config.accountId != null && config.token != null) {
|
||||
Log.d("Connlib", "Attempting to establish TunnelSession...")
|
||||
sessionPtr = TunnelSession.connect(
|
||||
BuildConfig.CONTROL_PLANE_URL,
|
||||
config.token,
|
||||
Settings.Secure.ANDROID_ID,
|
||||
TunnelCallbacks()
|
||||
)
|
||||
Log.d("Connlib", "connlib session started! sessionPtr: $sessionPtr")
|
||||
setConnectionStatus(true)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
Log.e("Connection error:", exception.message.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
try {
|
||||
TunnelSession.disconnect(sessionPtr!!)
|
||||
setConnectionStatus(false)
|
||||
} catch (exception: Exception) {
|
||||
Log.e("Disconnection error:", exception.message.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setConnectionStatus(value: Boolean) {
|
||||
saveIsConnectedUseCase.sync(value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
internal companion object {
|
||||
var sessionPtr: Long? = null
|
||||
init {
|
||||
Log.d("Connlib","Attempting to load library from main app...")
|
||||
System.loadLibrary("connlib")
|
||||
Log.d("Connlib","Library loaded from main app!")
|
||||
TunnelLogger.init()
|
||||
Log.d("Connlib","Connlib Logger initialized!")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package dev.firezone.android.features.session.ui
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dev.firezone.android.databinding.ListItemResourceBinding
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
import javax.annotation.Nullable
|
||||
|
||||
internal class ResourcesAdapter: RecyclerView.Adapter<ResourcesAdapter.ViewHolder>() {
|
||||
|
||||
private val resources: MutableList<Resource> = mutableListOf()
|
||||
|
||||
fun updateResources(updatedResources: List<Resource>) {
|
||||
val diffCallback = ResourcesCallback(resources, updatedResources)
|
||||
val diffCourses = DiffUtil.calculateDiff(diffCallback)
|
||||
resources.clear()
|
||||
resources.addAll(updatedResources)
|
||||
diffCourses.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(
|
||||
ListItemResourceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = resources.size
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(resources[position])
|
||||
}
|
||||
|
||||
class ViewHolder(private val binding: ListItemResourceBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(resource: Resource) {
|
||||
binding.resourceNameText.text = resource.name
|
||||
binding.typeChip.text = resource.type
|
||||
binding.addressText.text = resource.address
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ResourcesCallback(private val oldList: List<Resource>, private val newList: List<Resource>) : DiffUtil.Callback() {
|
||||
override fun getOldListSize(): Int = oldList.size
|
||||
|
||||
override fun getNewListSize(): Int = newList.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldList[oldItemPosition] === newList[newItemPosition]
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val (type1, id1, address1, name1) = oldList[oldItemPosition]
|
||||
val (type2, id2, address2, name2) = newList[newItemPosition]
|
||||
return type1 == type2 && id1 == id2 && address1 == address2 && name1 == name2
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,55 @@
|
||||
package dev.firezone.android.features.session.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.firezone.android.R
|
||||
import dev.firezone.android.databinding.FragmentSessionBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class SessionFragment : Fragment(R.layout.fragment_session) {
|
||||
private lateinit var binding: FragmentSessionBinding
|
||||
private val viewModel: SessionViewModel by viewModels()
|
||||
|
||||
private val resourcesAdapter: ResourcesAdapter = ResourcesAdapter()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding = FragmentSessionBinding.bind(view)
|
||||
|
||||
setupButtonListeners()
|
||||
setupActionObservers()
|
||||
setupViews()
|
||||
setupObservers()
|
||||
Log.d("SessionFragment", "Starting session...")
|
||||
viewModel.startSession()
|
||||
}
|
||||
|
||||
private fun setupActionObservers() {
|
||||
private fun setupViews() {
|
||||
binding.btSignOut.setOnClickListener {
|
||||
viewModel.onDisconnect()
|
||||
}
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
val dividerItemDecoration = DividerItemDecoration(
|
||||
requireContext(),
|
||||
layoutManager.orientation
|
||||
)
|
||||
binding.resourcesList.addItemDecoration(dividerItemDecoration)
|
||||
binding.resourcesList.adapter = resourcesAdapter
|
||||
binding.resourcesList.layoutManager = layoutManager
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.actionLiveData.observe(viewLifecycleOwner) { action ->
|
||||
when (action) {
|
||||
SessionViewModel.ViewAction.NavigateToSignInFragment ->
|
||||
@@ -36,11 +59,15 @@ internal class SessionFragment : Fragment(R.layout.fragment_session) {
|
||||
SessionViewModel.ViewAction.ShowError -> showError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupButtonListeners() {
|
||||
binding.btSignOut.setOnClickListener {
|
||||
viewModel.onDisconnect()
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.uiState.collect { uiState ->
|
||||
uiState.resources?.let {
|
||||
resourcesAdapter.updateResources(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,92 @@
|
||||
package dev.firezone.android.features.session.ui
|
||||
|
||||
import dev.firezone.android.features.session.backend.SessionManager
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.firezone.android.tunnel.callback.TunnelListener
|
||||
import dev.firezone.android.tunnel.TunnelManager
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
internal class SessionViewModel @Inject constructor(
|
||||
private val sessionManager: SessionManager
|
||||
private val tunnelManager: TunnelManager,
|
||||
) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(UiState())
|
||||
val uiState: StateFlow<UiState> = _uiState
|
||||
|
||||
private val actionMutableLiveData = MutableLiveData<ViewAction>()
|
||||
val actionLiveData: LiveData<ViewAction> = actionMutableLiveData
|
||||
|
||||
private val tunnelListener = object: TunnelListener {
|
||||
override fun onSetInterfaceConfig(
|
||||
tunnelAddressIPv4: String,
|
||||
tunnelAddressIPv6: String,
|
||||
dnsAddress: String,
|
||||
dnsFallbackStrategy: String
|
||||
) {
|
||||
//TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onTunnelReady(): Boolean {
|
||||
//TODO("Not yet implemented")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onAddRoute(cidrAddress: String) {
|
||||
//TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onRemoveRoute(cidrAddress: String) {
|
||||
//TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onUpdateResources(resources: List<Resource>) {
|
||||
//TODO("Not yet implemented")
|
||||
Log.d("TunnelManager", "onUpdateResources: $resources")
|
||||
_uiState.value = _uiState.value.copy (
|
||||
resources = resources
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDisconnect(error: String?): Boolean {
|
||||
//TODO("Not yet implemented")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onError(error: String): Boolean {
|
||||
//TODO("Not yet implemented")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
fun startSession() {
|
||||
viewModelScope.launch {
|
||||
sessionManager.connect()
|
||||
tunnelManager.addListener(tunnelListener)
|
||||
tunnelManager.connect()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
tunnelManager.removeListener(tunnelListener)
|
||||
}
|
||||
|
||||
fun onDisconnect() {
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateToSignInFragment)
|
||||
}
|
||||
|
||||
internal data class UiState(
|
||||
val resources: List<Resource>? = null,
|
||||
)
|
||||
|
||||
internal sealed class ViewAction {
|
||||
|
||||
object NavigateToSignInFragment : ViewAction()
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package dev.firezone.android.tunnel
|
||||
|
||||
class Tunnel(
|
||||
val config: TunnelConfig,
|
||||
var state: State = State.Down
|
||||
) {
|
||||
|
||||
sealed interface State {
|
||||
object Up: State
|
||||
object Down: State
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package dev.firezone.android.tunnel
|
||||
|
||||
import android.net.VpnService
|
||||
import android.util.Log
|
||||
|
||||
class TunnelCallbacks {
|
||||
fun onUpdateResources(resourceListJSON: String) {
|
||||
// TODO: Call into client app to update resources list and routing table
|
||||
Log.d(TunnelCallbacks.TAG, "onUpdateResources: $resourceListJSON")
|
||||
}
|
||||
|
||||
fun onSetInterfaceConfig(
|
||||
tunnelAddressIPv4: String,
|
||||
tunnelAddressIPv6: String,
|
||||
dnsAddress: String,
|
||||
dnsFallbackStrategy: String,
|
||||
): Int {
|
||||
Log.d(TunnelCallbacks.TAG, "onSetInterfaceConfig: [IPv4:$tunnelAddressIPv4] [IPv6:$tunnelAddressIPv6] [dns:$dnsAddress] [dnsFallbackStrategy:$dnsFallbackStrategy]")
|
||||
return buildVpnService(tunnelAddressIPv4, tunnelAddressIPv6).establish()?.detachFd() ?: -1
|
||||
}
|
||||
|
||||
fun onTunnelReady(): Boolean {
|
||||
Log.d(TunnelCallbacks.TAG, "onTunnelReady")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun onError(error: String): Boolean {
|
||||
Log.d(TunnelCallbacks.TAG, "onError: $error")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun onAddRoute(cidrAddress: String) {
|
||||
Log.d(TunnelCallbacks.TAG, "onAddRoute: $cidrAddress")
|
||||
}
|
||||
|
||||
fun onRemoveRoute(cidrAddress: String) {
|
||||
Log.d(TunnelCallbacks.TAG, "onRemoveRoute: $cidrAddress")
|
||||
}
|
||||
|
||||
fun onDisconnect(error: String?): Boolean {
|
||||
Log.d(TunnelCallbacks.TAG, "onDisconnect $error")
|
||||
|
||||
return true
|
||||
}
|
||||
private fun buildVpnService(ipv4Address: String, ipv6Address: String): VpnService.Builder =
|
||||
TunnelService().Builder().apply {
|
||||
addAddress(ipv4Address, 32)
|
||||
addAddress(ipv6Address, 128)
|
||||
|
||||
// TODO: These are the staging Resources. Remove these in favor of the onUpdateResources callback.
|
||||
addRoute("172.31.93.123", 32)
|
||||
addRoute("172.31.83.10", 32)
|
||||
addRoute("172.31.82.179", 32)
|
||||
|
||||
setSession("Firezone VPN")
|
||||
|
||||
// TODO: Can we do better?
|
||||
setMtu(1280)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "TunnelCallbacks"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,45 @@
|
||||
package dev.firezone.android.tunnel
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import dev.firezone.android.BuildConfig
|
||||
import dev.firezone.android.core.domain.preference.GetConfigUseCase
|
||||
import dev.firezone.android.core.domain.preference.SaveIsConnectedUseCase
|
||||
import dev.firezone.android.tunnel.callback.ConnlibCallback
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
import dev.firezone.android.tunnel.callback.TunnelListener
|
||||
import dev.firezone.android.tunnel.model.Tunnel
|
||||
import dev.firezone.android.tunnel.model.TunnelConfig
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
class TunnelManager {
|
||||
@Singleton
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
internal class TunnelManager @Inject constructor(
|
||||
private val appContext: Context,
|
||||
private val getConfigUseCase: GetConfigUseCase,
|
||||
private val saveIsConnectedUseCase: SaveIsConnectedUseCase,
|
||||
private val moshi: Moshi,
|
||||
) {
|
||||
|
||||
private var activeTunnel: Tunnel? = null
|
||||
|
||||
private val listeners: MutableSet<WeakReference<TunnelListener>> = mutableSetOf()
|
||||
|
||||
private val callback: TunnelListener = object: TunnelListener {
|
||||
private val callback: ConnlibCallback = object: ConnlibCallback {
|
||||
override fun onUpdateResources(resourceListJSON: String) {
|
||||
// TODO: Call into client app to update resources list and routing table
|
||||
Log.d(TAG, "onUpdateResources: $resourceListJSON")
|
||||
listeners.onEach {
|
||||
it.get()?.onUpdateResources(resourceListJSON)
|
||||
moshi.adapter<List<Resource>>().fromJson(resourceListJSON)?.let { resources ->
|
||||
listeners.onEach {
|
||||
it.get()?.onUpdateResources(resources)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +48,20 @@ class TunnelManager {
|
||||
tunnelAddressIPv6: String,
|
||||
dnsAddress: String,
|
||||
dnsFallbackStrategy: String
|
||||
) {
|
||||
): Int {
|
||||
Log.d(TAG, "onSetInterfaceConfig: [IPv4:$tunnelAddressIPv4] [IPv6:$tunnelAddressIPv6] [dns:$dnsAddress] [dnsFallbackStrategy:$dnsFallbackStrategy]")
|
||||
|
||||
val tunnel = Tunnel(
|
||||
config = TunnelConfig(
|
||||
tunnelAddressIPv4, tunnelAddressIPv6, dnsAddress, dnsFallbackStrategy
|
||||
)
|
||||
)
|
||||
|
||||
listeners.onEach {
|
||||
it.get()?.onSetInterfaceConfig(tunnelAddressIPv4, tunnelAddressIPv6, dnsAddress, dnsFallbackStrategy)
|
||||
}
|
||||
|
||||
return buildVpnService(tunnelAddressIPv4, tunnelAddressIPv6).establish()?.detachFd() ?: -1
|
||||
}
|
||||
|
||||
override fun onTunnelReady(): Boolean {
|
||||
@@ -94,7 +127,81 @@ class TunnelManager {
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun startVPN() {
|
||||
val intent = Intent(appContext, TunnelService::class.java)
|
||||
intent.action = TunnelService.ACTION_CONNECT
|
||||
appContext.startService(intent)
|
||||
}
|
||||
|
||||
fun stopVPN() {
|
||||
val intent = Intent(appContext, TunnelService::class.java)
|
||||
intent.action = TunnelService.ACTION_DISCONNECT
|
||||
appContext.startService(intent)
|
||||
}
|
||||
|
||||
fun connect() {
|
||||
try {
|
||||
val config = getConfigUseCase.sync()
|
||||
|
||||
Log.d("Connlib", "accountId: ${config.accountId}")
|
||||
Log.d("Connlib", "token: ${config.token}")
|
||||
|
||||
if (config.accountId != null && config.token != null) {
|
||||
Log.d("Connlib", "Attempting to establish TunnelSession...")
|
||||
sessionPtr = TunnelSession.connect(
|
||||
controlPlaneUrl = BuildConfig.CONTROL_PLANE_URL,
|
||||
token = config.token,
|
||||
externalId = Settings.Secure.ANDROID_ID,
|
||||
callback = callback
|
||||
)
|
||||
Log.d("Connlib", "connlib session started! sessionPtr: ${sessionPtr}")
|
||||
setConnectionStatus(true)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
Log.e("Connection error:", exception.message.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
try {
|
||||
TunnelSession.disconnect(sessionPtr!!)
|
||||
setConnectionStatus(false)
|
||||
} catch (exception: Exception) {
|
||||
Log.e("Disconnection error:", exception.message.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setConnectionStatus(value: Boolean) {
|
||||
saveIsConnectedUseCase.sync(value)
|
||||
}
|
||||
|
||||
private fun buildVpnService(ipv4Address: String, ipv6Address: String): VpnService.Builder =
|
||||
TunnelService().Builder().apply {
|
||||
addAddress(ipv4Address, 32)
|
||||
addAddress(ipv6Address, 128)
|
||||
|
||||
// TODO: These are the staging Resources. Remove these in favor of the onUpdateResources callback.
|
||||
addRoute("172.31.93.123", 32)
|
||||
addRoute("172.31.83.10", 32)
|
||||
addRoute("172.31.82.179", 32)
|
||||
|
||||
setSession("Firezone VPN")
|
||||
|
||||
// TODO: Can we do better?
|
||||
setMtu(1280)
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
var sessionPtr: Long? = null
|
||||
|
||||
private const val TAG: String = "TunnelManager"
|
||||
|
||||
init {
|
||||
Log.d("Connlib","Attempting to load library from main app...")
|
||||
System.loadLibrary("connlib")
|
||||
Log.d("Connlib","Library loaded from main app!")
|
||||
TunnelLogger.init()
|
||||
Log.d("Connlib","Connlib Logger initialized!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,8 @@ class TunnelService: VpnService() {
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
val ACTION_CONNECT = "dev.firezone.android.tunnel.CONNECT"
|
||||
val ACTION_DISCONNECT = "dev.firezone.android.tunnel.DISCONNECT"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package dev.firezone.android.tunnel
|
||||
package dev.firezone.android.tunnel.callback
|
||||
|
||||
interface TunnelListener {
|
||||
|
||||
fun onSetInterfaceConfig(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String, dnsFallbackStrategy: String)
|
||||
interface ConnlibCallback {
|
||||
fun onSetInterfaceConfig(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String, dnsFallbackStrategy: String): Int
|
||||
|
||||
fun onTunnelReady(): Boolean
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package dev.firezone.android.tunnel.callback
|
||||
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
|
||||
interface TunnelListener {
|
||||
|
||||
fun onSetInterfaceConfig(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String, dnsFallbackStrategy: String)
|
||||
|
||||
fun onTunnelReady(): Boolean
|
||||
|
||||
fun onAddRoute(cidrAddress: String)
|
||||
|
||||
fun onRemoveRoute(cidrAddress: String)
|
||||
|
||||
fun onUpdateResources(resources: List<Resource>)
|
||||
|
||||
fun onDisconnect(error: String?): Boolean
|
||||
|
||||
fun onError(error: String): Boolean
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dev.firezone.android.tunnel.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Resource(
|
||||
val type: String,
|
||||
val id: String,
|
||||
val address: String,
|
||||
val name: String,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
package dev.firezone.android.tunnel.model
|
||||
|
||||
data class Tunnel(
|
||||
val config: TunnelConfig,
|
||||
var state: State = State.Down,
|
||||
val routes: MutableList<String> = mutableListOf(),
|
||||
val resources: MutableList<String> = mutableListOf(),
|
||||
) {
|
||||
|
||||
sealed interface State {
|
||||
object Up: State
|
||||
object CONNECTING: State
|
||||
object Down: State
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package dev.firezone.android.tunnel
|
||||
package dev.firezone.android.tunnel.model
|
||||
|
||||
data class TunnelConfig (
|
||||
val tunnelAddressIPv4: String,
|
||||
@@ -35,16 +35,20 @@
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/tvSignStatus"
|
||||
style="@style/AppTheme.Base.HeaderText"
|
||||
style="@style/AppTheme.Base.H5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_small"
|
||||
android:text="@string/sign_out_activity_status_text"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btSignOut"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:text="@string/resources"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/llContainer" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/resourcesList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvSignStatus"
|
||||
app:layout_constraintBottom_toTopOf="@id/btSignOut" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btSignOut"
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/spacing_small">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/resourceNameText"
|
||||
style="@style/AppTheme.Base.Body1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Resource Name"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/typeChip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Type"
|
||||
android:textAllCaps="true"
|
||||
android:enabled="false"
|
||||
app:layout_constraintTop_toTopOf="@id/resourceNameText"
|
||||
app:layout_constraintBottom_toBottomOf="@id/resourceNameText"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/addressText"
|
||||
style="@style/AppTheme.Base.Body2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Resource Address"
|
||||
app:layout_constraintTop_toBottomOf="@id/resourceNameText"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -12,7 +12,7 @@
|
||||
<string name="sign_in_fragment_sign_status_text">Signed Out</string>
|
||||
|
||||
<!-- Sign Out Activity -->
|
||||
<string name="sign_out_activity_status_text">Authenticated</string>
|
||||
<string name="resources">Resources</string>
|
||||
<string name="sign_out_activity_button_text">Sign Out</string>
|
||||
|
||||
<!-- Error Dialog -->
|
||||
|
||||
@@ -26,4 +26,10 @@
|
||||
<item name="android:textColorPrimary">@color/black_firezone</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Base.Body2">
|
||||
<item name="fontFamily">@font/fira_sans_regular</item>
|
||||
<item name="android:textSize">@dimen/text_body2</item>
|
||||
<item name="android:textColorPrimary">@color/black_firezone</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user