mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 02:18:47 +00:00
feat(android): Managed configuration (#9227)
Adds managed configuration support for Android in line with other platforms. Related: #4505 Related: #9203 Related: #9196
This commit is contained in:
3
kotlin/android/.gitignore
vendored
3
kotlin/android/.gitignore
vendored
@@ -106,6 +106,9 @@ proguard/
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
|
||||
# Build files
|
||||
.kotlin/
|
||||
|
||||
# NDK
|
||||
obj/
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ android {
|
||||
isDebuggable = true
|
||||
resValue("string", "app_name", "\"Firezone (Dev)\"")
|
||||
|
||||
buildConfigField("String", "AUTH_BASE_URL", "\"https://app.firez.one\"")
|
||||
buildConfigField("String", "AUTH_URL", "\"https://app.firez.one\"")
|
||||
buildConfigField("String", "API_URL", "\"wss://api.firez.one\"")
|
||||
buildConfigField(
|
||||
"String",
|
||||
@@ -120,7 +120,7 @@ android {
|
||||
|
||||
resValue("string", "app_name", "\"Firezone\"")
|
||||
|
||||
buildConfigField("String", "AUTH_BASE_URL", "\"https://app.firezone.dev\"")
|
||||
buildConfigField("String", "AUTH_URL", "\"https://app.firezone.dev\"")
|
||||
buildConfigField("String", "API_URL", "\"wss://api.firezone.dev\"")
|
||||
buildConfigField("String", "LOG_FILTER", "\"info\"")
|
||||
firebaseAppDistribution {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -31,6 +32,15 @@
|
||||
android:theme="@style/AppTheme.Base"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<receiver
|
||||
android:name=".core.BootReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".core.presentation.MainActivity"
|
||||
android:exported="true">
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/* Licensed under Apache 2.0 (C) 2025 Firezone, Inc. */
|
||||
package dev.firezone.android.core
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.firezone.android.core.data.Repository
|
||||
import dev.firezone.android.core.di.ApplicationScope
|
||||
import dev.firezone.android.tunnel.TunnelService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var repo: Repository
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
override fun onReceive(
|
||||
context: Context,
|
||||
intent: Intent,
|
||||
) {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
applicationScope.launch(Dispatchers.IO) {
|
||||
val userConfig = repo.getConfigSync()
|
||||
if (userConfig.startOnLogin) {
|
||||
val serviceIntent = Intent(context, TunnelService::class.java)
|
||||
context.startService(serviceIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,12 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
internal class BaseUrlInterceptor(
|
||||
internal class UrlInterceptor(
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
) : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
val newUrl = BuildConfig.AUTH_BASE_URL.toHttpUrlOrNull()
|
||||
val newUrl = BuildConfig.AUTH_URL.toHttpUrlOrNull()
|
||||
|
||||
val newRequest =
|
||||
originalRequest
|
||||
@@ -3,11 +3,13 @@ package dev.firezone.android.core.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import dev.firezone.android.BuildConfig
|
||||
import dev.firezone.android.core.data.model.Config
|
||||
import dev.firezone.android.core.data.model.ManagedConfigStatus
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -53,7 +55,7 @@ class Favorites(
|
||||
val inner: HashSet<String>,
|
||||
)
|
||||
|
||||
internal class Repository
|
||||
class Repository
|
||||
@Inject
|
||||
constructor(
|
||||
private val context: Context,
|
||||
@@ -68,14 +70,50 @@ internal class Repository
|
||||
val favorites = _favorites.asStateFlow()
|
||||
|
||||
fun getConfigSync(): Config =
|
||||
Config(
|
||||
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,
|
||||
)
|
||||
{
|
||||
val authURL =
|
||||
sharedPreferences.getString(MANAGED_AUTH_URL_KEY, null)
|
||||
?: sharedPreferences.getString(AUTH_URL_KEY, null)
|
||||
?: BuildConfig.AUTH_URL
|
||||
|
||||
val apiURL =
|
||||
sharedPreferences.getString(MANAGED_API_URL_KEY, null)
|
||||
?: sharedPreferences.getString(API_URL_KEY, null)
|
||||
?: BuildConfig.API_URL
|
||||
|
||||
val logFilter =
|
||||
sharedPreferences.getString(MANAGED_LOG_FILTER_KEY, null)
|
||||
?: sharedPreferences.getString(LOG_FILTER_KEY, null)
|
||||
?: BuildConfig.LOG_FILTER
|
||||
|
||||
val accountSlug =
|
||||
sharedPreferences.getString(MANAGED_ACCOUNT_SLUG_KEY, null)
|
||||
?: sharedPreferences.getString(ACCOUNT_SLUG_KEY, null)
|
||||
?: ""
|
||||
|
||||
val startOnLogin =
|
||||
if (sharedPreferences.contains(MANAGED_START_ON_LOGIN_KEY)) {
|
||||
sharedPreferences.getBoolean(MANAGED_START_ON_LOGIN_KEY, false)
|
||||
} else {
|
||||
sharedPreferences.getBoolean(START_ON_LOGIN_KEY, false)
|
||||
}
|
||||
|
||||
val connectOnStart =
|
||||
if (sharedPreferences.contains(MANAGED_CONNECT_ON_START_KEY)) {
|
||||
sharedPreferences.getBoolean(MANAGED_CONNECT_ON_START_KEY, false)
|
||||
} else {
|
||||
sharedPreferences.getBoolean(CONNECT_ON_START_KEY, false)
|
||||
}
|
||||
|
||||
Config(
|
||||
authUrl = authURL,
|
||||
apiUrl = apiURL,
|
||||
logFilter = logFilter,
|
||||
accountSlug = accountSlug,
|
||||
startOnLogin = startOnLogin,
|
||||
connectOnStart = connectOnStart,
|
||||
)
|
||||
}()
|
||||
|
||||
fun getConfig(): Flow<Config> =
|
||||
flow {
|
||||
@@ -84,9 +122,12 @@ internal class Repository
|
||||
|
||||
fun getDefaultConfigSync(): Config =
|
||||
Config(
|
||||
BuildConfig.AUTH_BASE_URL,
|
||||
BuildConfig.AUTH_URL,
|
||||
BuildConfig.API_URL,
|
||||
BuildConfig.LOG_FILTER,
|
||||
accountSlug = "",
|
||||
startOnLogin = false,
|
||||
connectOnStart = false,
|
||||
)
|
||||
|
||||
fun getDefaultConfig(): Flow<Config> =
|
||||
@@ -99,13 +140,44 @@ internal class Repository
|
||||
emit(
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(AUTH_BASE_URL_KEY, value.authBaseUrl)
|
||||
.putString(AUTH_URL_KEY, value.authUrl)
|
||||
.putString(API_URL_KEY, value.apiUrl)
|
||||
.putString(LOG_FILTER_KEY, value.logFilter)
|
||||
.putString(ACCOUNT_SLUG_KEY, value.accountSlug)
|
||||
.putBoolean(START_ON_LOGIN_KEY, value.startOnLogin)
|
||||
.putBoolean(CONNECT_ON_START_KEY, value.connectOnStart)
|
||||
.apply(),
|
||||
)
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
// TODO: Consider adding support for the legacy managed configuration keys like token,
|
||||
// allowedApplications, etc from pilot customer.
|
||||
fun saveManagedConfiguration(bundle: Bundle): Flow<Unit> =
|
||||
flow {
|
||||
val editor = sharedPreferences.edit()
|
||||
|
||||
if (bundle.containsKey(AUTH_URL_KEY)) {
|
||||
editor.putString(MANAGED_AUTH_URL_KEY, bundle.getString(AUTH_URL_KEY))
|
||||
}
|
||||
if (bundle.containsKey(API_URL_KEY)) {
|
||||
editor.putString(MANAGED_API_URL_KEY, bundle.getString(API_URL_KEY))
|
||||
}
|
||||
if (bundle.containsKey(LOG_FILTER_KEY)) {
|
||||
editor.putString(MANAGED_LOG_FILTER_KEY, bundle.getString(LOG_FILTER_KEY))
|
||||
}
|
||||
if (bundle.containsKey(ACCOUNT_SLUG_KEY)) {
|
||||
editor.putString(MANAGED_ACCOUNT_SLUG_KEY, bundle.getString(ACCOUNT_SLUG_KEY))
|
||||
}
|
||||
if (bundle.containsKey(START_ON_LOGIN_KEY)) {
|
||||
editor.putBoolean(MANAGED_START_ON_LOGIN_KEY, bundle.getBoolean(START_ON_LOGIN_KEY, false))
|
||||
}
|
||||
if (bundle.containsKey(CONNECT_ON_START_KEY)) {
|
||||
editor.putBoolean(MANAGED_CONNECT_ON_START_KEY, bundle.getBoolean(CONNECT_ON_START_KEY, false))
|
||||
}
|
||||
|
||||
emit(editor.apply())
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
fun getDeviceIdSync(): String? = sharedPreferences.getString(DEVICE_ID_KEY, null)
|
||||
|
||||
private fun saveFavoritesSync() {
|
||||
@@ -137,6 +209,11 @@ internal class Repository
|
||||
|
||||
fun getStateSync(): String? = sharedPreferences.getString(STATE_KEY, null)
|
||||
|
||||
fun getAccountSlug(): Flow<String?> =
|
||||
flow {
|
||||
emit(sharedPreferences.getString(ACCOUNT_SLUG_KEY, null))
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
fun getActorName(): Flow<String?> =
|
||||
flow {
|
||||
emit(getActorNameSync())
|
||||
@@ -149,6 +226,16 @@ internal class Repository
|
||||
|
||||
fun getNonceSync(): String? = sharedPreferences.getString(NONCE_KEY, null)
|
||||
|
||||
fun saveAccountSlug(value: String): Flow<Unit> =
|
||||
flow {
|
||||
emit(
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(ACCOUNT_SLUG_KEY, value)
|
||||
.apply(),
|
||||
)
|
||||
}.flowOn(coroutineDispatcher)
|
||||
|
||||
fun saveDeviceIdSync(value: String): Unit =
|
||||
sharedPreferences
|
||||
.edit()
|
||||
@@ -236,12 +323,43 @@ internal class Repository
|
||||
}
|
||||
}
|
||||
|
||||
fun getManagedStatus(): ManagedConfigStatus =
|
||||
ManagedConfigStatus(
|
||||
isAuthUrlManaged = isAuthUrlManaged(),
|
||||
isApiUrlManaged = isApiUrlManaged(),
|
||||
isLogFilterManaged = isLogFilterManaged(),
|
||||
isAccountSlugManaged = isAccountSlugManaged(),
|
||||
isStartOnLoginManaged = isStartOnLoginManaged(),
|
||||
isConnectOnStartManaged = isConnectOnStartManaged(),
|
||||
)
|
||||
|
||||
fun isAuthUrlManaged(): Boolean = sharedPreferences.contains(MANAGED_AUTH_URL_KEY)
|
||||
|
||||
fun isApiUrlManaged(): Boolean = sharedPreferences.contains(MANAGED_API_URL_KEY)
|
||||
|
||||
fun isLogFilterManaged(): Boolean = sharedPreferences.contains(MANAGED_LOG_FILTER_KEY)
|
||||
|
||||
fun isAccountSlugManaged(): Boolean = sharedPreferences.contains(MANAGED_ACCOUNT_SLUG_KEY)
|
||||
|
||||
fun isStartOnLoginManaged(): Boolean = sharedPreferences.contains(MANAGED_START_ON_LOGIN_KEY)
|
||||
|
||||
fun isConnectOnStartManaged(): Boolean = sharedPreferences.contains(MANAGED_CONNECT_ON_START_KEY)
|
||||
|
||||
companion object {
|
||||
private const val AUTH_BASE_URL_KEY = "authBaseUrl"
|
||||
private const val AUTH_URL_KEY = "authUrl"
|
||||
private const val ACTOR_NAME_KEY = "actorName"
|
||||
private const val API_URL_KEY = "apiUrl"
|
||||
private const val FAVORITE_RESOURCES_KEY = "favoriteResources"
|
||||
private const val LOG_FILTER_KEY = "logFilter"
|
||||
private const val ACCOUNT_SLUG_KEY = "accountSlug"
|
||||
private const val START_ON_LOGIN_KEY = "startOnLogin"
|
||||
private const val CONNECT_ON_START_KEY = "connectOnStart"
|
||||
private const val MANAGED_AUTH_URL_KEY = "managedAuthUrl"
|
||||
private const val MANAGED_API_URL_KEY = "managedApiUrl"
|
||||
private const val MANAGED_LOG_FILTER_KEY = "managedLogFilter"
|
||||
private const val MANAGED_ACCOUNT_SLUG_KEY = "managedAccountSlug"
|
||||
private const val MANAGED_START_ON_LOGIN_KEY = "managedStartOnLogin"
|
||||
private const val MANAGED_CONNECT_ON_START_KEY = "managedConnectOnStart"
|
||||
private const val TOKEN_KEY = "token"
|
||||
private const val NONCE_KEY = "nonce"
|
||||
private const val STATE_KEY = "state"
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
package dev.firezone.android.core.data.model
|
||||
|
||||
data class Config(
|
||||
var authBaseUrl: String,
|
||||
var authUrl: String,
|
||||
var apiUrl: String,
|
||||
var logFilter: String,
|
||||
var accountSlug: String,
|
||||
var startOnLogin: Boolean,
|
||||
var connectOnStart: Boolean,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/* Licensed under Apache 2.0 (C) 2024 Firezone, Inc. */
|
||||
package dev.firezone.android.core.data.model
|
||||
|
||||
data class ManagedConfigStatus(
|
||||
val isAuthUrlManaged: Boolean,
|
||||
val isApiUrlManaged: Boolean,
|
||||
val isLogFilterManaged: Boolean,
|
||||
val isAccountSlugManaged: Boolean,
|
||||
val isStartOnLoginManaged: Boolean,
|
||||
val isConnectOnStartManaged: Boolean,
|
||||
)
|
||||
@@ -8,7 +8,7 @@ import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.firezone.android.core.BaseUrlInterceptor
|
||||
import dev.firezone.android.core.UrlInterceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
@@ -23,7 +23,7 @@ private const val NETWORK_TIMEOUT = 30L
|
||||
internal object NetworkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBaseUrlInterceptor(sharedPreferences: SharedPreferences): BaseUrlInterceptor = BaseUrlInterceptor(sharedPreferences)
|
||||
fun provideBaseUrlInterceptor(sharedPreferences: SharedPreferences): UrlInterceptor = UrlInterceptor(sharedPreferences)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@@ -35,7 +35,7 @@ internal object NetworkModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideOkHttpClient(
|
||||
baseUrlInterceptor: BaseUrlInterceptor,
|
||||
urlInterceptor: UrlInterceptor,
|
||||
loggingInterceptor: HttpLoggingInterceptor,
|
||||
) = OkHttpClient
|
||||
.Builder()
|
||||
@@ -44,7 +44,7 @@ internal object NetworkModule {
|
||||
.readTimeout(NETWORK_TIMEOUT, TimeUnit.SECONDS)
|
||||
.writeTimeout(NETWORK_TIMEOUT, TimeUnit.SECONDS)
|
||||
.connectTimeout(NETWORK_TIMEOUT, TimeUnit.SECONDS)
|
||||
.addInterceptor(baseUrlInterceptor)
|
||||
.addInterceptor(urlInterceptor)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build()
|
||||
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
/* Licensed under Apache 2.0 (C) 2024 Firezone, Inc. */
|
||||
package dev.firezone.android.core.presentation
|
||||
|
||||
import android.content.Context
|
||||
import android.content.RestrictionsManager
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope // For launching coroutines
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.firezone.android.R
|
||||
import dev.firezone.android.core.data.Repository
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class MainActivity : AppCompatActivity(R.layout.activity_main)
|
||||
internal class MainActivity : AppCompatActivity(R.layout.activity_main) {
|
||||
@Inject
|
||||
lateinit var repository: Repository
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Apply managed configurations when the app resumes since it's not guaranteed
|
||||
// the TunnelService is running when the app starts or is backgrounded.
|
||||
applyManagedConfigurations()
|
||||
}
|
||||
|
||||
private fun applyManagedConfigurations() {
|
||||
val restrictionsManager = getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager
|
||||
val appRestrictions: Bundle = restrictionsManager.applicationRestrictions
|
||||
lifecycleScope.launch {
|
||||
repository.saveManagedConfiguration(appRestrictions).collect {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ internal class AuthViewModel
|
||||
ViewAction.NavigateToSignIn
|
||||
} else {
|
||||
authFlowLaunched = true
|
||||
ViewAction.LaunchAuthFlow("${config.authBaseUrl}?state=$state&nonce=$nonce&as=client")
|
||||
ViewAction.LaunchAuthFlow("${config.authUrl}/${config.accountSlug}?state=$state&nonce=$nonce&as=client")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ internal class CustomUriViewModel
|
||||
|
||||
when (intent.data?.host) {
|
||||
PATH_CALLBACK -> {
|
||||
intent.data?.getQueryParameter(QUERY_ACCOUNT_SLUG)?.let { accountSlug ->
|
||||
repo.saveAccountSlug(accountSlug).collect()
|
||||
}
|
||||
intent.data?.getQueryParameter(QUERY_ACTOR_NAME)?.let { actorName ->
|
||||
repo.saveActorName(actorName).collect()
|
||||
}
|
||||
@@ -68,6 +71,7 @@ internal class CustomUriViewModel
|
||||
|
||||
companion object {
|
||||
private const val PATH_CALLBACK = "handle_client_sign_in_callback"
|
||||
private const val QUERY_ACCOUNT_SLUG = "account_slug"
|
||||
private const val QUERY_CLIENT_STATE = "state"
|
||||
private const val QUERY_CLIENT_AUTH_FRAGMENT = "fragment"
|
||||
private const val QUERY_ACTOR_NAME = "actor_name"
|
||||
|
||||
@@ -4,10 +4,13 @@ package dev.firezone.android.features.settings.ui
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import dev.firezone.android.R
|
||||
import dev.firezone.android.core.data.model.ManagedConfigStatus
|
||||
import dev.firezone.android.databinding.FragmentSettingsAdvancedBinding
|
||||
|
||||
class AdvancedSettingsFragment : Fragment(R.layout.fragment_settings_advanced) {
|
||||
@@ -30,11 +33,11 @@ class AdvancedSettingsFragment : Fragment(R.layout.fragment_settings_advanced) {
|
||||
|
||||
private fun setupViews() {
|
||||
binding.apply {
|
||||
etAuthBaseUrlInput.apply {
|
||||
etAuthUrlInput.apply {
|
||||
imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
setOnClickListener { isCursorVisible = true }
|
||||
doOnTextChanged { text, _, _, _ ->
|
||||
viewModel.onValidateAuthBaseUrl(text.toString())
|
||||
viewModel.onValidateAuthUrl(text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,20 +70,74 @@ class AdvancedSettingsFragment : Fragment(R.layout.fragment_settings_advanced) {
|
||||
requireActivity().finish()
|
||||
|
||||
is SettingsViewModel.ViewAction.FillSettings -> {
|
||||
binding.etAuthBaseUrlInput.apply {
|
||||
setText(action.config.authBaseUrl)
|
||||
binding.etAuthUrlInput.apply {
|
||||
setText(action.config.authUrl)
|
||||
}
|
||||
|
||||
binding.etApiUrlInput.apply {
|
||||
setText(action.config.apiUrl)
|
||||
}
|
||||
|
||||
binding.etLogFilterInput.apply {
|
||||
setText(action.config.logFilter)
|
||||
}
|
||||
|
||||
applyManagedStatus(action.managedStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyManagedStatus(status: ManagedConfigStatus) {
|
||||
binding.apply {
|
||||
val tooltipMessage = getString(R.string.managed_setting_info_description)
|
||||
|
||||
etAuthUrlInput.isEnabled = !status.isAuthUrlManaged
|
||||
etAuthUrlInput.isFocusable = !status.isAuthUrlManaged
|
||||
etAuthUrlInput.isClickable = !status.isAuthUrlManaged
|
||||
ilAuthUrlInput.isEnabled = !status.isAuthUrlManaged
|
||||
ilAuthUrlInput.isFocusable = !status.isAuthUrlManaged
|
||||
ilAuthUrlInput.isClickable = !status.isAuthUrlManaged
|
||||
setupInfoIcon(ivAuthUrlInfo, status.isAuthUrlManaged, tooltipMessage)
|
||||
|
||||
etApiUrlInput.isEnabled = !status.isApiUrlManaged
|
||||
etApiUrlInput.isFocusable = !status.isApiUrlManaged
|
||||
etApiUrlInput.isClickable = !status.isApiUrlManaged
|
||||
ilApiUrlInput.isEnabled = !status.isApiUrlManaged
|
||||
ilApiUrlInput.isFocusable = !status.isApiUrlManaged
|
||||
ilApiUrlInput.isClickable = !status.isApiUrlManaged
|
||||
setupInfoIcon(ivApiUrlInfo, status.isApiUrlManaged, tooltipMessage)
|
||||
|
||||
etLogFilterInput.isEnabled = !status.isLogFilterManaged
|
||||
etLogFilterInput.isFocusable = !status.isLogFilterManaged
|
||||
etLogFilterInput.isClickable = !status.isLogFilterManaged
|
||||
ilLogFilterInput.isEnabled = !status.isLogFilterManaged
|
||||
ilLogFilterInput.isFocusable = !status.isLogFilterManaged
|
||||
ilLogFilterInput.isClickable = !status.isLogFilterManaged
|
||||
setupInfoIcon(ivLogFilterInfo, status.isLogFilterManaged, tooltipMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupInfoIcon(
|
||||
infoIconView: View,
|
||||
isManaged: Boolean,
|
||||
tooltipMessage: String,
|
||||
) {
|
||||
if (isManaged) {
|
||||
infoIconView.visibility = View.VISIBLE
|
||||
TooltipCompat.setTooltipText(infoIconView, tooltipMessage)
|
||||
|
||||
infoIconView.setOnClickListener { v ->
|
||||
Toast.makeText(v.context, tooltipMessage, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
infoIconView.visibility = View.GONE
|
||||
TooltipCompat.setTooltipText(infoIconView, null)
|
||||
infoIconView.setOnClickListener(null)
|
||||
infoIconView.setOnLongClickListener(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/* Licensed under Apache 2.0 (C) 2024 Firezone, Inc. */
|
||||
package dev.firezone.android.features.settings.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import dev.firezone.android.R
|
||||
import dev.firezone.android.core.data.model.ManagedConfigStatus
|
||||
import dev.firezone.android.databinding.FragmentSettingsGeneralBinding
|
||||
|
||||
class GeneralSettingsFragment : Fragment(R.layout.fragment_settings_general) {
|
||||
private var _binding: FragmentSettingsGeneralBinding? = null
|
||||
|
||||
val binding get() = _binding!!
|
||||
|
||||
private val viewModel: SettingsViewModel by activityViewModels()
|
||||
|
||||
override fun onViewCreated(
|
||||
view: View,
|
||||
savedInstanceState: Bundle?,
|
||||
) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
_binding = FragmentSettingsGeneralBinding.bind(view)
|
||||
|
||||
setupViews()
|
||||
setupActionObservers()
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
binding.apply {
|
||||
etAccountSlugInput.apply {
|
||||
imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
setOnClickListener { isCursorVisible = true }
|
||||
doOnTextChanged { text, _, _, _ ->
|
||||
viewModel.onValidateAccountSlug(text.toString())
|
||||
}
|
||||
}
|
||||
switchStartOnLogin.apply {
|
||||
setOnCheckedChangeListener { _, isChecked ->
|
||||
viewModel.onStartOnLoginChanged(isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
switchConnectOnStart.apply {
|
||||
setOnCheckedChangeListener { _, isChecked ->
|
||||
viewModel.onConnectOnStartChanged(isChecked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupActionObservers() {
|
||||
viewModel.actionLiveData.observe(viewLifecycleOwner) { action ->
|
||||
when (action) {
|
||||
is SettingsViewModel.ViewAction.NavigateBack ->
|
||||
requireActivity().finish()
|
||||
|
||||
is SettingsViewModel.ViewAction.FillSettings -> {
|
||||
binding.etAccountSlugInput.apply {
|
||||
setText(action.config.accountSlug)
|
||||
}
|
||||
|
||||
binding.switchStartOnLogin.apply {
|
||||
isChecked = action.config.startOnLogin
|
||||
}
|
||||
|
||||
binding.switchConnectOnStart.apply {
|
||||
isChecked = action.config.connectOnStart
|
||||
}
|
||||
|
||||
applyManagedStatus(action.managedStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyManagedStatus(status: ManagedConfigStatus) {
|
||||
binding.apply {
|
||||
val tooltipMessage = getString(R.string.managed_setting_info_description)
|
||||
|
||||
etAccountSlugInput.isEnabled = !status.isAccountSlugManaged
|
||||
etAccountSlugInput.isFocusable = !status.isAccountSlugManaged
|
||||
etAccountSlugInput.isClickable = !status.isAccountSlugManaged
|
||||
ilAccountSlugInput.isEnabled = !status.isAccountSlugManaged
|
||||
setupInfoIcon(ivAccountSlugInfo, status.isAccountSlugManaged, tooltipMessage)
|
||||
|
||||
switchStartOnLogin.isEnabled = !status.isStartOnLoginManaged
|
||||
switchStartOnLogin.isFocusable = !status.isStartOnLoginManaged
|
||||
switchStartOnLogin.isClickable = !status.isStartOnLoginManaged
|
||||
setupTooltipForWrapper(flStartOnLoginWrapper, status.isStartOnLoginManaged, tooltipMessage)
|
||||
|
||||
switchConnectOnStart.isEnabled = !status.isConnectOnStartManaged
|
||||
switchConnectOnStart.isFocusable = !status.isConnectOnStartManaged
|
||||
switchConnectOnStart.isClickable = !status.isConnectOnStartManaged
|
||||
setupTooltipForWrapper(flConnectOnStartWrapper, status.isConnectOnStartManaged, tooltipMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTooltipForWrapper(
|
||||
view: View,
|
||||
isManaged: Boolean,
|
||||
tooltipMessage: String,
|
||||
) {
|
||||
if (isManaged) {
|
||||
view.isClickable = true
|
||||
view.isFocusable = true
|
||||
TooltipCompat.setTooltipText(view, tooltipMessage)
|
||||
|
||||
view.setOnClickListener { v ->
|
||||
Toast.makeText(v.context, tooltipMessage, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
view.isClickable = false
|
||||
view.isFocusable = false
|
||||
TooltipCompat.setTooltipText(view, null)
|
||||
view.setOnLongClickListener(null)
|
||||
view.setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupInfoIcon(
|
||||
infoIconView: View,
|
||||
isManaged: Boolean,
|
||||
tooltipMessage: String,
|
||||
) {
|
||||
if (isManaged) {
|
||||
infoIconView.visibility = View.VISIBLE
|
||||
TooltipCompat.setTooltipText(infoIconView, tooltipMessage)
|
||||
|
||||
infoIconView.setOnClickListener { v ->
|
||||
Toast.makeText(v.context, tooltipMessage, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
infoIconView.visibility = View.GONE
|
||||
TooltipCompat.setTooltipText(infoIconView, null)
|
||||
infoIconView.setOnClickListener(null)
|
||||
infoIconView.setOnLongClickListener(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,15 @@ internal class SettingsActivity : AppCompatActivity() {
|
||||
when (position) {
|
||||
0 -> {
|
||||
tab.setIcon(R.drawable.rounded_discover_tune_black_24dp)
|
||||
tab.setText("Advanced")
|
||||
tab.setText("General")
|
||||
}
|
||||
|
||||
1 -> {
|
||||
tab.setIcon(R.drawable.rounded_settings_black_24dp)
|
||||
tab.setText("Advanced")
|
||||
}
|
||||
|
||||
2 -> {
|
||||
tab.setIcon(R.drawable.rounded_description_black_24dp)
|
||||
tab.setText("Logs")
|
||||
}
|
||||
@@ -93,7 +98,7 @@ internal class SettingsActivity : AppCompatActivity() {
|
||||
private fun showSaveWarningDialog() {
|
||||
AlertDialog.Builder(this).apply {
|
||||
setTitle("Warning")
|
||||
setMessage("Changed settings will not be applied until you sign out and sign back in.")
|
||||
setMessage("Some changed settings will not be applied until you sign out and sign back in.")
|
||||
setPositiveButton("Okay") { _, _ ->
|
||||
viewModel.onSaveSettingsCompleted()
|
||||
}
|
||||
@@ -111,12 +116,13 @@ internal class SettingsActivity : AppCompatActivity() {
|
||||
private inner class SettingsPagerAdapter(
|
||||
activity: FragmentActivity,
|
||||
) : FragmentStateAdapter(activity) {
|
||||
override fun getItemCount(): Int = 2 // Two tabs
|
||||
override fun getItemCount(): Int = 3 // Three tabs
|
||||
|
||||
override fun createFragment(position: Int): Fragment =
|
||||
when (position) {
|
||||
0 -> AdvancedSettingsFragment()
|
||||
1 -> LogSettingsFragment()
|
||||
0 -> GeneralSettingsFragment()
|
||||
1 -> AdvancedSettingsFragment()
|
||||
2 -> LogSettingsFragment()
|
||||
else -> throw IllegalArgumentException("Invalid tab position: $position")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.firezone.android.core.data.Repository
|
||||
import dev.firezone.android.core.data.model.Config
|
||||
import dev.firezone.android.core.data.model.ManagedConfigStatus
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -40,14 +41,25 @@ internal class SettingsViewModel
|
||||
private val actionMutableLiveData = MutableLiveData<ViewAction>()
|
||||
val actionLiveData: LiveData<ViewAction> = actionMutableLiveData
|
||||
|
||||
private var config = Config(authBaseUrl = "", apiUrl = "", logFilter = "")
|
||||
private var config =
|
||||
Config(
|
||||
authUrl = "",
|
||||
apiUrl = "",
|
||||
logFilter = "",
|
||||
accountSlug = "",
|
||||
startOnLogin = false,
|
||||
connectOnStart = false,
|
||||
)
|
||||
|
||||
fun populateFieldsFromConfig() {
|
||||
viewModelScope.launch {
|
||||
repo.getConfig().collect {
|
||||
config = it
|
||||
onFieldUpdated()
|
||||
actionMutableLiveData.postValue(
|
||||
ViewAction.FillSettings(
|
||||
it,
|
||||
managedStatus = repo.getManagedStatus(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -81,8 +93,8 @@ internal class SettingsViewModel
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateBack)
|
||||
}
|
||||
|
||||
fun onValidateAuthBaseUrl(authBaseUrl: String) {
|
||||
this.config.authBaseUrl = authBaseUrl
|
||||
fun onValidateAuthUrl(authUrl: String) {
|
||||
this.config.authUrl = authUrl
|
||||
onFieldUpdated()
|
||||
}
|
||||
|
||||
@@ -96,6 +108,21 @@ internal class SettingsViewModel
|
||||
onFieldUpdated()
|
||||
}
|
||||
|
||||
fun onValidateAccountSlug(accountSlug: String) {
|
||||
this.config.accountSlug = accountSlug
|
||||
onFieldUpdated()
|
||||
}
|
||||
|
||||
fun onStartOnLoginChanged(isChecked: Boolean) {
|
||||
this.config.startOnLogin = isChecked
|
||||
onFieldUpdated()
|
||||
}
|
||||
|
||||
fun onConnectOnStartChanged(isChecked: Boolean) {
|
||||
this.config.connectOnStart = isChecked
|
||||
onFieldUpdated()
|
||||
}
|
||||
|
||||
fun deleteLogDirectory(context: Context) {
|
||||
viewModelScope.launch {
|
||||
val logDir = context.cacheDir.absolutePath + "/logs"
|
||||
@@ -152,7 +179,10 @@ internal class SettingsViewModel
|
||||
repo.resetFavorites()
|
||||
onFieldUpdated()
|
||||
actionMutableLiveData.postValue(
|
||||
ViewAction.FillSettings(config),
|
||||
ViewAction.FillSettings(
|
||||
config = config,
|
||||
managedStatus = repo.getManagedStatus(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -196,12 +226,10 @@ internal class SettingsViewModel
|
||||
)
|
||||
}
|
||||
|
||||
private fun areFieldsValid(): Boolean {
|
||||
// This comes from the backend account slug validator at elixir/apps/domain/lib/domain/accounts/account/changeset.ex
|
||||
return URLUtil.isValidUrl(config.authBaseUrl) &&
|
||||
private fun areFieldsValid(): Boolean =
|
||||
URLUtil.isValidUrl(config.authUrl) &&
|
||||
isUriValid(config.apiUrl) &&
|
||||
config.logFilter.isNotBlank()
|
||||
}
|
||||
|
||||
private fun isUriValid(uri: String): Boolean =
|
||||
try {
|
||||
@@ -221,6 +249,7 @@ internal class SettingsViewModel
|
||||
|
||||
data class FillSettings(
|
||||
val config: Config,
|
||||
val managedStatus: ManagedConfigStatus,
|
||||
) : ViewAction()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,13 +36,15 @@ internal class SplashViewModel
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateToVpnPermission)
|
||||
} else {
|
||||
val token = applicationRestrictions.getString("token") ?: repo.getTokenSync()
|
||||
if (token.isNullOrBlank()) {
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateToSignIn)
|
||||
} else {
|
||||
val connectOnStart = repo.getConfigSync().connectOnStart
|
||||
|
||||
if (!token.isNullOrBlank() && connectOnStart) {
|
||||
// token will be re-read by the TunnelService
|
||||
if (!TunnelService.isRunning(context)) TunnelService.start(context)
|
||||
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateToSession)
|
||||
} else {
|
||||
actionMutableLiveData.postValue(ViewAction.NavigateToSignIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ import dev.firezone.android.tunnel.callback.ConnlibCallback
|
||||
import dev.firezone.android.tunnel.model.Cidr
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
import dev.firezone.android.tunnel.model.isInternetResource
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.util.concurrent.Executors
|
||||
@@ -236,6 +238,7 @@ class TunnelService : VpnService() {
|
||||
// Only change VPN if appRestrictions have changed
|
||||
val restrictionsManager = context.getSystemService(Context.RESTRICTIONS_SERVICE) as android.content.RestrictionsManager
|
||||
val newAppRestrictions = restrictionsManager.applicationRestrictions
|
||||
GlobalScope.launch { repo.saveManagedConfiguration(newAppRestrictions).collect {} }
|
||||
val changed = MANAGED_CONFIGURATIONS.any { newAppRestrictions.getString(it) != appRestrictions.getString(it) }
|
||||
if (!changed) {
|
||||
return
|
||||
|
||||
10
kotlin/android/app/src/main/res/drawable/info_24px.xml
Normal file
10
kotlin/android/app/src/main/res/drawable/info_24px.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M440,680L520,680L520,440L440,440L440,680ZM480,360Q497,360 508.5,348.5Q520,337 520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320Q440,337 451.5,348.5Q463,360 480,360ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19.5,12c0,-0.23 -0.01,-0.45 -0.03,-0.68l1.86,-1.41c0.4,-0.3 0.51,-0.86 0.26,-1.3l-1.87,-3.23c-0.25,-0.44 -0.79,-0.62 -1.25,-0.42l-2.15,0.91c-0.37,-0.26 -0.76,-0.49 -1.17,-0.68l-0.29,-2.31C14.8,2.38 14.37,2 13.87,2h-3.73C9.63,2 9.2,2.38 9.14,2.88L8.85,5.19c-0.41,0.19 -0.8,0.42 -1.17,0.68L5.53,4.96c-0.46,-0.2 -1,-0.02 -1.25,0.42L2.41,8.62c-0.25,0.44 -0.14,0.99 0.26,1.3l1.86,1.41C4.51,11.55 4.5,11.77 4.5,12s0.01,0.45 0.03,0.68l-1.86,1.41c-0.4,0.3 -0.51,0.86 -0.26,1.3l1.87,3.23c0.25,0.44 0.79,0.62 1.25,0.42l2.15,-0.91c0.37,0.26 0.76,0.49 1.17,0.68l0.29,2.31C9.2,21.62 9.63,22 10.13,22h3.73c0.5,0 0.93,-0.38 0.99,-0.88l0.29,-2.31c0.41,-0.19 0.8,-0.42 1.17,-0.68l2.15,0.91c0.46,0.2 1,0.02 1.25,-0.42l1.87,-3.23c0.25,-0.44 0.14,-0.99 -0.26,-1.3l-1.86,-1.41C19.49,12.45 19.5,12.23 19.5,12zM12.04,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5s3.5,1.57 3.5,3.5S13.97,15.5 12.04,15.5z"/>
|
||||
|
||||
</vector>
|
||||
@@ -66,6 +66,12 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:icon="@drawable/rounded_discover_tune_black_24dp"
|
||||
android:text="@string/general_settings_title" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:icon="@drawable/rounded_settings_black_24dp"
|
||||
android:text="@string/advanced_settings_title" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
|
||||
@@ -13,31 +13,45 @@
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/ilAuthBaseUrlInput"
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/ilAuthUrlInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etAuthBaseUrlInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/auth_base_url"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/ivAuthUrlInfo"> <com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etAuthUrlInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/auth_url"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivAuthUrlInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_small"
|
||||
android:padding="@dimen/spacing_extra_small"
|
||||
android:src="@drawable/info_24px"
|
||||
android:contentDescription="@string/managed_setting_info_description"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/ilAuthUrlInput"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ilAuthUrlInput" />
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/ilApiUrlInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_1x"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ilAuthBaseUrlInput">
|
||||
app:layout_constraintTop_toBottomOf="@id/ilAuthUrlInput"
|
||||
app:layout_constraintEnd_toStartOf="@id/ivApiUrlInfo">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etApiUrlInput"
|
||||
@@ -49,14 +63,29 @@
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivApiUrlInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_small"
|
||||
android:padding="@dimen/spacing_extra_small"
|
||||
android:src="@drawable/info_24px"
|
||||
android:contentDescription="@string/managed_setting_info_description"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/ilApiUrlInput"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ilApiUrlInput" />
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/ilLogFilterInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_1x"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ilApiUrlInput">
|
||||
app:layout_constraintTop_toBottomOf="@id/ilApiUrlInput"
|
||||
app:layout_constraintEnd_toStartOf="@id/ivLogFilterInfo">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etLogFilterInput"
|
||||
@@ -68,6 +97,19 @@
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivLogFilterInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_small"
|
||||
android:padding="@dimen/spacing_extra_small"
|
||||
android:src="@drawable/info_24px"
|
||||
android:contentDescription="@string/managed_setting_info_description"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/ilLogFilterInput"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ilLogFilterInput" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btResetDefaults"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView 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"
|
||||
android:fillViewport="true"
|
||||
android:padding="@dimen/spacing_medium"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/ilAccountSlugInput"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/ivAccountSlugInfo"> <com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etAccountSlugInput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/account_slug"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivAccountSlugInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_small"
|
||||
android:padding="@dimen/spacing_extra_small"
|
||||
android:src="@drawable/info_24px" android:contentDescription="@string/managed_setting_info_description"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/ilAccountSlugInput"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ilAccountSlugInput" />
|
||||
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/flStartOnLoginWrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_1x"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ilAccountSlugInput"> <com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/switchStartOnLogin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/start_on_login" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/flConnectOnStartWrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_1x"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/flStartOnLoginWrapper">
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/switchConnectOnStart"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/connect_on_start" />
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -1,4 +1,5 @@
|
||||
<resources>
|
||||
<dimen name="spacing_extra_small">4dp</dimen>
|
||||
<dimen name="spacing_small">8dp</dimen>
|
||||
<dimen name="spacing_medium">16dp</dimen>
|
||||
|
||||
|
||||
@@ -8,14 +8,19 @@
|
||||
<!-- Settings -->
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="save">Save</string>
|
||||
<!-- General Settings -->
|
||||
<string name="general_settings_title">General</string>
|
||||
<string name="account_slug">Account Slug</string>
|
||||
<string name="start_on_login">Start on Boot</string>
|
||||
<string name="connect_on_start">Connect on Start</string>
|
||||
<!-- Advanced Settings -->
|
||||
<string name="advanced_settings_title">Advanced</string>
|
||||
<string name="auth_base_url">Auth Base URL</string>
|
||||
<string name="auth_url">Auth URL</string>
|
||||
<string name="api_url">API URL</string>
|
||||
<string name="log_filter">Log Filter</string>
|
||||
<string name="button_reset_to_defaults">Reset to Defaults</string>
|
||||
<!-- Diagnostic Logs Settings -->
|
||||
<string name="log_settings_title">Diagnostic Logs</string>
|
||||
<string name="log_settings_title">Logs</string>
|
||||
<string name="share_diagnostic_logs">Export Logs</string>
|
||||
<string name="log_directory_size">Log directory size: %1$s</string>
|
||||
<string name="clear_log_directory">Clear Log Directory</string>
|
||||
@@ -71,4 +76,37 @@
|
||||
The name of the device. This is used to identify the device in the admin portal.
|
||||
If unset, device\'s model name will be used.
|
||||
</string>
|
||||
<string name="config_auth_url_title">Auth URL</string>
|
||||
<string name="config_auth_url_description">
|
||||
The base URL used for authentication. If unset, the default of https://app.firezone.dev will
|
||||
be used. The account slug will be appended to this URL.
|
||||
</string>
|
||||
<string name="config_api_url_title">API URL</string>
|
||||
<string name="config_api_url_description">
|
||||
The WebSocket URL used to connect to the Firezone control plane. If unset, the default of
|
||||
wss://api.firezone.dev will be used.
|
||||
</string>
|
||||
<string name="config_log_filter_title">Log Filter</string>
|
||||
<string name="config_log_filter_description">
|
||||
A RUST_LOG-formatted filter string used to control the verbosity of the connectivity library
|
||||
logging. If unset, the default of "info" will be used.
|
||||
</string>
|
||||
<string name="config_account_slug_title">Account Slug</string>
|
||||
<string name="config_account_slug_description">
|
||||
The account ID or slug of the Firezone account to authenticate to. If unset, no account slug
|
||||
will be used, and the user will need to enter it manually on the first sign in attempt. Upon
|
||||
subsequent sign ins, the account slug will be automatically populated.
|
||||
</string>
|
||||
<string name="config_start_on_login_title">Start on Boot</string>
|
||||
<string name="config_start_on_login_description">
|
||||
Whether Firezone should automatically try to connect when the device boots up.
|
||||
</string>
|
||||
<string name="config_connect_on_start_title">Connect on Start</string>
|
||||
<string name="config_connect_on_start_description">
|
||||
Whether Firezone should automatically try to connect when the app is launched.
|
||||
</string>
|
||||
|
||||
<string name="managed_setting_info_description">
|
||||
This setting is being managed by your organization.
|
||||
</string>
|
||||
</resources>
|
||||
|
||||
@@ -24,4 +24,40 @@
|
||||
android:key="deviceName"
|
||||
android:restrictionType="string"
|
||||
android:title="@string/config_device_name_title" />
|
||||
|
||||
<restriction
|
||||
android:description="@string/config_auth_url_description"
|
||||
android:key="authUrl"
|
||||
android:restrictionType="string"
|
||||
android:title="@string/config_auth_url_title" />
|
||||
|
||||
<restriction
|
||||
android:description="@string/config_api_url_description"
|
||||
android:key="apiUrl"
|
||||
android:restrictionType="string"
|
||||
android:title="@string/config_api_url_title" />
|
||||
|
||||
<restriction
|
||||
android:description="@string/config_log_filter_description"
|
||||
android:key="logFilter"
|
||||
android:restrictionType="string"
|
||||
android:title="@string/config_log_filter_title" />
|
||||
|
||||
<restriction
|
||||
android:description="@string/config_account_slug_description"
|
||||
android:key="accountSlug"
|
||||
android:restrictionType="string"
|
||||
android:title="@string/config_account_slug_title" />
|
||||
|
||||
<restriction
|
||||
android:description="@string/config_start_on_login_description"
|
||||
android:key="startOnLogin"
|
||||
android:restrictionType="bool"
|
||||
android:title="@string/config_start_on_login_title" />
|
||||
|
||||
<restriction
|
||||
android:description="@string/config_connect_on_start_description"
|
||||
android:key="connectOnStart"
|
||||
android:restrictionType="bool"
|
||||
android:title="@string/config_connect_on_start_title" />
|
||||
</restrictions>
|
||||
|
||||
Reference in New Issue
Block a user