Pending messages
This commit is contained in:
parent
d618eb87c7
commit
5fc3f7d0cf
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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 -> {}
|
||||
|
10
app/src/main/res/drawable/offline_warning_background.xml
Normal file
10
app/src/main/res/drawable/offline_warning_background.xml
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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">
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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>
|
@ -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>
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user