all: add Makefile fmt and fmt-check targets, format all source code

Signed-off-by: Michael Nahkies <michael@nahkies.co.nz>
pull/700/head
Michael Nahkies 3 months ago committed by Brad Fitzpatrick
parent 53b746220b
commit 981f5e8770

@ -316,6 +316,14 @@ checkandroidsdk: ## Check that Android SDK is installed
test: gradle-dependencies ## Run the Android tests
(cd android && ./gradlew test)
.PHONY: fmt
fmt: gradle-dependencies ## Format the Android code
(cd android && ./gradlew ktfmtFormat)
.PHONY: fmt-check
fmt-check: gradle-dependencies ## Check the Android code is formatted
(cd android && ./gradlew ktfmtCheck)
.PHONY: emulator
emulator: ## Start an android emulator instance
@echo "Checking installed SDK packages..."

@ -1,6 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn
import android.Manifest
import android.app.Application
import android.app.Notification
@ -37,6 +38,10 @@ import com.tailscale.ipn.ui.viewModel.AppViewModelFactory
import com.tailscale.ipn.util.FeatureFlags
import com.tailscale.ipn.util.ShareFileHelper
import com.tailscale.ipn.util.TSLog
import java.io.IOException
import java.net.NetworkInterface
import java.security.GeneralSecurityException
import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@ -48,12 +53,10 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import libtailscale.Libtailscale
import java.io.IOException
import java.net.NetworkInterface
import java.security.GeneralSecurityException
import java.util.Locale
class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
companion object {
private const val FILE_CHANNEL_ID = "tailscale-files"
// Key to store the SAF URI in EncryptedSharedPreferences.
@ -70,26 +73,34 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
return appInstance
}
}
val dns = DnsConfig()
private lateinit var connectivityManager: ConnectivityManager
private lateinit var mdmChangeReceiver: MDMSettingsChangedReceiver
private lateinit var app: libtailscale.Application
override val viewModelStore: ViewModelStore
get() = appViewModelStore
private val appViewModelStore: ViewModelStore by lazy { ViewModelStore() }
var healthNotifier: HealthNotifier? = null
override fun getPlatformDNSConfig(): String = dns.dnsConfigAsString
override fun getInstallSource(): String = AppSourceChecker.getInstallSource(this)
override fun shouldUseGoogleDNSFallback(): Boolean = BuildConfig.USE_GOOGLE_DNS_FALLBACK
override fun log(s: String, s1: String) {
Log.d(s, s1)
}
fun getLibtailscaleApp(): libtailscale.Application {
if (!isInitialized) {
initOnce() // Calls the synchronized initialization logic
}
return app
}
override fun onCreate() {
super.onCreate()
appInstance = this
@ -113,6 +124,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
getString(R.string.health_channel_description),
NotificationManagerCompat.IMPORTANCE_HIGH)
}
override fun onTerminate() {
super.onTerminate()
Notifier.stop()
@ -121,7 +133,9 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
viewModelStore.clear()
unregisterReceiver(mdmChangeReceiver)
}
@Volatile private var isInitialized = false
@Synchronized
private fun initOnce() {
if (isInitialized) {
@ -130,6 +144,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
initializeApp()
isInitialized = true
}
private fun initializeApp() {
// Check if a directory URI has already been stored.
val storedUri = getStoredDirectoryUri()
@ -244,6 +259,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)
}
fun getStoredDirectoryUri(): Uri? {
val uriString = getEncryptedPrefs().getString(PREF_KEY_SAF_URI, null)
return uriString?.let { Uri.parse(it) }
@ -258,6 +274,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
QuickToggleService.updateTile()
TSLog.d("App", "Set Tile Ready: $ableToStartVPN")
}
override fun getModelName(): String {
val manu = Build.MANUFACTURER
var model = Build.MODEL
@ -268,10 +285,13 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
}
return "$manu $model"
}
override fun getOSVersion(): String = Build.VERSION.RELEASE
override fun isChromeOS(): Boolean {
return packageManager.hasSystemFeature("android.hardware.type.pc")
}
override fun getInterfacesAsString(): String {
val interfaces: ArrayList<NetworkInterface> =
java.util.Collections.list(NetworkInterface.getNetworkInterfaces())
@ -303,11 +323,13 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
}
return sb.toString()
}
@Throws(
IOException::class, GeneralSecurityException::class, MDMSettings.NoSuchKeyException::class)
override fun getSyspolicyBooleanValue(key: String): Boolean {
return getSyspolicyStringValue(key) == "true"
}
@Throws(
IOException::class, GeneralSecurityException::class, MDMSettings.NoSuchKeyException::class)
override fun getSyspolicyStringValue(key: String): String {
@ -317,6 +339,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
}
return setting.value?.toString() ?: ""
}
@Throws(
IOException::class, GeneralSecurityException::class, MDMSettings.NoSuchKeyException::class)
override fun getSyspolicyStringArrayJSONValue(key: String): String {
@ -332,6 +355,7 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
throw MDMSettings.NoSuchKeyException()
}
}
fun notifyPolicyChanged() {
app.notifyPolicyChanged()
}
@ -374,9 +398,11 @@ open class UninitializedApp : Application() {
}
}
}
protected fun setUnprotectedInstance(instance: UninitializedApp) {
appInstance = instance
}
protected fun setAbleToStartVPN(rdy: Boolean) {
getUnencryptedPrefs().edit().putBoolean(ABLE_TO_START_VPN_KEY, rdy).apply()
}
@ -384,9 +410,11 @@ open class UninitializedApp : Application() {
fun isAbleToStartVPN(): Boolean {
return getUnencryptedPrefs().getBoolean(ABLE_TO_START_VPN_KEY, false)
}
private fun getUnencryptedPrefs(): SharedPreferences {
return getSharedPreferences(UNENCRYPTED_PREFERENCES, MODE_PRIVATE)
}
fun startVPN() {
val intent = Intent(this, IPNService::class.java).apply { action = IPNService.ACTION_START_VPN }
// FLAG_UPDATE_CURRENT ensures that if the intent is already pending, the existing intent will
@ -411,6 +439,7 @@ open class UninitializedApp : Application() {
TSLog.e(TAG, "startVPN hit exception: $e")
}
}
fun stopVPN() {
val intent = Intent(this, IPNService::class.java).apply { action = IPNService.ACTION_STOP_VPN }
try {
@ -421,6 +450,7 @@ open class UninitializedApp : Application() {
TSLog.e(TAG, "stopVPN hit exception in startService(): $e")
}
}
fun restartVPN() {
val intent =
Intent(this, IPNService::class.java).apply { action = IPNService.ACTION_RESTART_VPN }
@ -432,12 +462,14 @@ open class UninitializedApp : Application() {
TSLog.e(TAG, "restartVPN hit exception in startService(): $e")
}
}
fun createNotificationChannel(id: String, name: String, description: String, importance: Int) {
val channel = NotificationChannel(id, name, importance)
channel.description = description
notificationManager = NotificationManagerCompat.from(this)
notificationManager.createNotificationChannel(channel)
}
fun notifyStatus(
vpnRunning: Boolean,
hideDisconnectAction: Boolean,
@ -445,6 +477,7 @@ open class UninitializedApp : Application() {
) {
notifyStatus(buildStatusNotification(vpnRunning, hideDisconnectAction, exitNodeName))
}
fun notifyStatus(notification: Notification) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED) {
@ -459,6 +492,7 @@ open class UninitializedApp : Application() {
}
notificationManager.notify(STATUS_NOTIFICATION_ID, notification)
}
fun buildStatusNotification(
vpnRunning: Boolean,
hideDisconnectAction: Boolean,
@ -504,6 +538,7 @@ open class UninitializedApp : Application() {
}
return builder.build()
}
fun updateUserDisallowedPackageNames(packageNames: List<String>) {
if (packageNames.any { it.isEmpty() }) {
TSLog.e(TAG, "updateUserDisallowedPackageNames called with empty packageName(s)")
@ -512,6 +547,7 @@ open class UninitializedApp : Application() {
getUnencryptedPrefs().edit().putStringSet(DISALLOWED_APPS_KEY, packageNames.toSet()).apply()
this.restartVPN()
}
fun disallowedPackageNames(): List<String> {
val mdmDisallowed =
MDMSettings.excludedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList()

@ -12,12 +12,12 @@ import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.util.TSLog
import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import libtailscale.Libtailscale
import java.util.UUID
open class IPNService : VpnService(), libtailscale.IPNService {
private val TAG = "IPNService"
@ -47,7 +47,7 @@ open class IPNService : VpnService(), libtailscale.IPNService {
START_NOT_STICKY
}
ACTION_RESTART_VPN -> {
app.setWantRunning(false){
app.setWantRunning(false) {
close()
app.startVPN()
}

@ -61,7 +61,8 @@ object MDMSettings {
// Handled on the backend
val deviceSerialNumber =
StringMDMSetting("DeviceSerialNumber", "Serial number of the device that is running Tailscale")
StringMDMSetting(
"DeviceSerialNumber", "Serial number of the device that is running Tailscale")
val useTailscaleDNSSettings =
AlwaysNeverUserDecidesMDMSetting("UseTailscaleDNSSettings", "Use Tailscale DNS Settings")

@ -14,6 +14,9 @@ import com.tailscale.ipn.ui.model.StableNodeID
import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.util.InputStreamAdapter
import com.tailscale.ipn.util.TSLog
import java.nio.charset.Charset
import kotlin.reflect.KType
import kotlin.reflect.typeOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -23,9 +26,6 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.serializer
import libtailscale.FilePart
import java.nio.charset.Charset
import kotlin.reflect.KType
import kotlin.reflect.typeOf
private object Endpoint {
const val DEBUG = "debug"

@ -4,9 +4,9 @@
package com.tailscale.ipn.ui.model
import android.net.Uri
import java.util.UUID
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.util.UUID
class Ipn {

@ -15,9 +15,9 @@ import com.tailscale.ipn.ui.util.DisplayAddress
import com.tailscale.ipn.ui.util.TimeUtil
import com.tailscale.ipn.ui.util.flag
import com.tailscale.ipn.ui.viewModel.PeerSettingInfo
import java.util.Date
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import java.util.Date
class Tailcfg {
@Serializable

@ -24,4 +24,3 @@ class OutputStreamAdapter(private val outputStream: OutputStream) : libtailscale
outputStream.close()
}
}

@ -145,8 +145,7 @@ fun LoginView(
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None, imeAction = ImeAction.Go),
keyboardActions =
KeyboardActions(onGo = { onSubmitAction(textVal) }))
keyboardActions = KeyboardActions(onGo = { onSubmitAction(textVal) }))
})
ListItem(

@ -38,9 +38,9 @@ import com.tailscale.ipn.ui.util.AndroidTVUtil.isAndroidTV
import com.tailscale.ipn.ui.util.AppVersion
import com.tailscale.ipn.ui.util.Lists
import com.tailscale.ipn.ui.util.set
import com.tailscale.ipn.ui.viewModel.AppViewModel
import com.tailscale.ipn.ui.viewModel.SettingsNav
import com.tailscale.ipn.ui.viewModel.SettingsViewModel
import com.tailscale.ipn.ui.viewModel.AppViewModel
@Composable
fun SettingsView(

@ -147,9 +147,11 @@ open class IpnViewModel : ViewModel() {
/**
* Order of operations:
* 1. editPrefs() with maskedPrefs (to allow ControlURL override), WantRunning=true, LoggedOut=false if AuthKey != null
* 1. editPrefs() with maskedPrefs (to allow ControlURL override), WantRunning=true,
* LoggedOut=false if AuthKey != null
* 2. start() starts the LocalBackend state machine
* 3. startLoginInteractive() is currently required for bother interactive and non-interactive (using auth key) login
* 3. startLoginInteractive() is currently required for bother interactive and non-interactive
* (using auth key) login
*
* Any failure shortcircuits the chain and invokes completionHandler once.
*/

@ -1,6 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.viewModel
import android.content.Intent
import android.net.Uri
import android.net.VpnService
@ -10,7 +11,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@ -21,12 +21,12 @@ import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Ipn.State
import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.util.AndroidTVUtil
import com.tailscale.ipn.ui.util.PeerCategorizer
import com.tailscale.ipn.ui.util.PeerSet
import com.tailscale.ipn.ui.util.TimeUtil
import com.tailscale.ipn.ui.util.set
import com.tailscale.ipn.util.TSLog
import java.time.Duration
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
@ -35,7 +35,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import java.time.Duration
class MainViewModelFactory(private val appViewModel: AppViewModel) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -91,18 +90,23 @@ class MainViewModel(private val appViewModel: AppViewModel) : IpnViewModel() {
// Icon displayed in the button to present the health view
val healthIcon: StateFlow<Int?> = MutableStateFlow(null)
fun updateSearchTerm(term: String) {
_searchTerm.value = term
}
fun hidePeerDropdownMenu() {
expandedMenuPeer.set(null)
}
fun copyIpAddress(peer: Tailcfg.Node, clipboardManager: ClipboardManager) {
clipboardManager.setText(AnnotatedString(peer.primaryIPv4Address ?: ""))
}
fun startPing(peer: Tailcfg.Node) {
this.pingViewModel.startPing(peer)
}
fun onPingDismissal() {
this.pingViewModel.handleDismissal()
}
@ -112,7 +116,9 @@ class MainViewModel(private val appViewModel: AppViewModel) : IpnViewModel() {
val v = MDMSettings.authKey.flow.value.value
return v != null && v != ""
}
private val peerCategorizer = PeerCategorizer()
init {
viewModelScope.launch {
var previousState: State? = null
@ -173,9 +179,11 @@ class MainViewModel(private val appViewModel: AppViewModel) : IpnViewModel() {
App.get().healthNotifier?.currentIcon?.collect { icon -> healthIcon.set(icon) }
}
}
fun maybeRequestVpnPermission() {
_requestVpnPermission.value = true
}
fun showVPNPermissionLauncherIfUnauthorized() {
val vpnIntent = VpnService.prepare(App.get())
TSLog.d("VpnPermissions", "vpnIntent=$vpnIntent")
@ -215,15 +223,19 @@ class MainViewModel(private val appViewModel: AppViewModel) : IpnViewModel() {
}
}
}
fun searchPeers(searchTerm: String) {
this.searchTerm.set(searchTerm)
}
fun enableSearchAutoFocus() {
autoFocusSearch = true
}
fun disableSearchAutoFocus() {
autoFocusSearch = false
}
fun setVpnPermissionLauncher(launcher: ActivityResultLauncher<Intent>) {
// No intent means we're already authorized
vpnPermissionLauncher = launcher

@ -1,6 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.util
import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
@ -9,6 +10,11 @@ import androidx.documentfile.provider.DocumentFile
import com.tailscale.ipn.TaildropDirectoryStore
import com.tailscale.ipn.ui.util.InputStreamAdapter
import com.tailscale.ipn.ui.util.OutputStreamAdapter
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@ -17,11 +23,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import libtailscale.Libtailscale
import org.json.JSONObject
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
data class SafFile(val fd: Int, val uri: String)
object ShareFileHelper : libtailscale.ShareFileHelper {
@ -96,6 +98,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
val os = context.contentResolver.openOutputStream(file.uri, "rw")
return file.uri.toString() to os
}
@Throws(IOException::class)
private fun openWriterFD(fileName: String, offset: Long): Pair<String, SeekableOutputStream> {
val ctx = appContext ?: throw IOException("App context not initialized")
@ -114,6 +117,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
if (offset != 0L) fos.channel.position(offset) else fos.channel.truncate(0)
return file.uri.toString() to SeekableOutputStream(fos, pfd)
}
private val currentUri = ConcurrentHashMap<String, String>()
@Throws(IOException::class)
@ -143,6 +147,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
currentUri[fileName] = uri
return uri
}
@Throws(IOException::class)
override fun renameFile(oldPath: String, targetName: String): String {
val ctx = appContext ?: throw IOException("not initialized")
@ -190,6 +195,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
cleanupPartials(dir, targetName)
return dest.uri.toString()
}
private fun lengthOfUri(ctx: Context, uri: Uri): Long =
ctx.contentResolver.openAssetFileDescriptor(uri, "r").use { it?.length ?: -1 }
// delete any stray “.partial” files for this base name
@ -201,6 +207,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
}
}
}
@Throws(IOException::class)
override fun deleteFile(uri: String) {
runBlocking { waitUntilTaildropDirReady() }
@ -213,6 +220,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
throw IOException("DeleteFile: delete() returned false for $uri")
}
}
@Throws(IOException::class)
override fun getFileInfo(fileName: String): String {
val context = appContext ?: throw IOException("app context not initialized")
@ -227,9 +235,11 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
val modTime = file.lastModified()
return """{"name":${JSONObject.quote(name)},"size":$size,"modTime":$modTime}"""
}
private fun jsonEscape(s: String): String {
return JSONObject.quote(s)
}
fun generateNewFilename(filename: String): String {
val dotIndex = filename.lastIndexOf('.')
val baseName = if (dotIndex != -1) filename.substring(0, dotIndex) else filename
@ -237,6 +247,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
val uuid = UUID.randomUUID()
return "$baseName-$uuid$extension"
}
fun listPartialFiles(suffix: String): Array<String> {
val context = appContext ?: return emptyArray()
val rootUri = savedUri ?: return emptyArray()
@ -246,6 +257,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
.mapNotNull { it.name }
.toTypedArray()
}
@Throws(IOException::class)
override fun listFilesJSON(suffix: String): String {
val list = listPartialFiles(suffix)
@ -254,6 +266,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
}
return list.joinToString(prefix = "[\"", separator = "\",\"", postfix = "\"]")
}
@Throws(IOException::class)
override fun openFileReader(name: String): libtailscale.InputStream {
val context = appContext ?: throw IOException("app context not initialized")
@ -282,11 +295,15 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
private val pfd: ParcelFileDescriptor
) : OutputStream() {
private var closed = false
override fun write(b: Int) = fos.write(b)
override fun write(b: ByteArray) = fos.write(b)
override fun write(b: ByteArray, off: Int, len: Int) {
fos.write(b, off, len)
}
override fun close() {
if (!closed) {
closed = true
@ -299,6 +316,7 @@ object ShareFileHelper : libtailscale.ShareFileHelper {
}
}
}
override fun flush() = fos.flush()
}
}
Loading…
Cancel
Save