refactor(android): Refactor navigation and session functionality (#2469)

This PR refactors navigation to fix nav issues and session
implementation to fix:
Fixes #2300 
Fixes #2186

---------

Signed-off-by: Pratik Velani <pratikvelani@gmail.com>
This commit is contained in:
Pratik Velani
2023-10-27 19:45:57 +05:30
committed by GitHub
parent 63542e2069
commit 28e1ad1523
21 changed files with 187 additions and 178 deletions

View File

@@ -72,6 +72,10 @@
android:name="dev.firezone.android.features.permission.vpn.ui.VpnPermissionActivity"
android:exported="false" />
<activity
android:name="dev.firezone.android.features.session.ui.SessionActivity"
android:exported="false" />
<service
android:name="dev.firezone.android.tunnel.TunnelService"
android:exported="true"

View File

@@ -3,7 +3,6 @@ package dev.firezone.android.features.applink.ui
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
@@ -29,17 +28,14 @@ class AppLinkHandlerActivity : AppCompatActivity(R.layout.activity_app_link_hand
viewModel.actionLiveData.observe(this) { action ->
when (action) {
AppLinkViewModel.ViewAction.AuthFlowComplete -> {
// TODO: Continue starting the session showing sessionFragment
Log.d("AppLinkHandlerActivity", "AuthFlowComplete")
val intent = Intent(this@AppLinkHandlerActivity, MainActivity::class.java)
this@AppLinkHandlerActivity.startActivity(intent)
this@AppLinkHandlerActivity.finish()
startActivity(
Intent(this@AppLinkHandlerActivity, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
},
)
finish()
}
AppLinkViewModel.ViewAction.ShowError -> showError()
else -> {
Log.d("AppLinkHandlerActivity", "Unhandled action: $action")
}
}
}
}

View File

@@ -41,7 +41,6 @@ internal class AppLinkViewModel
}
intent.data?.getQueryParameter(QUERY_CLIENT_AUTH_TOKEN)?.let { token ->
if (token.isNotBlank()) {
// TODO: Don't log auth token
Log.d("AppLinkViewModel", "Found valid auth token in response")
saveTokenUseCase(token).collect()
} else {

View File

@@ -38,7 +38,7 @@ class AuthActivity : AppCompatActivity(R.layout.activity_auth) {
viewModel.actionLiveData.observe(this) { action ->
when (action) {
is AuthViewModel.ViewAction.LaunchAuthFlow -> setupWebView(action.url)
is AuthViewModel.ViewAction.NavigateToSignInFragment -> {
is AuthViewModel.ViewAction.NavigateToSignIn -> {
navigateToSignIn()
}
is AuthViewModel.ViewAction.ShowError -> showError()

View File

@@ -39,7 +39,7 @@ internal class AuthViewModel
actionMutableLiveData.postValue(
if (authFlowLaunched || config.token != null) {
ViewAction.NavigateToSignInFragment
ViewAction.NavigateToSignIn
} else {
authFlowLaunched = true
ViewAction.LaunchAuthFlow(
@@ -59,7 +59,7 @@ internal class AuthViewModel
internal sealed class ViewAction {
data class LaunchAuthFlow(val url: String) : ViewAction()
object NavigateToSignInFragment : ViewAction()
object NavigateToSignIn : ViewAction()
object ShowError : ViewAction()
}

View File

@@ -3,11 +3,9 @@ package dev.firezone.android.features.session.ui
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@@ -17,41 +15,38 @@ 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.FragmentSessionBinding
import dev.firezone.android.databinding.ActivitySessionBinding
import kotlinx.coroutines.launch
@AndroidEntryPoint
internal class SessionFragment : Fragment(R.layout.fragment_session) {
private lateinit var binding: FragmentSessionBinding
internal class SessionActivity : AppCompatActivity() {
private lateinit var binding: ActivitySessionBinding
private val viewModel: SessionViewModel by viewModels()
private val resourcesAdapter: ResourcesAdapter =
ResourcesAdapter { resource ->
ClipboardUtils.copyToClipboard(requireContext(), resource.name, resource.address)
ClipboardUtils.copyToClipboard(this@SessionActivity, resource.name, resource.address)
}
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentSessionBinding.bind(view)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySessionBinding.inflate(layoutInflater)
setContentView(binding.root)
setupViews()
setupObservers()
Log.d("SessionFragment", "Starting session...")
viewModel.startSession()
viewModel.connect(this@SessionActivity)
}
private fun setupViews() {
binding.btSignOut.setOnClickListener {
viewModel.onDisconnect()
viewModel.disconnect()
}
val layoutManager = LinearLayoutManager(requireContext())
val layoutManager = LinearLayoutManager(this@SessionActivity)
val dividerItemDecoration =
DividerItemDecoration(
requireContext(),
this@SessionActivity,
layoutManager.orientation,
)
binding.resourcesList.addItemDecoration(dividerItemDecoration)
@@ -60,15 +55,13 @@ internal class SessionFragment : Fragment(R.layout.fragment_session) {
}
private fun setupObservers() {
viewModel.actionLiveData.observe(viewLifecycleOwner) { action ->
viewModel.actionLiveData.observe(this@SessionActivity) { action ->
when (action) {
SessionViewModel.ViewAction.NavigateToSignInFragment -> {
requireActivity().run {
startActivity(
Intent(this, MainActivity::class.java),
)
finish()
}
SessionViewModel.ViewAction.NavigateToSignIn -> {
startActivity(
Intent(this, MainActivity::class.java),
)
finish()
}
SessionViewModel.ViewAction.ShowError -> showError()
}
@@ -86,7 +79,7 @@ internal class SessionFragment : Fragment(R.layout.fragment_session) {
}
private fun showError() {
AlertDialog.Builder(requireContext())
AlertDialog.Builder(this@SessionActivity)
.setTitle(R.string.error_dialog_title)
.setMessage(R.string.error_dialog_message)
.setPositiveButton(

View File

@@ -1,6 +1,7 @@
/* 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
@@ -8,7 +9,9 @@ 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.tunnel.model.Resource
import dev.firezone.android.tunnel.model.Tunnel
import kotlinx.coroutines.flow.MutableStateFlow
@@ -21,6 +24,7 @@ internal class SessionViewModel
@Inject
constructor(
private val tunnelManager: TunnelManager,
private val tunnelRepository: TunnelRepository,
) : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState
@@ -31,7 +35,20 @@ internal class SessionViewModel
private val tunnelListener =
object : TunnelListener {
override fun onTunnelStateUpdate(state: Tunnel.State) {
TODO("Not yet implemented")
when (state) {
Tunnel.State.Down -> {
onDisconnect()
}
Tunnel.State.Closed -> {
onClosed()
}
else -> {
_uiState.value =
_uiState.value.copy(
state = state,
)
}
}
}
override fun onResourcesUpdate(resources: List<Resource>) {
@@ -48,10 +65,23 @@ internal class SessionViewModel
}
}
fun startSession() {
fun connect(context: Context) {
viewModelScope.launch {
tunnelManager.addListener(tunnelListener)
tunnelManager.connect()
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(),
)
}
}
}
@@ -61,18 +91,26 @@ internal class SessionViewModel
tunnelManager.removeListener(tunnelListener)
}
fun onDisconnect() {
fun disconnect() {
tunnelManager.disconnect()
}
private fun onDisconnect() {
// no-op
}
private fun onClosed() {
tunnelManager.removeListener(tunnelListener)
actionMutableLiveData.postValue(ViewAction.NavigateToSignInFragment)
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 NavigateToSignInFragment : ViewAction()
object NavigateToSignIn : ViewAction()
object ShowError : ViewAction()
}

View File

@@ -43,7 +43,7 @@ internal class SettingsFragment : Fragment(R.layout.fragment_settings) {
private fun setupActionObservers() {
viewModel.actionLiveData.observe(viewLifecycleOwner) { action ->
when (action) {
is SettingsViewModel.ViewAction.NavigateToSignInFragment ->
is SettingsViewModel.ViewAction.NavigateToSignIn ->
findNavController().navigate(
R.id.signInFragment,
)

View File

@@ -40,7 +40,7 @@ internal class SettingsViewModel
fun onSaveSettingsCompleted() {
viewModelScope.launch {
saveAccountIdUseCase(input).collect {
actionMutableLiveData.postValue(ViewAction.NavigateToSignInFragment)
actionMutableLiveData.postValue(ViewAction.NavigateToSignIn)
}
}
}
@@ -59,7 +59,7 @@ internal class SettingsViewModel
}
internal sealed class ViewAction {
object NavigateToSignInFragment : ViewAction()
object NavigateToSignIn : ViewAction()
data class FillAccountId(val value: String) : ViewAction()
}

View File

@@ -11,7 +11,6 @@ import dagger.hilt.android.AndroidEntryPoint
import dev.firezone.android.R
import dev.firezone.android.databinding.FragmentSignInBinding
import dev.firezone.android.features.auth.ui.AuthActivity
import dev.firezone.android.features.splash.ui.SplashFragmentDirections
@AndroidEntryPoint
internal class SignInFragment : Fragment(R.layout.fragment_sign_in) {
@@ -37,10 +36,11 @@ internal class SignInFragment : Fragment(R.layout.fragment_sign_in) {
AuthActivity::class.java,
),
)
requireActivity().finish()
}
btSettings.setOnClickListener {
findNavController().navigate(
SplashFragmentDirections.navigateToSettingsFragment(),
R.id.settingsFragment,
)
}
}

View File

@@ -1,6 +1,7 @@
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
package dev.firezone.android.features.splash.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
@@ -9,6 +10,7 @@ import androidx.navigation.fragment.findNavController
import dagger.hilt.android.AndroidEntryPoint
import dev.firezone.android.R
import dev.firezone.android.databinding.FragmentSplashBinding
import dev.firezone.android.features.session.ui.SessionActivity
@AndroidEntryPoint
internal class SplashFragment : Fragment(R.layout.fragment_splash) {
@@ -37,17 +39,19 @@ internal class SplashFragment : Fragment(R.layout.fragment_splash) {
findNavController().navigate(
R.id.vpnPermissionActivity,
)
SplashViewModel.ViewAction.NavigateToSignInFragment ->
SplashViewModel.ViewAction.NavigateToSignIn ->
findNavController().navigate(
R.id.signInFragment,
)
SplashViewModel.ViewAction.NavigateToSettingsFragment ->
SplashViewModel.ViewAction.NavigateToSettings ->
findNavController().navigate(
R.id.settingsFragment,
)
SplashViewModel.ViewAction.NavigateToSessionFragment ->
findNavController().navigate(
R.id.sessionFragment,
SplashViewModel.ViewAction.NavigateToSession ->
startActivity(
Intent(requireContext(), SessionActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
},
)
}
}

View File

@@ -37,11 +37,11 @@ internal class SplashViewModel
}
.collect { user ->
if (user.accountId.isNullOrEmpty()) {
actionMutableLiveData.postValue(ViewAction.NavigateToSettingsFragment)
actionMutableLiveData.postValue(ViewAction.NavigateToSettings)
} else if (user.token.isNullOrBlank()) {
actionMutableLiveData.postValue(ViewAction.NavigateToSignInFragment)
actionMutableLiveData.postValue(ViewAction.NavigateToSignIn)
} else {
actionMutableLiveData.postValue(ViewAction.NavigateToSessionFragment)
actionMutableLiveData.postValue(ViewAction.NavigateToSession)
}
}
}
@@ -54,8 +54,8 @@ internal class SplashViewModel
internal sealed class ViewAction {
object NavigateToVpnPermission : ViewAction()
object NavigateToSettingsFragment : ViewAction()
object NavigateToSignInFragment : ViewAction()
object NavigateToSessionFragment : ViewAction()
object NavigateToSettings : ViewAction()
object NavigateToSignIn : ViewAction()
object NavigateToSession : ViewAction()
}
}

View File

@@ -8,7 +8,6 @@ import android.util.Log
import dev.firezone.android.core.data.PreferenceRepository
import dev.firezone.android.tunnel.callback.TunnelListener
import dev.firezone.android.tunnel.data.TunnelRepository
import dev.firezone.android.tunnel.model.Tunnel
import java.lang.ref.WeakReference
import javax.inject.Inject
import javax.inject.Singleton
@@ -24,10 +23,17 @@ internal class TunnelManager
private val listeners: MutableSet<WeakReference<TunnelListener>> = mutableSetOf()
private val tunnelRepositoryListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, s ->
if (s == TunnelRepository.RESOURCES_KEY) {
listeners.forEach {
it.get()?.onResourcesUpdate(tunnelRepository.getResources())
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())
}
}
}
}
@@ -43,7 +49,6 @@ internal class TunnelManager
}
tunnelRepository.addListener(tunnelRepositoryListener)
tunnelRepository.setState(Tunnel.State.Connecting)
}
fun removeListener(listener: TunnelListener) {
@@ -65,6 +70,7 @@ internal class TunnelManager
fun disconnect() {
stopVPNService()
clearSessionData()
}
private fun startVPNService() {
@@ -77,8 +83,11 @@ internal class TunnelManager
val intent = Intent(appContext, TunnelService::class.java)
intent.action = TunnelService.ACTION_DISCONNECT
appContext.startService(intent)
tunnelRepository.clearAll()
}
private fun clearSessionData() {
preferenceRepository.clearToken()
tunnelRepository.clearAll()
}
internal companion object {

View File

@@ -1,10 +1,12 @@
/* Licensed under Apache 2.0 (C) 2023 Firezone, Inc. */
package dev.firezone.android.tunnel
import android.app.ActivityManager
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.VpnService
import android.system.OsConstants
@@ -16,6 +18,7 @@ import com.squareup.moshi.adapter
import dagger.hilt.android.AndroidEntryPoint
import dev.firezone.android.BuildConfig
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.presentation.MainActivity
import dev.firezone.android.tunnel.callback.ConnlibCallback
@@ -38,11 +41,16 @@ class TunnelService : VpnService() {
@Inject
internal lateinit var tunnelRepository: TunnelRepository
@Inject
internal lateinit var preferenceRepository: PreferenceRepository
@Inject
internal lateinit var moshi: Moshi
private var sessionPtr: Long? = null
private var shouldReconnect: Boolean = false
private val activeTunnel: Tunnel?
get() = tunnelRepository.get()
@@ -129,9 +137,9 @@ class TunnelService : VpnService() {
}
override fun onDisconnect(error: String?): Boolean {
Log.d(TAG, "onDisconnect $error")
onTunnelStateUpdate(Tunnel.State.Down)
onSessionDisconnected(
error = error?.takeUnless { it == "null" },
)
return true
}
}
@@ -169,11 +177,14 @@ class TunnelService : VpnService() {
try {
val config = getConfigUseCase.sync()
Log.d("Connlib", "accountId: ${config.accountId}")
Log.d("Connlib", "token: ${config.token}")
Log.d("Connlib", "connect(): accountId: ${config.accountId}")
if (tunnelRepository.getState() == Tunnel.State.Up) {
shouldReconnect = true
disconnect()
} else if (config.accountId != null && config.token != null) {
onTunnelStateUpdate(Tunnel.State.Connecting)
updateStatusNotification("Status: Connecting...")
if (config.accountId != null && config.token != null) {
Log.d("Connlib", "Attempting to establish TunnelSession...")
sessionPtr =
TunnelSession.connect(
controlPlaneUrl = BuildConfig.CONTROL_PLANE_URL,
@@ -183,33 +194,40 @@ class TunnelService : VpnService() {
logFilter = BuildConfig.CONNLIB_LOG_FILTER_STRING,
callback = callback,
)
Log.d(TAG, "connlib session started! sessionPtr: $sessionPtr")
onTunnelStateUpdate(Tunnel.State.Connecting)
updateStatusNotification("Status: Connecting...")
}
} catch (exception: Exception) {
Log.e(TAG, exception.message.toString())
Log.e(TAG, "connect(): " + exception.message.toString())
}
}
private fun disconnect() {
Log.d(TAG, "Attempting to disconnect session")
Log.d(TAG, "disconnect(): Attempting to disconnect session")
try {
sessionPtr?.let {
Log.d(TAG, "calling TunnelSession.disconnect")
TunnelSession.disconnect(it)
}
} ?: onSessionDisconnected(null)
} catch (exception: Exception) {
Log.e(TAG, exception.message.toString())
}
stopForeground(STOP_FOREGROUND_REMOVE)
}
private fun onSessionDisconnected(error: String?) {
sessionPtr = null
onTunnelStateUpdate(Tunnel.State.Down)
if (shouldReconnect && error == null) {
shouldReconnect = false
connect()
} else {
tunnelRepository.clearAll()
preferenceRepository.clearToken()
onTunnelStateUpdate(Tunnel.State.Closed)
stopForeground(STOP_FOREGROUND_REMOVE)
}
}
private fun deviceId(): String {
val deviceId = FirebaseInstallations.getInstance().id
Log.d(TAG, "Device ID: $deviceId")
return deviceId.toString()
@@ -294,5 +312,16 @@ class TunnelService : VpnService() {
private const val TAG: String = "TunnelService"
private const val SESSION_NAME: String = "Firezone Connection"
private const val DEFAULT_MTU: Int = 1280
@SuppressWarnings("deprecation")
fun isRunning(context: Context): Boolean {
val manager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
for (service in manager.getRunningServices(Int.MAX_VALUE)) {
if (TunnelService::class.java.name == service.service.className) {
return true
}
}
return false
}
}
}

View File

@@ -67,7 +67,7 @@ class TunnelRepositoryImpl
override fun getState(): Tunnel.State {
val json = sharedPreferences.getString(STATE_KEY, null)
return json?.let { Tunnel.State.valueOf(it) } ?: Tunnel.State.Down
return json?.let { Tunnel.State.valueOf(it) } ?: Tunnel.State.Closed
}
override fun setResources(resources: List<Resource>) {

View File

@@ -14,8 +14,9 @@ data class Tunnel(
val resources: List<Resource> = emptyList(),
) : Parcelable {
enum class State {
Up,
Connecting,
Up,
Down,
Closed,
}
}

View File

@@ -53,7 +53,7 @@
android:id="@+id/btSignOut"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/sign_out_activity_button_text"
android:text="@string/sign_out"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

View File

@@ -47,7 +47,7 @@
android:id="@+id/etInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/settings_fragment_input_hint"
android:hint="@string/account_id"
android:importantForAutofill="no"
android:inputType="text" />
@@ -58,7 +58,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:enabled="false"
android:text="@string/settings_fragment_button_text"
android:text="@string/save"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

View File

@@ -39,7 +39,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_small"
android:text="@string/sign_in_fragment_sign_status_text"
android:text="@string/signed_out"
app:layout_constraintBottom_toTopOf="@+id/btSignIn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -49,7 +49,7 @@
android:id="@+id/btSignIn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/sign_in_fragment_button_text"
android:text="@string/sign_in"
app:layout_constraintBottom_toTopOf="@+id/btSettings"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
@@ -58,7 +58,7 @@
android:id="@+id/btSettings"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/sign_in_settings_button_text"
android:text="@string/settings"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

View File

@@ -8,85 +8,22 @@
<fragment
android:id="@+id/splashFragment"
android:name="dev.firezone.android.features.splash.ui.SplashFragment"
tools:layout="@layout/fragment_splash">
<action
android:id="@+id/navigateToSettingsFragment"
app:destination="@id/settingsFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:popExitAnim="@anim/fade_out" />
<action
android:id="@+id/navigateToSignInFragment"
app:destination="@id/signInFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:popExitAnim="@anim/fade_out" />
<action
android:id="@+id/navigateToSessionFragment"
app:destination="@id/sessionFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:popExitAnim="@anim/fade_out" />
</fragment>
tools:layout="@layout/fragment_splash" />
<fragment
android:id="@+id/settingsFragment"
android:name="dev.firezone.android.features.settings.ui.SettingsFragment"
tools:layout="@layout/fragment_settings">
<action
android:id="@+id/navigateToSignInFragment"
app:destination="@id/signInFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:popExitAnim="@anim/fade_out" />
</fragment>
tools:layout="@layout/fragment_settings" />
<fragment
android:id="@+id/signInFragment"
android:name="dev.firezone.android.features.signin.ui.SignInFragment"
tools:layout="@layout/fragment_sign_in">
tools:layout="@layout/fragment_sign_in" />
<action
android:id="@+id/navigateToSessionFragment"
app:destination="@id/sessionFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:popExitAnim="@anim/fade_out" />
<action
android:id="@+id/navigateToSettingsFragment"
app:destination="@id/settingsFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:popExitAnim="@anim/fade_out" />
</fragment>
<fragment
android:id="@+id/sessionFragment"
android:name="dev.firezone.android.features.session.ui.SessionFragment"
tools:layout="@layout/fragment_session">
<action
android:id="@+id/navigateToSignInFragment"
app:destination="@id/signInFragment"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out"
app:popEnterAnim="@anim/fade_in"
app:popExitAnim="@anim/fade_out" />
</fragment>
<activity
android:id="@+id/sessionActivity"
android:name="dev.firezone.android.features.session.ui.SessionActivity"
tools:layout="@layout/activity_session" />
<activity
android:id="@+id/vpnPermissionActivity"

View File

@@ -1,21 +1,20 @@
<resources>
<string name="app_short_name">firezone</string>
<!-- Settings Fragment -->
<string name="settings_fragment_header_title">Login URL</string>
<string name="settings_fragment_input_hint">account-id</string>
<string name="settings_fragment_button_text">Save</string>
<!-- Settings -->
<string name="account_id">account-id</string>
<string name="save">Save</string>
<!-- Sign In Fragment -->
<string name="sign_in_fragment_button_text">Sign In</string>
<string name="sign_in_settings_button_text">Settings</string>
<string name="sign_in_fragment_sign_status_text">Signed Out</string>
<!-- Sign In -->
<string name="sign_in">Sign In</string>
<string name="settings">Settings</string>
<string name="signed_out">Signed Out</string>
<!-- Sign Out Activity -->
<!-- Sign Out -->
<string name="resources">Resources</string>
<string name="sign_out_activity_button_text">Sign Out</string>
<string name="sign_out">Sign Out</string>
<!-- Auth Activity -->
<!-- Auth -->
<string name="launching_auth_flow">Launching Auth Flow…</string>
<!-- Error Dialog -->