From 109fb34df9851d94f698e073ba6154aec39adfbe Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Wed, 3 Sep 2025 11:00:31 -0700 Subject: [PATCH] all: add Makefile fmt and fmt-check targets, format all source code Signed-off-by: Michael Nahkies --- Makefile | 8 ++++ .../src/main/java/com/tailscale/ipn/App.kt | 46 +++++++++++++++++-- .../main/java/com/tailscale/ipn/IPNService.kt | 4 +- .../java/com/tailscale/ipn/MainActivity.kt | 2 +- .../java/com/tailscale/ipn/mdm/MDMSettings.kt | 3 +- .../com/tailscale/ipn/ui/localapi/Client.kt | 6 +-- .../java/com/tailscale/ipn/ui/model/Ipn.kt | 2 +- .../java/com/tailscale/ipn/ui/model/NetMap.kt | 2 +- .../com/tailscale/ipn/ui/model/TailCfg.kt | 2 +- .../ipn/ui/util/OutputStreamAdapter.kt | 1 - .../com/tailscale/ipn/ui/view/CustomLogin.kt | 3 +- .../com/tailscale/ipn/ui/view/SettingsView.kt | 2 +- .../tailscale/ipn/ui/view/UserSwitcherView.kt | 2 +- .../ipn/ui/viewModel/AppViewModel.kt | 2 +- .../ipn/ui/viewModel/IpnViewModel.kt | 6 ++- .../ipn/ui/viewModel/MainViewModel.kt | 20 ++++++-- .../com/tailscale/ipn/util/ShareFileHelper.kt | 30 +++++++++--- 17 files changed, 108 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index 273f4197d4..39be7ad94d 100644 --- a/Makefile +++ b/Makefile @@ -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..." diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 7e4e514bdd..f89821e402 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -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 = 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) { 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 { val mdmDisallowed = MDMSettings.excludedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList() @@ -553,4 +589,4 @@ open class UninitializedApp : Application() { // Android Connectivity Service https://github.com/tailscale/tailscale/issues/14128 "com.google.android.apps.scone", ) -} \ No newline at end of file +} diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index e861d9c533..e6eb995d71 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -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() } diff --git a/android/src/main/java/com/tailscale/ipn/MainActivity.kt b/android/src/main/java/com/tailscale/ipn/MainActivity.kt index 28ed413684..2de4873587 100644 --- a/android/src/main/java/com/tailscale/ipn/MainActivity.kt +++ b/android/src/main/java/com/tailscale/ipn/MainActivity.kt @@ -224,7 +224,7 @@ class MainActivity : ComponentActivity() { appViewModel.directoryPickerLauncher = directoryPickerLauncher setContent { - var showDialog by remember { mutableStateOf(false) } + var showDialog by remember { mutableStateOf(false) } LaunchedEffect(Unit) { appViewModel.triggerDirectoryPicker.collect { showDialog = true } } diff --git a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt index d8df61d1f1..34b341fc76 100644 --- a/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt +++ b/android/src/main/java/com/tailscale/ipn/mdm/MDMSettings.kt @@ -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") diff --git a/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt b/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt index 2a30db4e5c..5b38b6a729 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/localapi/Client.kt @@ -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" diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt b/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt index a0b5c1b50e..38acc7fac9 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/Ipn.kt @@ -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 { diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/NetMap.kt b/android/src/main/java/com/tailscale/ipn/ui/model/NetMap.kt index f7a7a925e3..861e64c907 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/NetMap.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/NetMap.kt @@ -55,6 +55,6 @@ class Netmap { fun hasCap(capability: String): Boolean { return AllCaps.contains(capability) - } + } } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt b/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt index 2e9be750e2..511011cdcd 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt @@ -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 diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/OutputStreamAdapter.kt b/android/src/main/java/com/tailscale/ipn/ui/util/OutputStreamAdapter.kt index 2a9c2b2098..9e73a42837 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/OutputStreamAdapter.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/OutputStreamAdapter.kt @@ -24,4 +24,3 @@ class OutputStreamAdapter(private val outputStream: OutputStream) : libtailscale outputStream.close() } } - diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/CustomLogin.kt b/android/src/main/java/com/tailscale/ipn/ui/view/CustomLogin.kt index 83ae0b8cc3..2f8e60748b 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/CustomLogin.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/CustomLogin.kt @@ -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( diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt index c98f18c1f8..2dc187fc26 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt @@ -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( diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt index 8abfc943ad..64d4bd4d9c 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/UserSwitcherView.kt @@ -65,7 +65,7 @@ fun UserSwitcherView(nav: UserSwitcherNav, viewModel: UserSwitcherViewModel = vi val capabilityIsOwner = "https://tailscale.com/cap/is-owner" val isOwner = netmapState?.hasCap(capabilityIsOwner) == true - Scaffold( + Scaffold( topBar = { Header( R.string.accounts, diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/AppViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/AppViewModel.kt index 685c50d861..b5913a3474 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/AppViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/AppViewModel.kt @@ -116,4 +116,4 @@ class AppViewModel(application: Application, private val taildropPrompt: Flow = 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) { // No intent means we're already authorized vpnPermissionLauncher = launcher @@ -243,4 +255,4 @@ private fun userStringRes(currentState: State?, previousState: State?, vpnActive currentState == State.Running -> if (vpnActive) R.string.connected else R.string.placeholder else -> R.string.placeholder } -} \ No newline at end of file +} diff --git a/android/src/main/java/com/tailscale/ipn/util/ShareFileHelper.kt b/android/src/main/java/com/tailscale/ipn/util/ShareFileHelper.kt index e46738946b..d14f7911cb 100644 --- a/android/src/main/java/com/tailscale/ipn/util/ShareFileHelper.kt +++ b/android/src/main/java/com/tailscale/ipn/util/ShareFileHelper.kt @@ -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 { 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() @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 { 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() } -} \ No newline at end of file +}