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