@@ -7,6 +7,7 @@ import android.content.Context
77import android.net.Uri
88import android.os.Build
99import android.provider.MediaStore
10+ import android.util.Log
1011import androidx.core.app.NotificationCompat
1112import androidx.core.app.NotificationManagerCompat
1213import kotlinx.coroutines.CoroutineScope
@@ -23,21 +24,59 @@ object FileReceiver {
2324 val name : String ,
2425 val size : Int ,
2526 val mime : String ,
27+ val chunkSize : Int ,
2628 var checksum : String? = null ,
2729 var receivedBytes : Int = 0 ,
2830 var index : Int = 0 ,
29- var output : OutputStream ? = null ,
30- var uri : Uri ? = null
31+ var pfd : android.os.ParcelFileDescriptor ? = null ,
32+ var uri : Uri ? = null ,
33+ // Speed / ETA tracking
34+ var lastUpdateTime : Long = System .currentTimeMillis(),
35+ var bytesAtLastUpdate : Int = 0 ,
36+ var smoothedSpeed : Double? = null
3137 )
3238
3339 private val incoming = ConcurrentHashMap <String , IncomingFileState >()
3440
41+ fun clearAll () {
42+ incoming.keys.forEach { id ->
43+ incoming.remove(id)?.let { state ->
44+ try {
45+ state.pfd?.close()
46+ } catch (e: Exception ) { e.printStackTrace() }
47+ }
48+ }
49+ }
50+
3551 fun ensureChannel (context : Context ) {
3652 // Delegate to shared NotificationUtil
3753 NotificationUtil .createFileChannel(context)
3854 }
3955
40- fun handleInit (context : Context , id : String , name : String , size : Int , mime : String , checksum : String? = null) {
56+ fun cancelTransfer (context : Context , id : String ) {
57+ val state = incoming.remove(id) ? : return
58+ Log .d(" FileReceiver" , " Cancelling incoming transfer $id " )
59+
60+ CoroutineScope (Dispatchers .IO ).launch {
61+ try {
62+ // Close and delete
63+ state.pfd?.close()
64+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
65+ state.uri?.let { context.contentResolver.delete(it, null , null ) }
66+ }
67+
68+ // Cancel notification
69+ NotificationManagerCompat .from(context).cancel(id.hashCode())
70+
71+ // Send network cancel
72+ WebSocketUtil .sendMessage(FileTransferProtocol .buildCancel(id))
73+ } catch (e: Exception ) {
74+ e.printStackTrace()
75+ }
76+ }
77+ }
78+
79+ fun handleInit (context : Context , id : String , name : String , size : Int , mime : String , chunkSize : Int , checksum : String? = null) {
4180 ensureChannel(context)
4281 CoroutineScope (Dispatchers .IO ).launch {
4382 try {
@@ -55,11 +94,11 @@ object FileReceiver {
5594 }
5695
5796 val uri = resolver.insert(collection, values)
58- val out = uri?.let { resolver.openOutputStream (it) }
97+ val pfd = uri?.let { resolver.openFileDescriptor (it, " rw " ) }
5998
60- if (uri != null && out != null ) {
61- incoming[id] = IncomingFileState (name = name, size = size, mime = mime, checksum = checksum, output = out , uri = uri)
62- NotificationUtil .showFileProgress(context, id.hashCode(), name, 0 )
99+ if (uri != null && pfd != null ) {
100+ incoming[id] = IncomingFileState (name = name, size = size, mime = mime, chunkSize = chunkSize, checksum = checksum, pfd = pfd , uri = uri)
101+ NotificationUtil .showFileProgress(context, id.hashCode(), name, 0 , id )
63102 }
64103 } catch (e: Exception ) {
65104 e.printStackTrace()
@@ -72,9 +111,18 @@ object FileReceiver {
72111 try {
73112 val state = incoming[id] ? : return @launch
74113 val bytes = android.util.Base64 .decode(base64Chunk, android.util.Base64 .NO_WRAP )
75- state.output?.write(bytes)
76- state.receivedBytes + = bytes.size
77- state.index = index
114+
115+ synchronized(state) {
116+ state.pfd?.fileDescriptor?.let { fd ->
117+ val channel = java.io.FileOutputStream (fd).channel
118+ val offset = index.toLong() * state.chunkSize
119+ channel.position(offset)
120+ channel.write(java.nio.ByteBuffer .wrap(bytes))
121+ state.receivedBytes + = bytes.size
122+ state.index = index
123+ }
124+ }
125+
78126 updateProgressNotification(context, id, state)
79127 // send ack for this chunk
80128 try {
@@ -101,8 +149,7 @@ object FileReceiver {
101149 }
102150
103151 // Now flush and close
104- state.output?.flush()
105- state.output?.close()
152+ state.pfd?.close()
106153
107154 // Mark file as not pending (Android Q+)
108155 if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
@@ -136,7 +183,7 @@ object FileReceiver {
136183
137184 // Notify user with an action to open the file
138185 val notifId = id.hashCode()
139- NotificationUtil .showFileComplete(context, notifId, state.name, verified, state.uri)
186+ NotificationUtil .showFileComplete(context, notifId, state.name, verified, isSending = false , contentUri = state.uri)
140187
141188 // Send transferVerified back to sender
142189 try {
@@ -153,12 +200,48 @@ object FileReceiver {
153200 }
154201 }
155202
156- private fun showProgress (context : Context , id : String ) {
157- NotificationUtil .showFileProgress(context, id.hashCode(), " Receiving..." , 0 )
158- }
203+
159204
160205 private fun updateProgressNotification (context : Context , id : String , state : IncomingFileState ) {
161- val percent = if (state.size > 0 ) (state.receivedBytes * 100 / state.size) else 0
162- NotificationUtil .showFileProgress(context, id.hashCode(), state.name, percent)
206+ val now = System .currentTimeMillis()
207+ val timeDiff = (now - state.lastUpdateTime) / 1000.0
208+
209+ if (timeDiff >= 1.0 ) {
210+ val bytesDiff = state.receivedBytes - state.bytesAtLastUpdate
211+ val intervalSpeed = if (timeDiff > 0 ) bytesDiff / timeDiff else 0.0
212+
213+ val alpha = 0.4
214+ val lastSpeed = state.smoothedSpeed
215+ val newSpeed = if (lastSpeed != null ) {
216+ alpha * intervalSpeed + (1.0 - alpha) * lastSpeed
217+ } else {
218+ intervalSpeed
219+ }
220+ state.smoothedSpeed = newSpeed
221+
222+ var etaString: String? = null
223+ if (newSpeed > 0 ) {
224+ val remainingBytes = (state.size - state.receivedBytes).coerceAtLeast(0 )
225+ val secondsRemaining = (remainingBytes / newSpeed).toLong()
226+
227+ etaString = if (secondsRemaining < 60 ) {
228+ " $secondsRemaining sec remaining"
229+ } else {
230+ val mins = secondsRemaining / 60
231+ " $mins min remaining"
232+ }
233+ }
234+
235+ state.lastUpdateTime = now
236+ state.bytesAtLastUpdate = state.receivedBytes
237+
238+ val percent = if (state.size > 0 ) (state.receivedBytes * 100 / state.size) else 0
239+ NotificationUtil .showFileProgress(context, id.hashCode(), state.name, percent, id, isSending = false , etaString = etaString)
240+ } else if (state.receivedBytes == 0 ) {
241+ // Initial
242+ NotificationUtil .showFileProgress(context, id.hashCode(), state.name, 0 , id, isSending = false , etaString = " Calculating..." )
243+ state.lastUpdateTime = now
244+ state.bytesAtLastUpdate = 0
245+ }
163246 }
164247}
0 commit comments