Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
19ae8c6092 | |||
fcc7d64b9d | |||
81dc567ff0 | |||
0dc19529f6 | |||
eb36a8e879 | |||
2b50ffa409 | |||
928f97dc97 | |||
8354490940 | |||
c6f1e6d432 | |||
eac227085d | |||
fc46326deb | |||
3562362916 | |||
cc31423cc4 | |||
b20fded45b | |||
5fc3f7d0cf | |||
d618eb87c7 | |||
37e4c0a105 | |||
f186dbd0cb | |||
4cb127004f | |||
e06044acb8 | |||
05a9f5881d | |||
5a2a9ccc4f | |||
335e786f79 | |||
f7418cf9c2 | |||
38b322ebc5 | |||
0d3848360b | |||
8f33a640ce |
23
README.md
23
README.md
@ -3,6 +3,12 @@ AIRA is peer-to-peer encrypted communication tool for local networks built on th
|
||||
|
||||
Here is the Android version. You can find the original AIRA desktop version [here](https://forge.chapril.org/hardcoresushi/AIRA).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Screenshot of the main screen of AIRA-android, with Bob online and Angerfist and Barack Obama as contacts" src="https://forge.chapril.org/hardcoresushi/AIRA-android/raw/branch/master/screenshots/1.png" height="550"/>
|
||||
<img alt="Screenshot of a conversation between Alice and Bob about AIRA" src="https://forge.chapril.org/hardcoresushi/AIRA-android/raw/branch/master/screenshots/2.png" height="550"/>
|
||||
<img alt="Screenshot of the settings screen of AIRA-android" src="https://forge.chapril.org/hardcoresushi/AIRA-android/raw/branch/master/screenshots/3.png" height="550"/>
|
||||
</p>
|
||||
|
||||
# Disclaimer
|
||||
AIRA is still under developement and is not ready for production usage yet. Not all features have been implemented and bugs are expected. Neither the code or the PSEC protocol received any security audit and therefore shouldn't be considered fully secure. AIRA is provided "as is", without any warranty of any kind.
|
||||
|
||||
@ -17,17 +23,28 @@ AIRA is still under developement and is not ready for production usage yet. Not
|
||||
- IPv4/v6 compatibility
|
||||
- Free/Libre and Open Source
|
||||
|
||||
# Download
|
||||
AIRA releases are availables in the "Release" section. All APKs are signed with my PGP key available on keyservers. To download it:
|
||||
|
||||
`gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys AFE384344A45E13A` \
|
||||
Fingerprint: `B64E FE86 CEE1 D054 F082 1711 AFE3 8434 4A45 E13A` \
|
||||
Email: `Hardcore Sushi <hardcore.sushi@disroot.org>`
|
||||
|
||||
Then, verify APK: `gpg --verify AIRA.apk.asc AIRA.apk`
|
||||
|
||||
__Don't install the APK if the verification fails!__
|
||||
|
||||
# Build
|
||||
### Install Rust
|
||||
AIRA android uses some code from the desktop version which is written in Rust. Therefore, you need to compile this Rust code first.
|
||||
```
|
||||
curl --proto '=https' --tlsv1.3 -sSf https://sh.rustup.rs | sh
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
|
||||
```
|
||||
### Install NDK
|
||||
The Rust code uses a crate called "rusqlite" to store data in SQLite databases. This crates uses the original SQLite3 library written in C. Therefore, to compile it you need the Android NDK. You can find instructions to install the NDK here: https://developer.android.com/ndk/guides
|
||||
We also need the Android NDK to cross-compile the rust code to Android. Currently, only versions up to __r22b__ are supported. You can find instructions to install the NDK here: https://developer.android.com/ndk/guides
|
||||
|
||||
Once installed, you need to define the $ANDROID_NDK_HOME environment variable (if not already set):
|
||||
Once installed, you need to define the `$ANDROID_NDK_HOME` environment variable (if not already set):
|
||||
```
|
||||
export ANDROID_NDK_HOME=/home/<user>/Android/SDK/ndk/<NDK version>"
|
||||
```
|
||||
|
@ -4,20 +4,20 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion "32.0.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "sushi.hardcore.aira"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "0.0.1"
|
||||
targetSdkVersion 32
|
||||
versionCode 3
|
||||
versionName "0.1.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
ndk {
|
||||
abiFilters "x86", "armeabi-v7a", "arm64-v8a"
|
||||
abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a"
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled false // curve25519-android doesn't seem to support minification
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
@ -42,20 +42,21 @@ android {
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
namespace 'sushi.hardcore.aira'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.5.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.4"
|
||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation "androidx.fragment:fragment-ktx:1.4.1"
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
implementation 'com.google.android.material:material:1.6.0'
|
||||
|
||||
implementation 'net.i2p.crypto:eddsa:0.3.0'
|
||||
implementation "org.whispersystems:curve25519-android:0.5.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="sushi.hardcore.aira"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove"/>
|
||||
|
||||
<application
|
||||
android:icon="@drawable/ic_launcher"
|
||||
@ -27,11 +28,18 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".background_service.SystemBroadcastReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity android:name=".ChatActivity" android:theme="@style/Theme.AIRA.NoActionBar"/>
|
||||
<activity android:name=".MainActivity" android:theme="@style/Theme.AIRA.NoActionBar"/>
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
|
@ -3,7 +3,9 @@ package sushi.hardcore.aira
|
||||
import sushi.hardcore.aira.background_service.Contact
|
||||
|
||||
object AIRADatabase {
|
||||
external fun initLogging(): Boolean
|
||||
external fun isIdentityProtected(databaseFolder: String): Boolean
|
||||
external fun getIdentityName(databaseFolder: String): String?
|
||||
external fun loadIdentity(databaseFolder: String, password: ByteArray?): Boolean
|
||||
external fun addContact(name: String, avatarUuid: String?, publicKey: ByteArray): Contact?
|
||||
external fun removeContact(uuid: String): Boolean
|
||||
@ -12,7 +14,7 @@ object AIRADatabase {
|
||||
external fun setContactSeen(contactUuid: String, seen: Boolean): Boolean
|
||||
external fun changeContactName(contactUuid: String, newName: String): Boolean
|
||||
external fun setContactAvatar(contactUuid: String, avatarUuid: String?): Boolean
|
||||
external fun storeMsg(contactUuid: String, outgoing: Boolean, data: ByteArray): Boolean
|
||||
external fun storeMsg(contactUuid: String, outgoing: Boolean, timestamp: Long, data: ByteArray): Boolean
|
||||
external fun storeFile(contactUuid: String?, data: ByteArray): ByteArray?
|
||||
external fun loadMsgs(uuid: String, offset: Int, count: Int): ArrayList<ChatItem>?
|
||||
external fun loadFile(rawUuid: ByteArray): ByteArray?
|
||||
@ -30,6 +32,11 @@ object AIRADatabase {
|
||||
external fun removeIdentityAvatar(databaseFolder: String): Boolean
|
||||
external fun getIdentityAvatar(databaseFolder: String): ByteArray?
|
||||
|
||||
fun init() {
|
||||
System.loadLibrary("aira")
|
||||
initLogging()
|
||||
}
|
||||
|
||||
fun loadAvatar(avatarUuid: String?): ByteArray? {
|
||||
return avatarUuid?.let {
|
||||
getAvatar(it)
|
||||
|
@ -13,10 +13,9 @@ import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sushi.hardcore.aira.adapters.ChatAdapter
|
||||
import sushi.hardcore.aira.adapters.FuckRecyclerView
|
||||
import sushi.hardcore.aira.background_service.*
|
||||
import sushi.hardcore.aira.databinding.ActivityChatBinding
|
||||
import sushi.hardcore.aira.databinding.DialogFingerprintsBinding
|
||||
@ -35,7 +34,108 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
private var lastLoadedMessageOffset = 0
|
||||
private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
|
||||
if (isServiceInitialized() && uris.size > 0) {
|
||||
airaService.sendFilesFromUris(sessionId, uris)
|
||||
airaService.sendFilesFromUris(sessionId, uris) { buffer ->
|
||||
chatAdapter.newMessage(ChatItem(true, 0, buffer))
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
private val uiCallbacks = object : AIRAService.UiCallbacks {
|
||||
override fun onConnectFailed(ip: String, errorMsg: String?) {}
|
||||
override fun onNewSession(sessionId: Int, ip: String) {
|
||||
if (this@ChatActivity.sessionId == sessionId) {
|
||||
val contact = airaService.contacts[sessionId]
|
||||
val hasPendingMsgs = airaService.pendingMsgs[sessionId]?.size ?: 0 > 0
|
||||
runOnUiThread {
|
||||
if (contact == null) {
|
||||
binding.bottomPanel.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.offlineWarning.visibility = View.GONE
|
||||
if (hasPendingMsgs) {
|
||||
binding.sendingPendingMsgsIndicator.visibility = View.VISIBLE
|
||||
chatAdapter.removePendingMessages()
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onSessionDisconnect(sessionId: Int) {
|
||||
if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
if (airaService.isContact(sessionId)) {
|
||||
binding.offlineWarning.visibility = View.VISIBLE
|
||||
} else {
|
||||
hideBottomPanel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onNameTold(sessionId: Int, name: String) {
|
||||
if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
sessionName = name
|
||||
binding.toolbar.title.text = name
|
||||
if (avatar == null) {
|
||||
binding.toolbar.avatar.setTextAvatar(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
|
||||
if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
this@ChatActivity.avatar = avatar
|
||||
if (avatar == null) {
|
||||
binding.toolbar.avatar.setTextAvatar(sessionName)
|
||||
} else {
|
||||
binding.toolbar.avatar.setImageAvatar(avatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onSent(sessionId: Int, timestamp: Long, buffer: ByteArray) {
|
||||
if (this@ChatActivity.sessionId == sessionId) {
|
||||
if (airaService.isContact(sessionId)) {
|
||||
lastLoadedMessageOffset += 1
|
||||
}
|
||||
runOnUiThread {
|
||||
chatAdapter.newMessage(ChatItem(true, timestamp, buffer))
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onPendingMessagesSent(sessionId: Int) {
|
||||
if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
binding.sendingPendingMsgsIndicator.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean {
|
||||
return if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
chatAdapter.newMessage(ChatItem(false, timestamp, data))
|
||||
scrollToBottom()
|
||||
}
|
||||
if (airaService.isContact(sessionId)) {
|
||||
lastLoadedMessageOffset += 1
|
||||
}
|
||||
!airaService.isAppInBackground
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
override fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean {
|
||||
return if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
filesReceiver.ask(this@ChatActivity, sessionName ?: airaService.sessions[sessionId]!!.ip)
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +151,7 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
chatAdapter = ChatAdapter(this@ChatActivity, ::onClickSaveFile)
|
||||
binding.recyclerChat.apply {
|
||||
adapter = chatAdapter
|
||||
layoutManager = LinearLayoutManager(this@ChatActivity, LinearLayoutManager.VERTICAL, false).apply {
|
||||
layoutManager = FuckRecyclerView(this@ChatActivity).apply {
|
||||
stackFromEnd = true
|
||||
}
|
||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
@ -75,13 +175,11 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
}
|
||||
binding.buttonSend.setOnClickListener {
|
||||
val msg = binding.editMessage.text.toString()
|
||||
airaService.sendTo(sessionId, Protocol.newMessage(msg))
|
||||
binding.editMessage.text.clear()
|
||||
chatAdapter.newMessage(ChatItem(true, Protocol.newMessage(msg)))
|
||||
if (airaService.contacts.contains(sessionId)) {
|
||||
lastLoadedMessageOffset += 1
|
||||
if (!airaService.sendOrAddToPending(sessionId, Protocol.newMessage(msg))) {
|
||||
chatAdapter.newMessage(ChatItem(true, 0, Protocol.newMessage(msg)))
|
||||
scrollToBottom()
|
||||
}
|
||||
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
||||
}
|
||||
binding.buttonAttach.setOnClickListener {
|
||||
filePicker.launch("*/*")
|
||||
@ -94,9 +192,8 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
val session = airaService.sessions[sessionId]
|
||||
val contact = airaService.contacts[sessionId]
|
||||
if (session == null && contact == null) { //may happen when resuming activity after session disconnect
|
||||
onDisconnected()
|
||||
hideBottomPanel()
|
||||
} else {
|
||||
chatAdapter.clear()
|
||||
val avatar = if (contact == null) {
|
||||
displayIconTrustLevel(false, false)
|
||||
sessionName = airaService.savedNames[sessionId]
|
||||
@ -116,93 +213,53 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
binding.toolbar.avatar.setImageAvatar(image)
|
||||
}
|
||||
}
|
||||
chatAdapter.clear()
|
||||
lastLoadedMessageOffset = 0
|
||||
if (contact != null) {
|
||||
loadMsgs(contact.uuid)
|
||||
}
|
||||
airaService.savedMsgs[sessionId]?.let {
|
||||
for (chatItem in it.asReversed()) {
|
||||
chatAdapter.newLoadedMessage(chatItem)
|
||||
for (msg in it.asReversed()) {
|
||||
chatAdapter.newLoadedMessage(ChatItem(msg.outgoing, msg.timestamp, msg.data))
|
||||
}
|
||||
}
|
||||
var hasPendingMsgs = false
|
||||
airaService.pendingMsgs[sessionId]?.let {
|
||||
if (it.size > 0) {
|
||||
hasPendingMsgs = true
|
||||
for (msg in it) {
|
||||
if (msg[0] == Protocol.MESSAGE ||msg[0] == Protocol.FILE) {
|
||||
chatAdapter.newMessage(ChatItem(true, 0, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chatAdapter.itemCount > 0) {
|
||||
scrollToBottom()
|
||||
}
|
||||
airaService.receiveFileTransfers[sessionId]?.let {
|
||||
if (it.shouldAsk) {
|
||||
it.ask(this@ChatActivity, ipName)
|
||||
}
|
||||
}
|
||||
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
||||
if (airaService.isOnline(sessionId)) {
|
||||
binding.bottomPanel.visibility = View.VISIBLE
|
||||
binding.recyclerChat.updatePadding(bottom = 0)
|
||||
binding.sendingPendingMsgsIndicator.visibility = if (session == null) {
|
||||
if (contact == null) {
|
||||
hideBottomPanel()
|
||||
} else {
|
||||
binding.offlineWarning.visibility = View.VISIBLE
|
||||
}
|
||||
View.GONE
|
||||
} else {
|
||||
onDisconnected()
|
||||
binding.offlineWarning.visibility = View.GONE
|
||||
if (hasPendingMsgs) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
airaService.setSeen(sessionId, true)
|
||||
}
|
||||
airaService.uiCallbacks = object : AIRAService.UiCallbacks {
|
||||
override fun onConnectFailed(ip: String, errorMsg: String?) {}
|
||||
override fun onNewSession(sessionId: Int, ip: String) {
|
||||
if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
binding.bottomPanel.visibility = View.VISIBLE
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onSessionDisconnect(sessionId: Int) {
|
||||
if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
onDisconnected()
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onNameTold(sessionId: Int, name: String) {
|
||||
if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
sessionName = name
|
||||
binding.toolbar.title.text = name
|
||||
if (avatar == null) {
|
||||
binding.toolbar.avatar.setTextAvatar(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
|
||||
if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
this@ChatActivity.avatar = avatar
|
||||
if (avatar == null) {
|
||||
binding.toolbar.avatar.setTextAvatar(sessionName)
|
||||
} else {
|
||||
binding.toolbar.avatar.setImageAvatar(avatar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onNewMessage(sessionId: Int, data: ByteArray): Boolean {
|
||||
return if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
chatAdapter.newMessage(ChatItem(false, data))
|
||||
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
||||
}
|
||||
if (airaService.contacts.contains(sessionId)) {
|
||||
lastLoadedMessageOffset += 1
|
||||
}
|
||||
!airaService.isAppInBackground
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
override fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean {
|
||||
return if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
filesReceiver.ask(this@ChatActivity, sessionName ?: airaService.sessions[sessionId]!!.ip)
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
airaService.uiCallbacks = uiCallbacks
|
||||
airaService.isAppInBackground = false
|
||||
}
|
||||
override fun onServiceDisconnected(name: ComponentName?) {}
|
||||
@ -210,7 +267,7 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDisconnected() {
|
||||
private fun hideBottomPanel() {
|
||||
val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputManager.hideSoftInputFromWindow(binding.editMessage.windowToken, 0)
|
||||
binding.bottomPanel.visibility = View.GONE
|
||||
@ -253,6 +310,13 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToBottom() {
|
||||
val target = chatAdapter.itemCount-1
|
||||
if (target >= 0) {
|
||||
binding.recyclerChat.smoothScrollToPosition(target)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.chat_activity, menu)
|
||||
val contact = airaService.contacts[sessionId]
|
||||
@ -299,6 +363,10 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
if (airaService.removeContact(sessionId)) {
|
||||
invalidateOptionsMenu()
|
||||
displayIconTrustLevel(false, false)
|
||||
if (!airaService.isOnline(sessionId)) {
|
||||
hideBottomPanel()
|
||||
binding.offlineWarning.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
@ -340,7 +408,7 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
true
|
||||
}
|
||||
R.id.refresh_profile -> {
|
||||
airaService.sendTo(sessionId, Protocol.askProfileInfo())
|
||||
airaService.sendOrAddToPending(sessionId, Protocol.askProfileInfo())
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
@ -354,21 +422,12 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
lastLoadedMessageOffset = 0
|
||||
}
|
||||
|
||||
private fun onClickSaveFile(fileName: String, rawUuid: ByteArray) {
|
||||
val buffer = AIRADatabase.loadFile(rawUuid)
|
||||
if (buffer == null) {
|
||||
Toast.makeText(this, R.string.loadFile_failed, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
FileUtils.openFileForDownload(this, fileName)?.apply {
|
||||
write(buffer)
|
||||
close()
|
||||
Toast.makeText(this@ChatActivity, R.string.file_saved, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
private fun onClickSaveFile(fileName: String, fileContent: ByteArray) {
|
||||
val file = FileUtils.openFileForDownload(this, fileName)
|
||||
file.outputStream?.apply {
|
||||
write(fileContent)
|
||||
close()
|
||||
Toast.makeText(this@ChatActivity, getString(R.string.file_saved, file.fileName), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,17 +1,32 @@
|
||||
package sushi.hardcore.aira
|
||||
|
||||
import sushi.hardcore.aira.background_service.Protocol
|
||||
import java.util.*
|
||||
|
||||
class ChatItem(val outgoing: Boolean, val data: ByteArray) {
|
||||
class ChatItem(val outgoing: Boolean, val timestamp: Long, val data: ByteArray) {
|
||||
companion object {
|
||||
const val OUTGOING_MESSAGE = 0
|
||||
const val INCOMING_MESSAGE = 1
|
||||
const val OUTGOING_FILE = 2
|
||||
const val INCOMING_FILE = 3
|
||||
}
|
||||
val itemType = if (data[0] == Protocol.MESSAGE) {
|
||||
if (outgoing) OUTGOING_MESSAGE else INCOMING_MESSAGE
|
||||
} else {
|
||||
if (outgoing) OUTGOING_FILE else INCOMING_FILE
|
||||
val itemType: Int by lazy {
|
||||
if (data[0] == Protocol.MESSAGE) {
|
||||
if (outgoing) OUTGOING_MESSAGE else INCOMING_MESSAGE
|
||||
} else {
|
||||
if (outgoing) OUTGOING_FILE else INCOMING_FILE
|
||||
}
|
||||
}
|
||||
|
||||
val calendar: Calendar by lazy {
|
||||
Calendar.getInstance().apply {
|
||||
time = Date(timestamp * 1000)
|
||||
}
|
||||
}
|
||||
val year by lazy {
|
||||
calendar.get(Calendar.YEAR)
|
||||
}
|
||||
val dayOfYear by lazy {
|
||||
calendar.get(Calendar.DAY_OF_YEAR)
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ object Constants {
|
||||
const val port = 7530
|
||||
const val mDNSServiceName = "AIRA Node"
|
||||
const val mDNSServiceType = "_aira._tcp"
|
||||
const val fileSizeLimit = 32760000
|
||||
const val fileSizeLimit = 16380000
|
||||
const val MSG_LOADING_COUNT = 20
|
||||
const val FILE_CHUNK_SIZE = 1023996
|
||||
const val MAX_AVATAR_SIZE = 10000000
|
||||
|
@ -9,6 +9,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.bumptech.glide.Glide
|
||||
import sushi.hardcore.aira.databinding.FragmentCreateIdentityBinding
|
||||
import sushi.hardcore.aira.utils.AvatarPicker
|
||||
|
||||
@ -25,11 +26,9 @@ class CreateIdentityFragment(private val activity: AppCompatActivity) : Fragment
|
||||
}
|
||||
}
|
||||
|
||||
private val avatarPicker = AvatarPicker(activity) { picker, avatar ->
|
||||
picker.setOnAvatarCompressed { compressedAvatar ->
|
||||
AIRADatabase.setIdentityAvatar(Constants.getDatabaseFolder(activity), compressedAvatar)
|
||||
}
|
||||
avatar.circleCrop().into(binding.avatar)
|
||||
private val avatarPicker = AvatarPicker(activity) { avatar ->
|
||||
AIRADatabase.setIdentityAvatar(Constants.getDatabaseFolder(activity), avatar)
|
||||
Glide.with(this).load(avatar).circleCrop().into(binding.avatar)
|
||||
}
|
||||
private lateinit var binding: FragmentCreateIdentityBinding
|
||||
|
||||
@ -80,7 +79,7 @@ class CreateIdentityFragment(private val activity: AppCompatActivity) : Fragment
|
||||
bundle.getBinder(LoginActivity.BINDER_ARG)?.let { binder ->
|
||||
val databaseFolder = Constants.getDatabaseFolder(requireContext())
|
||||
if (createNewIdentity(databaseFolder, identityName, password)) {
|
||||
(binder as LoginActivity.ActivityLauncher).launch(identityName)
|
||||
(binder as LoginActivity.ActivityLauncher).launch()
|
||||
success = true
|
||||
}
|
||||
}
|
||||
|
@ -9,21 +9,18 @@ import sushi.hardcore.aira.background_service.AIRAService
|
||||
import java.io.File
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
private external fun getIdentityName(databaseFolder: String): String?
|
||||
|
||||
companion object {
|
||||
const val NAME_ARG = "identityName"
|
||||
const val BINDER_ARG = "binder"
|
||||
private external fun initLogging()
|
||||
init {
|
||||
System.loadLibrary("aira")
|
||||
initLogging()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
AIRADatabase.init()
|
||||
}
|
||||
|
||||
inner class ActivityLauncher: Binder() {
|
||||
fun launch(identityName: String) {
|
||||
startMainActivity(identityName)
|
||||
fun launch() {
|
||||
startMainActivity()
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,13 +35,13 @@ class LoginActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
val isProtected = AIRADatabase.isIdentityProtected(databaseFolder)
|
||||
val name = getIdentityName(databaseFolder)
|
||||
val name = AIRADatabase.getIdentityName(databaseFolder)
|
||||
if (AIRAService.isServiceRunning) {
|
||||
startMainActivity(null)
|
||||
startMainActivity()
|
||||
} else if (name != null && !isProtected) {
|
||||
if (AIRADatabase.loadIdentity(databaseFolder, null)) {
|
||||
AIRADatabase.clearCache()
|
||||
startMainActivity(name)
|
||||
startMainActivity()
|
||||
} else {
|
||||
Toast.makeText(this, R.string.identity_load_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -62,11 +59,10 @@ class LoginActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startMainActivity(identityName: String?) {
|
||||
private fun startMainActivity() {
|
||||
val mainActivityIntent = Intent(this, MainActivity::class.java)
|
||||
mainActivityIntent.action = intent.action
|
||||
mainActivityIntent.putExtras(intent)
|
||||
mainActivityIntent.putExtra(NAME_ARG, identityName)
|
||||
startActivity(mainActivityIntent)
|
||||
finish()
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ class LoginFragment : Fragment() {
|
||||
binding.buttonLogin.setOnClickListener {
|
||||
if (AIRADatabase.loadIdentity(databaseFolder, binding.editPassword.text.toString().toByteArray())) {
|
||||
AIRADatabase.clearCache()
|
||||
(binder as LoginActivity.ActivityLauncher).launch(name)
|
||||
(binder as LoginActivity.ActivityLauncher).launch()
|
||||
} else {
|
||||
Toast.makeText(activity, R.string.identity_load_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AbsListView
|
||||
import android.widget.AdapterView
|
||||
import android.widget.Toast
|
||||
@ -17,6 +16,7 @@ import sushi.hardcore.aira.adapters.Session
|
||||
import sushi.hardcore.aira.adapters.SessionAdapter
|
||||
import sushi.hardcore.aira.background_service.AIRAService
|
||||
import sushi.hardcore.aira.background_service.FilesReceiver
|
||||
import sushi.hardcore.aira.background_service.NotificationBroadcastReceiver
|
||||
import sushi.hardcore.aira.databinding.ActivityMainBinding
|
||||
import sushi.hardcore.aira.databinding.DialogIpAddressesBinding
|
||||
import sushi.hardcore.aira.utils.FileUtils
|
||||
@ -67,19 +67,19 @@ class MainActivity : ServiceBoundActivity() {
|
||||
onlineSessionAdapter.setName(sessionId, name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
|
||||
runOnUiThread {
|
||||
onlineSessionAdapter.setAvatar(sessionId, avatar)
|
||||
}
|
||||
}
|
||||
override fun onNewMessage(sessionId: Int, data: ByteArray): Boolean {
|
||||
override fun onSent(sessionId: Int, timestamp: Long, buffer: ByteArray) {}
|
||||
override fun onPendingMessagesSent(sessionId: Int) {}
|
||||
override fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean {
|
||||
runOnUiThread {
|
||||
onlineSessionAdapter.setSeen(sessionId, false)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean {
|
||||
runOnUiThread {
|
||||
filesReceiver.ask(this@MainActivity, airaService.getNameOf(sessionId))
|
||||
@ -94,11 +94,6 @@ class MainActivity : ServiceBoundActivity() {
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.toolbar.toolbar)
|
||||
|
||||
val identityName = intent.getStringExtra(LoginActivity.NAME_ARG)
|
||||
identityName?.let {
|
||||
initToolbar(it)
|
||||
}
|
||||
|
||||
val openedToShareFile = intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE
|
||||
|
||||
onlineSessionAdapter = SessionAdapter(this)
|
||||
@ -121,30 +116,28 @@ class MainActivity : ServiceBoundActivity() {
|
||||
}
|
||||
setOnScrollListener(onSessionsScrollListener)
|
||||
}
|
||||
if (openedToShareFile) {
|
||||
binding.offlineSessions.visibility = View.GONE
|
||||
binding.textOfflineSessions.visibility = View.GONE
|
||||
} else {
|
||||
offlineSessionAdapter = SessionAdapter(this)
|
||||
binding.offlineSessions.apply {
|
||||
adapter = offlineSessionAdapter
|
||||
onItemClickListener = if (openedToShareFile) {
|
||||
onSessionsItemClickSendFile
|
||||
} else {
|
||||
AdapterView.OnItemClickListener { _, _, position, _ ->
|
||||
if (isSelecting()) {
|
||||
changeSelection(offlineSessionAdapter!!, position)
|
||||
} else {
|
||||
launchChatActivity(offlineSessionAdapter!!.getItem(position))
|
||||
}
|
||||
offlineSessionAdapter = SessionAdapter(this)
|
||||
binding.offlineSessions.apply {
|
||||
adapter = offlineSessionAdapter
|
||||
onItemClickListener = if (openedToShareFile) {
|
||||
onSessionsItemClickSendFile
|
||||
} else {
|
||||
AdapterView.OnItemClickListener { _, _, position, _ ->
|
||||
if (isSelecting()) {
|
||||
changeSelection(offlineSessionAdapter!!, position)
|
||||
} else {
|
||||
launchChatActivity(offlineSessionAdapter!!.getItem(position))
|
||||
}
|
||||
}
|
||||
onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, position, _ ->
|
||||
changeSelection(offlineSessionAdapter!!, position)
|
||||
true
|
||||
}
|
||||
setOnScrollListener(onSessionsScrollListener)
|
||||
}
|
||||
onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, position, _ ->
|
||||
changeSelection(offlineSessionAdapter!!, position)
|
||||
true
|
||||
}
|
||||
setOnScrollListener(onSessionsScrollListener)
|
||||
}
|
||||
if (intent.action == NotificationBroadcastReceiver.ACTION_LOGOUT) {
|
||||
askLogOut()
|
||||
}
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
|
||||
@ -153,12 +146,10 @@ class MainActivity : ServiceBoundActivity() {
|
||||
airaService.uiCallbacks = uiCallbacks
|
||||
airaService.isAppInBackground = false
|
||||
refreshSessions()
|
||||
if (AIRAService.isServiceRunning) {
|
||||
airaService.identityName?.let { initToolbar(it) }
|
||||
} else {
|
||||
airaService.identityName = identityName
|
||||
if (!AIRAService.isServiceRunning) {
|
||||
startService(serviceIntent)
|
||||
}
|
||||
initToolbar(airaService.identityName)
|
||||
}
|
||||
override fun onServiceDisconnected(name: ComponentName?) {}
|
||||
}
|
||||
@ -197,7 +188,10 @@ class MainActivity : ServiceBoundActivity() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.main_activity, menu)
|
||||
menu.findItem(R.id.remove_contact).isVisible = isSelecting()
|
||||
val isSelecting = isSelecting()
|
||||
menu.findItem(R.id.settings).isVisible = !isSelecting
|
||||
menu.findItem(R.id.close).isVisible = !isSelecting
|
||||
menu.findItem(R.id.remove_contact).isVisible = isSelecting
|
||||
return true
|
||||
}
|
||||
|
||||
@ -209,18 +203,7 @@ class MainActivity : ServiceBoundActivity() {
|
||||
}
|
||||
R.id.close -> {
|
||||
if (isServiceInitialized()) {
|
||||
AlertDialog.Builder(this, R.style.CustomAlertDialog)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.ask_log_out)
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
airaService.logOut()
|
||||
if (AIRADatabase.isIdentityProtected(Constants.getDatabaseFolder(this))) {
|
||||
startActivity(Intent(this, LoginActivity::class.java))
|
||||
}
|
||||
finish()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
askLogOut()
|
||||
}
|
||||
true
|
||||
}
|
||||
@ -334,6 +317,21 @@ class MainActivity : ServiceBoundActivity() {
|
||||
})
|
||||
}
|
||||
|
||||
private fun askLogOut() {
|
||||
AlertDialog.Builder(this, R.style.CustomAlertDialog)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.ask_log_out)
|
||||
.setPositiveButton(R.string.yes) { _, _ ->
|
||||
airaService.logOut()
|
||||
if (AIRADatabase.isIdentityProtected(Constants.getDatabaseFolder(this))) {
|
||||
startActivity(Intent(this, LoginActivity::class.java))
|
||||
}
|
||||
finish()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askShareFileTo(session: Session) {
|
||||
var uris: ArrayList<Uri>? = null
|
||||
when (intent.action) {
|
||||
@ -346,13 +344,15 @@ class MainActivity : ServiceBoundActivity() {
|
||||
Toast.makeText(this, R.string.open_uri_failed, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
val msg = if (uris!!.size == 1) {
|
||||
val sendFile = FileUtils.openFileFromUri(this, uris!![0])
|
||||
if (sendFile == null) {
|
||||
Toast.makeText(this, R.string.open_uri_failed, Toast.LENGTH_SHORT).show()
|
||||
val result = FileUtils.openFileFromUri(this, uris!![0])
|
||||
if (result.file == null) {
|
||||
if (!result.errorHandled) {
|
||||
Toast.makeText(this, R.string.open_uri_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return
|
||||
} else {
|
||||
sendFile.inputStream.close()
|
||||
getString(R.string.ask_send_single_file, sendFile.fileName, FileUtils.formatSize(sendFile.fileSize), session.name ?: session.ip)
|
||||
result.file.inputStream.close()
|
||||
getString(R.string.ask_send_single_file, result.file.fileName, FileUtils.formatSize(result.file.fileSize), session.name ?: session.ip)
|
||||
}
|
||||
} else {
|
||||
getString(R.string.ask_send_multiple_files, uris!!.size, session.name ?: session.ip)
|
||||
|
@ -10,7 +10,6 @@ import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
@ -28,16 +27,16 @@ import sushi.hardcore.aira.utils.StringUtils
|
||||
|
||||
class SettingsActivity: AppCompatActivity() {
|
||||
class MySettingsFragment(private val activity: AppCompatActivity): PreferenceFragmentCompat() {
|
||||
private lateinit var databaseFolder: String
|
||||
private lateinit var airaService: AIRAService
|
||||
private val avatarPicker = AvatarPicker(activity) { picker, avatar ->
|
||||
private val avatarPicker = AvatarPicker(activity) { avatar ->
|
||||
if (::airaService.isInitialized) {
|
||||
picker.setOnAvatarCompressed { compressedAvatar ->
|
||||
airaService.changeAvatar(compressedAvatar)
|
||||
}
|
||||
airaService.changeAvatar(avatar)
|
||||
}
|
||||
displayAvatar(avatar)
|
||||
}
|
||||
private lateinit var identityAvatarPreference: Preference
|
||||
private lateinit var startAtBootSwitch: SwitchPreferenceCompat
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
@ -46,10 +45,13 @@ class SettingsActivity: AppCompatActivity() {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
databaseFolder = Constants.getDatabaseFolder(activity)
|
||||
findPreference<Preference>("identityAvatar")?.let { identityAvatarPreference = it }
|
||||
startAtBootSwitch = findPreference("startAtBoot")!!
|
||||
updateStartAtBootSwitch(AIRADatabase.isIdentityProtected(databaseFolder))
|
||||
val paddingPreference = findPreference<SwitchPreferenceCompat>("psecPadding")
|
||||
paddingPreference?.isPersistent = false
|
||||
AIRADatabase.getIdentityAvatar(Constants.getDatabaseFolder(activity))?.let { avatar ->
|
||||
AIRADatabase.getIdentityAvatar(databaseFolder)?.let { avatar ->
|
||||
displayAvatar(avatar)
|
||||
}
|
||||
Intent(activity, AIRAService::class.java).also { serviceIntent ->
|
||||
@ -69,9 +71,9 @@ class SettingsActivity: AppCompatActivity() {
|
||||
avatarPicker.launch()
|
||||
}
|
||||
val dialogBinding = ChangeAvatarDialogBinding.inflate(layoutInflater)
|
||||
val avatar = AIRADatabase.getIdentityAvatar(Constants.getDatabaseFolder(activity))
|
||||
val avatar = AIRADatabase.getIdentityAvatar(databaseFolder)
|
||||
if (avatar == null) {
|
||||
dialogBinding.avatar.setTextAvatar(airaService.identityName!!)
|
||||
dialogBinding.avatar.setTextAvatar(airaService.identityName)
|
||||
} else {
|
||||
dialogBinding.avatar.setImageAvatar(avatar)
|
||||
dialogBuilder.setNegativeButton(R.string.remove) { _, _ ->
|
||||
@ -113,7 +115,7 @@ class SettingsActivity: AppCompatActivity() {
|
||||
findPreference<Preference>("identityPassword")?.setOnPreferenceClickListener {
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_password, null)
|
||||
val oldPasswordEditText = dialogView.findViewById<EditText>(R.id.old_password)
|
||||
val isIdentityProtected = AIRADatabase.isIdentityProtected(Constants.getDatabaseFolder(activity))
|
||||
val isIdentityProtected = AIRADatabase.isIdentityProtected(databaseFolder)
|
||||
if (!isIdentityProtected) {
|
||||
oldPasswordEditText.visibility = View.GONE
|
||||
}
|
||||
@ -124,24 +126,24 @@ class SettingsActivity: AppCompatActivity() {
|
||||
.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)
|
||||
val newPasswordConfirm = newPasswordConfirmEditText.text.toString().toByteArray()
|
||||
if (newPassword.contentEquals(newPasswordConfirm)) {
|
||||
if (newPassword.isEmpty()) {
|
||||
if (isIdentityProtected) { //don't change password if identity is not protected and new password is blank
|
||||
changePassword(isIdentityProtected, oldPasswordEditText, null)
|
||||
}
|
||||
} else {
|
||||
changePassword(isIdentityProtected, oldPasswordEditText, newPassword)
|
||||
}
|
||||
} 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)
|
||||
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()
|
||||
@ -170,26 +172,33 @@ class SettingsActivity: AppCompatActivity() {
|
||||
if (avatar == null) {
|
||||
identityAvatarPreference.setIcon(R.drawable.ic_face)
|
||||
} else {
|
||||
displayAvatar(Glide.with(this).load(avatar))
|
||||
Glide
|
||||
.with(this)
|
||||
.load(avatar)
|
||||
.apply(RequestOptions().override(90)) //reduce image to be the same size as other icons
|
||||
.circleCrop()
|
||||
.into(object : CustomTarget<Drawable>() {
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
identityAvatarPreference.icon = resource
|
||||
}
|
||||
override fun onLoadCleared(placeholder: Drawable?) {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
oldPasswordEditText.text.toString().toByteArray()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (!AIRADatabase.changePassword(Constants.getDatabaseFolder(activity), oldPassword, newPassword)) {
|
||||
if (AIRADatabase.changePassword(databaseFolder, oldPassword, newPassword)) {
|
||||
val isNowIdentityProtected = newPassword != null
|
||||
updateStartAtBootSwitch(isNowIdentityProtected)
|
||||
if (isIdentityProtected && !isNowIdentityProtected ) {
|
||||
startAtBootSwitch.isChecked = true
|
||||
}
|
||||
} else {
|
||||
AlertDialog.Builder(activity, R.style.CustomAlertDialog)
|
||||
.setMessage(R.string.change_password_failed)
|
||||
.setTitle(R.string.error)
|
||||
@ -198,6 +207,15 @@ class SettingsActivity: AppCompatActivity() {
|
||||
}
|
||||
oldPassword?.fill(0)
|
||||
}
|
||||
|
||||
private fun updateStartAtBootSwitch(isIdentityProtected: Boolean) {
|
||||
startAtBootSwitch.isEnabled = !isIdentityProtected
|
||||
startAtBootSwitch.summary = getString(if (isIdentityProtected) {
|
||||
R.string.start_at_boot_summary_identity_protected
|
||||
} else {
|
||||
R.string.start_at_boot_summary
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
@ -1,8 +1,8 @@
|
||||
package sushi.hardcore.aira.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@ -11,10 +11,17 @@ import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateMargins
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sushi.hardcore.aira.AIRADatabase
|
||||
import sushi.hardcore.aira.ChatItem
|
||||
import sushi.hardcore.aira.R
|
||||
import sushi.hardcore.aira.background_service.Protocol
|
||||
import sushi.hardcore.aira.utils.StringUtils
|
||||
import sushi.hardcore.aira.utils.TimeUtils
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
class ChatAdapter(
|
||||
private val context: Context,
|
||||
@ -22,8 +29,11 @@ class ChatAdapter(
|
||||
): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
companion object {
|
||||
const val CONTAINER_MARGIN = 150
|
||||
const val BUBBLE_HORIZONTAL_PADDING = 40
|
||||
const val BUBBLE_MARGIN = 150
|
||||
const val CONTAINER_PADDING = 40
|
||||
const val BUBBLE_VERTICAL_MARGIN = 30
|
||||
const val BUBBLE_CORNER_NORMAL = 50f
|
||||
const val BUBBLE_CORNER_ARROW = 20f
|
||||
}
|
||||
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
@ -31,12 +41,22 @@ class ChatAdapter(
|
||||
|
||||
fun newMessage(chatItem: ChatItem) {
|
||||
chatItems.add(chatItem)
|
||||
notifyItemChanged(chatItems.size-2)
|
||||
notifyItemInserted(chatItems.size-1)
|
||||
}
|
||||
|
||||
fun newLoadedMessage(chatItem: ChatItem) {
|
||||
chatItems.add(0, chatItem)
|
||||
notifyItemInserted(0)
|
||||
notifyItemChanged(1)
|
||||
}
|
||||
|
||||
fun removePendingMessages() {
|
||||
val oldSize = chatItems.size
|
||||
chatItems.removeAll {
|
||||
it.timestamp == 0L
|
||||
}
|
||||
notifyItemRangeRemoved(chatItems.size, oldSize-chatItems.size)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
@ -44,95 +64,205 @@ class ChatAdapter(
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
internal open class BubbleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
|
||||
protected fun setPadding(outgoing: Boolean) {
|
||||
if (outgoing) {
|
||||
itemView.updatePadding(right = BUBBLE_HORIZONTAL_PADDING)
|
||||
} else {
|
||||
itemView.updatePadding(left = BUBBLE_HORIZONTAL_PADDING)
|
||||
internal open class BubbleViewHolder(private val context: Context, itemView: View): RecyclerView.ViewHolder(itemView) {
|
||||
private fun generateCorners(topLeft: Float, topRight: Float, bottomRight: Float, bottomLeft: Float): FloatArray {
|
||||
return floatArrayOf(topLeft, topLeft, topRight, topRight, bottomRight, bottomRight, bottomLeft, bottomLeft)
|
||||
}
|
||||
protected fun setBubbleContent(layoutResource: Int) {
|
||||
//if the view was recycled bubble_content will be null and we don't need to inflate a layout
|
||||
itemView.findViewById<View>(R.id.bubble_content)?.let { placeHolder ->
|
||||
val parent = placeHolder.parent as ViewGroup
|
||||
val index = parent.indexOfChild(placeHolder)
|
||||
parent.removeView(placeHolder)
|
||||
val bubbleContent = LayoutInflater.from(context).inflate(layoutResource, parent, false)
|
||||
parent.addView(bubbleContent, index)
|
||||
}
|
||||
}
|
||||
protected fun configureBubble(context: Context, view: View, outgoing: Boolean) {
|
||||
view.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
gravity = if (outgoing) {
|
||||
marginStart = CONTAINER_MARGIN
|
||||
protected fun configureContainer(outgoing: Boolean, previousOutgoing: Boolean?, isLast: Boolean) {
|
||||
val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
if (previousOutgoing != null && previousOutgoing != outgoing) {
|
||||
layoutParams.updateMargins(top = BUBBLE_VERTICAL_MARGIN)
|
||||
}
|
||||
if (isLast) {
|
||||
layoutParams.updateMargins(bottom = BUBBLE_VERTICAL_MARGIN)
|
||||
}
|
||||
itemView.layoutParams = layoutParams //set layoutParams anyway to reset margins if the view was recycled
|
||||
if (outgoing) {
|
||||
itemView.updatePadding(right = CONTAINER_PADDING)
|
||||
} else {
|
||||
itemView.updatePadding(left = CONTAINER_PADDING)
|
||||
}
|
||||
}
|
||||
protected fun configureBubble(chatItem: ChatItem, previousChatItem: ChatItem?, nextChatItem: ChatItem?) {
|
||||
val bubble = itemView.findViewById<LinearLayout>(R.id.bubble)
|
||||
bubble.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
gravity = if (chatItem.outgoing) {
|
||||
marginStart = BUBBLE_MARGIN
|
||||
Gravity.END
|
||||
} else {
|
||||
marginEnd = CONTAINER_MARGIN
|
||||
marginEnd = BUBBLE_MARGIN
|
||||
Gravity.START
|
||||
}
|
||||
}
|
||||
if (!outgoing) {
|
||||
view.background.colorFilter = PorterDuffColorFilter(
|
||||
ContextCompat.getColor(context, R.color.incomingBubbleBackground),
|
||||
PorterDuff.Mode.SRC
|
||||
)
|
||||
val backgroundDrawable = GradientDrawable()
|
||||
backgroundDrawable.setColor(ContextCompat.getColor(context, if (chatItem.outgoing) {
|
||||
R.color.bubbleBackground
|
||||
} else {
|
||||
R.color.incomingBubbleBackground
|
||||
}))
|
||||
var topLeft = BUBBLE_CORNER_NORMAL
|
||||
var topRight = BUBBLE_CORNER_NORMAL
|
||||
var bottomRight = BUBBLE_CORNER_NORMAL
|
||||
var bottomLeft = BUBBLE_CORNER_NORMAL
|
||||
if (previousChatItem?.outgoing == chatItem.outgoing && TimeUtils.isInTheSameDay(chatItem, previousChatItem)) {
|
||||
if (chatItem.outgoing) {
|
||||
topRight = BUBBLE_CORNER_ARROW
|
||||
} else {
|
||||
topLeft = BUBBLE_CORNER_ARROW
|
||||
}
|
||||
}
|
||||
if (nextChatItem?.outgoing == chatItem.outgoing && TimeUtils.isInTheSameDay(chatItem, nextChatItem)) {
|
||||
if (chatItem.outgoing) {
|
||||
bottomRight = BUBBLE_CORNER_ARROW
|
||||
} else {
|
||||
bottomLeft = BUBBLE_CORNER_ARROW
|
||||
}
|
||||
}
|
||||
backgroundDrawable.cornerRadii = generateCorners(topLeft, topRight, bottomRight, bottomLeft)
|
||||
bubble.background = backgroundDrawable
|
||||
}
|
||||
protected fun showDateAndTime(chatItem: ChatItem, previousChatItem: ChatItem?) {
|
||||
var showTextPendingMsg = false
|
||||
val textPendingMsg = itemView.findViewById<TextView>(R.id.text_pending_msg)
|
||||
val textDate = itemView.findViewById<TextView>(R.id.text_date)
|
||||
val textHour = itemView.findViewById<TextView>(R.id.text_hour)
|
||||
val showDate = if (chatItem.timestamp == 0L) {
|
||||
if (previousChatItem == null || previousChatItem.timestamp != 0L) {
|
||||
showTextPendingMsg = true
|
||||
}
|
||||
textHour.visibility = View.GONE
|
||||
false
|
||||
} else {
|
||||
textHour.apply {
|
||||
visibility = View.VISIBLE
|
||||
@SuppressLint("SetTextI18n")
|
||||
text = StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.HOUR_OF_DAY))+":"+StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.MINUTE))
|
||||
setTextColor(ContextCompat.getColor(context, if (chatItem.outgoing) {
|
||||
R.color.outgoingTimestamp
|
||||
} else {
|
||||
R.color.incomingTimestamp
|
||||
}))
|
||||
}
|
||||
if (previousChatItem == null) {
|
||||
true
|
||||
} else {
|
||||
!TimeUtils.isInTheSameDay(chatItem, previousChatItem)
|
||||
}
|
||||
}
|
||||
textDate.visibility = if (showDate) {
|
||||
textDate.text = DateFormat.getDateInstance().format(chatItem.calendar.time)
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
textPendingMsg.visibility = if (showTextPendingMsg) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal open class MessageViewHolder(itemView: View): BubbleViewHolder(itemView) {
|
||||
protected fun bindMessage(chatItem: ChatItem): TextView {
|
||||
internal open class MessageViewHolder(context: Context, itemView: View): BubbleViewHolder(context, itemView) {
|
||||
protected fun bindMessage(chatItem: ChatItem, outgoing: Boolean): TextView {
|
||||
setBubbleContent(R.layout.message_bubble_content)
|
||||
itemView.findViewById<TextView>(R.id.text_message).apply {
|
||||
text = chatItem.data.sliceArray(1 until chatItem.data.size).decodeToString()
|
||||
if (!outgoing) {
|
||||
highlightColor = ContextCompat.getColor(context, R.color.incomingHighlight)
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class OutgoingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) {
|
||||
fun bind(chatItem: ChatItem) {
|
||||
configureBubble(context, bindMessage(chatItem).apply {
|
||||
setLinkTextColor(ContextCompat.getColor(context, R.color.outgoingTextLink))
|
||||
}, true)
|
||||
setPadding(true)
|
||||
}
|
||||
}
|
||||
|
||||
internal class IncomingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) {
|
||||
fun bind(chatItem: ChatItem) {
|
||||
configureBubble(context, bindMessage(chatItem).apply {
|
||||
setLinkTextColor(ContextCompat.getColor(context, R.color.incomingTextLink))
|
||||
}, false)
|
||||
setPadding(false)
|
||||
}
|
||||
}
|
||||
|
||||
internal open class FileViewHolder(itemView: View, private val onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): BubbleViewHolder(itemView) {
|
||||
protected fun bindFile(chatItem: ChatItem): LinearLayout {
|
||||
val filename = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString()
|
||||
itemView.findViewById<TextView>(R.id.text_filename).text = filename
|
||||
itemView.findViewById<ImageButton>(R.id.button_save).setOnClickListener {
|
||||
onSavingFile(filename, chatItem.data.sliceArray(1 until 17))
|
||||
internal class OutgoingMessageViewHolder(context: Context, itemView: View): MessageViewHolder(context, itemView) {
|
||||
fun bind(chatItem: ChatItem, previousChatItem: ChatItem?, nextChatItem: ChatItem?) {
|
||||
bindMessage(chatItem, true).apply {
|
||||
setLinkTextColor(ContextCompat.getColor(context, R.color.outgoingTextLink))
|
||||
}
|
||||
return itemView.findViewById(R.id.bubble_content)
|
||||
showDateAndTime(chatItem, previousChatItem)
|
||||
configureBubble(chatItem, previousChatItem, nextChatItem)
|
||||
configureContainer(true, previousChatItem?.outgoing, nextChatItem == null)
|
||||
}
|
||||
}
|
||||
|
||||
internal class OutgoingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) {
|
||||
fun bind(chatItem: ChatItem) {
|
||||
configureBubble(context, bindFile(chatItem), true)
|
||||
setPadding(true)
|
||||
internal class IncomingMessageViewHolder(context: Context, itemView: View): MessageViewHolder(context, itemView) {
|
||||
fun bind(chatItem: ChatItem, previousChatItem: ChatItem?, nextChatItem: ChatItem?) {
|
||||
bindMessage(chatItem, false).apply {
|
||||
setLinkTextColor(ContextCompat.getColor(context, R.color.incomingTextLink))
|
||||
}
|
||||
showDateAndTime(chatItem, previousChatItem)
|
||||
configureBubble(chatItem, previousChatItem, nextChatItem)
|
||||
configureContainer(false, previousChatItem?.outgoing, nextChatItem == null)
|
||||
}
|
||||
}
|
||||
|
||||
internal class IncomingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) {
|
||||
fun bind(chatItem: ChatItem) {
|
||||
configureBubble(context, bindFile(chatItem), false)
|
||||
setPadding(false)
|
||||
internal open class FileViewHolder(context: Context, itemView: View, private val onSavingFile: (fileName: String, fileContent: ByteArray) -> Unit): BubbleViewHolder(context, itemView) {
|
||||
protected fun bindFile(chatItem: ChatItem, outgoing: Boolean) {
|
||||
setBubbleContent(R.layout.file_bubble_content)
|
||||
val buttonSave = itemView.findViewById<ImageButton>(R.id.button_save)
|
||||
val fileName: String
|
||||
if (chatItem.timestamp == 0L) { //pending
|
||||
val file = Protocol.parseSmallFile(chatItem.data)!!
|
||||
fileName = file.rawFileName.decodeToString()
|
||||
buttonSave.setOnClickListener {
|
||||
onSavingFile(fileName, file.fileContent)
|
||||
}
|
||||
} else {
|
||||
fileName = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString()
|
||||
buttonSave.setOnClickListener {
|
||||
AIRADatabase.loadFile(chatItem.data.sliceArray(1 until 17))?.let {
|
||||
onSavingFile(fileName, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
itemView.findViewById<TextView>(R.id.text_filename).apply {
|
||||
text = fileName
|
||||
if (!outgoing) {
|
||||
highlightColor = ContextCompat.getColor(context, R.color.incomingHighlight)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class OutgoingFileViewHolder(context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(context, itemView, onSavingFile) {
|
||||
fun bind(chatItem: ChatItem, previousChatItem: ChatItem?, nextChatItem: ChatItem?) {
|
||||
bindFile(chatItem, true)
|
||||
showDateAndTime(chatItem, previousChatItem)
|
||||
configureBubble(chatItem, previousChatItem, nextChatItem)
|
||||
configureContainer(true, previousChatItem?.outgoing, nextChatItem == null)
|
||||
}
|
||||
}
|
||||
|
||||
internal class IncomingFileViewHolder(context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(context, itemView, onSavingFile) {
|
||||
fun bind(chatItem: ChatItem, previousChatItem: ChatItem?, nextChatItem: ChatItem?) {
|
||||
bindFile(chatItem, false)
|
||||
showDateAndTime(chatItem, previousChatItem)
|
||||
configureBubble(chatItem, previousChatItem, nextChatItem)
|
||||
configureContainer(false, previousChatItem?.outgoing, nextChatItem == null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val view = inflater.inflate(R.layout.adapter_chat_item, parent, false)
|
||||
return if (viewType == ChatItem.OUTGOING_MESSAGE || viewType == ChatItem.INCOMING_MESSAGE) {
|
||||
val view = inflater.inflate(R.layout.adapter_chat_message, parent, false)
|
||||
if (viewType == ChatItem.OUTGOING_MESSAGE) {
|
||||
OutgoingMessageViewHolder(context, view)
|
||||
} else {
|
||||
IncomingMessageViewHolder(context, view)
|
||||
}
|
||||
} else {
|
||||
val view = inflater.inflate(R.layout.adapter_chat_file, parent, false)
|
||||
if (viewType == ChatItem.OUTGOING_FILE) {
|
||||
OutgoingFileViewHolder(context, view, onSavingFile)
|
||||
} else {
|
||||
@ -143,11 +273,21 @@ class ChatAdapter(
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val chatItem = chatItems[position]
|
||||
val previousChatItem = if (position == 0) {
|
||||
null
|
||||
} else {
|
||||
chatItems[position-1]
|
||||
}
|
||||
val nextChatItem = if (position == chatItems.size - 1) {
|
||||
null
|
||||
} else {
|
||||
chatItems[position+1]
|
||||
}
|
||||
when (chatItem.itemType) {
|
||||
ChatItem.OUTGOING_MESSAGE -> (holder as OutgoingMessageViewHolder).bind(chatItem)
|
||||
ChatItem.INCOMING_MESSAGE -> (holder as IncomingMessageViewHolder).bind(chatItem)
|
||||
ChatItem.OUTGOING_FILE -> (holder as OutgoingFileViewHolder).bind(chatItem)
|
||||
ChatItem.INCOMING_FILE -> (holder as IncomingFileViewHolder).bind(chatItem)
|
||||
ChatItem.OUTGOING_MESSAGE -> (holder as OutgoingMessageViewHolder).bind(chatItem, previousChatItem, nextChatItem)
|
||||
ChatItem.INCOMING_MESSAGE -> (holder as IncomingMessageViewHolder).bind(chatItem, previousChatItem, nextChatItem)
|
||||
ChatItem.OUTGOING_FILE -> (holder as OutgoingFileViewHolder).bind(chatItem, previousChatItem, nextChatItem)
|
||||
ChatItem.INCOMING_FILE -> (holder as IncomingFileViewHolder).bind(chatItem, previousChatItem, nextChatItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,18 @@
|
||||
package sushi.hardcore.aira.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
|
||||
//https://stackoverflow.com/questions/36724898/notifyitemchanged-make-the-recyclerview-scroll-and-jump-to-up
|
||||
|
||||
class FuckRecyclerView(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
defStyleRes: Int = 0,
|
||||
): LinearLayoutManager(context, attrs, defStyleAttr, defStyleRes) {
|
||||
override fun isAutoMeasureEnabled(): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package sushi.hardcore.aira.background_service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.*
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -13,14 +14,19 @@ import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.app.RemoteInput
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.bumptech.glide.Glide
|
||||
import sushi.hardcore.aira.*
|
||||
import sushi.hardcore.aira.utils.FileUtils
|
||||
import sushi.hardcore.aira.utils.StringUtils
|
||||
import sushi.hardcore.aira.utils.TimeUtils
|
||||
import java.io.IOException
|
||||
import java.net.*
|
||||
import java.nio.channels.*
|
||||
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
class AIRAService : Service() {
|
||||
private external fun releaseIdentity()
|
||||
|
||||
@ -35,6 +41,7 @@ class AIRAService : Service() {
|
||||
const val MESSAGE_SEND_NAME = 3
|
||||
const val MESSAGE_SEND_AVATAR = 4
|
||||
const val MESSAGE_CANCEL_FILE_TRANSFER = 5
|
||||
const val FLAG_PENDING_INTENT = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
|
||||
var isServiceRunning = false
|
||||
}
|
||||
@ -42,9 +49,11 @@ class AIRAService : Service() {
|
||||
private val binder = AIRABinder()
|
||||
val sessions = mutableMapOf<Int, Session>()
|
||||
private var sessionCounter = 0
|
||||
private var server: ServerSocketChannel? = null
|
||||
private lateinit var selector: Selector
|
||||
private val sessionIdByKey = mutableMapOf<SelectionKey, Int>()
|
||||
private val databaseFolder by lazy {
|
||||
Constants.getDatabaseFolder(this)
|
||||
}
|
||||
private val notificationIdManager = NotificationIdManager()
|
||||
private val sendFileTransfers = mutableMapOf<Int, FilesSender>()
|
||||
val receiveFileTransfers = mutableMapOf<Int, FilesReceiver>()
|
||||
@ -86,13 +95,14 @@ class AIRAService : Service() {
|
||||
}
|
||||
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {}
|
||||
}
|
||||
lateinit var identityName: String
|
||||
val savedMsgs = mutableMapOf<Int, MutableList<ChatItem>>()
|
||||
val pendingMsgs = mutableMapOf<Int, MutableList<ByteArray>>()
|
||||
val savedNames = mutableMapOf<Int, String>()
|
||||
val savedAvatars = mutableMapOf<Int, String>()
|
||||
val notSeen = mutableListOf<Int>()
|
||||
var uiCallbacks: UiCallbacks? = null
|
||||
var isAppInBackground = true
|
||||
var identityName: String? = null
|
||||
|
||||
inner class AIRABinder : Binder() {
|
||||
fun getService(): AIRAService = this@AIRAService
|
||||
@ -108,7 +118,9 @@ class AIRAService : Service() {
|
||||
fun onSessionDisconnect(sessionId: Int)
|
||||
fun onNameTold(sessionId: Int, name: String)
|
||||
fun onAvatarChanged(sessionId: Int, avatar: ByteArray?)
|
||||
fun onNewMessage(sessionId: Int, data: ByteArray): Boolean
|
||||
fun onSent(sessionId: Int, timestamp: Long, buffer: ByteArray)
|
||||
fun onPendingMessagesSent(sessionId: Int)
|
||||
fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean
|
||||
fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean
|
||||
}
|
||||
|
||||
@ -120,7 +132,7 @@ class AIRAService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun sendTo(sessionId: Int, buffer: ByteArray) {
|
||||
private fun sendTo(sessionId: Int, buffer: ByteArray) {
|
||||
serviceHandler.obtainMessage().apply {
|
||||
what = MESSAGE_SEND_TO
|
||||
data = Bundle().apply {
|
||||
@ -131,6 +143,16 @@ class AIRAService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun sendOrAddToPending(sessionId: Int, buffer: ByteArray): Boolean {
|
||||
return if (isOnline(sessionId)) {
|
||||
sendTo(sessionId, buffer)
|
||||
true
|
||||
} else {
|
||||
pendingMsgs[sessionId]?.add(buffer)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelFileTransfer(sessionId: Int) {
|
||||
serviceHandler.obtainMessage().apply {
|
||||
what = MESSAGE_CANCEL_FILE_TRANSFER
|
||||
@ -139,11 +161,11 @@ class AIRAService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun sendFilesFromUris(sessionId: Int, uris: List<Uri>) {
|
||||
fun sendFilesFromUris(sessionId: Int, uris: List<Uri>, onPendingSmallFile: ((ByteArray) -> Unit)? = null) {
|
||||
val files = mutableListOf<SendFile>()
|
||||
var useLargeFileTransfer = false
|
||||
for (uri in uris) {
|
||||
FileUtils.openFileFromUri(this, uri)?.let { sendFile ->
|
||||
FileUtils.openFileFromUri(this, uri).file?.let { sendFile ->
|
||||
files.add(sendFile)
|
||||
if (sendFile.fileSize > Constants.fileSizeLimit) {
|
||||
useLargeFileTransfer = true
|
||||
@ -154,26 +176,22 @@ class AIRAService : Service() {
|
||||
sendLargeFilesTo(sessionId, files)
|
||||
} else {
|
||||
for (file in files) {
|
||||
sendSmallFileTo(sessionId, file)
|
||||
var buffer = file.inputStream.readBytes()
|
||||
file.inputStream.close()
|
||||
buffer = Protocol.newFile(file.fileName, buffer)
|
||||
if (!sendOrAddToPending(sessionId, buffer)) {
|
||||
onPendingSmallFile?.let { it(buffer) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendSmallFileTo(sessionId: Int, sendFile: SendFile) {
|
||||
val buffer = sendFile.inputStream.readBytes()
|
||||
sendFile.inputStream.close()
|
||||
sendTo(sessionId, Protocol.newFile(sendFile.fileName, buffer))
|
||||
AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer)?.let { rawFileUuid ->
|
||||
saveMsg(sessionId, byteArrayOf(Protocol.FILE) + rawFileUuid + sendFile.fileName.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendLargeFilesTo(sessionId: Int, files: MutableList<SendFile>) {
|
||||
if (sendFileTransfers[sessionId] == null && receiveFileTransfers[sessionId] == null) {
|
||||
val filesSender = FilesSender(files, this, notificationManager)
|
||||
initFileTransferNotification(sessionId, filesSender.fileTransferNotification, filesSender.files[0])
|
||||
sendFileTransfers[sessionId] = filesSender
|
||||
sendTo(sessionId, Protocol.askLargeFiles(files))
|
||||
sendOrAddToPending(sessionId, Protocol.askLargeFiles(files))
|
||||
} else {
|
||||
Toast.makeText(this, R.string.file_transfer_already_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -193,7 +211,7 @@ class AIRAService : Service() {
|
||||
return contacts[sessionId]?.name ?: savedNames[sessionId] ?: sessions[sessionId]!!.ip
|
||||
}
|
||||
|
||||
private fun isContact(sessionId: Int): Boolean {
|
||||
fun isContact(sessionId: Int): Boolean {
|
||||
return contacts.contains(sessionId)
|
||||
}
|
||||
|
||||
@ -203,10 +221,11 @@ class AIRAService : Service() {
|
||||
contacts[sessionId] = contact
|
||||
savedMsgs.remove(sessionId)?.let { msgs ->
|
||||
for (msg in msgs) {
|
||||
AIRADatabase.storeMsg(contact.uuid, msg.outgoing, msg.data)
|
||||
AIRADatabase.storeMsg(contact.uuid, msg.outgoing, msg.timestamp, msg.data)
|
||||
}
|
||||
}
|
||||
savedNames.remove(sessionId)
|
||||
pendingMsgs[sessionId] = mutableListOf()
|
||||
savedAvatars.remove(sessionId)
|
||||
return true
|
||||
}
|
||||
@ -244,6 +263,7 @@ class AIRAService : Service() {
|
||||
contacts.remove(sessionId)?.let {
|
||||
return if (AIRADatabase.removeContact(it.uuid)) {
|
||||
savedMsgs[sessionId] = mutableListOf()
|
||||
pendingMsgs.remove(sessionId)
|
||||
savedNames[sessionId] = it.name
|
||||
it.avatar?.let { avatarUuid ->
|
||||
savedAvatars[sessionId] = avatarUuid
|
||||
@ -279,7 +299,6 @@ class AIRAService : Service() {
|
||||
}
|
||||
|
||||
fun changeAvatar(avatar: ByteArray?): Boolean {
|
||||
val databaseFolder = Constants.getDatabaseFolder(applicationContext)
|
||||
val success = if (avatar == null) {
|
||||
AIRADatabase.removeIdentityAvatar(databaseFolder)
|
||||
} else {
|
||||
@ -327,7 +346,12 @@ class AIRAService : Service() {
|
||||
val key = session.register(selector, SelectionKey.OP_READ)
|
||||
sessionIdByKey[key] = sessionId
|
||||
uiCallbacks?.onNewSession(sessionId, session.ip)
|
||||
if (!isContact(sessionId)) {
|
||||
if (isContact(sessionId)) {
|
||||
for (i in 0 until pendingMsgs[sessionId]!!.size) {
|
||||
sendAndSave(sessionId, pendingMsgs[sessionId]!!.removeAt(0))
|
||||
}
|
||||
uiCallbacks?.onPendingMessagesSent(sessionId)
|
||||
} else {
|
||||
session.encryptAndSend(Protocol.askProfileInfo(), usePadding)
|
||||
}
|
||||
} else {
|
||||
@ -343,65 +367,82 @@ class AIRAService : Service() {
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun sendNotification(sessionId: Int, msgContent: ByteArray) {
|
||||
val notificationBuilder = NotificationCompat.Builder(this, MESSAGES_NOTIFICATION_CHANNEL_ID)
|
||||
private fun avatarToIcon(avatar: ByteArray): IconCompat {
|
||||
return IconCompat.createWithBitmap(
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(avatar)
|
||||
.submit()
|
||||
.get()
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendNotification(sessionId: Int, msgContent: ByteArray, timestamp: Long) {
|
||||
val name = getNameOf(sessionId)
|
||||
val text = if (msgContent[0] == Protocol.MESSAGE) {
|
||||
msgContent.decodeToString(1)
|
||||
} else { //file
|
||||
msgContent.decodeToString(17)
|
||||
}
|
||||
val notification = NotificationCompat.Builder(this, MESSAGES_NOTIFICATION_CHANNEL_ID)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentTitle(getNameOf(sessionId))
|
||||
.setContentText(
|
||||
if (msgContent[0] == Protocol.MESSAGE) {
|
||||
msgContent.decodeToString(1)
|
||||
} else { //file
|
||||
msgContent.decodeToString(17)
|
||||
.setContentTitle(name)
|
||||
.setContentText(text)
|
||||
.setStyle(NotificationCompat.MessagingStyle(
|
||||
Person.Builder()
|
||||
.setName(identityName)
|
||||
.apply {
|
||||
AIRADatabase.getIdentityAvatar(databaseFolder)?.let {
|
||||
setIcon(avatarToIcon(it))
|
||||
}
|
||||
}
|
||||
.build()
|
||||
)
|
||||
.addMessage(text, timestamp, Person.Builder()
|
||||
.setName(name)
|
||||
.apply {
|
||||
(savedAvatars[sessionId] ?: contacts[sessionId]?.avatar)?.let { uuid ->
|
||||
AIRADatabase.loadAvatar(uuid)?.let {
|
||||
setIcon(avatarToIcon(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
)
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(this, 0, Intent(this, ChatActivity::class.java).apply {
|
||||
putExtra("sessionId", sessionId)
|
||||
}, 0)
|
||||
}, FLAG_PENDING_INTENT)
|
||||
)
|
||||
.setAutoCancel(true)
|
||||
.setDefaults(Notification.DEFAULT_ALL)
|
||||
.apply {
|
||||
priority = NotificationCompat.PRIORITY_HIGH
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
|
||||
val markReadIntent = PendingIntent.getBroadcast(this, 0,
|
||||
Intent(this, NotificationBroadcastReceiver::class.java).apply {
|
||||
val bundle = Bundle()
|
||||
bundle.putBinder("binder", AIRABinder())
|
||||
bundle.putInt("sessionId", sessionId)
|
||||
putExtra("bundle", bundle)
|
||||
action = NotificationBroadcastReceiver.ACTION_MARK_READ
|
||||
}, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
notificationBuilder.addAction(
|
||||
NotificationCompat.Action(
|
||||
R.drawable.ic_launcher,
|
||||
getString(R.string.mark_read),
|
||||
markReadIntent
|
||||
.addAction(NotificationCompat.Action(
|
||||
R.drawable.ic_launcher,
|
||||
getString(R.string.mark_read),
|
||||
newActionPendingIntent {
|
||||
it.putBinder(sessionId)
|
||||
it.action = NotificationBroadcastReceiver.ACTION_MARK_READ
|
||||
}
|
||||
))
|
||||
.addAction(
|
||||
NotificationCompat.Action.Builder(R.drawable.ic_launcher, getString(R.string.reply), newActionPendingIntent {
|
||||
it.putBinder(sessionId)
|
||||
it.action = NotificationBroadcastReceiver.ACTION_REPLY
|
||||
})
|
||||
.addRemoteInput(
|
||||
RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
|
||||
.setLabel(getString(R.string.reply))
|
||||
.build()
|
||||
)
|
||||
)
|
||||
val replyPendingIntent: PendingIntent =
|
||||
PendingIntent.getBroadcast(this, 0,
|
||||
Intent(this, NotificationBroadcastReceiver::class.java).apply {
|
||||
val bundle = Bundle()
|
||||
bundle.putBinder("binder", AIRABinder())
|
||||
bundle.putInt("sessionId", sessionId)
|
||||
putExtra("bundle", bundle)
|
||||
action = NotificationBroadcastReceiver.ACTION_REPLY
|
||||
}, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
notificationBuilder.addAction(
|
||||
NotificationCompat.Action.Builder(R.drawable.ic_launcher, getString(R.string.reply), replyPendingIntent)
|
||||
.addRemoteInput(
|
||||
RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
|
||||
.setLabel(getString(R.string.reply))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
}
|
||||
notificationManager.notify(notificationIdManager.getMessageNotificationId(sessionId), notificationBuilder.build())
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
notificationManager.notify(notificationIdManager.getMessageNotificationId(sessionId), notification)
|
||||
}
|
||||
|
||||
private fun initFileTransferNotification(sessionId: Int, fileTransferNotification: FileTransferNotification, file: PendingFile) {
|
||||
@ -410,29 +451,43 @@ class AIRAService : Service() {
|
||||
file.fileName,
|
||||
file.fileSize.toInt(),
|
||||
Intent(this, NotificationBroadcastReceiver::class.java).apply {
|
||||
val bundle = Bundle()
|
||||
bundle.putBinder("binder", AIRABinder())
|
||||
bundle.putInt("sessionId", sessionId)
|
||||
putExtra("bundle", bundle)
|
||||
putBinder(sessionId)
|
||||
action = NotificationBroadcastReceiver.ACTION_CANCEL_FILE_TRANSFER
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveMsg(sessionId: Int, msg: ByteArray) {
|
||||
private fun Intent.putBinder(sessionId: Int) {
|
||||
val bundle = Bundle()
|
||||
bundle.putBinder("binder", AIRABinder())
|
||||
bundle.putInt("sessionId", sessionId)
|
||||
putExtra("bundle", bundle)
|
||||
}
|
||||
|
||||
private fun saveMsg(sessionId: Int, timestamp: Long, msg: ByteArray) {
|
||||
var msgSaved = false
|
||||
contacts[sessionId]?.uuid?.let { uuid ->
|
||||
msgSaved = AIRADatabase.storeMsg(uuid, true, msg)
|
||||
msgSaved = AIRADatabase.storeMsg(uuid, true, timestamp, msg)
|
||||
}
|
||||
if (!msgSaved) {
|
||||
savedMsgs[sessionId]?.add(ChatItem(true, msg))
|
||||
savedMsgs[sessionId]?.add(ChatItem(true, timestamp, msg))
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendAndSave(sessionId: Int, msg: ByteArray) {
|
||||
sessions[sessionId]?.encryptAndSend(msg, usePadding)
|
||||
if (msg[0] == Protocol.MESSAGE) {
|
||||
saveMsg(sessionId, msg)
|
||||
private fun sendAndSave(sessionId: Int, buffer: ByteArray) {
|
||||
sessions[sessionId]?.encryptAndSend(buffer, usePadding)
|
||||
val timestamp = TimeUtils.getTimestamp()
|
||||
if (buffer[0] == Protocol.MESSAGE) {
|
||||
uiCallbacks?.onSent(sessionId, timestamp, buffer)
|
||||
saveMsg(sessionId, timestamp, buffer)
|
||||
} else if (buffer[0] == Protocol.FILE) {
|
||||
Protocol.parseSmallFile(buffer)?.let { file ->
|
||||
AIRADatabase.storeFile(contacts[sessionId]?.uuid, file.fileContent)?.let { rawFileUuid ->
|
||||
val msg = byteArrayOf(Protocol.FILE) + rawFileUuid + file.rawFileName
|
||||
uiCallbacks?.onSent(sessionId, timestamp, msg)
|
||||
saveMsg(sessionId, timestamp, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -482,14 +537,12 @@ class AIRAService : Service() {
|
||||
}
|
||||
}
|
||||
MESSAGE_SEND_NAME -> {
|
||||
identityName?.let {
|
||||
val tellingName = Protocol.name(it)
|
||||
for (session in sessions.values) {
|
||||
try {
|
||||
session.encryptAndSend(tellingName, usePadding)
|
||||
} catch (e: SocketException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
val tellingName = Protocol.name(identityName)
|
||||
for (session in sessions.values) {
|
||||
try {
|
||||
session.encryptAndSend(tellingName, usePadding)
|
||||
} catch (e: SocketException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -514,10 +567,6 @@ class AIRAService : Service() {
|
||||
quit()
|
||||
stopSelf()
|
||||
uiCallbacks = null
|
||||
for (session in sessions.values) {
|
||||
session.close()
|
||||
}
|
||||
server?.close()
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
@ -526,6 +575,7 @@ class AIRAService : Service() {
|
||||
}
|
||||
}
|
||||
}
|
||||
identityName = AIRADatabase.getIdentityName(databaseFolder)!!
|
||||
val contactList = AIRADatabase.loadContacts()
|
||||
if (contactList == null) {
|
||||
contacts = HashMap(0)
|
||||
@ -533,6 +583,7 @@ class AIRAService : Service() {
|
||||
contacts = HashMap(contactList.size)
|
||||
for (contact in contactList) {
|
||||
contacts[sessionCounter] = contact
|
||||
pendingMsgs[sessionCounter] = mutableListOf()
|
||||
if (!contact.seen) {
|
||||
notSeen.add(sessionCounter)
|
||||
}
|
||||
@ -565,8 +616,8 @@ class AIRAService : Service() {
|
||||
0
|
||||
}
|
||||
filesSender.nextChunk = if (read > 0) {
|
||||
filesSender.lastChunkSizes.add(nextChunk.size)
|
||||
session.encrypt(nextChunk, usePadding)
|
||||
filesSender.lastChunkSizes.add(read+1)
|
||||
session.encrypt(nextChunk.sliceArray(0 until read+1), usePadding)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -601,7 +652,7 @@ class AIRAService : Service() {
|
||||
}
|
||||
|
||||
private fun startListening() {
|
||||
server = try {
|
||||
val server = try {
|
||||
ServerSocketChannel.open().apply {
|
||||
configureBlocking(false)
|
||||
socket().bind(InetSocketAddress(Constants.port))
|
||||
@ -632,7 +683,7 @@ class AIRAService : Service() {
|
||||
receiveFileTransfers[sessionId]?.let { filesReceiver ->
|
||||
val file = filesReceiver.files[filesReceiver.index]
|
||||
if (file.outputStream == null) {
|
||||
val outputStream = FileUtils.openFileForDownload(this, file.fileName)
|
||||
val outputStream = FileUtils.openFileForDownload(this, file.fileName).outputStream
|
||||
if (outputStream == null) {
|
||||
cancelFileTransfer(sessionId)
|
||||
} else {
|
||||
@ -746,7 +797,7 @@ class AIRAService : Service() {
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(this, 0, Intent(this, ChatActivity::class.java).apply {
|
||||
putExtra("sessionId", sessionId)
|
||||
}, 0)
|
||||
}, FLAG_PENDING_INTENT)
|
||||
)
|
||||
.setDefaults(Notification.DEFAULT_ALL)
|
||||
.apply {
|
||||
@ -758,10 +809,8 @@ 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.name(identityName), usePadding)
|
||||
AIRADatabase.getIdentityAvatar(databaseFolder)?.let { avatar ->
|
||||
session.encryptAndSend(Protocol.avatar(avatar), usePadding)
|
||||
}
|
||||
}
|
||||
@ -810,20 +859,21 @@ class AIRAService : Service() {
|
||||
null
|
||||
}
|
||||
}?.let { handledMsg ->
|
||||
val timestamp = TimeUtils.getTimestamp()
|
||||
var seen = false
|
||||
uiCallbacks?.let { uiCallbacks ->
|
||||
seen = uiCallbacks.onNewMessage(sessionId, handledMsg)
|
||||
seen = uiCallbacks.onNewMessage(sessionId, timestamp, handledMsg)
|
||||
}
|
||||
setSeen(sessionId, seen)
|
||||
var msgSaved = false
|
||||
contacts[sessionId]?.let { contact ->
|
||||
msgSaved = AIRADatabase.storeMsg(contact.uuid, false, handledMsg)
|
||||
msgSaved = AIRADatabase.storeMsg(contact.uuid, false, timestamp, handledMsg)
|
||||
}
|
||||
if (!msgSaved){
|
||||
savedMsgs[sessionId]?.add(ChatItem(false, handledMsg))
|
||||
savedMsgs[sessionId]?.add(ChatItem(false, timestamp, handledMsg))
|
||||
}
|
||||
if (isAppInBackground) {
|
||||
sendNotification(sessionId, handledMsg)
|
||||
sendNotification(sessionId, handledMsg, timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -852,7 +902,12 @@ class AIRAService : Service() {
|
||||
}
|
||||
}
|
||||
}
|
||||
keys.clear()
|
||||
}
|
||||
for (session in sessions.values) {
|
||||
session.close()
|
||||
}
|
||||
server?.close()
|
||||
}.start()
|
||||
}
|
||||
|
||||
@ -901,21 +956,37 @@ class AIRAService : Service() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun newActionPendingIntent(intentBuilder: (Intent) -> Unit): PendingIntent {
|
||||
return PendingIntent.getBroadcast(this, 0,
|
||||
Intent(this, NotificationBroadcastReceiver::class.java).apply {
|
||||
intentBuilder(this)
|
||||
}, FLAG_PENDING_INTENT
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
val notificationBuilder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createNotificationChannels()
|
||||
Notification.Builder(this, SERVICE_NOTIFICATION_CHANNEL_ID)
|
||||
} else {
|
||||
@Suppress("Deprecation")
|
||||
Notification.Builder(this)
|
||||
}
|
||||
val notificationBuilder = NotificationCompat.Builder(this, SERVICE_NOTIFICATION_CHANNEL_ID)
|
||||
val notification: Notification = notificationBuilder
|
||||
.setContentTitle(getString(R.string.background_service))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), 0)
|
||||
PendingIntent.getActivity(this, 0,
|
||||
Intent(this, MainActivity::class.java).apply {
|
||||
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
},
|
||||
FLAG_PENDING_INTENT)
|
||||
)
|
||||
.addAction(NotificationCompat.Action(
|
||||
R.drawable.ic_launcher,
|
||||
getString(R.string.stop),
|
||||
newActionPendingIntent {
|
||||
it.action = NotificationBroadcastReceiver.ACTION_LOGOUT
|
||||
}
|
||||
))
|
||||
.build()
|
||||
startForeground(1, notification)
|
||||
|
||||
|
@ -4,28 +4,38 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.RemoteInput
|
||||
import sushi.hardcore.aira.MainActivity
|
||||
|
||||
class NotificationBroadcastReceiver: BroadcastReceiver() {
|
||||
companion object {
|
||||
const val ACTION_LOGOUT = "logout"
|
||||
const val ACTION_MARK_READ = "mark_read"
|
||||
const val ACTION_CANCEL_FILE_TRANSFER = "cancel"
|
||||
const val ACTION_REPLY = "reply"
|
||||
const val KEY_TEXT_REPLY = "key_text_reply"
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent) {
|
||||
intent.getBundleExtra("bundle")?.let { bundle ->
|
||||
(bundle.getBinder("binder") as AIRAService.AIRABinder?)?.let { binder ->
|
||||
val sessionId = bundle.getInt("sessionId")
|
||||
val airaService = binder.getService()
|
||||
when (intent.action) {
|
||||
ACTION_MARK_READ -> airaService.setSeen(sessionId, true)
|
||||
ACTION_CANCEL_FILE_TRANSFER -> airaService.cancelFileTransfer(sessionId)
|
||||
ACTION_REPLY -> RemoteInput.getResultsFromIntent(intent)?.getString(KEY_TEXT_REPLY)?.let { reply ->
|
||||
airaService.sendTo(sessionId, Protocol.newMessage(reply))
|
||||
airaService.setSeen(sessionId, true)
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == ACTION_LOGOUT) {
|
||||
context.startActivity(Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
action = ACTION_LOGOUT
|
||||
})
|
||||
context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
|
||||
} else {
|
||||
intent.getBundleExtra("bundle")?.let { bundle ->
|
||||
(bundle.getBinder("binder") as AIRAService.AIRABinder?)?.let { binder ->
|
||||
val sessionId = bundle.getInt("sessionId")
|
||||
val airaService = binder.getService()
|
||||
when (intent.action) {
|
||||
ACTION_MARK_READ -> airaService.setSeen(sessionId, true)
|
||||
ACTION_CANCEL_FILE_TRANSFER -> airaService.cancelFileTransfer(sessionId)
|
||||
ACTION_REPLY -> RemoteInput.getResultsFromIntent(intent)?.getString(KEY_TEXT_REPLY)?.let { reply ->
|
||||
airaService.sendOrAddToPending(sessionId, Protocol.newMessage(reply))
|
||||
airaService.setSeen(sessionId, true)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package sushi.hardcore.aira.background_service
|
||||
|
||||
import sushi.hardcore.aira.widgets.Avatar
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class Protocol {
|
||||
@ -38,15 +37,17 @@ class Protocol {
|
||||
}
|
||||
|
||||
fun newFile(fileName: String, buffer: ByteArray): ByteArray {
|
||||
return byteArrayOf(FILE)+ByteBuffer.allocate(2).putShort(fileName.length.toShort()).array()+fileName.toByteArray()+buffer
|
||||
val fileNameBytes = fileName.toByteArray()
|
||||
return byteArrayOf(FILE)+ByteBuffer.allocate(2).putShort(fileNameBytes.size.toShort()).array()+fileNameBytes+buffer
|
||||
}
|
||||
|
||||
fun askLargeFiles(files: List<SendFile>): ByteArray {
|
||||
var buff = byteArrayOf(ASK_LARGE_FILES)
|
||||
for (file in files) {
|
||||
val fileName = file.fileName.toByteArray()
|
||||
buff += ByteBuffer.allocate(8).putLong(file.fileSize).array()
|
||||
buff += ByteBuffer.allocate(2).putShort(file.fileName.length.toShort()).array()
|
||||
buff += file.fileName.toByteArray()
|
||||
buff += ByteBuffer.allocate(2).putShort(fileName.size.toShort()).array()
|
||||
buff += fileName
|
||||
}
|
||||
return buff
|
||||
}
|
||||
|
@ -38,8 +38,8 @@ class Session(private val socket: SocketChannel, val outgoing: Boolean): Selecta
|
||||
private const val handshakeBufferLen = (2*(RANDOM_LEN+PUBLIC_KEY_LEN))+SIGNATURE_LEN+AES_TAG_LEN
|
||||
private const val CIPHER_TYPE = "AES/GCM/NoPadding"
|
||||
private const val MESSAGE_LEN_LEN = 4
|
||||
private const val PADDED_MAX_SIZE = 32768000
|
||||
private const val MAX_RECV_SIZE = MESSAGE_LEN_LEN + PADDED_MAX_SIZE + AES_TAG_LEN
|
||||
private const val PADDED_MAX_SIZE = 16384000
|
||||
private const val MAX_RECV_SIZE = PADDED_MAX_SIZE + AES_TAG_LEN
|
||||
}
|
||||
|
||||
private val prng = SecureRandom()
|
||||
@ -188,7 +188,7 @@ class Session(private val socket: SocketChannel, val outgoing: Boolean): Selecta
|
||||
|
||||
fun encrypt(plainText: ByteArray, usePadding: Boolean): ByteArray {
|
||||
val padded = pad(plainText, usePadding)
|
||||
val rawMsgLen = ByteBuffer.allocate(MESSAGE_LEN_LEN).putInt(padded.size).array()
|
||||
val rawMsgLen = ByteBuffer.allocate(MESSAGE_LEN_LEN).putInt(padded.size+AES_TAG_LEN).array()
|
||||
val nonce = ivToNonce(applicationKeys.localIv, localCounter)
|
||||
localCounter++
|
||||
localCipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(applicationKeys.localKey, "AES"), GCMParameterSpec(AES_TAG_LEN*8, nonce))
|
||||
@ -219,7 +219,7 @@ class Session(private val socket: SocketChannel, val outgoing: Boolean): Selecta
|
||||
val rawMessageLen = ByteBuffer.allocate(MESSAGE_LEN_LEN)
|
||||
if (readAll(rawMessageLen)) {
|
||||
rawMessageLen.position(0)
|
||||
val messageLen = rawMessageLen.int + AES_TAG_LEN
|
||||
val messageLen = rawMessageLen.int
|
||||
if (messageLen in 1..MAX_RECV_SIZE) {
|
||||
val cipherText = ByteBuffer.allocate(messageLen)
|
||||
if (readAll(cipherText)) {
|
||||
|
@ -0,0 +1,36 @@
|
||||
package sushi.hardcore.aira.background_service
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.preference.PreferenceManager
|
||||
import sushi.hardcore.aira.AIRADatabase
|
||||
import sushi.hardcore.aira.Constants
|
||||
|
||||
class SystemBroadcastReceiver: BroadcastReceiver() {
|
||||
init {
|
||||
AIRADatabase.init()
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean("startAtBoot", true) && !AIRAService.isServiceRunning) {
|
||||
val databaseFolder = Constants.getDatabaseFolder(context)
|
||||
val isProtected = AIRADatabase.isIdentityProtected(databaseFolder)
|
||||
val name = AIRADatabase.getIdentityName(databaseFolder)
|
||||
if (name != null && !isProtected) {
|
||||
if (AIRADatabase.loadIdentity(databaseFolder, null)) {
|
||||
AIRADatabase.clearCache()
|
||||
val serviceIntent = Intent(context, AIRAService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(serviceIntent)
|
||||
} else {
|
||||
context.startService(serviceIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +1,17 @@
|
||||
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 val onAvatarPicked: (ByteArray) -> Unit,
|
||||
) {
|
||||
private lateinit var picker: ActivityResultLauncher<String>
|
||||
private lateinit var avatar: RequestBuilder<Drawable>
|
||||
fun register() {
|
||||
picker = activity.registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
if (uri != null) {
|
||||
@ -30,26 +21,13 @@ class AvatarPicker(
|
||||
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)
|
||||
onAvatarPicked(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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/*")
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
package sushi.hardcore.aira.utils
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import sushi.hardcore.aira.background_service.ReceiveFile
|
||||
import sushi.hardcore.aira.background_service.SendFile
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
@ -17,8 +16,8 @@ import java.io.OutputStream
|
||||
import java.text.DecimalFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.log10
|
||||
import kotlin.math.pow
|
||||
|
||||
object FileUtils {
|
||||
private val units = arrayOf("B", "kB", "MB", "GB", "TB")
|
||||
@ -31,8 +30,16 @@ object FileUtils {
|
||||
) + " " + units[digitGroups]
|
||||
}
|
||||
|
||||
fun openFileFromUri(context: Context, uri: Uri): SendFile? {
|
||||
class SendFileResult(val file: SendFile? = null, val errorHandled: Boolean = false)
|
||||
|
||||
fun openFileFromUri(context: Context, uri: Uri): SendFileResult {
|
||||
var sendFile: SendFile? = null
|
||||
try {
|
||||
context.grantUriPermission(context.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
} catch (e: SecurityException) {
|
||||
Toast.makeText(context, e.localizedMessage, Toast.LENGTH_LONG).show()
|
||||
return SendFileResult(sendFile, true)
|
||||
}
|
||||
val cursor = context.contentResolver.query(uri, null, null, null, null)
|
||||
if (cursor != null) {
|
||||
if (cursor.moveToFirst()) {
|
||||
@ -48,10 +55,12 @@ object FileUtils {
|
||||
}
|
||||
cursor.close()
|
||||
}
|
||||
return sendFile
|
||||
return SendFileResult(sendFile)
|
||||
}
|
||||
|
||||
fun openFileForDownload(context: Context, fileName: String): OutputStream? {
|
||||
class DownloadFile(val fileName: String, val outputStream: OutputStream?)
|
||||
|
||||
fun openFileForDownload(context: Context, fileName: String): DownloadFile {
|
||||
val fileExtension = fileName.substringAfterLast(".")
|
||||
val dateExtension = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date())
|
||||
val datedFilename = if (fileName.contains(".")) {
|
||||
@ -60,7 +69,7 @@ object FileUtils {
|
||||
} else {
|
||||
fileName + "_" + dateExtension
|
||||
}
|
||||
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
val outputStream = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
context.contentResolver.insert(
|
||||
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
||||
ContentValues().apply {
|
||||
@ -75,5 +84,6 @@ object FileUtils {
|
||||
@Suppress("Deprecation")
|
||||
File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), File(datedFilename).name).outputStream()
|
||||
}
|
||||
return DownloadFile(datedFilename, outputStream)
|
||||
}
|
||||
}
|
@ -25,4 +25,12 @@ object StringUtils {
|
||||
fun sanitizeName(name: String): String {
|
||||
return name.replace('\n', ' ')
|
||||
}
|
||||
|
||||
fun toTwoDigits(number: Int): String {
|
||||
return if (number < 10) {
|
||||
"0$number"
|
||||
} else {
|
||||
number.toString()
|
||||
}
|
||||
}
|
||||
}
|
12
app/src/main/java/sushi/hardcore/aira/utils/TimeUtils.kt
Normal file
12
app/src/main/java/sushi/hardcore/aira/utils/TimeUtils.kt
Normal file
@ -0,0 +1,12 @@
|
||||
package sushi.hardcore.aira.utils
|
||||
|
||||
import sushi.hardcore.aira.ChatItem
|
||||
|
||||
object TimeUtils {
|
||||
fun getTimestamp(): Long {
|
||||
return System.currentTimeMillis()/1000
|
||||
}
|
||||
fun isInTheSameDay(first: ChatItem, second: ChatItem): Boolean {
|
||||
return first.year == second.year && first.dayOfYear == second.dayOfYear
|
||||
}
|
||||
}
|
1
app/src/main/native/.gitignore
vendored
1
app/src/main/native/.gitignore
vendored
@ -1,2 +1 @@
|
||||
Cargo.lock
|
||||
target
|
||||
|
898
app/src/main/native/Cargo.lock
generated
Normal file
898
app/src/main/native/Cargo.lock
generated
Normal file
@ -0,0 +1,898 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8aeaececfd937b9762778f2aca063ad24fdc827c48c07aeae36fb20c03afa11a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99446914425f48a667458b33c7fb920e24cf9e7c149a072a9fc420731b353835"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher 0.3.0",
|
||||
"cpufeatures 0.1.0",
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "614d3cc56d7840c3dba6ea1c1754afd3d0aa244c4902776b8d109a7be764f31f"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher 0.3.0",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm-siv"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e28b4604398f52cd06867fe0531b62b35f0edddb561e806945cbab9af7fe6a8"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher 0.3.0",
|
||||
"ctr",
|
||||
"polyval",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
|
||||
dependencies = [
|
||||
"getrandom 0.2.2",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aira"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aes-gcm-siv",
|
||||
"android_log",
|
||||
"ed25519-dalek",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"jni",
|
||||
"lazy_static",
|
||||
"log 0.4.14",
|
||||
"rand 0.7.3",
|
||||
"rand 0.8.3",
|
||||
"rusqlite",
|
||||
"scrypt",
|
||||
"sha2 0.10.2",
|
||||
"strum_macros",
|
||||
"uuid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_liblog-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aaf82c031178ca72b38595a54d16df8a257df9deea7d97a8992870e5c6a738e7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_log"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc00e0d3a060cce3fa338f9644ce9a93901c79f5405330891aeca69c9957009a"
|
||||
dependencies = [
|
||||
"android_liblog-sys",
|
||||
"log 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0d27fb6b6f1e43147af148af49d49329413ba781aa0d5e10979831c210173b5"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4369b5e4c0cddf64ad8981c0111e7df4f7078f4d6ba98fb31f2e17c4c57b7e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cd5a7748210e7ec1a9696610b1015e6e31fbf58f77a160801f124bd1c36592a"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpuid-bool"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481"
|
||||
dependencies = [
|
||||
"cipher 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f627126b946c25a4638eec0ea634fc52506dea98db118aae985118ce7c3d723f"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"digest 0.9.0",
|
||||
"rand_core 0.5.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
|
||||
dependencies = [
|
||||
"block-buffer 0.10.2",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37c66a534cbb46ab4ea03477eae19d5c22c01da8258030280b7bd9d8433fb6ef"
|
||||
dependencies = [
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"sha2 0.9.3",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.10.1+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90df9c2964d71ba6591379cc3bff5127cc19dcbe6a62088a1b3718d6cbfe08cf"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest 0.10.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log 0.4.14",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
|
||||
dependencies = [
|
||||
"log 0.4.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e029e94abc8fb0065241c308f1ac6bc8d20f450e8f7c5f0b25cd9b8d526ba294"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core 0.6.2",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
|
||||
dependencies = [
|
||||
"digest 0.10.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "864231b0b86ce05168a8e6da0fea2e67275dacf25f75b00a62cfd341aab904a9"
|
||||
dependencies = [
|
||||
"cpufeatures 0.1.0",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
"libc",
|
||||
"rand_chacha 0.2.2",
|
||||
"rand_core 0.5.1",
|
||||
"rand_hc 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.0",
|
||||
"rand_core 0.6.2",
|
||||
"rand_hc 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
|
||||
dependencies = [
|
||||
"getrandom 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||
dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
|
||||
dependencies = [
|
||||
"rand_core 0.6.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"memchr",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f"
|
||||
|
||||
[[package]]
|
||||
name = "salsa20"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
|
||||
dependencies = [
|
||||
"cipher 0.4.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
"password-hash",
|
||||
"pbkdf2",
|
||||
"salsa20",
|
||||
"sha2 0.10.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.120"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "166b2349061381baf54a58e4b13c89369feb0ef2eaa57198899e2312aac30aab"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa827a14b29ab7f44778d14a88d3cb76e949c45083f7dbfa507d0cb699dc12de"
|
||||
dependencies = [
|
||||
"block-buffer 0.9.0",
|
||||
"cfg-if",
|
||||
"cpuid-bool",
|
||||
"digest 0.9.0",
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.2",
|
||||
"digest 0.10.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f0242b8e50dd9accdd56170e94ca1ebd223b098eb9c83539a6e367d0f36ae68"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6878079b17446e4d3eba6192bb0a2950d5b14f0ed8424b852310e5a94345d0ef"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cfcd319456c4d6ea10087ed423473267e1a071f3bc0aa89f80d60997843c6f0"
|
||||
dependencies = [
|
||||
"getrandom 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.9.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93c6c3420963c5c64bca373b25e77acb562081b9bb4dd5bb864187742186cea9"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3f369ddb18862aba61aa49bf31e74d29f0f162dec753063200e1dc084345d16"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "aira"
|
||||
version = "0.0.1"
|
||||
version = "0.1.0"
|
||||
authors = ["Hardcore Sushi <hardcore.sushi@disroot.org>"]
|
||||
edition = "2018"
|
||||
|
||||
@ -11,21 +11,20 @@ jni = { version = "0.19", default-features = false }
|
||||
crate-type = ["dylib"]
|
||||
|
||||
[dependencies]
|
||||
rand = "0.8.3"
|
||||
rand-7 = {package = "rand", version = "0.7.3"}
|
||||
lazy_static = "1.4.0"
|
||||
rusqlite = { version = "0.25.1", features = ["bundled"] }
|
||||
rand = "0.8"
|
||||
rand-7 = {package = "rand", version = "0.7"}
|
||||
lazy_static = "1.4"
|
||||
rusqlite = { version = "0.27", features = ["bundled"] }
|
||||
ed25519-dalek = "1" #for singing
|
||||
x25519-dalek = "1.1" #for shared secret
|
||||
sha2 = "0.9.3"
|
||||
hkdf = "0.11.0"
|
||||
aes-gcm = "0.9.0" #PSEC
|
||||
aes-gcm-siv = "0.10.0" #Database
|
||||
hmac = "0.11.0"
|
||||
hex = "0.4.3"
|
||||
strum_macros = "0.20.1" #display enums
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
scrypt = "0.7.0"
|
||||
zeroize = "1.2.0"
|
||||
log = "0.4.14"
|
||||
android_log = "0.1.3"
|
||||
sha2 = "0.10"
|
||||
hkdf = "0.12"
|
||||
aes-gcm = "0.9" #PSEC
|
||||
aes-gcm-siv = "0.10" #Database
|
||||
hmac = "0.12"
|
||||
hex = "0.4"
|
||||
strum_macros = "0.24" #display enums
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
scrypt = "0.10"
|
||||
zeroize = "1.3"
|
||||
log = "0.4"
|
||||
android_log = "0.1"
|
@ -4,10 +4,15 @@ if [ -z ${ANDROID_NDK_HOME+x} ]; then
|
||||
echo "Error: \$ANDROID_NDK_HOME is not defined."
|
||||
else
|
||||
export PATH=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/:$PATH
|
||||
declare -a androidABIs=("arm64-v8a" "armeabi-v7a" "x86")
|
||||
declare -a targets=("aarch64-linux-android" "armv7-linux-androideabi" "i686-linux-android")
|
||||
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=aarch64-linux-android21-clang
|
||||
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=armv7a-linux-androideabi21-clang
|
||||
export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=x86_64-linux-android21-clang
|
||||
export CARGO_TARGET_I686_LINUX_ANDROID_LINKER=i686-linux-android21-clang
|
||||
declare -a androidABIs=("arm64-v8a" "armeabi-v7a" "x86_64" "x86")
|
||||
declare -a targets=("aarch64-linux-android" "armv7-linux-androideabi" "x86_64-linux-android" "i686-linux-android")
|
||||
for (( i=0; i < ${#targets[@]}; i++ )) do
|
||||
cargo build --target ${targets[i]} --release
|
||||
cp target/${targets[i]}/release/libaira.so ../jniLibs/${androidABIs[i]}/
|
||||
cargo build --target ${targets[i]} --release || exit 1
|
||||
TARGET_DIR=../jniLibs/${androidABIs[i]}
|
||||
mkdir -p $TARGET_DIR && cp target/${targets[i]}/release/libaira.so $TARGET_DIR
|
||||
done
|
||||
fi
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::{convert::TryInto, fmt::Display};
|
||||
use hkdf::Hkdf;
|
||||
use sha2::Sha384;
|
||||
use hmac::{Hmac, NewMac, Mac};
|
||||
use hmac::{Hmac, Mac};
|
||||
use scrypt::{scrypt, Params};
|
||||
use rand::{RngCore, rngs::OsRng};
|
||||
use aes_gcm::{aead::Aead, NewAead, Nonce};
|
||||
@ -86,10 +86,10 @@ impl HandshakeKeys {
|
||||
HandshakeKeys {
|
||||
local_key: local_handshake_key,
|
||||
local_iv: local_handshake_iv,
|
||||
local_handshake_traffic_secret: local_handshake_traffic_secret,
|
||||
local_handshake_traffic_secret,
|
||||
peer_key: peer_handshake_key,
|
||||
peer_iv: peer_handshake_iv,
|
||||
peer_handshake_traffic_secret: peer_handshake_traffic_secret,
|
||||
peer_handshake_traffic_secret,
|
||||
handshake_secret: handshake_secret.as_slice().try_into().unwrap(),
|
||||
}
|
||||
}
|
||||
@ -148,7 +148,7 @@ pub fn verify_handshake_finished(peer_handshake_finished: [u8; HASH_OUTPUT_LEN],
|
||||
hkdf_expand_label(&peer_handshake_traffic_secret, "finished", None, &mut peer_finished_key);
|
||||
let mut hmac = Hmac::<Sha384>::new_from_slice(&peer_finished_key).unwrap();
|
||||
hmac.update(&handshake_hash);
|
||||
hmac.verify(&peer_handshake_finished).is_ok()
|
||||
hmac.verify_slice(&peer_handshake_finished).is_ok()
|
||||
}
|
||||
|
||||
|
||||
|
@ -43,12 +43,11 @@ fn get_database_path(database_folder: &str) -> String {
|
||||
Path::new(database_folder).join(DB_NAME).to_str().unwrap().to_owned()
|
||||
}
|
||||
|
||||
struct EncryptedIdentity {
|
||||
name: String,
|
||||
encrypted_keypair: Vec<u8>,
|
||||
salt: Vec<u8>,
|
||||
encrypted_master_key: Vec<u8>,
|
||||
encrypted_use_padding: Vec<u8>,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Message {
|
||||
pub outgoing: bool,
|
||||
pub timestamp: u64,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct Contact {
|
||||
@ -60,6 +59,14 @@ pub struct Contact {
|
||||
pub seen: bool,
|
||||
}
|
||||
|
||||
struct EncryptedIdentity {
|
||||
name: String,
|
||||
encrypted_keypair: Vec<u8>,
|
||||
salt: Vec<u8>,
|
||||
encrypted_master_key: Vec<u8>,
|
||||
encrypted_use_padding: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct Identity {
|
||||
pub name: String,
|
||||
keypair: Keypair,
|
||||
@ -96,8 +103,8 @@ impl Identity {
|
||||
};
|
||||
Ok(Contact {
|
||||
uuid: contact_uuid,
|
||||
public_key: public_key,
|
||||
name: name,
|
||||
public_key,
|
||||
name,
|
||||
avatar: avatar_uuid,
|
||||
verified: false,
|
||||
seen: true,
|
||||
@ -107,7 +114,7 @@ impl Identity {
|
||||
pub fn remove_contact(&self, uuid: &Uuid) -> Result<usize, rusqlite::Error> {
|
||||
let db = Connection::open(self.get_database_path())?;
|
||||
self.delete_conversation(uuid)?;
|
||||
db.execute(&("DELETE FROM ".to_owned()+CONTACTS_TABLE+" WHERE uuid=?"), [&uuid.as_bytes()[..]])
|
||||
db.execute(&format!("DELETE FROM {} WHERE uuid=?", CONTACTS_TABLE), [&uuid.as_bytes()[..]])
|
||||
}
|
||||
|
||||
pub fn set_verified(&self, uuid: &Uuid) -> Result<usize, rusqlite::Error> {
|
||||
@ -251,16 +258,17 @@ impl Identity {
|
||||
Ok(file_uuid)
|
||||
}
|
||||
|
||||
pub fn store_msg(&self, contact_uuid: &Uuid, outgoing: bool, data: &[u8]) -> Result<usize, rusqlite::Error> {
|
||||
pub fn store_msg(&self, contact_uuid: &Uuid, message: Message) -> Result<usize, rusqlite::Error> {
|
||||
let db = Connection::open(self.get_database_path())?;
|
||||
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (outgoing BLOB, data BLOB)", contact_uuid), [])?;
|
||||
let outgoing_byte: u8 = bool_to_byte(outgoing);
|
||||
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (outgoing BLOB, timestamp BLOB, data BLOB)", contact_uuid), [])?;
|
||||
let outgoing_byte: u8 = bool_to_byte(message.outgoing);
|
||||
let encrypted_outgoing = crypto::encrypt_data(&[outgoing_byte], &self.master_key).unwrap();
|
||||
let encrypted_data = crypto::encrypt_data(data, &self.master_key).unwrap();
|
||||
db.execute(&format!("INSERT INTO \"{}\" (outgoing, data) VALUES (?1, ?2)", contact_uuid), params![encrypted_outgoing, encrypted_data])
|
||||
let encrypted_timestamp = crypto::encrypt_data(&message.timestamp.to_be_bytes(), &self.master_key).unwrap();
|
||||
let encrypted_data = crypto::encrypt_data(&message.data, &self.master_key).unwrap();
|
||||
db.execute(&format!("INSERT INTO \"{}\" (outgoing, timestamp, data) VALUES (?1, ?2, ?3)", contact_uuid), params![encrypted_outgoing, encrypted_timestamp, encrypted_data])
|
||||
}
|
||||
|
||||
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<Message>> {
|
||||
match Connection::open(self.get_database_path()) {
|
||||
Ok(db) => {
|
||||
if let Ok(mut stmt) = db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
|
||||
@ -271,24 +279,30 @@ impl Identity {
|
||||
if offset+count >= total {
|
||||
count = total-offset;
|
||||
}
|
||||
let mut stmt = db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)).unwrap();
|
||||
let mut stmt = db.prepare(&format!("SELECT outgoing, timestamp, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)).unwrap();
|
||||
let mut rows = stmt.query([]).unwrap();
|
||||
let mut msgs = Vec::new();
|
||||
while let Ok(Some(row)) = rows.next() {
|
||||
let encrypted_outgoing: Vec<u8> = row.get(0).unwrap();
|
||||
match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
|
||||
Ok(outgoing) => {
|
||||
match byte_to_bool(outgoing[0]) {
|
||||
Ok(outgoing) => {
|
||||
let encrypted_data: Vec<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)
|
||||
if let Ok(outgoing) = byte_to_bool(outgoing[0]) {
|
||||
let encrypted_timestamp: Vec<u8> = row.get(1).unwrap();
|
||||
match crypto::decrypt_data(&encrypted_timestamp, &self.master_key) {
|
||||
Ok(timestamp) => {
|
||||
let encrypted_data: Vec<u8> = row.get(2).unwrap();
|
||||
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
|
||||
Ok(data) => msgs.push(Message {
|
||||
outgoing,
|
||||
timestamp: u64::from_be_bytes(timestamp.try_into().unwrap()),
|
||||
data,
|
||||
}),
|
||||
Err(e) => print_error!(e)
|
||||
}
|
||||
}
|
||||
Err(e) => print_error!(e)
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
}
|
||||
Err(e) => print_error!(e)
|
||||
}
|
||||
@ -302,7 +316,7 @@ impl Identity {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
pub fn delete_conversation(&self, contact_uuid: &Uuid) -> Result<usize, rusqlite::Error> {
|
||||
let db = Connection::open(self.get_database_path())?;
|
||||
@ -373,14 +387,8 @@ impl Identity {
|
||||
pub fn load_identity(database_folder: String, password: Option<&[u8]>) -> Result<Identity, String> {
|
||||
match Identity::load_encrypted_identity(&database_folder) {
|
||||
Ok(encrypted_identity) => {
|
||||
let master_key: [u8; crypto::MASTER_KEY_LEN] = if password.is_none() {
|
||||
if encrypted_identity.encrypted_master_key.len() == crypto::MASTER_KEY_LEN {
|
||||
encrypted_identity.encrypted_master_key.try_into().unwrap()
|
||||
} else {
|
||||
return Err(String::from(DATABASE_CORRUPED_ERROR))
|
||||
}
|
||||
} else {
|
||||
match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, password.unwrap(), &encrypted_identity.salt) {
|
||||
let master_key: [u8; crypto::MASTER_KEY_LEN] = match password {
|
||||
Some(password) => match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, password, &encrypted_identity.salt) {
|
||||
Ok(master_key) => master_key,
|
||||
Err(e) => return Err(
|
||||
match e {
|
||||
@ -389,6 +397,11 @@ impl Identity {
|
||||
}
|
||||
)
|
||||
}
|
||||
None => if encrypted_identity.encrypted_master_key.len() == crypto::MASTER_KEY_LEN {
|
||||
encrypted_identity.encrypted_master_key.try_into().unwrap()
|
||||
} else {
|
||||
return Err(String::from(DATABASE_CORRUPED_ERROR))
|
||||
}
|
||||
};
|
||||
match crypto::decrypt_data(&encrypted_identity.encrypted_keypair, &master_key) {
|
||||
Ok(keypair) => {
|
||||
@ -399,7 +412,7 @@ impl Identity {
|
||||
keypair: Keypair::from_bytes(&keypair[..]).unwrap(),
|
||||
master_key,
|
||||
use_padding: byte_to_bool(use_padding[0]).unwrap(),
|
||||
database_folder: database_folder,
|
||||
database_folder,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
@ -435,13 +448,16 @@ impl Identity {
|
||||
let db = KeyValueTable::new(&get_database_path(&database_folder), MAIN_TABLE)?;
|
||||
db.set(DBKeys::NAME, name.as_bytes())?;
|
||||
db.set(DBKeys::KEYPAIR, &encrypted_keypair)?;
|
||||
let salt = if password.is_none() { //no password
|
||||
db.set(DBKeys::MASTER_KEY, &master_key)?; //storing master_key in plaintext
|
||||
[0; crypto::SALT_LEN]
|
||||
} else {
|
||||
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, password.unwrap());
|
||||
db.set(DBKeys::MASTER_KEY, &encrypted_master_key)?;
|
||||
salt
|
||||
let salt = match password {
|
||||
Some(password) => {
|
||||
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, password);
|
||||
db.set(DBKeys::MASTER_KEY, &encrypted_master_key)?;
|
||||
salt
|
||||
}
|
||||
None => {
|
||||
db.set(DBKeys::MASTER_KEY, &master_key)?; //storing master_key in plaintext
|
||||
[0; crypto::SALT_LEN]
|
||||
}
|
||||
};
|
||||
db.set(DBKeys::SALT, &salt)?;
|
||||
let encrypted_use_padding = crypto::encrypt_data(&[bool_to_byte(true)], &master_key).unwrap();
|
||||
@ -457,13 +473,16 @@ impl Identity {
|
||||
|
||||
fn update_master_key(database_folder: String, master_key: [u8; crypto::MASTER_KEY_LEN], new_password: Option<&[u8]>) -> Result<usize, rusqlite::Error> {
|
||||
let db = KeyValueTable::new(&get_database_path(&database_folder), MAIN_TABLE)?;
|
||||
let salt = if new_password.is_none() { //no password
|
||||
db.update(DBKeys::MASTER_KEY, &master_key)?;
|
||||
[0; crypto::SALT_LEN]
|
||||
} else {
|
||||
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, new_password.unwrap());
|
||||
db.update(DBKeys::MASTER_KEY, &encrypted_master_key)?;
|
||||
salt
|
||||
let salt = match new_password {
|
||||
Some(new_password) => {
|
||||
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, new_password);
|
||||
db.update(DBKeys::MASTER_KEY, &encrypted_master_key)?;
|
||||
salt
|
||||
}
|
||||
None => {
|
||||
db.update(DBKeys::MASTER_KEY, &master_key)?;
|
||||
[0; crypto::SALT_LEN]
|
||||
}
|
||||
};
|
||||
db.update(DBKeys::SALT, &salt)
|
||||
}
|
||||
@ -471,20 +490,19 @@ impl Identity {
|
||||
pub fn change_password(database_folder: String, old_password: Option<&[u8]>, new_password: Option<&[u8]>) -> Result<bool, String> {
|
||||
match Identity::load_encrypted_identity(&database_folder) {
|
||||
Ok(encrypted_identity) => {
|
||||
let master_key: [u8; crypto::MASTER_KEY_LEN] = if old_password.is_none() {
|
||||
if encrypted_identity.encrypted_master_key.len() == crypto::MASTER_KEY_LEN {
|
||||
encrypted_identity.encrypted_master_key.try_into().unwrap()
|
||||
} else {
|
||||
return Err(String::from(DATABASE_CORRUPED_ERROR))
|
||||
}
|
||||
} else {
|
||||
match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, old_password.unwrap(), &encrypted_identity.salt) {
|
||||
let master_key: [u8; crypto::MASTER_KEY_LEN] = match old_password {
|
||||
Some(old_password) => match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, old_password, &encrypted_identity.salt) {
|
||||
Ok(master_key) => master_key,
|
||||
Err(e) => return match e {
|
||||
CryptoError::DecryptionFailed => Ok(false),
|
||||
CryptoError::InvalidLength => Err(String::from(DATABASE_CORRUPED_ERROR))
|
||||
}
|
||||
}
|
||||
None => if encrypted_identity.encrypted_master_key.len() == crypto::MASTER_KEY_LEN {
|
||||
encrypted_identity.encrypted_master_key.try_into().unwrap()
|
||||
} else {
|
||||
return Err(String::from(DATABASE_CORRUPED_ERROR))
|
||||
}
|
||||
};
|
||||
match Identity::update_master_key(database_folder, master_key, new_password) {
|
||||
Ok(_) => Ok(true),
|
||||
|
@ -12,7 +12,7 @@ impl<'a> KeyValueTable<'a> {
|
||||
Ok(KeyValueTable {db, table_name})
|
||||
}
|
||||
pub fn set(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
|
||||
Ok(self.db.execute(&format!("INSERT INTO {} (key, value) VALUES (?1, ?2)", self.table_name), params![key, value])?)
|
||||
self.db.execute(&format!("INSERT INTO {} (key, value) VALUES (?1, ?2)", self.table_name), params![key, value])
|
||||
}
|
||||
pub fn get(&self, key: &str) -> Result<Vec<u8>, Error> {
|
||||
let mut stmt = self.db.prepare(&format!("SELECT value FROM {} WHERE key=\"{}\"", self.table_name, key))?;
|
||||
|
@ -6,8 +6,7 @@ mod utils;
|
||||
use std::{convert::TryInto, fmt::Display, str::FromStr, sync::{Mutex}};
|
||||
use lazy_static::lazy_static;
|
||||
use uuid::Uuid;
|
||||
use android_log;
|
||||
use identity::{Identity, Contact};
|
||||
use identity::{Identity, Contact, Message};
|
||||
use crate::crypto::{HandshakeKeys, ApplicationKeys};
|
||||
|
||||
lazy_static! {
|
||||
@ -17,7 +16,7 @@ lazy_static! {
|
||||
#[cfg(target_os="android")]
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObject, JString, JList, JValue};
|
||||
use jni::sys::{jboolean, jint, jbyteArray, jobject};
|
||||
use jni::sys::{jboolean, jint, jlong, jbyteArray, jobject};
|
||||
|
||||
fn jstring_to_string(env: JNIEnv, input: JString) -> String {
|
||||
String::from(env.get_string(input).unwrap())
|
||||
@ -53,7 +52,7 @@ fn slice_to_jvalue<'a>(env: JNIEnv, input: &'a [u8]) -> JValue<'a> {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern fn Java_sushi_hardcore_aira_LoginActivity_00024Companion_initLogging(_: JNIEnv, _: JClass) -> jboolean {
|
||||
pub extern fn Java_sushi_hardcore_aira_AIRADatabase_initLogging(_: JNIEnv, _: JClass) -> jboolean {
|
||||
bool_to_jboolean(android_log::init("AIRA Native").is_ok())
|
||||
}
|
||||
|
||||
@ -76,7 +75,7 @@ pub extern fn Java_sushi_hardcore_aira_CreateIdentityFragment_createNewIdentity(
|
||||
|
||||
|
||||
#[no_mangle]
|
||||
pub extern fn Java_sushi_hardcore_aira_LoginActivity_getIdentityName(env: JNIEnv, _: JClass, database_folder: JString) -> jobject {
|
||||
pub extern fn Java_sushi_hardcore_aira_AIRADatabase_getIdentityName(env: JNIEnv, _: JClass, database_folder: JString) -> jobject {
|
||||
*match Identity::get_identity_name(&jstring_to_string(env, database_folder)) {
|
||||
Ok(name) => *env.new_string(name).unwrap(),
|
||||
Err(e) => {
|
||||
@ -287,8 +286,13 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_setContactSeen(env: JNIEnv, _: JCla
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
pub fn Java_sushi_hardcore_aira_AIRADatabase_storeMsg(env: JNIEnv, _: JClass, contactUuid: JString, outgoing: jboolean, data: jbyteArray) -> jboolean {
|
||||
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()))
|
||||
pub fn Java_sushi_hardcore_aira_AIRADatabase_storeMsg(env: JNIEnv, _: JClass, contactUuid: JString, outgoing: jboolean, timestamp: jlong, data: jbyteArray) -> jboolean {
|
||||
let message = Message {
|
||||
outgoing: jboolean_to_bool(outgoing),
|
||||
timestamp: timestamp as u64,
|
||||
data: env.convert_byte_array(data).unwrap(),
|
||||
};
|
||||
result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().store_msg(&jstring_to_uuid(env, contactUuid).unwrap(), message))
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
@ -301,7 +305,11 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_loadMsgs(env: JNIEnv, _: JClass, uu
|
||||
let array_list = JList::from_env(&env, array_list).unwrap();
|
||||
let chat_item_class = env.find_class("sushi/hardcore/aira/ChatItem").unwrap();
|
||||
for msg in msgs {
|
||||
let chat_item_object = env.new_object(chat_item_class, "(Z[B)V", &[JValue::Bool(bool_to_jboolean(msg.0)), slice_to_jvalue(env, &msg.1)]).unwrap();
|
||||
let chat_item_object = env.new_object(chat_item_class, "(ZJ[B)V", &[
|
||||
JValue::Bool(bool_to_jboolean(msg.outgoing)),
|
||||
JValue::Long(msg.timestamp as jlong),
|
||||
slice_to_jvalue(env, &msg.data),
|
||||
]).unwrap();
|
||||
array_list.add(chat_item_object).unwrap();
|
||||
}
|
||||
*array_list
|
||||
|
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/bubbleBackground"/>
|
||||
<corners android:radius="20dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
@ -1,5 +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="M6,13c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM6,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM6,9c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM3,9.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM6,5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM21,10.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM14,7c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1 -1,0.45 -1,1 0.45,1 1,1zM14,3.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM3,13.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,20.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,3.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM10,7c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1 -1,0.45 -1,1 0.45,1 1,1zM10,12.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM18,13c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,9c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM21,13.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM14,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM14,20.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,8.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM10,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM14,12.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM14,8.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5z"/>
|
||||
<path android:fillColor="#ffffff" android:pathData="M6,13c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM6,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM6,9c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM3,9.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM6,5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM21,10.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM14,7c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1 -1,0.45 -1,1 0.45,1 1,1zM14,3.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM3,13.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,20.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,3.5c0.28,0 0.5,-0.22 0.5,-0.5s-0.22,-0.5 -0.5,-0.5 -0.5,0.22 -0.5,0.5 0.22,0.5 0.5,0.5zM10,7c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1 -1,0.45 -1,1 0.45,1 1,1zM10,12.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM18,13c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,9c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM18,5c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM21,13.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM14,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM14,20.5c-0.28,0 -0.5,0.22 -0.5,0.5s0.22,0.5 0.5,0.5 0.5,-0.22 0.5,-0.5 -0.22,-0.5 -0.5,-0.5zM10,8.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM10,17c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1zM14,12.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM14,8.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5z"/>
|
||||
</vector>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
300
app/src/main/res/drawable/ic_launcher.xml
Normal file
300
app/src/main/res/drawable/ic_launcher.xml
Normal file
@ -0,0 +1,300 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="191.7dp"
|
||||
android:height="168.39dp"
|
||||
android:viewportWidth="191.7"
|
||||
android:viewportHeight="168.39">
|
||||
<path
|
||||
android:pathData="m72.76,168.384c14.507,-12.545 13.641,-67.907 13.641,-67.907l18.56,0s-0.865,55.362 13.641,67.907z"
|
||||
android:fillColor="#803300"/>
|
||||
<path
|
||||
android:pathData="M9.905,46.504m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M30.859,67.255m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M70.85,78.933m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M59.056,112.634m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M96.713,53.515m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M29.607,111.094m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M117.963,103.294m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M88.672,98.684m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M10.523,88.614m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M38.991,38.909m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M86.556,23.775m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M67.987,46.865m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M111.893,8.68m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M110.583,77.617m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M42.798,88.024m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="m29.607,111.094 l-19.084,-22.481"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m10.523,88.614 l20.336,-21.356"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m9.905,46.504 l20.953,20.75"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m42.798,88.024 l16.258,24.602"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m42.798,88.024 l28.052,-9.094"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m38.991,38.909 l-8.132,28.346"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m38.991,38.909 l28.996,7.956"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m67.987,46.865 l28.73,6.65"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m67.987,46.865 l18.569,-23.089"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m86.556,23.775 l25.333,-15.095"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m96.713,53.515 l13.863,24.103"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m64.435,112.604 l24.237,-13.916"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m88.672,98.684 l21.907,-21.071"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m88.672,98.684 l29.289,4.606"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m8.68,132.524 l20.926,-21.434"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="M8.68,132.524m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M44.718,138.394m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="m29.607,111.094 l15.111,27.303"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m117.963,103.294 l13.863,24.103"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="M131.822,127.394m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="m131.822,127.394 l20.559,-17.279"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="M152.382,110.114m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M139.622,84.614m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M152.782,60.155m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M125.293,52.545m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M163.313,23.778m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M183.022,111.944m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M166.242,134.224m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M59.081,13.061m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M134.802,24.57m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="m152.382,110.114 l13.863,24.103"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m166.242,134.224 l16.778,-22.271"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m110.583,77.617 l29.041,6.993"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m139.622,84.614 l12.763,25.506"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m96.713,53.515 l28.58,-0.97"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m139.622,84.614 l29.455,-1.419"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m134.802,24.57 l28.511,-0.792"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m111.893,8.68 l22.914,15.89"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m29.607,111.094 l29.449,1.538"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m67.987,46.865 l2.863,32.068"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m59.081,13.061 l27.474,10.715"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m38.991,38.909 l20.091,-25.848"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m134.802,24.57 l-9.506,27.975"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="M169.072,83.194m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="m163.313,23.778 l16.663,26.606"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m125.293,52.545 l27.488,7.61"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="M179.972,50.384m-8.681,0a8.681,8.681 0,1 1,17.362 0a8.681,8.681 0,1 1,-17.362 0"
|
||||
android:fillColor="#19a52c"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="m179.972,50.384 l-27.193,9.771"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
<path
|
||||
android:pathData="m152.782,60.155 l16.292,23.036"
|
||||
android:strokeWidth="1.8939"
|
||||
android:fillColor="#19a52c"
|
||||
android:strokeColor="#19a52c"/>
|
||||
</vector>
|
30
app/src/main/res/drawable/ic_shuttle.xml
Normal file
30
app/src/main/res/drawable/ic_shuttle.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="512.004"
|
||||
android:viewportHeight="512.004">
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="m130.239,138.268 l-44.358,3.427c-12.343,0.954 -23.336,7.423 -30.162,17.748l-51.157,77.372c-5.177,7.83 -6,17.629 -2.203,26.213 3.798,8.584 11.603,14.566 20.878,16.003l40.615,6.29c9.501,-50.42 32.245,-100.716 66.387,-147.053z"/>
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="m226.682,448.151 l6.291,40.615c1.437,9.275 7.419,17.08 16.002,20.877 3.571,1.58 7.351,2.36 11.112,2.36 5.283,0 10.529,-1.539 15.102,-4.563l77.374,-51.156c10.325,-6.827 16.794,-17.821 17.746,-30.162l3.427,-44.358c-46.338,34.143 -96.633,56.887 -147.054,66.387z"/>
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="m211.407,420c1.41,0 2.828,-0.116 4.243,-0.352 21.124,-3.532 41.484,-9.482 60.906,-17.27l-166.93,-166.93c-7.788,19.421 -13.738,39.781 -17.27,60.906 -1.392,8.327 1.401,16.81 7.37,22.78l93.144,93.144c4.956,4.955 11.645,7.722 18.537,7.722z"/>
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="m471.178,227.003c40.849,-78.974 42.362,-162.43 40.227,-201.57 -0.731,-13.411 -11.423,-24.103 -24.835,-24.834 -6.373,-0.348 -13.926,-0.599 -22.439,-0.599 -43.766,0 -113.017,6.629 -179.131,40.826 -52.542,27.177 -121.439,87.018 -162.087,165.66 0.48,0.375 0.949,0.773 1.391,1.215l180,180c0.442,0.442 0.839,0.91 1.214,1.39 78.642,-40.649 138.483,-109.546 165.66,-162.088zM297.698,108.24c29.241,-29.241 76.822,-29.244 106.065,0 14.166,14.165 21.967,33 21.967,53.033s-7.801,38.868 -21.967,53.033c-14.619,14.619 -33.829,21.93 -53.032,21.932 -19.209,0.001 -38.41,-7.309 -53.033,-21.932 -14.166,-14.165 -21.968,-33 -21.968,-53.033s7.802,-38.868 21.968,-53.033z"/>
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="m318.911,193.092c17.545,17.545 46.095,17.546 63.64,0 8.499,-8.5 13.18,-19.8 13.18,-31.82s-4.681,-23.32 -13.18,-31.819c-8.772,-8.773 -20.296,-13.159 -31.82,-13.159 -11.523,0 -23.047,4.386 -31.819,13.159 -8.499,8.499 -13.181,19.799 -13.181,31.819s4.681,23.321 13.18,31.82z"/>
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="m15.305,421.938c3.839,0 7.678,-1.464 10.606,-4.394l48.973,-48.973c5.858,-5.858 5.858,-15.355 0,-21.213 -5.857,-5.858 -15.355,-5.858 -21.213,0l-48.973,48.973c-5.858,5.858 -5.858,15.355 0,21.213 2.929,2.929 6.768,4.394 10.607,4.394z"/>
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="m119.765,392.239c-5.857,-5.858 -15.355,-5.858 -21.213,0l-94.155,94.155c-5.858,5.858 -5.858,15.355 0,21.213 2.929,2.929 6.768,4.393 10.607,4.393s7.678,-1.464 10.606,-4.394l94.154,-94.154c5.859,-5.858 5.859,-15.355 0.001,-21.213z"/>
|
||||
<path
|
||||
android:fillColor="#FFF"
|
||||
android:pathData="m143.432,437.12 l-48.972,48.973c-5.858,5.858 -5.858,15.355 0,21.213 2.929,2.929 6.768,4.394 10.606,4.394s7.678,-1.464 10.606,-4.394l48.973,-48.973c5.858,-5.858 5.858,-15.355 0,-21.213 -5.857,-5.858 -15.355,-5.858 -21.213,0z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/offline_warning_background.xml
Normal file
10
app/src/main/res/drawable/offline_warning_background.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:bottom="-2dp" android:left="-2dp" android:right="-2dp">
|
||||
<shape android:shape="rectangle">
|
||||
<stroke android:width="1dp" android:color="@color/offline_warning"/>
|
||||
<solid android:color="@color/transparent"/>
|
||||
<padding android:top="15dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:bottom="-2dp" android:left="-2dp" android:right="-2dp">
|
||||
<shape android:shape="rectangle">
|
||||
<stroke android:width="1dp" android:color="@color/pending_msg_indicator"/>
|
||||
<solid android:color="@color/transparent"/>
|
||||
<padding android:top="5dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:bottom="-2dp" android:left="-2dp" android:right="-2dp">
|
||||
<shape android:shape="rectangle">
|
||||
<stroke android:width="1dp" android:color="@color/secondary"/>
|
||||
<solid android:color="@color/transparent"/>
|
||||
<padding android:top="15dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
@ -13,14 +13,79 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_panel"/>
|
||||
app:layout_constraintBottom_toTopOf="@id/offline_warning"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sending_pending_msgs_indicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/recycler_chat"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_panel"
|
||||
android:visibility="gone"
|
||||
android:gravity="center"
|
||||
android:paddingVertical="10dp"
|
||||
android:background="@drawable/sending_pending_msg_indictor_background">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sending_pending_messages"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/offline_warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/recycler_chat"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_panel"
|
||||
android:visibility="gone"
|
||||
android:paddingHorizontal="30dp"
|
||||
android:layout_marginBottom="15dp"
|
||||
android:background="@drawable/offline_warning_background">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:src="@drawable/ic_warning"
|
||||
app:tint="@color/offline_warning"
|
||||
android:contentDescription="@string/warning_desc"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginStart="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/offline_warning_title"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/offline_warning_msg"
|
||||
android:textColor="#80ffffff"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/bottom_panel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:visibility="gone"
|
||||
android:paddingVertical="5dp"
|
||||
android:background="@color/primary">
|
||||
|
||||
@ -54,7 +119,7 @@
|
||||
android:hint="@string/message_hint"
|
||||
android:autofillHints="message"
|
||||
style="@style/EditText"
|
||||
android:inputType="textShortMessage|textAutoCorrect|textCapSentences"
|
||||
android:inputType="textMultiLine|textShortMessage|textAutoCorrect|textCapSentences"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_image_trust_level"
|
||||
|
@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bubble_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
style="@style/Bubble"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingVertical="5dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_save"
|
||||
android:layout_width="@dimen/image_button_size"
|
||||
android:layout_height="@dimen/image_button_size"
|
||||
android:src="@drawable/ic_save"
|
||||
style="@style/ImageButton"
|
||||
android:contentDescription="@string/download" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_filename"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:textColor="@color/messageTextColor"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
45
app/src/main/res/layout/adapter_chat_item.xml
Normal file
45
app/src/main/res/layout/adapter_chat_item.xml
Normal file
@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="1dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:paddingVertical="5dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_pending_msg"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/pending_messages"
|
||||
android:textColor="@color/pending_msg_indicator"
|
||||
android:background="@drawable/pending_msg_indicator_background"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bubble"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
style="@style/Bubble">
|
||||
|
||||
<View
|
||||
android:id="@+id/bubble_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_hour"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="1dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:autoLink="all"
|
||||
android:textColor="@color/messageTextColor"
|
||||
style="@style/Bubble"/>
|
||||
|
||||
</LinearLayout>
|
24
app/src/main/res/layout/file_bubble_content.xml
Normal file
24
app/src/main/res/layout/file_bubble_content.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_save"
|
||||
android:layout_width="@dimen/image_button_size"
|
||||
android:layout_height="@dimen/image_button_size"
|
||||
android:src="@drawable/ic_save"
|
||||
android:scaleType="fitXY"
|
||||
android:background="@color/transparent"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="@string/download" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_filename"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:textColor="@color/messageTextColor"/>
|
||||
|
||||
</LinearLayout>
|
8
app/src/main/res/layout/message_bubble_content.xml
Normal file
8
app/src/main/res/layout/message_bubble_content.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/text_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="all"
|
||||
android:textColor="@color/messageTextColor"
|
||||
android:textIsSelectable="true"/>
|
121
app/src/main/res/values-es/strings-es.xml
Normal file
121
app/src/main/res/values-es/strings-es.xml
Normal file
@ -0,0 +1,121 @@
|
||||
<resources>
|
||||
<string name="app_name">AIRA</string>
|
||||
<string name="create_identity_test">Crear una nueva identidad:</string>
|
||||
<string name="identity_name">Nombre de la identidad</string>
|
||||
<string name="password_hint">Contraseña</string>
|
||||
<string name="password_confirm_hint">Contraseña (confirmación)</string>
|
||||
<string name="create">Crear</string>
|
||||
<string name="password_mismatch">¡Las contraseñas no coinciden!</string>
|
||||
<string name="online_peers">Pares en línea:</string>
|
||||
<string name="offline_contacts">Contactos desconectados:</string>
|
||||
<string name="identity_create_failed">Fallo al crear la identidad</string>
|
||||
<string name="identity_load_failed">Fallo al cargar la identidad. Por favor, verifica tu contraseña</string>
|
||||
<string name="service_name">Servicio en segundo plano de AIRA</string>
|
||||
<string name="login">Acceder</string>
|
||||
<string name="add_peer_ip">Añadir pares por IP</string>
|
||||
<string name="unknown">Desconocido</string>
|
||||
<string name="file_saved">Archivo guardado en %s !</string>
|
||||
<string name="enter_password">Ingresa tu contraseña:</string>
|
||||
<string name="yes">Sí</string>
|
||||
<string name="no">No</string>
|
||||
<string name="warning">¡Advertencia!</string>
|
||||
<string name="ask_log_out">¿En verdad quieres cerrar sesión? (No recibirás nuevos mensajes hasta que accedas nuevamente)</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="background_service">Servicio en segundo plano</string>
|
||||
<string name="verifying_contact">Verificación del contacto</string>
|
||||
<string name="fingerprints_instructions">Compara las siguientes huellas digitales por un modo de comunicación seguro (como en persona) y asegurate de que coinciden..</string>
|
||||
<string name="local_fingerprint">Huellas digital local:</string>
|
||||
<string name="peer_fingerprint">Huellas digital del par:</string>
|
||||
<string name="summary_name">El nombre de tu identiad. Se muestra a todas las sesiones activas.</string>
|
||||
<string name="delete_identity">Eliminar identidad</string>
|
||||
<string name="summary_delete_identity">Eliminar todos tus datos. Ya no podrás ser reconocido por tus contactos.</string>
|
||||
<string name="preference_password">Contraseña de identidad</string>
|
||||
<string name="summary_password">Sin esta contraseña no puedes acceder a tus datos ni ser reconocido por tus contactos.</string>
|
||||
<string name="change_password">Cambiar contraseña:</string>
|
||||
<string name="ok">De acuerdo</string>
|
||||
<string name="change_password_failed">Operación fallida. Por favor, compruebe su antigua contraseña.</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="current_password">Contraseña actual</string>
|
||||
<string name="new_password">Nueva contraseña (vacío para no usar contraseña)</string>
|
||||
<string name="new_password_confirm">Nueva contraseña (confirmación)</string>
|
||||
<string name="confirm_delete">¿Estás seguro de que quieres eliminar todas tus conversaciones (mensajes y archivos), todos tus contactos y tu clave privada?</string>
|
||||
<string name="they_match">Coinciden</string>
|
||||
<string name="ask_delete_conversation">Borrar una conversación sólo te afecta a ti. Tu contacto seguirá teniendo una copia de esta conversación si no la borra también. ¿Realmente quieres borrar toda esta conversación (mensajes y archivos)?</string>
|
||||
<string name="delete">Eliminar</string>
|
||||
<string name="ask_remove_contact">Al borrar el contacto se eliminará su clave de identidad y su conversación (mensajes y archivos). Ya no podrás reconocerlo. Esta acción sólo te afecta a ti. ¿Realmente quieres eliminar este contacto?</string>
|
||||
<string name="ask_remove_contacts">Al borrar los contactos se eliminarán sus claves de identidad y sus conversaciones (mensajes y archivos). Ya no podrás reconocerlos. Esta acción sólo te afecta a ti. ¿Realmente quieres eliminar estos contactos?</string>
|
||||
<string name="enable_password">Encriptar con contraseña</string>
|
||||
<string name="msg_notification_channel_name">Nuevos mensajes</string>
|
||||
<string name="mark_read">Marcar como leído</string>
|
||||
<string name="ask_file_notification_channel">Solicitudes de descarga de archivos</string>
|
||||
<string name="db_mkdir_failed">Fallo en la creación del directorio de bases de datos</string>
|
||||
<string name="want_to_send_files">%s quiere enviarte algunos archivos</string>
|
||||
<string name="download_file_request">Solicitud de descarga de archivos</string>
|
||||
<string name="download">Descargar</string>
|
||||
<string name="refuse">Rechazar</string>
|
||||
<string name="file_transfers">Transferencias de archivos</string>
|
||||
<string name="transfer_aborted">Transferencia abortada</string>
|
||||
<string name="transfer_completed">Transferencia completa</string>
|
||||
<string name="reply">Responder</string>
|
||||
<string name="open_uri_failed">Fallo en la apertura de la URI</string>
|
||||
<string name="ask_send_single_file">¿Enviar %s (%s) a %s ?</string>
|
||||
<string name="ask_send_multiple_files">¿Enviar %d archivos a %s?</string>
|
||||
<string name="share_label">Enviar con AIRA</string>
|
||||
<string name="identity_fingerprint">La huella digital de tu identidad:</string>
|
||||
<string name="fingerprint">Huella digital:</string>
|
||||
<string name="peer_ip">IP de par:</string>
|
||||
<string name="connection">Conexión:</string>
|
||||
<string name="outgoing">Saliente</string>
|
||||
<string name="incoming">Entrante</string>
|
||||
<string name="delete_conversation">Eliminar la convesación</string>
|
||||
<string name="verify">Verificando</string>
|
||||
<string name="add_contact">Añadir contacto</string>
|
||||
<string name="remove_contact">Eliminar contacto</string>
|
||||
<string name="details">Detalles</string>
|
||||
<string name="your_addresses">Tu dirección IP:</string>
|
||||
<string name="file_transfer_already_in_progress">Ya está en marcha otra transferencia de archivos</string>
|
||||
<string name="settings">Configuraciones</string>
|
||||
<string name="log_out">Cerrar sesión</string>
|
||||
<string name="copied">¡Copiado al portapapeles!</string>
|
||||
<string name="identity">Identidad</string>
|
||||
<string name="about">Acerca de</string>
|
||||
<string name="version">Versión de AIRA</string>
|
||||
<string name="refresh_profile">Actualizar perfil</string>
|
||||
<string name="security">Seguridad</string>
|
||||
<string name="use_psec_padding">Usar PSEC padding</string>
|
||||
<string name="psec_padding_summary">PSEC padding ofusca la longitud de sus mensajes pero utiliza más ancho de banda de la red.</string>
|
||||
<string name="is_contact">Es un contacto:</string>
|
||||
<string name="is_verified">Está verificado:</string>
|
||||
<string name="avatar_too_large">El avatar no puede ser mayor de 10MB.</string>
|
||||
<string name="your_avatar">Tu avatar:</string>
|
||||
<string name="set_a_new_one">Establecer uno nuevo</string>
|
||||
<string name="remove">Eliminar</string>
|
||||
<string name="choose_avatar">Seleccionar avatar</string>
|
||||
<string name="message_hint">Enviar un mensaje…</string>
|
||||
<string name="no_name_error">¡Esta sesión no tiene nombre!</string>
|
||||
<string name="invalid_ip">Dirección IP inválida</string>
|
||||
<string name="unable_to_connect_to">No se puede conectar con %s</string>
|
||||
<string name="github">GitHub</string>
|
||||
<string name="github_summary">Repositorio AIRA-android en GitHub.</string>
|
||||
<string name="gitea">Gitea</string>
|
||||
<string name="gitea_summary">Repositorio AIRA-android en la instancia Gitea del proyecto Chapril. A diferencia de GitHub, Gitea es un software totalmente libre y autoalojado.</string>
|
||||
<string name="offline_warning_title">Su contacto parece estar desconectado.</string>
|
||||
<string name="offline_warning_msg">Los mensajes enviados se almacenarán hasta que se establezca la conexión.</string>
|
||||
<string name="pending_messages">Mensajes pendientes:</string>
|
||||
<string name="sending_pending_messages">Enviando de mensajes pendientes…</string>
|
||||
<string name="stop">Detener</string>
|
||||
<string name="app">Aplicación</string>
|
||||
<string name="start_at_boot">Iniciar el servicio AIRA en el arranque</string>
|
||||
<string name="start_at_boot_summary">Si se desactiva, no recibirás mensajes hasta que abras la aplicación manualmente.</string>
|
||||
<string name="start_at_boot_summary_identity_protected">Sólo está disponible si la identidad no está protegida por una contraseña.</string>
|
||||
|
||||
<!--accessibility strings-->
|
||||
<string name="send_file">Enviar archivo</string>
|
||||
<string name="trust_level_indicator">Indicador del nivel de confianza</string>
|
||||
<string name="send_message">Envíar mensaje</string>
|
||||
<string name="show_your_ips">Muestra tus IPs</string>
|
||||
<string name="clickable_indicator">Indicador de clicks</string>
|
||||
<string name="avatar">Avatar</string>
|
||||
<string name="name">Nombre</string>
|
||||
<string name="warning_desc">Icono de advertencia</string>
|
||||
</resources>
|
@ -12,4 +12,9 @@
|
||||
<color name="itemSelected">#66666666</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
<color name="sessionArrow">#777777</color>
|
||||
<color name="outgoingTimestamp">#777777</color>
|
||||
<color name="incomingTimestamp">#d7d7d7</color>
|
||||
<color name="incomingHighlight">@color/outgoingTextLink</color>
|
||||
<color name="offline_warning">#ff0000</color>
|
||||
<color name="pending_msg_indicator">#909090</color>
|
||||
</resources>
|
@ -14,7 +14,7 @@
|
||||
<string name="login">Login</string>
|
||||
<string name="add_peer_ip">Add peer by IP</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
<string name="file_saved">File saved !</string>
|
||||
<string name="file_saved">File saved to %s !</string>
|
||||
<string name="enter_password">Enter your password:</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
@ -76,7 +76,6 @@
|
||||
<string name="file_transfer_already_in_progress">Another file transfer is already in progress</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="log_out">Log out</string>
|
||||
<string name="loadFile_failed">File extraction failed</string>
|
||||
<string name="copied">Copied to clipboard !</string>
|
||||
<string name="identity">Identity</string>
|
||||
<string name="about">About</string>
|
||||
@ -100,6 +99,15 @@
|
||||
<string name="github_summary">AIRA-android repository on GitHub.</string>
|
||||
<string name="gitea">Gitea</string>
|
||||
<string name="gitea_summary">AIRA-android repository on the Gitea instance of the Chapril project. Unlike GitHub, Gitea is fully free software and self-hosted.</string>
|
||||
<string name="offline_warning_title">Your contact seems to be offline.</string>
|
||||
<string name="offline_warning_msg">Sent messages will be stored until a connection is established.</string>
|
||||
<string name="pending_messages">Pending messages:</string>
|
||||
<string name="sending_pending_messages">Sending pending messages…</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="app">App</string>
|
||||
<string name="start_at_boot">Start AIRA service at boot</string>
|
||||
<string name="start_at_boot_summary">If disabled, you won\'t receive messages until you open the app manually.</string>
|
||||
<string name="start_at_boot_summary_identity_protected">Only available if identity is not protected by a password.</string>
|
||||
|
||||
<!--accessibility strings-->
|
||||
<string name="send_file">Send file</string>
|
||||
@ -109,4 +117,5 @@
|
||||
<string name="clickable_indicator">Clickable indicator</string>
|
||||
<string name="avatar">Avatar</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="warning_desc">Warning icon</string>
|
||||
</resources>
|
@ -7,7 +7,6 @@
|
||||
<item name="android:paddingBottom">8dp</item>
|
||||
</style>
|
||||
<style name="Bubble">
|
||||
<item name="android:background">@drawable/background_adapter_bubble</item>
|
||||
<item name="android:paddingTop">10dp</item>
|
||||
<item name="android:paddingBottom">10dp</item>
|
||||
<item name="android:paddingStart">15dp</item>
|
||||
|
@ -34,6 +34,17 @@
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/app">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="startAtBoot"
|
||||
android:defaultValue="true"
|
||||
android:title="@string/start_at_boot"
|
||||
android:summary="@string/start_at_boot_summary"
|
||||
android:icon="@drawable/ic_shuttle"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/security">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
|
@ -1,11 +1,11 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.5.10"
|
||||
ext.kotlin_version = "1.6.21"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||
classpath 'com.android.tools.build:gradle:7.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
6
gradle/wrapper/gradle-wrapper.properties
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Thu Jan 21 14:03:53 UTC 2021
|
||||
#Thu Sep 02 16:55:14 UTC 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
BIN
screenshots/1.png
Normal file
BIN
screenshots/1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
screenshots/2.png
Normal file
BIN
screenshots/2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 86 KiB |
BIN
screenshots/3.png
Normal file
BIN
screenshots/3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
Loading…
Reference in New Issue
Block a user