diff --git a/app/build.gradle b/app/build.gradle
index 17b5c4e..5c99947 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -47,12 +47,11 @@ dependencies {
implementation "androidx.fragment:fragment-ktx:1.3.4"
implementation "androidx.preference:preference-ktx:1.1.1"
implementation 'com.google.android.material:material:1.3.0'
- //implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
- //implementation 'androidx.legacy:legacy-support-v4:1.0.0'
- //testImplementation 'junit:junit:4.13.1'
implementation 'net.i2p.crypto:eddsa:0.3.0'
implementation "org.whispersystems:curve25519-android:0.5.0"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
+ implementation 'com.github.bumptech.glide:glide:4.12.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d89d63d..2b0effd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -37,7 +37,9 @@
-
+
diff --git a/app/src/main/java/sushi/hardcore/aira/AIRADatabase.kt b/app/src/main/java/sushi/hardcore/aira/AIRADatabase.kt
index 2e0e813..1f98086 100644
--- a/app/src/main/java/sushi/hardcore/aira/AIRADatabase.kt
+++ b/app/src/main/java/sushi/hardcore/aira/AIRADatabase.kt
@@ -5,22 +5,34 @@ import sushi.hardcore.aira.background_service.Contact
object AIRADatabase {
external fun isIdentityProtected(databaseFolder: String): Boolean
external fun loadIdentity(databaseFolder: String, password: ByteArray?): Boolean
- external fun addContact(name: String, publicKey: ByteArray): Contact?
+ external fun addContact(name: String, avatarUuid: String?, publicKey: ByteArray): Contact?
external fun removeContact(uuid: String): Boolean
external fun loadContacts(): ArrayList?
external fun setVerified(uuid: String): Boolean
external fun setContactSeen(contactUuid: String, seen: Boolean): Boolean
external fun changeContactName(contactUuid: String, newName: String): Boolean
+ external fun setContactAvatar(contactUuid: String, avatarUuid: String?): Boolean
external fun storeMsg(contactUuid: String, outgoing: Boolean, data: ByteArray): Boolean
external fun storeFile(contactUuid: String?, data: ByteArray): ByteArray?
external fun loadMsgs(uuid: String, offset: Int, count: Int): ArrayList?
external fun loadFile(rawUuid: ByteArray): ByteArray?
external fun deleteConversation(contactUuid: String): Boolean
- external fun clearTemporaryFiles(): Int
+ external fun clearCache()
external fun getIdentityPublicKey(): ByteArray
external fun getIdentityFingerprint(): String
external fun getUsePadding(): Boolean
external fun setUsePadding(usePadding: Boolean): Boolean
+ external fun storeAvatar(avatar: ByteArray): String?
+ external fun getAvatar(avatarUuid: String): ByteArray?
external fun changeName(newName: String): Boolean
external fun changePassword(databaseFolder: String, oldPassword: ByteArray?, newPassword: ByteArray?): Boolean
+ external fun setIdentityAvatar(databaseFolder: String, avatar: ByteArray): Boolean
+ external fun removeIdentityAvatar(databaseFolder: String): Boolean
+ external fun getIdentityAvatar(databaseFolder: String): ByteArray?
+
+ fun loadAvatar(avatarUuid: String?): ByteArray? {
+ return avatarUuid?.let {
+ getAvatar(it)
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/aira/ChatActivity.kt b/app/src/main/java/sushi/hardcore/aira/ChatActivity.kt
index 1d2b793..d3e96f1 100644
--- a/app/src/main/java/sushi/hardcore/aira/ChatActivity.kt
+++ b/app/src/main/java/sushi/hardcore/aira/ChatActivity.kt
@@ -29,6 +29,7 @@ class ChatActivity : ServiceBoundActivity() {
private lateinit var binding: ActivityChatBinding
private var sessionId = -1
private lateinit var sessionName: String
+ private var avatar: ByteArray? = null
private lateinit var chatAdapter: ChatAdapter
private var lastLoadedMessageOffset = 0
private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
@@ -48,7 +49,7 @@ class ChatActivity : ServiceBoundActivity() {
if (sessionId != -1) {
intent.getStringExtra("sessionName")?.let { name ->
sessionName = name
- binding.toolbar.textAvatar.setLetterFrom(name)
+ binding.toolbar.avatar.setTextAvatar(name)
binding.toolbar.title.text = name
chatAdapter = ChatAdapter(this@ChatActivity, ::onClickSaveFile)
binding.recyclerChat.apply {
@@ -94,7 +95,18 @@ class ChatActivity : ServiceBoundActivity() {
airaService = binder.getService()
chatAdapter.clear()
- airaService.contacts[sessionId]?.let { contact ->
+ val contact = airaService.contacts[sessionId]
+ if (contact == null) {
+ airaService.savedAvatars[sessionId]
+ } else {
+ contact.avatar
+ }?.let {
+ AIRADatabase.loadAvatar(it)?.let { image ->
+ avatar = image
+ binding.toolbar.avatar.setImageAvatar(image)
+ }
+ }
+ if (contact != null) {
displayIconTrustLevel(true, contact.verified)
loadMsgs(contact.uuid)
}
@@ -126,6 +138,7 @@ class ChatActivity : ServiceBoundActivity() {
val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.hideSoftInputFromWindow(binding.editMessage.windowToken, 0)
binding.bottomPanel.visibility = View.GONE
+ invalidateOptionsMenu()
}
}
}
@@ -133,7 +146,19 @@ class ChatActivity : ServiceBoundActivity() {
if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
sessionName = name
- title = name
+ binding.toolbar.title.text = 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)
+ }
}
}
}
@@ -151,7 +176,6 @@ class ChatActivity : ServiceBoundActivity() {
false
}
}
-
override fun onAskLargeFiles(sessionId: Int, name: String, filesReceiver: FilesReceiver): Boolean {
return if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
@@ -202,15 +226,17 @@ class ChatActivity : ServiceBoundActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.chat_activity, menu)
val contact = airaService.contacts[sessionId]
+ val isOnline = airaService.isOnline(sessionId)
menu.findItem(R.id.delete_conversation).isVisible = contact != null
- menu.findItem(R.id.set_as_contact).isVisible = contact == null
+ menu.findItem(R.id.set_as_contact).isVisible = contact == null && isOnline
menu.findItem(R.id.remove_contact).isVisible = contact != null
if (contact == null) {
menu.findItem(R.id.verify).isVisible = false
} else {
menu.findItem(R.id.verify).isVisible = !contact.verified
}
- menu.findItem(R.id.refresh_name).isEnabled = airaService.isOnline(sessionId)
+ menu.findItem(R.id.refresh_profile).isEnabled = isOnline
+ menu.findItem(R.id.session_info).isVisible = isOnline || contact != null
return true
}
@@ -232,7 +258,7 @@ class ChatActivity : ServiceBoundActivity() {
true
}
R.id.remove_contact -> {
- AlertDialog.Builder(this)
+ AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.warning)
.setMessage(R.string.ask_remove_contact)
.setPositiveButton(R.string.delete) { _, _ ->
@@ -252,7 +278,7 @@ class ChatActivity : ServiceBoundActivity() {
val dialogBinding = DialogFingerprintsBinding.inflate(layoutInflater)
dialogBinding.textLocalFingerprint.text = localFingerprint
dialogBinding.textPeerFingerprint.text = peerFingerprint
- AlertDialog.Builder(this)
+ AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.verifying_contact)
.setView(dialogBinding.root)
.setPositiveButton(R.string.they_match) { _, _ ->
@@ -267,7 +293,7 @@ class ChatActivity : ServiceBoundActivity() {
true
}
R.id.delete_conversation -> {
- AlertDialog.Builder(this)
+ AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.warning)
.setMessage(R.string.ask_delete_conversation)
.setPositiveButton(R.string.delete) { _, _ ->
@@ -279,8 +305,8 @@ class ChatActivity : ServiceBoundActivity() {
.show()
true
}
- R.id.refresh_name -> {
- airaService.sendTo(sessionId, Protocol.askName())
+ R.id.refresh_profile -> {
+ airaService.sendTo(sessionId, Protocol.askProfileInfo())
true
}
else -> super.onOptionsItemSelected(item)
@@ -315,35 +341,40 @@ class ChatActivity : ServiceBoundActivity() {
private fun showSessionInfo() {
val contact = airaService.contacts[sessionId]
val session = airaService.sessions[sessionId]
- val publicKey = contact?.publicKey ?: session?.peerPublicKey
- val dialogBinding = DialogInfoBinding.inflate(layoutInflater)
- dialogBinding.textAvatar.setLetterFrom(sessionName)
- dialogBinding.textFingerprint.text = StringUtils.beautifyFingerprint(generateFingerprint(publicKey!!))
- if (session == null) {
- dialogBinding.onlineFields.visibility = View.GONE
- } else {
- dialogBinding.textIp.text = session.ip
- dialogBinding.textOutgoing.text = getString(if (session.outgoing) {
- R.string.outgoing
+ (contact?.publicKey ?: session?.peerPublicKey)?.let { publicKey -> //can be null if disconnected and not a contact
+ val dialogBinding = DialogInfoBinding.inflate(layoutInflater)
+ if (avatar == null) {
+ dialogBinding.avatar.setTextAvatar(sessionName)
} else {
- R.string.incoming
- })
- }
- dialogBinding.textIsContact.text = getString(if (contact == null) {
- dialogBinding.fieldIsVerified.visibility = View.GONE
- R.string.no
- } else {
- dialogBinding.textIsVerified.text = getString(if (contact.verified) {
- R.string.yes
+ dialogBinding.avatar.setImageAvatar(avatar!!)
+ }
+ dialogBinding.textFingerprint.text = StringUtils.beautifyFingerprint(generateFingerprint(publicKey))
+ if (session == null) {
+ dialogBinding.onlineFields.visibility = View.GONE
} else {
+ dialogBinding.textIp.text = session.ip
+ dialogBinding.textOutgoing.text = getString(if (session.outgoing) {
+ R.string.outgoing
+ } else {
+ R.string.incoming
+ })
+ }
+ dialogBinding.textIsContact.text = getString(if (contact == null) {
+ dialogBinding.fieldIsVerified.visibility = View.GONE
R.string.no
+ } else {
+ dialogBinding.textIsVerified.text = getString(if (contact.verified) {
+ R.string.yes
+ } else {
+ R.string.no
+ })
+ R.string.yes
})
- R.string.yes
- })
- AlertDialog.Builder(this)
- .setTitle(sessionName)
- .setView(dialogBinding.root)
- .setPositiveButton(R.string.ok, null)
- .show()
+ AlertDialog.Builder(this, R.style.CustomAlertDialog)
+ .setTitle(sessionName)
+ .setView(dialogBinding.root)
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/aira/Constants.kt b/app/src/main/java/sushi/hardcore/aira/Constants.kt
index 782402b..6119690 100644
--- a/app/src/main/java/sushi/hardcore/aira/Constants.kt
+++ b/app/src/main/java/sushi/hardcore/aira/Constants.kt
@@ -10,6 +10,7 @@ object Constants {
const val fileSizeLimit = 32760000
const val MSG_LOADING_COUNT = 20
const val FILE_CHUNK_SIZE = 1023996
+ const val MAX_AVATAR_SIZE = 10000000
private const val databaseName = "AIRA.db"
fun getDatabaseFolder(context: Context): String {
diff --git a/app/src/main/java/sushi/hardcore/aira/CreateIdentityFragment.kt b/app/src/main/java/sushi/hardcore/aira/CreateIdentityFragment.kt
index 1d3f463..fba464f 100644
--- a/app/src/main/java/sushi/hardcore/aira/CreateIdentityFragment.kt
+++ b/app/src/main/java/sushi/hardcore/aira/CreateIdentityFragment.kt
@@ -1,5 +1,6 @@
package sushi.hardcore.aira
+import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
@@ -7,25 +8,41 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
import sushi.hardcore.aira.databinding.FragmentCreateIdentityBinding
+import sushi.hardcore.aira.utils.AvatarPicker
-class CreateIdentityFragment : Fragment() {
+class CreateIdentityFragment(private val activity: AppCompatActivity) : Fragment() {
private external fun createNewIdentity(databaseFolder: String, name: String, password: ByteArray?): Boolean
companion object {
- fun newInstance(): CreateIdentityFragment {
- return CreateIdentityFragment()
+ fun newInstance(activity: AppCompatActivity): CreateIdentityFragment {
+ return CreateIdentityFragment(activity)
}
}
+ private val avatarPicker = AvatarPicker(activity) { picker, avatar ->
+ picker.setOnAvatarCompressed { compressedAvatar ->
+ AIRADatabase.setIdentityAvatar(Constants.getDatabaseFolder(activity), compressedAvatar)
+ }
+ avatar.circleCrop().into(binding.avatar)
+ }
private lateinit var binding: FragmentCreateIdentityBinding
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ avatarPicker.register()
+ }
+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentCreateIdentityBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ binding.buttonSetAvatar.setOnClickListener {
+ avatarPicker.launch()
+ }
binding.checkboxEnablePassword.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
binding.editPassword.visibility = View.VISIBLE
@@ -63,7 +80,7 @@ class CreateIdentityFragment : Fragment() {
val intent = Intent(activity, MainActivity::class.java)
intent.putExtra("identityName", identityName)
startActivity(intent)
- activity?.finish()
+ activity.finish()
} else {
Toast.makeText(activity, R.string.identity_create_failed, Toast.LENGTH_SHORT).show()
}
diff --git a/app/src/main/java/sushi/hardcore/aira/LoginActivity.kt b/app/src/main/java/sushi/hardcore/aira/LoginActivity.kt
index 5009ae5..a9d0b7b 100644
--- a/app/src/main/java/sushi/hardcore/aira/LoginActivity.kt
+++ b/app/src/main/java/sushi/hardcore/aira/LoginActivity.kt
@@ -11,8 +11,10 @@ class LoginActivity : AppCompatActivity() {
private external fun getIdentityName(databaseFolder: String): String?
companion object {
+ private external fun initLogging()
init {
System.loadLibrary("aira")
+ initLogging()
}
}
@@ -33,7 +35,7 @@ class LoginActivity : AppCompatActivity() {
finish()
} else if (name != null && !isProtected) {
if (AIRADatabase.loadIdentity(databaseFolder, null)) {
- AIRADatabase.clearTemporaryFiles()
+ AIRADatabase.clearCache()
startMainActivity(name)
} else {
Toast.makeText(this, R.string.identity_load_failed, Toast.LENGTH_SHORT).show()
@@ -42,7 +44,8 @@ class LoginActivity : AppCompatActivity() {
supportFragmentManager.beginTransaction()
.add(
R.id.fragment_container, if (name == null) {
- CreateIdentityFragment.newInstance()
+ AIRADatabase.removeIdentityAvatar(databaseFolder)
+ CreateIdentityFragment.newInstance(this)
} else {
LoginFragment.newInstance(name)
}
diff --git a/app/src/main/java/sushi/hardcore/aira/LoginFragment.kt b/app/src/main/java/sushi/hardcore/aira/LoginFragment.kt
index dccc35d..46a4fa8 100644
--- a/app/src/main/java/sushi/hardcore/aira/LoginFragment.kt
+++ b/app/src/main/java/sushi/hardcore/aira/LoginFragment.kt
@@ -5,11 +5,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import sushi.hardcore.aira.databinding.FragmentLoginBinding
-import sushi.hardcore.aira.widgets.TextAvatar
class LoginFragment : Fragment() {
companion object {
@@ -31,11 +29,17 @@ class LoginFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
arguments?.let { bundle ->
bundle.getString(NAME_ARG)?.let { name ->
- view.findViewById(R.id.text_avatar).setLetterFrom(name)
- view.findViewById(R.id.text_identity_name).text = name
+ val databaseFolder = Constants.getDatabaseFolder(requireContext())
+ val avatar = AIRADatabase.getIdentityAvatar(databaseFolder)
+ if (avatar == null) {
+ binding.avatar.setTextAvatar(name)
+ } else {
+ binding.avatar.setImageAvatar(avatar)
+ }
+ binding.textIdentityName.text = name
binding.buttonLogin.setOnClickListener {
- if (AIRADatabase.loadIdentity(Constants.getDatabaseFolder(requireContext()), binding.editPassword.text.toString().toByteArray())) {
- AIRADatabase.clearTemporaryFiles()
+ if (AIRADatabase.loadIdentity(databaseFolder, binding.editPassword.text.toString().toByteArray())) {
+ AIRADatabase.clearCache()
val intent = Intent(activity, MainActivity::class.java)
intent.putExtra("identityName", name)
startActivity(intent)
diff --git a/app/src/main/java/sushi/hardcore/aira/MainActivity.kt b/app/src/main/java/sushi/hardcore/aira/MainActivity.kt
index 7cde35b..26c4397 100644
--- a/app/src/main/java/sushi/hardcore/aira/MainActivity.kt
+++ b/app/src/main/java/sushi/hardcore/aira/MainActivity.kt
@@ -58,6 +58,12 @@ class MainActivity : ServiceBoundActivity() {
onlineSessionAdapter.setName(sessionId, name)
}
}
+
+ override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
+ runOnUiThread {
+ onlineSessionAdapter.setAvatar(sessionId, avatar)
+ }
+ }
override fun onNewMessage(sessionId: Int, data: ByteArray): Boolean {
runOnUiThread {
onlineSessionAdapter.setSeen(sessionId, false)
@@ -165,7 +171,7 @@ class MainActivity : ServiceBoundActivity() {
}
val dialogBinding = DialogIpAddressesBinding.inflate(layoutInflater)
dialogBinding.textIpAddresses.text = ipAddresses.substring(0, ipAddresses.length-1) //remove last LF
- AlertDialog.Builder(this)
+ AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.your_addresses)
.setView(dialogBinding.root)
.setPositiveButton(R.string.ok, null)
@@ -194,7 +200,7 @@ class MainActivity : ServiceBoundActivity() {
}
R.id.close -> {
if (isServiceInitialized()) {
- AlertDialog.Builder(this)
+ AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.warning)
.setMessage(R.string.ask_log_out)
.setPositiveButton(R.string.yes) { _, _ ->
@@ -210,7 +216,7 @@ class MainActivity : ServiceBoundActivity() {
true
}
R.id.remove_contact -> {
- AlertDialog.Builder(this)
+ AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.warning)
.setMessage(R.string.ask_remove_contacts)
.setPositiveButton(R.string.delete) { _, _ ->
@@ -253,7 +259,12 @@ class MainActivity : ServiceBoundActivity() {
}
private fun initToolbar(identityName: String) {
- binding.toolbar.textAvatar.setLetterFrom(identityName)
+ val avatar = AIRADatabase.getIdentityAvatar(Constants.getDatabaseFolder(this))
+ if (avatar == null) {
+ binding.toolbar.avatar.setTextAvatar(identityName)
+ } else {
+ binding.toolbar.avatar.setImageAvatar(avatar)
+ }
binding.toolbar.title.text = identityName
}
@@ -286,7 +297,7 @@ class MainActivity : ServiceBoundActivity() {
private fun loadContacts() {
if (offlineSessionAdapter != null) {
for ((sessionId, contact) in airaService.contacts) {
- offlineSessionAdapter!!.add(Session(sessionId, true, contact.verified, contact.seen, null, contact.name))
+ offlineSessionAdapter!!.add(Session(sessionId, true, contact.verified, contact.seen, null, contact.name, AIRADatabase.loadAvatar(contact.avatar)))
}
}
}
@@ -301,9 +312,9 @@ class MainActivity : ServiceBoundActivity() {
val seen = !airaService.notSeen.contains(sessionId)
val contact = airaService.contacts[sessionId]
if (contact == null) {
- onlineSessionAdapter.add(Session(sessionId, false, false, seen, ip, airaService.savedNames[sessionId]))
+ onlineSessionAdapter.add(Session(sessionId, false, false, seen, ip, airaService.savedNames[sessionId], AIRADatabase.loadAvatar(airaService.savedAvatars[sessionId])))
} else {
- onlineSessionAdapter.add(Session(sessionId, true, contact.verified, seen, ip, contact.name))
+ onlineSessionAdapter.add(Session(sessionId, true, contact.verified, seen, ip, contact.name, AIRADatabase.loadAvatar(contact.avatar)))
offlineSessionAdapter?.remove(sessionId)
}
}
@@ -311,7 +322,7 @@ class MainActivity : ServiceBoundActivity() {
private fun launchChatActivity(session: Session) {
startActivity(Intent(this, ChatActivity::class.java).apply {
putExtra("sessionId", session.sessionId)
- putExtra("sessionName", session.name)
+ putExtra("sessionName", airaService.getNameOf(session.sessionId))
})
}
@@ -338,7 +349,7 @@ class MainActivity : ServiceBoundActivity() {
} else {
getString(R.string.ask_send_multiple_files, uris!!.size, session.name ?: session.ip)
}
- AlertDialog.Builder(this)
+ AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.warning)
.setMessage(msg)
.setPositiveButton(R.string.yes) { _, _ ->
diff --git a/app/src/main/java/sushi/hardcore/aira/SettingsActivity.kt b/app/src/main/java/sushi/hardcore/aira/SettingsActivity.kt
index 06d7d57..2da237d 100644
--- a/app/src/main/java/sushi/hardcore/aira/SettingsActivity.kt
+++ b/app/src/main/java/sushi/hardcore/aira/SettingsActivity.kt
@@ -1,6 +1,7 @@
package sushi.hardcore.aira
import android.content.*
+import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.IBinder
import android.view.MenuItem
@@ -13,21 +14,47 @@ import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
+import com.bumptech.glide.Glide
+import com.bumptech.glide.RequestBuilder
+import com.bumptech.glide.request.RequestOptions
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.transition.Transition
import sushi.hardcore.aira.background_service.AIRAService
import sushi.hardcore.aira.databinding.ActivitySettingsBinding
+import sushi.hardcore.aira.databinding.ChangeAvatarDialogBinding
+import sushi.hardcore.aira.utils.AvatarPicker
import sushi.hardcore.aira.utils.StringUtils
class SettingsActivity: AppCompatActivity() {
- class MySettingsFragment : PreferenceFragmentCompat() {
+ class MySettingsFragment(private val activity: AppCompatActivity): PreferenceFragmentCompat() {
private lateinit var airaService: AIRAService
+ private val avatarPicker = AvatarPicker(activity) { picker, avatar ->
+ if (::airaService.isInitialized) {
+ picker.setOnAvatarCompressed { compressedAvatar ->
+ airaService.changeAvatar(compressedAvatar)
+ }
+ }
+ displayAvatar(avatar)
+ }
+ private lateinit var identityAvatarPreference: Preference
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ avatarPicker.register()
+ }
+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
+ findPreference("identityAvatar")?.let { identityAvatarPreference = it }
val identityNamePreference = findPreference("identityName")
val paddingPreference = findPreference("psecPadding")
identityNamePreference?.isPersistent = false
paddingPreference?.isPersistent = false
+ AIRADatabase.getIdentityAvatar(Constants.getDatabaseFolder(activity))?.let { avatar ->
+ displayAvatar(avatar)
+ }
Intent(activity, AIRAService::class.java).also { serviceIntent ->
- activity?.bindService(serviceIntent, object : ServiceConnection {
+ activity.bindService(serviceIntent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
val binder = service as AIRAService.AIRABinder
airaService = binder.getService()
@@ -37,6 +64,26 @@ class SettingsActivity: AppCompatActivity() {
override fun onServiceDisconnected(name: ComponentName?) {}
}, Context.BIND_AUTO_CREATE)
}
+ identityAvatarPreference.setOnPreferenceClickListener {
+ val dialogBuilder = AlertDialog.Builder(activity, R.style.CustomAlertDialog)
+ .setTitle(R.string.your_avatar)
+ .setPositiveButton(R.string.set_a_new_one) { _, _ ->
+ avatarPicker.launch()
+ }
+ val dialogBinding = ChangeAvatarDialogBinding.inflate(layoutInflater)
+ val avatar = AIRADatabase.getIdentityAvatar(Constants.getDatabaseFolder(activity))
+ if (avatar == null) {
+ dialogBinding.avatar.setTextAvatar(airaService.identityName!!)
+ } else {
+ dialogBinding.avatar.setImageAvatar(avatar)
+ dialogBuilder.setNegativeButton(R.string.remove) { _, _ ->
+ displayAvatar(null)
+ airaService.changeAvatar(null)
+ }
+ }
+ dialogBuilder.setView(dialogBinding.root).show()
+ false
+ }
identityNamePreference?.setOnPreferenceChangeListener { _, newValue ->
if (airaService.changeName(newValue as String)) {
identityNamePreference.text = newValue
@@ -44,70 +91,66 @@ class SettingsActivity: AppCompatActivity() {
false
}
findPreference("deleteIdentity")?.setOnPreferenceClickListener {
- activity?.let { activity ->
- AlertDialog.Builder(activity)
- .setMessage(R.string.confirm_delete)
- .setTitle(R.string.warning)
- .setPositiveButton(R.string.ok) { _, _ ->
- if (Constants.getDatabasePath(activity).delete()) {
- airaService.logOut()
- startActivity(Intent(activity, LoginActivity::class.java))
- activity.finish()
- }
+ AlertDialog.Builder(activity, R.style.CustomAlertDialog)
+ .setMessage(R.string.confirm_delete)
+ .setTitle(R.string.warning)
+ .setPositiveButton(R.string.ok) { _, _ ->
+ if (Constants.getDatabasePath(activity).delete()) {
+ airaService.logOut()
+ startActivity(Intent(activity, LoginActivity::class.java))
+ activity.finish()
}
- .setNegativeButton(R.string.cancel, null)
- .show()
- }
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .show()
false
}
findPreference("identityPassword")?.setOnPreferenceClickListener {
- activity?.let { activity ->
- val dialogView = layoutInflater.inflate(R.layout.dialog_password, null)
- val oldPasswordEditText = dialogView.findViewById(R.id.old_password)
- val isIdentityProtected = AIRADatabase.isIdentityProtected(Constants.getDatabaseFolder(activity))
- if (!isIdentityProtected) {
- oldPasswordEditText.visibility = View.GONE
- }
- val newPasswordEditText = dialogView.findViewById(R.id.new_password)
- val newPasswordConfirmEditText = dialogView.findViewById(R.id.new_password_confirm)
- AlertDialog.Builder(activity)
- .setView(dialogView)
- .setTitle(R.string.change_password)
- .setPositiveButton(R.string.ok) { _, _ ->
- val newPassword = newPasswordEditText.text.toString().toByteArray()
- if (newPassword.isEmpty()) {
- if (isIdentityProtected) { //don't change password if identity is not protected and new password is blank
- changePassword(activity, isIdentityProtected, oldPasswordEditText, null)
- }
- } else {
- val newPasswordConfirm = newPasswordConfirmEditText.text.toString().toByteArray()
- if (newPassword.contentEquals(newPasswordConfirm)) {
- changePassword(activity, isIdentityProtected, oldPasswordEditText, newPassword)
- } else {
- AlertDialog.Builder(activity)
- .setMessage(R.string.password_mismatch)
- .setTitle(R.string.error)
- .setPositiveButton(R.string.ok, null)
- .show()
- }
- newPassword.fill(0)
- newPasswordConfirm.fill(0)
- }
- }
- .setNegativeButton(R.string.cancel, null)
- .show()
+ val dialogView = layoutInflater.inflate(R.layout.dialog_password, null)
+ val oldPasswordEditText = dialogView.findViewById(R.id.old_password)
+ val isIdentityProtected = AIRADatabase.isIdentityProtected(Constants.getDatabaseFolder(activity))
+ if (!isIdentityProtected) {
+ oldPasswordEditText.visibility = View.GONE
}
+ val newPasswordEditText = dialogView.findViewById(R.id.new_password)
+ val newPasswordConfirmEditText = dialogView.findViewById(R.id.new_password_confirm)
+ AlertDialog.Builder(activity, R.style.CustomAlertDialog)
+ .setView(dialogView)
+ .setTitle(R.string.change_password)
+ .setPositiveButton(R.string.ok) { _, _ ->
+ val newPassword = newPasswordEditText.text.toString().toByteArray()
+ if (newPassword.isEmpty()) {
+ if (isIdentityProtected) { //don't change password if identity is not protected and new password is blank
+ changePassword(isIdentityProtected, oldPasswordEditText, null)
+ }
+ } else {
+ val newPasswordConfirm = newPasswordConfirmEditText.text.toString().toByteArray()
+ if (newPassword.contentEquals(newPasswordConfirm)) {
+ changePassword(isIdentityProtected, oldPasswordEditText, newPassword)
+ } else {
+ AlertDialog.Builder(activity, R.style.CustomAlertDialog)
+ .setMessage(R.string.password_mismatch)
+ .setTitle(R.string.error)
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+ newPassword.fill(0)
+ newPasswordConfirm.fill(0)
+ }
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .show()
false
}
findPreference("fingerprint")?.let { fingerprintPreference ->
val fingerprint = StringUtils.beautifyFingerprint(AIRADatabase.getIdentityFingerprint())
fingerprintPreference.summary = fingerprint
fingerprintPreference.setOnPreferenceClickListener {
- activity?.getSystemService(CLIPBOARD_SERVICE)?.let { service ->
+ activity.getSystemService(CLIPBOARD_SERVICE)?.let { service ->
val clipboardManager = service as ClipboardManager
clipboardManager.setPrimaryClip(ClipData.newPlainText("", fingerprint))
}
- Toast.makeText(activity, R.string.fingerprint_copied, Toast.LENGTH_SHORT).show()
+ Toast.makeText(activity, R.string.copied, Toast.LENGTH_SHORT).show()
false
}
}
@@ -118,14 +161,31 @@ class SettingsActivity: AppCompatActivity() {
}
}
- private fun changePassword(context: Context, isIdentityProtected: Boolean, oldPasswordEditText: EditText, newPassword: ByteArray?) {
+ private fun displayAvatar(avatar: ByteArray?) {
+ if (avatar == null) {
+ identityAvatarPreference.setIcon(R.drawable.ic_face)
+ } else {
+ displayAvatar(Glide.with(this).load(avatar))
+ }
+ }
+
+ private fun displayAvatar(glideBuilder: RequestBuilder) {
+ glideBuilder.apply(RequestOptions().override(90)).circleCrop().into(object : CustomTarget() {
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ identityAvatarPreference.icon = resource
+ }
+ override fun onLoadCleared(placeholder: Drawable?) {}
+ })
+ }
+
+ private fun changePassword(isIdentityProtected: Boolean, oldPasswordEditText: EditText, newPassword: ByteArray?) {
val oldPassword = if (isIdentityProtected) {
oldPasswordEditText.text.toString().toByteArray()
} else {
null
}
- if (!AIRADatabase.changePassword(Constants.getDatabaseFolder(context), oldPassword, newPassword)) {
- AlertDialog.Builder(context)
+ if (!AIRADatabase.changePassword(Constants.getDatabaseFolder(activity), oldPassword, newPassword)) {
+ AlertDialog.Builder(activity, R.style.CustomAlertDialog)
.setMessage(R.string.change_password_failed)
.setTitle(R.string.error)
.setPositiveButton(R.string.ok, null)
@@ -150,7 +210,7 @@ class SettingsActivity: AppCompatActivity() {
setContentView(binding.root)
supportFragmentManager
.beginTransaction()
- .replace(R.id.settings_container, MySettingsFragment())
+ .replace(R.id.settings_container, MySettingsFragment(this))
.commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
diff --git a/app/src/main/java/sushi/hardcore/aira/adapters/Session.kt b/app/src/main/java/sushi/hardcore/aira/adapters/Session.kt
index 5f915ed..9a9e91d 100644
--- a/app/src/main/java/sushi/hardcore/aira/adapters/Session.kt
+++ b/app/src/main/java/sushi/hardcore/aira/adapters/Session.kt
@@ -6,5 +6,6 @@ class Session(
val isVerified: Boolean,
var seen: Boolean,
val ip: String?,
- var name: String?
+ var name: String?,
+ var avatar: ByteArray?,
)
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/aira/adapters/SessionAdapter.kt b/app/src/main/java/sushi/hardcore/aira/adapters/SessionAdapter.kt
index 2039da0..c0744eb 100644
--- a/app/src/main/java/sushi/hardcore/aira/adapters/SessionAdapter.kt
+++ b/app/src/main/java/sushi/hardcore/aira/adapters/SessionAdapter.kt
@@ -1,18 +1,18 @@
package sushi.hardcore.aira.adapters
-import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
+import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import sushi.hardcore.aira.R
-import sushi.hardcore.aira.widgets.TextAvatar
+import sushi.hardcore.aira.widgets.Avatar
-class SessionAdapter(private val context: Context): BaseAdapter() {
+class SessionAdapter(private val activity: AppCompatActivity): BaseAdapter() {
private val sessions = mutableListOf()
- private val inflater: LayoutInflater = LayoutInflater.from(context)
+ private val inflater: LayoutInflater = LayoutInflater.from(activity)
val selectedItems = mutableListOf()
override fun getCount(): Int {
@@ -31,7 +31,7 @@ class SessionAdapter(private val context: Context): BaseAdapter() {
val view: View = convertView ?: inflater.inflate(R.layout.adapter_session, parent, false)
val currentSession = getItem(position)
view.findViewById(R.id.text_name).apply {
- view.findViewById(R.id.text_avatar).setLetterFrom(if (currentSession.name == null) {
+ val avatarName = if (currentSession.name == null) {
text = currentSession.ip
setTextColor(Color.RED)
"?"
@@ -39,7 +39,13 @@ class SessionAdapter(private val context: Context): BaseAdapter() {
text = currentSession.name
setTextColor(Color.WHITE)
currentSession.name!!
- })
+ }
+ val avatar = view.findViewById(R.id.avatar)
+ if (currentSession.avatar == null) {
+ avatar.setTextAvatar(avatarName)
+ } else {
+ avatar.setImageAvatar(currentSession.avatar!!)
+ }
}
view.findViewById(R.id.image_trust_level).apply {
if (currentSession.isVerified) {
@@ -55,12 +61,12 @@ class SessionAdapter(private val context: Context): BaseAdapter() {
} else {
View.VISIBLE
}
- view.findViewById(R.id.image_arrow).setColorFilter(ContextCompat.getColor(context, if (currentSession.seen) {
+ view.findViewById(R.id.image_arrow).setColorFilter(ContextCompat.getColor(activity, if (currentSession.seen) {
R.color.sessionArrow
} else {
R.color.secondary
}))
- view.setBackgroundColor(ContextCompat.getColor(context, if (selectedItems.contains(position)) {
+ view.setBackgroundColor(ContextCompat.getColor(activity, if (selectedItems.contains(position)) {
R.color.itemSelected
} else {
R.color.sessionBackground
@@ -98,6 +104,13 @@ class SessionAdapter(private val context: Context): BaseAdapter() {
}
}
+ fun setAvatar(sessionId: Int, avatar: ByteArray?) {
+ getSessionById(sessionId)?.let {
+ it.avatar = avatar
+ notifyDataSetChanged()
+ }
+ }
+
fun setSeen(sessionId: Int, seen: Boolean) {
getSessionById(sessionId)?.let {
it.seen = seen
diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/AIRAService.kt b/app/src/main/java/sushi/hardcore/aira/background_service/AIRAService.kt
index 95ea6e1..8f02046 100644
--- a/app/src/main/java/sushi/hardcore/aira/background_service/AIRAService.kt
+++ b/app/src/main/java/sushi/hardcore/aira/background_service/AIRAService.kt
@@ -32,8 +32,9 @@ class AIRAService : Service() {
const val MESSAGE_CONNECT_TO = 0
const val MESSAGE_SEND_TO = 1
const val MESSAGE_LOGOUT = 2
- const val MESSAGE_TELL_NAME = 3
- const val MESSAGE_CANCEL_FILE_TRANSFER = 4
+ const val MESSAGE_SEND_NAME = 3
+ const val MESSAGE_SEND_AVATAR = 4
+ const val MESSAGE_CANCEL_FILE_TRANSFER = 5
var isServiceRunning = false
}
@@ -87,6 +88,7 @@ class AIRAService : Service() {
}
val savedMsgs = mutableMapOf>()
val savedNames = mutableMapOf()
+ val savedAvatars = mutableMapOf()
val notSeen = mutableListOf()
var uiCallbacks: UiCallbacks? = null
var isAppInBackground = true
@@ -104,6 +106,7 @@ class AIRAService : Service() {
fun onNewSession(sessionId: Int, ip: String)
fun onSessionDisconnect(sessionId: Int)
fun onNameTold(sessionId: Int, name: String)
+ fun onAvatarChanged(sessionId: Int, avatar: ByteArray?)
fun onNewMessage(sessionId: Int, data: ByteArray): Boolean
fun onAskLargeFiles(sessionId: Int, name: String, filesReceiver: FilesReceiver): Boolean
}
@@ -185,7 +188,7 @@ class AIRAService : Service() {
return sessions.contains(sessionId)
}
- private fun getNameOf(sessionId: Int): String {
+ fun getNameOf(sessionId: Int): String {
return contacts[sessionId]?.name ?: savedNames[sessionId] ?: sessions[sessionId]!!.ip
}
@@ -195,7 +198,7 @@ class AIRAService : Service() {
fun setAsContact(sessionId: Int, name: String): Boolean {
sessions[sessionId]?.peerPublicKey?.let {
- AIRADatabase.addContact(name, it)?.let { contact ->
+ AIRADatabase.addContact(name, savedAvatars[sessionId], it)?.let { contact ->
contacts[sessionId] = contact
savedMsgs.remove(sessionId)?.let { msgs ->
for (msg in msgs) {
@@ -203,6 +206,7 @@ class AIRAService : Service() {
}
}
savedNames.remove(sessionId)
+ savedAvatars.remove(sessionId)
return true
}
}
@@ -240,6 +244,9 @@ class AIRAService : Service() {
return if (AIRADatabase.removeContact(it.uuid)) {
savedMsgs[sessionId] = mutableListOf()
savedNames[sessionId] = it.name
+ it.avatar?.let { avatarUuid ->
+ savedAvatars[sessionId] = avatarUuid
+ }
true
} else {
false
@@ -263,7 +270,26 @@ class AIRAService : Service() {
fun changeName(newName: String): Boolean {
return if (AIRADatabase.changeName(newName)) {
identityName = newName
- serviceHandler.sendEmptyMessage(MESSAGE_TELL_NAME)
+ serviceHandler.sendEmptyMessage(MESSAGE_SEND_NAME)
+ true
+ } else {
+ false
+ }
+ }
+
+ fun changeAvatar(avatar: ByteArray?): Boolean {
+ val databaseFolder = Constants.getDatabaseFolder(applicationContext)
+ val success = if (avatar == null) {
+ AIRADatabase.removeIdentityAvatar(databaseFolder)
+ } else {
+ AIRADatabase.setIdentityAvatar(databaseFolder, avatar)
+ }
+ return if (success) {
+ serviceHandler.obtainMessage().apply {
+ what = MESSAGE_SEND_AVATAR
+ data = Bundle().apply { putByteArray("avatar", avatar) }
+ serviceHandler.sendMessage(this)
+ }
true
} else {
false
@@ -301,7 +327,7 @@ class AIRAService : Service() {
sessionIdByKey[key] = sessionId
uiCallbacks?.onNewSession(sessionId, session.ip)
if (!isContact(sessionId)) {
- session.encryptAndSend(Protocol.askName(), usePadding)
+ session.encryptAndSend(Protocol.askProfileInfo(), usePadding)
}
} else {
session.close()
@@ -449,17 +475,33 @@ class AIRAService : Service() {
cancelFileTransfer(sessionId, session, true)
}
}
- MESSAGE_TELL_NAME -> {
+ MESSAGE_SEND_NAME -> {
identityName?.let {
+ val tellingName = Protocol.name(it)
for (session in sessions.values) {
try {
- session.encryptAndSend(Protocol.tellName(it), usePadding)
+ session.encryptAndSend(tellingName, usePadding)
} catch (e: SocketException) {
e.printStackTrace()
}
}
}
}
+ MESSAGE_SEND_AVATAR -> {
+ val avatar = msg.data.getByteArray("avatar")
+ val avatarMsg = if (avatar == null) {
+ Protocol.removeAvatar()
+ } else {
+ Protocol.avatar(avatar)
+ }
+ for (session in sessions.values) {
+ try {
+ session.encryptAndSend(avatarMsg, usePadding)
+ } catch (e: SocketException) {
+ e.printStackTrace()
+ }
+ }
+ }
MESSAGE_LOGOUT -> {
nsdManager.unregisterService(nsdRegistrationListener)
stopDiscovery()
@@ -494,6 +536,20 @@ class AIRAService : Service() {
usePadding = AIRADatabase.getUsePadding()
}
+ private fun setAvatarUuid(sessionId: Int, avatarUuid: String?) {
+ val contact = contacts[sessionId]
+ if (contact == null) {
+ if (avatarUuid == null) {
+ savedAvatars.remove(sessionId)
+ } else {
+ savedAvatars[sessionId] = avatarUuid
+ }
+ } else {
+ AIRADatabase.setContactAvatar(contact.uuid, avatarUuid)
+ contact.avatar = avatarUuid
+ }
+ }
+
private fun encryptNextChunk(session: Session, filesSender: FilesSender) {
val nextChunk = ByteArray(Constants.FILE_CHUNK_SIZE)
nextChunk[0] = Protocol.LARGE_FILE_CHUNK
@@ -566,22 +622,6 @@ class AIRAService : Service() {
shouldCloseSession = true
} else {
when (buffer[0]) {
- Protocol.ASK_NAME -> {
- identityName?.let { name ->
- session.encryptAndSend(Protocol.tellName(name), usePadding)
- }
- }
- Protocol.TELL_NAME -> {
- val name = StringUtils.sanitizeName(buffer.sliceArray(1 until buffer.size).decodeToString())
- uiCallbacks?.onNameTold(sessionId, name)
- val contact = contacts[sessionId]
- if (contact == null) {
- savedNames[sessionId] = name
- } else {
- contact.name = name
- AIRADatabase.changeContactName(contact.uuid, name)
- }
- }
Protocol.LARGE_FILE_CHUNK -> {
receiveFileTransfers[sessionId]?.let { filesReceiver ->
val file = filesReceiver.files[filesReceiver.index]
@@ -714,6 +754,38 @@ class AIRAService : Service() {
}
}
}
+ Protocol.ASK_PROFILE_INFO -> {
+ identityName?.let { name ->
+ session.encryptAndSend(Protocol.name(name), usePadding)
+ }
+ AIRADatabase.getIdentityAvatar(Constants.getDatabaseFolder(this))?.let { avatar ->
+ session.encryptAndSend(Protocol.avatar(avatar), usePadding)
+ }
+ }
+ Protocol.NAME -> {
+ val name = StringUtils.sanitizeName(buffer.sliceArray(1 until buffer.size).decodeToString())
+ uiCallbacks?.onNameTold(sessionId, name)
+ val contact = contacts[sessionId]
+ if (contact == null) {
+ savedNames[sessionId] = name
+ } else {
+ contact.name = name
+ AIRADatabase.changeContactName(contact.uuid, name)
+ }
+ }
+ Protocol.AVATAR -> {
+ if (buffer.size < Constants.MAX_AVATAR_SIZE) {
+ val avatar = buffer.sliceArray(1 until buffer.size)
+ uiCallbacks?.onAvatarChanged(sessionId, avatar)
+ AIRADatabase.storeAvatar(avatar)?.let { avatarUuid ->
+ setAvatarUuid(sessionId, avatarUuid)
+ }
+ }
+ }
+ Protocol.REMOVE_AVATAR -> {
+ uiCallbacks?.onAvatarChanged(sessionId, null)
+ setAvatarUuid(sessionId, null)
+ }
else -> {
when (buffer[0]){
Protocol.MESSAGE -> buffer
diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/Contact.kt b/app/src/main/java/sushi/hardcore/aira/background_service/Contact.kt
index d27f37b..a718833 100644
--- a/app/src/main/java/sushi/hardcore/aira/background_service/Contact.kt
+++ b/app/src/main/java/sushi/hardcore/aira/background_service/Contact.kt
@@ -1,3 +1,10 @@
package sushi.hardcore.aira.background_service
-class Contact(val uuid: String, val publicKey: ByteArray, var name: String, var verified: Boolean, var seen: Boolean)
\ No newline at end of file
+class Contact(
+ val uuid: String,
+ val publicKey: ByteArray,
+ var name: String,
+ var avatar: String?,
+ var verified: Boolean,
+ var seen: Boolean
+)
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/FilesReceiver.kt b/app/src/main/java/sushi/hardcore/aira/background_service/FilesReceiver.kt
index 05a5f8a..6d5beac 100644
--- a/app/src/main/java/sushi/hardcore/aira/background_service/FilesReceiver.kt
+++ b/app/src/main/java/sushi/hardcore/aira/background_service/FilesReceiver.kt
@@ -28,7 +28,7 @@ class FilesReceiver(
filesInfo.appendLine(file.fileName+" ("+FileUtils.formatSize(file.fileSize)+')')
}
dialogBinding.textFilesInfo.text = filesInfo.substring(0, filesInfo.length-1)
- AlertDialog.Builder(activity)
+ AlertDialog.Builder(activity, R.style.CustomAlertDialog)
.setTitle(R.string.download_file_request)
.setView(dialogBinding.root)
.setCancelable(false)
diff --git a/app/src/main/java/sushi/hardcore/aira/background_service/Protocol.kt b/app/src/main/java/sushi/hardcore/aira/background_service/Protocol.kt
index 06a23fd..c0d97bb 100644
--- a/app/src/main/java/sushi/hardcore/aira/background_service/Protocol.kt
+++ b/app/src/main/java/sushi/hardcore/aira/background_service/Protocol.kt
@@ -1,25 +1,36 @@
package sushi.hardcore.aira.background_service
+import sushi.hardcore.aira.widgets.Avatar
import java.nio.ByteBuffer
class Protocol {
companion object {
const val MESSAGE: Byte = 0x00
- const val ASK_NAME: Byte = 0x01
- const val TELL_NAME: Byte = 0x02
- const val FILE: Byte = 0x03
- const val ASK_LARGE_FILES: Byte = 0x04
- const val ACCEPT_LARGE_FILES: Byte = 0x05
- const val LARGE_FILE_CHUNK: Byte = 0x06
- const val ACK_CHUNK: Byte = 0x07
- const val ABORT_FILES_TRANSFER: Byte = 0x08
+ const val FILE: Byte = 0x01
+ const val ASK_PROFILE_INFO: Byte = 0x02
+ const val NAME: Byte = 0x03
+ const val AVATAR: Byte = 0x04
+ const val REMOVE_AVATAR: Byte = 0x05
+ const val ASK_LARGE_FILES: Byte = 0x06
+ const val ACCEPT_LARGE_FILES: Byte = 0x07
+ const val LARGE_FILE_CHUNK: Byte = 0x08
+ const val ACK_CHUNK: Byte = 0x09
+ const val ABORT_FILES_TRANSFER: Byte = 0x0a
- fun askName(): ByteArray {
- return byteArrayOf(ASK_NAME)
+ fun askProfileInfo(): ByteArray {
+ return byteArrayOf(ASK_PROFILE_INFO)
}
- fun tellName(name: String): ByteArray {
- return byteArrayOf(TELL_NAME)+name.toByteArray()
+ fun name(name: String): ByteArray {
+ return byteArrayOf(NAME)+name.toByteArray()
+ }
+
+ fun avatar(avatar: ByteArray): ByteArray {
+ return byteArrayOf(AVATAR)+avatar
+ }
+
+ fun removeAvatar(): ByteArray {
+ return byteArrayOf(REMOVE_AVATAR)
}
fun newMessage(msg: String): ByteArray {
diff --git a/app/src/main/java/sushi/hardcore/aira/utils/AvatarPicker.kt b/app/src/main/java/sushi/hardcore/aira/utils/AvatarPicker.kt
new file mode 100644
index 0000000..ce786b6
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/aira/utils/AvatarPicker.kt
@@ -0,0 +1,56 @@
+package sushi.hardcore.aira.utils
+
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.graphics.drawable.toBitmap
+import com.bumptech.glide.Glide
+import com.bumptech.glide.RequestBuilder
+import com.bumptech.glide.request.target.CustomTarget
+import com.bumptech.glide.request.transition.Transition
+import sushi.hardcore.aira.Constants
+import sushi.hardcore.aira.R
+import java.io.ByteArrayOutputStream
+
+class AvatarPicker(
+ private val activity: AppCompatActivity,
+ private val onAvatarPicked: (AvatarPicker, RequestBuilder) -> Unit,
+) {
+ private lateinit var picker: ActivityResultLauncher
+ private lateinit var avatar: RequestBuilder
+ fun register() {
+ picker = activity.registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
+ if (uri != null) {
+ activity.contentResolver.openInputStream(uri)?.let { stream ->
+ val image = stream.readBytes()
+ stream.close()
+ if (image.size > Constants.MAX_AVATAR_SIZE) {
+ Toast.makeText(activity, R.string.avatar_too_large, Toast.LENGTH_SHORT).show()
+ } else {
+ avatar = Glide.with(activity).load(image).centerCrop()
+ onAvatarPicked(this, avatar)
+ }
+ }
+ }
+ }
+ }
+
+ fun setOnAvatarCompressed(onCompressed: (ByteArray) -> Unit) {
+ avatar.into(object: CustomTarget() {
+ override fun onResourceReady(resource: Drawable, transition: Transition?) {
+ val avatar = ByteArrayOutputStream()
+ if (resource.toBitmap().compress(Bitmap.CompressFormat.PNG, 100, avatar)) {
+ onCompressed(avatar.toByteArray())
+ }
+ }
+ override fun onLoadCleared(placeholder: Drawable?) {}
+ })
+ }
+
+ fun launch() {
+ picker.launch("image/*")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/aira/widgets/Avatar.kt b/app/src/main/java/sushi/hardcore/aira/widgets/Avatar.kt
new file mode 100644
index 0000000..6305a40
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/aira/widgets/Avatar.kt
@@ -0,0 +1,49 @@
+package sushi.hardcore.aira.widgets
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.RelativeLayout
+import com.bumptech.glide.Glide
+import sushi.hardcore.aira.R
+import sushi.hardcore.aira.databinding.AvatarBinding
+
+class Avatar @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : RelativeLayout(context, attrs, defStyle) {
+
+ private val binding = AvatarBinding.inflate(LayoutInflater.from(context), this, true)
+
+ init {
+ attrs?.let {
+ val typedArray = context.obtainStyledAttributes(it, R.styleable.Avatar)
+ for (i in 0..typedArray.indexCount) {
+ val attr = typedArray.getIndex(i)
+ if (attr == R.styleable.Avatar_textSize) {
+ val textSize = typedArray.getDimension(attr, -1F)
+ if (textSize != -1F) {
+ binding.textLetter.textSize = textSize
+ }
+ }
+ }
+ typedArray.recycle()
+ }
+ }
+
+ fun setTextAvatar(name: String) {
+ if (name.isNotEmpty()) {
+ binding.textLetter.text = name[0].toString()
+ binding.imageAvatar.visibility = View.GONE
+ binding.textAvatar.visibility = View.VISIBLE
+ }
+ }
+
+ fun setImageAvatar(avatar: ByteArray) {
+ Glide.with(this).load(avatar).circleCrop().into(binding.imageAvatar)
+ binding.textAvatar.visibility = View.GONE
+ binding.imageAvatar.visibility = View.VISIBLE
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/aira/widgets/TextAvatar.kt b/app/src/main/java/sushi/hardcore/aira/widgets/TextAvatar.kt
deleted file mode 100644
index 9898876..0000000
--- a/app/src/main/java/sushi/hardcore/aira/widgets/TextAvatar.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package sushi.hardcore.aira.widgets
-
-import android.content.Context
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.widget.RelativeLayout
-import android.widget.TextView
-import sushi.hardcore.aira.R
-
-class TextAvatar @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyle: Int = 0
-) : RelativeLayout(context, attrs, defStyle) {
-
- private val view = LayoutInflater.from(context).inflate(R.layout.text_avatar, this, true)
-
- init {
- attrs?.let {
- val typedArray = context.obtainStyledAttributes(it, R.styleable.TextAvatar)
- for (i in 0..typedArray.indexCount) {
- val attr = typedArray.getIndex(i)
- if (attr == R.styleable.TextAvatar_textSize) {
- val textSize = typedArray.getDimension(attr, -1F)
- if (textSize != -1F) {
- view.findViewById(R.id.text_letter).textSize = textSize
- }
- break
- }
- }
- typedArray.recycle()
- }
- }
-
- fun setLetterFrom(name: String) {
- if (name.isNotEmpty()) {
- view.findViewById(R.id.text_letter).text = name[0].toUpperCase().toString()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/native/src/identity.rs b/app/src/main/native/src/identity.rs
index 7fde962..fd4c610 100644
--- a/app/src/main/native/src/identity.rs
+++ b/app/src/main/native/src/identity.rs
@@ -11,6 +11,7 @@ const DB_NAME: &str = "AIRA.db";
const MAIN_TABLE: &str = "main";
const CONTACTS_TABLE: &str = "contacts";
const FILES_TABLE: &str = "files";
+const AVATARS_TABLE: &str = "avatars";
const DATABASE_CORRUPED_ERROR: &str = "Database corrupted";
@@ -21,6 +22,7 @@ impl<'a> DBKeys {
pub const SALT: &'a str = "salt";
pub const MASTER_KEY: &'a str = "master_key";
pub const USE_PADDING: &'a str = "use_padding";
+ pub const AVATAR: &'a str = "avatar";
}
fn bool_to_byte(b: bool) -> u8 {
@@ -53,6 +55,7 @@ pub struct Contact {
pub uuid: Uuid,
pub public_key: [u8; PUBLIC_KEY_LENGTH],
pub name: String,
+ pub avatar: Option,
pub verified: bool,
pub seen: bool,
}
@@ -79,19 +82,23 @@ impl Identity {
get_database_path(&self.database_folder)
}
- pub fn add_contact(&self, name: String, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result {
+ pub fn add_contact(&self, name: String, avatar_uuid: Option, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result {
let db = Connection::open(self.get_database_path())?;
- db.execute(&("CREATE TABLE IF NOT EXISTS ".to_owned()+CONTACTS_TABLE+"(uuid BLOB PRIMARY KEY, name BLOB, key BLOB, verified BLOB, seen BLOB)"), [])?;
+ db.execute(&("CREATE TABLE IF NOT EXISTS ".to_owned()+CONTACTS_TABLE+"(uuid BLOB PRIMARY KEY, name BLOB, avatar BLOB, key BLOB, verified BLOB, seen BLOB)"), [])?;
let contact_uuid = Uuid::new_v4();
let encrypted_name = crypto::encrypt_data(name.as_bytes(), &self.master_key).unwrap();
let encrypted_public_key = crypto::encrypt_data(&public_key, &self.master_key).unwrap();
let encrypted_verified = crypto::encrypt_data(&[bool_to_byte(false)], &self.master_key).unwrap();
let encrypted_seen = crypto::encrypt_data(&[bool_to_byte(true)], &self.master_key).unwrap();
- db.execute(&("INSERT INTO ".to_owned()+CONTACTS_TABLE+" (uuid, name, key, verified, seen) VALUES (?1, ?2, ?3, ?4, ?5)"), params![&contact_uuid.as_bytes()[..], encrypted_name, encrypted_public_key, encrypted_verified, encrypted_seen])?;
+ match avatar_uuid {
+ Some(avatar_uuid) => db.execute(&format!("INSERT INTO {} (uuid, name, avatar, key, verified, seen) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", CONTACTS_TABLE), params![&contact_uuid.as_bytes()[..], encrypted_name, &avatar_uuid.as_bytes()[..], encrypted_public_key, encrypted_verified, encrypted_seen])?,
+ None => db.execute(&format!("INSERT INTO {} (uuid, name, key, verified, seen) VALUES (?1, ?2, ?3, ?4, ?5)", CONTACTS_TABLE), params![&contact_uuid.as_bytes()[..], encrypted_name, encrypted_public_key, encrypted_verified, encrypted_seen])?
+ };
Ok(Contact {
uuid: contact_uuid,
public_key: public_key,
name: name,
+ avatar: avatar_uuid,
verified: false,
seen: true,
})
@@ -115,6 +122,17 @@ impl Identity {
db.execute(&format!("UPDATE {} SET name=?1 WHERE uuid=?2", CONTACTS_TABLE), [encrypted_name.as_slice(), uuid.as_bytes()])
}
+ pub fn set_contact_avatar(&self, contact_uuid: &Uuid, avatar_uuid: Option<&Uuid>) -> Result {
+ let db = Connection::open(self.get_database_path())?;
+ match avatar_uuid {
+ Some(avatar_uuid) => db.execute(&format!("UPDATE {} SET avatar=?1 WHERE uuid=?2", CONTACTS_TABLE), params![&avatar_uuid.as_bytes()[..], &contact_uuid.as_bytes()[..]]),
+ None => {
+ db.execute(&format!("DELETE FROM {} WHERE uuid=(SELECT avatar FROM {} WHERE uuid=?)", AVATARS_TABLE, CONTACTS_TABLE), params![&contact_uuid.as_bytes()[..]])?;
+ db.execute(&format!("UPDATE {} SET avatar=NULL WHERE uuid=?", CONTACTS_TABLE), params![&contact_uuid.as_bytes()[..]])
+ }
+ }
+ }
+
pub fn set_contact_seen(&self, uuid: &Uuid, seen: bool) -> Result {
let db = Connection::open(self.get_database_path())?;
let encrypted_seen = crypto::encrypt_data(&[bool_to_byte(seen)], &self.master_key).unwrap();
@@ -124,107 +142,99 @@ impl Identity {
pub fn load_contacts(&self) -> Option> {
match Connection::open(self.get_database_path()) {
Ok(db) => {
- match db.prepare(&("SELECT uuid, name, key, verified, seen FROM ".to_owned()+CONTACTS_TABLE)) {
- Ok(mut stmt) => {
- let mut rows = stmt.query([]).unwrap();
- let mut contacts = Vec::new();
- while let Some(row) = rows.next().unwrap() {
- let encrypted_public_key: Vec = row.get(2).unwrap();
- match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) {
- Ok(public_key) => {
- if public_key.len() == PUBLIC_KEY_LENGTH {
- let encrypted_name: Vec = row.get(1).unwrap();
- match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) {
- Ok(name) => {
- let encrypted_verified: Vec = row.get(3).unwrap();
- match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) {
- Ok(verified) => {
- let encrypted_seen: Vec = row.get(4).unwrap();
- match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) {
- Ok(seen) => {
- let uuid: Vec = row.get(0).unwrap();
- match to_uuid_bytes(&uuid) {
- Some(uuid_bytes) => {
- contacts.push(Contact {
- uuid: Uuid::from_bytes(uuid_bytes),
- public_key: public_key.try_into().unwrap(),
- name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(),
- verified: byte_to_bool(verified[0]).unwrap(),
- seen: byte_to_bool(seen[0]).unwrap(),
- })
- }
- None => {}
- }
- }
- Err(e) => print_error!(e)
- }
+ if let Ok(mut stmt) = db.prepare(&("SELECT uuid, name, avatar, key, verified, seen FROM ".to_owned()+CONTACTS_TABLE)) {
+ let mut rows = stmt.query([]).unwrap();
+ let mut contacts = Vec::new();
+ while let Ok(Some(row)) = rows.next() {
+ let encrypted_public_key: Vec = row.get(3).unwrap();
+ match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) {
+ Ok(public_key) => {
+ let encrypted_name: Vec = row.get(1).unwrap();
+ match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) {
+ Ok(name) => {
+ let encrypted_verified: Vec = row.get(4).unwrap();
+ match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) {
+ Ok(verified) => {
+ let encrypted_seen: Vec = row.get(5).unwrap();
+ match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) {
+ Ok(seen) => {
+ let contact_uuid: Vec = row.get(0).unwrap();
+ let avatar_result: Result, rusqlite::Error> = row.get(2);
+ let avatar = match avatar_result {
+ Ok(avatar_uuid) => Some(Uuid::from_bytes(to_uuid_bytes(&avatar_uuid).unwrap())),
+ Err(_) => None
+ };
+ contacts.push(Contact {
+ uuid: Uuid::from_bytes(to_uuid_bytes(&contact_uuid).unwrap()),
+ public_key: public_key.try_into().unwrap(),
+ name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(),
+ avatar,
+ verified: byte_to_bool(verified[0]).unwrap(),
+ seen: byte_to_bool(seen[0]).unwrap(),
+ })
}
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
- } else {
- print_error!("Invalid public key length: database corrupted");
}
+ Err(e) => print_error!(e)
}
- Err(e) => print_error!(e)
}
+ Err(e) => print_error!(e)
}
- Some(contacts)
}
- Err(e) => {
- print_error!(e);
- None
- }
- }
- }
- Err(e) => {
- print_error!(e);
- None
+ return Some(contacts);
+ }
}
+ Err(e) => print_error!(e)
}
+ None
}
- pub fn clear_temporary_files(&self) -> Result {
+ pub fn clear_cache(&self) -> Result<(), rusqlite::Error> {
let db = Connection::open(self.get_database_path())?;
- db.execute(&format!("DELETE FROM {} WHERE contact_uuid IS NULL", FILES_TABLE), [])
+ let mut stmt = db.prepare(&format!("SELECT name FROM sqlite_master WHERE type='table' AND name='{}'", CONTACTS_TABLE))?;
+ let mut rows = stmt.query([])?;
+ let contact_table_exists = rows.next()?.is_some();
+ if contact_table_exists {
+ #[allow(unused_must_use)]
+ {
+ db.execute(&format!("DELETE FROM {} WHERE contact_uuid IS NULL", FILES_TABLE), []);
+ db.execute(&format!("DELETE FROM {} WHERE uuid NOT IN (SELECT avatar FROM {})", AVATARS_TABLE, CONTACTS_TABLE), []);
+ }
+ } else {
+ db.execute(&format!("DROP TABLE IF EXISTS {}", FILES_TABLE), [])?;
+ db.execute(&format!("DROP TABLE IF EXISTS {}", AVATARS_TABLE), [])?;
+ }
+ Ok(())
}
pub fn load_file(&self, uuid: Uuid) -> Option> {
match Connection::open(self.get_database_path()) {
Ok(db) => {
- match db.prepare(&format!("SELECT uuid, data FROM \"{}\"", FILES_TABLE)) {
- Ok(mut stmt) => {
- let mut rows = stmt.query([]).unwrap();
- while let Some(row) = rows.next().unwrap() {
- let encrypted_uuid: Vec = row.get(0).unwrap();
- match crypto::decrypt_data(encrypted_uuid.as_slice(), &self.master_key){
- Ok(test_uuid) => {
- if test_uuid == uuid.as_bytes() {
- let encrypted_data: Vec = row.get(1).unwrap();
- match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
- Ok(data) => return Some(data),
- Err(e) => print_error!(e)
- }
- }
+ let mut stmt = db.prepare(&format!("SELECT uuid, data FROM \"{}\"", FILES_TABLE)).unwrap();
+ let mut rows = stmt.query([]).unwrap();
+ while let Ok(Some(row)) = rows.next() {
+ let encrypted_uuid: Vec = row.get(0).unwrap();
+ match crypto::decrypt_data(encrypted_uuid.as_slice(), &self.master_key){
+ Ok(test_uuid) => {
+ if test_uuid == uuid.as_bytes() {
+ let encrypted_data: Vec = row.get(1).unwrap();
+ match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
+ Ok(data) => return Some(data),
+ Err(e) => print_error!(e)
}
- Err(e) => print_error!(e)
}
}
- None
- }
- Err(e) => {
- print_error!(e);
- None
+ Err(e) => print_error!(e)
}
}
}
- Err(e) => {
- print_error!(e);
- None
- }
+ Err(e) => print_error!(e)
}
+ None
}
pub fn store_file(&self, contact_uuid: Option, data: &[u8]) -> Result {
@@ -253,73 +263,44 @@ impl Identity {
pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option)>> {
match Connection::open(self.get_database_path()) {
Ok(db) => {
- match db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
- Ok(mut stmt) => {
- let mut rows = stmt.query([]).unwrap();
- match rows.next() {
- Ok(row) => if row.is_some() {
- let total: usize = row.unwrap().get(0).unwrap();
- if offset >= total {
- None
- } else {
- if offset+count >= total {
- count = total-offset;
- }
- match db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)) {
- Ok(mut stmt) => {
- let mut rows = stmt.query([]).unwrap();
- let mut msgs = Vec::new();
- while let Some(row) = rows.next().unwrap() {
- let encrypted_outgoing: Vec = row.get(0).unwrap();
- match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
- Ok(outgoing) => {
- match byte_to_bool(outgoing[0]) {
- Ok(outgoing) => {
- let encrypted_data: Vec = row.get(1).unwrap();
- match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
- Ok(data) => {
- msgs.push(
- (
- outgoing,
- data
- )
- )
- },
- Err(e) => print_error!(e)
- }
- }
- Err(_) => {}
- }
-
- }
+ if let Ok(mut stmt) = db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
+ let mut rows = stmt.query([]).unwrap();
+ if let Ok(Some(row)) = rows.next() {
+ let total: usize = row.get(0).unwrap();
+ if offset < total {
+ if offset+count >= total {
+ count = total-offset;
+ }
+ let mut stmt = db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)).unwrap();
+ let mut rows = stmt.query([]).unwrap();
+ let mut msgs = Vec::new();
+ while let Ok(Some(row)) = rows.next() {
+ let encrypted_outgoing: Vec = row.get(0).unwrap();
+ match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
+ Ok(outgoing) => {
+ match byte_to_bool(outgoing[0]) {
+ Ok(outgoing) => {
+ let encrypted_data: Vec = row.get(1).unwrap();
+ match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
+ Ok(data) => msgs.push((outgoing, data)),
Err(e) => print_error!(e)
}
}
- Some(msgs)
- }
- Err(e) => {
- print_error!(e);
- None
+ Err(_) => {}
}
+
}
+ Err(e) => print_error!(e)
}
- } else {
- None
}
- Err(_) => None
+ return Some(msgs);
}
}
- Err(e) => {
- print_error!(e);
- None
- }
}
}
- Err(e) => {
- print_error!(e);
- None
- }
+ Err(e) => print_error!(e)
}
+ None
}
#[allow(unused_must_use)]
@@ -345,6 +326,29 @@ impl Identity {
db.update(DBKeys::USE_PADDING, &encrypted_use_padding)
}
+ pub fn store_avatar(&self, avatar: &[u8]) -> Result {
+ let db = Connection::open(self.get_database_path())?;
+ db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (uuid BLOB PRIMARY KEY, data BLOB)", AVATARS_TABLE), [])?;
+ let uuid = Uuid::new_v4();
+ let encrypted_avatar = crypto::encrypt_data(avatar, &self.master_key).unwrap();
+ db.execute(&format!("INSERT INTO {} (uuid, data) VALUES (?1, ?2)", AVATARS_TABLE), params![&uuid.as_bytes()[..], encrypted_avatar])?;
+ Ok(uuid)
+ }
+
+ pub fn get_avatar(&self, avatar_uuid: &Uuid) -> Option> {
+ let db = Connection::open(self.get_database_path()).ok()?;
+ let mut stmt = db.prepare(&format!("SELECT data FROM {} WHERE uuid=?", AVATARS_TABLE)).unwrap();
+ let mut rows = stmt.query(params![&avatar_uuid.as_bytes()[..]]).unwrap();
+ let encrypted_avatar: Vec = rows.next().ok()??.get(0).unwrap();
+ match crypto::decrypt_data(&encrypted_avatar, &self.master_key) {
+ Ok(avatar) => Some(avatar),
+ Err(e) => {
+ print_error!(e);
+ None
+ }
+ }
+ }
+
pub fn zeroize(&mut self){
self.master_key.zeroize();
self.keypair.secret.zeroize();
@@ -490,4 +494,19 @@ impl Identity {
Err(e) => Err(e.to_string())
}
}
+
+ pub fn set_identity_avatar(database_folder: &str, avatar: &[u8]) -> Result {
+ let db = KeyValueTable::new(&get_database_path(database_folder), MAIN_TABLE)?;
+ db.upsert(DBKeys::AVATAR, avatar)
+ }
+
+ pub fn remove_identity_avatar(database_folder: &str) -> Result {
+ let db = KeyValueTable::new(&get_database_path(database_folder), MAIN_TABLE)?;
+ db.del(DBKeys::AVATAR)
+ }
+
+ pub fn get_identity_avatar(database_folder: &str) -> Result, rusqlite::Error> {
+ let db = KeyValueTable::new(&get_database_path(database_folder), MAIN_TABLE)?;
+ db.get(DBKeys::AVATAR)
+ }
}
diff --git a/app/src/main/native/src/key_value_table.rs b/app/src/main/native/src/key_value_table.rs
index b0556bd..e478233 100644
--- a/app/src/main/native/src/key_value_table.rs
+++ b/app/src/main/native/src/key_value_table.rs
@@ -22,13 +22,13 @@ impl<'a> KeyValueTable<'a> {
None => Err(rusqlite::Error::QueryReturnedNoRows)
}
}
- /*pub fn del(&self, key: &str) -> Result {
- self.db.execute(&format!("DELETE FROM {} WHERE key=\"{}\"", self.table_name, key), NO_PARAMS)
- }*/
+ pub fn del(&self, key: &str) -> Result {
+ self.db.execute(&format!("DELETE FROM {} WHERE key=\"{}\"", self.table_name, key), [])
+ }
pub fn update(&self, key: &str, value: &[u8]) -> Result {
self.db.execute(&format!("UPDATE {} SET value=? WHERE key=\"{}\"", self.table_name, key), params![value])
}
- /*pub fn upsert(&self, key: &str, value: &[u8]) -> Result {
+ pub fn upsert(&self, key: &str, value: &[u8]) -> Result {
self.db.execute(&format!("INSERT INTO {} (key, value) VALUES(?1, ?2) ON CONFLICT(key) DO UPDATE SET value=?3", self.table_name), params![key, value, value])
- }*/
-}
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/native/src/lib.rs b/app/src/main/native/src/lib.rs
index e319373..aa5f1b4 100644
--- a/app/src/main/native/src/lib.rs
+++ b/app/src/main/native/src/lib.rs
@@ -3,10 +3,9 @@ mod identity;
mod crypto;
mod utils;
-use std::{convert::TryInto, str::FromStr, fmt::Display, sync::{Mutex}};
+use std::{convert::TryInto, fmt::Display, str::FromStr, sync::{Mutex}};
use lazy_static::lazy_static;
use uuid::Uuid;
-use log::*;
use android_log;
use identity::{Identity, Contact};
use crate::crypto::{HandshakeKeys, ApplicationKeys};
@@ -24,6 +23,13 @@ fn jstring_to_string(env: JNIEnv, input: JString) -> String {
String::from(env.get_string(input).unwrap())
}
+fn jstring_to_uuid(env: JNIEnv, input: JString) -> Option {
+ match env.get_string(input) {
+ Ok(uuid) => Some(Uuid::from_str(&String::from(uuid)).unwrap()),
+ Err(_) => None
+ }
+}
+
fn jboolean_to_bool(input: jboolean) -> bool {
input == 1
}
@@ -32,14 +38,23 @@ fn bool_to_jboolean(input: bool) -> u8 {
if input { 1 } else { 0 }
}
+fn result_to_jboolean(result: Result) -> jboolean {
+ match result {
+ Ok(_) => 1,
+ Err(e) => {
+ print_error!(e);
+ 0
+ }
+ }
+}
+
fn slice_to_jvalue<'a>(env: JNIEnv, input: &'a [u8]) -> JValue<'a> {
JValue::Object(env.byte_array_from_slice(input).unwrap().into())
}
-#[allow(unused_must_use)]
-fn log_error(e: T) {
- android_log::init("AIRA Native");
- error!("Error: {}", e)
+#[no_mangle]
+pub extern fn Java_sushi_hardcore_aira_LoginActivity_00024Companion_initLogging(_: JNIEnv, _: JClass) -> jboolean {
+ bool_to_jboolean(android_log::init("AIRA Native").is_ok())
}
#[allow(non_snake_case)]
@@ -53,7 +68,7 @@ pub extern fn Java_sushi_hardcore_aira_CreateIdentityFragment_createNewIdentity(
1
}
Err(e) => {
- log_error(e);
+ print_error!(e);
0
}
}
@@ -65,7 +80,7 @@ pub extern fn Java_sushi_hardcore_aira_LoginActivity_getIdentityName(env: JNIEnv
*match Identity::get_identity_name(&jstring_to_string(env, database_folder)) {
Ok(name) => *env.new_string(name).unwrap(),
Err(e) => {
- log_error(e);
+ print_error!(e);
JObject::null()
}
}
@@ -76,7 +91,7 @@ pub extern fn Java_sushi_hardcore_aira_AIRADatabase_isIdentityProtected(env: JNI
match Identity::is_protected(jstring_to_string(env, database_folder)) {
Ok(is_protected) => bool_to_jboolean(is_protected),
Err(e) => {
- log_error(e);
+ print_error!(e);
0
}
}
@@ -91,7 +106,7 @@ pub extern fn Java_sushi_hardcore_aira_AIRADatabase_loadIdentity(env: JNIEnv, _:
1
}
Err(e) => {
- log_error(e);
+ print_error!(e);
0
}
}
@@ -104,12 +119,36 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_changePassword(env: JNIEnv, _: JCla
match Identity::change_password(database_folder, env.convert_byte_array(old_password).ok().as_deref(), env.convert_byte_array(new_password).ok().as_deref()) {
Ok(success) => bool_to_jboolean(success),
Err(e) => {
- log_error(e);
+ print_error!(e);
0
}
}
}
+#[allow(non_snake_case)]
+#[no_mangle]
+pub fn Java_sushi_hardcore_aira_AIRADatabase_setIdentityAvatar(env: JNIEnv, _: JClass, database_folder: JString, avatar: jbyteArray) -> jboolean {
+ result_to_jboolean(Identity::set_identity_avatar(&jstring_to_string(env, database_folder), &env.convert_byte_array(avatar).unwrap()))
+}
+
+#[allow(non_snake_case)]
+#[no_mangle]
+pub fn Java_sushi_hardcore_aira_AIRADatabase_removeIdentityAvatar(env: JNIEnv, _: JClass, database_folder: JString) -> jboolean {
+ result_to_jboolean(Identity::remove_identity_avatar(&jstring_to_string(env, database_folder)))
+}
+
+#[allow(non_snake_case)]
+#[no_mangle]
+pub fn Java_sushi_hardcore_aira_AIRADatabase_getIdentityAvatar(env: JNIEnv, _: JClass, database_folder: JString) -> jbyteArray {
+ match Identity::get_identity_avatar(&jstring_to_string(env, database_folder)) {
+ Ok(avatar) => env.byte_array_from_slice(&avatar).unwrap(),
+ Err(e) => {
+ print_error!(e);
+ *JObject::null()
+ }
+ }
+}
+
#[allow(non_snake_case)]
#[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_getIdentityPublicKey(env: JNIEnv, _: JClass) -> jbyteArray {
@@ -172,16 +211,27 @@ pub fn Java_sushi_hardcore_aira_background_1service_AIRAService_releaseIdentity(
fn new_contact(env: JNIEnv, contact: Contact) -> JObject {
let contact_class = env.find_class("sushi/hardcore/aira/background_service/Contact").unwrap();
- env.new_object(contact_class, "(Ljava/lang/String;[BLjava/lang/String;ZZ)V", &[JValue::Object(*env.new_string(contact.uuid.to_string()).unwrap()), slice_to_jvalue(env, &contact.public_key), JValue::Object(*env.new_string(contact.name).unwrap()), JValue::Bool(bool_to_jboolean(contact.verified)), JValue::Bool(bool_to_jboolean(contact.seen))]).unwrap()
+ let avatar_uuid = match contact.avatar {
+ Some(uuid) => JValue::Object(*env.new_string(uuid.to_string()).unwrap()),
+ None => JValue::Object(JObject::null())
+ };
+ env.new_object(contact_class, "(Ljava/lang/String;[BLjava/lang/String;Ljava/lang/String;ZZ)V", &[
+ JValue::Object(*env.new_string(contact.uuid.to_string()).unwrap()),
+ slice_to_jvalue(env, &contact.public_key),
+ JValue::Object(*env.new_string(contact.name).unwrap()),
+ avatar_uuid,
+ JValue::Bool(bool_to_jboolean(contact.verified)),
+ JValue::Bool(bool_to_jboolean(contact.seen))
+ ]).unwrap()
}
#[allow(non_snake_case)]
#[no_mangle]
-pub fn Java_sushi_hardcore_aira_AIRADatabase_addContact(env: JNIEnv, _: JClass, name: JString, public_key: jbyteArray) -> jobject {
- *match loaded_identity.lock().unwrap().as_ref().unwrap().add_contact(jstring_to_string(env, name), env.convert_byte_array(public_key).unwrap().try_into().unwrap()) {
+pub fn Java_sushi_hardcore_aira_AIRADatabase_addContact(env: JNIEnv, _: JClass, name: JString, avatarUuid: JString, public_key: jbyteArray) -> jobject {
+ *match loaded_identity.lock().unwrap().as_ref().unwrap().add_contact(jstring_to_string(env, name), jstring_to_uuid(env, avatarUuid), env.convert_byte_array(public_key).unwrap().try_into().unwrap()) {
Ok(contact) => new_contact(env, contact),
Err(e) => {
- log_error(e);
+ print_error!(e);
JObject::null()
}
}
@@ -190,13 +240,7 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_addContact(env: JNIEnv, _: JClass,
#[allow(non_snake_case)]
#[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_removeContact(env: JNIEnv, _: JClass, uuid: JString) -> jboolean {
- match loaded_identity.lock().unwrap().as_ref().unwrap().remove_contact(&Uuid::from_str(&jstring_to_string(env, uuid)).unwrap()) {
- Ok(_) => 1,
- Err(e) => {
- log_error(e);
- 0
- }
- }
+ result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().remove_contact(&Uuid::from_str(&jstring_to_string(env, uuid)).unwrap()))
}
#[allow(non_snake_case)]
@@ -220,55 +264,37 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_loadContacts(env: JNIEnv, _: JClass
#[allow(non_snake_case)]
#[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_setVerified(env: JNIEnv, _: JClass, uuid: JString) -> jboolean {
- match loaded_identity.lock().unwrap().as_ref().unwrap().set_verified(&Uuid::from_str(&jstring_to_string(env, uuid)).unwrap()) {
- Ok(_) => 1,
- Err(e) => {
- log_error(e);
- 0
- }
- }
+ result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().set_verified(&jstring_to_uuid(env, uuid).unwrap()))
}
#[allow(non_snake_case)]
#[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_changeContactName(env: JNIEnv, _: JClass, contactUuid: JString, newName: JString) -> jboolean {
- match loaded_identity.lock().unwrap().as_ref().unwrap().change_contact_name(&Uuid::from_str(&jstring_to_string(env, contactUuid)).unwrap(), &jstring_to_string(env, newName)) {
- Ok(_) => 1,
- Err(e) => {
- log_error(e);
- 0
- }
- }
+ result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().change_contact_name(&jstring_to_uuid(env, contactUuid).unwrap(), &jstring_to_string(env, newName)))
+}
+
+#[allow(non_snake_case)]
+#[no_mangle]
+pub fn Java_sushi_hardcore_aira_AIRADatabase_setContactAvatar(env: JNIEnv, _: JClass, contactUuid: JString, avatarUuid: JString) -> jboolean {
+ result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().set_contact_avatar(&jstring_to_uuid(env, contactUuid).unwrap(), jstring_to_uuid(env, avatarUuid).as_ref()))
}
#[allow(non_snake_case)]
#[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_setContactSeen(env: JNIEnv, _: JClass, contactUuid: JString, seen: jboolean) -> jboolean {
- match loaded_identity.lock().unwrap().as_ref().unwrap().set_contact_seen(&Uuid::from_str(&jstring_to_string(env, contactUuid)).unwrap(), jboolean_to_bool(seen)) {
- Ok(_) => 1,
- Err(e) => {
- log_error(e);
- 0
- }
- }
+ result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().set_contact_seen(&jstring_to_uuid(env, contactUuid).unwrap(), jboolean_to_bool(seen)))
}
#[allow(non_snake_case)]
#[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_storeMsg(env: JNIEnv, _: JClass, contactUuid: JString, outgoing: jboolean, data: jbyteArray) -> jboolean {
- match loaded_identity.lock().unwrap().as_ref().unwrap().store_msg(&Uuid::from_str(&jstring_to_string(env, contactUuid)).unwrap(), jboolean_to_bool(outgoing), &env.convert_byte_array(data).unwrap()) {
- Ok(_) => 1,
- Err(e) => {
- log_error(e);
- 0
- }
- }
+ result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().store_msg(&jstring_to_uuid(env, contactUuid).unwrap(), jboolean_to_bool(outgoing), &env.convert_byte_array(data).unwrap()))
}
#[allow(non_snake_case)]
#[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_loadMsgs(env: JNIEnv, _: JClass, uuid: JString, offset: jint, count: jint) -> jobject {
- *match loaded_identity.lock().unwrap().as_ref().unwrap().load_msgs(&Uuid::from_str(&jstring_to_string(env, uuid)).unwrap(), offset as usize, count as usize) {
+ *match loaded_identity.lock().unwrap().as_ref().unwrap().load_msgs(&jstring_to_uuid(env, uuid).unwrap(), offset as usize, count as usize) {
Some(msgs) => {
let array_list_class = env.find_class("java/util/ArrayList").unwrap();
let array_list = env.new_object(array_list_class, "(I)V", &[JValue::Int(msgs.len().try_into().unwrap())]).unwrap();
@@ -287,11 +313,7 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_loadMsgs(env: JNIEnv, _: JClass, uu
#[allow(non_snake_case)]
#[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_storeFile(env: JNIEnv, _: JClass, contactUuid: JString, data: jbyteArray) -> jbyteArray {
- let contact_uuid = match env.get_string(contactUuid) {
- Ok(uuid) => Some(Uuid::from_str(&String::from(uuid)).unwrap()),
- Err(_) => None
- };
- match loaded_identity.lock().unwrap().as_ref().unwrap().store_file(contact_uuid, &env.convert_byte_array(data).unwrap()) {
+ match loaded_identity.lock().unwrap().as_ref().unwrap().store_file(jstring_to_uuid(env, contactUuid), &env.convert_byte_array(data).unwrap()) {
Ok(uuid) => env.byte_array_from_slice(uuid.as_bytes()).unwrap(),
Err(_) => *JObject::null()
}
@@ -309,25 +331,15 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_loadFile(env: JNIEnv, _: JClass, ra
#[allow(non_snake_case)]
#[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_deleteConversation(env: JNIEnv, _: JClass, contactUuid: JString) -> jboolean {
- let contact_uuid = Uuid::from_str(&String::from(env.get_string(contactUuid).unwrap())).unwrap();
- match loaded_identity.lock().unwrap().as_ref().unwrap().delete_conversation(&contact_uuid) {
- Ok(_) => 1,
- Err(e) => {
- log_error(e);
- 0
- }
- }
+ let contact_uuid = jstring_to_uuid(env, contactUuid).unwrap();
+ result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().delete_conversation(&contact_uuid))
}
#[allow(non_snake_case)]
#[no_mangle]
-pub fn Java_sushi_hardcore_aira_AIRADatabase_clearTemporaryFiles(_: JNIEnv, _: JClass) -> jint {
- match loaded_identity.lock().unwrap().as_ref().unwrap().clear_temporary_files() {
- Ok(r) => r.try_into().unwrap(),
- Err(e) => {
- log_error(e);
- 0
- }
+pub fn Java_sushi_hardcore_aira_AIRADatabase_clearCache(_: JNIEnv, _: JClass) {
+ if let Err(e) = loaded_identity.lock().unwrap().as_ref().unwrap().clear_cache() {
+ print_error!(e);
}
}
@@ -338,7 +350,7 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_changeName(env: JNIEnv, _: JClass,
match loaded_identity.lock().unwrap().as_mut().unwrap().change_name(new_name) {
Ok(u) => bool_to_jboolean(u == 1),
Err(e) => {
- log_error(e);
+ print_error!(e);
0
}
}
@@ -353,12 +365,24 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_getUsePadding(_: JNIEnv, _: JClass)
#[allow(non_snake_case)]
#[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_setUsePadding(_: JNIEnv, _: JClass, use_padding: jboolean) -> jboolean {
- match loaded_identity.lock().unwrap().as_mut().unwrap().set_use_padding(jboolean_to_bool(use_padding)) {
- Ok(_) => 1,
- Err(e) => {
- log_error(e);
- 0
- }
+ result_to_jboolean(loaded_identity.lock().unwrap().as_mut().unwrap().set_use_padding(jboolean_to_bool(use_padding)))
+}
+
+#[allow(non_snake_case)]
+#[no_mangle]
+pub fn Java_sushi_hardcore_aira_AIRADatabase_storeAvatar(env: JNIEnv, _: JClass, avatar: jbyteArray) -> jobject {
+ *match loaded_identity.lock().unwrap().as_ref().unwrap().store_avatar(&env.convert_byte_array(avatar).unwrap()) {
+ Ok(uuid) => *env.new_string(uuid.to_string()).unwrap(),
+ Err(_) => JObject::null()
+ }
+}
+
+#[allow(non_snake_case)]
+#[no_mangle]
+pub fn Java_sushi_hardcore_aira_AIRADatabase_getAvatar(env: JNIEnv, _: JClass, avatarUuid: JString) -> jbyteArray {
+ match loaded_identity.lock().unwrap().as_ref().unwrap().get_avatar(&jstring_to_uuid(env, avatarUuid).unwrap()) {
+ Some(buffer) => env.byte_array_from_slice(&buffer).unwrap(),
+ None => *JObject::null()
}
}
diff --git a/app/src/main/native/src/utils.rs b/app/src/main/native/src/utils.rs
index c4074fa..2488ac5 100644
--- a/app/src/main/native/src/utils.rs
+++ b/app/src/main/native/src/utils.rs
@@ -15,9 +15,9 @@ pub fn to_uuid_bytes(bytes: &[u8]) -> Option {
#[macro_export]
macro_rules! print_error {
($arg:tt) => ({
- println!("[{}:{}] {}", file!(), line!(), $arg);
+ log::error!("[{}:{}] {}", file!(), line!(), $arg);
});
($($arg:tt)*) => ({
- println!("[{}:{}] {}", file!(), line!(), format_args!($($arg)*));
+ log::error!("[{}:{}] {}", file!(), line!(), format_args!($($arg)*));
})
}
diff --git a/app/src/main/res/drawable/ic_face.xml b/app/src/main/res/drawable/ic_face.xml
new file mode 100644
index 0000000..ef34eab
--- /dev/null
+++ b/app/src/main/res/drawable/ic_face.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml
index 94cb05f..c44c10b 100644
--- a/app/src/main/res/layout/activity_chat.xml
+++ b/app/src/main/res/layout/activity_chat.xml
@@ -30,16 +30,18 @@
android:layout_height="@dimen/image_button_size"
android:src="@drawable/ic_attach_file"
style="@style/ImageButton"
+ android:contentDescription="@string/send_file"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toStartOf="@id/image_trust_level" />
+ app:layout_constraintEnd_toStartOf="@id/image_trust_level"/>
+ app:layout_constraintEnd_toEndOf="parent"/>
diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml
index 1e8dce3..efcc6dc 100644
--- a/app/src/main/res/layout/activity_login.xml
+++ b/app/src/main/res/layout/activity_login.xml
@@ -8,6 +8,7 @@
+ android:layout_height="match_parent"
+ android:layout_marginHorizontal="50dp"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 89f039e..d848946 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -68,6 +68,7 @@
android:maxLines="1"
android:imeOptions="actionGo"
android:hint="@string/add_peer_ip"
+ android:autofillHints="ip"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -79,6 +80,7 @@
android:layout_height="@dimen/image_button_size"
android:src="@drawable/ic_info"
style="@style/ImageButton"
+ android:contentDescription="@string/show_your_ips"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/edit_peer_ip"
diff --git a/app/src/main/res/layout/adapter_chat_file.xml b/app/src/main/res/layout/adapter_chat_file.xml
index 1ffd92a..a95e6b3 100644
--- a/app/src/main/res/layout/adapter_chat_file.xml
+++ b/app/src/main/res/layout/adapter_chat_file.xml
@@ -19,7 +19,8 @@
android:layout_width="@dimen/image_button_size"
android:layout_height="@dimen/image_button_size"
android:src="@drawable/ic_save"
- style="@style/ImageButton"/>
+ style="@style/ImageButton"
+ android:contentDescription="@string/download" />
-
+ app:layout_constraintStart_toEndOf="@id/avatar"/>
@@ -53,6 +54,7 @@
android:layout_height="15dp"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_arrow_forward"
+ android:contentDescription="@string/clickable_indicator"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
diff --git a/app/src/main/res/layout/avatar.xml b/app/src/main/res/layout/avatar.xml
new file mode 100644
index 0000000..5d70d2b
--- /dev/null
+++ b/app/src/main/res/layout/avatar.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/change_avatar_dialog.xml b/app/src/main/res/layout/change_avatar_dialog.xml
new file mode 100644
index 0000000..fb51d2e
--- /dev/null
+++ b/app/src/main/res/layout/change_avatar_dialog.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_info.xml b/app/src/main/res/layout/dialog_info.xml
index 7fea2f2..7d49144 100644
--- a/app/src/main/res/layout/dialog_info.xml
+++ b/app/src/main/res/layout/dialog_info.xml
@@ -6,8 +6,8 @@
android:layout_height="match_parent"
android:paddingHorizontal="20dp">
-
+ android:inputType="textPassword"
+ android:autofillHints="password"/>
+ android:inputType="textPassword"
+ android:autofillHints="password"/>
+ android:inputType="textPassword"
+ android:autofillHints="password"/>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_create_identity.xml b/app/src/main/res/layout/fragment_create_identity.xml
index b095092..c1b394d 100644
--- a/app/src/main/res/layout/fragment_create_identity.xml
+++ b/app/src/main/res/layout/fragment_create_identity.xml
@@ -1,60 +1,69 @@
-
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_gravity="center_vertical">
-
+ android:text="@string/create_identity_test"
+ style="@style/Label"/>
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml
index d5d7f0a..5a5b520 100644
--- a/app/src/main/res/layout/fragment_login.xml
+++ b/app/src/main/res/layout/fragment_login.xml
@@ -1,54 +1,46 @@
-
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_gravity="center_vertical">
-
+
+
+ android:textAlignment="center"
+ android:textStyle="bold"
+ android:textSize="25sp"
+ android:layout_marginTop="10dp"/>
-
-
-
+
-
+
-
+
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/profile_toolbar.xml b/app/src/main/res/layout/profile_toolbar.xml
index cf7b69e..86fbbe7 100644
--- a/app/src/main/res/layout/profile_toolbar.xml
+++ b/app/src/main/res/layout/profile_toolbar.xml
@@ -12,8 +12,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/chat_activity.xml b/app/src/main/res/menu/chat_activity.xml
index cbab399..80542de 100644
--- a/app/src/main/res/menu/chat_activity.xml
+++ b/app/src/main/res/menu/chat_activity.xml
@@ -33,8 +33,8 @@
android:title="@string/details"/>
+ android:title="@string/refresh_profile"/>
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 8442b54..f7b0123 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index bb951f8..541ae46 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -2,4 +2,5 @@
30dp
5sp
+ 130dp
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4541b19..0683d5b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -77,14 +77,28 @@
Settings
Log out
File extraction failed
- Fingerprint copied to clipboard
+ Copied to clipboard !
Identity
About
AIRA version
- Refresh name
+ Refresh profile
Security
Use PSEC padding
PSEC padding obfuscates the length of your messages but uses more network bandwidth.
Is contact:
Is verified:
+ Avatar cannot be larger than 10MB.
+ Your avatar:
+ Set a new one
+ Remove
+ Choose avatar
+ Send a messageā¦
+
+
+ Send file
+ Trust level indicator
+ Send message
+ Show your IPs
+ Clickable indicator
+ Avatar
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 8f9bbee..47c5fc0 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -22,4 +22,8 @@
- false
- true
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 1f45f48..0be71f0 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -3,6 +3,12 @@
+
+