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" /> android:value="true" />
</service> </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 <meta-data
android:name="android.content.APP_RESTRICTIONS" android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/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.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import libtailscale.Libtailscale import libtailscale.Libtailscale
@ -52,6 +53,8 @@ import java.io.IOException
import java.net.NetworkInterface import java.net.NetworkInterface
import java.security.GeneralSecurityException import java.security.GeneralSecurityException
import java.util.Locale import java.util.Locale
import kotlin.time.Duration.Companion.hours
class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
companion object { companion object {
@ -178,6 +181,9 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
applicationScope.launch { applicationScope.launch {
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first() val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
} }
applicationScope.launch {
cleanupOldCacheFiles()
}
TSLog.init(this) TSLog.init(this)
FeatureFlags.initialize(mapOf("enable_new_search" to true)) FeatureFlags.initialize(mapOf("enable_new_search" to true))
} }
@ -335,6 +341,18 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
fun notifyPolicyChanged() { fun notifyPolicyChanged() {
app.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 * UninitializedApp contains all of the methods of App that can be used without having to initialize

@ -3,6 +3,7 @@
package com.tailscale.ipn package com.tailscale.ipn
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -14,6 +15,7 @@ import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.theme.AppTheme import com.tailscale.ipn.ui.theme.AppTheme
@ -27,6 +29,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
// ShareActivity is the entry point for Taildrop share intents // ShareActivity is the entry point for Taildrop share intents
class ShareActivity : ComponentActivity() { class ShareActivity : ComponentActivity() {
@ -57,11 +60,13 @@ class ShareActivity : ComponentActivity() {
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
setIntent(intent) setIntent(intent)
loadFiles() lifecycleScope.launch {
loadFiles()
}
} }
// Loads the files from the intent. // Loads the files from the intent.
fun loadFiles() { suspend fun loadFiles() {
if (intent == null) { if (intent == null) {
TSLog.e(TAG, "Share failure - No intent found") TSLog.e(TAG, "Share failure - No intent found")
return return
@ -76,6 +81,13 @@ class ShareActivity : ComponentActivity() {
// If EXTRA_STREAM is present, get the single URI for that stream // If EXTRA_STREAM is present, get the single URI for that stream
listOfNotNull(intent.versionSafeGetStreamUri()) 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 { else {
TSLog.e(TAG, "No extras found in intent - nothing to share") TSLog.e(TAG, "No extras found in intent - nothing to share")
emptyList() emptyList()
@ -140,4 +152,36 @@ private fun Intent.versionSafeGetStreamUris(): List<Uri> =
getParcelableArrayListExtra<Uri?>(Intent.EXTRA_STREAM) getParcelableArrayListExtra<Uri?>(Intent.EXTRA_STREAM)
} }
?.filterNotNull() ?.filterNotNull()
?: emptyList() ?: 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