android: ViewModel cleanup
- Replace IpnManager, IpnModel and PrefsEditor with IpnViewModel - Use lazy StateFlows in Notifier - Manage view model lifecycles using viewModel() function - Stop watching IPN bus when MainActivity stops - Pass IPN notifications as ByteArray instead of string Updates tailscale/corp#18202 Signed-off-by: Percy Wegmann <percy@tailscale.com>oxtoacart/reactive_exit_node_bak
parent
d42329e2e2
commit
a1e67ff1e9
@ -1,81 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.service
|
||||
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import com.tailscale.ipn.App
|
||||
import com.tailscale.ipn.IPNReceiver
|
||||
import com.tailscale.ipn.mdm.MDMSettings
|
||||
import com.tailscale.ipn.ui.localapi.Client
|
||||
import com.tailscale.ipn.ui.model.Ipn
|
||||
import com.tailscale.ipn.ui.notifier.Notifier
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
typealias PrefChangeCallback = (Result<Boolean>) -> Unit
|
||||
|
||||
// Abstracts the actions that can be taken by the UI so that the concept of an IPNManager
|
||||
// itself is hidden from the viewModel implementations.
|
||||
interface IpnActions {
|
||||
fun startVPN()
|
||||
fun stopVPN()
|
||||
fun login()
|
||||
fun logout()
|
||||
fun updatePrefs(prefs: Ipn.MaskedPrefs, callback: PrefChangeCallback)
|
||||
}
|
||||
|
||||
class IpnManager(private val scope: CoroutineScope) : IpnActions {
|
||||
companion object {
|
||||
private const val TAG = "IpnManager"
|
||||
}
|
||||
|
||||
private var notifier = Notifier(scope)
|
||||
var mdmSettings = MDMSettings()
|
||||
val model = IpnModel(notifier, scope)
|
||||
|
||||
override fun startVPN() {
|
||||
val context = App.getApplication().applicationContext
|
||||
val intent = Intent(context, IPNReceiver::class.java)
|
||||
intent.action = IPNReceiver.INTENT_CONNECT_VPN
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
override fun stopVPN() {
|
||||
val context = App.getApplication().applicationContext
|
||||
val intent = Intent(context, IPNReceiver::class.java)
|
||||
intent.action = IPNReceiver.INTENT_DISCONNECT_VPN
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
override fun login() {
|
||||
Client(scope).startLoginInteractive { result ->
|
||||
result.onSuccess {
|
||||
Log.d(TAG, "Login started: $it")
|
||||
}.onFailure {
|
||||
Log.e(TAG, "Error starting login: ${it.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
Client(scope).logout { result ->
|
||||
result.onSuccess {
|
||||
Log.d(TAG, "Logout started: $it")
|
||||
}.onFailure {
|
||||
Log.e(TAG, "Error starting logout: ${it.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updatePrefs(prefs: Ipn.MaskedPrefs, callback: PrefChangeCallback) {
|
||||
Client(scope).editPrefs(prefs) { result ->
|
||||
result.onSuccess {
|
||||
callback(Result.success(true))
|
||||
}.onFailure {
|
||||
callback(Result.failure(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.service
|
||||
|
||||
import android.util.Log
|
||||
import com.tailscale.ipn.ui.localapi.Client
|
||||
import com.tailscale.ipn.ui.model.Ipn
|
||||
import com.tailscale.ipn.ui.model.IpnLocal
|
||||
import com.tailscale.ipn.ui.model.Netmap
|
||||
import com.tailscale.ipn.ui.notifier.Notifier
|
||||
import com.tailscale.ipn.ui.util.set
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class IpnModel(notifier: Notifier, val scope: CoroutineScope) {
|
||||
companion object {
|
||||
private const val TAG = "IpnModel"
|
||||
}
|
||||
|
||||
private var notifierSessions: MutableList<String> = mutableListOf()
|
||||
|
||||
val state: StateFlow<Ipn.State> = MutableStateFlow(Ipn.State.NoState)
|
||||
val netmap: StateFlow<Netmap.NetworkMap?> = MutableStateFlow(null)
|
||||
val prefs: StateFlow<Ipn.Prefs?> = MutableStateFlow(null)
|
||||
val engineStatus: StateFlow<Ipn.EngineStatus?> = MutableStateFlow(null)
|
||||
val tailFSShares: StateFlow<Map<String, String>?> = MutableStateFlow(null)
|
||||
val browseToURL: StateFlow<String?> = MutableStateFlow(null)
|
||||
val loginFinished: StateFlow<String?> = MutableStateFlow(null)
|
||||
val version: StateFlow<String?> = MutableStateFlow(null)
|
||||
val loggedInUser: StateFlow<IpnLocal.LoginProfile?> = MutableStateFlow(null)
|
||||
val loginProfiles: StateFlow<List<IpnLocal.LoginProfile>?> = MutableStateFlow(null)
|
||||
|
||||
val isUsingExitNode: Boolean
|
||||
get() {
|
||||
return prefs.value != null
|
||||
}
|
||||
|
||||
// Backend Observation
|
||||
|
||||
private suspend fun loadUserProfiles() {
|
||||
Client(scope).profiles { result ->
|
||||
result.onSuccess(loginProfiles::set).onFailure {
|
||||
Log.e(TAG, "Error loading profiles: ${it.message}")
|
||||
}
|
||||
}
|
||||
|
||||
Client(scope).currentProfile { result ->
|
||||
result.onSuccess(loggedInUser::set).onFailure {
|
||||
Log.e(TAG, "Error loading current profile: ${it.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNotifyChange(notify: Ipn.Notify) {
|
||||
notify.State?.let { s ->
|
||||
// Refresh the user profiles if we're transitioning out of the
|
||||
// NeedsLogin state.
|
||||
if (state.value == Ipn.State.NeedsLogin) {
|
||||
scope.launch { loadUserProfiles() }
|
||||
}
|
||||
|
||||
Log.d("IpnModel", "State changed: $s")
|
||||
state.set(Ipn.State.fromInt(s))
|
||||
}
|
||||
|
||||
notify.NetMap?.let(netmap::set)
|
||||
notify.Prefs?.let(prefs::set)
|
||||
notify.Engine?.let(engineStatus::set)
|
||||
notify.TailFSShares?.let(tailFSShares::set)
|
||||
notify.BrowseToURL?.let(browseToURL::set)
|
||||
notify.LoginFinished?.let { loginFinished.set(it.property) }
|
||||
notify.Version?.let(version::set)
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d("IpnModel", "IpnModel created")
|
||||
val session = notifier.watchAll { n -> onNotifyChange(n) }
|
||||
notifierSessions.add(session)
|
||||
scope.launch { loadUserProfiles() }
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.service
|
||||
|
||||
import com.tailscale.ipn.ui.localapi.Client
|
||||
import com.tailscale.ipn.ui.model.Ipn
|
||||
|
||||
|
||||
// Handles all types of preference modifications typically invoked by the UI.
|
||||
// Callers generally shouldn't care about the returned prefs value - the source of
|
||||
// truth is the IPNModel, who's prefs flow will change in value to reflect the true
|
||||
// value of the pref setting in the back end (and will match the value returned here).
|
||||
// Generally, you will want to inspect the returned value in the callback for errors
|
||||
// to indicate why a particular setting did not change in the interface.
|
||||
//
|
||||
// Usage:
|
||||
// - User/Interface changed to new value. Render the new value.
|
||||
// - Submit the new value to the PrefsEditor
|
||||
// - Observe the prefs on the IpnModel and update the UI when/if the value changes.
|
||||
// For a typical flow, the changed value should reflect the value already shown.
|
||||
// - Inform the user of any error which may have occurred
|
||||
//
|
||||
// The "toggle' functions here will attempt to set the pref value to the inverse of
|
||||
// what is currently known in the IpnModel.prefs. If IpnModel.prefs is not available,
|
||||
// the callback will be called with a NO_PREFS error
|
||||
|
||||
fun IpnModel.setWantRunning(wantRunning: Boolean, callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
Ipn.MaskedPrefs().WantRunning = wantRunning
|
||||
Client(scope).editPrefs(Ipn.MaskedPrefs(), callback)
|
||||
}
|
||||
|
||||
fun IpnModel.toggleCorpDNS(callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
val prefs = prefs.value ?: run {
|
||||
callback(Result.failure(Exception("no prefs")))
|
||||
return@toggleCorpDNS
|
||||
}
|
||||
|
||||
val prefsOut = Ipn.MaskedPrefs()
|
||||
prefsOut.CorpDNS = !prefs.CorpDNS
|
||||
Client(scope).editPrefs(prefsOut, callback)
|
||||
}
|
||||
|
||||
fun IpnModel.toggleShieldsUp(callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
val prefs = prefs.value ?: run {
|
||||
callback(Result.failure(Exception("no prefs")))
|
||||
return@toggleShieldsUp
|
||||
}
|
||||
|
||||
val prefsOut = Ipn.MaskedPrefs()
|
||||
prefsOut.ShieldsUp = !prefs.ShieldsUp
|
||||
Client(scope).editPrefs(prefsOut, callback)
|
||||
}
|
||||
|
||||
fun IpnModel.setExitNodeId(id: String?, callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
val prefsOut = Ipn.MaskedPrefs()
|
||||
prefsOut.ExitNodeID = id
|
||||
Client(scope).editPrefs(prefsOut, callback)
|
||||
}
|
||||
|
||||
fun IpnModel.toggleRouteAll(callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
val prefs = prefs.value ?: run {
|
||||
callback(Result.failure(Exception("no prefs")))
|
||||
return@toggleRouteAll
|
||||
}
|
||||
|
||||
val prefsOut = Ipn.MaskedPrefs()
|
||||
prefsOut.RouteAll = !prefs.RouteAll
|
||||
Client(scope).editPrefs(prefsOut, callback)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,134 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package com.tailscale.ipn.ui.viewModel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tailscale.ipn.mdm.MDMSettings
|
||||
import com.tailscale.ipn.ui.localapi.Client
|
||||
import com.tailscale.ipn.ui.model.Ipn
|
||||
import com.tailscale.ipn.ui.model.IpnLocal
|
||||
import com.tailscale.ipn.ui.notifier.Notifier
|
||||
import com.tailscale.ipn.ui.util.set
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Base model for most models in this application. Provides common facilities for watching IPN
|
||||
* notifications, managing login/logout, updating preferences, etc.
|
||||
*/
|
||||
open class IpnViewModel : ViewModel() {
|
||||
companion object {
|
||||
val mdmSettings: StateFlow<MDMSettings> = MutableStateFlow(MDMSettings())
|
||||
}
|
||||
|
||||
protected val TAG = this::class.simpleName
|
||||
val loggedInUser: StateFlow<IpnLocal.LoginProfile?> = MutableStateFlow(null)
|
||||
val loginProfiles: StateFlow<List<IpnLocal.LoginProfile>?> = MutableStateFlow(null)
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
Notifier.state.collect {
|
||||
// Refresh the user profiles if we're transitioning out of the
|
||||
// NeedsLogin state.
|
||||
if (it == Ipn.State.NeedsLogin) {
|
||||
viewModelScope.launch { loadUserProfiles() }
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch { loadUserProfiles() }
|
||||
Log.d(TAG, "Created")
|
||||
}
|
||||
|
||||
private fun loadUserProfiles() {
|
||||
Client(viewModelScope).profiles { result ->
|
||||
result.onSuccess(loginProfiles::set).onFailure {
|
||||
Log.e(TAG, "Error loading profiles: ${it.message}")
|
||||
}
|
||||
}
|
||||
|
||||
Client(viewModelScope).currentProfile { result ->
|
||||
result.onSuccess(loggedInUser::set).onFailure {
|
||||
Log.e(TAG, "Error loading current profile: ${it.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login() {
|
||||
Client(viewModelScope).startLoginInteractive { result ->
|
||||
result.onSuccess {
|
||||
Log.d(TAG, "Login started: $it")
|
||||
}.onFailure {
|
||||
Log.e(TAG, "Error starting login: ${it.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
Client(viewModelScope).logout { result ->
|
||||
result.onSuccess {
|
||||
Log.d(TAG, "Logout started: $it")
|
||||
}.onFailure {
|
||||
Log.e(TAG, "Error starting logout: ${it.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The below handle all types of preference modifications typically invoked by the UI.
|
||||
// Callers generally shouldn't care about the returned prefs value - the source of
|
||||
// truth is the IPNModel, who's prefs flow will change in value to reflect the true
|
||||
// value of the pref setting in the back end (and will match the value returned here).
|
||||
// Generally, you will want to inspect the returned value in the callback for errors
|
||||
// to indicate why a particular setting did not change in the interface.
|
||||
//
|
||||
// Usage:
|
||||
// - User/Interface changed to new value. Render the new value.
|
||||
// - Submit the new value to the PrefsEditor
|
||||
// - Observe the prefs on the IpnModel and update the UI when/if the value changes.
|
||||
// For a typical flow, the changed value should reflect the value already shown.
|
||||
// - Inform the user of any error which may have occurred
|
||||
//
|
||||
// The "toggle' functions here will attempt to set the pref value to the inverse of
|
||||
// what is currently known in the IpnModel.prefs. If IpnModel.prefs is not available,
|
||||
// the callback will be called with a NO_PREFS error
|
||||
fun setWantRunning(wantRunning: Boolean, callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
Ipn.MaskedPrefs().WantRunning = wantRunning
|
||||
Client(viewModelScope).editPrefs(Ipn.MaskedPrefs(), callback)
|
||||
}
|
||||
|
||||
fun toggleCorpDNS(callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
val prefs = Notifier.prefs.value ?: run {
|
||||
callback(Result.failure(Exception("no prefs")))
|
||||
return@toggleCorpDNS
|
||||
}
|
||||
|
||||
val prefsOut = Ipn.MaskedPrefs()
|
||||
prefsOut.CorpDNS = !prefs.CorpDNS
|
||||
Client(viewModelScope).editPrefs(prefsOut, callback)
|
||||
}
|
||||
|
||||
fun toggleShieldsUp(callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
val prefs = Notifier.prefs.value ?: run {
|
||||
callback(Result.failure(Exception("no prefs")))
|
||||
return@toggleShieldsUp
|
||||
}
|
||||
|
||||
val prefsOut = Ipn.MaskedPrefs()
|
||||
prefsOut.ShieldsUp = !prefs.ShieldsUp
|
||||
Client(viewModelScope).editPrefs(prefsOut, callback)
|
||||
}
|
||||
|
||||
fun toggleRouteAll(callback: (Result<Ipn.Prefs>) -> Unit) {
|
||||
val prefs = Notifier.prefs.value ?: run {
|
||||
callback(Result.failure(Exception("no prefs")))
|
||||
return@toggleRouteAll
|
||||
}
|
||||
|
||||
val prefsOut = Ipn.MaskedPrefs()
|
||||
prefsOut.RouteAll = !prefs.RouteAll
|
||||
Client(viewModelScope).editPrefs(prefsOut, callback)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue