From 51ebfb22db56e6547d94b623647475a6f934185c Mon Sep 17 00:00:00 2001 From: Hardcore Sushi Date: Thu, 6 May 2021 22:57:47 +0200 Subject: [PATCH] Multiple Files Transfers --- app/src/main/AndroidManifest.xml | 1 + .../java/sushi/hardcore/aira/ChatActivity.kt | 221 ++++++-------- .../java/sushi/hardcore/aira/MainActivity.kt | 136 ++++----- .../hardcore/aira/ServiceBoundActivity.kt | 32 ++ .../aira/background_service/AIRAService.kt | 289 +++++++++++------- .../aira/background_service/FileTransfer.kt | 39 --- .../FileTransferNotification.kt | 88 ++++++ .../aira/background_service/FilesReceiver.kt | 46 +++ .../aira/background_service/FilesSender.kt | 15 + .../aira/background_service/FilesTransfer.kt | 9 + .../aira/background_service/PendingFile.kt | 5 + .../aira/background_service/Protocol.kt | 51 ++-- .../aira/background_service/ReceiveFile.kt | 10 + .../background_service/ReceiveFileTransfer.kt | 44 --- .../{SendFileTransfer.kt => SendFile.kt} | 7 +- .../sushi/hardcore/aira/utils/FileUtils.kt | 27 ++ app/src/main/res/layout/dialog_ask_file.xml | 17 +- app/src/main/res/values/strings.xml | 8 +- 18 files changed, 612 insertions(+), 433 deletions(-) create mode 100644 app/src/main/java/sushi/hardcore/aira/ServiceBoundActivity.kt delete mode 100644 app/src/main/java/sushi/hardcore/aira/background_service/FileTransfer.kt create mode 100644 app/src/main/java/sushi/hardcore/aira/background_service/FileTransferNotification.kt create mode 100644 app/src/main/java/sushi/hardcore/aira/background_service/FilesReceiver.kt create mode 100644 app/src/main/java/sushi/hardcore/aira/background_service/FilesSender.kt create mode 100644 app/src/main/java/sushi/hardcore/aira/background_service/FilesTransfer.kt create mode 100644 app/src/main/java/sushi/hardcore/aira/background_service/PendingFile.kt create mode 100644 app/src/main/java/sushi/hardcore/aira/background_service/ReceiveFile.kt delete mode 100644 app/src/main/java/sushi/hardcore/aira/background_service/ReceiveFileTransfer.kt rename app/src/main/java/sushi/hardcore/aira/background_service/{SendFileTransfer.kt => SendFile.kt} (52%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ae4f9ea..dc3194f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,7 @@ + diff --git a/app/src/main/java/sushi/hardcore/aira/ChatActivity.kt b/app/src/main/java/sushi/hardcore/aira/ChatActivity.kt index 59d1fbc..b85b47f 100644 --- a/app/src/main/java/sushi/hardcore/aira/ChatActivity.kt +++ b/app/src/main/java/sushi/hardcore/aira/ChatActivity.kt @@ -1,64 +1,44 @@ package sushi.hardcore.aira import android.content.ComponentName -import android.content.Context -import android.content.Intent import android.content.ServiceConnection import android.os.Bundle import android.os.IBinder -import android.provider.OpenableColumns import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.updatePadding import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import sushi.hardcore.aira.adapters.ChatAdapter -import sushi.hardcore.aira.background_service.AIRAService -import sushi.hardcore.aira.background_service.Protocol -import sushi.hardcore.aira.background_service.ReceiveFileTransfer +import sushi.hardcore.aira.background_service.* import sushi.hardcore.aira.databinding.ActivityChatBinding import sushi.hardcore.aira.databinding.DialogFingerprintsBinding import sushi.hardcore.aira.databinding.DialogInfoBinding import sushi.hardcore.aira.utils.FileUtils import sushi.hardcore.aira.utils.StringUtils -import java.io.FileNotFoundException -class ChatActivity : AppCompatActivity() { +class ChatActivity : ServiceBoundActivity() { private external fun generateFingerprint(publicKey: ByteArray): String private lateinit var binding: ActivityChatBinding private var sessionId = -1 private lateinit var sessionName: String - private lateinit var airaService: AIRAService private lateinit var chatAdapter: ChatAdapter private var lastLoadedMessageOffset = 0 - private var isActivityInForeground = false - private val filePicker = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> - if (::airaService.isInitialized && uri != null) { - contentResolver.query(uri, null, null, null, null)?.let { cursor -> - if (cursor.moveToFirst()) { - try { - contentResolver.openInputStream(uri)?.let { inputStream -> - val fileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) - val fileSize = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE)) - airaService.sendFileTo(sessionId, fileName, fileSize, inputStream)?.let { msg -> - chatAdapter.newMessage(ChatItem(true, msg)) - } - if (airaService.contacts.contains(sessionId)) { - lastLoadedMessageOffset += 1 - } - } - } catch (e: FileNotFoundException) { - Toast.makeText(this, e.localizedMessage, Toast.LENGTH_SHORT).show() + private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> + if (isServiceInitialized() && uris.size > 0) { + airaService.sendFilesFromUris(sessionId, uris)?.let { msgs -> + for (msg in msgs) { + chatAdapter.newMessage(ChatItem(true, msg)) + if (airaService.contacts.contains(sessionId)) { + lastLoadedMessageOffset += 1 } } - cursor.close() } } } @@ -83,7 +63,7 @@ class ChatActivity : AppCompatActivity() { } addOnScrollListener(object : RecyclerView.OnScrollListener() { fun loadMsgsIfNeeded(recyclerView: RecyclerView) { - if (!recyclerView.canScrollVertically(-1) && ::airaService.isInitialized) { + if (!recyclerView.canScrollVertically(-1) && isServiceInitialized()) { airaService.contacts[sessionId]?.let { contact -> loadMsgs(contact.uuid) } @@ -97,109 +77,108 @@ class ChatActivity : AppCompatActivity() { } }) } - Intent(this, AIRAService::class.java).also { serviceIntent -> - bindService(serviceIntent, object : ServiceConnection { - override fun onServiceConnected(name: ComponentName?, service: IBinder) { - val binder = service as AIRAService.AIRABinder - airaService = binder.getService() + binding.buttonSend.setOnClickListener { + val msg = binding.editMessage.text.toString() + airaService.sendTo(sessionId, Protocol.newMessage(msg)) + binding.editMessage.text.clear() + chatAdapter.newMessage(ChatItem(true, Protocol.newMessage(msg))) + if (airaService.contacts.contains(sessionId)) { + lastLoadedMessageOffset += 1 + } + binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount) + } + binding.buttonAttach.setOnClickListener { + filePicker.launch("*/*") + } + serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder) { + val binder = service as AIRAService.AIRABinder + airaService = binder.getService() - airaService.contacts[sessionId]?.let { contact -> - displayIconTrustLevel(true, contact.verified) - loadMsgs(contact.uuid) + chatAdapter.clear() + airaService.contacts[sessionId]?.let { contact -> + displayIconTrustLevel(true, contact.verified) + loadMsgs(contact.uuid) + } + airaService.savedMsgs[sessionId]?.let { + for (chatItem in it.asReversed()) { + chatAdapter.newLoadedMessage(chatItem) } - airaService.receiveFileTransfers[sessionId]?.let { - if (it.shouldAsk) { - it.ask(this@ChatActivity, sessionName) + } + airaService.receiveFileTransfers[sessionId]?.let { + if (it.shouldAsk) { + it.ask(this@ChatActivity, sessionName) + } + } + binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount) + val showBottomPanel = { + findViewById(R.id.bottom_panel).visibility = View.VISIBLE + } + airaService.uiCallbacks = object : AIRAService.UiCallbacks { + override fun onNewSession(sessionId: Int, ip: String) { + if (this@ChatActivity.sessionId == sessionId) { + runOnUiThread { + showBottomPanel() + } } } - airaService.savedMsgs[sessionId]?.let { - for (chatItem in it.asReversed()) { - chatAdapter.newLoadedMessage(chatItem) + override fun onSessionDisconnect(sessionId: Int) { + if (this@ChatActivity.sessionId == sessionId) { + runOnUiThread { + findViewById(R.id.bottom_panel).visibility = View.GONE + binding.buttonSend.setOnClickListener(null) + binding.buttonAttach.setOnClickListener(null) + } } } - binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount) - val onConnected = { - findViewById(R.id.bottom_panel).visibility = View.VISIBLE - binding.buttonSend.setOnClickListener { - val msg = binding.editMessage.text.toString() - airaService.sendTo(sessionId, Protocol.newMessage(msg)) - binding.editMessage.text.clear() - chatAdapter.newMessage(ChatItem(true, Protocol.newMessage(msg))) + override fun onNameTold(sessionId: Int, name: String) { + if (this@ChatActivity.sessionId == sessionId) { + runOnUiThread { + sessionName = name + title = name + } + } + } + override fun onNewMessage(sessionId: Int, data: ByteArray): Boolean { + return if (this@ChatActivity.sessionId == sessionId) { + runOnUiThread { + chatAdapter.newMessage(ChatItem(false, data)) + binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount) + } if (airaService.contacts.contains(sessionId)) { lastLoadedMessageOffset += 1 } - binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount) - } - binding.buttonAttach.setOnClickListener { - filePicker.launch("*/*") + airaService.isAppInBackground + } else { + false } } - airaService.uiCallbacks = object : AIRAService.UiCallbacks { - override fun onNewSession(sessionId: Int, ip: String) { - if (this@ChatActivity.sessionId == sessionId) { - runOnUiThread { - onConnected() - } - } - } - override fun onSessionDisconnect(sessionId: Int) { - if (this@ChatActivity.sessionId == sessionId) { - runOnUiThread { - findViewById(R.id.bottom_panel).visibility = View.GONE - binding.buttonSend.setOnClickListener(null) - binding.buttonAttach.setOnClickListener(null) - } - } - } - override fun onNameTold(sessionId: Int, name: String) { - if (this@ChatActivity.sessionId == sessionId) { - runOnUiThread { - sessionName = name - title = name - } - } - } - override fun onNewMessage(sessionId: Int, data: ByteArray): Boolean { - return if (this@ChatActivity.sessionId == sessionId) { - runOnUiThread { - chatAdapter.newMessage(ChatItem(false, data)) - binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount) - } - if (airaService.contacts.contains(sessionId)) { - lastLoadedMessageOffset += 1 - } - isActivityInForeground - } else { - false - } - } - override fun onAskLargeFile(sessionId: Int, name: String, receiveFileTransfer: ReceiveFileTransfer): Boolean { - return if (this@ChatActivity.sessionId == sessionId) { - runOnUiThread { - receiveFileTransfer.ask(this@ChatActivity, name) - } - true - } else { - false + override fun onAskLargeFiles(sessionId: Int, name: String, filesReceiver: FilesReceiver): Boolean { + return if (this@ChatActivity.sessionId == sessionId) { + runOnUiThread { + filesReceiver.ask(this@ChatActivity, name) } + true + } else { + false } } - airaService.isAppInBackground = false - if (airaService.isOnline(sessionId)) { - onConnected() - binding.recyclerChat.updatePadding(bottom = 0) - } - airaService.setSeen(sessionId, true) } - override fun onServiceDisconnected(name: ComponentName?) {} - }, Context.BIND_AUTO_CREATE) + airaService.isAppInBackground = false + if (airaService.isOnline(sessionId)) { + showBottomPanel() + binding.recyclerChat.updatePadding(bottom = 0) + } + airaService.setSeen(sessionId, true) + } + override fun onServiceDisconnected(name: ComponentName?) {} } } } } - fun displayIconTrustLevel(isContact: Boolean, isVerified: Boolean) { + private fun displayIconTrustLevel(isContact: Boolean, isVerified: Boolean) { when { isVerified -> { binding.imageTrustLevel.setImageResource(R.drawable.ic_verified) @@ -213,7 +192,7 @@ class ChatActivity : AppCompatActivity() { } } - fun loadMsgs(contactUuid: String) { + private fun loadMsgs(contactUuid: String) { AIRADatabase.loadMsgs(contactUuid, lastLoadedMessageOffset, Constants.MSG_LOADING_COUNT)?.let { for (chatItem in it.asReversed()) { chatAdapter.newLoadedMessage(chatItem) @@ -328,20 +307,10 @@ class ChatActivity : AppCompatActivity() { } } - override fun onResume() { - super.onResume() - isActivityInForeground = true - if (::airaService.isInitialized) { + override fun onStart() { + super.onStart() + if (isServiceInitialized()) { airaService.setSeen(sessionId, true) - airaService.isAppInBackground = false - } - } - - override fun onPause() { - super.onPause() - isActivityInForeground = false - if (::airaService.isInitialized) { - airaService.isAppInBackground = true } } diff --git a/app/src/main/java/sushi/hardcore/aira/MainActivity.kt b/app/src/main/java/sushi/hardcore/aira/MainActivity.kt index e83379a..5ddc703 100644 --- a/app/src/main/java/sushi/hardcore/aira/MainActivity.kt +++ b/app/src/main/java/sushi/hardcore/aira/MainActivity.kt @@ -1,13 +1,11 @@ package sushi.hardcore.aira import android.content.ComponentName -import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.net.Uri import android.os.Bundle import android.os.IBinder -import android.provider.OpenableColumns import android.view.Menu import android.view.MenuItem import android.view.View @@ -15,11 +13,10 @@ import android.widget.AbsListView import android.widget.AdapterView import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity import sushi.hardcore.aira.adapters.Session import sushi.hardcore.aira.adapters.SessionAdapter import sushi.hardcore.aira.background_service.AIRAService -import sushi.hardcore.aira.background_service.ReceiveFileTransfer +import sushi.hardcore.aira.background_service.FilesReceiver import sushi.hardcore.aira.databinding.ActivityMainBinding import sushi.hardcore.aira.databinding.DialogIpAddressesBinding import sushi.hardcore.aira.utils.FileUtils @@ -27,9 +24,8 @@ import sushi.hardcore.aira.utils.StringUtils import java.lang.StringBuilder import java.net.NetworkInterface -class MainActivity : AppCompatActivity() { +class MainActivity : ServiceBoundActivity() { private lateinit var binding: ActivityMainBinding - private lateinit var airaService: AIRAService private lateinit var onlineSessionAdapter: SessionAdapter private var offlineSessionAdapter: SessionAdapter? = null private val onSessionsItemClickSendFile = AdapterView.OnItemClickListener { adapter, _, position, _ -> @@ -70,9 +66,9 @@ class MainActivity : AppCompatActivity() { return false } - override fun onAskLargeFile(sessionId: Int, name: String, receiveFileTransfer: ReceiveFileTransfer): Boolean { + override fun onAskLargeFiles(sessionId: Int, name: String, filesReceiver: FilesReceiver): Boolean { runOnUiThread { - receiveFileTransfer.ask(this@MainActivity, name) + filesReceiver.ask(this@MainActivity, name) } return true } @@ -86,7 +82,7 @@ class MainActivity : AppCompatActivity() { val identityName = intent.getStringExtra("identityName") identityName?.let { title = it } - val openedToShareFile = intent.action == Intent.ACTION_SEND + val openedToShareFile = intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE onlineSessionAdapter = SessionAdapter(this) binding.onlineSessions.apply { @@ -133,30 +129,29 @@ class MainActivity : AppCompatActivity() { setOnScrollListener(onSessionsScrollListener) } } - Intent(this, AIRAService::class.java).also { serviceIntent -> - bindService(serviceIntent, object : ServiceConnection { - override fun onServiceConnected(name: ComponentName?, service: IBinder) { - val binder = service as AIRAService.AIRABinder - airaService = binder.getService() - airaService.uiCallbacks = uiCallbacks - airaService.isAppInBackground = false - loadContacts() - if (AIRAService.isServiceRunning) { - title = airaService.identityName - loadSessions() - } else { - airaService.identityName = identityName - startService(serviceIntent) - } - binding.refresher.setOnRefreshListener { - airaService.restartDiscovery() - binding.refresher.isRefreshing = false - } + serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder) { + val binder = service as AIRAService.AIRABinder + airaService = binder.getService() + airaService.uiCallbacks = uiCallbacks + airaService.isAppInBackground = false + refreshSessions() + if (AIRAService.isServiceRunning) { + title = airaService.identityName + } else { + airaService.identityName = identityName + startService(serviceIntent) } - override fun onServiceDisconnected(name: ComponentName?) {} - }, Context.BIND_AUTO_CREATE) + } + override fun onServiceDisconnected(name: ComponentName?) {} } + binding.refresher.setOnRefreshListener { + if (isServiceInitialized()) { + airaService.restartDiscovery() + } + binding.refresher.isRefreshing = false + } binding.buttonShowIp.setOnClickListener { val ipAddresses = StringBuilder() for (iface in NetworkInterface.getNetworkInterfaces()) { @@ -175,7 +170,7 @@ class MainActivity : AppCompatActivity() { .show() } binding.editPeerIp.setOnEditorActionListener { _, _, _ -> - if (::airaService.isInitialized){ + if (isServiceInitialized()){ airaService.connectTo(binding.editPeerIp.text.toString()) } binding.editPeerIp.text.clear() @@ -196,7 +191,7 @@ class MainActivity : AppCompatActivity() { true } R.id.close -> { - if (::airaService.isInitialized) { + if (isServiceInitialized()) { AlertDialog.Builder(this) .setTitle(R.string.warning) .setMessage(R.string.ask_log_out) @@ -240,27 +235,13 @@ class MainActivity : AppCompatActivity() { } } - override fun onPause() { - super.onPause() - if (::airaService.isInitialized) { + override fun onStop() { + super.onStop() + if (isServiceInitialized()) { airaService.isAppInBackground = true } } - override fun onResume() { - super.onResume() - if (::airaService.isInitialized) { - if (AIRAService.isServiceRunning) { - airaService.isAppInBackground = false - airaService.uiCallbacks = uiCallbacks //restoring callbacks - refreshSessions() - title = airaService.identityName - } else { - finish() - } - } - } - override fun onBackPressed() { if (isSelecting()) { unSelectAll() @@ -328,34 +309,37 @@ class MainActivity : AppCompatActivity() { } private fun askShareFileTo(session: Session) { - val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) - if (uri == null) { - Toast.makeText(this, R.string.share_uri_null, Toast.LENGTH_SHORT).show() - } else { - val cursor = contentResolver.query(uri, null, null, null, null) - if (cursor == null) { - Toast.makeText(this, R.string.file_open_failed, Toast.LENGTH_SHORT).show() - } else { - if (cursor.moveToFirst()) { - val fileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) - val fileSize = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE)) - val inputStream = contentResolver.openInputStream(uri) - if (inputStream == null) { - Toast.makeText(this, R.string.file_open_failed, Toast.LENGTH_SHORT).show() - } else { - AlertDialog.Builder(this) - .setTitle(R.string.warning) - .setMessage(getString(R.string.ask_send_file, fileName, FileUtils.formatSize(fileSize), session.name ?: session.ip)) - .setPositiveButton(R.string.yes) { _, _ -> - airaService.sendFileTo(session.sessionId, fileName, fileSize, inputStream) - finish() - } - .setNegativeButton(R.string.cancel, null) - .show() - } - } - cursor.close() + var uris: ArrayList? = null + when (intent.action) { + Intent.ACTION_SEND -> intent.getParcelableExtra(Intent.EXTRA_STREAM)?.let { + uris = arrayListOf(it) } + Intent.ACTION_SEND_MULTIPLE -> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) + } + if (uris == null) { + Toast.makeText(this, R.string.open_uri_failed, Toast.LENGTH_SHORT).show() + } else { + val msg = if (uris!!.size == 1) { + val sendFile = FileUtils.openFileFromUri(this, uris!![0]) + if (sendFile == null) { + Toast.makeText(this, R.string.open_uri_failed, Toast.LENGTH_SHORT).show() + return + } else { + sendFile.inputStream.close() + getString(R.string.ask_send_single_file, sendFile.fileName, FileUtils.formatSize(sendFile.fileSize), session.name ?: session.ip) + } + } else { + getString(R.string.ask_send_multiple_files, uris!!.size, session.name ?: session.ip) + } + AlertDialog.Builder(this) + .setTitle(R.string.warning) + .setMessage(msg) + .setPositiveButton(R.string.yes) { _, _ -> + airaService.sendFilesFromUris(session.sessionId, uris!!) + finish() + } + .setNegativeButton(R.string.cancel, null) + .show() } } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/ServiceBoundActivity.kt b/app/src/main/java/sushi/hardcore/aira/ServiceBoundActivity.kt new file mode 100644 index 0000000..6e98cf3 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/aira/ServiceBoundActivity.kt @@ -0,0 +1,32 @@ +package sushi.hardcore.aira + +import android.content.Intent +import android.content.ServiceConnection +import androidx.appcompat.app.AppCompatActivity +import sushi.hardcore.aira.background_service.AIRAService + +open class ServiceBoundActivity: AppCompatActivity() { + protected lateinit var airaService: AIRAService + protected lateinit var serviceConnection: ServiceConnection + protected lateinit var serviceIntent: Intent + + protected fun isServiceInitialized(): Boolean { + return ::airaService.isInitialized + } + + override fun onStop() { + super.onStop() + if (::airaService.isInitialized) { + airaService.isAppInBackground = true + airaService.uiCallbacks = null + unbindService(serviceConnection) + } + } + + override fun onStart() { + super.onStart() + if (!::serviceIntent.isInitialized) { + serviceIntent = Intent(this, AIRAService::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/AIRAService.kt b/app/src/main/java/sushi/hardcore/aira/background_service/AIRAService.kt index d28ebc2..266a580 100644 --- a/app/src/main/java/sushi/hardcore/aira/background_service/AIRAService.kt +++ b/app/src/main/java/sushi/hardcore/aira/background_service/AIRAService.kt @@ -3,6 +3,7 @@ package sushi.hardcore.aira.background_service import android.app.* import android.content.Context import android.content.Intent +import android.net.Uri import android.net.nsd.NsdManager import android.net.nsd.NsdServiceInfo import android.os.* @@ -14,9 +15,9 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.RemoteInput import sushi.hardcore.aira.* +import sushi.hardcore.aira.utils.FileUtils import sushi.hardcore.aira.utils.StringUtils import java.io.IOException -import java.io.InputStream import java.net.* import java.nio.channels.* @@ -44,8 +45,8 @@ class AIRAService : Service() { private lateinit var selector: Selector private val sessionIdByKey = mutableMapOf() private val notificationIdManager = NotificationIdManager() - private val sendFileTransfers = mutableMapOf() - val receiveFileTransfers = mutableMapOf() + private val sendFileTransfers = mutableMapOf() + val receiveFileTransfers = mutableMapOf() lateinit var contacts: HashMap private lateinit var serviceHandler: Handler private lateinit var notificationManager: NotificationManagerCompat @@ -103,7 +104,7 @@ class AIRAService : Service() { fun onSessionDisconnect(sessionId: Int) fun onNameTold(sessionId: Int, name: String) fun onNewMessage(sessionId: Int, data: ByteArray): Boolean - fun onAskLargeFile(sessionId: Int, name: String, receiveFileTransfer: ReceiveFileTransfer): Boolean + fun onAskLargeFiles(sessionId: Int, name: String, filesReceiver: FilesReceiver): Boolean } fun connectTo(ip: String) { @@ -133,29 +134,54 @@ class AIRAService : Service() { } } - fun sendFileTo(sessionId: Int, fileName: String, fileSize: Long, inputStream: InputStream): ByteArray? { - if (fileSize <= Constants.fileSizeLimit) { - val buffer = inputStream.readBytes() - inputStream.close() - sendTo(sessionId, Protocol.newFile(fileName, buffer)) - AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer)?.let { rawFileUuid -> - val msg = byteArrayOf(Protocol.FILE)+rawFileUuid+fileName.toByteArray() - saveMsg(sessionId, msg) - return msg - } - } else { - if (sendFileTransfers[sessionId] == null && receiveFileTransfers[sessionId] == null) { - val fileTransfer = SendFileTransfer(fileName, fileSize, inputStream) - sendFileTransfers[sessionId] = fileTransfer - createFileTransferNotification(sessionId, fileTransfer) - sendTo(sessionId, Protocol.askLargeFile(fileSize, fileName)) - } else { - Toast.makeText(this, R.string.file_transfer_already_in_progress, Toast.LENGTH_SHORT).show() + fun sendFilesFromUris(sessionId: Int, uris: List): List? { + val files = mutableListOf() + var useLargeFileTransfer = false + for (uri in uris) { + FileUtils.openFileFromUri(this, uri)?.let { sendFile -> + files.add(sendFile) + if (sendFile.fileSize > Constants.fileSizeLimit) { + useLargeFileTransfer = true + } } } + return if (useLargeFileTransfer) { + sendLargeFilesTo(sessionId, files) + null + } else { + val msgs = mutableListOf() + for (file in files) { + sendSmallFileTo(sessionId, file)?.let { msg -> + msgs.add(msg) + } + } + msgs + } + } + + private fun sendSmallFileTo(sessionId: Int, sendFile: SendFile): ByteArray? { + val buffer = sendFile.inputStream.readBytes() + sendFile.inputStream.close() + sendTo(sessionId, Protocol.newFile(sendFile.fileName, buffer)) + AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer)?.let { rawFileUuid -> + val msg = byteArrayOf(Protocol.FILE) + rawFileUuid + sendFile.fileName.toByteArray() + saveMsg(sessionId, msg) + return msg + } return null } + private fun sendLargeFilesTo(sessionId: Int, files: MutableList) { + if (sendFileTransfers[sessionId] == null && receiveFileTransfers[sessionId] == null) { + val filesSender = FilesSender(files, this, notificationManager, getNameOf(sessionId)) + initFileTransferNotification(sessionId, filesSender.fileTransferNotification, filesSender.files[0]) + sendFileTransfers[sessionId] = filesSender + sendTo(sessionId, Protocol.askLargeFiles(files)) + } else { + Toast.makeText(this, R.string.file_transfer_already_in_progress, Toast.LENGTH_SHORT).show() + } + } + fun logOut() { serviceHandler.sendEmptyMessage(MESSAGE_LOGOUT) isServiceRunning = false @@ -232,11 +258,11 @@ class AIRAService : Service() { fun deleteConversation(sessionId: Int): Boolean { contacts[sessionId]?.let { return if (AIRADatabase.deleteConversation(it.uuid)) { - savedMsgs[sessionId] = mutableListOf() - true - } else { - false - } + savedMsgs[sessionId] = mutableListOf() + true + } else { + false + } } return true } @@ -358,33 +384,19 @@ class AIRAService : Service() { notificationManager.notify(notificationIdManager.getMessageNotificationId(sessionId), notificationBuilder.build()) } - private fun createFileTransferNotification(sessionId: Int, fileTransfer: FileTransfer) { - fileTransfer.notificationBuilder = NotificationCompat.Builder(this, FILE_TRANSFER_NOTIFICATION_CHANNEL_ID) - .setCategory(NotificationCompat.CATEGORY_PROGRESS) - .setSmallIcon(R.drawable.ic_launcher) - .setContentTitle(getNameOf(sessionId)) - .setContentText(fileTransfer.fileName) - .setOngoing(true) - .setProgress(fileTransfer.fileSize.toInt(), fileTransfer.transferred, true) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { - val cancelIntent = PendingIntent.getBroadcast(this, 0, - Intent(this, NotificationBroadcastReceiver::class.java).apply { - val bundle = Bundle() - bundle.putBinder("binder", AIRABinder()) - bundle.putInt("sessionId", sessionId) - putExtra("bundle", bundle) - action = NotificationBroadcastReceiver.ACTION_CANCEL_FILE_TRANSFER - }, PendingIntent.FLAG_UPDATE_CURRENT) - fileTransfer.notificationBuilder.addAction( - NotificationCompat.Action( - R.drawable.ic_launcher, - getString(R.string.cancel), - cancelIntent - ) - ) - } - fileTransfer.notificationId = notificationIdManager.getFileTransferNotificationId(sessionId) - notificationManager.notify(fileTransfer.notificationId, fileTransfer.notificationBuilder.build()) + private fun initFileTransferNotification(sessionId: Int, fileTransferNotification: FileTransferNotification, file: PendingFile) { + fileTransferNotification.initFileTransferNotification( + notificationIdManager.getFileTransferNotificationId(sessionId), + file.fileName, + file.fileSize.toInt(), + Intent(this, NotificationBroadcastReceiver::class.java).apply { + val bundle = Bundle() + bundle.putBinder("binder", AIRABinder()) + bundle.putInt("sessionId", sessionId) + putExtra("bundle", bundle) + action = NotificationBroadcastReceiver.ACTION_CANCEL_FILE_TRANSFER + } + ) } private fun saveMsg(sessionId: Int, msg: ByteArray) { @@ -485,41 +497,47 @@ class AIRAService : Service() { } } - private fun encryptNextChunk(session: Session, fileTransfer: SendFileTransfer) { + private fun encryptNextChunk(session: Session, filesSender: FilesSender) { val nextChunk = ByteArray(Constants.FILE_CHUNK_SIZE) nextChunk[0] = Protocol.LARGE_FILE_CHUNK - val read = fileTransfer.inputStream.read(nextChunk, 1, Constants.FILE_CHUNK_SIZE-1) - fileTransfer.nextChunk = if (read > 0) { + val read = try { + filesSender.files[filesSender.index].inputStream.read(nextChunk, 1, Constants.FILE_CHUNK_SIZE-1) + } catch (e: IOException) { + 0 + } + filesSender.nextChunk = if (read > 0) { + filesSender.lastChunkSizes.add(nextChunk.size) session.encrypt(nextChunk) } else { null } } - private fun flushSendFileTransfer(sessionId: Int, session: Session, fileTransfer: SendFileTransfer) { - fileTransfer.nextChunk?.let { - session.writeAll(it) - while (fileTransfer.msgQueue.size > 0) { - val msg = fileTransfer.msgQueue.removeAt(0) - sendAndSave(sessionId, msg) + private fun flushSendFileTransfer(sessionId: Int, session: Session, filesSender: FilesSender) { + synchronized(filesSender) { //prevent sending nextChunk two times when canceling + filesSender.nextChunk?.let { + session.writeAll(it) + filesSender.nextChunk = null + while (filesSender.msgQueue.size > 0) { + val msg = filesSender.msgQueue.removeAt(0) + sendAndSave(sessionId, msg) + } } } } private fun cancelFileTransfer(sessionId: Int, session: Session, outgoing: Boolean) { sendFileTransfers[sessionId]?.let { - it.inputStream.close() - it.onAborted(this, notificationManager, getNameOf(sessionId)) + it.files[it.index].inputStream.close() flushSendFileTransfer(sessionId, session, it) - sendFileTransfers.remove(sessionId) + sendFileTransfers.remove(sessionId)!!.fileTransferNotification.onAborted() } receiveFileTransfers[sessionId]?.let { - it.outputStream?.close() - it.onAborted(this, notificationManager, getNameOf(sessionId)) - receiveFileTransfers.remove(sessionId) + it.files[it.index].outputStream?.close() + receiveFileTransfers.remove(sessionId)!!.fileTransferNotification.onAborted() } if (outgoing) { - session.encryptAndSend(Protocol.abortFileTransfer()) + session.encryptAndSend(Protocol.abortFilesTransfer()) } } @@ -568,19 +586,38 @@ class AIRAService : Service() { } } Protocol.LARGE_FILE_CHUNK -> { - receiveFileTransfers[sessionId]?.let { fileTransfer -> - fileTransfer.outputStream?.let { outputStream -> + receiveFileTransfers[sessionId]?.let { filesReceiver -> + val file = filesReceiver.files[filesReceiver.index] + if (file.outputStream == null) { + val outputStream = FileUtils.openFileForDownload(this, file.fileName) + if (outputStream == null) { + cancelFileTransfer(sessionId) + } else { + file.outputStream = outputStream + } + } + file.outputStream?.let { outputStream -> val chunk = buffer.sliceArray(1 until buffer.size) try { outputStream.write(chunk) session.encryptAndSend(Protocol.ackChunk()) - fileTransfer.transferred += chunk.size - if (fileTransfer.transferred >= fileTransfer.fileSize) { + file.transferred += chunk.size + if (file.transferred >= file.fileSize) { outputStream.close() - receiveFileTransfers.remove(sessionId) - fileTransfer.onCompleted(this, notificationManager, getNameOf(sessionId)) + if (filesReceiver.index == filesReceiver.files.size-1) { + receiveFileTransfers.remove(sessionId) + filesReceiver.fileTransferNotification.onCompleted() + } else { + filesReceiver.index += 1 + val nextFile = filesReceiver.files[filesReceiver.index] + initFileTransferNotification( + sessionId, + filesReceiver.fileTransferNotification, + nextFile + ) + } } else { - fileTransfer.updateNotificationProgress(notificationManager) + filesReceiver.fileTransferNotification.updateNotificationProgress(chunk.size) } } catch (e: IOException) { cancelFileTransfer(sessionId) @@ -589,46 +626,72 @@ class AIRAService : Service() { } } Protocol.ACK_CHUNK -> { - sendFileTransfers[sessionId]?.let { fileTransfer -> - flushSendFileTransfer(sessionId, session, fileTransfer) - fileTransfer.transferred += fileTransfer.nextChunk?.size ?: 0 - if (fileTransfer.transferred >= fileTransfer.fileSize) { - fileTransfer.inputStream.close() - sendFileTransfers.remove(sessionId) - fileTransfer.onCompleted(this, notificationManager, getNameOf(sessionId)) + sendFileTransfers[sessionId]?.let { filesSender -> + flushSendFileTransfer(sessionId, session, filesSender) + val file = filesSender.files[filesSender.index] + val chunkSize = filesSender.lastChunkSizes.removeAt(0) + file.transferred += chunkSize + if (file.transferred >= file.fileSize) { + file.inputStream.close() + if (filesSender.index == filesSender.files.size-1) { + sendFileTransfers.remove(sessionId) + filesSender.fileTransferNotification.onCompleted() + } else { + filesSender.index += 1 + val nextFile = filesSender.files[filesSender.index] + initFileTransferNotification( + sessionId, + filesSender.fileTransferNotification, + nextFile + ) + encryptNextChunk(session, filesSender) + filesSender.nextChunk?.let { + session.writeAll(it) + encryptNextChunk(session, filesSender) + } + } } else { - encryptNextChunk(session, fileTransfer) - fileTransfer.updateNotificationProgress(notificationManager) + encryptNextChunk(session, filesSender) + filesSender.fileTransferNotification.updateNotificationProgress(chunkSize) } } } - Protocol.ABORT_FILE_TRANSFER -> cancelFileTransfer(sessionId, session, false) - Protocol.ACCEPT_LARGE_FILE -> { - sendFileTransfers[sessionId]?.let { fileTransfer -> - encryptNextChunk(session, fileTransfer) - fileTransfer.nextChunk?.let { + Protocol.ABORT_FILES_TRANSFER -> cancelFileTransfer(sessionId, session, false) + Protocol.ACCEPT_LARGE_FILES -> { + sendFileTransfers[sessionId]?.let { filesSender -> + encryptNextChunk(session, filesSender) + filesSender.nextChunk?.let { session.writeAll(it) - fileTransfer.transferred += fileTransfer.nextChunk?.size ?: 0 - encryptNextChunk(session, fileTransfer) + encryptNextChunk(session, filesSender) } } } - Protocol.ASK_LARGE_FILE -> { - if (receiveFileTransfers[sessionId] == null) { - Protocol.parseAskFile(buffer)?.let { fileInfo -> - val fileTransfer = ReceiveFileTransfer(fileInfo.fileName, fileInfo.fileSize, { fileTransfer -> - createFileTransferNotification(sessionId, fileTransfer) - sendTo(sessionId, Protocol.acceptLargeFile()) - }, { - receiveFileTransfers.remove(sessionId) - sendTo(sessionId, Protocol.abortFileTransfer()) - notificationManager.cancel(notificationIdManager.getFileTransferNotificationId(sessionId)) - }) - receiveFileTransfers[sessionId] = fileTransfer + Protocol.ASK_LARGE_FILES -> { + if (!receiveFileTransfers.containsKey(sessionId) && !sendFileTransfers.containsKey(sessionId)) { + Protocol.parseAskFiles(buffer)?.let { files -> val name = getNameOf(sessionId) + val filesReceiver = FilesReceiver( + files, + { filesReceiver -> + initFileTransferNotification( + sessionId, + filesReceiver.fileTransferNotification, + filesReceiver.files[0] + ) + sendTo(sessionId, Protocol.acceptLargeFiles()) + }, { filesReceiver -> + receiveFileTransfers.remove(sessionId) + sendTo(sessionId, Protocol.abortFilesTransfer()) + filesReceiver.fileTransferNotification.cancel() + }, + this, + notificationManager, + name + ) + receiveFileTransfers[sessionId] = filesReceiver var shouldSendNotification = true if (!isAppInBackground) { - if (uiCallbacks?.onAskLargeFile(sessionId, name, fileTransfer) == true) { + if (uiCallbacks?.onAskLargeFiles(sessionId, name, filesReceiver) == true) { shouldSendNotification = false } } @@ -637,7 +700,7 @@ class AIRAService : Service() { .setCategory(NotificationCompat.CATEGORY_EVENT) .setSmallIcon(R.drawable.ic_launcher) .setContentTitle(getString(R.string.download_file_request)) - .setContentText(getString(R.string.want_to_send_a_file, name, ": "+fileInfo.fileName)) + .setContentText(getString(R.string.want_to_send_files, name)) .setOngoing(true) //not cancelable .setContentIntent( PendingIntent.getActivity(this, 0, Intent(this, ChatActivity::class.java).apply { @@ -705,16 +768,8 @@ class AIRAService : Service() { sessions.remove(sessionId) savedMsgs.remove(sessionId) savedNames.remove(sessionId) - sendFileTransfers.remove(sessionId)?.notificationId?.let { - if (it != -1) { - notificationManager.cancel(it) - } - } - receiveFileTransfers.remove(sessionId)?.notificationId?.let { - if (it != -1) { - notificationManager.cancel(it) - } - } + sendFileTransfers.remove(sessionId)?.fileTransferNotification?.cancel() + receiveFileTransfers.remove(sessionId)?.fileTransferNotification?.cancel() } } } diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/FileTransfer.kt b/app/src/main/java/sushi/hardcore/aira/background_service/FileTransfer.kt deleted file mode 100644 index e32f9d1..0000000 --- a/app/src/main/java/sushi/hardcore/aira/background_service/FileTransfer.kt +++ /dev/null @@ -1,39 +0,0 @@ -package sushi.hardcore.aira.background_service - -import android.content.Context -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import sushi.hardcore.aira.R - -open class FileTransfer(val fileName: String, val fileSize: Long) { - var transferred = 0 - lateinit var notificationBuilder: NotificationCompat.Builder - var notificationId = -1 - - fun updateNotificationProgress(notificationManager: NotificationManagerCompat) { - notificationBuilder.setProgress(fileSize.toInt(), transferred, false) - notificationManager.notify(notificationId, notificationBuilder.build()) - } - - private fun endNotification(context: Context, notificationManager: NotificationManagerCompat, sessionName: String, string: Int) { - notificationManager.notify( - notificationId, - NotificationCompat.Builder(context, AIRAService.FILE_TRANSFER_NOTIFICATION_CHANNEL_ID) - .setCategory(NotificationCompat.CATEGORY_EVENT) - .setSmallIcon(R.drawable.ic_launcher) - .setContentTitle(sessionName) - .setContentText(context.getString(string)) - .build() - ) - } - - fun onAborted(context: Context, notificationManager: NotificationManagerCompat, sessionName: String) { - if (::notificationBuilder.isInitialized) { - endNotification(context, notificationManager, sessionName, R.string.transfer_aborted) - } - } - - fun onCompleted(context: Context, notificationManager: NotificationManagerCompat, sessionName: String) { - endNotification(context, notificationManager, sessionName, R.string.transfer_completed) - } -} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/FileTransferNotification.kt b/app/src/main/java/sushi/hardcore/aira/background_service/FileTransferNotification.kt new file mode 100644 index 0000000..d03e84e --- /dev/null +++ b/app/src/main/java/sushi/hardcore/aira/background_service/FileTransferNotification.kt @@ -0,0 +1,88 @@ +package sushi.hardcore.aira.background_service + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import sushi.hardcore.aira.R + +class FileTransferNotification( + private val context: Context, + private val notificationManager: NotificationManagerCompat, + private val sessionName: String +) { + private var fileSize = -1 + private var transferred = 0 + private lateinit var notificationBuilder: NotificationCompat.Builder + private var notificationId = -1 + private var isEnded = false + + fun initFileTransferNotification(id: Int, fileName: String, size: Int, cancelIntent: Intent) { + fileSize = size + transferred = 0 + notificationBuilder = NotificationCompat.Builder(context, AIRAService.FILE_TRANSFER_NOTIFICATION_CHANNEL_ID) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setSmallIcon(R.drawable.ic_launcher) + .setContentTitle(sessionName) + .setContentText(fileName) + .setOngoing(true) + .setProgress(fileSize, 0, true) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { + val cancelPendingIntent = PendingIntent.getBroadcast(context, transferred, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT) + notificationBuilder.addAction( + NotificationCompat.Action( + R.drawable.ic_launcher, + context.getString(R.string.cancel), + cancelPendingIntent + ) + ) + } + synchronized(this) { + if (!isEnded) { + notificationId = id + notificationManager.notify(notificationId, notificationBuilder.build()) + } + } + } + + fun updateNotificationProgress(size: Int) { + transferred += size + notificationBuilder.setProgress(fileSize, transferred, false) + synchronized(this) { + if (!isEnded) { + notificationManager.notify(notificationId, notificationBuilder.build()) + } + } + } + + private fun endNotification(string: Int) { + synchronized(this) { + notificationManager.notify( + notificationId, + NotificationCompat.Builder(context, AIRAService.FILE_TRANSFER_NOTIFICATION_CHANNEL_ID) + .setCategory(NotificationCompat.CATEGORY_EVENT) + .setSmallIcon(R.drawable.ic_launcher) + .setContentTitle(sessionName) + .setContentText(context.getString(string)) + .build() + ) + isEnded = true + } + } + + fun onAborted() { + if (::notificationBuilder.isInitialized) { + endNotification(R.string.transfer_aborted) + } + } + + fun onCompleted() { + endNotification(R.string.transfer_completed) + } + + fun cancel() { + notificationManager.cancel(notificationId) + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/FilesReceiver.kt b/app/src/main/java/sushi/hardcore/aira/background_service/FilesReceiver.kt new file mode 100644 index 0000000..05a5f8a --- /dev/null +++ b/app/src/main/java/sushi/hardcore/aira/background_service/FilesReceiver.kt @@ -0,0 +1,46 @@ +package sushi.hardcore.aira.background_service + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.content.Context +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.NotificationManagerCompat +import sushi.hardcore.aira.R +import sushi.hardcore.aira.databinding.DialogAskFileBinding +import sushi.hardcore.aira.utils.FileUtils + +class FilesReceiver( + val files: List, + private val onAccepted: (FilesReceiver) -> Unit, + private val onAborted: (FilesReceiver) -> Unit, + context: Context, + notificationManager: NotificationManagerCompat, + sessionName: String +): FilesTransfer(context, notificationManager, sessionName) { + var shouldAsk = true + + @SuppressLint("SetTextI18n") + fun ask(activity: AppCompatActivity, senderName: String) { + val dialogBinding = DialogAskFileBinding.inflate(activity.layoutInflater) + dialogBinding.textTitle.text = activity.getString(R.string.want_to_send_files, senderName)+':' + val filesInfo = StringBuilder() + for (file in files) { + filesInfo.appendLine(file.fileName+" ("+FileUtils.formatSize(file.fileSize)+')') + } + dialogBinding.textFilesInfo.text = filesInfo.substring(0, filesInfo.length-1) + AlertDialog.Builder(activity) + .setTitle(R.string.download_file_request) + .setView(dialogBinding.root) + .setCancelable(false) + .setPositiveButton(R.string.download) { _, _ -> + onAccepted(this) + } + .setNegativeButton(R.string.refuse) { _, _ -> + onAborted(this) + } + .setOnDismissListener { + shouldAsk = false + } + .show() + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/FilesSender.kt b/app/src/main/java/sushi/hardcore/aira/background_service/FilesSender.kt new file mode 100644 index 0000000..fad443f --- /dev/null +++ b/app/src/main/java/sushi/hardcore/aira/background_service/FilesSender.kt @@ -0,0 +1,15 @@ +package sushi.hardcore.aira.background_service + +import android.content.Context +import androidx.core.app.NotificationManagerCompat + +class FilesSender( + val files: List, + context: Context, + notificationManager: NotificationManagerCompat, + sessionName: String +): FilesTransfer(context, notificationManager, sessionName) { + val lastChunkSizes = mutableListOf() + var nextChunk: ByteArray? = null + val msgQueue = mutableListOf() +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/FilesTransfer.kt b/app/src/main/java/sushi/hardcore/aira/background_service/FilesTransfer.kt new file mode 100644 index 0000000..f38de6c --- /dev/null +++ b/app/src/main/java/sushi/hardcore/aira/background_service/FilesTransfer.kt @@ -0,0 +1,9 @@ +package sushi.hardcore.aira.background_service + +import android.content.Context +import androidx.core.app.NotificationManagerCompat + +open class FilesTransfer(context: Context, notificationManager: NotificationManagerCompat, sessionName: String) { + val fileTransferNotification = FileTransferNotification(context, notificationManager, sessionName) + var index = 0 +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/PendingFile.kt b/app/src/main/java/sushi/hardcore/aira/background_service/PendingFile.kt new file mode 100644 index 0000000..3e8f336 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/aira/background_service/PendingFile.kt @@ -0,0 +1,5 @@ +package sushi.hardcore.aira.background_service + +open class PendingFile(val fileName: String, val fileSize: Long) { + var transferred = 0 +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/Protocol.kt b/app/src/main/java/sushi/hardcore/aira/background_service/Protocol.kt index 5e9c220..06a23fd 100644 --- a/app/src/main/java/sushi/hardcore/aira/background_service/Protocol.kt +++ b/app/src/main/java/sushi/hardcore/aira/background_service/Protocol.kt @@ -8,11 +8,11 @@ class Protocol { const val ASK_NAME: Byte = 0x01 const val TELL_NAME: Byte = 0x02 const val FILE: Byte = 0x03 - const val ASK_LARGE_FILE: Byte = 0x04 - const val ACCEPT_LARGE_FILE: Byte = 0x05 + const val ASK_LARGE_FILES: Byte = 0x04 + const val ACCEPT_LARGE_FILES: Byte = 0x05 const val LARGE_FILE_CHUNK: Byte = 0x06 const val ACK_CHUNK: Byte = 0x07 - const val ABORT_FILE_TRANSFER: Byte = 0x08 + const val ABORT_FILES_TRANSFER: Byte = 0x08 fun askName(): ByteArray { return byteArrayOf(ASK_NAME) @@ -30,16 +30,22 @@ class Protocol { return byteArrayOf(FILE)+ByteBuffer.allocate(2).putShort(fileName.length.toShort()).array()+fileName.toByteArray()+buffer } - fun askLargeFile(fileSize: Long, fileName: String): ByteArray { - return byteArrayOf(ASK_LARGE_FILE)+ByteBuffer.allocate(8).putLong(fileSize).array()+fileName.toByteArray() + fun askLargeFiles(files: List): ByteArray { + var buff = byteArrayOf(ASK_LARGE_FILES) + for (file in files) { + buff += ByteBuffer.allocate(8).putLong(file.fileSize).array() + buff += ByteBuffer.allocate(2).putShort(file.fileName.length.toShort()).array() + buff += file.fileName.toByteArray() + } + return buff } - fun acceptLargeFile(): ByteArray { - return byteArrayOf(ACCEPT_LARGE_FILE) + fun acceptLargeFiles(): ByteArray { + return byteArrayOf(ACCEPT_LARGE_FILES) } - fun abortFileTransfer(): ByteArray { - return byteArrayOf(ABORT_FILE_TRANSFER) + fun abortFilesTransfer(): ByteArray { + return byteArrayOf(ABORT_FILES_TRANSFER) } fun ackChunk(): ByteArray { @@ -59,16 +65,25 @@ class Protocol { return null } - class FileInfo(val fileName: String, val fileSize: Long) - - fun parseAskFile(buffer: ByteArray): FileInfo? { - return if (buffer.size > 9) { - val fileSize = ByteBuffer.wrap(buffer.sliceArray(1..8)).long - val fileName = buffer.sliceArray(9 until buffer.size).decodeToString() - FileInfo(fileName, fileSize) - } else { - null + fun parseAskFiles(buffer: ByteArray): List? { + val files = mutableListOf() + var n = 1 + while (n < buffer.size) { + if (buffer.size > n+10) { + val fileSize = ByteBuffer.wrap(buffer.sliceArray(n..n+8)).long + val fileNameLen = ByteBuffer.wrap(buffer.sliceArray(n+8..n+10)).short + if (buffer.size >= n+10+fileNameLen) { + val fileName = buffer.sliceArray(n+10 until n+10+fileNameLen).decodeToString() + files.add(ReceiveFile(fileName, fileSize)) + n += 10+fileNameLen + } else { + return null + } + } else { + return null + } } + return files } } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/ReceiveFile.kt b/app/src/main/java/sushi/hardcore/aira/background_service/ReceiveFile.kt new file mode 100644 index 0000000..c6b256c --- /dev/null +++ b/app/src/main/java/sushi/hardcore/aira/background_service/ReceiveFile.kt @@ -0,0 +1,10 @@ +package sushi.hardcore.aira.background_service + +import java.io.OutputStream + +class ReceiveFile ( + fileName: String, + fileSize: Long +): PendingFile(fileName, fileSize) { + var outputStream: OutputStream? = null +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/ReceiveFileTransfer.kt b/app/src/main/java/sushi/hardcore/aira/background_service/ReceiveFileTransfer.kt deleted file mode 100644 index 5650f3c..0000000 --- a/app/src/main/java/sushi/hardcore/aira/background_service/ReceiveFileTransfer.kt +++ /dev/null @@ -1,44 +0,0 @@ -package sushi.hardcore.aira.background_service - -import android.annotation.SuppressLint -import android.app.AlertDialog -import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity -import sushi.hardcore.aira.R -import sushi.hardcore.aira.utils.FileUtils -import java.io.OutputStream - -class ReceiveFileTransfer( - fileName: String, - fileSize: Long, - private val onAccepted: (fileTransfer: ReceiveFileTransfer) -> Unit, - private val onAborted: () -> Unit, -): FileTransfer(fileName, fileSize) { - var shouldAsk = true - var outputStream: OutputStream? = null - - @SuppressLint("SetTextI18n") - fun ask(activity: AppCompatActivity, senderName: String) { - val dialogView = activity.layoutInflater.inflate(R.layout.dialog_ask_file, null) - dialogView.findViewById(R.id.text_title).text = activity.getString(R.string.want_to_send_a_file, senderName, ":") - dialogView.findViewById(R.id.text_file_info).text = fileName+" ("+FileUtils.formatSize(fileSize)+")" - AlertDialog.Builder(activity) - .setTitle(R.string.download_file_request) - .setView(dialogView) - .setCancelable(false) - .setPositiveButton(R.string.download) { _, _ -> - outputStream = FileUtils.openFileForDownload(activity, fileName) - if (outputStream == null) { - onAborted() - } else { - onAccepted(this) - } - shouldAsk = false - } - .setNegativeButton(R.string.refuse) { _, _ -> - onAborted() - shouldAsk = false - } - .show() - } -} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/SendFileTransfer.kt b/app/src/main/java/sushi/hardcore/aira/background_service/SendFile.kt similarity index 52% rename from app/src/main/java/sushi/hardcore/aira/background_service/SendFileTransfer.kt rename to app/src/main/java/sushi/hardcore/aira/background_service/SendFile.kt index 5e8f4cd..66ff84a 100644 --- a/app/src/main/java/sushi/hardcore/aira/background_service/SendFileTransfer.kt +++ b/app/src/main/java/sushi/hardcore/aira/background_service/SendFile.kt @@ -2,11 +2,8 @@ package sushi.hardcore.aira.background_service import java.io.InputStream -class SendFileTransfer( +class SendFile( fileName: String, fileSize: Long, val inputStream: InputStream -): FileTransfer(fileName, fileSize) { - var nextChunk: ByteArray? = null - val msgQueue = mutableListOf() -} \ No newline at end of file +): PendingFile(fileName, fileSize) \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/aira/utils/FileUtils.kt b/app/src/main/java/sushi/hardcore/aira/utils/FileUtils.kt index 40e305c..37291e7 100644 --- a/app/src/main/java/sushi/hardcore/aira/utils/FileUtils.kt +++ b/app/src/main/java/sushi/hardcore/aira/utils/FileUtils.kt @@ -1,11 +1,18 @@ package sushi.hardcore.aira.utils +import android.content.ContentResolver import android.content.ContentValues import android.content.Context +import android.net.Uri import android.os.Environment import android.provider.MediaStore +import android.provider.OpenableColumns import android.webkit.MimeTypeMap +import android.widget.Toast +import sushi.hardcore.aira.background_service.ReceiveFile +import sushi.hardcore.aira.background_service.SendFile import java.io.File +import java.io.FileNotFoundException import java.io.OutputStream import java.text.DecimalFormat import java.text.SimpleDateFormat @@ -24,6 +31,26 @@ object FileUtils { ) + " " + units[digitGroups] } + fun openFileFromUri(context: Context, uri: Uri): SendFile? { + var sendFile: SendFile? = null + val cursor = context.contentResolver.query(uri, null, null, null, null) + if (cursor != null) { + if (cursor.moveToFirst()) { + try { + context.contentResolver.openInputStream(uri)?.let { inputStream -> + val fileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + val fileSize = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE)) + sendFile = SendFile(fileName, fileSize, inputStream) + } + } catch (e: FileNotFoundException) { + Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show() + } + } + cursor.close() + } + return sendFile + } + fun openFileForDownload(context: Context, fileName: String): OutputStream? { val fileExtension = fileName.substringAfterLast(".") val dateExtension = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date()) diff --git a/app/src/main/res/layout/dialog_ask_file.xml b/app/src/main/res/layout/dialog_ask_file.xml index a0ab6c4..05d591d 100644 --- a/app/src/main/res/layout/dialog_ask_file.xml +++ b/app/src/main/res/layout/dialog_ask_file.xml @@ -10,10 +10,19 @@ android:layout_height="wrap_content" style="@style/Label"/> - + android:layout_height="wrap_content"> + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8abd17f..b231b5a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,7 +48,7 @@ Mark read Download file requests Databases directory creation failed - %s wants to send you a file%s + %s wants to send you some files Download file request Download Refuse @@ -56,9 +56,9 @@ Transfer aborted Transfer completed Reply - Failed to retrieve URI - Send %s (%s) to %s ? - Failed to open file + Failed to open URI + Send %s (%s) to %s ? + Send %d files to %s ? Send with AIRA Your identity\'s fingerprint: Fingerprint: