This commit is contained in:
Matéo Duparc 2021-05-27 20:15:03 +02:00
parent 004277dc42
commit c18bb1cb60
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
44 changed files with 977 additions and 553 deletions

View File

@ -47,12 +47,11 @@ dependencies {
implementation "androidx.fragment:fragment-ktx:1.3.4" 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.legacy:legacy-support-v4:1.0.0'
//testImplementation 'junit:junit:4.13.1'
implementation 'net.i2p.crypto:eddsa:0.3.0' implementation 'net.i2p.crypto:eddsa:0.3.0'
implementation "org.whispersystems:curve25519-android:0.5.0" implementation "org.whispersystems:curve25519-android:0.5.0"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" 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'
} }

View File

@ -37,7 +37,9 @@
<data android:mimeType="*/*"/> <data android:mimeType="*/*"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".LoginActivity"> <activity
android:name=".LoginActivity"
android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>

View File

@ -5,22 +5,34 @@ import sushi.hardcore.aira.background_service.Contact
object AIRADatabase { object AIRADatabase {
external fun isIdentityProtected(databaseFolder: String): Boolean external fun isIdentityProtected(databaseFolder: String): Boolean
external fun loadIdentity(databaseFolder: String, password: ByteArray?): 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 removeContact(uuid: String): Boolean
external fun loadContacts(): ArrayList<Contact>? external fun loadContacts(): ArrayList<Contact>?
external fun setVerified(uuid: String): Boolean external fun setVerified(uuid: String): Boolean
external fun setContactSeen(contactUuid: String, seen: Boolean): Boolean external fun setContactSeen(contactUuid: String, seen: Boolean): Boolean
external fun changeContactName(contactUuid: String, newName: String): 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 storeMsg(contactUuid: String, outgoing: Boolean, data: ByteArray): Boolean
external fun storeFile(contactUuid: String?, data: ByteArray): ByteArray? external fun storeFile(contactUuid: String?, data: ByteArray): ByteArray?
external fun loadMsgs(uuid: String, offset: Int, count: Int): ArrayList<ChatItem>? external fun loadMsgs(uuid: String, offset: Int, count: Int): ArrayList<ChatItem>?
external fun loadFile(rawUuid: ByteArray): ByteArray? external fun loadFile(rawUuid: ByteArray): ByteArray?
external fun deleteConversation(contactUuid: String): Boolean external fun deleteConversation(contactUuid: String): Boolean
external fun clearTemporaryFiles(): Int external fun clearCache()
external fun getIdentityPublicKey(): ByteArray external fun getIdentityPublicKey(): ByteArray
external fun getIdentityFingerprint(): String external fun getIdentityFingerprint(): String
external fun getUsePadding(): Boolean external fun getUsePadding(): Boolean
external fun setUsePadding(usePadding: Boolean): 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 changeName(newName: String): Boolean
external fun changePassword(databaseFolder: String, oldPassword: ByteArray?, newPassword: ByteArray?): 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)
}
}
} }

View File

@ -29,6 +29,7 @@ class ChatActivity : ServiceBoundActivity() {
private lateinit var binding: ActivityChatBinding private lateinit var binding: ActivityChatBinding
private var sessionId = -1 private var sessionId = -1
private lateinit var sessionName: String private lateinit var sessionName: String
private var avatar: ByteArray? = null
private lateinit var chatAdapter: ChatAdapter private lateinit var chatAdapter: ChatAdapter
private var lastLoadedMessageOffset = 0 private var lastLoadedMessageOffset = 0
private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
@ -48,7 +49,7 @@ class ChatActivity : ServiceBoundActivity() {
if (sessionId != -1) { if (sessionId != -1) {
intent.getStringExtra("sessionName")?.let { name -> intent.getStringExtra("sessionName")?.let { name ->
sessionName = name sessionName = name
binding.toolbar.textAvatar.setLetterFrom(name) binding.toolbar.avatar.setTextAvatar(name)
binding.toolbar.title.text = name binding.toolbar.title.text = name
chatAdapter = ChatAdapter(this@ChatActivity, ::onClickSaveFile) chatAdapter = ChatAdapter(this@ChatActivity, ::onClickSaveFile)
binding.recyclerChat.apply { binding.recyclerChat.apply {
@ -94,7 +95,18 @@ class ChatActivity : ServiceBoundActivity() {
airaService = binder.getService() airaService = binder.getService()
chatAdapter.clear() 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) displayIconTrustLevel(true, contact.verified)
loadMsgs(contact.uuid) loadMsgs(contact.uuid)
} }
@ -126,6 +138,7 @@ class ChatActivity : ServiceBoundActivity() {
val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.hideSoftInputFromWindow(binding.editMessage.windowToken, 0) inputManager.hideSoftInputFromWindow(binding.editMessage.windowToken, 0)
binding.bottomPanel.visibility = View.GONE binding.bottomPanel.visibility = View.GONE
invalidateOptionsMenu()
} }
} }
} }
@ -133,7 +146,19 @@ class ChatActivity : ServiceBoundActivity() {
if (this@ChatActivity.sessionId == sessionId) { if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread { runOnUiThread {
sessionName = name 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 false
} }
} }
override fun onAskLargeFiles(sessionId: Int, name: String, filesReceiver: FilesReceiver): Boolean { override fun onAskLargeFiles(sessionId: Int, name: String, filesReceiver: FilesReceiver): Boolean {
return if (this@ChatActivity.sessionId == sessionId) { return if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread { runOnUiThread {
@ -202,15 +226,17 @@ class ChatActivity : ServiceBoundActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.chat_activity, menu) menuInflater.inflate(R.menu.chat_activity, menu)
val contact = airaService.contacts[sessionId] val contact = airaService.contacts[sessionId]
val isOnline = airaService.isOnline(sessionId)
menu.findItem(R.id.delete_conversation).isVisible = contact != null 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 menu.findItem(R.id.remove_contact).isVisible = contact != null
if (contact == null) { if (contact == null) {
menu.findItem(R.id.verify).isVisible = false menu.findItem(R.id.verify).isVisible = false
} else { } else {
menu.findItem(R.id.verify).isVisible = !contact.verified 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 return true
} }
@ -232,7 +258,7 @@ class ChatActivity : ServiceBoundActivity() {
true true
} }
R.id.remove_contact -> { R.id.remove_contact -> {
AlertDialog.Builder(this) AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.warning) .setTitle(R.string.warning)
.setMessage(R.string.ask_remove_contact) .setMessage(R.string.ask_remove_contact)
.setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->
@ -252,7 +278,7 @@ class ChatActivity : ServiceBoundActivity() {
val dialogBinding = DialogFingerprintsBinding.inflate(layoutInflater) val dialogBinding = DialogFingerprintsBinding.inflate(layoutInflater)
dialogBinding.textLocalFingerprint.text = localFingerprint dialogBinding.textLocalFingerprint.text = localFingerprint
dialogBinding.textPeerFingerprint.text = peerFingerprint dialogBinding.textPeerFingerprint.text = peerFingerprint
AlertDialog.Builder(this) AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.verifying_contact) .setTitle(R.string.verifying_contact)
.setView(dialogBinding.root) .setView(dialogBinding.root)
.setPositiveButton(R.string.they_match) { _, _ -> .setPositiveButton(R.string.they_match) { _, _ ->
@ -267,7 +293,7 @@ class ChatActivity : ServiceBoundActivity() {
true true
} }
R.id.delete_conversation -> { R.id.delete_conversation -> {
AlertDialog.Builder(this) AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.warning) .setTitle(R.string.warning)
.setMessage(R.string.ask_delete_conversation) .setMessage(R.string.ask_delete_conversation)
.setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->
@ -279,8 +305,8 @@ class ChatActivity : ServiceBoundActivity() {
.show() .show()
true true
} }
R.id.refresh_name -> { R.id.refresh_profile -> {
airaService.sendTo(sessionId, Protocol.askName()) airaService.sendTo(sessionId, Protocol.askProfileInfo())
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
@ -315,35 +341,40 @@ class ChatActivity : ServiceBoundActivity() {
private fun showSessionInfo() { private fun showSessionInfo() {
val contact = airaService.contacts[sessionId] val contact = airaService.contacts[sessionId]
val session = airaService.sessions[sessionId] val session = airaService.sessions[sessionId]
val publicKey = contact?.publicKey ?: session?.peerPublicKey (contact?.publicKey ?: session?.peerPublicKey)?.let { publicKey -> //can be null if disconnected and not a contact
val dialogBinding = DialogInfoBinding.inflate(layoutInflater) val dialogBinding = DialogInfoBinding.inflate(layoutInflater)
dialogBinding.textAvatar.setLetterFrom(sessionName) if (avatar == null) {
dialogBinding.textFingerprint.text = StringUtils.beautifyFingerprint(generateFingerprint(publicKey!!)) dialogBinding.avatar.setTextAvatar(sessionName)
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 { } else {
R.string.incoming dialogBinding.avatar.setImageAvatar(avatar!!)
}) }
} dialogBinding.textFingerprint.text = StringUtils.beautifyFingerprint(generateFingerprint(publicKey))
dialogBinding.textIsContact.text = getString(if (contact == null) { if (session == null) {
dialogBinding.fieldIsVerified.visibility = View.GONE dialogBinding.onlineFields.visibility = View.GONE
R.string.no
} else {
dialogBinding.textIsVerified.text = getString(if (contact.verified) {
R.string.yes
} else { } 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 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, R.style.CustomAlertDialog)
}) .setTitle(sessionName)
AlertDialog.Builder(this) .setView(dialogBinding.root)
.setTitle(sessionName) .setPositiveButton(R.string.ok, null)
.setView(dialogBinding.root) .show()
.setPositiveButton(R.string.ok, null) }
.show()
} }
} }

View File

@ -10,6 +10,7 @@ object Constants {
const val fileSizeLimit = 32760000 const val fileSizeLimit = 32760000
const val MSG_LOADING_COUNT = 20 const val MSG_LOADING_COUNT = 20
const val FILE_CHUNK_SIZE = 1023996 const val FILE_CHUNK_SIZE = 1023996
const val MAX_AVATAR_SIZE = 10000000
private const val databaseName = "AIRA.db" private const val databaseName = "AIRA.db"
fun getDatabaseFolder(context: Context): String { fun getDatabaseFolder(context: Context): String {

View File

@ -1,5 +1,6 @@
package sushi.hardcore.aira package sushi.hardcore.aira
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -7,25 +8,41 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import sushi.hardcore.aira.databinding.FragmentCreateIdentityBinding 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 private external fun createNewIdentity(databaseFolder: String, name: String, password: ByteArray?): Boolean
companion object { companion object {
fun newInstance(): CreateIdentityFragment { fun newInstance(activity: AppCompatActivity): CreateIdentityFragment {
return 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 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 { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentCreateIdentityBinding.inflate(inflater, container, false) binding = FragmentCreateIdentityBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.buttonSetAvatar.setOnClickListener {
avatarPicker.launch()
}
binding.checkboxEnablePassword.setOnCheckedChangeListener { _, isChecked -> binding.checkboxEnablePassword.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) { if (isChecked) {
binding.editPassword.visibility = View.VISIBLE binding.editPassword.visibility = View.VISIBLE
@ -63,7 +80,7 @@ class CreateIdentityFragment : Fragment() {
val intent = Intent(activity, MainActivity::class.java) val intent = Intent(activity, MainActivity::class.java)
intent.putExtra("identityName", identityName) intent.putExtra("identityName", identityName)
startActivity(intent) startActivity(intent)
activity?.finish() activity.finish()
} else { } else {
Toast.makeText(activity, R.string.identity_create_failed, Toast.LENGTH_SHORT).show() Toast.makeText(activity, R.string.identity_create_failed, Toast.LENGTH_SHORT).show()
} }

View File

@ -11,8 +11,10 @@ class LoginActivity : AppCompatActivity() {
private external fun getIdentityName(databaseFolder: String): String? private external fun getIdentityName(databaseFolder: String): String?
companion object { companion object {
private external fun initLogging()
init { init {
System.loadLibrary("aira") System.loadLibrary("aira")
initLogging()
} }
} }
@ -33,7 +35,7 @@ class LoginActivity : AppCompatActivity() {
finish() finish()
} else if (name != null && !isProtected) { } else if (name != null && !isProtected) {
if (AIRADatabase.loadIdentity(databaseFolder, null)) { if (AIRADatabase.loadIdentity(databaseFolder, null)) {
AIRADatabase.clearTemporaryFiles() AIRADatabase.clearCache()
startMainActivity(name) startMainActivity(name)
} else { } else {
Toast.makeText(this, R.string.identity_load_failed, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.identity_load_failed, Toast.LENGTH_SHORT).show()
@ -42,7 +44,8 @@ class LoginActivity : AppCompatActivity() {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.add( .add(
R.id.fragment_container, if (name == null) { R.id.fragment_container, if (name == null) {
CreateIdentityFragment.newInstance() AIRADatabase.removeIdentityAvatar(databaseFolder)
CreateIdentityFragment.newInstance(this)
} else { } else {
LoginFragment.newInstance(name) LoginFragment.newInstance(name)
} }

View File

@ -5,11 +5,9 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import sushi.hardcore.aira.databinding.FragmentLoginBinding import sushi.hardcore.aira.databinding.FragmentLoginBinding
import sushi.hardcore.aira.widgets.TextAvatar
class LoginFragment : Fragment() { class LoginFragment : Fragment() {
companion object { companion object {
@ -31,11 +29,17 @@ class LoginFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
arguments?.let { bundle -> arguments?.let { bundle ->
bundle.getString(NAME_ARG)?.let { name -> bundle.getString(NAME_ARG)?.let { name ->
view.findViewById<TextAvatar>(R.id.text_avatar).setLetterFrom(name) val databaseFolder = Constants.getDatabaseFolder(requireContext())
view.findViewById<TextView>(R.id.text_identity_name).text = name val avatar = AIRADatabase.getIdentityAvatar(databaseFolder)
if (avatar == null) {
binding.avatar.setTextAvatar(name)
} else {
binding.avatar.setImageAvatar(avatar)
}
binding.textIdentityName.text = name
binding.buttonLogin.setOnClickListener { binding.buttonLogin.setOnClickListener {
if (AIRADatabase.loadIdentity(Constants.getDatabaseFolder(requireContext()), binding.editPassword.text.toString().toByteArray())) { if (AIRADatabase.loadIdentity(databaseFolder, binding.editPassword.text.toString().toByteArray())) {
AIRADatabase.clearTemporaryFiles() AIRADatabase.clearCache()
val intent = Intent(activity, MainActivity::class.java) val intent = Intent(activity, MainActivity::class.java)
intent.putExtra("identityName", name) intent.putExtra("identityName", name)
startActivity(intent) startActivity(intent)

View File

@ -58,6 +58,12 @@ class MainActivity : ServiceBoundActivity() {
onlineSessionAdapter.setName(sessionId, name) onlineSessionAdapter.setName(sessionId, name)
} }
} }
override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
runOnUiThread {
onlineSessionAdapter.setAvatar(sessionId, avatar)
}
}
override fun onNewMessage(sessionId: Int, data: ByteArray): Boolean { override fun onNewMessage(sessionId: Int, data: ByteArray): Boolean {
runOnUiThread { runOnUiThread {
onlineSessionAdapter.setSeen(sessionId, false) onlineSessionAdapter.setSeen(sessionId, false)
@ -165,7 +171,7 @@ class MainActivity : ServiceBoundActivity() {
} }
val dialogBinding = DialogIpAddressesBinding.inflate(layoutInflater) val dialogBinding = DialogIpAddressesBinding.inflate(layoutInflater)
dialogBinding.textIpAddresses.text = ipAddresses.substring(0, ipAddresses.length-1) //remove last LF 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) .setTitle(R.string.your_addresses)
.setView(dialogBinding.root) .setView(dialogBinding.root)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
@ -194,7 +200,7 @@ class MainActivity : ServiceBoundActivity() {
} }
R.id.close -> { R.id.close -> {
if (isServiceInitialized()) { if (isServiceInitialized()) {
AlertDialog.Builder(this) AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.warning) .setTitle(R.string.warning)
.setMessage(R.string.ask_log_out) .setMessage(R.string.ask_log_out)
.setPositiveButton(R.string.yes) { _, _ -> .setPositiveButton(R.string.yes) { _, _ ->
@ -210,7 +216,7 @@ class MainActivity : ServiceBoundActivity() {
true true
} }
R.id.remove_contact -> { R.id.remove_contact -> {
AlertDialog.Builder(this) AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.warning) .setTitle(R.string.warning)
.setMessage(R.string.ask_remove_contacts) .setMessage(R.string.ask_remove_contacts)
.setPositiveButton(R.string.delete) { _, _ -> .setPositiveButton(R.string.delete) { _, _ ->
@ -253,7 +259,12 @@ class MainActivity : ServiceBoundActivity() {
} }
private fun initToolbar(identityName: String) { 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 binding.toolbar.title.text = identityName
} }
@ -286,7 +297,7 @@ class MainActivity : ServiceBoundActivity() {
private fun loadContacts() { private fun loadContacts() {
if (offlineSessionAdapter != null) { if (offlineSessionAdapter != null) {
for ((sessionId, contact) in airaService.contacts) { 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 seen = !airaService.notSeen.contains(sessionId)
val contact = airaService.contacts[sessionId] val contact = airaService.contacts[sessionId]
if (contact == null) { 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 { } 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) offlineSessionAdapter?.remove(sessionId)
} }
} }
@ -311,7 +322,7 @@ class MainActivity : ServiceBoundActivity() {
private fun launchChatActivity(session: Session) { private fun launchChatActivity(session: Session) {
startActivity(Intent(this, ChatActivity::class.java).apply { startActivity(Intent(this, ChatActivity::class.java).apply {
putExtra("sessionId", session.sessionId) putExtra("sessionId", session.sessionId)
putExtra("sessionName", session.name) putExtra("sessionName", airaService.getNameOf(session.sessionId))
}) })
} }
@ -338,7 +349,7 @@ class MainActivity : ServiceBoundActivity() {
} else { } else {
getString(R.string.ask_send_multiple_files, uris!!.size, session.name ?: session.ip) 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) .setTitle(R.string.warning)
.setMessage(msg) .setMessage(msg)
.setPositiveButton(R.string.yes) { _, _ -> .setPositiveButton(R.string.yes) { _, _ ->

View File

@ -1,6 +1,7 @@
package sushi.hardcore.aira package sushi.hardcore.aira
import android.content.* import android.content.*
import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.view.MenuItem import android.view.MenuItem
@ -13,21 +14,47 @@ import androidx.preference.EditTextPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat 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.background_service.AIRAService
import sushi.hardcore.aira.databinding.ActivitySettingsBinding import sushi.hardcore.aira.databinding.ActivitySettingsBinding
import sushi.hardcore.aira.databinding.ChangeAvatarDialogBinding
import sushi.hardcore.aira.utils.AvatarPicker
import sushi.hardcore.aira.utils.StringUtils import sushi.hardcore.aira.utils.StringUtils
class SettingsActivity: AppCompatActivity() { class SettingsActivity: AppCompatActivity() {
class MySettingsFragment : PreferenceFragmentCompat() { class MySettingsFragment(private val activity: AppCompatActivity): PreferenceFragmentCompat() {
private lateinit var airaService: AIRAService 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?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey) setPreferencesFromResource(R.xml.preferences, rootKey)
findPreference<Preference>("identityAvatar")?.let { identityAvatarPreference = it }
val identityNamePreference = findPreference<EditTextPreference>("identityName") val identityNamePreference = findPreference<EditTextPreference>("identityName")
val paddingPreference = findPreference<SwitchPreferenceCompat>("psecPadding") val paddingPreference = findPreference<SwitchPreferenceCompat>("psecPadding")
identityNamePreference?.isPersistent = false identityNamePreference?.isPersistent = false
paddingPreference?.isPersistent = false paddingPreference?.isPersistent = false
AIRADatabase.getIdentityAvatar(Constants.getDatabaseFolder(activity))?.let { avatar ->
displayAvatar(avatar)
}
Intent(activity, AIRAService::class.java).also { serviceIntent -> Intent(activity, AIRAService::class.java).also { serviceIntent ->
activity?.bindService(serviceIntent, object : ServiceConnection { activity.bindService(serviceIntent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder) { override fun onServiceConnected(name: ComponentName?, service: IBinder) {
val binder = service as AIRAService.AIRABinder val binder = service as AIRAService.AIRABinder
airaService = binder.getService() airaService = binder.getService()
@ -37,6 +64,26 @@ class SettingsActivity: AppCompatActivity() {
override fun onServiceDisconnected(name: ComponentName?) {} override fun onServiceDisconnected(name: ComponentName?) {}
}, Context.BIND_AUTO_CREATE) }, 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 -> identityNamePreference?.setOnPreferenceChangeListener { _, newValue ->
if (airaService.changeName(newValue as String)) { if (airaService.changeName(newValue as String)) {
identityNamePreference.text = newValue identityNamePreference.text = newValue
@ -44,70 +91,66 @@ class SettingsActivity: AppCompatActivity() {
false false
} }
findPreference<Preference>("deleteIdentity")?.setOnPreferenceClickListener { findPreference<Preference>("deleteIdentity")?.setOnPreferenceClickListener {
activity?.let { activity -> AlertDialog.Builder(activity, R.style.CustomAlertDialog)
AlertDialog.Builder(activity) .setMessage(R.string.confirm_delete)
.setMessage(R.string.confirm_delete) .setTitle(R.string.warning)
.setTitle(R.string.warning) .setPositiveButton(R.string.ok) { _, _ ->
.setPositiveButton(R.string.ok) { _, _ -> if (Constants.getDatabasePath(activity).delete()) {
if (Constants.getDatabasePath(activity).delete()) { airaService.logOut()
airaService.logOut() startActivity(Intent(activity, LoginActivity::class.java))
startActivity(Intent(activity, LoginActivity::class.java)) activity.finish()
activity.finish()
}
} }
.setNegativeButton(R.string.cancel, null) }
.show() .setNegativeButton(R.string.cancel, null)
} .show()
false false
} }
findPreference<Preference>("identityPassword")?.setOnPreferenceClickListener { findPreference<Preference>("identityPassword")?.setOnPreferenceClickListener {
activity?.let { activity -> val dialogView = layoutInflater.inflate(R.layout.dialog_password, null)
val dialogView = layoutInflater.inflate(R.layout.dialog_password, null) val oldPasswordEditText = dialogView.findViewById<EditText>(R.id.old_password)
val oldPasswordEditText = dialogView.findViewById<EditText>(R.id.old_password) val isIdentityProtected = AIRADatabase.isIdentityProtected(Constants.getDatabaseFolder(activity))
val isIdentityProtected = AIRADatabase.isIdentityProtected(Constants.getDatabaseFolder(activity)) if (!isIdentityProtected) {
if (!isIdentityProtected) { oldPasswordEditText.visibility = View.GONE
oldPasswordEditText.visibility = View.GONE
}
val newPasswordEditText = dialogView.findViewById<EditText>(R.id.new_password)
val newPasswordConfirmEditText = dialogView.findViewById<EditText>(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 newPasswordEditText = dialogView.findViewById<EditText>(R.id.new_password)
val newPasswordConfirmEditText = dialogView.findViewById<EditText>(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 false
} }
findPreference<Preference>("fingerprint")?.let { fingerprintPreference -> findPreference<Preference>("fingerprint")?.let { fingerprintPreference ->
val fingerprint = StringUtils.beautifyFingerprint(AIRADatabase.getIdentityFingerprint()) val fingerprint = StringUtils.beautifyFingerprint(AIRADatabase.getIdentityFingerprint())
fingerprintPreference.summary = fingerprint fingerprintPreference.summary = fingerprint
fingerprintPreference.setOnPreferenceClickListener { fingerprintPreference.setOnPreferenceClickListener {
activity?.getSystemService(CLIPBOARD_SERVICE)?.let { service -> activity.getSystemService(CLIPBOARD_SERVICE)?.let { service ->
val clipboardManager = service as ClipboardManager val clipboardManager = service as ClipboardManager
clipboardManager.setPrimaryClip(ClipData.newPlainText("", fingerprint)) 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 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<Drawable>) {
glideBuilder.apply(RequestOptions().override(90)).circleCrop().into(object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
identityAvatarPreference.icon = resource
}
override fun onLoadCleared(placeholder: Drawable?) {}
})
}
private fun changePassword(isIdentityProtected: Boolean, oldPasswordEditText: EditText, newPassword: ByteArray?) {
val oldPassword = if (isIdentityProtected) { val oldPassword = if (isIdentityProtected) {
oldPasswordEditText.text.toString().toByteArray() oldPasswordEditText.text.toString().toByteArray()
} else { } else {
null null
} }
if (!AIRADatabase.changePassword(Constants.getDatabaseFolder(context), oldPassword, newPassword)) { if (!AIRADatabase.changePassword(Constants.getDatabaseFolder(activity), oldPassword, newPassword)) {
AlertDialog.Builder(context) AlertDialog.Builder(activity, R.style.CustomAlertDialog)
.setMessage(R.string.change_password_failed) .setMessage(R.string.change_password_failed)
.setTitle(R.string.error) .setTitle(R.string.error)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
@ -150,7 +210,7 @@ class SettingsActivity: AppCompatActivity() {
setContentView(binding.root) setContentView(binding.root)
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.replace(R.id.settings_container, MySettingsFragment()) .replace(R.id.settings_container, MySettingsFragment(this))
.commit() .commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
} }

View File

@ -6,5 +6,6 @@ class Session(
val isVerified: Boolean, val isVerified: Boolean,
var seen: Boolean, var seen: Boolean,
val ip: String?, val ip: String?,
var name: String? var name: String?,
var avatar: ByteArray?,
) )

View File

@ -1,18 +1,18 @@
package sushi.hardcore.aira.adapters package sushi.hardcore.aira.adapters
import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import sushi.hardcore.aira.R 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<Session>() private val sessions = mutableListOf<Session>()
private val inflater: LayoutInflater = LayoutInflater.from(context) private val inflater: LayoutInflater = LayoutInflater.from(activity)
val selectedItems = mutableListOf<Int>() val selectedItems = mutableListOf<Int>()
override fun getCount(): Int { 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 view: View = convertView ?: inflater.inflate(R.layout.adapter_session, parent, false)
val currentSession = getItem(position) val currentSession = getItem(position)
view.findViewById<TextView>(R.id.text_name).apply { view.findViewById<TextView>(R.id.text_name).apply {
view.findViewById<TextAvatar>(R.id.text_avatar).setLetterFrom(if (currentSession.name == null) { val avatarName = if (currentSession.name == null) {
text = currentSession.ip text = currentSession.ip
setTextColor(Color.RED) setTextColor(Color.RED)
"?" "?"
@ -39,7 +39,13 @@ class SessionAdapter(private val context: Context): BaseAdapter() {
text = currentSession.name text = currentSession.name
setTextColor(Color.WHITE) setTextColor(Color.WHITE)
currentSession.name!! currentSession.name!!
}) }
val avatar = view.findViewById<Avatar>(R.id.avatar)
if (currentSession.avatar == null) {
avatar.setTextAvatar(avatarName)
} else {
avatar.setImageAvatar(currentSession.avatar!!)
}
} }
view.findViewById<ImageView>(R.id.image_trust_level).apply { view.findViewById<ImageView>(R.id.image_trust_level).apply {
if (currentSession.isVerified) { if (currentSession.isVerified) {
@ -55,12 +61,12 @@ class SessionAdapter(private val context: Context): BaseAdapter() {
} else { } else {
View.VISIBLE View.VISIBLE
} }
view.findViewById<ImageView>(R.id.image_arrow).setColorFilter(ContextCompat.getColor(context, if (currentSession.seen) { view.findViewById<ImageView>(R.id.image_arrow).setColorFilter(ContextCompat.getColor(activity, if (currentSession.seen) {
R.color.sessionArrow R.color.sessionArrow
} else { } else {
R.color.secondary R.color.secondary
})) }))
view.setBackgroundColor(ContextCompat.getColor(context, if (selectedItems.contains(position)) { view.setBackgroundColor(ContextCompat.getColor(activity, if (selectedItems.contains(position)) {
R.color.itemSelected R.color.itemSelected
} else { } else {
R.color.sessionBackground 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) { fun setSeen(sessionId: Int, seen: Boolean) {
getSessionById(sessionId)?.let { getSessionById(sessionId)?.let {
it.seen = seen it.seen = seen

View File

@ -32,8 +32,9 @@ class AIRAService : Service() {
const val MESSAGE_CONNECT_TO = 0 const val MESSAGE_CONNECT_TO = 0
const val MESSAGE_SEND_TO = 1 const val MESSAGE_SEND_TO = 1
const val MESSAGE_LOGOUT = 2 const val MESSAGE_LOGOUT = 2
const val MESSAGE_TELL_NAME = 3 const val MESSAGE_SEND_NAME = 3
const val MESSAGE_CANCEL_FILE_TRANSFER = 4 const val MESSAGE_SEND_AVATAR = 4
const val MESSAGE_CANCEL_FILE_TRANSFER = 5
var isServiceRunning = false var isServiceRunning = false
} }
@ -87,6 +88,7 @@ class AIRAService : Service() {
} }
val savedMsgs = mutableMapOf<Int, MutableList<ChatItem>>() val savedMsgs = mutableMapOf<Int, MutableList<ChatItem>>()
val savedNames = mutableMapOf<Int, String>() val savedNames = mutableMapOf<Int, String>()
val savedAvatars = mutableMapOf<Int, String>()
val notSeen = mutableListOf<Int>() val notSeen = mutableListOf<Int>()
var uiCallbacks: UiCallbacks? = null var uiCallbacks: UiCallbacks? = null
var isAppInBackground = true var isAppInBackground = true
@ -104,6 +106,7 @@ class AIRAService : Service() {
fun onNewSession(sessionId: Int, ip: String) fun onNewSession(sessionId: Int, ip: String)
fun onSessionDisconnect(sessionId: Int) fun onSessionDisconnect(sessionId: Int)
fun onNameTold(sessionId: Int, name: String) fun onNameTold(sessionId: Int, name: String)
fun onAvatarChanged(sessionId: Int, avatar: ByteArray?)
fun onNewMessage(sessionId: Int, data: ByteArray): Boolean fun onNewMessage(sessionId: Int, data: ByteArray): Boolean
fun onAskLargeFiles(sessionId: Int, name: String, filesReceiver: FilesReceiver): Boolean fun onAskLargeFiles(sessionId: Int, name: String, filesReceiver: FilesReceiver): Boolean
} }
@ -185,7 +188,7 @@ class AIRAService : Service() {
return sessions.contains(sessionId) return sessions.contains(sessionId)
} }
private fun getNameOf(sessionId: Int): String { fun getNameOf(sessionId: Int): String {
return contacts[sessionId]?.name ?: savedNames[sessionId] ?: sessions[sessionId]!!.ip return contacts[sessionId]?.name ?: savedNames[sessionId] ?: sessions[sessionId]!!.ip
} }
@ -195,7 +198,7 @@ class AIRAService : Service() {
fun setAsContact(sessionId: Int, name: String): Boolean { fun setAsContact(sessionId: Int, name: String): Boolean {
sessions[sessionId]?.peerPublicKey?.let { sessions[sessionId]?.peerPublicKey?.let {
AIRADatabase.addContact(name, it)?.let { contact -> AIRADatabase.addContact(name, savedAvatars[sessionId], it)?.let { contact ->
contacts[sessionId] = contact contacts[sessionId] = contact
savedMsgs.remove(sessionId)?.let { msgs -> savedMsgs.remove(sessionId)?.let { msgs ->
for (msg in msgs) { for (msg in msgs) {
@ -203,6 +206,7 @@ class AIRAService : Service() {
} }
} }
savedNames.remove(sessionId) savedNames.remove(sessionId)
savedAvatars.remove(sessionId)
return true return true
} }
} }
@ -240,6 +244,9 @@ class AIRAService : Service() {
return if (AIRADatabase.removeContact(it.uuid)) { return if (AIRADatabase.removeContact(it.uuid)) {
savedMsgs[sessionId] = mutableListOf() savedMsgs[sessionId] = mutableListOf()
savedNames[sessionId] = it.name savedNames[sessionId] = it.name
it.avatar?.let { avatarUuid ->
savedAvatars[sessionId] = avatarUuid
}
true true
} else { } else {
false false
@ -263,7 +270,26 @@ class AIRAService : Service() {
fun changeName(newName: String): Boolean { fun changeName(newName: String): Boolean {
return if (AIRADatabase.changeName(newName)) { return if (AIRADatabase.changeName(newName)) {
identityName = 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 true
} else { } else {
false false
@ -301,7 +327,7 @@ class AIRAService : Service() {
sessionIdByKey[key] = sessionId sessionIdByKey[key] = sessionId
uiCallbacks?.onNewSession(sessionId, session.ip) uiCallbacks?.onNewSession(sessionId, session.ip)
if (!isContact(sessionId)) { if (!isContact(sessionId)) {
session.encryptAndSend(Protocol.askName(), usePadding) session.encryptAndSend(Protocol.askProfileInfo(), usePadding)
} }
} else { } else {
session.close() session.close()
@ -449,17 +475,33 @@ class AIRAService : Service() {
cancelFileTransfer(sessionId, session, true) cancelFileTransfer(sessionId, session, true)
} }
} }
MESSAGE_TELL_NAME -> { MESSAGE_SEND_NAME -> {
identityName?.let { identityName?.let {
val tellingName = Protocol.name(it)
for (session in sessions.values) { for (session in sessions.values) {
try { try {
session.encryptAndSend(Protocol.tellName(it), usePadding) session.encryptAndSend(tellingName, usePadding)
} catch (e: SocketException) { } catch (e: SocketException) {
e.printStackTrace() 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 -> { MESSAGE_LOGOUT -> {
nsdManager.unregisterService(nsdRegistrationListener) nsdManager.unregisterService(nsdRegistrationListener)
stopDiscovery() stopDiscovery()
@ -494,6 +536,20 @@ class AIRAService : Service() {
usePadding = AIRADatabase.getUsePadding() 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) { private fun encryptNextChunk(session: Session, filesSender: FilesSender) {
val nextChunk = ByteArray(Constants.FILE_CHUNK_SIZE) val nextChunk = ByteArray(Constants.FILE_CHUNK_SIZE)
nextChunk[0] = Protocol.LARGE_FILE_CHUNK nextChunk[0] = Protocol.LARGE_FILE_CHUNK
@ -566,22 +622,6 @@ class AIRAService : Service() {
shouldCloseSession = true shouldCloseSession = true
} else { } else {
when (buffer[0]) { 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 -> { Protocol.LARGE_FILE_CHUNK -> {
receiveFileTransfers[sessionId]?.let { filesReceiver -> receiveFileTransfers[sessionId]?.let { filesReceiver ->
val file = filesReceiver.files[filesReceiver.index] 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 -> { else -> {
when (buffer[0]){ when (buffer[0]){
Protocol.MESSAGE -> buffer Protocol.MESSAGE -> buffer

View File

@ -1,3 +1,10 @@
package sushi.hardcore.aira.background_service package sushi.hardcore.aira.background_service
class Contact(val uuid: String, val publicKey: ByteArray, var name: String, var verified: Boolean, var seen: Boolean) class Contact(
val uuid: String,
val publicKey: ByteArray,
var name: String,
var avatar: String?,
var verified: Boolean,
var seen: Boolean
)

View File

@ -28,7 +28,7 @@ class FilesReceiver(
filesInfo.appendLine(file.fileName+" ("+FileUtils.formatSize(file.fileSize)+')') filesInfo.appendLine(file.fileName+" ("+FileUtils.formatSize(file.fileSize)+')')
} }
dialogBinding.textFilesInfo.text = filesInfo.substring(0, filesInfo.length-1) dialogBinding.textFilesInfo.text = filesInfo.substring(0, filesInfo.length-1)
AlertDialog.Builder(activity) AlertDialog.Builder(activity, R.style.CustomAlertDialog)
.setTitle(R.string.download_file_request) .setTitle(R.string.download_file_request)
.setView(dialogBinding.root) .setView(dialogBinding.root)
.setCancelable(false) .setCancelable(false)

View File

@ -1,25 +1,36 @@
package sushi.hardcore.aira.background_service package sushi.hardcore.aira.background_service
import sushi.hardcore.aira.widgets.Avatar
import java.nio.ByteBuffer import java.nio.ByteBuffer
class Protocol { class Protocol {
companion object { companion object {
const val MESSAGE: Byte = 0x00 const val MESSAGE: Byte = 0x00
const val ASK_NAME: Byte = 0x01 const val FILE: Byte = 0x01
const val TELL_NAME: Byte = 0x02 const val ASK_PROFILE_INFO: Byte = 0x02
const val FILE: Byte = 0x03 const val NAME: Byte = 0x03
const val ASK_LARGE_FILES: Byte = 0x04 const val AVATAR: Byte = 0x04
const val ACCEPT_LARGE_FILES: Byte = 0x05 const val REMOVE_AVATAR: Byte = 0x05
const val LARGE_FILE_CHUNK: Byte = 0x06 const val ASK_LARGE_FILES: Byte = 0x06
const val ACK_CHUNK: Byte = 0x07 const val ACCEPT_LARGE_FILES: Byte = 0x07
const val ABORT_FILES_TRANSFER: Byte = 0x08 const val LARGE_FILE_CHUNK: Byte = 0x08
const val ACK_CHUNK: Byte = 0x09
const val ABORT_FILES_TRANSFER: Byte = 0x0a
fun askName(): ByteArray { fun askProfileInfo(): ByteArray {
return byteArrayOf(ASK_NAME) return byteArrayOf(ASK_PROFILE_INFO)
} }
fun tellName(name: String): ByteArray { fun name(name: String): ByteArray {
return byteArrayOf(TELL_NAME)+name.toByteArray() 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 { fun newMessage(msg: String): ByteArray {

View File

@ -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<Drawable>) -> Unit,
) {
private lateinit var picker: ActivityResultLauncher<String>
private lateinit var avatar: RequestBuilder<Drawable>
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<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
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/*")
}
}

View File

@ -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
}
}

View File

@ -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<TextView>(R.id.text_letter).textSize = textSize
}
break
}
}
typedArray.recycle()
}
}
fun setLetterFrom(name: String) {
if (name.isNotEmpty()) {
view.findViewById<TextView>(R.id.text_letter).text = name[0].toUpperCase().toString()
}
}
}

View File

@ -11,6 +11,7 @@ const DB_NAME: &str = "AIRA.db";
const MAIN_TABLE: &str = "main"; const MAIN_TABLE: &str = "main";
const CONTACTS_TABLE: &str = "contacts"; const CONTACTS_TABLE: &str = "contacts";
const FILES_TABLE: &str = "files"; const FILES_TABLE: &str = "files";
const AVATARS_TABLE: &str = "avatars";
const DATABASE_CORRUPED_ERROR: &str = "Database corrupted"; const DATABASE_CORRUPED_ERROR: &str = "Database corrupted";
@ -21,6 +22,7 @@ impl<'a> DBKeys {
pub const SALT: &'a str = "salt"; pub const SALT: &'a str = "salt";
pub const MASTER_KEY: &'a str = "master_key"; pub const MASTER_KEY: &'a str = "master_key";
pub const USE_PADDING: &'a str = "use_padding"; pub const USE_PADDING: &'a str = "use_padding";
pub const AVATAR: &'a str = "avatar";
} }
fn bool_to_byte(b: bool) -> u8 { fn bool_to_byte(b: bool) -> u8 {
@ -53,6 +55,7 @@ pub struct Contact {
pub uuid: Uuid, pub uuid: Uuid,
pub public_key: [u8; PUBLIC_KEY_LENGTH], pub public_key: [u8; PUBLIC_KEY_LENGTH],
pub name: String, pub name: String,
pub avatar: Option<Uuid>,
pub verified: bool, pub verified: bool,
pub seen: bool, pub seen: bool,
} }
@ -79,19 +82,23 @@ impl Identity {
get_database_path(&self.database_folder) get_database_path(&self.database_folder)
} }
pub fn add_contact(&self, name: String, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result<Contact, rusqlite::Error> { pub fn add_contact(&self, name: String, avatar_uuid: Option<Uuid>, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result<Contact, rusqlite::Error> {
let db = Connection::open(self.get_database_path())?; 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 contact_uuid = Uuid::new_v4();
let encrypted_name = crypto::encrypt_data(name.as_bytes(), &self.master_key).unwrap(); 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_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_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(); 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 { Ok(Contact {
uuid: contact_uuid, uuid: contact_uuid,
public_key: public_key, public_key: public_key,
name: name, name: name,
avatar: avatar_uuid,
verified: false, verified: false,
seen: true, 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()]) 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<usize, rusqlite::Error> {
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<usize, rusqlite::Error> { pub fn set_contact_seen(&self, uuid: &Uuid, seen: bool) -> Result<usize, rusqlite::Error> {
let db = Connection::open(self.get_database_path())?; let db = Connection::open(self.get_database_path())?;
let encrypted_seen = crypto::encrypt_data(&[bool_to_byte(seen)], &self.master_key).unwrap(); 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<Vec<Contact>> { pub fn load_contacts(&self) -> Option<Vec<Contact>> {
match Connection::open(self.get_database_path()) { match Connection::open(self.get_database_path()) {
Ok(db) => { Ok(db) => {
match db.prepare(&("SELECT uuid, name, key, verified, seen FROM ".to_owned()+CONTACTS_TABLE)) { if let Ok(mut stmt) = db.prepare(&("SELECT uuid, name, avatar, key, verified, seen FROM ".to_owned()+CONTACTS_TABLE)) {
Ok(mut stmt) => { let mut rows = stmt.query([]).unwrap();
let mut rows = stmt.query([]).unwrap(); let mut contacts = Vec::new();
let mut contacts = Vec::new(); while let Ok(Some(row)) = rows.next() {
while let Some(row) = rows.next().unwrap() { let encrypted_public_key: Vec<u8> = row.get(3).unwrap();
let encrypted_public_key: Vec<u8> = row.get(2).unwrap(); match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) {
match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) { Ok(public_key) => {
Ok(public_key) => { let encrypted_name: Vec<u8> = row.get(1).unwrap();
if public_key.len() == PUBLIC_KEY_LENGTH { match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) {
let encrypted_name: Vec<u8> = row.get(1).unwrap(); Ok(name) => {
match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) { let encrypted_verified: Vec<u8> = row.get(4).unwrap();
Ok(name) => { match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) {
let encrypted_verified: Vec<u8> = row.get(3).unwrap(); Ok(verified) => {
match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) { let encrypted_seen: Vec<u8> = row.get(5).unwrap();
Ok(verified) => { match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) {
let encrypted_seen: Vec<u8> = row.get(4).unwrap(); Ok(seen) => {
match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) { let contact_uuid: Vec<u8> = row.get(0).unwrap();
Ok(seen) => { let avatar_result: Result<Vec<u8>, rusqlite::Error> = row.get(2);
let uuid: Vec<u8> = row.get(0).unwrap(); let avatar = match avatar_result {
match to_uuid_bytes(&uuid) { Ok(avatar_uuid) => Some(Uuid::from_bytes(to_uuid_bytes(&avatar_uuid).unwrap())),
Some(uuid_bytes) => { Err(_) => None
contacts.push(Contact { };
uuid: Uuid::from_bytes(uuid_bytes), contacts.push(Contact {
public_key: public_key.try_into().unwrap(), uuid: Uuid::from_bytes(to_uuid_bytes(&contact_uuid).unwrap()),
name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(), public_key: public_key.try_into().unwrap(),
verified: byte_to_bool(verified[0]).unwrap(), name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(),
seen: byte_to_bool(seen[0]).unwrap(), avatar,
}) verified: byte_to_bool(verified[0]).unwrap(),
} seen: byte_to_bool(seen[0]).unwrap(),
None => {} })
}
}
Err(e) => print_error!(e)
}
} }
Err(e) => print_error!(e) Err(e) => print_error!(e)
} }
} }
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) => { return Some(contacts);
print_error!(e); }
None
}
}
}
Err(e) => {
print_error!(e);
None
} }
Err(e) => print_error!(e)
} }
None
} }
pub fn clear_temporary_files(&self) -> Result<usize, rusqlite::Error> { pub fn clear_cache(&self) -> Result<(), rusqlite::Error> {
let db = Connection::open(self.get_database_path())?; 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<Vec<u8>> { pub fn load_file(&self, uuid: Uuid) -> Option<Vec<u8>> {
match Connection::open(self.get_database_path()) { match Connection::open(self.get_database_path()) {
Ok(db) => { Ok(db) => {
match db.prepare(&format!("SELECT uuid, data FROM \"{}\"", FILES_TABLE)) { let mut stmt = db.prepare(&format!("SELECT uuid, data FROM \"{}\"", FILES_TABLE)).unwrap();
Ok(mut stmt) => { let mut rows = stmt.query([]).unwrap();
let mut rows = stmt.query([]).unwrap(); while let Ok(Some(row)) = rows.next() {
while let Some(row) = rows.next().unwrap() { let encrypted_uuid: Vec<u8> = row.get(0).unwrap();
let encrypted_uuid: Vec<u8> = row.get(0).unwrap(); match crypto::decrypt_data(encrypted_uuid.as_slice(), &self.master_key){
match crypto::decrypt_data(encrypted_uuid.as_slice(), &self.master_key){ Ok(test_uuid) => {
Ok(test_uuid) => { if test_uuid == uuid.as_bytes() {
if test_uuid == uuid.as_bytes() { 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) => return Some(data),
Ok(data) => return Some(data), Err(e) => print_error!(e)
Err(e) => print_error!(e)
}
}
} }
Err(e) => print_error!(e)
} }
} }
None Err(e) => print_error!(e)
}
Err(e) => {
print_error!(e);
None
} }
} }
} }
Err(e) => { Err(e) => print_error!(e)
print_error!(e);
None
}
} }
None
} }
pub fn store_file(&self, contact_uuid: Option<Uuid>, data: &[u8]) -> Result<Uuid, rusqlite::Error> { pub fn store_file(&self, contact_uuid: Option<Uuid>, data: &[u8]) -> Result<Uuid, rusqlite::Error> {
@ -253,73 +263,44 @@ impl Identity {
pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<(bool, Vec<u8>)>> { pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<(bool, Vec<u8>)>> {
match Connection::open(self.get_database_path()) { match Connection::open(self.get_database_path()) {
Ok(db) => { Ok(db) => {
match db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) { if let Ok(mut stmt) = db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
Ok(mut stmt) => { let mut rows = stmt.query([]).unwrap();
let mut rows = stmt.query([]).unwrap(); if let Ok(Some(row)) = rows.next() {
match rows.next() { let total: usize = row.get(0).unwrap();
Ok(row) => if row.is_some() { if offset < total {
let total: usize = row.unwrap().get(0).unwrap(); if offset+count >= total {
if offset >= total { count = total-offset;
None }
} else { let mut stmt = db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)).unwrap();
if offset+count >= total { let mut rows = stmt.query([]).unwrap();
count = total-offset; let mut msgs = Vec::new();
} while let Ok(Some(row)) = rows.next() {
match db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)) { let encrypted_outgoing: Vec<u8> = row.get(0).unwrap();
Ok(mut stmt) => { match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
let mut rows = stmt.query([]).unwrap(); Ok(outgoing) => {
let mut msgs = Vec::new(); match byte_to_bool(outgoing[0]) {
while let Some(row) = rows.next().unwrap() { Ok(outgoing) => {
let encrypted_outgoing: Vec<u8> = row.get(0).unwrap(); let encrypted_data: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){ match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
Ok(outgoing) => { Ok(data) => msgs.push((outgoing, data)),
match byte_to_bool(outgoing[0]) {
Ok(outgoing) => {
let encrypted_data: Vec<u8> = 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(_) => {}
}
}
Err(e) => print_error!(e) Err(e) => print_error!(e)
} }
} }
Some(msgs) Err(_) => {}
}
Err(e) => {
print_error!(e);
None
} }
} }
Err(e) => print_error!(e)
} }
} else {
None
} }
Err(_) => None return Some(msgs);
} }
} }
Err(e) => {
print_error!(e);
None
}
} }
} }
Err(e) => { Err(e) => print_error!(e)
print_error!(e);
None
}
} }
None
} }
#[allow(unused_must_use)] #[allow(unused_must_use)]
@ -345,6 +326,29 @@ impl Identity {
db.update(DBKeys::USE_PADDING, &encrypted_use_padding) db.update(DBKeys::USE_PADDING, &encrypted_use_padding)
} }
pub fn store_avatar(&self, avatar: &[u8]) -> Result<Uuid, rusqlite::Error> {
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<Vec<u8>> {
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<u8> = 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){ pub fn zeroize(&mut self){
self.master_key.zeroize(); self.master_key.zeroize();
self.keypair.secret.zeroize(); self.keypair.secret.zeroize();
@ -490,4 +494,19 @@ impl Identity {
Err(e) => Err(e.to_string()) Err(e) => Err(e.to_string())
} }
} }
pub fn set_identity_avatar(database_folder: &str, avatar: &[u8]) -> Result<usize, rusqlite::Error> {
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<usize, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(database_folder), MAIN_TABLE)?;
db.del(DBKeys::AVATAR)
}
pub fn get_identity_avatar(database_folder: &str) -> Result<Vec<u8>, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(database_folder), MAIN_TABLE)?;
db.get(DBKeys::AVATAR)
}
} }

View File

@ -22,13 +22,13 @@ impl<'a> KeyValueTable<'a> {
None => Err(rusqlite::Error::QueryReturnedNoRows) None => Err(rusqlite::Error::QueryReturnedNoRows)
} }
} }
/*pub fn del(&self, key: &str) -> Result<usize, Error> { pub fn del(&self, key: &str) -> Result<usize, Error> {
self.db.execute(&format!("DELETE FROM {} WHERE key=\"{}\"", self.table_name, key), NO_PARAMS) self.db.execute(&format!("DELETE FROM {} WHERE key=\"{}\"", self.table_name, key), [])
}*/ }
pub fn update(&self, key: &str, value: &[u8]) -> Result<usize, Error> { pub fn update(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
self.db.execute(&format!("UPDATE {} SET value=? WHERE key=\"{}\"", self.table_name, key), params![value]) self.db.execute(&format!("UPDATE {} SET value=? WHERE key=\"{}\"", self.table_name, key), params![value])
} }
/*pub fn upsert(&self, key: &str, value: &[u8]) -> Result<usize, Error> { pub fn upsert(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
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]) 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])
}*/ }
} }

View File

@ -3,10 +3,9 @@ mod identity;
mod crypto; mod crypto;
mod utils; 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 lazy_static::lazy_static;
use uuid::Uuid; use uuid::Uuid;
use log::*;
use android_log; use android_log;
use identity::{Identity, Contact}; use identity::{Identity, Contact};
use crate::crypto::{HandshakeKeys, ApplicationKeys}; 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()) String::from(env.get_string(input).unwrap())
} }
fn jstring_to_uuid(env: JNIEnv, input: JString) -> Option<Uuid> {
match env.get_string(input) {
Ok(uuid) => Some(Uuid::from_str(&String::from(uuid)).unwrap()),
Err(_) => None
}
}
fn jboolean_to_bool(input: jboolean) -> bool { fn jboolean_to_bool(input: jboolean) -> bool {
input == 1 input == 1
} }
@ -32,14 +38,23 @@ fn bool_to_jboolean(input: bool) -> u8 {
if input { 1 } else { 0 } if input { 1 } else { 0 }
} }
fn result_to_jboolean<T, E: Display>(result: Result<T, E>) -> jboolean {
match result {
Ok(_) => 1,
Err(e) => {
print_error!(e);
0
}
}
}
fn slice_to_jvalue<'a>(env: JNIEnv, input: &'a [u8]) -> JValue<'a> { fn slice_to_jvalue<'a>(env: JNIEnv, input: &'a [u8]) -> JValue<'a> {
JValue::Object(env.byte_array_from_slice(input).unwrap().into()) JValue::Object(env.byte_array_from_slice(input).unwrap().into())
} }
#[allow(unused_must_use)] #[no_mangle]
fn log_error<T: Display>(e: T) { pub extern fn Java_sushi_hardcore_aira_LoginActivity_00024Companion_initLogging(_: JNIEnv, _: JClass) -> jboolean {
android_log::init("AIRA Native"); bool_to_jboolean(android_log::init("AIRA Native").is_ok())
error!("Error: {}", e)
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
@ -53,7 +68,7 @@ pub extern fn Java_sushi_hardcore_aira_CreateIdentityFragment_createNewIdentity(
1 1
} }
Err(e) => { Err(e) => {
log_error(e); print_error!(e);
0 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)) { *match Identity::get_identity_name(&jstring_to_string(env, database_folder)) {
Ok(name) => *env.new_string(name).unwrap(), Ok(name) => *env.new_string(name).unwrap(),
Err(e) => { Err(e) => {
log_error(e); print_error!(e);
JObject::null() 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)) { match Identity::is_protected(jstring_to_string(env, database_folder)) {
Ok(is_protected) => bool_to_jboolean(is_protected), Ok(is_protected) => bool_to_jboolean(is_protected),
Err(e) => { Err(e) => {
log_error(e); print_error!(e);
0 0
} }
} }
@ -91,7 +106,7 @@ pub extern fn Java_sushi_hardcore_aira_AIRADatabase_loadIdentity(env: JNIEnv, _:
1 1
} }
Err(e) => { Err(e) => {
log_error(e); print_error!(e);
0 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()) { 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), Ok(success) => bool_to_jboolean(success),
Err(e) => { Err(e) => {
log_error(e); print_error!(e);
0 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)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_getIdentityPublicKey(env: JNIEnv, _: JClass) -> jbyteArray { 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 { fn new_contact(env: JNIEnv, contact: Contact) -> JObject {
let contact_class = env.find_class("sushi/hardcore/aira/background_service/Contact").unwrap(); 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)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_addContact(env: JNIEnv, _: JClass, name: JString, public_key: jbyteArray) -> jobject { 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), env.convert_byte_array(public_key).unwrap().try_into().unwrap()) { *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), Ok(contact) => new_contact(env, contact),
Err(e) => { Err(e) => {
log_error(e); print_error!(e);
JObject::null() JObject::null()
} }
} }
@ -190,13 +240,7 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_addContact(env: JNIEnv, _: JClass,
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_removeContact(env: JNIEnv, _: JClass, uuid: JString) -> jboolean { 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()) { result_to_jboolean(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
}
}
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
@ -220,55 +264,37 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_loadContacts(env: JNIEnv, _: JClass
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_setVerified(env: JNIEnv, _: JClass, uuid: JString) -> jboolean { 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()) { result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().set_verified(&jstring_to_uuid(env, uuid).unwrap()))
Ok(_) => 1,
Err(e) => {
log_error(e);
0
}
}
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_changeContactName(env: JNIEnv, _: JClass, contactUuid: JString, newName: JString) -> jboolean { 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)) { result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().change_contact_name(&jstring_to_uuid(env, contactUuid).unwrap(), &jstring_to_string(env, newName)))
Ok(_) => 1, }
Err(e) => {
log_error(e); #[allow(non_snake_case)]
0 #[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)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_setContactSeen(env: JNIEnv, _: JClass, contactUuid: JString, seen: jboolean) -> jboolean { 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)) { result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().set_contact_seen(&jstring_to_uuid(env, contactUuid).unwrap(), jboolean_to_bool(seen)))
Ok(_) => 1,
Err(e) => {
log_error(e);
0
}
}
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_storeMsg(env: JNIEnv, _: JClass, contactUuid: JString, outgoing: jboolean, data: jbyteArray) -> jboolean { 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()) { 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()))
Ok(_) => 1,
Err(e) => {
log_error(e);
0
}
}
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_loadMsgs(env: JNIEnv, _: JClass, uuid: JString, offset: jint, count: jint) -> jobject { 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) => { Some(msgs) => {
let array_list_class = env.find_class("java/util/ArrayList").unwrap(); 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(); 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)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_storeFile(env: JNIEnv, _: JClass, contactUuid: JString, data: jbyteArray) -> jbyteArray { pub fn Java_sushi_hardcore_aira_AIRADatabase_storeFile(env: JNIEnv, _: JClass, contactUuid: JString, data: jbyteArray) -> jbyteArray {
let contact_uuid = match env.get_string(contactUuid) { match loaded_identity.lock().unwrap().as_ref().unwrap().store_file(jstring_to_uuid(env, contactUuid), &env.convert_byte_array(data).unwrap()) {
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()) {
Ok(uuid) => env.byte_array_from_slice(uuid.as_bytes()).unwrap(), Ok(uuid) => env.byte_array_from_slice(uuid.as_bytes()).unwrap(),
Err(_) => *JObject::null() Err(_) => *JObject::null()
} }
@ -309,25 +331,15 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_loadFile(env: JNIEnv, _: JClass, ra
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_deleteConversation(env: JNIEnv, _: JClass, contactUuid: JString) -> jboolean { 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(); let contact_uuid = jstring_to_uuid(env, contactUuid).unwrap();
match loaded_identity.lock().unwrap().as_ref().unwrap().delete_conversation(&contact_uuid) { result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().delete_conversation(&contact_uuid))
Ok(_) => 1,
Err(e) => {
log_error(e);
0
}
}
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_clearTemporaryFiles(_: JNIEnv, _: JClass) -> jint { pub fn Java_sushi_hardcore_aira_AIRADatabase_clearCache(_: JNIEnv, _: JClass) {
match loaded_identity.lock().unwrap().as_ref().unwrap().clear_temporary_files() { if let Err(e) = loaded_identity.lock().unwrap().as_ref().unwrap().clear_cache() {
Ok(r) => r.try_into().unwrap(), print_error!(e);
Err(e) => {
log_error(e);
0
}
} }
} }
@ -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) { match loaded_identity.lock().unwrap().as_mut().unwrap().change_name(new_name) {
Ok(u) => bool_to_jboolean(u == 1), Ok(u) => bool_to_jboolean(u == 1),
Err(e) => { Err(e) => {
log_error(e); print_error!(e);
0 0
} }
} }
@ -353,12 +365,24 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_getUsePadding(_: JNIEnv, _: JClass)
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub fn Java_sushi_hardcore_aira_AIRADatabase_setUsePadding(_: JNIEnv, _: JClass, use_padding: jboolean) -> jboolean { 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)) { result_to_jboolean(loaded_identity.lock().unwrap().as_mut().unwrap().set_use_padding(jboolean_to_bool(use_padding)))
Ok(_) => 1, }
Err(e) => {
log_error(e); #[allow(non_snake_case)]
0 #[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()
} }
} }

View File

@ -15,9 +15,9 @@ pub fn to_uuid_bytes(bytes: &[u8]) -> Option<Bytes> {
#[macro_export] #[macro_export]
macro_rules! print_error { macro_rules! print_error {
($arg:tt) => ({ ($arg:tt) => ({
println!("[{}:{}] {}", file!(), line!(), $arg); log::error!("[{}:{}] {}", file!(), line!(), $arg);
}); });
($($arg:tt)*) => ({ ($($arg:tt)*) => ({
println!("[{}:{}] {}", file!(), line!(), format_args!($($arg)*)); log::error!("[{}:{}] {}", file!(), line!(), format_args!($($arg)*));
}) })
} }

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,11.75c-0.69,0 -1.25,0.56 -1.25,1.25s0.56,1.25 1.25,1.25 1.25,-0.56 1.25,-1.25 -0.56,-1.25 -1.25,-1.25zM15,11.75c-0.69,0 -1.25,0.56 -1.25,1.25s0.56,1.25 1.25,1.25 1.25,-0.56 1.25,-1.25 -0.56,-1.25 -1.25,-1.25zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8 0,-0.29 0.02,-0.58 0.05,-0.86 2.36,-1.05 4.23,-2.98 5.21,-5.37C11.07,8.33 14.05,10 17.42,10c0.78,0 1.53,-0.09 2.25,-0.26 0.21,0.71 0.33,1.47 0.33,2.26 0,4.41 -3.59,8 -8,8z"/>
</vector>

View File

@ -30,16 +30,18 @@
android:layout_height="@dimen/image_button_size" android:layout_height="@dimen/image_button_size"
android:src="@drawable/ic_attach_file" android:src="@drawable/ic_attach_file"
style="@style/ImageButton" style="@style/ImageButton"
android:contentDescription="@string/send_file"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/image_trust_level" /> app:layout_constraintEnd_toStartOf="@id/image_trust_level"/>
<ImageView <ImageView
android:id="@+id/image_trust_level" android:id="@+id/image_trust_level"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@drawable/ic_warning" android:src="@drawable/ic_warning"
android:contentDescription="@string/trust_level_indicator"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/button_attach" app:layout_constraintStart_toEndOf="@id/button_attach"
@ -49,6 +51,9 @@
android:id="@+id/edit_message" android:id="@+id/edit_message"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/message_hint"
android:autofillHints="message"
android:inputType="text"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/image_trust_level" app:layout_constraintStart_toEndOf="@id/image_trust_level"
@ -60,10 +65,11 @@
android:layout_height="@dimen/image_button_size" android:layout_height="@dimen/image_button_size"
android:src="@drawable/ic_send" android:src="@drawable/ic_send"
style="@style/ImageButton" style="@style/ImageButton"
android:contentDescription="@string/send_message"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/edit_message" app:layout_constraintStart_toEndOf="@id/edit_message"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -8,6 +8,7 @@
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container" android:id="@+id/fragment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"
android:layout_marginHorizontal="50dp"/>
</RelativeLayout> </RelativeLayout>

View File

@ -68,6 +68,7 @@
android:maxLines="1" android:maxLines="1"
android:imeOptions="actionGo" android:imeOptions="actionGo"
android:hint="@string/add_peer_ip" android:hint="@string/add_peer_ip"
android:autofillHints="ip"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -79,6 +80,7 @@
android:layout_height="@dimen/image_button_size" android:layout_height="@dimen/image_button_size"
android:src="@drawable/ic_info" android:src="@drawable/ic_info"
style="@style/ImageButton" style="@style/ImageButton"
android:contentDescription="@string/show_your_ips"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/edit_peer_ip" app:layout_constraintStart_toEndOf="@id/edit_peer_ip"

View File

@ -19,7 +19,8 @@
android:layout_width="@dimen/image_button_size" android:layout_width="@dimen/image_button_size"
android:layout_height="@dimen/image_button_size" android:layout_height="@dimen/image_button_size"
android:src="@drawable/ic_save" android:src="@drawable/ic_save"
style="@style/ImageButton"/> style="@style/ImageButton"
android:contentDescription="@string/download" />
<TextView <TextView
android:id="@+id/text_filename" android:id="@+id/text_filename"

View File

@ -21,8 +21,8 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:padding="15dp"> android:padding="15dp">
<sushi.hardcore.aira.widgets.TextAvatar <sushi.hardcore.aira.widgets.Avatar
android:id="@+id/text_avatar" android:id="@+id/avatar"
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="40dp"
app:textSize="8sp" app:textSize="8sp"
@ -37,12 +37,13 @@
android:layout_marginHorizontal="5dp" android:layout_marginHorizontal="5dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/text_avatar"/> app:layout_constraintStart_toEndOf="@id/avatar"/>
<ImageView <ImageView
android:id="@+id/image_trust_level" android:id="@+id/image_trust_level"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/trust_level_indicator"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/text_name"/> app:layout_constraintStart_toEndOf="@id/text_name"/>
@ -53,6 +54,7 @@
android:layout_height="15dp" android:layout_height="15dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:src="@drawable/ic_arrow_forward" android:src="@drawable/ic_arrow_forward"
android:contentDescription="@string/clickable_indicator"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/text_avatar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/round_background"
android:visibility="gone">
<TextView
android:id="@+id/text_letter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@color/messageTextColor"/>
</RelativeLayout>
<ImageView
android:id="@+id/image_avatar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:contentDescription="@string/avatar" />
</RelativeLayout>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<sushi.hardcore.aira.widgets.Avatar
android:id="@+id/avatar"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true"
app:textSize="26sp"/>
</RelativeLayout>

View File

@ -6,8 +6,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingHorizontal="20dp"> android:paddingHorizontal="20dp">
<sushi.hardcore.aira.widgets.TextAvatar <sushi.hardcore.aira.widgets.Avatar
android:id="@+id/text_avatar" android:id="@+id/avatar"
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="100dp" android:layout_height="100dp"
app:textSize="20sp" app:textSize="20sp"

View File

@ -9,20 +9,23 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/current_password" android:hint="@string/current_password"
android:inputType="textPassword"/> android:inputType="textPassword"
android:autofillHints="password"/>
<EditText <EditText
android:id="@+id/new_password" android:id="@+id/new_password"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/new_password" android:hint="@string/new_password"
android:inputType="textPassword"/> android:inputType="textPassword"
android:autofillHints="password"/>
<EditText <EditText
android:id="@+id/new_password_confirm" android:id="@+id/new_password_confirm"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/new_password_confirm" android:hint="@string/new_password_confirm"
android:inputType="textPassword"/> android:inputType="textPassword"
android:autofillHints="password"/>
</LinearLayout> </LinearLayout>

View File

@ -1,60 +1,69 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
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="wrap_content"
tools:context=".CreateIdentityFragment"> android:orientation="vertical"
android:layout_gravity="center_vertical">
<LinearLayout <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:text="@string/create_identity_test"
android:layout_marginHorizontal="50dp" style="@style/Label"/>
android:layout_centerInParent="true">
<TextView <ImageView
android:layout_width="match_parent" android:id="@+id/avatar"
android:layout_height="wrap_content" android:layout_width="@dimen/identityAvatarSize"
android:text="@string/create_identity_test" android:layout_height="@dimen/identityAvatarSize"
style="@style/Label"/> android:layout_gravity="center_horizontal"
android:src="@drawable/ic_face"
android:contentDescription="@string/avatar"/>
<EditText <Button
android:id="@+id/edit_name" android:id="@+id/button_set_avatar"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/identity_name" android:layout_gravity="center_horizontal"
android:inputType="text"/> android:text="@string/choose_avatar"/>
<androidx.appcompat.widget.SwitchCompat <EditText
android:id="@+id/checkbox_enable_password" android:id="@+id/edit_name"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:hint="@string/identity_name"
android:text="@string/enable_password"/> android:inputType="text"
android:autofillHints="name"/>
<EditText <androidx.appcompat.widget.SwitchCompat
android:id="@+id/edit_password" android:id="@+id/checkbox_enable_password"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/password_hint" android:layout_gravity="center_horizontal"
android:inputType="textPassword" android:text="@string/enable_password"/>
android:visibility="gone"/>
<EditText <EditText
android:id="@+id/edit_password_confirm" android:id="@+id/edit_password"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/password_confirm_hint" android:hint="@string/password_hint"
android:inputType="textPassword" android:inputType="textPassword"
android:visibility="gone"/> android:visibility="gone"
android:autofillHints="password"/>
<Button <EditText
android:id="@+id/button_create" android:id="@+id/edit_password_confirm"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:hint="@string/password_confirm_hint"
android:text="@string/create"/> android:inputType="textPassword"
android:visibility="gone"
android:autofillHints="password"/>
</LinearLayout> <Button
android:id="@+id/button_create"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/create"/>
</RelativeLayout> </LinearLayout>

View File

@ -1,54 +1,46 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="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=".LoginFragment"> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center_vertical">
<LinearLayout <sushi.hardcore.aira.widgets.Avatar
android:id="@+id/avatar"
android:layout_height="@dimen/identityAvatarSize"
android:layout_width="@dimen/identityAvatarSize"
app:textSize="20sp"
android:layout_gravity="center"/>
<TextView
android:id="@+id/text_identity_name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:textAlignment="center"
android:layout_marginHorizontal="50dp" android:textStyle="bold"
android:layout_centerInParent="true"> android:textSize="25sp"
android:layout_marginTop="10dp"/>
<sushi.hardcore.aira.widgets.TextAvatar <TextView
android:id="@+id/text_avatar" android:layout_width="match_parent"
android:layout_height="90dp" android:layout_height="wrap_content"
android:layout_width="90dp" android:text="@string/enter_password"
app:textSize="20sp" style="@style/Label"/>
android:layout_gravity="center"/>
<TextView
android:id="@+id/text_identity_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textStyle="bold"
android:textSize="25sp"
android:layout_marginTop="10dp"/>
<TextView <EditText
android:layout_width="match_parent" android:id="@+id/edit_password"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="@string/enter_password" android:layout_height="wrap_content"
style="@style/Label"/> android:inputType="textPassword"
android:hint="@string/password_hint"
android:autofillHints="password"/>
<EditText <Button
android:id="@+id/edit_password" android:id="@+id/button_login"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="textPassword" android:layout_gravity="center"
android:hint="@string/password_hint"/> android:text="@string/login"/>
<Button </LinearLayout>
android:id="@+id/button_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/login"/>
</LinearLayout>
</RelativeLayout>

View File

@ -12,8 +12,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<sushi.hardcore.aira.widgets.TextAvatar <sushi.hardcore.aira.widgets.Avatar
android:id="@+id/text_avatar" android:id="@+id/avatar"
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="40dp"
app:textSize="8sp" app:textSize="8sp"

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/round_background">
<TextView
android:id="@+id/text_letter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
</RelativeLayout>

View File

@ -33,8 +33,8 @@
android:title="@string/details"/> android:title="@string/details"/>
<item <item
android:id="@+id/refresh_name" android:id="@+id/refresh_profile"
app:showAsAction="never" app:showAsAction="never"
android:title="@string/refresh_name"/> android:title="@string/refresh_profile"/>
</menu> </menu>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<declare-styleable name="TextAvatar"> <declare-styleable name="Avatar">
<attr name="textSize" format="dimension"/> <attr name="textSize" format="dimension"/>
</declare-styleable> </declare-styleable>
</resources> </resources>

View File

@ -2,4 +2,5 @@
<resources> <resources>
<dimen name="image_button_size">30dp</dimen> <dimen name="image_button_size">30dp</dimen>
<dimen name="field_value_margin_start">5sp</dimen> <dimen name="field_value_margin_start">5sp</dimen>
<dimen name="identityAvatarSize">130dp</dimen>
</resources> </resources>

View File

@ -77,14 +77,28 @@
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="log_out">Log out</string> <string name="log_out">Log out</string>
<string name="loadFile_failed">File extraction failed</string> <string name="loadFile_failed">File extraction failed</string>
<string name="fingerprint_copied">Fingerprint copied to clipboard</string> <string name="copied">Copied to clipboard !</string>
<string name="identity">Identity</string> <string name="identity">Identity</string>
<string name="about">About</string> <string name="about">About</string>
<string name="version">AIRA version</string> <string name="version">AIRA version</string>
<string name="refresh_name">Refresh name</string> <string name="refresh_profile">Refresh profile</string>
<string name="security">Security</string> <string name="security">Security</string>
<string name="use_psec_padding">Use PSEC padding</string> <string name="use_psec_padding">Use PSEC padding</string>
<string name="psec_padding_summary">PSEC padding obfuscates the length of your messages but uses more network bandwidth.</string> <string name="psec_padding_summary">PSEC padding obfuscates the length of your messages but uses more network bandwidth.</string>
<string name="is_contact">Is contact:</string> <string name="is_contact">Is contact:</string>
<string name="is_verified">Is verified:</string> <string name="is_verified">Is verified:</string>
<string name="avatar_too_large">Avatar cannot be larger than 10MB.</string>
<string name="your_avatar">Your avatar:</string>
<string name="set_a_new_one">Set a new one</string>
<string name="remove">Remove</string>
<string name="choose_avatar">Choose avatar</string>
<string name="message_hint">Send a message…</string>
<!--accessibility strings-->
<string name="send_file">Send file</string>
<string name="trust_level_indicator">Trust level indicator</string>
<string name="send_message">Send message</string>
<string name="show_your_ips">Show your IPs</string>
<string name="clickable_indicator">Clickable indicator</string>
<string name="avatar">Avatar</string>
</resources> </resources>

View File

@ -22,4 +22,8 @@
<item name="windowActionBar">false</item> <item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item> <item name="windowNoTitle">true</item>
</style> </style>
<style name="CustomAlertDialog" parent="Theme.AppCompat.Dialog.Alert">
<item name="colorAccent">@color/secondary</item>
<item name="android:background">@color/backgroundColor</item>
</style>
</resources> </resources>

View File

@ -3,6 +3,12 @@
<PreferenceCategory android:title="@string/identity"> <PreferenceCategory android:title="@string/identity">
<Preference
android:key="identityAvatar"
android:title="Identity Avatar"
android:summary="The avatar of your identity. Shown to all active sessions."
android:icon="@drawable/ic_face"/>
<EditTextPreference <EditTextPreference
android:key="identityName" android:key="identityName"
android:title="@string/identity_name" android:title="@string/identity_name"

View File

@ -1,6 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.5.0" ext.kotlin_version = "1.5.10"
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@ -8,9 +7,6 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.2.1' 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
// in the individual module build.gradle files
} }
} }