android: Added the ability to share text as a .txt file

pull/690/head
Doug Melton 4 months ago
parent b1a58b37d3
commit cef7bae00b

@ -131,6 +131,16 @@
android:value="true" />
</service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.tailscale.ipn.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<meta-data
android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />

@ -45,6 +45,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import libtailscale.Libtailscale
@ -52,6 +53,8 @@ import java.io.IOException
import java.net.NetworkInterface
import java.security.GeneralSecurityException
import java.util.Locale
import kotlin.time.Duration.Companion.hours
class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
companion object {
@ -178,6 +181,9 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
applicationScope.launch {
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
}
applicationScope.launch {
cleanupOldCacheFiles()
}
TSLog.init(this)
FeatureFlags.initialize(mapOf("enable_new_search" to true))
}
@ -335,6 +341,18 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
fun notifyPolicyChanged() {
app.notifyPolicyChanged()
}
suspend fun cleanupOldCacheFiles() {
val maxAgeMs = 1.hours.inWholeMilliseconds
val cutoffTime = System.currentTimeMillis() - maxAgeMs
withContext(Dispatchers.IO) {
cacheDir.listFiles()?.forEach { file ->
if (file.name.startsWith("shared_text_") && file.lastModified() < cutoffTime) {
file.delete()
}
}
}
}
}
/**
* UninitializedApp contains all of the methods of App that can be used without having to initialize

@ -3,6 +3,7 @@
package com.tailscale.ipn
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
@ -14,6 +15,7 @@ import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.theme.AppTheme
@ -27,6 +29,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
// ShareActivity is the entry point for Taildrop share intents
class ShareActivity : ComponentActivity() {
@ -57,11 +60,13 @@ class ShareActivity : ComponentActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
lifecycleScope.launch {
loadFiles()
}
}
// Loads the files from the intent.
fun loadFiles() {
suspend fun loadFiles() {
if (intent == null) {
TSLog.e(TAG, "Share failure - No intent found")
return
@ -76,6 +81,13 @@ class ShareActivity : ComponentActivity() {
// If EXTRA_STREAM is present, get the single URI for that stream
listOfNotNull(intent.versionSafeGetStreamUri())
}
else if (intent.extras?.containsKey(Intent.EXTRA_TEXT) == true) {
// If EXTRA_TEXT is present, create a temporary file with the text content.
// This could be any shared text, like a URL or plain text from the clipboard.
val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: ""
val uri = createTemporaryFile(text)
listOf(uri)
}
else {
TSLog.e(TAG, "No extras found in intent - nothing to share")
emptyList()
@ -141,3 +153,35 @@ private fun Intent.versionSafeGetStreamUris(): List<Uri> =
}
?.filterNotNull()
?: emptyList()
/**
* Creates a temporary txt file in the app's cache directory with the given content.
* Then grants temporary read permission to the file using FileProvider and returns its URI.
*/
private suspend fun Context.createTemporaryFile(
content: String,
dir: File = cacheDir,
fileName: String = "shared_text_${System.currentTimeMillis()}.txt",
): Uri {
// Create temporary file in cache directory
val tempFile = File(dir, fileName)
withContext(Dispatchers.IO) {
tempFile.writeText(content)
}
// Get content URI using FileProvider
val uri = FileProvider.getUriForFile(
this,
"${applicationContext.packageName}.fileprovider",
tempFile
)
// Grant temporary read permission
grantUriPermission(
packageName,
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
return uri
}

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="shared" path="/" />
</paths>
Loading…
Cancel
Save