Pending messages

This commit is contained in:
Matéo Duparc 2021-07-31 22:07:32 +02:00
parent d618eb87c7
commit 5fc3f7d0cf
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
16 changed files with 395 additions and 171 deletions

View File

@ -13,17 +13,15 @@ import android.widget.ImageView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
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.adapters.FuckRecyclerView
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 sushi.hardcore.aira.utils.TimeUtils
class ChatActivity : ServiceBoundActivity() {
private external fun generateFingerprint(publicKey: ByteArray): String
@ -36,7 +34,108 @@ class ChatActivity : ServiceBoundActivity() {
private var lastLoadedMessageOffset = 0
private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
if (isServiceInitialized() && uris.size > 0) {
airaService.sendFilesFromUris(sessionId, uris)
airaService.sendFilesFromUris(sessionId, uris) { buffer ->
chatAdapter.newMessage(ChatItem(true, 0, buffer))
scrollToBottom()
}
}
}
private val uiCallbacks = object : AIRAService.UiCallbacks {
override fun onConnectFailed(ip: String, errorMsg: String?) {}
override fun onNewSession(sessionId: Int, ip: String) {
if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
val contact = airaService.contacts[sessionId]
if (contact == null) {
binding.bottomPanel.visibility = View.VISIBLE
} else {
binding.offlineWarning.visibility = View.GONE
if (airaService.pendingMsgs[sessionId]!!.size > 0) {
binding.sendingPendingMsgsIndicator.visibility = View.VISIBLE
//remove pending messages
reloadHistory(contact)
scrollToBottom()
}
}
invalidateOptionsMenu()
}
}
}
override fun onSessionDisconnect(sessionId: Int) {
if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
if (airaService.isContact(sessionId)) {
binding.offlineWarning.visibility = View.VISIBLE
} else {
hideBottomPanel()
}
}
}
}
override fun onNameTold(sessionId: Int, name: String) {
if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
sessionName = name
binding.toolbar.title.text = name
if (avatar == null) {
binding.toolbar.avatar.setTextAvatar(name)
}
}
}
}
override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
this@ChatActivity.avatar = avatar
if (avatar == null) {
binding.toolbar.avatar.setTextAvatar(sessionName)
} else {
binding.toolbar.avatar.setImageAvatar(avatar)
}
}
}
}
override fun onSent(sessionId: Int, timestamp: Long, buffer: ByteArray) {
if (this@ChatActivity.sessionId == sessionId) {
if (airaService.isContact(sessionId)) {
lastLoadedMessageOffset += 1
}
runOnUiThread {
chatAdapter.newMessage(ChatItem(true, timestamp, buffer))
scrollToBottom()
}
}
}
override fun onPendingMessagesSent(sessionId: Int) {
if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
binding.sendingPendingMsgsIndicator.visibility = View.GONE
}
}
}
override fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean {
return if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
chatAdapter.newMessage(ChatItem(false, timestamp, data))
scrollToBottom()
}
if (airaService.isContact(sessionId)) {
lastLoadedMessageOffset += 1
}
!airaService.isAppInBackground
} else {
false
}
}
override fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean {
return if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
filesReceiver.ask(this@ChatActivity, sessionName ?: airaService.sessions[sessionId]!!.ip)
}
true
} else {
false
}
}
}
@ -52,7 +151,7 @@ class ChatActivity : ServiceBoundActivity() {
chatAdapter = ChatAdapter(this@ChatActivity, ::onClickSaveFile)
binding.recyclerChat.apply {
adapter = chatAdapter
layoutManager = LinearLayoutManager(this@ChatActivity, LinearLayoutManager.VERTICAL, false).apply {
layoutManager = FuckRecyclerView(this@ChatActivity).apply {
stackFromEnd = true
}
addOnScrollListener(object : RecyclerView.OnScrollListener() {
@ -76,13 +175,11 @@ class ChatActivity : ServiceBoundActivity() {
}
binding.buttonSend.setOnClickListener {
val msg = binding.editMessage.text.toString()
airaService.sendTo(sessionId, Protocol.newMessage(msg))
binding.editMessage.text.clear()
chatAdapter.newMessage(ChatItem(true, TimeUtils.getTimestamp(), Protocol.newMessage(msg)))
if (airaService.contacts.contains(sessionId)) {
lastLoadedMessageOffset += 1
if (!airaService.sendOrAddToPending(sessionId, Protocol.newMessage(msg))) {
chatAdapter.newMessage(ChatItem(true, 0, Protocol.newMessage(msg)))
scrollToBottom()
}
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
}
binding.buttonAttach.setOnClickListener {
filePicker.launch("*/*")
@ -95,9 +192,8 @@ class ChatActivity : ServiceBoundActivity() {
val session = airaService.sessions[sessionId]
val contact = airaService.contacts[sessionId]
if (session == null && contact == null) { //may happen when resuming activity after session disconnect
onDisconnected()
hideBottomPanel()
} else {
chatAdapter.clear()
val avatar = if (contact == null) {
displayIconTrustLevel(false, false)
sessionName = airaService.savedNames[sessionId]
@ -117,93 +213,32 @@ class ChatActivity : ServiceBoundActivity() {
binding.toolbar.avatar.setImageAvatar(image)
}
}
if (contact != null) {
loadMsgs(contact.uuid)
}
airaService.savedMsgs[sessionId]?.let {
for (chatItem in it.asReversed()) {
chatAdapter.newLoadedMessage(chatItem)
reloadHistory(contact)
airaService.pendingMsgs[sessionId]?.let {
for (msg in it) {
if (msg[0] == Protocol.MESSAGE ||msg[0] == Protocol.FILE) {
chatAdapter.newMessage(ChatItem(true, 0, msg))
}
}
}
if (chatAdapter.itemCount > 0) {
scrollToBottom()
}
airaService.receiveFileTransfers[sessionId]?.let {
if (it.shouldAsk) {
it.ask(this@ChatActivity, ipName)
}
}
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
if (airaService.isOnline(sessionId)) {
binding.bottomPanel.visibility = View.VISIBLE
binding.recyclerChat.updatePadding(bottom = 0)
} else {
onDisconnected()
if (session == null) {
if (contact == null) {
hideBottomPanel()
} else {
binding.offlineWarning.visibility = View.VISIBLE
}
}
airaService.setSeen(sessionId, true)
}
airaService.uiCallbacks = object : AIRAService.UiCallbacks {
override fun onConnectFailed(ip: String, errorMsg: String?) {}
override fun onNewSession(sessionId: Int, ip: String) {
if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
binding.bottomPanel.visibility = View.VISIBLE
invalidateOptionsMenu()
}
}
}
override fun onSessionDisconnect(sessionId: Int) {
if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
onDisconnected()
}
}
}
override fun onNameTold(sessionId: Int, name: String) {
if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
sessionName = name
binding.toolbar.title.text = name
if (avatar == null) {
binding.toolbar.avatar.setTextAvatar(name)
}
}
}
}
override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
this@ChatActivity.avatar = avatar
if (avatar == null) {
binding.toolbar.avatar.setTextAvatar(sessionName)
} else {
binding.toolbar.avatar.setImageAvatar(avatar)
}
}
}
}
override fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean {
return if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
chatAdapter.newMessage(ChatItem(false, timestamp, data))
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
}
if (airaService.contacts.contains(sessionId)) {
lastLoadedMessageOffset += 1
}
!airaService.isAppInBackground
} else {
false
}
}
override fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean {
return if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
filesReceiver.ask(this@ChatActivity, sessionName ?: airaService.sessions[sessionId]!!.ip)
}
true
} else {
false
}
}
}
airaService.uiCallbacks = uiCallbacks
airaService.isAppInBackground = false
}
override fun onServiceDisconnected(name: ComponentName?) {}
@ -211,7 +246,7 @@ class ChatActivity : ServiceBoundActivity() {
}
}
private fun onDisconnected() {
private fun hideBottomPanel() {
val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.hideSoftInputFromWindow(binding.editMessage.windowToken, 0)
binding.bottomPanel.visibility = View.GONE
@ -254,6 +289,23 @@ class ChatActivity : ServiceBoundActivity() {
}
}
private fun reloadHistory(contact: Contact?) {
chatAdapter.clear()
lastLoadedMessageOffset = 0
if (contact != null) {
loadMsgs(contact.uuid)
}
airaService.savedMsgs[sessionId]?.let {
for (msg in it.asReversed()) {
chatAdapter.newLoadedMessage(ChatItem(msg.outgoing, msg.timestamp, msg.data))
}
}
}
private fun scrollToBottom() {
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount-1)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.chat_activity, menu)
val contact = airaService.contacts[sessionId]
@ -341,7 +393,7 @@ class ChatActivity : ServiceBoundActivity() {
true
}
R.id.refresh_profile -> {
airaService.sendTo(sessionId, Protocol.askProfileInfo())
airaService.sendOrAddToPending(sessionId, Protocol.askProfileInfo())
true
}
else -> super.onOptionsItemSelected(item)
@ -355,22 +407,12 @@ class ChatActivity : ServiceBoundActivity() {
}
}
override fun onPause() {
super.onPause()
lastLoadedMessageOffset = 0
}
private fun onClickSaveFile(fileName: String, rawUuid: ByteArray) {
val buffer = AIRADatabase.loadFile(rawUuid)
if (buffer == null) {
Toast.makeText(this, R.string.loadFile_failed, Toast.LENGTH_SHORT).show()
} else {
val file = FileUtils.openFileForDownload(this, fileName)
file.outputStream?.apply {
write(buffer)
close()
Toast.makeText(this@ChatActivity, getString(R.string.file_saved, file.fileName), Toast.LENGTH_SHORT).show()
}
private fun onClickSaveFile(fileName: String, fileContent: ByteArray) {
val file = FileUtils.openFileForDownload(this, fileName)
file.outputStream?.apply {
write(fileContent)
close()
Toast.makeText(this@ChatActivity, getString(R.string.file_saved, file.fileName), Toast.LENGTH_SHORT).show()
}
}

View File

@ -10,10 +10,12 @@ class ChatItem(val outgoing: Boolean, val timestamp: Long, val data: ByteArray)
const val OUTGOING_FILE = 2
const val INCOMING_FILE = 3
}
val itemType = if (data[0] == Protocol.MESSAGE) {
if (outgoing) OUTGOING_MESSAGE else INCOMING_MESSAGE
} else {
if (outgoing) OUTGOING_FILE else INCOMING_FILE
val itemType: Int by lazy {
if (data[0] == Protocol.MESSAGE) {
if (outgoing) OUTGOING_MESSAGE else INCOMING_MESSAGE
} else {
if (outgoing) OUTGOING_FILE else INCOMING_FILE
}
}
val calendar: Calendar by lazy {

View File

@ -67,19 +67,19 @@ class MainActivity : ServiceBoundActivity() {
onlineSessionAdapter.setName(sessionId, name)
}
}
override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
runOnUiThread {
onlineSessionAdapter.setAvatar(sessionId, avatar)
}
}
override fun onSent(sessionId: Int, timestamp: Long, buffer: ByteArray) {}
override fun onPendingMessagesSent(sessionId: Int) {}
override fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean {
runOnUiThread {
onlineSessionAdapter.setSeen(sessionId, false)
}
return false
}
override fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean {
runOnUiThread {
filesReceiver.ask(this@MainActivity, airaService.getNameOf(sessionId))

View File

@ -3,8 +3,6 @@ package sushi.hardcore.aira.adapters
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.os.Handler
import android.os.Looper
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
@ -16,8 +14,10 @@ import androidx.core.content.ContextCompat
import androidx.core.view.updateMargins
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView
import sushi.hardcore.aira.AIRADatabase
import sushi.hardcore.aira.ChatItem
import sushi.hardcore.aira.R
import sushi.hardcore.aira.background_service.Protocol
import sushi.hardcore.aira.utils.StringUtils
import sushi.hardcore.aira.utils.TimeUtils
import java.text.DateFormat
@ -41,9 +41,7 @@ class ChatAdapter(
fun newMessage(chatItem: ChatItem) {
chatItems.add(chatItem)
Handler(Looper.getMainLooper()).postDelayed({
notifyItemChanged(chatItems.size-2)
}, 100)
notifyItemChanged(chatItems.size-2)
notifyItemInserted(chatItems.size-1)
}
@ -126,26 +124,44 @@ class ChatAdapter(
bubble.background = backgroundDrawable
}
protected fun showDateAndTime(chatItem: ChatItem, previousChatItem: ChatItem?) {
val showDate = if (previousChatItem == null) {
true
} else {
!TimeUtils.isInTheSameDay(chatItem, previousChatItem)
}
var showTextPendingMsg = false
val textPendingMsg = itemView.findViewById<TextView>(R.id.text_pending_msg)
val textDate = itemView.findViewById<TextView>(R.id.text_date)
val textHour = itemView.findViewById<TextView>(R.id.text_hour)
val showDate = if (chatItem.timestamp == 0L) {
if (previousChatItem == null || previousChatItem.timestamp != 0L) {
showTextPendingMsg = true
}
textHour.visibility = View.GONE
false
} else {
textHour.apply {
visibility = View.VISIBLE
@SuppressLint("SetTextI18n")
text = StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.HOUR_OF_DAY))+":"+StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.MINUTE))
setTextColor(ContextCompat.getColor(context, if (chatItem.outgoing) {
R.color.outgoingTimestamp
} else {
R.color.incomingTimestamp
}))
}
if (previousChatItem == null) {
true
} else {
!TimeUtils.isInTheSameDay(chatItem, previousChatItem)
}
}
textDate.visibility = if (showDate) {
textDate.text = DateFormat.getDateInstance().format(chatItem.calendar.time)
View.VISIBLE
} else {
View.GONE
}
val textHour = itemView.findViewById<TextView>(R.id.text_hour)
@SuppressLint("SetTextI18n")
textHour.text = StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.HOUR_OF_DAY))+":"+StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.MINUTE))
textHour.setTextColor(ContextCompat.getColor(context, if (chatItem.outgoing) {
R.color.outgoingTimestamp
textPendingMsg.visibility = if (showTextPendingMsg) {
View.VISIBLE
} else {
R.color.incomingTimestamp
}))
View.GONE
}
}
}
@ -184,19 +200,31 @@ class ChatAdapter(
}
}
internal open class FileViewHolder(context: Context, itemView: View, private val onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): BubbleViewHolder(context, itemView) {
internal open class FileViewHolder(context: Context, itemView: View, private val onSavingFile: (fileName: String, fileContent: ByteArray) -> Unit): BubbleViewHolder(context, itemView) {
protected fun bindFile(chatItem: ChatItem, outgoing: Boolean) {
setBubbleContent(R.layout.file_bubble_content)
val filename = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString()
val buttonSave = itemView.findViewById<ImageButton>(R.id.button_save)
val fileName: String
if (chatItem.timestamp == 0L) { //pending
val file = Protocol.parseSmallFile(chatItem.data)!!
fileName = file.rawFileName.decodeToString()
buttonSave.setOnClickListener {
onSavingFile(fileName, file.fileContent)
}
} else {
fileName = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString()
buttonSave.setOnClickListener {
AIRADatabase.loadFile(chatItem.data.sliceArray(1 until 17))?.let {
onSavingFile(fileName, it)
}
}
}
itemView.findViewById<TextView>(R.id.text_filename).apply {
text = filename
text = fileName
if (!outgoing) {
highlightColor = ContextCompat.getColor(context, R.color.incomingHighlight)
}
}
itemView.findViewById<ImageButton>(R.id.button_save).setOnClickListener {
onSavingFile(filename, chatItem.data.sliceArray(1 until 17))
}
}
}

View File

@ -0,0 +1,18 @@
package sushi.hardcore.aira.adapters
import android.content.Context
import android.util.AttributeSet
import androidx.recyclerview.widget.LinearLayoutManager
//https://stackoverflow.com/questions/36724898/notifyitemchanged-make-the-recyclerview-scroll-and-jump-to-up
class FuckRecyclerView(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0,
): LinearLayoutManager(context, attrs, defStyleAttr, defStyleRes) {
override fun isAutoMeasureEnabled(): Boolean {
return false
}
}

View File

@ -88,6 +88,7 @@ class AIRAService : Service() {
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {}
}
val savedMsgs = mutableMapOf<Int, MutableList<ChatItem>>()
val pendingMsgs = mutableMapOf<Int, MutableList<ByteArray>>()
val savedNames = mutableMapOf<Int, String>()
val savedAvatars = mutableMapOf<Int, String>()
val notSeen = mutableListOf<Int>()
@ -109,6 +110,8 @@ class AIRAService : Service() {
fun onSessionDisconnect(sessionId: Int)
fun onNameTold(sessionId: Int, name: String)
fun onAvatarChanged(sessionId: Int, avatar: ByteArray?)
fun onSent(sessionId: Int, timestamp: Long, buffer: ByteArray)
fun onPendingMessagesSent(sessionId: Int)
fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean
fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean
}
@ -121,14 +124,20 @@ class AIRAService : Service() {
}
}
fun sendTo(sessionId: Int, buffer: ByteArray) {
serviceHandler.obtainMessage().apply {
what = MESSAGE_SEND_TO
data = Bundle().apply {
putInt("sessionId", sessionId)
putByteArray("buff", buffer)
fun sendOrAddToPending(sessionId: Int, buffer: ByteArray): Boolean {
return if (isOnline(sessionId)) {
serviceHandler.obtainMessage().apply {
what = MESSAGE_SEND_TO
data = Bundle().apply {
putInt("sessionId", sessionId)
putByteArray("buff", buffer)
}
serviceHandler.sendMessage(this)
}
serviceHandler.sendMessage(this)
true
} else {
pendingMsgs[sessionId]?.add(buffer)
false
}
}
@ -140,7 +149,7 @@ class AIRAService : Service() {
}
}
fun sendFilesFromUris(sessionId: Int, uris: List<Uri>) {
fun sendFilesFromUris(sessionId: Int, uris: List<Uri>, onPendingSmallFile: ((ByteArray) -> Unit)? = null) {
val files = mutableListOf<SendFile>()
var useLargeFileTransfer = false
for (uri in uris) {
@ -155,26 +164,22 @@ class AIRAService : Service() {
sendLargeFilesTo(sessionId, files)
} else {
for (file in files) {
sendSmallFileTo(sessionId, file)
var buffer = file.inputStream.readBytes()
file.inputStream.close()
buffer = Protocol.newFile(file.fileName, buffer)
if (!sendOrAddToPending(sessionId, buffer)) {
onPendingSmallFile?.let { it(buffer) }
}
}
}
}
private fun sendSmallFileTo(sessionId: Int, sendFile: SendFile) {
val buffer = sendFile.inputStream.readBytes()
sendFile.inputStream.close()
sendTo(sessionId, Protocol.newFile(sendFile.fileName, buffer))
AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer)?.let { rawFileUuid ->
saveMsg(sessionId, TimeUtils.getTimestamp(), byteArrayOf(Protocol.FILE) + rawFileUuid + sendFile.fileName.toByteArray())
}
}
private fun sendLargeFilesTo(sessionId: Int, files: MutableList<SendFile>) {
if (sendFileTransfers[sessionId] == null && receiveFileTransfers[sessionId] == null) {
val filesSender = FilesSender(files, this, notificationManager)
initFileTransferNotification(sessionId, filesSender.fileTransferNotification, filesSender.files[0])
sendFileTransfers[sessionId] = filesSender
sendTo(sessionId, Protocol.askLargeFiles(files))
sendOrAddToPending(sessionId, Protocol.askLargeFiles(files))
} else {
Toast.makeText(this, R.string.file_transfer_already_in_progress, Toast.LENGTH_SHORT).show()
}
@ -194,7 +199,7 @@ class AIRAService : Service() {
return contacts[sessionId]?.name ?: savedNames[sessionId] ?: sessions[sessionId]!!.ip
}
private fun isContact(sessionId: Int): Boolean {
fun isContact(sessionId: Int): Boolean {
return contacts.contains(sessionId)
}
@ -208,6 +213,7 @@ class AIRAService : Service() {
}
}
savedNames.remove(sessionId)
pendingMsgs[sessionId] = mutableListOf()
savedAvatars.remove(sessionId)
return true
}
@ -245,6 +251,7 @@ class AIRAService : Service() {
contacts.remove(sessionId)?.let {
return if (AIRADatabase.removeContact(it.uuid)) {
savedMsgs[sessionId] = mutableListOf()
pendingMsgs.remove(sessionId)
savedNames[sessionId] = it.name
it.avatar?.let { avatarUuid ->
savedAvatars[sessionId] = avatarUuid
@ -328,7 +335,12 @@ class AIRAService : Service() {
val key = session.register(selector, SelectionKey.OP_READ)
sessionIdByKey[key] = sessionId
uiCallbacks?.onNewSession(sessionId, session.ip)
if (!isContact(sessionId)) {
if (isContact(sessionId)) {
for (i in 0 until pendingMsgs[sessionId]!!.size) {
sendAndSave(sessionId, pendingMsgs[sessionId]!!.removeAt(0))
}
uiCallbacks?.onPendingMessagesSent(sessionId)
} else {
session.encryptAndSend(Protocol.askProfileInfo(), usePadding)
}
} else {
@ -429,10 +441,20 @@ class AIRAService : Service() {
}
}
private fun sendAndSave(sessionId: Int, msg: ByteArray) {
sessions[sessionId]?.encryptAndSend(msg, usePadding)
if (msg[0] == Protocol.MESSAGE) {
saveMsg(sessionId, TimeUtils.getTimestamp(), msg)
private fun sendAndSave(sessionId: Int, buffer: ByteArray) {
sessions[sessionId]?.encryptAndSend(buffer, usePadding)
val timestamp = TimeUtils.getTimestamp()
if (buffer[0] == Protocol.MESSAGE) {
uiCallbacks?.onSent(sessionId, timestamp, buffer)
saveMsg(sessionId, timestamp, buffer)
} else if (buffer[0] == Protocol.FILE) {
Protocol.parseSmallFile(buffer)?.let { file ->
AIRADatabase.storeFile(contacts[sessionId]?.uuid, file.fileContent)?.let { rawFileUuid ->
val msg = byteArrayOf(Protocol.FILE) + rawFileUuid + file.rawFileName
uiCallbacks?.onSent(sessionId, timestamp, msg)
saveMsg(sessionId, timestamp, msg)
}
}
}
}
@ -533,6 +555,7 @@ class AIRAService : Service() {
contacts = HashMap(contactList.size)
for (contact in contactList) {
contacts[sessionCounter] = contact
pendingMsgs[sessionCounter] = mutableListOf()
if (!contact.seen) {
notSeen.add(sessionCounter)
}
@ -720,10 +743,10 @@ class AIRAService : Service() {
filesReceiver.fileTransferNotification,
filesReceiver.files[0],
)
sendTo(sessionId, Protocol.acceptLargeFiles())
session.encryptAndSend(Protocol.acceptLargeFiles(), usePadding)
}, { filesReceiver ->
receiveFileTransfers.remove(sessionId)
sendTo(sessionId, Protocol.abortFilesTransfer())
session.encryptAndSend(Protocol.abortFilesTransfer(), usePadding)
filesReceiver.fileTransferNotification.cancel()
},
this,

View File

@ -22,7 +22,7 @@ class NotificationBroadcastReceiver: BroadcastReceiver() {
ACTION_MARK_READ -> airaService.setSeen(sessionId, true)
ACTION_CANCEL_FILE_TRANSFER -> airaService.cancelFileTransfer(sessionId)
ACTION_REPLY -> RemoteInput.getResultsFromIntent(intent)?.getString(KEY_TEXT_REPLY)?.let { reply ->
airaService.sendTo(sessionId, Protocol.newMessage(reply))
airaService.sendOrAddToPending(sessionId, Protocol.newMessage(reply))
airaService.setSeen(sessionId, true)
}
else -> {}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:bottom="-2dp" android:left="-2dp" android:right="-2dp">
<shape android:shape="rectangle">
<stroke android:width="1dp" android:color="@color/offline_warning"/>
<solid android:color="@color/transparent"/>
<padding android:top="15dp"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:bottom="-2dp" android:left="-2dp" android:right="-2dp">
<shape android:shape="rectangle">
<stroke android:width="1dp" android:color="@color/pending_msg_indicator"/>
<solid android:color="@color/transparent"/>
<padding android:top="5dp"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:bottom="-2dp" android:left="-2dp" android:right="-2dp">
<shape android:shape="rectangle">
<stroke android:width="1dp" android:color="@color/secondary"/>
<solid android:color="@color/transparent"/>
<padding android:top="15dp"/>
</shape>
</item>
</layer-list>

View File

@ -13,14 +13,79 @@
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toTopOf="@id/bottom_panel"/>
app:layout_constraintBottom_toTopOf="@id/offline_warning"/>
<LinearLayout
android:id="@+id/sending_pending_msgs_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/recycler_chat"
app:layout_constraintBottom_toTopOf="@id/bottom_panel"
android:visibility="gone"
android:gravity="center"
android:paddingVertical="10dp"
android:background="@drawable/sending_pending_msg_indictor_background">
<ProgressBar
android:layout_width="50dp"
android:layout_height="50dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sending_pending_messages"
android:layout_gravity="center"
android:layout_marginStart="20dp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/offline_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/recycler_chat"
app:layout_constraintBottom_toTopOf="@id/bottom_panel"
android:visibility="gone"
android:paddingHorizontal="30dp"
android:layout_marginBottom="15dp"
android:background="@drawable/offline_warning_background">
<ImageView
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/ic_warning"
app:tint="@color/offline_warning"
android:contentDescription="@string/warning_desc"
android:layout_gravity="center_vertical"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:text="@string/offline_warning_title"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/offline_warning_msg"
android:textColor="#80ffffff"/>
</LinearLayout>
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottom_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
android:visibility="gone"
android:paddingVertical="5dp"
android:background="@color/primary">

View File

@ -9,9 +9,19 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:visibility="gone"
android:paddingVertical="5dp"/>
<TextView
android:id="@+id/text_pending_msg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginVertical="10dp"
android:textAlignment="center"
android:text="@string/pending_messages"
android:textColor="@color/pending_msg_indicator"
android:background="@drawable/pending_msg_indicator_background"/>
<LinearLayout
android:id="@+id/bubble"
android:layout_width="wrap_content"

View File

@ -15,4 +15,6 @@
<color name="outgoingTimestamp">#777777</color>
<color name="incomingTimestamp">#d7d7d7</color>
<color name="incomingHighlight">@color/outgoingTextLink</color>
<color name="offline_warning">#ff0000</color>
<color name="pending_msg_indicator">#909090</color>
</resources>

View File

@ -76,7 +76,6 @@
<string name="file_transfer_already_in_progress">Another file transfer is already in progress</string>
<string name="settings">Settings</string>
<string name="log_out">Log out</string>
<string name="loadFile_failed">File extraction failed</string>
<string name="copied">Copied to clipboard !</string>
<string name="identity">Identity</string>
<string name="about">About</string>
@ -109,4 +108,9 @@
<string name="clickable_indicator">Clickable indicator</string>
<string name="avatar">Avatar</string>
<string name="name">Name</string>
<string name="offline_warning_title">Your contact seems to be offline.</string>
<string name="offline_warning_msg">Sent messages will be stored until a connection is established.</string>
<string name="warning_desc">Warning icon</string>
<string name="pending_messages">Pending messages:</string>
<string name="sending_pending_messages">Sending pending messages…</string>
</resources>

View File

@ -1,11 +1,11 @@
buildscript {
ext.kotlin_version = "1.5.10"
ext.kotlin_version = "1.5.21"
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.2'
classpath 'com.android.tools.build:gradle:7.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip