Multiple Files Transfers
This commit is contained in:
parent
458be75114
commit
51ebfb22db
@ -32,6 +32,7 @@
|
|||||||
<activity android:name=".MainActivity">
|
<activity android:name=".MainActivity">
|
||||||
<intent-filter android:label="@string/share_label">
|
<intent-filter android:label="@string/share_label">
|
||||||
<action android:name="android.intent.action.SEND"/>
|
<action android:name="android.intent.action.SEND"/>
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<data android:mimeType="*/*"/>
|
<data android:mimeType="*/*"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -1,64 +1,44 @@
|
|||||||
package sushi.hardcore.aira
|
package sushi.hardcore.aira
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import sushi.hardcore.aira.adapters.ChatAdapter
|
import sushi.hardcore.aira.adapters.ChatAdapter
|
||||||
import sushi.hardcore.aira.background_service.AIRAService
|
import sushi.hardcore.aira.background_service.*
|
||||||
import sushi.hardcore.aira.background_service.Protocol
|
|
||||||
import sushi.hardcore.aira.background_service.ReceiveFileTransfer
|
|
||||||
import sushi.hardcore.aira.databinding.ActivityChatBinding
|
import sushi.hardcore.aira.databinding.ActivityChatBinding
|
||||||
import sushi.hardcore.aira.databinding.DialogFingerprintsBinding
|
import sushi.hardcore.aira.databinding.DialogFingerprintsBinding
|
||||||
import sushi.hardcore.aira.databinding.DialogInfoBinding
|
import sushi.hardcore.aira.databinding.DialogInfoBinding
|
||||||
import sushi.hardcore.aira.utils.FileUtils
|
import sushi.hardcore.aira.utils.FileUtils
|
||||||
import sushi.hardcore.aira.utils.StringUtils
|
import sushi.hardcore.aira.utils.StringUtils
|
||||||
import java.io.FileNotFoundException
|
|
||||||
|
|
||||||
class ChatActivity : AppCompatActivity() {
|
class ChatActivity : ServiceBoundActivity() {
|
||||||
private external fun generateFingerprint(publicKey: ByteArray): String
|
private external fun generateFingerprint(publicKey: ByteArray): String
|
||||||
|
|
||||||
private lateinit var binding: ActivityChatBinding
|
private lateinit var binding: ActivityChatBinding
|
||||||
private var sessionId = -1
|
private var sessionId = -1
|
||||||
private lateinit var sessionName: String
|
private lateinit var sessionName: String
|
||||||
private lateinit var airaService: AIRAService
|
|
||||||
private lateinit var chatAdapter: ChatAdapter
|
private lateinit var chatAdapter: ChatAdapter
|
||||||
private var lastLoadedMessageOffset = 0
|
private var lastLoadedMessageOffset = 0
|
||||||
private var isActivityInForeground = false
|
private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
|
||||||
private val filePicker = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
if (isServiceInitialized() && uris.size > 0) {
|
||||||
if (::airaService.isInitialized && uri != null) {
|
airaService.sendFilesFromUris(sessionId, uris)?.let { msgs ->
|
||||||
contentResolver.query(uri, null, null, null, null)?.let { cursor ->
|
for (msg in msgs) {
|
||||||
if (cursor.moveToFirst()) {
|
chatAdapter.newMessage(ChatItem(true, msg))
|
||||||
try {
|
if (airaService.contacts.contains(sessionId)) {
|
||||||
contentResolver.openInputStream(uri)?.let { inputStream ->
|
lastLoadedMessageOffset += 1
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cursor.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,7 +63,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
fun loadMsgsIfNeeded(recyclerView: RecyclerView) {
|
fun loadMsgsIfNeeded(recyclerView: RecyclerView) {
|
||||||
if (!recyclerView.canScrollVertically(-1) && ::airaService.isInitialized) {
|
if (!recyclerView.canScrollVertically(-1) && isServiceInitialized()) {
|
||||||
airaService.contacts[sessionId]?.let { contact ->
|
airaService.contacts[sessionId]?.let { contact ->
|
||||||
loadMsgs(contact.uuid)
|
loadMsgs(contact.uuid)
|
||||||
}
|
}
|
||||||
@ -97,109 +77,108 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Intent(this, AIRAService::class.java).also { serviceIntent ->
|
binding.buttonSend.setOnClickListener {
|
||||||
bindService(serviceIntent, object : ServiceConnection {
|
val msg = binding.editMessage.text.toString()
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
|
airaService.sendTo(sessionId, Protocol.newMessage(msg))
|
||||||
val binder = service as AIRAService.AIRABinder
|
binding.editMessage.text.clear()
|
||||||
airaService = binder.getService()
|
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 ->
|
chatAdapter.clear()
|
||||||
displayIconTrustLevel(true, contact.verified)
|
airaService.contacts[sessionId]?.let { contact ->
|
||||||
loadMsgs(contact.uuid)
|
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) {
|
airaService.receiveFileTransfers[sessionId]?.let {
|
||||||
it.ask(this@ChatActivity, sessionName)
|
if (it.shouldAsk) {
|
||||||
|
it.ask(this@ChatActivity, sessionName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
||||||
|
val showBottomPanel = {
|
||||||
|
findViewById<ConstraintLayout>(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 {
|
override fun onSessionDisconnect(sessionId: Int) {
|
||||||
for (chatItem in it.asReversed()) {
|
if (this@ChatActivity.sessionId == sessionId) {
|
||||||
chatAdapter.newLoadedMessage(chatItem)
|
runOnUiThread {
|
||||||
|
findViewById<ConstraintLayout>(R.id.bottom_panel).visibility = View.GONE
|
||||||
|
binding.buttonSend.setOnClickListener(null)
|
||||||
|
binding.buttonAttach.setOnClickListener(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
override fun onNameTold(sessionId: Int, name: String) {
|
||||||
val onConnected = {
|
if (this@ChatActivity.sessionId == sessionId) {
|
||||||
findViewById<ConstraintLayout>(R.id.bottom_panel).visibility = View.VISIBLE
|
runOnUiThread {
|
||||||
binding.buttonSend.setOnClickListener {
|
sessionName = name
|
||||||
val msg = binding.editMessage.text.toString()
|
title = name
|
||||||
airaService.sendTo(sessionId, Protocol.newMessage(msg))
|
}
|
||||||
binding.editMessage.text.clear()
|
}
|
||||||
chatAdapter.newMessage(ChatItem(true, Protocol.newMessage(msg)))
|
}
|
||||||
|
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)) {
|
if (airaService.contacts.contains(sessionId)) {
|
||||||
lastLoadedMessageOffset += 1
|
lastLoadedMessageOffset += 1
|
||||||
}
|
}
|
||||||
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
airaService.isAppInBackground
|
||||||
}
|
} else {
|
||||||
binding.buttonAttach.setOnClickListener {
|
false
|
||||||
filePicker.launch("*/*")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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<ConstraintLayout>(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 {
|
override fun onAskLargeFiles(sessionId: Int, name: String, filesReceiver: FilesReceiver): Boolean {
|
||||||
return if (this@ChatActivity.sessionId == sessionId) {
|
return if (this@ChatActivity.sessionId == sessionId) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
receiveFileTransfer.ask(this@ChatActivity, name)
|
filesReceiver.ask(this@ChatActivity, name)
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
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?) {}
|
airaService.isAppInBackground = false
|
||||||
}, Context.BIND_AUTO_CREATE)
|
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 {
|
when {
|
||||||
isVerified -> {
|
isVerified -> {
|
||||||
binding.imageTrustLevel.setImageResource(R.drawable.ic_verified)
|
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 {
|
AIRADatabase.loadMsgs(contactUuid, lastLoadedMessageOffset, Constants.MSG_LOADING_COUNT)?.let {
|
||||||
for (chatItem in it.asReversed()) {
|
for (chatItem in it.asReversed()) {
|
||||||
chatAdapter.newLoadedMessage(chatItem)
|
chatAdapter.newLoadedMessage(chatItem)
|
||||||
@ -328,20 +307,10 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onStart() {
|
||||||
super.onResume()
|
super.onStart()
|
||||||
isActivityInForeground = true
|
if (isServiceInitialized()) {
|
||||||
if (::airaService.isInitialized) {
|
|
||||||
airaService.setSeen(sessionId, true)
|
airaService.setSeen(sessionId, true)
|
||||||
airaService.isAppInBackground = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
isActivityInForeground = false
|
|
||||||
if (::airaService.isInitialized) {
|
|
||||||
airaService.isAppInBackground = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
package sushi.hardcore.aira
|
package sushi.hardcore.aira
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -15,11 +13,10 @@ import android.widget.AbsListView
|
|||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import sushi.hardcore.aira.adapters.Session
|
import sushi.hardcore.aira.adapters.Session
|
||||||
import sushi.hardcore.aira.adapters.SessionAdapter
|
import sushi.hardcore.aira.adapters.SessionAdapter
|
||||||
import sushi.hardcore.aira.background_service.AIRAService
|
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.ActivityMainBinding
|
||||||
import sushi.hardcore.aira.databinding.DialogIpAddressesBinding
|
import sushi.hardcore.aira.databinding.DialogIpAddressesBinding
|
||||||
import sushi.hardcore.aira.utils.FileUtils
|
import sushi.hardcore.aira.utils.FileUtils
|
||||||
@ -27,9 +24,8 @@ import sushi.hardcore.aira.utils.StringUtils
|
|||||||
import java.lang.StringBuilder
|
import java.lang.StringBuilder
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : ServiceBoundActivity() {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
private lateinit var airaService: AIRAService
|
|
||||||
private lateinit var onlineSessionAdapter: SessionAdapter
|
private lateinit var onlineSessionAdapter: SessionAdapter
|
||||||
private var offlineSessionAdapter: SessionAdapter? = null
|
private var offlineSessionAdapter: SessionAdapter? = null
|
||||||
private val onSessionsItemClickSendFile = AdapterView.OnItemClickListener { adapter, _, position, _ ->
|
private val onSessionsItemClickSendFile = AdapterView.OnItemClickListener { adapter, _, position, _ ->
|
||||||
@ -70,9 +66,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAskLargeFile(sessionId: Int, name: String, receiveFileTransfer: ReceiveFileTransfer): Boolean {
|
override fun onAskLargeFiles(sessionId: Int, name: String, filesReceiver: FilesReceiver): Boolean {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
receiveFileTransfer.ask(this@MainActivity, name)
|
filesReceiver.ask(this@MainActivity, name)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -86,7 +82,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val identityName = intent.getStringExtra("identityName")
|
val identityName = intent.getStringExtra("identityName")
|
||||||
identityName?.let { title = it }
|
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)
|
onlineSessionAdapter = SessionAdapter(this)
|
||||||
binding.onlineSessions.apply {
|
binding.onlineSessions.apply {
|
||||||
@ -133,30 +129,29 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setOnScrollListener(onSessionsScrollListener)
|
setOnScrollListener(onSessionsScrollListener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Intent(this, AIRAService::class.java).also { serviceIntent ->
|
serviceConnection = object : ServiceConnection {
|
||||||
bindService(serviceIntent, object : ServiceConnection {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
|
val binder = service as AIRAService.AIRABinder
|
||||||
val binder = service as AIRAService.AIRABinder
|
airaService = binder.getService()
|
||||||
airaService = binder.getService()
|
airaService.uiCallbacks = uiCallbacks
|
||||||
airaService.uiCallbacks = uiCallbacks
|
airaService.isAppInBackground = false
|
||||||
airaService.isAppInBackground = false
|
refreshSessions()
|
||||||
loadContacts()
|
if (AIRAService.isServiceRunning) {
|
||||||
if (AIRAService.isServiceRunning) {
|
title = airaService.identityName
|
||||||
title = airaService.identityName
|
} else {
|
||||||
loadSessions()
|
airaService.identityName = identityName
|
||||||
} else {
|
startService(serviceIntent)
|
||||||
airaService.identityName = identityName
|
|
||||||
startService(serviceIntent)
|
|
||||||
}
|
|
||||||
binding.refresher.setOnRefreshListener {
|
|
||||||
airaService.restartDiscovery()
|
|
||||||
binding.refresher.isRefreshing = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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 {
|
binding.buttonShowIp.setOnClickListener {
|
||||||
val ipAddresses = StringBuilder()
|
val ipAddresses = StringBuilder()
|
||||||
for (iface in NetworkInterface.getNetworkInterfaces()) {
|
for (iface in NetworkInterface.getNetworkInterfaces()) {
|
||||||
@ -175,7 +170,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
binding.editPeerIp.setOnEditorActionListener { _, _, _ ->
|
binding.editPeerIp.setOnEditorActionListener { _, _, _ ->
|
||||||
if (::airaService.isInitialized){
|
if (isServiceInitialized()){
|
||||||
airaService.connectTo(binding.editPeerIp.text.toString())
|
airaService.connectTo(binding.editPeerIp.text.toString())
|
||||||
}
|
}
|
||||||
binding.editPeerIp.text.clear()
|
binding.editPeerIp.text.clear()
|
||||||
@ -196,7 +191,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.close -> {
|
R.id.close -> {
|
||||||
if (::airaService.isInitialized) {
|
if (isServiceInitialized()) {
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(R.string.warning)
|
.setTitle(R.string.warning)
|
||||||
.setMessage(R.string.ask_log_out)
|
.setMessage(R.string.ask_log_out)
|
||||||
@ -240,27 +235,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onStop() {
|
||||||
super.onPause()
|
super.onStop()
|
||||||
if (::airaService.isInitialized) {
|
if (isServiceInitialized()) {
|
||||||
airaService.isAppInBackground = true
|
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() {
|
override fun onBackPressed() {
|
||||||
if (isSelecting()) {
|
if (isSelecting()) {
|
||||||
unSelectAll()
|
unSelectAll()
|
||||||
@ -328,34 +309,37 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun askShareFileTo(session: Session) {
|
private fun askShareFileTo(session: Session) {
|
||||||
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
|
var uris: ArrayList<Uri>? = null
|
||||||
if (uri == null) {
|
when (intent.action) {
|
||||||
Toast.makeText(this, R.string.share_uri_null, Toast.LENGTH_SHORT).show()
|
Intent.ACTION_SEND -> intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)?.let {
|
||||||
} else {
|
uris = arrayListOf(it)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ package sushi.hardcore.aira.background_service
|
|||||||
import android.app.*
|
import android.app.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.net.nsd.NsdManager
|
import android.net.nsd.NsdManager
|
||||||
import android.net.nsd.NsdServiceInfo
|
import android.net.nsd.NsdServiceInfo
|
||||||
import android.os.*
|
import android.os.*
|
||||||
@ -14,9 +15,9 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.app.RemoteInput
|
import androidx.core.app.RemoteInput
|
||||||
import sushi.hardcore.aira.*
|
import sushi.hardcore.aira.*
|
||||||
|
import sushi.hardcore.aira.utils.FileUtils
|
||||||
import sushi.hardcore.aira.utils.StringUtils
|
import sushi.hardcore.aira.utils.StringUtils
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
|
||||||
import java.net.*
|
import java.net.*
|
||||||
import java.nio.channels.*
|
import java.nio.channels.*
|
||||||
|
|
||||||
@ -44,8 +45,8 @@ class AIRAService : Service() {
|
|||||||
private lateinit var selector: Selector
|
private lateinit var selector: Selector
|
||||||
private val sessionIdByKey = mutableMapOf<SelectionKey, Int>()
|
private val sessionIdByKey = mutableMapOf<SelectionKey, Int>()
|
||||||
private val notificationIdManager = NotificationIdManager()
|
private val notificationIdManager = NotificationIdManager()
|
||||||
private val sendFileTransfers = mutableMapOf<Int, SendFileTransfer>()
|
private val sendFileTransfers = mutableMapOf<Int, FilesSender>()
|
||||||
val receiveFileTransfers = mutableMapOf<Int, ReceiveFileTransfer>()
|
val receiveFileTransfers = mutableMapOf<Int, FilesReceiver>()
|
||||||
lateinit var contacts: HashMap<Int, Contact>
|
lateinit var contacts: HashMap<Int, Contact>
|
||||||
private lateinit var serviceHandler: Handler
|
private lateinit var serviceHandler: Handler
|
||||||
private lateinit var notificationManager: NotificationManagerCompat
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
@ -103,7 +104,7 @@ class AIRAService : Service() {
|
|||||||
fun onSessionDisconnect(sessionId: Int)
|
fun onSessionDisconnect(sessionId: Int)
|
||||||
fun onNameTold(sessionId: Int, name: String)
|
fun onNameTold(sessionId: Int, name: String)
|
||||||
fun onNewMessage(sessionId: Int, data: ByteArray): Boolean
|
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) {
|
fun connectTo(ip: String) {
|
||||||
@ -133,29 +134,54 @@ class AIRAService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendFileTo(sessionId: Int, fileName: String, fileSize: Long, inputStream: InputStream): ByteArray? {
|
fun sendFilesFromUris(sessionId: Int, uris: List<Uri>): List<ByteArray>? {
|
||||||
if (fileSize <= Constants.fileSizeLimit) {
|
val files = mutableListOf<SendFile>()
|
||||||
val buffer = inputStream.readBytes()
|
var useLargeFileTransfer = false
|
||||||
inputStream.close()
|
for (uri in uris) {
|
||||||
sendTo(sessionId, Protocol.newFile(fileName, buffer))
|
FileUtils.openFileFromUri(this, uri)?.let { sendFile ->
|
||||||
AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer)?.let { rawFileUuid ->
|
files.add(sendFile)
|
||||||
val msg = byteArrayOf(Protocol.FILE)+rawFileUuid+fileName.toByteArray()
|
if (sendFile.fileSize > Constants.fileSizeLimit) {
|
||||||
saveMsg(sessionId, msg)
|
useLargeFileTransfer = true
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return if (useLargeFileTransfer) {
|
||||||
|
sendLargeFilesTo(sessionId, files)
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
val msgs = mutableListOf<ByteArray>()
|
||||||
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sendLargeFilesTo(sessionId: Int, files: MutableList<SendFile>) {
|
||||||
|
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() {
|
fun logOut() {
|
||||||
serviceHandler.sendEmptyMessage(MESSAGE_LOGOUT)
|
serviceHandler.sendEmptyMessage(MESSAGE_LOGOUT)
|
||||||
isServiceRunning = false
|
isServiceRunning = false
|
||||||
@ -232,11 +258,11 @@ class AIRAService : Service() {
|
|||||||
fun deleteConversation(sessionId: Int): Boolean {
|
fun deleteConversation(sessionId: Int): Boolean {
|
||||||
contacts[sessionId]?.let {
|
contacts[sessionId]?.let {
|
||||||
return if (AIRADatabase.deleteConversation(it.uuid)) {
|
return if (AIRADatabase.deleteConversation(it.uuid)) {
|
||||||
savedMsgs[sessionId] = mutableListOf()
|
savedMsgs[sessionId] = mutableListOf()
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -358,33 +384,19 @@ class AIRAService : Service() {
|
|||||||
notificationManager.notify(notificationIdManager.getMessageNotificationId(sessionId), notificationBuilder.build())
|
notificationManager.notify(notificationIdManager.getMessageNotificationId(sessionId), notificationBuilder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createFileTransferNotification(sessionId: Int, fileTransfer: FileTransfer) {
|
private fun initFileTransferNotification(sessionId: Int, fileTransferNotification: FileTransferNotification, file: PendingFile) {
|
||||||
fileTransfer.notificationBuilder = NotificationCompat.Builder(this, FILE_TRANSFER_NOTIFICATION_CHANNEL_ID)
|
fileTransferNotification.initFileTransferNotification(
|
||||||
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
notificationIdManager.getFileTransferNotificationId(sessionId),
|
||||||
.setSmallIcon(R.drawable.ic_launcher)
|
file.fileName,
|
||||||
.setContentTitle(getNameOf(sessionId))
|
file.fileSize.toInt(),
|
||||||
.setContentText(fileTransfer.fileName)
|
Intent(this, NotificationBroadcastReceiver::class.java).apply {
|
||||||
.setOngoing(true)
|
val bundle = Bundle()
|
||||||
.setProgress(fileTransfer.fileSize.toInt(), fileTransfer.transferred, true)
|
bundle.putBinder("binder", AIRABinder())
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
|
bundle.putInt("sessionId", sessionId)
|
||||||
val cancelIntent = PendingIntent.getBroadcast(this, 0,
|
putExtra("bundle", bundle)
|
||||||
Intent(this, NotificationBroadcastReceiver::class.java).apply {
|
action = NotificationBroadcastReceiver.ACTION_CANCEL_FILE_TRANSFER
|
||||||
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 saveMsg(sessionId: Int, msg: ByteArray) {
|
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)
|
val nextChunk = ByteArray(Constants.FILE_CHUNK_SIZE)
|
||||||
nextChunk[0] = Protocol.LARGE_FILE_CHUNK
|
nextChunk[0] = Protocol.LARGE_FILE_CHUNK
|
||||||
val read = fileTransfer.inputStream.read(nextChunk, 1, Constants.FILE_CHUNK_SIZE-1)
|
val read = try {
|
||||||
fileTransfer.nextChunk = if (read > 0) {
|
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)
|
session.encrypt(nextChunk)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun flushSendFileTransfer(sessionId: Int, session: Session, fileTransfer: SendFileTransfer) {
|
private fun flushSendFileTransfer(sessionId: Int, session: Session, filesSender: FilesSender) {
|
||||||
fileTransfer.nextChunk?.let {
|
synchronized(filesSender) { //prevent sending nextChunk two times when canceling
|
||||||
session.writeAll(it)
|
filesSender.nextChunk?.let {
|
||||||
while (fileTransfer.msgQueue.size > 0) {
|
session.writeAll(it)
|
||||||
val msg = fileTransfer.msgQueue.removeAt(0)
|
filesSender.nextChunk = null
|
||||||
sendAndSave(sessionId, msg)
|
while (filesSender.msgQueue.size > 0) {
|
||||||
|
val msg = filesSender.msgQueue.removeAt(0)
|
||||||
|
sendAndSave(sessionId, msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelFileTransfer(sessionId: Int, session: Session, outgoing: Boolean) {
|
private fun cancelFileTransfer(sessionId: Int, session: Session, outgoing: Boolean) {
|
||||||
sendFileTransfers[sessionId]?.let {
|
sendFileTransfers[sessionId]?.let {
|
||||||
it.inputStream.close()
|
it.files[it.index].inputStream.close()
|
||||||
it.onAborted(this, notificationManager, getNameOf(sessionId))
|
|
||||||
flushSendFileTransfer(sessionId, session, it)
|
flushSendFileTransfer(sessionId, session, it)
|
||||||
sendFileTransfers.remove(sessionId)
|
sendFileTransfers.remove(sessionId)!!.fileTransferNotification.onAborted()
|
||||||
}
|
}
|
||||||
receiveFileTransfers[sessionId]?.let {
|
receiveFileTransfers[sessionId]?.let {
|
||||||
it.outputStream?.close()
|
it.files[it.index].outputStream?.close()
|
||||||
it.onAborted(this, notificationManager, getNameOf(sessionId))
|
receiveFileTransfers.remove(sessionId)!!.fileTransferNotification.onAborted()
|
||||||
receiveFileTransfers.remove(sessionId)
|
|
||||||
}
|
}
|
||||||
if (outgoing) {
|
if (outgoing) {
|
||||||
session.encryptAndSend(Protocol.abortFileTransfer())
|
session.encryptAndSend(Protocol.abortFilesTransfer())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -568,19 +586,38 @@ class AIRAService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Protocol.LARGE_FILE_CHUNK -> {
|
Protocol.LARGE_FILE_CHUNK -> {
|
||||||
receiveFileTransfers[sessionId]?.let { fileTransfer ->
|
receiveFileTransfers[sessionId]?.let { filesReceiver ->
|
||||||
fileTransfer.outputStream?.let { outputStream ->
|
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)
|
val chunk = buffer.sliceArray(1 until buffer.size)
|
||||||
try {
|
try {
|
||||||
outputStream.write(chunk)
|
outputStream.write(chunk)
|
||||||
session.encryptAndSend(Protocol.ackChunk())
|
session.encryptAndSend(Protocol.ackChunk())
|
||||||
fileTransfer.transferred += chunk.size
|
file.transferred += chunk.size
|
||||||
if (fileTransfer.transferred >= fileTransfer.fileSize) {
|
if (file.transferred >= file.fileSize) {
|
||||||
outputStream.close()
|
outputStream.close()
|
||||||
receiveFileTransfers.remove(sessionId)
|
if (filesReceiver.index == filesReceiver.files.size-1) {
|
||||||
fileTransfer.onCompleted(this, notificationManager, getNameOf(sessionId))
|
receiveFileTransfers.remove(sessionId)
|
||||||
|
filesReceiver.fileTransferNotification.onCompleted()
|
||||||
|
} else {
|
||||||
|
filesReceiver.index += 1
|
||||||
|
val nextFile = filesReceiver.files[filesReceiver.index]
|
||||||
|
initFileTransferNotification(
|
||||||
|
sessionId,
|
||||||
|
filesReceiver.fileTransferNotification,
|
||||||
|
nextFile
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fileTransfer.updateNotificationProgress(notificationManager)
|
filesReceiver.fileTransferNotification.updateNotificationProgress(chunk.size)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
cancelFileTransfer(sessionId)
|
cancelFileTransfer(sessionId)
|
||||||
@ -589,46 +626,72 @@ class AIRAService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Protocol.ACK_CHUNK -> {
|
Protocol.ACK_CHUNK -> {
|
||||||
sendFileTransfers[sessionId]?.let { fileTransfer ->
|
sendFileTransfers[sessionId]?.let { filesSender ->
|
||||||
flushSendFileTransfer(sessionId, session, fileTransfer)
|
flushSendFileTransfer(sessionId, session, filesSender)
|
||||||
fileTransfer.transferred += fileTransfer.nextChunk?.size ?: 0
|
val file = filesSender.files[filesSender.index]
|
||||||
if (fileTransfer.transferred >= fileTransfer.fileSize) {
|
val chunkSize = filesSender.lastChunkSizes.removeAt(0)
|
||||||
fileTransfer.inputStream.close()
|
file.transferred += chunkSize
|
||||||
sendFileTransfers.remove(sessionId)
|
if (file.transferred >= file.fileSize) {
|
||||||
fileTransfer.onCompleted(this, notificationManager, getNameOf(sessionId))
|
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 {
|
} else {
|
||||||
encryptNextChunk(session, fileTransfer)
|
encryptNextChunk(session, filesSender)
|
||||||
fileTransfer.updateNotificationProgress(notificationManager)
|
filesSender.fileTransferNotification.updateNotificationProgress(chunkSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Protocol.ABORT_FILE_TRANSFER -> cancelFileTransfer(sessionId, session, false)
|
Protocol.ABORT_FILES_TRANSFER -> cancelFileTransfer(sessionId, session, false)
|
||||||
Protocol.ACCEPT_LARGE_FILE -> {
|
Protocol.ACCEPT_LARGE_FILES -> {
|
||||||
sendFileTransfers[sessionId]?.let { fileTransfer ->
|
sendFileTransfers[sessionId]?.let { filesSender ->
|
||||||
encryptNextChunk(session, fileTransfer)
|
encryptNextChunk(session, filesSender)
|
||||||
fileTransfer.nextChunk?.let {
|
filesSender.nextChunk?.let {
|
||||||
session.writeAll(it)
|
session.writeAll(it)
|
||||||
fileTransfer.transferred += fileTransfer.nextChunk?.size ?: 0
|
encryptNextChunk(session, filesSender)
|
||||||
encryptNextChunk(session, fileTransfer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Protocol.ASK_LARGE_FILE -> {
|
Protocol.ASK_LARGE_FILES -> {
|
||||||
if (receiveFileTransfers[sessionId] == null) {
|
if (!receiveFileTransfers.containsKey(sessionId) && !sendFileTransfers.containsKey(sessionId)) {
|
||||||
Protocol.parseAskFile(buffer)?.let { fileInfo ->
|
Protocol.parseAskFiles(buffer)?.let { files ->
|
||||||
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
|
|
||||||
val name = getNameOf(sessionId)
|
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
|
var shouldSendNotification = true
|
||||||
if (!isAppInBackground) {
|
if (!isAppInBackground) {
|
||||||
if (uiCallbacks?.onAskLargeFile(sessionId, name, fileTransfer) == true) {
|
if (uiCallbacks?.onAskLargeFiles(sessionId, name, filesReceiver) == true) {
|
||||||
shouldSendNotification = false
|
shouldSendNotification = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -637,7 +700,7 @@ class AIRAService : Service() {
|
|||||||
.setCategory(NotificationCompat.CATEGORY_EVENT)
|
.setCategory(NotificationCompat.CATEGORY_EVENT)
|
||||||
.setSmallIcon(R.drawable.ic_launcher)
|
.setSmallIcon(R.drawable.ic_launcher)
|
||||||
.setContentTitle(getString(R.string.download_file_request))
|
.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
|
.setOngoing(true) //not cancelable
|
||||||
.setContentIntent(
|
.setContentIntent(
|
||||||
PendingIntent.getActivity(this, 0, Intent(this, ChatActivity::class.java).apply {
|
PendingIntent.getActivity(this, 0, Intent(this, ChatActivity::class.java).apply {
|
||||||
@ -705,16 +768,8 @@ class AIRAService : Service() {
|
|||||||
sessions.remove(sessionId)
|
sessions.remove(sessionId)
|
||||||
savedMsgs.remove(sessionId)
|
savedMsgs.remove(sessionId)
|
||||||
savedNames.remove(sessionId)
|
savedNames.remove(sessionId)
|
||||||
sendFileTransfers.remove(sessionId)?.notificationId?.let {
|
sendFileTransfers.remove(sessionId)?.fileTransferNotification?.cancel()
|
||||||
if (it != -1) {
|
receiveFileTransfers.remove(sessionId)?.fileTransferNotification?.cancel()
|
||||||
notificationManager.cancel(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
receiveFileTransfers.remove(sessionId)?.notificationId?.let {
|
|
||||||
if (it != -1) {
|
|
||||||
notificationManager.cancel(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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<ReceiveFile>,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package sushi.hardcore.aira.background_service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
|
||||||
|
class FilesSender(
|
||||||
|
val files: List<SendFile>,
|
||||||
|
context: Context,
|
||||||
|
notificationManager: NotificationManagerCompat,
|
||||||
|
sessionName: String
|
||||||
|
): FilesTransfer(context, notificationManager, sessionName) {
|
||||||
|
val lastChunkSizes = mutableListOf<Int>()
|
||||||
|
var nextChunk: ByteArray? = null
|
||||||
|
val msgQueue = mutableListOf<ByteArray>()
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package sushi.hardcore.aira.background_service
|
||||||
|
|
||||||
|
open class PendingFile(val fileName: String, val fileSize: Long) {
|
||||||
|
var transferred = 0
|
||||||
|
}
|
@ -8,11 +8,11 @@ class Protocol {
|
|||||||
const val ASK_NAME: Byte = 0x01
|
const val ASK_NAME: Byte = 0x01
|
||||||
const val TELL_NAME: Byte = 0x02
|
const val TELL_NAME: Byte = 0x02
|
||||||
const val FILE: Byte = 0x03
|
const val FILE: Byte = 0x03
|
||||||
const val ASK_LARGE_FILE: Byte = 0x04
|
const val ASK_LARGE_FILES: Byte = 0x04
|
||||||
const val ACCEPT_LARGE_FILE: Byte = 0x05
|
const val ACCEPT_LARGE_FILES: Byte = 0x05
|
||||||
const val LARGE_FILE_CHUNK: Byte = 0x06
|
const val LARGE_FILE_CHUNK: Byte = 0x06
|
||||||
const val ACK_CHUNK: Byte = 0x07
|
const val ACK_CHUNK: Byte = 0x07
|
||||||
const val ABORT_FILE_TRANSFER: Byte = 0x08
|
const val ABORT_FILES_TRANSFER: Byte = 0x08
|
||||||
|
|
||||||
fun askName(): ByteArray {
|
fun askName(): ByteArray {
|
||||||
return byteArrayOf(ASK_NAME)
|
return byteArrayOf(ASK_NAME)
|
||||||
@ -30,16 +30,22 @@ class Protocol {
|
|||||||
return byteArrayOf(FILE)+ByteBuffer.allocate(2).putShort(fileName.length.toShort()).array()+fileName.toByteArray()+buffer
|
return byteArrayOf(FILE)+ByteBuffer.allocate(2).putShort(fileName.length.toShort()).array()+fileName.toByteArray()+buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
fun askLargeFile(fileSize: Long, fileName: String): ByteArray {
|
fun askLargeFiles(files: List<SendFile>): ByteArray {
|
||||||
return byteArrayOf(ASK_LARGE_FILE)+ByteBuffer.allocate(8).putLong(fileSize).array()+fileName.toByteArray()
|
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 {
|
fun acceptLargeFiles(): ByteArray {
|
||||||
return byteArrayOf(ACCEPT_LARGE_FILE)
|
return byteArrayOf(ACCEPT_LARGE_FILES)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun abortFileTransfer(): ByteArray {
|
fun abortFilesTransfer(): ByteArray {
|
||||||
return byteArrayOf(ABORT_FILE_TRANSFER)
|
return byteArrayOf(ABORT_FILES_TRANSFER)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ackChunk(): ByteArray {
|
fun ackChunk(): ByteArray {
|
||||||
@ -59,16 +65,25 @@ class Protocol {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
class FileInfo(val fileName: String, val fileSize: Long)
|
fun parseAskFiles(buffer: ByteArray): List<ReceiveFile>? {
|
||||||
|
val files = mutableListOf<ReceiveFile>()
|
||||||
fun parseAskFile(buffer: ByteArray): FileInfo? {
|
var n = 1
|
||||||
return if (buffer.size > 9) {
|
while (n < buffer.size) {
|
||||||
val fileSize = ByteBuffer.wrap(buffer.sliceArray(1..8)).long
|
if (buffer.size > n+10) {
|
||||||
val fileName = buffer.sliceArray(9 until buffer.size).decodeToString()
|
val fileSize = ByteBuffer.wrap(buffer.sliceArray(n..n+8)).long
|
||||||
FileInfo(fileName, fileSize)
|
val fileNameLen = ByteBuffer.wrap(buffer.sliceArray(n+8..n+10)).short
|
||||||
} else {
|
if (buffer.size >= n+10+fileNameLen) {
|
||||||
null
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
@ -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<TextView>(R.id.text_title).text = activity.getString(R.string.want_to_send_a_file, senderName, ":")
|
|
||||||
dialogView.findViewById<TextView>(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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,11 +2,8 @@ package sushi.hardcore.aira.background_service
|
|||||||
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
class SendFileTransfer(
|
class SendFile(
|
||||||
fileName: String,
|
fileName: String,
|
||||||
fileSize: Long,
|
fileSize: Long,
|
||||||
val inputStream: InputStream
|
val inputStream: InputStream
|
||||||
): FileTransfer(fileName, fileSize) {
|
): PendingFile(fileName, fileSize)
|
||||||
var nextChunk: ByteArray? = null
|
|
||||||
val msgQueue = mutableListOf<ByteArray>()
|
|
||||||
}
|
|
@ -1,11 +1,18 @@
|
|||||||
package sushi.hardcore.aira.utils
|
package sushi.hardcore.aira.utils
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import android.provider.OpenableColumns
|
||||||
import android.webkit.MimeTypeMap
|
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.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -24,6 +31,26 @@ object FileUtils {
|
|||||||
) + " " + units[digitGroups]
|
) + " " + 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? {
|
fun openFileForDownload(context: Context, fileName: String): OutputStream? {
|
||||||
val fileExtension = fileName.substringAfterLast(".")
|
val fileExtension = fileName.substringAfterLast(".")
|
||||||
val dateExtension = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date())
|
val dateExtension = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date())
|
||||||
|
@ -10,10 +10,19 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
style="@style/Label"/>
|
style="@style/Label"/>
|
||||||
|
|
||||||
<TextView
|
<ScrollView
|
||||||
android:id="@+id/text_file_info"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
style="@style/Label"/>
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_files_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginVertical="5dp"
|
||||||
|
android:layout_marginHorizontal="15dp"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:textAlignment="center"/>
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
@ -48,7 +48,7 @@
|
|||||||
<string name="mark_read">Mark read</string>
|
<string name="mark_read">Mark read</string>
|
||||||
<string name="ask_file_notification_channel">Download file requests</string>
|
<string name="ask_file_notification_channel">Download file requests</string>
|
||||||
<string name="db_mkdir_failed">Databases directory creation failed</string>
|
<string name="db_mkdir_failed">Databases directory creation failed</string>
|
||||||
<string name="want_to_send_a_file">%s wants to send you a file%s</string>
|
<string name="want_to_send_files">%s wants to send you some files</string>
|
||||||
<string name="download_file_request">Download file request</string>
|
<string name="download_file_request">Download file request</string>
|
||||||
<string name="download">Download</string>
|
<string name="download">Download</string>
|
||||||
<string name="refuse">Refuse</string>
|
<string name="refuse">Refuse</string>
|
||||||
@ -56,9 +56,9 @@
|
|||||||
<string name="transfer_aborted">Transfer aborted</string>
|
<string name="transfer_aborted">Transfer aborted</string>
|
||||||
<string name="transfer_completed">Transfer completed</string>
|
<string name="transfer_completed">Transfer completed</string>
|
||||||
<string name="reply">Reply</string>
|
<string name="reply">Reply</string>
|
||||||
<string name="share_uri_null">Failed to retrieve URI</string>
|
<string name="open_uri_failed">Failed to open URI</string>
|
||||||
<string name="ask_send_file">Send %s (%s) to %s ?</string>
|
<string name="ask_send_single_file">Send %s (%s) to %s ?</string>
|
||||||
<string name="file_open_failed">Failed to open file</string>
|
<string name="ask_send_multiple_files">Send %d files to %s ?</string>
|
||||||
<string name="share_label">Send with AIRA</string>
|
<string name="share_label">Send with AIRA</string>
|
||||||
<string name="identity_fingerprint_label">Your identity\'s fingerprint:</string>
|
<string name="identity_fingerprint_label">Your identity\'s fingerprint:</string>
|
||||||
<string name="fingerprint">Fingerprint:</string>
|
<string name="fingerprint">Fingerprint:</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user