Makefile: move NDK_ROOT below ANDROID_HOME detection
If ANDROID_HOME is being detected by the code that finds a valid home with an empty host environment, then NDK_ROOT should be able to use that, but it was out of order in the evaluation. Updates #cleanup Signed-off-by: James Tucker <james@tailscale.com>kari/dirselmove
parent
e71641a422
commit
c887c926da
@ -0,0 +1,115 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package com.tailscale.ipn.ui.viewModel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.net.Uri
|
||||||
|
import android.net.VpnService
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.tailscale.ipn.App
|
||||||
|
import com.tailscale.ipn.util.ShareFileHelper
|
||||||
|
import com.tailscale.ipn.util.TSLog
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class AppViewModelFactory(val application: Application, private val taildropPrompt: Flow<Unit>) :
|
||||||
|
ViewModelProvider.Factory {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
if (modelClass.isAssignableFrom(AppViewModel::class.java)) {
|
||||||
|
return AppViewModel(application, taildropPrompt) as T
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application context-aware ViewModel used to track app-wide VPN and Taildrop state.
|
||||||
|
// This must be application-scoped because Tailscale may be enabled, disabled, or used for
|
||||||
|
// file transfers (Taildrop) outside the activity lifecycle.
|
||||||
|
//
|
||||||
|
// Responsibilities:
|
||||||
|
// - Track VPN preparation state (e.g., whether permission has been granted) and activity state
|
||||||
|
// - Monitor incoming Taildrop file transfers
|
||||||
|
// - Coordinate prompts for Taildrop directory selection if not yet configured
|
||||||
|
class AppViewModel(application: Application, private val taildropPrompt: Flow<Unit>) :
|
||||||
|
AndroidViewModel(application) {
|
||||||
|
// Whether the VPN is prepared. This is set to true if the VPN application is already prepared, or
|
||||||
|
// if the user has previously consented to the VPN application. This is used to determine whether
|
||||||
|
// a VPN permission launcher needs to be shown.
|
||||||
|
val _vpnPrepared = MutableStateFlow(false)
|
||||||
|
val vpnPrepared: StateFlow<Boolean> = _vpnPrepared
|
||||||
|
// Whether a VPN interface has been established. This is set by net.updateTUN upon
|
||||||
|
// VpnServiceBuilder.establish, and consumed by UI to reflect VPN state.
|
||||||
|
val _vpnActive = MutableStateFlow(false)
|
||||||
|
val vpnActive: StateFlow<Boolean> = _vpnActive
|
||||||
|
// Select Taildrop directory
|
||||||
|
var directoryPickerLauncher: ActivityResultLauncher<Uri?>? = null
|
||||||
|
private val _showDirectoryPickerInterstitial = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
|
val showDirectoryPickerInterstitial: SharedFlow<Unit> = _showDirectoryPickerInterstitial
|
||||||
|
val TAG = "AppViewModel"
|
||||||
|
|
||||||
|
init {
|
||||||
|
observeIncomingTaildrop()
|
||||||
|
prepareVpn()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeIncomingTaildrop() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
taildropPrompt.collect {
|
||||||
|
TSLog.d(TAG, "Taildrop event received, checking directory")
|
||||||
|
checkIfTaildropDirectorySelected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareVpn() {
|
||||||
|
// Check if the user has granted permission yet.
|
||||||
|
if (!vpnPrepared.value) {
|
||||||
|
val vpnIntent = VpnService.prepare(getApplication())
|
||||||
|
if (vpnIntent != null) {
|
||||||
|
setVpnPrepared(false)
|
||||||
|
Log.d(TAG, "VpnService.prepare returned non-null intent")
|
||||||
|
} else {
|
||||||
|
setVpnPrepared(true)
|
||||||
|
Log.d(TAG, "VpnService.prepare returned null intent, VPN is already prepared")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkIfTaildropDirectorySelected() {
|
||||||
|
val app = App.get()
|
||||||
|
val storedUri = app.getStoredDirectoryUri()
|
||||||
|
if (ShareFileHelper.hasValidTaildropDir()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val documentFile = storedUri?.let { DocumentFile.fromTreeUri(app, it) }
|
||||||
|
if (documentFile == null || !documentFile.exists() || !documentFile.canWrite()) {
|
||||||
|
TSLog.d(
|
||||||
|
"MainViewModel",
|
||||||
|
"Stored directory URI is invalid or inaccessible; launching directory picker.")
|
||||||
|
viewModelScope.launch { _showDirectoryPickerInterstitial.tryEmit(Unit) }
|
||||||
|
} else {
|
||||||
|
TSLog.d("MainViewModel", "Using stored directory URI: $storedUri")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVpnActive(isActive: Boolean) {
|
||||||
|
_vpnActive.value = isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVpnPrepared(isPrepared: Boolean) {
|
||||||
|
_vpnPrepared.value = isPrepared
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,65 +0,0 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
package com.tailscale.ipn.ui.viewModel
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.net.VpnService
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
|
|
||||||
class VpnViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
if (modelClass.isAssignableFrom(VpnViewModel::class.java)) {
|
|
||||||
return VpnViewModel(application) as T
|
|
||||||
}
|
|
||||||
throw IllegalArgumentException("Unknown ViewModel class")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Application context aware view model that tracks whether the VPN has been prepared. This must be
|
|
||||||
// application scoped because Tailscale might be toggled on and off outside of the activity
|
|
||||||
// lifecycle.
|
|
||||||
class VpnViewModel(application: Application) : AndroidViewModel(application) {
|
|
||||||
// Whether the VPN is prepared. This is set to true if the VPN application is already prepared, or
|
|
||||||
// if the user has previously consented to the VPN application. This is used to determine whether
|
|
||||||
// a VPN permission launcher needs to be shown.
|
|
||||||
val _vpnPrepared = MutableStateFlow(false)
|
|
||||||
val vpnPrepared: StateFlow<Boolean> = _vpnPrepared
|
|
||||||
// Whether a VPN interface has been established. This is set by net.updateTUN upon
|
|
||||||
// VpnServiceBuilder.establish, and consumed by UI to reflect VPN state.
|
|
||||||
val _vpnActive = MutableStateFlow(false)
|
|
||||||
val vpnActive: StateFlow<Boolean> = _vpnActive
|
|
||||||
val TAG = "VpnViewModel"
|
|
||||||
|
|
||||||
init {
|
|
||||||
prepareVpn()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun prepareVpn() {
|
|
||||||
// Check if the user has granted permission yet.
|
|
||||||
if (!vpnPrepared.value) {
|
|
||||||
val vpnIntent = VpnService.prepare(getApplication())
|
|
||||||
if (vpnIntent != null) {
|
|
||||||
setVpnPrepared(false)
|
|
||||||
Log.d(TAG, "VpnService.prepare returned non-null intent")
|
|
||||||
} else {
|
|
||||||
setVpnPrepared(true)
|
|
||||||
Log.d(TAG, "VpnService.prepare returned null intent, VPN is already prepared")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setVpnActive(isActive: Boolean) {
|
|
||||||
_vpnActive.value = isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setVpnPrepared(isPrepared: Boolean) {
|
|
||||||
_vpnPrepared.value = isPrepared
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue