Skip to content

Commit c2300f0

Browse files
committed
feat: implement receiving bulk files
1 parent 1571d5f commit c2300f0

File tree

3 files changed

+151
-54
lines changed

3 files changed

+151
-54
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,16 @@
5555
android:launchMode="singleTask"
5656
android:theme="@style/TranslucentTheme.Dark"
5757
android:excludeFromRecents="true">
58+
<!-- Single file share -->
5859
<intent-filter>
5960
<action android:name="android.intent.action.SEND" />
6061
<category android:name="android.intent.category.DEFAULT" />
61-
<data android:mimeType="text/plain" />
62-
</intent-filter>
63-
<intent-filter>
64-
<action android:name="android.intent.action.SEND" />
65-
<category android:name="android.intent.category.DEFAULT" />
66-
<data android:mimeType="images/*" />
62+
<data android:mimeType="*/*" />
6763
</intent-filter>
64+
65+
<!-- Multiple file share -->
6866
<intent-filter>
69-
<action android:name="android.intent.action.SEND" />
67+
<action android:name="android.intent.action.SEND_MULTIPLE" />
7068
<category android:name="android.intent.category.DEFAULT" />
7169
<data android:mimeType="*/*" />
7270
</intent-filter>

app/src/main/java/com/castle/sefirah/presentation/deeplink/ShareDeepLinkActivity.kt

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import kotlinx.coroutines.launch
1212
import sefirah.domain.model.ClipboardMessage
1313
import sefirah.network.FileTransferService
1414
import sefirah.network.FileTransferService.Companion.ACTION_SEND_FILE
15+
import sefirah.network.FileTransferService.Companion.ACTION_SEND_BULK_FILES
16+
import sefirah.network.FileTransferService.Companion.EXTRA_FILE_URIS
1517

1618
@AndroidEntryPoint
1719
class ShareDeepLinkActivity: BaseActivity() {
@@ -44,20 +46,35 @@ class ShareDeepLinkActivity: BaseActivity() {
4446
}
4547

4648
private fun handleIntent(intent: Intent?) {
47-
if (intent?.action == Intent.ACTION_SEND) {
48-
Log.d("ShareDeepLinkActivity", "Handling intent: ${intent.type}")
49-
when {
50-
intent.type?.startsWith("text/plain") == true -> handleText(intent)
51-
intent.type?.startsWith("image/") == true -> handleFileTransfer(intent)
52-
intent.type?.startsWith("video/") == true -> handleFileTransfer(intent)
53-
intent.type?.startsWith("application/") == true -> handleFileTransfer(intent)
54-
else -> {
55-
handleFileTransfer(intent)
49+
when (intent?.action) {
50+
Intent.ACTION_SEND -> {
51+
Log.d("ShareDeepLinkActivity", "Handling single share intent: ${intent.type}")
52+
when {
53+
intent.type?.startsWith("text/plain") == true -> handleText(intent)
54+
intent.type?.startsWith("image/") == true -> handleSingleFileTransfer(intent)
55+
intent.type?.startsWith("video/") == true -> handleSingleFileTransfer(intent)
56+
intent.type?.startsWith("application/") == true -> handleSingleFileTransfer(intent)
57+
else -> {
58+
handleSingleFileTransfer(intent)
59+
}
5660
}
5761
}
58-
} else {
59-
Log.e("ShareToPc", "Unsupported intent action: ${intent?.action}")
60-
finishAffinity()
62+
Intent.ACTION_SEND_MULTIPLE -> {
63+
Log.d("ShareDeepLinkActivity", "Handling multiple share intent: ${intent.type}")
64+
when {
65+
intent.type?.startsWith("text/plain") == true -> handleText(intent)
66+
intent.type?.startsWith("image/") == true -> handleMultipleFileTransfer(intent)
67+
intent.type?.startsWith("video/") == true -> handleMultipleFileTransfer(intent)
68+
intent.type?.startsWith("application/") == true -> handleMultipleFileTransfer(intent)
69+
else -> {
70+
handleMultipleFileTransfer(intent)
71+
}
72+
}
73+
}
74+
else -> {
75+
Log.e("ShareToPc", "Unsupported intent action: ${intent?.action}")
76+
finishAffinity()
77+
}
6178
}
6279
}
6380

@@ -73,9 +90,9 @@ class ShareDeepLinkActivity: BaseActivity() {
7390
}
7491
}
7592

76-
private fun handleFileTransfer(intent: Intent) {
93+
private fun handleSingleFileTransfer(intent: Intent) {
7794
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
78-
Log.d("ShareToPc", "Handling file share: $uri")
95+
Log.d("ShareToPc", "Handling single file share: $uri")
7996

8097
if (uri != null) {
8198
val serviceIntent = Intent(applicationContext, FileTransferService::class.java).apply {
@@ -88,4 +105,20 @@ class ShareDeepLinkActivity: BaseActivity() {
88105
finishAffinity()
89106
}
90107
}
108+
109+
private fun handleMultipleFileTransfer(intent: Intent) {
110+
val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
111+
Log.d("ShareToPc", "Handling multiple file share: ${uris?.size} files")
112+
113+
if (!uris.isNullOrEmpty()) {
114+
val serviceIntent = Intent(applicationContext, FileTransferService::class.java).apply {
115+
action = ACTION_SEND_BULK_FILES
116+
putParcelableArrayListExtra(EXTRA_FILE_URIS, uris)
117+
}
118+
startForegroundService(serviceIntent)
119+
} else {
120+
Log.e("ShareToPc", "Received null or empty URIs")
121+
finishAffinity()
122+
}
123+
}
91124
}

core/network/src/main/java/sefirah/network/FileTransferService.kt

Lines changed: 99 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import java.io.File
5151
import java.io.IOException
5252
import java.io.InputStream
5353
import java.io.InputStreamReader
54+
import java.lang.Thread.sleep
5455
import java.net.Socket
5556
import javax.inject.Inject
5657
import javax.net.ssl.SSLServerSocket
@@ -109,6 +110,22 @@ class FileTransferService : Service() {
109110
}
110111
}
111112

113+
ACTION_SEND_BULK_FILES -> {
114+
val fileUris = intent.getParcelableArrayListExtra<Uri>(EXTRA_FILE_URIS) ?: run {
115+
Log.e(TAG, "No URIs provided in intent")
116+
return START_NOT_STICKY
117+
}
118+
serviceScope.launch {
119+
try {
120+
Log.d(TAG, "Starting bulk file transfer process for ${fileUris.size} files")
121+
sendBulkFiles(fileUris)
122+
} catch (e: Exception) {
123+
Log.e(TAG, "Bulk file transfer failed", e)
124+
updateNotificationForError(e.message ?: "Bulk transfer failed")
125+
}
126+
}
127+
}
128+
112129
ACTION_RECEIVE_FILE -> {
113130
val fileTransfer = intent.getParcelableExtra<FileTransfer>(EXTRA_FILE_TRANSFER)
114131
val bulkTransfer = intent.getParcelableExtra<BulkFileTransfer>(EXTRA_BULK_TRANSFER)
@@ -143,62 +160,109 @@ class FileTransferService : Service() {
143160
}
144161

145162
private suspend fun sendFile(fileUri: Uri) {
146-
val fileName = getFileMetadata(this, fileUri).fileName
147-
setupNotification(TransferType.Sending(fileName))
148-
try {
149-
val password = generateRandomPassword()
150-
val serverInfo = initializeServer(password) ?: return
151-
fileInputStream = this.contentResolver.openInputStream(fileUri)
152-
val fileMetadata = getFileMetadata(this, fileUri)
163+
val fileMetadata = getFileMetadata(this, fileUri)
164+
setupNotification(TransferType.Sending(fileMetadata.fileName))
165+
166+
val password = generateRandomPassword()
167+
val serverInfo = initializeServer(password) ?: return
168+
networkManager.sendMessage(FileTransfer(serverInfo, fileMetadata))
169+
170+
sendFileInternal(
171+
fileUris = listOf(fileUri),
172+
filesMetadata = listOf(fileMetadata),
173+
password = password,
174+
isBulk = false
175+
)
176+
}
153177

154-
val message = FileTransfer(serverInfo, fileMetadata)
155-
networkManager.sendMessage(message)
178+
private suspend fun sendBulkFiles(fileUris: List<Uri>) {
179+
val filesMetadata = fileUris.map { getFileMetadata(this, it) }
180+
setupNotification(TransferType.Sending("${filesMetadata.size} files"))
181+
182+
val password = generateRandomPassword()
183+
val serverInfo = initializeServer(password) ?: return
184+
networkManager.sendMessage(BulkFileTransfer(serverInfo, filesMetadata))
185+
186+
sendFileInternal(
187+
fileUris = fileUris,
188+
filesMetadata = filesMetadata,
189+
password = password,
190+
isBulk = true
191+
)
192+
}
156193

194+
private suspend fun sendFileInternal(
195+
fileUris: List<Uri>,
196+
filesMetadata: List<FileMetadata>,
197+
password: String,
198+
isBulk: Boolean
199+
) {
200+
try {
157201
withContext(Dispatchers.IO) {
158202
socket = serverSocket?.accept() as? SSLSocket
159203
?: throw IOException("Failed to accept SSL connection")
160204

161205
val outputStream = socket!!.getOutputStream()
162206
val inputStream = socket!!.getInputStream()
163-
164207
val reader = BufferedReader(InputStreamReader(inputStream, Charsets.UTF_8))
165208

166209
withTimeout(5000) {
167210
if (reader.readLine() != password) throw IOException("Invalid password")
168211
}
169212

170-
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
171-
var bytesRead: Int
213+
val totalSize = filesMetadata.sumOf { it.fileSize }
172214
var totalBytesTransferred = 0L
173215

174-
try {
175-
// Send the file data
216+
fileUris.forEachIndexed { index, fileUri ->
217+
val fileMetadata = filesMetadata[index]
218+
219+
if (isBulk) {
220+
setupNotification(TransferType.Sending("${fileMetadata.fileName} (${index + 1}/${fileUris.size})"))
221+
}
222+
fileInputStream = contentResolver.openInputStream(fileUri)
223+
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
224+
var bytesRead: Int
225+
outputStream.flush()
226+
176227
while (fileInputStream?.read(buffer).also { bytesRead = it ?: -1 } != -1) {
177228
outputStream.write(buffer, 0, bytesRead)
178229
outputStream.flush()
179230

180231
totalBytesTransferred += bytesRead
181-
updateTransferProgress(totalBytesTransferred, fileMetadata.fileSize)
232+
updateTransferProgress(totalBytesTransferred, totalSize)
182233
}
183-
184-
// Signal end of file stream
185234
outputStream.flush()
186-
socket?.shutdownOutput()
235+
fileInputStream?.close()
236+
fileInputStream = null
187237

188238
withTimeout(5000) {
189-
if (reader.readLine() == "Complete") {
190-
delay(1000)
191-
withContext(Dispatchers.Main) {
192-
showCompletedNotification()
193-
}
194-
} else {
195-
throw IOException("Invalid confirmation received")
239+
val message = reader.readLine()
240+
if (message != FILE_TRANSFER_COMPLETION)
241+
throw IOException("Invalid bulk transfer confirmation received: '$message'")
242+
}
243+
sleep(150)
244+
}
245+
246+
// Signal end of transfer
247+
outputStream.flush()
248+
249+
withTimeout(5000) {
250+
val finalMessage = reader.readLine()
251+
if (finalMessage == FILE_TRANSFER_COMPLETION) {
252+
delay(100)
253+
withContext(Dispatchers.Main) {
254+
showBulkTransferCompletedNotification(fileUris.size)
196255
}
256+
} else {
257+
throw IOException("Invalid bulk transfer confirmation received")
197258
}
198-
} catch (e: Exception) {
199-
Log.e(TAG, "Error during file transfer", e)
200-
throw IOException("File transfer interrupted", e)
201259
}
260+
261+
// Shutdown output after receiving final confirmation
262+
socket?.shutdownOutput()
263+
}
264+
withContext(Dispatchers.Main) {
265+
showCompletedNotification()
202266
}
203267
} catch (e: Exception) {
204268
Log.e(TAG, "File transfer failed", e)
@@ -216,8 +280,6 @@ class FileTransferService : Service() {
216280
try {
217281
serverSocket = ipAddress?.let { socketFactory.tcpServerSocket(port, it) } ?: return null
218282
Log.d(TAG, "Server socket created on ${ipAddress}:${port}, waiting for client to connect")
219-
220-
221283
return ServerInfo(ipAddress, port, password)
222284
} catch (e: Exception) {
223285
Log.w(TAG, "Failed to create server socket on port $port, trying next port", e)
@@ -247,13 +309,13 @@ class FileTransferService : Service() {
247309
readChannel = readChannel,
248310
metadata = fileTransfer.fileMetadata
249311
)
250-
writeChannel.writeStringUtf8("Success")
312+
writeChannel.writeStringUtf8(FILE_TRANSFER_COMPLETION)
251313
writeChannel.flush()
252314

253315
showCompletedNotification(fileUri = fileUri, mimeType = fileTransfer.fileMetadata.mimeType)
254316

255317
if (preferencesRepository.readImageClipboardSettings().first()) {
256-
val clipboardManager = this.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
318+
val clipboardManager = this.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
257319
clipboardManager.setPrimaryClip(ClipData.newUri(contentResolver, "received file", fileUri))
258320
}
259321
} finally {
@@ -294,7 +356,7 @@ class FileTransferService : Service() {
294356
totalReceived = totalReceived,
295357
totalSize = totalSize
296358
)
297-
writeChannel.writeStringUtf8("Success")
359+
writeChannel.writeStringUtf8(FILE_TRANSFER_COMPLETION)
298360
writeChannel.flush()
299361
totalReceived += metadata.fileSize
300362

@@ -584,10 +646,13 @@ class FileTransferService : Service() {
584646
companion object {
585647

586648
const val ACTION_SEND_FILE = "sefirah.network.action.SEND_FILE"
649+
const val ACTION_SEND_BULK_FILES = "sefirah.network.action.SEND_BULK_FILES"
587650
const val ACTION_RECEIVE_FILE = "sefirah.network.action.RECEIVE_FILE"
588651
const val EXTRA_FILE_TRANSFER = "sefirah.network.extra.FILE_TRANSFER"
589652
const val EXTRA_BULK_TRANSFER = "sefirah.network.extra.BULK_TRANSFER"
653+
const val EXTRA_FILE_URIS = "sefirah.network.extra.FILE_URIS"
590654
const val ACTION_CANCEL_TRANSFER = "sefirah.network.action.CANCEL_TRANSFER"
655+
const val FILE_TRANSFER_COMPLETION = "Complete"
591656

592657
private const val TAG = "FileTransferService"
593658

@@ -597,3 +662,4 @@ class FileTransferService : Service() {
597662
private const val CANCEL_REQUEST_CODE = 100
598663
}
599664
}
665+

0 commit comments

Comments
 (0)