mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
refactor(android): Simplify tunnel implementation and fix tunnel lifecycle (#3583)
Fixes #3578 Fixes #3551 The issue turned out to be a bunk Repository. Upon unraveling that ball of yarn, I decided to clean up the Tunnel implementation altogether. It uses the existing tunnel in-memory store for pushing updates to a connected SessionActivity. This PR includes many bug fixes as well.
This commit is contained in:
8
.github/workflows/_kotlin.yml
vendored
8
.github/workflows/_kotlin.yml
vendored
@@ -68,6 +68,12 @@ jobs:
|
||||
# TODO: See https://github.com/firezone/firezone/issues/2311
|
||||
# TODO: See https://github.com/firezone/firezone/issues/2309
|
||||
./gradlew testReleaseUnitTest
|
||||
- name: Upload app bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Android app bundle
|
||||
path: |
|
||||
./kotlin/android/app/build/outputs/bundle/*
|
||||
- name: Upload release
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || (github.ref == 'refs/heads/main' && contains(github.event.head_commit.modified, 'elixir/VERSION')) }}
|
||||
env:
|
||||
@@ -76,4 +82,4 @@ jobs:
|
||||
FIREBASE_APP_TESTERS: ${{ vars.FIREBASE_APP_TESTERS }}
|
||||
run: |
|
||||
echo -n "$FIREBASE_APP_DISTRIBUTION_CREDENTIALS" > $FIREBASE_CREDENTIALS_PATH
|
||||
./gradlew --info appDistributionUploadRelease
|
||||
./gradlew --info appDistributionUploadRelease uploadCrashlyticsSymbolFileRelease
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.mozilla.rust-android-gradle.rust-android")
|
||||
@@ -42,13 +44,13 @@ android {
|
||||
|
||||
namespace = "dev.firezone.android"
|
||||
compileSdk = 34
|
||||
ndkVersion = "25.2.9519653"
|
||||
ndkVersion = "26.1.10909125"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "dev.firezone.android"
|
||||
// Android 8
|
||||
minSdk = 26
|
||||
targetSdk = 33
|
||||
targetSdk = 34
|
||||
versionCode = (System.currentTimeMillis() / 1000 / 10).toInt()
|
||||
// mark:automatic-version
|
||||
versionName = "1.0.0"
|
||||
@@ -105,6 +107,15 @@ android {
|
||||
)
|
||||
isDebuggable = false
|
||||
|
||||
configure<CrashlyticsExtension> {
|
||||
// Enable processing and uploading of native symbols to Firebase servers.
|
||||
// By default, this is disabled to improve build speeds.
|
||||
// This flag must be enabled to see properly-symbolicated native
|
||||
// stack traces in the Crashlytics dashboard.
|
||||
nativeSymbolUploadEnabled = true
|
||||
unstrippedNativeLibsDir = layout.buildDirectory.dir("rustJniLibs")
|
||||
}
|
||||
|
||||
resValue("string", "app_name", "\"Firezone\"")
|
||||
|
||||
buildConfigField("String", "AUTH_BASE_URL", "\"https://app.firezone.dev\"")
|
||||
@@ -196,11 +207,12 @@ dependencies {
|
||||
implementation("androidx.browser:browser:1.7.0")
|
||||
|
||||
// Import the BoM for the Firebase platform
|
||||
implementation(platform("com.google.firebase:firebase-bom:32.3.1"))
|
||||
implementation(platform("com.google.firebase:firebase-bom:32.7.1"))
|
||||
|
||||
// Add the dependencies for the Crashlytics and Analytics libraries
|
||||
// When using the BoM, you don't specify versions in Firebase library dependencies
|
||||
implementation("com.google.firebase:firebase-crashlytics-ktx")
|
||||
implementation("com.google.firebase:firebase-crashlytics-ndk")
|
||||
implementation("com.google.firebase:firebase-analytics-ktx")
|
||||
implementation("com.google.firebase:firebase-installations-ktx")
|
||||
}
|
||||
@@ -217,7 +229,13 @@ cargo {
|
||||
verbose = true
|
||||
module = "../../../rust/connlib/clients/android"
|
||||
libname = "connlib"
|
||||
targets = listOf("arm", "arm64", "x86_64", "x86")
|
||||
targets =
|
||||
listOf(
|
||||
"arm64",
|
||||
"x86_64",
|
||||
"x86",
|
||||
"arm",
|
||||
)
|
||||
targetDirectory = "../../../rust/target"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@@ -8,6 +7,8 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -16,6 +17,7 @@
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:gwpAsanMode="always"
|
||||
android:name=".core.FirezoneApp"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@@ -71,6 +73,7 @@
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:foregroundServiceType="systemExempted"
|
||||
android:name="dev.firezone.android.tunnel.TunnelService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_VPN_SERVICE">
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.core.data
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal interface AuthRepository {
|
||||
fun generateNonce(key: String): Flow<String>
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.core.data
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import java.security.SecureRandom
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AuthRepositoryImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val coroutineDispatcher: CoroutineDispatcher,
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
) : AuthRepository {
|
||||
override fun generateNonce(key: String): Flow<String> =
|
||||
flow {
|
||||
val random = SecureRandom.getInstanceStrong()
|
||||
val bytes = ByteArray(NONCE_LENGTH)
|
||||
random.nextBytes(bytes)
|
||||
val encodedStr: String = bytes.joinToString("") { "%02x".format(it) }
|
||||
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(key, encodedStr)
|
||||
.apply()
|
||||
|
||||
emit(encodedStr)
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
companion object {
|
||||
private const val NONCE_LENGTH = 32
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ package dev.firezone.android.core.data
|
||||
import dev.firezone.android.core.data.model.Config
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal interface PreferenceRepository {
|
||||
interface Repository {
|
||||
fun getConfigSync(): Config
|
||||
|
||||
fun getConfig(): Flow<Config>
|
||||
@@ -17,11 +17,39 @@ internal interface PreferenceRepository {
|
||||
|
||||
fun saveDeviceIdSync(value: String): Unit
|
||||
|
||||
fun getToken(): Flow<String?>
|
||||
|
||||
fun getTokenSync(): String?
|
||||
|
||||
fun getStateSync(): String?
|
||||
|
||||
fun getNonceSync(): String?
|
||||
|
||||
fun getDeviceIdSync(): String?
|
||||
|
||||
fun getActorName(): Flow<String?>
|
||||
|
||||
fun getActorNameSync(): String?
|
||||
|
||||
fun saveNonce(value: String): Flow<Unit>
|
||||
|
||||
fun saveState(value: String): Flow<Unit>
|
||||
|
||||
fun saveStateSync(value: String): Unit
|
||||
|
||||
fun saveNonceSync(value: String): Unit
|
||||
|
||||
fun saveToken(value: String): Flow<Unit>
|
||||
|
||||
fun saveActorName(value: String): Flow<Unit>
|
||||
|
||||
fun validateState(value: String): Flow<Boolean>
|
||||
|
||||
fun clearToken()
|
||||
|
||||
fun clearNonce()
|
||||
|
||||
fun clearState()
|
||||
|
||||
fun clearActorName()
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package dev.firezone.android.core.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import dev.firezone.android.BuildConfig
|
||||
import dev.firezone.android.core.data.model.Config
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
@@ -12,29 +13,22 @@ import kotlinx.coroutines.flow.flowOn
|
||||
import java.security.MessageDigest
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class PreferenceRepositoryImpl
|
||||
internal class RepositoryImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val context: Context,
|
||||
private val coroutineDispatcher: CoroutineDispatcher,
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
) : PreferenceRepository {
|
||||
private val appRestrictions: Bundle,
|
||||
) : Repository {
|
||||
override fun getConfigSync(): Config {
|
||||
val restrictionsManager =
|
||||
context.getSystemService(Context.RESTRICTIONS_SERVICE)
|
||||
as android.content.RestrictionsManager
|
||||
val appRestrictions = restrictionsManager.applicationRestrictions
|
||||
return Config(
|
||||
authBaseUrl =
|
||||
sharedPreferences.getString(AUTH_BASE_URL_KEY, null)
|
||||
?: BuildConfig.AUTH_BASE_URL,
|
||||
apiUrl = sharedPreferences.getString(API_URL_KEY, null) ?: BuildConfig.API_URL,
|
||||
logFilter =
|
||||
sharedPreferences.getString(LOG_FILTER_KEY, null)
|
||||
?: BuildConfig.LOG_FILTER,
|
||||
token =
|
||||
appRestrictions.getString(TOKEN_KEY, null)
|
||||
?: sharedPreferences.getString(TOKEN_KEY, null),
|
||||
sharedPreferences.getString(AUTH_BASE_URL_KEY, null)
|
||||
?: BuildConfig.AUTH_BASE_URL,
|
||||
sharedPreferences.getString(API_URL_KEY, null)
|
||||
?: BuildConfig.API_URL,
|
||||
sharedPreferences.getString(LOG_FILTER_KEY, null)
|
||||
?: BuildConfig.LOG_FILTER,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,12 +55,52 @@ internal class PreferenceRepositoryImpl
|
||||
|
||||
override fun getDeviceIdSync(): String? = sharedPreferences.getString(DEVICE_ID_KEY, null)
|
||||
|
||||
override fun getToken(): Flow<String?> =
|
||||
flow {
|
||||
emit(
|
||||
appRestrictions.getString(TOKEN_KEY, null)
|
||||
?: sharedPreferences.getString(TOKEN_KEY, null),
|
||||
)
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
override fun getTokenSync(): String? =
|
||||
appRestrictions.getString(TOKEN_KEY, null)
|
||||
?: sharedPreferences.getString(TOKEN_KEY, null)
|
||||
|
||||
override fun getStateSync(): String? = sharedPreferences.getString(STATE_KEY, null)
|
||||
|
||||
override fun getActorName(): Flow<String?> =
|
||||
flow {
|
||||
emit(getActorNameSync())
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
override fun getActorNameSync(): String? =
|
||||
sharedPreferences.getString(ACTOR_NAME_KEY, null)?.let {
|
||||
if (it.isNotEmpty()) "Signed in as $it" else "Signed in"
|
||||
}
|
||||
|
||||
override fun getNonceSync(): String? = sharedPreferences.getString(NONCE_KEY, null)
|
||||
|
||||
override fun saveDeviceIdSync(value: String): Unit =
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(DEVICE_ID_KEY, value)
|
||||
.apply()
|
||||
|
||||
override fun saveNonce(value: String): Flow<Unit> =
|
||||
flow {
|
||||
emit(saveNonceSync(value))
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
override fun saveNonceSync(value: String) = sharedPreferences.edit().putString(NONCE_KEY, value).apply()
|
||||
|
||||
override fun saveState(value: String): Flow<Unit> =
|
||||
flow {
|
||||
emit(saveStateSync(value))
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
override fun saveStateSync(value: String) = sharedPreferences.edit().putString(STATE_KEY, value).apply()
|
||||
|
||||
override fun saveToken(value: String): Flow<Unit> =
|
||||
flow {
|
||||
val nonce = sharedPreferences.getString(NONCE_KEY, "").orEmpty()
|
||||
@@ -78,6 +112,16 @@ internal class PreferenceRepositoryImpl
|
||||
)
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
override fun saveActorName(value: String): Flow<Unit> =
|
||||
flow {
|
||||
emit(
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(ACTOR_NAME_KEY, value)
|
||||
.apply(),
|
||||
)
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
override fun validateState(value: String): Flow<Boolean> =
|
||||
flow {
|
||||
val state = sharedPreferences.getString(STATE_KEY, "").orEmpty()
|
||||
@@ -86,15 +130,35 @@ internal class PreferenceRepositoryImpl
|
||||
|
||||
override fun clearToken() {
|
||||
sharedPreferences.edit().apply {
|
||||
remove(NONCE_KEY)
|
||||
remove(TOKEN_KEY)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearNonce() {
|
||||
sharedPreferences.edit().apply {
|
||||
remove(NONCE_KEY)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearState() {
|
||||
sharedPreferences.edit().apply {
|
||||
remove(STATE_KEY)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearActorName() {
|
||||
sharedPreferences.edit().apply {
|
||||
remove(ACTOR_NAME_KEY)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val AUTH_BASE_URL_KEY = "authBaseUrl"
|
||||
private const val ACTOR_NAME_KEY = "actorName"
|
||||
private const val API_URL_KEY = "apiUrl"
|
||||
private const val LOG_FILTER_KEY = "logFilter"
|
||||
private const val TOKEN_KEY = "token"
|
||||
@@ -1,9 +1,8 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.core.data.model
|
||||
|
||||
internal data class Config(
|
||||
data class Config(
|
||||
val authBaseUrl: String,
|
||||
val apiUrl: String,
|
||||
val logFilter: String,
|
||||
val token: String?,
|
||||
)
|
||||
|
||||
@@ -3,30 +3,32 @@ package dev.firezone.android.core.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.getSystemService
|
||||
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.data.AuthRepository
|
||||
import dev.firezone.android.core.data.AuthRepositoryImpl
|
||||
import dev.firezone.android.core.data.PreferenceRepository
|
||||
import dev.firezone.android.core.data.PreferenceRepositoryImpl
|
||||
import dev.firezone.android.core.data.Repository
|
||||
import dev.firezone.android.core.data.RepositoryImpl
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class DataModule {
|
||||
@Provides
|
||||
internal fun provideAuthRepository(
|
||||
@IoDispatcher coroutineDispatcher: CoroutineDispatcher,
|
||||
sharedPreferences: SharedPreferences,
|
||||
): AuthRepository = AuthRepositoryImpl(coroutineDispatcher, sharedPreferences)
|
||||
|
||||
@Provides
|
||||
internal fun providePreferenceRepository(
|
||||
internal fun provideRepository(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher coroutineDispatcher: CoroutineDispatcher,
|
||||
sharedPreferences: SharedPreferences,
|
||||
): PreferenceRepository = PreferenceRepositoryImpl(context, coroutineDispatcher, sharedPreferences)
|
||||
): Repository =
|
||||
RepositoryImpl(
|
||||
context,
|
||||
coroutineDispatcher,
|
||||
sharedPreferences,
|
||||
(
|
||||
context.getSystemService(Context.RESTRICTIONS_SERVICE)
|
||||
as android.content.RestrictionsManager
|
||||
).applicationRestrictions,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.core.domain.auth
|
||||
|
||||
import dev.firezone.android.core.data.AuthRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class GetNonceUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val repository: AuthRepository,
|
||||
) {
|
||||
operator fun invoke(): Flow<String> = repository.generateNonce("nonce")
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.core.domain.auth
|
||||
|
||||
import dev.firezone.android.core.data.AuthRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class GetStateUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val repository: AuthRepository,
|
||||
) {
|
||||
operator fun invoke(): Flow<String> = repository.generateNonce("state")
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.core.domain.preference
|
||||
|
||||
import dev.firezone.android.core.data.PreferenceRepository
|
||||
import dev.firezone.android.core.data.model.Config
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class GetConfigUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val repository: PreferenceRepository,
|
||||
) {
|
||||
operator fun invoke(): Flow<Config> = repository.getConfig()
|
||||
|
||||
fun sync(): Config = repository.getConfigSync()
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.core.domain.preference
|
||||
|
||||
import dev.firezone.android.core.data.PreferenceRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SaveSettingsUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val repository: PreferenceRepository,
|
||||
) {
|
||||
operator fun invoke(
|
||||
authBaseUrl: String,
|
||||
apiUrl: String,
|
||||
logFilter: String,
|
||||
): Flow<Unit> = repository.saveSettings(authBaseUrl, apiUrl, logFilter)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.core.domain.preference
|
||||
|
||||
import dev.firezone.android.core.data.PreferenceRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SaveTokenUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val repository: PreferenceRepository,
|
||||
) {
|
||||
operator fun invoke(token: String): Flow<Unit> = repository.saveToken(token)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.core.domain.preference
|
||||
|
||||
import dev.firezone.android.core.data.PreferenceRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ValidateStateUseCase
|
||||
@Inject
|
||||
constructor(
|
||||
private val repository: PreferenceRepository,
|
||||
) {
|
||||
operator fun invoke(value: String): Flow<Boolean> = repository.validateState(value)
|
||||
}
|
||||
@@ -43,6 +43,9 @@ class AuthActivity : AppCompatActivity(R.layout.activity_auth) {
|
||||
}
|
||||
}
|
||||
|
||||
// We can't close this webview because it's opened with ACTION_VIEW.
|
||||
// If we want more control over it we need to embed our own WebView which
|
||||
// has its own set of tradeoffs.
|
||||
private fun setupWebView(url: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
startActivity(intent)
|
||||
|
||||
@@ -6,21 +6,16 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.firezone.android.core.domain.auth.GetNonceUseCase
|
||||
import dev.firezone.android.core.domain.auth.GetStateUseCase
|
||||
import dev.firezone.android.core.domain.preference.GetConfigUseCase
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import dev.firezone.android.core.data.Repository
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.Exception
|
||||
import java.security.SecureRandom
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class AuthViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val getConfigUseCase: GetConfigUseCase,
|
||||
private val getStateUseCase: GetStateUseCase,
|
||||
private val getNonceUseCase: GetNonceUseCase,
|
||||
private val repo: Repository,
|
||||
) : ViewModel() {
|
||||
private val actionMutableLiveData = MutableLiveData<ViewAction>()
|
||||
val actionLiveData: LiveData<ViewAction> = actionMutableLiveData
|
||||
@@ -28,37 +23,31 @@ internal class AuthViewModel
|
||||
private var authFlowLaunched: Boolean = false
|
||||
|
||||
fun onActivityResume() =
|
||||
try {
|
||||
viewModelScope.launch {
|
||||
val config =
|
||||
getConfigUseCase()
|
||||
.firstOrNull() ?: throw Exception("config cannot be null")
|
||||
viewModelScope.launch {
|
||||
val state = generateRandomString(NONCE_LENGTH)
|
||||
val nonce = generateRandomString(NONCE_LENGTH)
|
||||
repo.saveNonceSync(nonce)
|
||||
repo.saveStateSync(state)
|
||||
val config = repo.getConfigSync()!!
|
||||
val token = repo.getTokenSync()
|
||||
|
||||
val state =
|
||||
getStateUseCase()
|
||||
.firstOrNull() ?: throw Exception("state cannot be null")
|
||||
|
||||
val nonce =
|
||||
getNonceUseCase()
|
||||
.firstOrNull() ?: throw Exception("nonce cannot be null")
|
||||
|
||||
actionMutableLiveData.postValue(
|
||||
if (authFlowLaunched || config.token != null) {
|
||||
ViewAction.NavigateToSignIn
|
||||
} else {
|
||||
authFlowLaunched = true
|
||||
ViewAction.LaunchAuthFlow(
|
||||
url =
|
||||
"${config.authBaseUrl}" +
|
||||
"?state=$state&nonce=$nonce&as=client",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
actionMutableLiveData.postValue(ViewAction.ShowError)
|
||||
actionMutableLiveData.postValue(
|
||||
if (authFlowLaunched || token != null) {
|
||||
ViewAction.NavigateToSignIn
|
||||
} else {
|
||||
authFlowLaunched = true
|
||||
ViewAction.LaunchAuthFlow("${config.authBaseUrl}?state=$state&nonce=$nonce&as=client")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateRandomString(length: Int): String {
|
||||
val random = SecureRandom.getInstanceStrong()
|
||||
val bytes = ByteArray(length)
|
||||
random.nextBytes(bytes)
|
||||
return bytes.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
internal sealed class ViewAction {
|
||||
data class LaunchAuthFlow(val url: String) : ViewAction()
|
||||
|
||||
@@ -66,4 +55,8 @@ internal class AuthViewModel
|
||||
|
||||
object ShowError : ViewAction()
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
private const val NONCE_LENGTH = 32
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ package dev.firezone.android.features.customuri.ui
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.firezone.android.R
|
||||
import dev.firezone.android.core.presentation.MainActivity
|
||||
import dev.firezone.android.databinding.ActivityCustomUriHandlerBinding
|
||||
import dev.firezone.android.tunnel.TunnelService
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CustomUriHandlerActivity : AppCompatActivity(R.layout.activity_custom_uri_handler) {
|
||||
@@ -28,28 +28,17 @@ class CustomUriHandlerActivity : AppCompatActivity(R.layout.activity_custom_uri_
|
||||
viewModel.actionLiveData.observe(this) { action ->
|
||||
when (action) {
|
||||
CustomUriViewModel.ViewAction.AuthFlowComplete -> {
|
||||
TunnelService.start(this@CustomUriHandlerActivity)
|
||||
startActivity(
|
||||
Intent(this@CustomUriHandlerActivity, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
},
|
||||
Intent(this, MainActivity::class.java),
|
||||
)
|
||||
finish()
|
||||
}
|
||||
CustomUriViewModel.ViewAction.ShowError -> showError()
|
||||
else -> {
|
||||
throw IllegalStateException("Unknown action: $action")
|
||||
}
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError() {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.error_dialog_title)
|
||||
.setMessage(R.string.error_dialog_message)
|
||||
.setPositiveButton(
|
||||
R.string.error_dialog_button_text,
|
||||
) { _, _ ->
|
||||
this@CustomUriHandlerActivity.finish()
|
||||
}
|
||||
.setIcon(R.drawable.ic_firezone_logo)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,53 +7,59 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.firezone.android.core.domain.preference.SaveTokenUseCase
|
||||
import dev.firezone.android.core.domain.preference.ValidateStateUseCase
|
||||
import dev.firezone.android.core.data.Repository
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.IllegalStateException
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class CustomUriViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val validateStateUseCase: ValidateStateUseCase,
|
||||
private val saveTokenUseCase: SaveTokenUseCase,
|
||||
private val repo: Repository,
|
||||
) : ViewModel() {
|
||||
private val actionMutableLiveData = MutableLiveData<ViewAction>()
|
||||
val actionLiveData: LiveData<ViewAction> = actionMutableLiveData
|
||||
|
||||
fun parseCustomUri(intent: Intent) {
|
||||
Log.d("CustomUriViewModel", "Parsing callback...")
|
||||
viewModelScope.launch {
|
||||
Log.d("CustomUriViewModel", "viewmodelScope.launch")
|
||||
when (intent.data?.host) {
|
||||
PATH_CALLBACK -> {
|
||||
Log.d("CustomUriViewModel", "PATH_CALLBACK")
|
||||
intent.data?.getQueryParameter(QUERY_ACTOR_NAME)?.let { actorName ->
|
||||
Log.d("CustomUriViewModel", "Found actor name: $actorName")
|
||||
repo.saveActorName(actorName).collect()
|
||||
}
|
||||
intent.data?.getQueryParameter(QUERY_CLIENT_STATE)?.let { state ->
|
||||
Log.d("CustomUriViewModel", "state: $state")
|
||||
if (validateStateUseCase(state).firstOrNull() == true) {
|
||||
if (repo.validateState(state).firstOrNull() == true) {
|
||||
Log.d("CustomUriViewModel", "Valid state parameter. Continuing to save state...")
|
||||
} else {
|
||||
Log.d("CustomUriViewModel", "Invalid state parameter! Ignoring...")
|
||||
actionMutableLiveData.postValue(ViewAction.ShowError)
|
||||
throw IllegalStateException("Invalid state parameter $state! Authentication will not succeed...")
|
||||
}
|
||||
intent.data?.getQueryParameter(QUERY_CLIENT_AUTH_FRAGMENT)?.let { fragment ->
|
||||
if (fragment.isNotBlank()) {
|
||||
Log.d("CustomUriViewModel", "Found valid auth fragment in response")
|
||||
saveTokenUseCase(fragment).collect()
|
||||
|
||||
// Save token, then clear nonce and state since we don't
|
||||
// need to keep them around anymore
|
||||
repo.saveToken(fragment).collect()
|
||||
repo.clearNonce()
|
||||
repo.clearState()
|
||||
|
||||
actionMutableLiveData.postValue(ViewAction.AuthFlowComplete)
|
||||
} else {
|
||||
Log.d("CustomUriViewModel", "Didn't find auth fragment in response!")
|
||||
throw IllegalStateException("Invalid auth fragment $fragment! Authentication will not succeed...")
|
||||
}
|
||||
}
|
||||
|
||||
actionMutableLiveData.postValue(ViewAction.AuthFlowComplete)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.d("CustomUriViewModel", "Unknown path segment: ${intent.data?.lastPathSegment}")
|
||||
Firebase.crashlytics.log("Unknown path segment: ${intent.data?.lastPathSegment}")
|
||||
Log.e("CustomUriViewModel", "Unknown path segment: ${intent.data?.lastPathSegment}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,12 +70,9 @@ internal class CustomUriViewModel
|
||||
private const val QUERY_CLIENT_STATE = "state"
|
||||
private const val QUERY_CLIENT_AUTH_FRAGMENT = "fragment"
|
||||
private const val QUERY_ACTOR_NAME = "actor_name"
|
||||
private const val QUERY_IDENTITY_PROVIDER_IDENTIFIER = "identity_provider_identifier"
|
||||
}
|
||||
|
||||
internal sealed class ViewAction {
|
||||
object AuthFlowComplete : ViewAction()
|
||||
|
||||
object ShowError : ViewAction()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,49 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.features.session.ui
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.firezone.android.R
|
||||
import dev.firezone.android.core.presentation.MainActivity
|
||||
import dev.firezone.android.core.utils.ClipboardUtils
|
||||
import dev.firezone.android.databinding.ActivitySessionBinding
|
||||
import dev.firezone.android.features.settings.ui.SettingsActivity
|
||||
import kotlinx.coroutines.launch
|
||||
import dev.firezone.android.tunnel.TunnelService
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class SessionActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivitySessionBinding
|
||||
private var tunnelService: TunnelService? = null
|
||||
private var serviceBound = false
|
||||
private val viewModel: SessionViewModel by viewModels()
|
||||
|
||||
private val serviceConnection =
|
||||
object : ServiceConnection {
|
||||
override fun onServiceConnected(
|
||||
name: ComponentName?,
|
||||
service: IBinder?,
|
||||
) {
|
||||
Log.d(TAG, "onServiceConnected")
|
||||
val binder = service as TunnelService.LocalBinder
|
||||
tunnelService = binder.getService()
|
||||
serviceBound = true
|
||||
tunnelService?.setServiceStateLiveData(viewModel.serviceStatusLiveData)
|
||||
tunnelService?.setResourcesLiveData(viewModel.resourcesLiveData)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
Log.d(TAG, "onServiceDisconnected")
|
||||
serviceBound = false
|
||||
}
|
||||
}
|
||||
private val resourcesAdapter: ResourcesAdapter =
|
||||
ResourcesAdapter { resource ->
|
||||
ClipboardUtils.copyToClipboard(this@SessionActivity, resource.name, resource.address)
|
||||
@@ -34,19 +54,31 @@ internal class SessionActivity : AppCompatActivity() {
|
||||
binding = ActivitySessionBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
// Bind to existing TunnelService
|
||||
val intent = Intent(this, TunnelService::class.java)
|
||||
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||
|
||||
setupViews()
|
||||
setupObservers()
|
||||
viewModel.connect(this@SessionActivity)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (serviceBound) {
|
||||
unbindService(serviceConnection)
|
||||
serviceBound = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
binding.btSignOut.setOnClickListener {
|
||||
viewModel.disconnect()
|
||||
Log.d(TAG, "Sign out button clicked")
|
||||
viewModel.clearToken()
|
||||
viewModel.clearActorName()
|
||||
tunnelService?.disconnect()
|
||||
}
|
||||
|
||||
binding.btSettings.setOnClickListener {
|
||||
startActivity(Intent(this@SessionActivity, SettingsActivity::class.java))
|
||||
}
|
||||
binding.tvActorName.text = viewModel.getActorName()
|
||||
|
||||
val layoutManager = LinearLayoutManager(this@SessionActivity)
|
||||
val dividerItemDecoration =
|
||||
@@ -57,42 +89,25 @@ internal class SessionActivity : AppCompatActivity() {
|
||||
binding.rvResourcesList.addItemDecoration(dividerItemDecoration)
|
||||
binding.rvResourcesList.adapter = resourcesAdapter
|
||||
binding.rvResourcesList.layoutManager = layoutManager
|
||||
|
||||
// Hack to show a connecting message until the service is bound
|
||||
resourcesAdapter.updateResources(listOf(Resource("", "", "", "Connecting...")))
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.actionLiveData.observe(this@SessionActivity) { action ->
|
||||
when (action) {
|
||||
SessionViewModel.ViewAction.NavigateToSignIn -> {
|
||||
startActivity(
|
||||
Intent(this, MainActivity::class.java),
|
||||
)
|
||||
finish()
|
||||
}
|
||||
SessionViewModel.ViewAction.ShowError -> showError()
|
||||
// Go back to MainActivity if the service dies
|
||||
viewModel.serviceStatusLiveData.observe(this) { tunnelState ->
|
||||
if (tunnelState == TunnelService.Companion.State.DOWN) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.uiState.collect { uiState ->
|
||||
uiState.resources?.let {
|
||||
resourcesAdapter.updateResources(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel.resourcesLiveData.observe(this) { resources ->
|
||||
resourcesAdapter.updateResources(resources)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError() {
|
||||
AlertDialog.Builder(this@SessionActivity)
|
||||
.setTitle(R.string.error_dialog_title)
|
||||
.setMessage(R.string.error_dialog_message)
|
||||
.setPositiveButton(
|
||||
R.string.error_dialog_button_text,
|
||||
) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setIcon(R.drawable.ic_firezone_logo)
|
||||
.show()
|
||||
companion object {
|
||||
private const val TAG = "SessionActivity"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,112 +1,31 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.features.session.ui
|
||||
|
||||
import android.content.Context
|
||||
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.TunnelManager
|
||||
import dev.firezone.android.tunnel.TunnelService
|
||||
import dev.firezone.android.tunnel.callback.TunnelListener
|
||||
import dev.firezone.android.tunnel.data.TunnelRepository
|
||||
import dev.firezone.android.core.data.Repository
|
||||
import dev.firezone.android.tunnel.TunnelService.Companion.State
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
import dev.firezone.android.tunnel.model.Tunnel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class SessionViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val tunnelManager: TunnelManager,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(UiState())
|
||||
val uiState: StateFlow<UiState> = _uiState
|
||||
constructor() : ViewModel() {
|
||||
@Inject
|
||||
internal lateinit var repo: Repository
|
||||
private val _serviceStatusLiveData = MutableLiveData<State>()
|
||||
private val _resourcesLiveData = MutableLiveData<List<Resource>>(emptyList())
|
||||
|
||||
private val actionMutableLiveData = MutableLiveData<ViewAction>()
|
||||
val actionLiveData: LiveData<ViewAction> = actionMutableLiveData
|
||||
val serviceStatusLiveData: MutableLiveData<State>
|
||||
get() = _serviceStatusLiveData
|
||||
val resourcesLiveData: MutableLiveData<List<Resource>>
|
||||
get() = _resourcesLiveData
|
||||
|
||||
private val tunnelListener =
|
||||
object : TunnelListener {
|
||||
override fun onTunnelStateUpdate(state: Tunnel.State) {
|
||||
when (state) {
|
||||
Tunnel.State.Down -> {
|
||||
onDisconnect()
|
||||
}
|
||||
Tunnel.State.Closed -> {
|
||||
onClosed()
|
||||
}
|
||||
else -> {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun clearToken() = repo.clearToken()
|
||||
|
||||
override fun onResourcesUpdate(resources: List<Resource>) {
|
||||
Log.d("TunnelManager", "onUpdateResources: $resources")
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
resources = resources,
|
||||
)
|
||||
}
|
||||
}
|
||||
fun clearActorName() = repo.clearActorName()
|
||||
|
||||
fun connect(context: Context) {
|
||||
viewModelScope.launch {
|
||||
tunnelManager.addListener(tunnelListener)
|
||||
|
||||
val isServiceRunning = TunnelService.isRunning(context)
|
||||
if (!isServiceRunning ||
|
||||
tunnelRepository.getState() == Tunnel.State.Down ||
|
||||
tunnelRepository.getState() == Tunnel.State.Closed
|
||||
) {
|
||||
tunnelManager.connect()
|
||||
} else {
|
||||
_uiState.value =
|
||||
_uiState.value.copy(
|
||||
state = tunnelRepository.getState(),
|
||||
resources = tunnelRepository.getResources(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
tunnelManager.removeListener(tunnelListener)
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
tunnelManager.disconnect()
|
||||
}
|
||||
|
||||
private fun onDisconnect() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
private fun onClosed() {
|
||||
tunnelManager.removeListener(tunnelListener)
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateToSignIn)
|
||||
}
|
||||
|
||||
internal data class UiState(
|
||||
val state: Tunnel.State = Tunnel.State.Down,
|
||||
val resources: List<Resource>? = null,
|
||||
)
|
||||
|
||||
internal sealed class ViewAction {
|
||||
object NavigateToSignIn : ViewAction()
|
||||
|
||||
object ShowError : ViewAction()
|
||||
}
|
||||
fun getActorName() = repo.getActorNameSync()
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.firezone.android.core.domain.preference.GetConfigUseCase
|
||||
import dev.firezone.android.core.domain.preference.SaveSettingsUseCase
|
||||
import dev.firezone.android.core.data.Repository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -32,8 +31,7 @@ import javax.inject.Inject
|
||||
internal class SettingsViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val getConfigUseCase: GetConfigUseCase,
|
||||
private val saveSettingsUseCase: SaveSettingsUseCase,
|
||||
private val repo: Repository,
|
||||
) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(UiState())
|
||||
val uiState: StateFlow<UiState> = _uiState
|
||||
@@ -47,12 +45,12 @@ internal class SettingsViewModel
|
||||
|
||||
fun populateFieldsFromConfig() {
|
||||
viewModelScope.launch {
|
||||
getConfigUseCase().collect {
|
||||
repo.getConfig().collect {
|
||||
actionMutableLiveData.postValue(
|
||||
ViewAction.FillSettings(
|
||||
it.authBaseUrl.orEmpty(),
|
||||
it.apiUrl.orEmpty(),
|
||||
it.logFilter.orEmpty(),
|
||||
it.authBaseUrl,
|
||||
it.apiUrl,
|
||||
it.logFilter,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -73,7 +71,7 @@ internal class SettingsViewModel
|
||||
|
||||
fun onSaveSettingsCompleted() {
|
||||
viewModelScope.launch {
|
||||
saveSettingsUseCase(authBaseUrl, apiUrl, logFilter).collect {
|
||||
repo.saveSettings(authBaseUrl, apiUrl, logFilter).collect {
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateBack)
|
||||
}
|
||||
}
|
||||
@@ -209,7 +207,7 @@ internal class SettingsViewModel
|
||||
)
|
||||
|
||||
internal sealed class ViewAction {
|
||||
object NavigateBack : ViewAction()
|
||||
data object NavigateBack : ViewAction()
|
||||
|
||||
data class FillSettings(
|
||||
val authBaseUrl: String,
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.firezone.android.R
|
||||
@@ -15,7 +14,6 @@ import dev.firezone.android.features.auth.ui.AuthActivity
|
||||
@AndroidEntryPoint
|
||||
internal class SignInFragment : Fragment(R.layout.fragment_sign_in) {
|
||||
private lateinit var binding: FragmentSignInBinding
|
||||
private val viewModel: SignInViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(
|
||||
view: View,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.features.signin.ui
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.firezone.android.core.domain.preference.GetConfigUseCase
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class SignInViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val useCase: GetConfigUseCase,
|
||||
) : ViewModel() {
|
||||
private val actionMutableLiveData = MutableLiveData<SignInViewAction>()
|
||||
val actionLiveData: LiveData<SignInViewAction> = actionMutableLiveData
|
||||
|
||||
internal sealed class SignInViewAction {
|
||||
object NavigateToAuthActivity : SignInViewAction()
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ internal class SplashFragment : Fragment(R.layout.fragment_splash) {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.checkUserState(requireContext())
|
||||
viewModel.checkTunnelState(requireContext())
|
||||
}
|
||||
|
||||
private fun setupActionObservers() {
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
package dev.firezone.android.features.splash.ui
|
||||
|
||||
import android.content.Context
|
||||
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.core.domain.preference.GetConfigUseCase
|
||||
import dev.firezone.android.core.data.Repository
|
||||
import dev.firezone.android.tunnel.TunnelService
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -20,28 +20,28 @@ private const val REQUEST_DELAY = 1000L
|
||||
internal class SplashViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val useCase: GetConfigUseCase,
|
||||
private val repo: Repository,
|
||||
) : ViewModel() {
|
||||
private val actionMutableLiveData = MutableLiveData<ViewAction>()
|
||||
val actionLiveData: LiveData<ViewAction> = actionMutableLiveData
|
||||
|
||||
internal fun checkUserState(context: Context) {
|
||||
internal fun checkTunnelState(context: Context) {
|
||||
viewModelScope.launch {
|
||||
// Stay a while and enjoy the logo
|
||||
delay(REQUEST_DELAY)
|
||||
if (!hasVpnPermissions(context)) {
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateToVpnPermission)
|
||||
} else {
|
||||
useCase.invoke()
|
||||
.catch {
|
||||
Log.e("Error", it.message.toString())
|
||||
}
|
||||
.collect { user ->
|
||||
if (user.token.isNullOrBlank()) {
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateToSignIn)
|
||||
} else {
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateToSession)
|
||||
}
|
||||
repo.getToken().collect {
|
||||
if (it.isNullOrBlank()) {
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateToSignIn)
|
||||
} else {
|
||||
// token will be re-read by the TunnelService
|
||||
if (!TunnelService.isRunning(context)) TunnelService.start(context)
|
||||
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateToSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.features.webview.ui
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.http.SslError
|
||||
import android.os.Bundle
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.firezone.android.R
|
||||
import dev.firezone.android.databinding.ActivityWebViewBinding
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class WebViewActivity : AppCompatActivity(R.layout.activity_web_view) {
|
||||
private lateinit var binding: ActivityWebViewBinding
|
||||
private val viewModel: WebViewViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityWebViewBinding.inflate(layoutInflater)
|
||||
|
||||
setupActionObservers()
|
||||
}
|
||||
|
||||
private fun setupActionObservers() {
|
||||
viewModel.actionLiveData.observe(this) { action ->
|
||||
when (action) {
|
||||
is WebViewViewModel.ViewAction.ShowError -> showError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupWebView(url: String) {
|
||||
binding.webview.apply {
|
||||
settings.apply {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
setSupportMultipleWindows(true)
|
||||
allowFileAccess = true
|
||||
allowContentAccess = true
|
||||
javaScriptCanOpenWindowsAutomatically = true
|
||||
cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
useWideViewPort = true
|
||||
}
|
||||
loadUrl(url)
|
||||
webViewClient = WebViewBrowser()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class WebViewBrowser : WebViewClient() {
|
||||
override fun onPageStarted(
|
||||
view: WebView?,
|
||||
url: String?,
|
||||
favicon: Bitmap?,
|
||||
) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
}
|
||||
|
||||
override fun onPageFinished(
|
||||
view: WebView?,
|
||||
url: String?,
|
||||
) {
|
||||
super.onPageFinished(view, url)
|
||||
}
|
||||
|
||||
override fun onPageCommitVisible(
|
||||
view: WebView?,
|
||||
url: String?,
|
||||
) {
|
||||
super.onPageCommitVisible(view, url)
|
||||
}
|
||||
|
||||
override fun onReceivedSslError(
|
||||
view: WebView?,
|
||||
handler: SslErrorHandler?,
|
||||
error: SslError?,
|
||||
) {
|
||||
super.onReceivedSslError(view, handler, error)
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
error: WebResourceError?,
|
||||
) {
|
||||
showError()
|
||||
super.onReceivedError(view, request, error)
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): Boolean {
|
||||
return super.shouldOverrideUrlLoading(view, request)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError() {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.error_dialog_title)
|
||||
.setMessage(R.string.error_dialog_message)
|
||||
.setPositiveButton(
|
||||
R.string.error_dialog_button_text,
|
||||
) { _, _ ->
|
||||
this@WebViewActivity.finish()
|
||||
}
|
||||
.setIcon(R.drawable.ic_firezone_logo)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.features.webview.ui
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.firezone.android.core.domain.preference.GetConfigUseCase
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class WebViewViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val getConfigUseCase: GetConfigUseCase,
|
||||
) : ViewModel() {
|
||||
private val actionMutableLiveData = MutableLiveData<ViewAction>()
|
||||
val actionLiveData: LiveData<ViewAction> = actionMutableLiveData
|
||||
|
||||
internal sealed class ViewAction {
|
||||
object ShowError : ViewAction()
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.tunnel
|
||||
|
||||
object TunnelSession {
|
||||
object ConnlibSession {
|
||||
external fun connect(
|
||||
apiUrl: String,
|
||||
token: String,
|
||||
@@ -13,5 +13,5 @@ object TunnelSession {
|
||||
callback: Any,
|
||||
): Long
|
||||
|
||||
external fun disconnect(session: Long): Boolean
|
||||
external fun disconnect(connlibSession: Long): Boolean
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.tunnel
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import dev.firezone.android.core.data.PreferenceRepository
|
||||
import dev.firezone.android.tunnel.callback.TunnelListener
|
||||
import dev.firezone.android.tunnel.data.TunnelRepository
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class TunnelManager
|
||||
@Inject
|
||||
constructor(
|
||||
private val appContext: Context,
|
||||
private val tunnelRepository: TunnelRepository,
|
||||
private val preferenceRepository: PreferenceRepository,
|
||||
) {
|
||||
private val listeners: MutableSet<WeakReference<TunnelListener>> = mutableSetOf()
|
||||
|
||||
private val tunnelRepositoryListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
when (key) {
|
||||
TunnelRepository.STATE_KEY -> {
|
||||
listeners.forEach {
|
||||
it.get()?.onTunnelStateUpdate(tunnelRepository.getState())
|
||||
}
|
||||
}
|
||||
TunnelRepository.RESOURCES_KEY -> {
|
||||
listeners.forEach {
|
||||
it.get()?.onResourcesUpdate(tunnelRepository.getResources())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addListener(listener: TunnelListener) {
|
||||
val contains =
|
||||
listeners.any {
|
||||
it.get() == listener
|
||||
}
|
||||
|
||||
if (!contains) {
|
||||
listeners.add(WeakReference(listener))
|
||||
}
|
||||
|
||||
tunnelRepository.addListener(tunnelRepositoryListener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: TunnelListener) {
|
||||
listeners.firstOrNull {
|
||||
it.get() == listener
|
||||
}?.let {
|
||||
it.clear()
|
||||
listeners.remove(it)
|
||||
}
|
||||
|
||||
if (listeners.isEmpty()) {
|
||||
tunnelRepository.removeListener(tunnelRepositoryListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun connect() {
|
||||
startVPNService()
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
stopVPNService()
|
||||
clearSessionData()
|
||||
}
|
||||
|
||||
private fun startVPNService() {
|
||||
val intent = Intent(appContext, TunnelService::class.java)
|
||||
intent.action = TunnelService.ACTION_CONNECT
|
||||
appContext.startService(intent)
|
||||
}
|
||||
|
||||
private fun stopVPNService() {
|
||||
val intent = Intent(appContext, TunnelService::class.java)
|
||||
intent.action = TunnelService.ACTION_DISCONNECT
|
||||
appContext.startService(intent)
|
||||
}
|
||||
|
||||
private fun clearSessionData() {
|
||||
preferenceRepository.clearToken()
|
||||
tunnelRepository.clearAll()
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
private const val TAG: String = "TunnelManager"
|
||||
}
|
||||
}
|
||||
@@ -9,23 +9,23 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.VpnService
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.system.OsConstants
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.firezone.android.R
|
||||
import dev.firezone.android.core.data.PreferenceRepository
|
||||
import dev.firezone.android.core.domain.preference.GetConfigUseCase
|
||||
import dev.firezone.android.core.data.Repository
|
||||
import dev.firezone.android.core.presentation.MainActivity
|
||||
import dev.firezone.android.tunnel.callback.ConnlibCallback
|
||||
import dev.firezone.android.tunnel.data.TunnelRepository
|
||||
import dev.firezone.android.tunnel.model.Cidr
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
import dev.firezone.android.tunnel.model.Tunnel
|
||||
import dev.firezone.android.tunnel.model.TunnelConfig
|
||||
import dev.firezone.android.tunnel.util.DnsServersDetector
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
@@ -36,59 +36,73 @@ import javax.inject.Inject
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class TunnelService : VpnService() {
|
||||
@Inject
|
||||
internal lateinit var getConfigUseCase: GetConfigUseCase
|
||||
|
||||
@Inject
|
||||
internal lateinit var tunnelRepository: TunnelRepository
|
||||
|
||||
@Inject
|
||||
internal lateinit var preferenceRepository: PreferenceRepository
|
||||
internal lateinit var repo: Repository
|
||||
|
||||
@Inject
|
||||
internal lateinit var moshi: Moshi
|
||||
|
||||
private var sessionPtr: Long? = null
|
||||
private var tunnelIpv4Address: String? = null
|
||||
private var tunnelIpv6Address: String? = null
|
||||
private var tunnelDnsAddresses: MutableList<String> = mutableListOf()
|
||||
private var tunnelRoutes: MutableList<Cidr> = mutableListOf()
|
||||
private var connlibSessionPtr: Long? = null
|
||||
private var _tunnelResources: List<Resource> = emptyList()
|
||||
private var _tunnelState: State = State.DOWN
|
||||
|
||||
private var shouldReconnect: Boolean = false
|
||||
var startedByUser: Boolean = false
|
||||
|
||||
private val activeTunnel: Tunnel?
|
||||
get() = tunnelRepository.get()
|
||||
var tunnelResources: List<Resource>
|
||||
get() = _tunnelResources
|
||||
set(value) {
|
||||
_tunnelResources = value
|
||||
updateResourcesLiveData(value)
|
||||
}
|
||||
var tunnelState: State
|
||||
get() = _tunnelState
|
||||
set(value) {
|
||||
_tunnelState = value
|
||||
updateServiceStateLiveData(value)
|
||||
}
|
||||
|
||||
// Used to update the UI when the SessionActivity is bound to this service
|
||||
private var serviceStateLiveData: MutableLiveData<State>? = null
|
||||
private var resourcesLiveData: MutableLiveData<List<Resource>>? = null
|
||||
|
||||
// For binding the SessionActivity view to this service
|
||||
private val binder = LocalBinder()
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): TunnelService = this@TunnelService
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
private val callback: ConnlibCallback =
|
||||
object : ConnlibCallback {
|
||||
override fun onUpdateResources(resourceListJSON: String) {
|
||||
Log.d(TAG, "onUpdateResources: $resourceListJSON")
|
||||
moshi.adapter<List<Resource>>().fromJson(resourceListJSON)?.let { resources ->
|
||||
tunnelRepository.setResources(resources)
|
||||
Firebase.crashlytics.log("onUpdateResources: $resourceListJSON")
|
||||
moshi.adapter<List<Resource>>().fromJson(resourceListJSON)?.let {
|
||||
tunnelResources = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSetInterfaceConfig(
|
||||
tunnelAddressIPv4: String,
|
||||
tunnelAddressIPv6: String,
|
||||
addressIPv4: String,
|
||||
addressIPv6: String,
|
||||
dnsAddresses: String,
|
||||
): Int {
|
||||
Log.d(
|
||||
TAG,
|
||||
"""
|
||||
onSetInterfaceConfig:
|
||||
[IPv4:$tunnelAddressIPv4]
|
||||
[IPv6:$tunnelAddressIPv6]
|
||||
[dns:$dnsAddresses]
|
||||
""".trimIndent(),
|
||||
)
|
||||
Log.d(TAG, "onSetInterfaceConfig: $addressIPv4, $addressIPv6, $dnsAddresses")
|
||||
Firebase.crashlytics.log("onSetInterfaceConfig: $addressIPv4, $addressIPv6, $dnsAddresses")
|
||||
|
||||
moshi.adapter<List<String>>().fromJson(dnsAddresses)?.let { dns ->
|
||||
tunnelRepository.setConfig(
|
||||
TunnelConfig(
|
||||
tunnelAddressIPv4,
|
||||
tunnelAddressIPv6,
|
||||
dns,
|
||||
),
|
||||
)
|
||||
}
|
||||
// init tunnel config
|
||||
tunnelDnsAddresses = moshi.adapter<MutableList<String>>().fromJson(dnsAddresses)!!
|
||||
tunnelIpv4Address = addressIPv4
|
||||
tunnelIpv6Address = addressIPv6
|
||||
|
||||
// TODO: throw error if failed to establish VpnService
|
||||
// start VPN
|
||||
val fd = buildVpnService().establish()?.detachFd() ?: -1
|
||||
protect(fd)
|
||||
return fd
|
||||
@@ -96,9 +110,11 @@ class TunnelService : VpnService() {
|
||||
|
||||
override fun onTunnelReady(): Boolean {
|
||||
Log.d(TAG, "onTunnelReady")
|
||||
Firebase.crashlytics.log("onTunnelReady")
|
||||
|
||||
tunnelRepository.setState(Tunnel.State.Up)
|
||||
tunnelState = State.UP
|
||||
updateStatusNotification("Status: Connected")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -107,8 +123,10 @@ class TunnelService : VpnService() {
|
||||
prefix: Int,
|
||||
): Int {
|
||||
Log.d(TAG, "onAddRoute: $addr/$prefix")
|
||||
Firebase.crashlytics.log("onAddRoute: $addr/$prefix")
|
||||
|
||||
val route = Cidr(addr, prefix)
|
||||
tunnelRepository.addRoute(route)
|
||||
tunnelRoutes.add(route)
|
||||
val fd = buildVpnService().establish()?.detachFd() ?: -1
|
||||
protect(fd)
|
||||
return fd
|
||||
@@ -119,130 +137,153 @@ class TunnelService : VpnService() {
|
||||
prefix: Int,
|
||||
): Int {
|
||||
Log.d(TAG, "onRemoveRoute: $addr/$prefix")
|
||||
Firebase.crashlytics.log("onRemoveRoute: $addr/$prefix")
|
||||
|
||||
val route = Cidr(addr, prefix)
|
||||
tunnelRepository.removeRoute(route)
|
||||
tunnelRoutes.remove(route)
|
||||
val fd = buildVpnService().establish()?.detachFd() ?: -1
|
||||
protect(fd)
|
||||
return fd
|
||||
}
|
||||
|
||||
override fun getSystemDefaultResolvers(): Array<ByteArray> {
|
||||
return DnsServersDetector(this@TunnelService).servers.map { it.address }
|
||||
.toTypedArray()
|
||||
val found = DnsServersDetector(this@TunnelService).servers
|
||||
Log.d(TAG, "getSystemDefaultResolvers: $found")
|
||||
Firebase.crashlytics.log("getSystemDefaultResolvers: $found")
|
||||
|
||||
return found.map {
|
||||
it.address
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
override fun onDisconnect(error: String?): Boolean {
|
||||
onSessionDisconnected(
|
||||
error = error?.takeUnless { it == "null" },
|
||||
)
|
||||
// Something called disconnect() already, so assume it was user or system initiated.
|
||||
override fun onDisconnect(): Boolean {
|
||||
Log.d(TAG, "onDisconnect")
|
||||
Firebase.crashlytics.log("onDisconnect")
|
||||
|
||||
shutdown()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Unexpected disconnect, most likely a 401. Clear the token and initiate a stop of the
|
||||
// service.
|
||||
override fun onDisconnect(error: String): Boolean {
|
||||
Log.d(TAG, "onDisconnect: $error")
|
||||
Firebase.crashlytics.log("onDisconnect: $error")
|
||||
|
||||
// This is a no-op if the token is being read from MDM
|
||||
repo.clearToken()
|
||||
repo.clearActorName()
|
||||
|
||||
shutdown()
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "onCreate")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.d(TAG, "onDestroy")
|
||||
}
|
||||
|
||||
// Primary callback used to start and stop the VPN service
|
||||
// This can be called either from the UI or from the system
|
||||
// via AlwaysOnVpn.
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
): Int {
|
||||
Log.d(TAG, "onStartCommand")
|
||||
|
||||
if (intent != null && ACTION_DISCONNECT == intent.action) {
|
||||
disconnect()
|
||||
return START_NOT_STICKY
|
||||
if (intent?.getBooleanExtra("startedByUser", false) == true) {
|
||||
startedByUser = true
|
||||
}
|
||||
|
||||
connect()
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun onTunnelStateUpdate(state: Tunnel.State) {
|
||||
tunnelRepository.setState(state)
|
||||
// Happens when a user removes the VPN configuration in the System settings
|
||||
override fun onRevoke() {
|
||||
Log.d(TAG, "onRevoke")
|
||||
|
||||
connlibSessionPtr?.let {
|
||||
ConnlibSession.disconnect(it)
|
||||
}
|
||||
}
|
||||
|
||||
// Call this to stop the tunnel and shutdown the service, leaving the token intact.
|
||||
fun disconnect() {
|
||||
Log.d(TAG, "disconnect")
|
||||
|
||||
// Connlib should call onDisconnect() when it's done, with no error.
|
||||
connlibSessionPtr!!.let {
|
||||
ConnlibSession.disconnect(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shutdown() {
|
||||
Log.d(TAG, "shutdown")
|
||||
|
||||
connlibSessionPtr = null
|
||||
stopSelf()
|
||||
tunnelState = State.DOWN
|
||||
}
|
||||
|
||||
private fun connect() {
|
||||
try {
|
||||
val config = getConfigUseCase.sync()
|
||||
val token = repo.getTokenSync()
|
||||
val config = repo.getConfigSync()
|
||||
|
||||
if (tunnelRepository.getState() == Tunnel.State.Up) {
|
||||
shouldReconnect = true
|
||||
disconnect()
|
||||
} else if (config.token != null) {
|
||||
onTunnelStateUpdate(Tunnel.State.Connecting)
|
||||
updateStatusNotification("Status: Connecting...")
|
||||
System.loadLibrary("connlib")
|
||||
if (!token.isNullOrBlank()) {
|
||||
tunnelState = State.CONNECTING
|
||||
updateStatusNotification("Status: Connecting...")
|
||||
System.loadLibrary("connlib")
|
||||
|
||||
sessionPtr =
|
||||
TunnelSession.connect(
|
||||
apiUrl = config.apiUrl,
|
||||
token = config.token,
|
||||
deviceId = deviceId(),
|
||||
deviceName = Build.MODEL,
|
||||
osVersion = Build.VERSION.RELEASE,
|
||||
logDir = getLogDir(),
|
||||
logFilter = config.logFilter,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
Log.e(TAG, "connect(): " + exception.message.toString())
|
||||
connlibSessionPtr =
|
||||
ConnlibSession.connect(
|
||||
apiUrl = config.apiUrl,
|
||||
token = token,
|
||||
deviceId = deviceId(),
|
||||
deviceName = Build.MODEL,
|
||||
osVersion = Build.VERSION.RELEASE,
|
||||
logDir = getLogDir(),
|
||||
logFilter = config.logFilter,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun disconnect() {
|
||||
Log.d(TAG, "disconnect(): Attempting to disconnect session")
|
||||
try {
|
||||
sessionPtr?.let {
|
||||
TunnelSession.disconnect(it)
|
||||
} ?: onSessionDisconnected(null)
|
||||
} catch (exception: Exception) {
|
||||
Log.e(TAG, exception.message.toString())
|
||||
}
|
||||
fun setServiceStateLiveData(liveData: MutableLiveData<State>) {
|
||||
serviceStateLiveData = liveData
|
||||
|
||||
// Update the newly bound SessionActivity with our current state
|
||||
serviceStateLiveData?.postValue(tunnelState)
|
||||
}
|
||||
|
||||
private fun onSessionDisconnected(error: String?) {
|
||||
sessionPtr = null
|
||||
onTunnelStateUpdate(Tunnel.State.Down)
|
||||
fun setResourcesLiveData(liveData: MutableLiveData<List<Resource>>) {
|
||||
resourcesLiveData = liveData
|
||||
|
||||
if (shouldReconnect && error == null) {
|
||||
shouldReconnect = false
|
||||
connect()
|
||||
} else {
|
||||
tunnelRepository.clearAll()
|
||||
preferenceRepository.clearToken()
|
||||
onTunnelStateUpdate(Tunnel.State.Closed)
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
// Update the newly bound SessionActivity with our current resources
|
||||
resourcesLiveData?.postValue(tunnelResources)
|
||||
}
|
||||
|
||||
private fun updateServiceStateLiveData(state: State) {
|
||||
serviceStateLiveData?.postValue(state)
|
||||
}
|
||||
|
||||
private fun updateResourcesLiveData(resources: List<Resource>) {
|
||||
resourcesLiveData?.postValue(resources)
|
||||
}
|
||||
|
||||
private fun deviceId(): String {
|
||||
// Get the deviceId from the preferenceRepository, or save a new UUIDv4 and return that if it doesn't exist
|
||||
val deviceId =
|
||||
preferenceRepository.getDeviceIdSync() ?: run {
|
||||
repo.getDeviceIdSync() ?: run {
|
||||
val newDeviceId = UUID.randomUUID().toString()
|
||||
preferenceRepository.saveDeviceIdSync(newDeviceId)
|
||||
repo.saveDeviceIdSync(newDeviceId)
|
||||
newDeviceId
|
||||
} ?: throw IllegalStateException("Device ID is null")
|
||||
}
|
||||
Log.d(TAG, "Device ID: $deviceId")
|
||||
|
||||
return deviceId
|
||||
}
|
||||
|
||||
private fun getLogDir(): String {
|
||||
// Create log directory if it doesn't exist
|
||||
val logDir = cacheDir.absolutePath + "/logs"
|
||||
Files.createDirectories(Paths.get(logDir))
|
||||
return logDir
|
||||
}
|
||||
|
||||
private fun configIntent(): PendingIntent? {
|
||||
return PendingIntent.getActivity(
|
||||
this,
|
||||
@@ -252,37 +293,51 @@ class TunnelService : VpnService() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildVpnService(): VpnService.Builder =
|
||||
TunnelService().Builder().apply {
|
||||
activeTunnel?.let { tunnel ->
|
||||
allowFamily(OsConstants.AF_INET)
|
||||
allowFamily(OsConstants.AF_INET6)
|
||||
// Allow traffic to bypass the VPN interface when Always-on VPN is enabled.
|
||||
allowBypass()
|
||||
private fun getLogDir(): String {
|
||||
// Create log directory if it doesn't exist
|
||||
val logDir = cacheDir.absolutePath + "/logs"
|
||||
Files.createDirectories(Paths.get(logDir))
|
||||
return logDir
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
setMetered(false) // Inherit the metered status from the underlying networks.
|
||||
}
|
||||
private fun buildVpnService(): VpnService.Builder {
|
||||
return Builder().apply {
|
||||
Firebase.crashlytics.log("Building VPN service")
|
||||
// Allow traffic to bypass the VPN interface when Always-on VPN is enabled.
|
||||
allowBypass()
|
||||
|
||||
setUnderlyingNetworks(null) // Use all available networks.
|
||||
|
||||
addAddress(tunnel.config.tunnelAddressIPv4, 32)
|
||||
addAddress(tunnel.config.tunnelAddressIPv6, 128)
|
||||
|
||||
tunnel.config.dnsAddresses.forEach { dns ->
|
||||
addDnsServer(dns)
|
||||
}
|
||||
|
||||
tunnel.routes.forEach {
|
||||
addRoute(it.address, it.prefix)
|
||||
}
|
||||
|
||||
setSession(SESSION_NAME)
|
||||
|
||||
// TODO: Can we do better?
|
||||
setMtu(DEFAULT_MTU)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
Firebase.crashlytics.log("Setting transport info")
|
||||
setMetered(false) // Inherit the metered status from the underlying networks.
|
||||
}
|
||||
|
||||
Firebase.crashlytics.log("Setting underlying networks")
|
||||
setUnderlyingNetworks(null) // Use all available networks.
|
||||
|
||||
Log.d(TAG, "Routes: $tunnelRoutes")
|
||||
Firebase.crashlytics.log("Routes: $tunnelRoutes")
|
||||
tunnelRoutes.forEach {
|
||||
addRoute(it.address, it.prefix)
|
||||
}
|
||||
|
||||
Log.d(TAG, "DNS Servers: $tunnelDnsAddresses")
|
||||
Firebase.crashlytics.log("DNS Servers: $tunnelDnsAddresses")
|
||||
tunnelDnsAddresses.forEach { dns ->
|
||||
addDnsServer(dns)
|
||||
}
|
||||
|
||||
Log.d(TAG, "IPv4 Address: $tunnelIpv4Address")
|
||||
Firebase.crashlytics.log("IPv4 Address: $tunnelIpv4Address")
|
||||
addAddress(tunnelIpv4Address!!, 32)
|
||||
|
||||
Log.d(TAG, "IPv6 Address: $tunnelIpv6Address")
|
||||
Firebase.crashlytics.log("IPv6 Address: $tunnelIpv6Address")
|
||||
addAddress(tunnelIpv6Address!!, 128)
|
||||
|
||||
setSession(SESSION_NAME)
|
||||
setMtu(MTU)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStatusNotification(message: String?) {
|
||||
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
@@ -293,7 +348,7 @@ class TunnelService : VpnService() {
|
||||
NOTIFICATION_CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
)
|
||||
chan.description = "firezone connection status"
|
||||
chan.description = "Firezone connection status"
|
||||
|
||||
manager.createNotificationChannel(chan)
|
||||
|
||||
@@ -312,8 +367,11 @@ class TunnelService : VpnService() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_CONNECT = "dev.firezone.android.tunnel.CONNECT"
|
||||
const val ACTION_DISCONNECT = "dev.firezone.android.tunnel.DISCONNECT"
|
||||
enum class State {
|
||||
CONNECTING,
|
||||
UP,
|
||||
DOWN,
|
||||
}
|
||||
|
||||
private const val NOTIFICATION_CHANNEL_ID = "firezone-connection-status"
|
||||
private const val NOTIFICATION_CHANNEL_NAME = "firezone-connection-status"
|
||||
@@ -322,8 +380,9 @@ class TunnelService : VpnService() {
|
||||
|
||||
private const val TAG: String = "TunnelService"
|
||||
private const val SESSION_NAME: String = "Firezone Connection"
|
||||
private const val DEFAULT_MTU: Int = 1280
|
||||
private const val MTU: Int = 1280
|
||||
|
||||
// FIXME: Find another way to check if we're running
|
||||
@SuppressWarnings("deprecation")
|
||||
fun isRunning(context: Context): Boolean {
|
||||
val manager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
||||
@@ -332,7 +391,15 @@ class TunnelService : VpnService() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun start(context: Context) {
|
||||
Log.d(TAG, "Starting TunnelService")
|
||||
val intent = Intent(context, TunnelService::class.java)
|
||||
intent.putExtra("startedByUser", true)
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package dev.firezone.android.tunnel.callback
|
||||
|
||||
interface ConnlibCallback {
|
||||
fun onSetInterfaceConfig(
|
||||
tunnelAddressIPv4: String,
|
||||
tunnelAddressIPv6: String,
|
||||
addressIPv4: String,
|
||||
addressIPv6: String,
|
||||
dnsAddresses: String,
|
||||
): Int
|
||||
|
||||
@@ -22,7 +22,10 @@ interface ConnlibCallback {
|
||||
|
||||
fun onUpdateResources(resourceListJSON: String)
|
||||
|
||||
fun onDisconnect(error: String?): Boolean
|
||||
// The JNI doesn't support nullable types, so we need two method signatures
|
||||
fun onDisconnect(error: String): Boolean
|
||||
|
||||
fun onDisconnect(): Boolean
|
||||
|
||||
fun getSystemDefaultResolvers(): Array<ByteArray>
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.tunnel.callback
|
||||
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
import dev.firezone.android.tunnel.model.Tunnel
|
||||
|
||||
interface TunnelListener {
|
||||
fun onTunnelStateUpdate(state: Tunnel.State)
|
||||
|
||||
fun onResourcesUpdate(resources: List<Resource>)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.tunnel.data
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import dev.firezone.android.tunnel.model.Cidr
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
import dev.firezone.android.tunnel.model.Tunnel
|
||||
import dev.firezone.android.tunnel.model.TunnelConfig
|
||||
|
||||
interface TunnelRepository {
|
||||
fun get(): Tunnel?
|
||||
|
||||
fun setConfig(config: TunnelConfig)
|
||||
|
||||
fun getConfig(): TunnelConfig?
|
||||
|
||||
fun setState(state: Tunnel.State)
|
||||
|
||||
fun getState(): Tunnel.State
|
||||
|
||||
fun setResources(resources: List<Resource>)
|
||||
|
||||
fun getResources(): List<Resource>
|
||||
|
||||
fun addRoute(route: Cidr)
|
||||
|
||||
fun removeRoute(route: Cidr)
|
||||
|
||||
fun getRoutes(): List<Cidr>
|
||||
|
||||
fun clearAll()
|
||||
|
||||
fun addListener(callback: SharedPreferences.OnSharedPreferenceChangeListener)
|
||||
|
||||
fun removeListener(callback: SharedPreferences.OnSharedPreferenceChangeListener)
|
||||
|
||||
companion object {
|
||||
const val TAG = "TunnelRepository"
|
||||
const val CONFIG_KEY = "tunnelConfigKey"
|
||||
const val STATE_KEY = "tunnelStateKey"
|
||||
const val RESOURCES_KEY = "tunnelResourcesKey"
|
||||
const val ROUTES_KEY = "tunnelRoutesKey"
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.tunnel.data
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import dev.firezone.android.tunnel.data.TunnelRepository.Companion.CONFIG_KEY
|
||||
import dev.firezone.android.tunnel.data.TunnelRepository.Companion.RESOURCES_KEY
|
||||
import dev.firezone.android.tunnel.data.TunnelRepository.Companion.ROUTES_KEY
|
||||
import dev.firezone.android.tunnel.data.TunnelRepository.Companion.STATE_KEY
|
||||
import dev.firezone.android.tunnel.model.Cidr
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
import dev.firezone.android.tunnel.model.Tunnel
|
||||
import dev.firezone.android.tunnel.model.TunnelConfig
|
||||
import java.lang.Exception
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class TunnelRepositoryImpl
|
||||
@Inject
|
||||
constructor(
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
private val moshi: Moshi,
|
||||
) : TunnelRepository {
|
||||
private val lock = Any()
|
||||
|
||||
override fun addListener(callback: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(callback)
|
||||
}
|
||||
|
||||
override fun removeListener(callback: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(callback)
|
||||
}
|
||||
|
||||
override fun get(): Tunnel? =
|
||||
synchronized(lock) {
|
||||
return try {
|
||||
Tunnel(
|
||||
config = requireNotNull(getConfig()),
|
||||
state = getState(),
|
||||
routes = getRoutes(),
|
||||
resources = getResources(),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun setConfig(config: TunnelConfig) {
|
||||
synchronized(lock) {
|
||||
val json = moshi.adapter<TunnelConfig>().toJson(config)
|
||||
sharedPreferences.edit().putString(CONFIG_KEY, json).apply()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getConfig(): TunnelConfig? =
|
||||
synchronized(lock) {
|
||||
val json = sharedPreferences.getString(CONFIG_KEY, "{}") ?: "{}"
|
||||
return moshi.adapter<TunnelConfig>().fromJson(json)
|
||||
}
|
||||
|
||||
override fun setState(state: Tunnel.State) {
|
||||
synchronized(lock) {
|
||||
sharedPreferences.edit().putString(STATE_KEY, state.name).apply()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getState(): Tunnel.State {
|
||||
val json = sharedPreferences.getString(STATE_KEY, null)
|
||||
return json?.let { Tunnel.State.valueOf(it) } ?: Tunnel.State.Closed
|
||||
}
|
||||
|
||||
override fun setResources(resources: List<Resource>) {
|
||||
synchronized(lock) {
|
||||
val json = moshi.adapter<List<Resource>>().toJson(resources)
|
||||
sharedPreferences.edit().putString(RESOURCES_KEY, json).apply()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getResources(): List<Resource> {
|
||||
synchronized(lock) {
|
||||
val json = sharedPreferences.getString(RESOURCES_KEY, "[]") ?: "[]"
|
||||
return moshi.adapter<List<Resource>>().fromJson(json) ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun addRoute(route: Cidr) {
|
||||
synchronized(lock) {
|
||||
getRoutes().toMutableList().run {
|
||||
add(route)
|
||||
val json = moshi.adapter<List<Cidr>>().toJson(this)
|
||||
sharedPreferences.edit().putString(ROUTES_KEY, json).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeRoute(route: Cidr) {
|
||||
synchronized(lock) {
|
||||
getRoutes().toMutableList().run {
|
||||
remove(route)
|
||||
val json = moshi.adapter<List<Cidr>>().toJson(this)
|
||||
sharedPreferences.edit().putString(ROUTES_KEY, json).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRoutes(): List<Cidr> =
|
||||
synchronized(lock) {
|
||||
val json = sharedPreferences.getString(ROUTES_KEY, "[]") ?: "[]"
|
||||
return moshi.adapter<List<Cidr>>().fromJson(json) ?: emptyList()
|
||||
}
|
||||
|
||||
override fun clearAll() {
|
||||
synchronized(lock) {
|
||||
sharedPreferences.edit().clear().apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.tunnel.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
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.data.PreferenceRepository
|
||||
import dev.firezone.android.tunnel.TunnelManager
|
||||
import dev.firezone.android.tunnel.data.TunnelRepository
|
||||
import dev.firezone.android.tunnel.data.TunnelRepositoryImpl
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
internal const val TUNNEL_ENCRYPTED_SHARED_PREFERENCES = "tunnelEncryptedSharedPreferences"
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object TunnelModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
internal fun provideTunnelRepository(
|
||||
@Named(TunnelRepository.TAG) sharedPreferences: SharedPreferences,
|
||||
moshi: Moshi,
|
||||
): TunnelRepository = TunnelRepositoryImpl(sharedPreferences, moshi)
|
||||
|
||||
@Provides
|
||||
@Named(TunnelRepository.TAG)
|
||||
internal fun provideTunnelEncryptedSharedPreferences(app: Application): SharedPreferences =
|
||||
EncryptedSharedPreferences.create(
|
||||
app.applicationContext,
|
||||
TUNNEL_ENCRYPTED_SHARED_PREFERENCES,
|
||||
MasterKey.Builder(app.applicationContext)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build(),
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||
)
|
||||
|
||||
@Provides
|
||||
internal fun provideTunnelManager(
|
||||
@ApplicationContext appContext: Context,
|
||||
tunnelRepository: TunnelRepository,
|
||||
preferenceRepository: PreferenceRepository,
|
||||
): TunnelManager = TunnelManager(appContext, tunnelRepository, preferenceRepository)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.tunnel.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class Tunnel(
|
||||
val config: TunnelConfig = TunnelConfig(),
|
||||
var state: State = State.Down,
|
||||
val routes: List<Cidr> = emptyList(),
|
||||
val resources: List<Resource> = emptyList(),
|
||||
) : Parcelable {
|
||||
enum class State {
|
||||
Connecting,
|
||||
Up,
|
||||
Down,
|
||||
Closed,
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
|
||||
package dev.firezone.android.tunnel.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class TunnelConfig(
|
||||
val tunnelAddressIPv4: String = "",
|
||||
val tunnelAddressIPv6: String = "",
|
||||
val dnsAddresses: List<String> = emptyList(),
|
||||
val dnsFallbackStrategy: String = "",
|
||||
) : Parcelable
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
@@ -56,27 +57,27 @@
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="@dimen/spacing_2x"
|
||||
android:layout_marginBottom="@dimen/spacing_2x"
|
||||
app:layout_constraintBottom_toTopOf="@id/btSignOut"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvResourcesList"
|
||||
app:layout_constraintBottom_toTopOf="@id/btSignOut" />
|
||||
tools:layout_editor_absoluteX="16dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btSignOut"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sign_out"
|
||||
android:layout_marginBottom="@dimen/spacing_2x"
|
||||
app:layout_constraintBottom_toTopOf="@id/btSettings"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btSettings"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/settings"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvActorName"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginTop="26dp"
|
||||
android:textAlignment="textEnd"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/llContainer" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -416,7 +416,7 @@ fn connect(
|
||||
/// fd must be a valid file descriptor
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_TunnelSession_connect(
|
||||
pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_ConnlibSession_connect(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
api_url: JString,
|
||||
@@ -462,7 +462,7 @@ pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_TunnelSession_con
|
||||
/// Pointers must be valid
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_TunnelSession_disconnect(
|
||||
pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_ConnlibSession_disconnect(
|
||||
mut env: JNIEnv,
|
||||
_: JClass,
|
||||
session: *mut Session<CallbackHandler>,
|
||||
|
||||
Reference in New Issue
Block a user