Multiple Files Transfers
This commit is contained in:
parent
458be75114
commit
51ebfb22db
@ -32,6 +32,7 @@
|
||||
<activity android:name=".MainActivity">
|
||||
<intent-filter android:label="@string/share_label">
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="*/*"/>
|
||||
</intent-filter>
|
||||
|
@ -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<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 {
|
||||
for (chatItem in it.asReversed()) {
|
||||
chatAdapter.newLoadedMessage(chatItem)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
||||
val onConnected = {
|
||||
findViewById<ConstraintLayout>(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<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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Uri>(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<Uri>? = null
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SEND -> intent.getParcelableExtra<Uri>(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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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.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<SelectionKey, Int>()
|
||||
private val notificationIdManager = NotificationIdManager()
|
||||
private val sendFileTransfers = mutableMapOf<Int, SendFileTransfer>()
|
||||
val receiveFileTransfers = mutableMapOf<Int, ReceiveFileTransfer>()
|
||||
private val sendFileTransfers = mutableMapOf<Int, FilesSender>()
|
||||
val receiveFileTransfers = mutableMapOf<Int, FilesReceiver>()
|
||||
lateinit var contacts: HashMap<Int, Contact>
|
||||
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<Uri>): List<ByteArray>? {
|
||||
val files = mutableListOf<SendFile>()
|
||||
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<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
|
||||
}
|
||||
|
||||
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() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 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<SendFile>): 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<ReceiveFile>? {
|
||||
val files = mutableListOf<ReceiveFile>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
class SendFileTransfer(
|
||||
class SendFile(
|
||||
fileName: String,
|
||||
fileSize: Long,
|
||||
val inputStream: InputStream
|
||||
): FileTransfer(fileName, fileSize) {
|
||||
var nextChunk: ByteArray? = null
|
||||
val msgQueue = mutableListOf<ByteArray>()
|
||||
}
|
||||
): PendingFile(fileName, fileSize)
|
@ -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())
|
||||
|
@ -10,10 +10,19 @@
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Label"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_file_info"
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Label"/>
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<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>
|
@ -48,7 +48,7 @@
|
||||
<string name="mark_read">Mark read</string>
|
||||
<string name="ask_file_notification_channel">Download file requests</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">Download</string>
|
||||
<string name="refuse">Refuse</string>
|
||||
@ -56,9 +56,9 @@
|
||||
<string name="transfer_aborted">Transfer aborted</string>
|
||||
<string name="transfer_completed">Transfer completed</string>
|
||||
<string name="reply">Reply</string>
|
||||
<string name="share_uri_null">Failed to retrieve URI</string>
|
||||
<string name="ask_send_file">Send %s (%s) to %s ?</string>
|
||||
<string name="file_open_failed">Failed to open file</string>
|
||||
<string name="open_uri_failed">Failed to open URI</string>
|
||||
<string name="ask_send_single_file">Send %s (%s) to %s ?</string>
|
||||
<string name="ask_send_multiple_files">Send %d files to %s ?</string>
|
||||
<string name="share_label">Send with AIRA</string>
|
||||
<string name="identity_fingerprint_label">Your identity\'s fingerprint:</string>
|
||||
<string name="fingerprint">Fingerprint:</string>
|
||||
|
Loading…
Reference in New Issue
Block a user