// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package com.tailscale.ipn import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.OpenableColumns import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.theme.AppTheme import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.util.universalFit import com.tailscale.ipn.ui.view.TaildropView import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow // ShareActivity is the entry point for Taildrop share intents class ShareActivity : ComponentActivity() { private val TAG = ShareActivity::class.simpleName private val requestedTransfers: StateFlow> = MutableStateFlow(emptyList()) override fun onCreate(state: Bundle?) { super.onCreate(state) setContent { AppTheme { Surface(color = MaterialTheme.colorScheme.inverseSurface) { // Background for the letterbox Surface(modifier = Modifier.universalFit()) { TaildropView(requestedTransfers, (application as App).applicationScope) } } } } } override fun onStart() { super.onStart() Notifier.start(lifecycleScope) loadFiles() } override fun onStop() { super.onStop() Notifier.stop() } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) setIntent(intent) loadFiles() } // Loads the files from the intent. fun loadFiles() { if (intent == null) { Log.e(TAG, "Share failure - No intent found") return } val act = intent.action val uris: List? uris = when (act) { Intent.ACTION_SEND -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { listOf(intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)) } else { listOf(intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri) } } Intent.ACTION_SEND_MULTIPLE -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java) } else { intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) } } else -> { Log.e(TAG, "No extras found in intent - nothing to share") null } } val pendingFiles: List = uris?.filterNotNull()?.mapNotNull { contentResolver?.query(it, null, null, null, null)?.let { c -> val nameCol = c.getColumnIndex(OpenableColumns.DISPLAY_NAME) val sizeCol = c.getColumnIndex(OpenableColumns.SIZE) c.moveToFirst() val name = c.getString(nameCol) val size = c.getLong(sizeCol) c.close() val file = Ipn.OutgoingFile(Name = name, DeclaredSize = size) file.uri = it file } } ?: emptyList() if (pendingFiles.isEmpty()) { Log.e(TAG, "Share failure - no files extracted from intent") } requestedTransfers.set(pendingFiles) } }