Profile toolbar & 4 ChatItem types & Other UI improvements
This commit is contained in:
parent
e77887a51c
commit
2f01c86359
@ -42,9 +42,9 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.5.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||||
implementation "androidx.fragment:fragment-ktx:1.3.3"
|
implementation "androidx.fragment:fragment-ktx:1.3.4"
|
||||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||||
implementation 'com.google.android.material:material:1.3.0'
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
//implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
//implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
|
@ -28,8 +28,8 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<activity android:name=".ChatActivity"/>
|
<activity android:name=".ChatActivity" android:theme="@style/Theme.AIRA.NoActionBar"/>
|
||||||
<activity android:name=".MainActivity">
|
<activity android:name=".MainActivity" android:theme="@style/Theme.AIRA.NoActionBar">
|
||||||
<intent-filter android:label="@string/share_label">
|
<intent-filter android:label="@string/share_label">
|
||||||
<action android:name="android.intent.action.SEND"/>
|
<action android:name="android.intent.action.SEND"/>
|
||||||
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
package sushi.hardcore.aira
|
package sushi.hardcore.aira
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@ -32,14 +33,7 @@ class ChatActivity : ServiceBoundActivity() {
|
|||||||
private var lastLoadedMessageOffset = 0
|
private var lastLoadedMessageOffset = 0
|
||||||
private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
|
private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
|
||||||
if (isServiceInitialized() && uris.size > 0) {
|
if (isServiceInitialized() && uris.size > 0) {
|
||||||
airaService.sendFilesFromUris(sessionId, uris)?.let { msgs ->
|
airaService.sendFilesFromUris(sessionId, uris)
|
||||||
for (msg in msgs) {
|
|
||||||
chatAdapter.newMessage(ChatItem(true, msg))
|
|
||||||
if (airaService.contacts.contains(sessionId)) {
|
|
||||||
lastLoadedMessageOffset += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,13 +41,15 @@ class ChatActivity : ServiceBoundActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityChatBinding.inflate(layoutInflater)
|
binding = ActivityChatBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
|
|
||||||
sessionId = intent.getIntExtra("sessionId", -1)
|
sessionId = intent.getIntExtra("sessionId", -1)
|
||||||
if (sessionId != -1) {
|
if (sessionId != -1) {
|
||||||
intent.getStringExtra("sessionName")?.let { name ->
|
intent.getStringExtra("sessionName")?.let { name ->
|
||||||
sessionName = name
|
sessionName = name
|
||||||
title = name
|
setSupportActionBar(binding.toolbar.toolbar)
|
||||||
|
binding.toolbar.textAvatar.setLetterFrom(name)
|
||||||
|
binding.toolbar.title.text = name
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
chatAdapter = ChatAdapter(this@ChatActivity, ::onClickSaveFile)
|
chatAdapter = ChatAdapter(this@ChatActivity, ::onClickSaveFile)
|
||||||
binding.recyclerChat.apply {
|
binding.recyclerChat.apply {
|
||||||
@ -112,7 +108,7 @@ class ChatActivity : ServiceBoundActivity() {
|
|||||||
}
|
}
|
||||||
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
||||||
val showBottomPanel = {
|
val showBottomPanel = {
|
||||||
findViewById<ConstraintLayout>(R.id.bottom_panel).visibility = View.VISIBLE
|
binding.bottomPanel.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
airaService.uiCallbacks = object : AIRAService.UiCallbacks {
|
airaService.uiCallbacks = object : AIRAService.UiCallbacks {
|
||||||
override fun onNewSession(sessionId: Int, ip: String) {
|
override fun onNewSession(sessionId: Int, ip: String) {
|
||||||
@ -125,7 +121,9 @@ class ChatActivity : ServiceBoundActivity() {
|
|||||||
override fun onSessionDisconnect(sessionId: Int) {
|
override fun onSessionDisconnect(sessionId: Int) {
|
||||||
if (this@ChatActivity.sessionId == sessionId) {
|
if (this@ChatActivity.sessionId == sessionId) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
findViewById<ConstraintLayout>(R.id.bottom_panel).visibility = View.GONE
|
val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
inputManager.hideSoftInputFromWindow(binding.editMessage.windowToken, 0)
|
||||||
|
binding.bottomPanel.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,14 @@ import sushi.hardcore.aira.background_service.Protocol
|
|||||||
|
|
||||||
class ChatItem(val outgoing: Boolean, val data: ByteArray) {
|
class ChatItem(val outgoing: Boolean, val data: ByteArray) {
|
||||||
companion object {
|
companion object {
|
||||||
const val MESSAGE = 0
|
const val OUTGOING_MESSAGE = 0
|
||||||
const val FILE = 1
|
const val INCOMING_MESSAGE = 1
|
||||||
|
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 = if (data[0] == Protocol.MESSAGE) { MESSAGE } else { FILE }
|
|
||||||
}
|
}
|
@ -79,7 +79,11 @@ class MainActivity : ServiceBoundActivity() {
|
|||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
val identityName = intent.getStringExtra("identityName")
|
val identityName = intent.getStringExtra("identityName")
|
||||||
identityName?.let { title = it }
|
identityName?.let {
|
||||||
|
setSupportActionBar(binding.toolbar.toolbar)
|
||||||
|
binding.toolbar.textAvatar.setLetterFrom(it)
|
||||||
|
binding.toolbar.title.text = it
|
||||||
|
}
|
||||||
|
|
||||||
val openedToShareFile = intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE
|
val openedToShareFile = intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ import android.widget.ImageButton
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.marginEnd
|
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import sushi.hardcore.aira.ChatItem
|
import sushi.hardcore.aira.ChatItem
|
||||||
@ -23,7 +22,8 @@ class ChatAdapter(
|
|||||||
): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val BUBBLE_MARGIN = 70
|
const val CONTAINER_MARGIN = 70
|
||||||
|
const val BUBBLE_HORIZONTAL_PADDING = 40
|
||||||
}
|
}
|
||||||
|
|
||||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||||
@ -44,88 +44,106 @@ class ChatAdapter(
|
|||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal open class BubbleViewHolder(private val context: Context, itemView: View): RecyclerView.ViewHolder(itemView) {
|
internal open class BubbleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
|
||||||
fun handleItemView(position: Int) {
|
protected fun setPadding(outgoing: Boolean) {
|
||||||
itemView.updatePadding(top = if (position == 0) {
|
|
||||||
50
|
|
||||||
} else {
|
|
||||||
itemView.paddingBottom
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fun setBubbleColor(bubble: View, outgoing: Boolean) {
|
|
||||||
if (outgoing) {
|
if (outgoing) {
|
||||||
bubble.background.clearColorFilter()
|
itemView.updatePadding(right = BUBBLE_HORIZONTAL_PADDING)
|
||||||
} else {
|
} else {
|
||||||
bubble.background.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.incomingBubbleBackground), PorterDuff.Mode.SRC)
|
itemView.updatePadding(left = BUBBLE_HORIZONTAL_PADDING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected fun configureBubble(context: Context, view: View, outgoing: Boolean) {
|
||||||
|
view.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||||
|
gravity = if (outgoing) {
|
||||||
|
marginStart = CONTAINER_MARGIN
|
||||||
|
Gravity.END
|
||||||
|
} else {
|
||||||
|
marginEnd = CONTAINER_MARGIN
|
||||||
|
Gravity.START
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!outgoing) {
|
||||||
|
view.background.colorFilter = PorterDuffColorFilter(
|
||||||
|
ContextCompat.getColor(context, R.color.incomingBubbleBackground),
|
||||||
|
PorterDuff.Mode.SRC
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class MessageViewHolder(context: Context, itemView: View): BubbleViewHolder(context, itemView) {
|
internal open class MessageViewHolder(itemView: View): BubbleViewHolder(itemView) {
|
||||||
fun bind(chatItem: ChatItem, position: Int) {
|
protected fun bindMessage(chatItem: ChatItem): TextView {
|
||||||
itemView.findViewById<TextView>(R.id.text_message).apply {
|
itemView.findViewById<TextView>(R.id.text_message).apply {
|
||||||
text = chatItem.data.sliceArray(1 until chatItem.data.size).decodeToString()
|
text = chatItem.data.sliceArray(1 until chatItem.data.size).decodeToString()
|
||||||
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
return this
|
||||||
if (chatItem.outgoing) {
|
|
||||||
gravity = Gravity.END
|
|
||||||
marginStart = BUBBLE_MARGIN
|
|
||||||
} else {
|
|
||||||
gravity = Gravity.START
|
|
||||||
marginEnd = BUBBLE_MARGIN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setBubbleColor(this, chatItem.outgoing)
|
|
||||||
}
|
}
|
||||||
handleItemView(position)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class FileViewHolder(context: Context, itemView: View): BubbleViewHolder(context, itemView) {
|
internal class OutgoingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) {
|
||||||
fun bind(chatItem: ChatItem, position: Int, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit) {
|
fun bind(chatItem: ChatItem) {
|
||||||
|
configureBubble(context, bindMessage(chatItem), true)
|
||||||
|
setPadding(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class IncomingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) {
|
||||||
|
fun bind(chatItem: ChatItem) {
|
||||||
|
configureBubble(context, bindMessage(chatItem), false)
|
||||||
|
setPadding(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal open class FileViewHolder(itemView: View, private val onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): BubbleViewHolder(itemView) {
|
||||||
|
protected fun bindFile(chatItem: ChatItem): LinearLayout {
|
||||||
val filename = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString()
|
val filename = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString()
|
||||||
itemView.findViewById<TextView>(R.id.text_filename).text = filename
|
itemView.findViewById<TextView>(R.id.text_filename).text = filename
|
||||||
itemView.findViewById<ImageButton>(R.id.button_save).setOnClickListener {
|
itemView.findViewById<ImageButton>(R.id.button_save).setOnClickListener {
|
||||||
onSavingFile(filename, chatItem.data.sliceArray(1 until 17))
|
onSavingFile(filename, chatItem.data.sliceArray(1 until 17))
|
||||||
}
|
}
|
||||||
itemView.findViewById<LinearLayout>(R.id.bubble_content).apply {
|
return itemView.findViewById(R.id.bubble_content)
|
||||||
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
}
|
||||||
if (chatItem.outgoing) {
|
}
|
||||||
gravity = Gravity.END
|
|
||||||
marginStart = BUBBLE_MARGIN
|
internal class OutgoingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) {
|
||||||
} else {
|
fun bind(chatItem: ChatItem) {
|
||||||
gravity = Gravity.START
|
configureBubble(context, bindFile(chatItem), true)
|
||||||
marginEnd = BUBBLE_MARGIN
|
setPadding(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setBubbleColor(this, chatItem.outgoing)
|
|
||||||
}
|
internal class IncomingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) {
|
||||||
handleItemView(position)
|
fun bind(chatItem: ChatItem) {
|
||||||
|
configureBubble(context, bindFile(chatItem), false)
|
||||||
|
setPadding(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return when (viewType) {
|
return if (viewType == ChatItem.OUTGOING_MESSAGE || viewType == ChatItem.INCOMING_MESSAGE) {
|
||||||
ChatItem.MESSAGE -> {
|
val view = inflater.inflate(R.layout.adapter_chat_message, parent, false)
|
||||||
val view = inflater.inflate(R.layout.adapter_chat_message, parent, false)
|
if (viewType == ChatItem.OUTGOING_MESSAGE) {
|
||||||
MessageViewHolder(context, view)
|
OutgoingMessageViewHolder(context, view)
|
||||||
|
} else {
|
||||||
|
IncomingMessageViewHolder(context, view)
|
||||||
}
|
}
|
||||||
ChatItem.FILE -> {
|
} else {
|
||||||
val view = inflater.inflate(R.layout.adapter_chat_file, parent, false)
|
val view = inflater.inflate(R.layout.adapter_chat_file, parent, false)
|
||||||
FileViewHolder(context, view)
|
if (viewType == ChatItem.OUTGOING_FILE) {
|
||||||
|
OutgoingFileViewHolder(context, view, onSavingFile)
|
||||||
|
} else {
|
||||||
|
IncomingFileViewHolder(context, view, onSavingFile)
|
||||||
}
|
}
|
||||||
else -> throw RuntimeException("Invalid chat item type")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
val chatItem = chatItems[position]
|
val chatItem = chatItems[position]
|
||||||
when (chatItem.itemType) {
|
when (chatItem.itemType) {
|
||||||
ChatItem.MESSAGE -> {
|
ChatItem.OUTGOING_MESSAGE -> (holder as OutgoingMessageViewHolder).bind(chatItem)
|
||||||
(holder as MessageViewHolder).bind(chatItem, position)
|
ChatItem.INCOMING_MESSAGE -> (holder as IncomingMessageViewHolder).bind(chatItem)
|
||||||
}
|
ChatItem.OUTGOING_FILE -> (holder as OutgoingFileViewHolder).bind(chatItem)
|
||||||
ChatItem.FILE -> {
|
ChatItem.INCOMING_FILE -> (holder as IncomingFileViewHolder).bind(chatItem)
|
||||||
(holder as FileViewHolder).bind(chatItem, position, onSavingFile)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ class AIRAService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendFilesFromUris(sessionId: Int, uris: List<Uri>): List<ByteArray>? {
|
fun sendFilesFromUris(sessionId: Int, uris: List<Uri>) {
|
||||||
val files = mutableListOf<SendFile>()
|
val files = mutableListOf<SendFile>()
|
||||||
var useLargeFileTransfer = false
|
var useLargeFileTransfer = false
|
||||||
for (uri in uris) {
|
for (uri in uris) {
|
||||||
@ -146,30 +146,22 @@ class AIRAService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if (useLargeFileTransfer) {
|
if (useLargeFileTransfer) {
|
||||||
sendLargeFilesTo(sessionId, files)
|
sendLargeFilesTo(sessionId, files)
|
||||||
null
|
|
||||||
} else {
|
} else {
|
||||||
val msgs = mutableListOf<ByteArray>()
|
|
||||||
for (file in files) {
|
for (file in files) {
|
||||||
sendSmallFileTo(sessionId, file)?.let { msg ->
|
sendSmallFileTo(sessionId, file)
|
||||||
msgs.add(msg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
msgs
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendSmallFileTo(sessionId: Int, sendFile: SendFile): ByteArray? {
|
private fun sendSmallFileTo(sessionId: Int, sendFile: SendFile) {
|
||||||
val buffer = sendFile.inputStream.readBytes()
|
val buffer = sendFile.inputStream.readBytes()
|
||||||
sendFile.inputStream.close()
|
sendFile.inputStream.close()
|
||||||
sendTo(sessionId, Protocol.newFile(sendFile.fileName, buffer))
|
sendTo(sessionId, Protocol.newFile(sendFile.fileName, buffer))
|
||||||
AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer)?.let { rawFileUuid ->
|
AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer)?.let { rawFileUuid ->
|
||||||
val msg = byteArrayOf(Protocol.FILE) + rawFileUuid + sendFile.fileName.toByteArray()
|
saveMsg(sessionId, byteArrayOf(Protocol.FILE) + rawFileUuid + sendFile.fileName.toByteArray())
|
||||||
saveMsg(sessionId, msg)
|
|
||||||
return msg
|
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendLargeFilesTo(sessionId: Int, files: MutableList<SendFile>) {
|
private fun sendLargeFilesTo(sessionId: Int, files: MutableList<SendFile>) {
|
||||||
|
@ -11,10 +11,10 @@ jni = { version = "0.19", default-features = false }
|
|||||||
crate-type = ["dylib"]
|
crate-type = ["dylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand-8 = {package = "rand", version = "0.8.3"}
|
rand = "0.8.3"
|
||||||
rand-7 = {package = "rand", version = "0.7.3"}
|
rand-7 = {package = "rand", version = "0.7.3"}
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
rusqlite = {version = "0.25.1", features = ["bundled"]}
|
rusqlite = { version = "0.25.1", features = ["bundled"] }
|
||||||
ed25519-dalek = "1" #for singing
|
ed25519-dalek = "1" #for singing
|
||||||
x25519-dalek = "1.1" #for shared secret
|
x25519-dalek = "1.1" #for shared secret
|
||||||
sha2 = "0.9.3"
|
sha2 = "0.9.3"
|
||||||
@ -24,7 +24,7 @@ aes-gcm-siv = "0.10.0" #Database
|
|||||||
hmac = "0.11.0"
|
hmac = "0.11.0"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
strum_macros = "0.20.1" #display enums
|
strum_macros = "0.20.1" #display enums
|
||||||
uuid = {version = "0.8", features = ["v4"]}
|
uuid = { version = "0.8", features = ["v4"] }
|
||||||
scrypt = "0.7.0"
|
scrypt = "0.7.0"
|
||||||
zeroize = "1.2.0"
|
zeroize = "1.2.0"
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
use std::convert::TryInto;
|
use std::{convert::TryInto, fmt::Display};
|
||||||
use hkdf::Hkdf;
|
use hkdf::Hkdf;
|
||||||
use sha2::Sha384;
|
use sha2::Sha384;
|
||||||
use hmac::{Hmac, Mac, NewMac};
|
use hmac::{Hmac, NewMac, Mac};
|
||||||
use scrypt::{scrypt, Params};
|
use scrypt::{scrypt, Params};
|
||||||
use rand_8::{RngCore, rngs::OsRng};
|
use rand::{RngCore, rngs::OsRng};
|
||||||
use aes_gcm::{aead::Aead, NewAead, Nonce};
|
use aes_gcm::{aead::Aead, NewAead, Nonce};
|
||||||
use aes_gcm_siv::Aes256GcmSiv;
|
use aes_gcm_siv::Aes256GcmSiv;
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
use strum_macros::Display;
|
|
||||||
use crate::utils::*;
|
|
||||||
|
|
||||||
pub const HASH_OUTPUT_LEN: usize = 48; //SHA384
|
pub const HASH_OUTPUT_LEN: usize = 48; //SHA384
|
||||||
const KEY_LEN: usize = 16;
|
const KEY_LEN: usize = 16;
|
||||||
@ -33,6 +31,26 @@ fn hkdf_expand_label(key: &[u8], label: &str, context: Option<&[u8]>, okm: &mut
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_labels(handshake: bool, i_am_bob: bool) -> (String, String) {
|
||||||
|
let mut label = if handshake {
|
||||||
|
"handshake"
|
||||||
|
} else {
|
||||||
|
"application"
|
||||||
|
}.to_owned();
|
||||||
|
label += "_i_am_";
|
||||||
|
let local_label = label.clone() + if i_am_bob {
|
||||||
|
"bob"
|
||||||
|
} else {
|
||||||
|
"alice"
|
||||||
|
};
|
||||||
|
let peer_label = label + if i_am_bob {
|
||||||
|
"alice"
|
||||||
|
} else {
|
||||||
|
"bob"
|
||||||
|
};
|
||||||
|
(local_label, peer_label)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct HandshakeKeys {
|
pub struct HandshakeKeys {
|
||||||
pub local_key: [u8; KEY_LEN],
|
pub local_key: [u8; KEY_LEN],
|
||||||
pub local_iv: [u8; IV_LEN],
|
pub local_iv: [u8; IV_LEN],
|
||||||
@ -47,11 +65,11 @@ impl HandshakeKeys {
|
|||||||
pub fn derive_keys(shared_secret: [u8; 32], handshake_hash: [u8; HASH_OUTPUT_LEN], i_am_bob: bool) -> HandshakeKeys {
|
pub fn derive_keys(shared_secret: [u8; 32], handshake_hash: [u8; HASH_OUTPUT_LEN], i_am_bob: bool) -> HandshakeKeys {
|
||||||
let (handshake_secret, _) = Hkdf::<Sha384>::extract(None, &shared_secret);
|
let (handshake_secret, _) = Hkdf::<Sha384>::extract(None, &shared_secret);
|
||||||
|
|
||||||
let local_label = "handshake".to_owned() + if i_am_bob {"i_am_bob"} else {"i_am_alice"};
|
let (local_label, peer_label) = get_labels(true, i_am_bob);
|
||||||
|
|
||||||
let mut local_handshake_traffic_secret = [0; HASH_OUTPUT_LEN];
|
let mut local_handshake_traffic_secret = [0; HASH_OUTPUT_LEN];
|
||||||
hkdf_expand_label(handshake_secret.as_slice(), &local_label, Some(&handshake_hash), &mut local_handshake_traffic_secret);
|
hkdf_expand_label(handshake_secret.as_slice(), &local_label, Some(&handshake_hash), &mut local_handshake_traffic_secret);
|
||||||
|
|
||||||
let peer_label = "handshake".to_owned() + if i_am_bob {"i_am_alice"} else {"i_am_bob"};
|
|
||||||
let mut peer_handshake_traffic_secret = [0; HASH_OUTPUT_LEN];
|
let mut peer_handshake_traffic_secret = [0; HASH_OUTPUT_LEN];
|
||||||
hkdf_expand_label(handshake_secret.as_slice(), &peer_label, Some(&handshake_hash), &mut peer_handshake_traffic_secret);
|
hkdf_expand_label(handshake_secret.as_slice(), &peer_label, Some(&handshake_hash), &mut peer_handshake_traffic_secret);
|
||||||
|
|
||||||
@ -72,7 +90,7 @@ impl HandshakeKeys {
|
|||||||
peer_key: peer_handshake_key,
|
peer_key: peer_handshake_key,
|
||||||
peer_iv: peer_handshake_iv,
|
peer_iv: peer_handshake_iv,
|
||||||
peer_handshake_traffic_secret: peer_handshake_traffic_secret,
|
peer_handshake_traffic_secret: peer_handshake_traffic_secret,
|
||||||
handshake_secret: to_array_48(handshake_secret.as_slice())
|
handshake_secret: handshake_secret.as_slice().try_into().unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,11 +108,11 @@ impl ApplicationKeys {
|
|||||||
hkdf_expand_label(&handshake_secret, "derived", None, &mut derived_secret);
|
hkdf_expand_label(&handshake_secret, "derived", None, &mut derived_secret);
|
||||||
let (master_secret, _) = Hkdf::<Sha384>::extract(Some(&derived_secret), b"");
|
let (master_secret, _) = Hkdf::<Sha384>::extract(Some(&derived_secret), b"");
|
||||||
|
|
||||||
let local_label = "application".to_owned() + if i_am_bob {"i_am_bob"} else {"i_am_alice"};
|
let (local_label, peer_label) = get_labels(false, i_am_bob);
|
||||||
|
|
||||||
let mut local_application_traffic_secret = [0; HASH_OUTPUT_LEN];
|
let mut local_application_traffic_secret = [0; HASH_OUTPUT_LEN];
|
||||||
hkdf_expand_label(&master_secret, &local_label, Some(&handshake_hash), &mut local_application_traffic_secret);
|
hkdf_expand_label(&master_secret, &local_label, Some(&handshake_hash), &mut local_application_traffic_secret);
|
||||||
|
|
||||||
let peer_label = "application".to_owned() + if i_am_bob {"i_am_alice"} else {"i_am_bob"};
|
|
||||||
let mut peer_application_traffic_secret = [0; HASH_OUTPUT_LEN];
|
let mut peer_application_traffic_secret = [0; HASH_OUTPUT_LEN];
|
||||||
hkdf_expand_label(&master_secret, &peer_label, Some(&handshake_hash), &mut peer_application_traffic_secret);
|
hkdf_expand_label(&master_secret, &peer_label, Some(&handshake_hash), &mut peer_application_traffic_secret);
|
||||||
|
|
||||||
@ -161,12 +179,21 @@ pub fn encrypt_data(data: &[u8], master_key: &[u8]) -> Result<Vec<u8>, CryptoErr
|
|||||||
Ok(cipher_text)
|
Ok(cipher_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Display, Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum CryptoError {
|
pub enum CryptoError {
|
||||||
DecryptionFailed,
|
DecryptionFailed,
|
||||||
InvalidLength
|
InvalidLength
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for CryptoError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
CryptoError::DecryptionFailed => "Decryption failed",
|
||||||
|
CryptoError::InvalidLength => "Invalid length",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn decrypt_data(data: &[u8], master_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
pub fn decrypt_data(data: &[u8], master_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
if data.len() <= IV_LEN || master_key.len() != MASTER_KEY_LEN {
|
if data.len() <= IV_LEN || master_key.len() != MASTER_KEY_LEN {
|
||||||
return Err(CryptoError::InvalidLength);
|
return Err(CryptoError::InvalidLength);
|
||||||
|
@ -256,56 +256,57 @@ impl Identity {
|
|||||||
match db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
|
match db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
|
||||||
Ok(mut stmt) => {
|
Ok(mut stmt) => {
|
||||||
let mut rows = stmt.query([]).unwrap();
|
let mut rows = stmt.query([]).unwrap();
|
||||||
let row = rows.next().unwrap();
|
match rows.next() {
|
||||||
if row.is_some() {
|
Ok(row) => if row.is_some() {
|
||||||
let total: usize = row.unwrap().get(0).unwrap();
|
let total: usize = row.unwrap().get(0).unwrap();
|
||||||
if offset >= total {
|
if offset >= total {
|
||||||
print_error!("Offset larger than total numbers of rows");
|
None
|
||||||
None
|
} else {
|
||||||
} else {
|
if offset+count >= total {
|
||||||
if offset+count >= total {
|
count = total-offset;
|
||||||
count = total-offset;
|
}
|
||||||
}
|
match db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)) {
|
||||||
match db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)) {
|
Ok(mut stmt) => {
|
||||||
Ok(mut stmt) => {
|
let mut rows = stmt.query([]).unwrap();
|
||||||
let mut rows = stmt.query([]).unwrap();
|
let mut msgs = Vec::new();
|
||||||
let mut msgs = Vec::new();
|
while let Some(row) = rows.next().unwrap() {
|
||||||
while let Some(row) = rows.next().unwrap() {
|
let encrypted_outgoing: Vec<u8> = row.get(0).unwrap();
|
||||||
let encrypted_outgoing: Vec<u8> = row.get(0).unwrap();
|
match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
|
||||||
match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
|
Ok(outgoing) => {
|
||||||
Ok(outgoing) => {
|
match byte_to_bool(outgoing[0]) {
|
||||||
match byte_to_bool(outgoing[0]) {
|
Ok(outgoing) => {
|
||||||
Ok(outgoing) => {
|
let encrypted_data: Vec<u8> = row.get(1).unwrap();
|
||||||
let encrypted_data: Vec<u8> = row.get(1).unwrap();
|
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
|
||||||
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
|
Ok(data) => {
|
||||||
Ok(data) => {
|
msgs.push(
|
||||||
msgs.push(
|
(
|
||||||
(
|
outgoing,
|
||||||
outgoing,
|
data
|
||||||
data
|
)
|
||||||
)
|
)
|
||||||
)
|
},
|
||||||
},
|
Err(e) => print_error!(e)
|
||||||
Err(e) => print_error!(e)
|
}
|
||||||
}
|
}
|
||||||
|
Err(_) => {}
|
||||||
}
|
}
|
||||||
Err(_) => {}
|
|
||||||
}
|
}
|
||||||
|
Err(e) => print_error!(e)
|
||||||
}
|
}
|
||||||
Err(e) => print_error!(e)
|
|
||||||
}
|
}
|
||||||
|
Some(msgs)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
print_error!(e);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
Some(msgs)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
print_error!(e);
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
} else {
|
Err(_) => None
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -2,10 +2,6 @@ use std::convert::TryInto;
|
|||||||
use uuid::Bytes;
|
use uuid::Bytes;
|
||||||
use crate::print_error;
|
use crate::print_error;
|
||||||
|
|
||||||
pub fn to_array_48(s: &[u8]) -> [u8; 48] {
|
|
||||||
s.try_into().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_uuid_bytes(bytes: &[u8]) -> Option<Bytes> {
|
pub fn to_uuid_bytes(bytes: &[u8]) -> Option<Bytes> {
|
||||||
match bytes.try_into() {
|
match bytes.try_into() {
|
||||||
Ok(uuid) => Some(uuid),
|
Ok(uuid) => Some(uuid),
|
||||||
|
@ -6,13 +6,13 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
tools:context=".ChatActivity">
|
tools:context=".ChatActivity">
|
||||||
|
|
||||||
|
<include layout="@layout/profile_toolbar" android:id="@+id/toolbar"/>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler_chat"
|
android:id="@+id/recycler_chat"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:paddingHorizontal="20dp"
|
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||||
android:paddingBottom="20dp"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/bottom_panel"/>
|
app:layout_constraintBottom_toTopOf="@id/bottom_panel"/>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
@ -20,7 +20,9 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:visibility="gone">
|
android:visibility="gone"
|
||||||
|
android:paddingVertical="5dp"
|
||||||
|
android:background="@color/primary">
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/button_attach"
|
android:id="@+id/button_attach"
|
||||||
|
@ -1,79 +1,93 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/refresher"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
tools:context=".MainActivity">
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<include layout="@layout/profile_toolbar" android:id="@+id/toolbar"/>
|
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
android:id="@+id/refresher"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||||
<TextView
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
android:id="@+id/text_online_sessions"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/online_peers"
|
|
||||||
style="@style/Label"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
|
||||||
|
|
||||||
<ListView
|
|
||||||
android:id="@+id/online_sessions"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_online_sessions"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_offline_sessions"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/offline_contacts"
|
|
||||||
style="@style/Label"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/online_sessions"/>
|
|
||||||
|
|
||||||
<ListView
|
|
||||||
android:id="@+id/offline_sessions"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_offline_sessions"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/bottom_panel"/>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/bottom_panel"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent">
|
||||||
android:orientation="horizontal"
|
|
||||||
android:layout_marginHorizontal="10dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent">
|
|
||||||
|
|
||||||
<EditText
|
<TextView
|
||||||
android:id="@+id/edit_peer_ip"
|
android:id="@+id/text_online_sessions"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="text"
|
android:text="@string/online_peers"
|
||||||
android:maxLines="1"
|
style="@style/Label"
|
||||||
android:imeOptions="actionGo"
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
android:hint="@string/add_peer_ip"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/button_show_ip"/>
|
|
||||||
|
|
||||||
<ImageButton
|
<ListView
|
||||||
android:id="@+id/button_show_ip"
|
android:id="@+id/online_sessions"
|
||||||
android:layout_width="@dimen/image_button_size"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="@dimen/image_button_size"
|
android:layout_height="0dp"
|
||||||
android:src="@drawable/ic_info"
|
android:divider="@color/transparent"
|
||||||
style="@style/ImageButton"
|
app:layout_constraintTop_toBottomOf="@id/text_online_sessions"/>
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_offline_sessions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/offline_contacts"
|
||||||
|
style="@style/Label"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/online_sessions"/>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/offline_sessions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:divider="@color/transparent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_offline_sessions"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/bottom_panel"/>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/bottom_panel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/edit_peer_ip"
|
android:paddingHorizontal="10dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"/>
|
android:paddingVertical="5dp"
|
||||||
|
android:background="@color/primary">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_peer_ip"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:imeOptions="actionGo"
|
||||||
|
android:hint="@string/add_peer_ip"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/button_show_ip"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_show_ip"
|
||||||
|
android:layout_width="@dimen/image_button_size"
|
||||||
|
android:layout_height="@dimen/image_button_size"
|
||||||
|
android:src="@drawable/ic_info"
|
||||||
|
style="@style/ImageButton"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/edit_peer_ip"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -4,7 +4,7 @@
|
|||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="10dp">
|
android:padding="15dp">
|
||||||
|
|
||||||
<sushi.hardcore.aira.widgets.TextAvatar
|
<sushi.hardcore.aira.widgets.TextAvatar
|
||||||
android:id="@+id/text_avatar"
|
android:id="@+id/text_avatar"
|
||||||
|
32
app/src/main/res/layout/profile_toolbar.xml
Normal file
32
app/src/main/res/layout/profile_toolbar.xml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:background="@color/primary"
|
||||||
|
app:contentInsetStartWithNavigation="0dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<sushi.hardcore.aira.widgets.TextAvatar
|
||||||
|
android:id="@+id/text_avatar"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
app:textSize="8sp"
|
||||||
|
android:layout_gravity="center_vertical"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_gravity="center_vertical"/>
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.Toolbar>
|
@ -4,7 +4,7 @@
|
|||||||
<color name="secondary">#19a52c</color>
|
<color name="secondary">#19a52c</color>
|
||||||
<color name="white">#FFFFFF</color>
|
<color name="white">#FFFFFF</color>
|
||||||
<color name="backgroundColor">#111111</color>
|
<color name="backgroundColor">#111111</color>
|
||||||
<color name="sessionBackground">#1F1F1F</color>
|
<color name="sessionBackground">#1A1A1A</color>
|
||||||
<color name="bubbleBackground">@color/sessionBackground</color>
|
<color name="bubbleBackground">@color/sessionBackground</color>
|
||||||
<color name="incomingBubbleBackground">@color/secondary</color>
|
<color name="incomingBubbleBackground">@color/secondary</color>
|
||||||
<color name="textLink">#3845A3</color>
|
<color name="textLink">#3845A3</color>
|
||||||
|
@ -1,18 +1,11 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Theme.AIRA" parent="Theme.AppCompat">
|
<style name="Theme.AIRA" parent="Theme.AppCompat">
|
||||||
<!-- Primary brand color. -->
|
|
||||||
<item name="colorPrimary">@color/primary</item>
|
<item name="colorPrimary">@color/primary</item>
|
||||||
<item name="colorPrimaryVariant">@color/primary</item>
|
<item name="colorPrimaryVariant">@color/primary</item>
|
||||||
<!-- Secondary brand color. -->
|
|
||||||
<item name="colorSecondary">@color/secondary</item>
|
<item name="colorSecondary">@color/secondary</item>
|
||||||
<item name="colorSecondaryVariant">@color/secondary</item>
|
<item name="colorSecondaryVariant">@color/secondary</item>
|
||||||
|
|
||||||
<item name="colorAccent">@color/secondary</item>
|
<item name="colorAccent">@color/secondary</item>
|
||||||
|
|
||||||
<!-- Status bar color. -->
|
|
||||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||||
|
|
||||||
<item name="android:windowBackground">@color/backgroundColor</item>
|
<item name="android:windowBackground">@color/backgroundColor</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
@ -1,12 +1,12 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = "1.4.32"
|
ext.kotlin_version = "1.5.0"
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.2.0'
|
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
@ -17,7 +17,7 @@ buildscript {
|
|||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user