Compare commits
4 Commits
60f9376e83
...
558248b87e
Author | SHA1 | Date | |
---|---|---|---|
558248b87e | |||
fee2cfaf3d | |||
5b439898d8 | |||
78f120c627 |
@ -9,6 +9,7 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
@ -93,9 +94,11 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
chatAdapter.clear()
|
||||
val contact = airaService.contacts[sessionId]
|
||||
val avatar = if (contact == null) {
|
||||
displayIconTrustLevel(false, false)
|
||||
sessionName = airaService.savedNames[sessionId]
|
||||
airaService.savedAvatars[sessionId]
|
||||
} else {
|
||||
displayIconTrustLevel(true, contact.verified)
|
||||
sessionName = contact.name
|
||||
contact.avatar
|
||||
}
|
||||
@ -110,7 +113,6 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
}
|
||||
}
|
||||
if (contact != null) {
|
||||
displayIconTrustLevel(true, contact.verified)
|
||||
loadMsgs(contact.uuid)
|
||||
}
|
||||
airaService.savedMsgs[sessionId]?.let {
|
||||
@ -128,6 +130,7 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
binding.bottomPanel.visibility = View.VISIBLE
|
||||
}
|
||||
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 {
|
||||
@ -206,15 +209,28 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
}
|
||||
|
||||
private fun displayIconTrustLevel(isContact: Boolean, isVerified: Boolean) {
|
||||
val setResource = fun (imageView: ImageView, resource: Int?) {
|
||||
imageView.apply {
|
||||
visibility = if (resource == null) {
|
||||
View.GONE
|
||||
} else {
|
||||
setImageResource(resource)
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
when {
|
||||
isVerified -> {
|
||||
binding.imageTrustLevel.setImageResource(R.drawable.ic_verified)
|
||||
setResource(binding.toolbar.toolbarImageTrustLevel, R.drawable.ic_verified)
|
||||
setResource(binding.bottomImageTrustLevel, R.drawable.ic_verified)
|
||||
}
|
||||
isContact -> {
|
||||
binding.imageTrustLevel.setImageDrawable(null)
|
||||
setResource(binding.toolbar.toolbarImageTrustLevel, null)
|
||||
setResource(binding.bottomImageTrustLevel, null)
|
||||
}
|
||||
else -> {
|
||||
binding.imageTrustLevel.setImageResource(R.drawable.ic_warning)
|
||||
setResource(binding.toolbar.toolbarImageTrustLevel, R.drawable.ic_warning)
|
||||
setResource(binding.bottomImageTrustLevel, R.drawable.ic_warning)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,15 @@ class MainActivity : ServiceBoundActivity() {
|
||||
}
|
||||
}
|
||||
private val uiCallbacks = object : AIRAService.UiCallbacks {
|
||||
override fun onConnectFailed(ip: String, errorMsg: String?) {
|
||||
var msg = getString(R.string.unable_to_connect_to, ip)
|
||||
errorMsg?.let {
|
||||
msg += ": $it"
|
||||
}
|
||||
runOnUiThread {
|
||||
Toast.makeText(this@MainActivity, msg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
override fun onNewSession(sessionId: Int, ip: String) {
|
||||
runOnUiThread {
|
||||
handleNewSession(sessionId, ip)
|
||||
|
@ -22,6 +22,7 @@ import com.bumptech.glide.request.transition.Transition
|
||||
import sushi.hardcore.aira.background_service.AIRAService
|
||||
import sushi.hardcore.aira.databinding.ActivitySettingsBinding
|
||||
import sushi.hardcore.aira.databinding.ChangeAvatarDialogBinding
|
||||
import sushi.hardcore.aira.databinding.DialogEditTextBinding
|
||||
import sushi.hardcore.aira.utils.AvatarPicker
|
||||
import sushi.hardcore.aira.utils.StringUtils
|
||||
|
||||
@ -46,9 +47,7 @@ class SettingsActivity: AppCompatActivity() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
findPreference<Preference>("identityAvatar")?.let { identityAvatarPreference = it }
|
||||
val identityNamePreference = findPreference<EditTextPreference>("identityName")
|
||||
val paddingPreference = findPreference<SwitchPreferenceCompat>("psecPadding")
|
||||
identityNamePreference?.isPersistent = false
|
||||
paddingPreference?.isPersistent = false
|
||||
AIRADatabase.getIdentityAvatar(Constants.getDatabaseFolder(activity))?.let { avatar ->
|
||||
displayAvatar(avatar)
|
||||
@ -58,7 +57,6 @@ class SettingsActivity: AppCompatActivity() {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
|
||||
val binder = service as AIRAService.AIRABinder
|
||||
airaService = binder.getService()
|
||||
identityNamePreference?.text = airaService.identityName
|
||||
paddingPreference?.isChecked = airaService.usePadding
|
||||
}
|
||||
override fun onServiceDisconnected(name: ComponentName?) {}
|
||||
@ -84,10 +82,17 @@ class SettingsActivity: AppCompatActivity() {
|
||||
dialogBuilder.setView(dialogBinding.root).show()
|
||||
false
|
||||
}
|
||||
identityNamePreference?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (airaService.changeName(newValue as String)) {
|
||||
identityNamePreference.text = newValue
|
||||
}
|
||||
findPreference<Preference>("identityName")?.setOnPreferenceClickListener {
|
||||
val dialogBinding = DialogEditTextBinding.inflate(layoutInflater)
|
||||
dialogBinding.editText.setText(airaService.identityName)
|
||||
AlertDialog.Builder(activity, R.style.CustomAlertDialog)
|
||||
.setTitle(it.title)
|
||||
.setView(dialogBinding.root)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
airaService.changeName(dialogBinding.editText.text.toString())
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
false
|
||||
}
|
||||
findPreference<Preference>("deleteIdentity")?.setOnPreferenceClickListener {
|
||||
|
@ -103,6 +103,7 @@ class AIRAService : Service() {
|
||||
}
|
||||
|
||||
interface UiCallbacks {
|
||||
fun onConnectFailed(ip: String, errorMsg: String?)
|
||||
fun onNewSession(sessionId: Int, ip: String)
|
||||
fun onSessionDisconnect(sessionId: Int)
|
||||
fun onNameTold(sessionId: Int, name: String)
|
||||
@ -456,13 +457,20 @@ class AIRAService : Service() {
|
||||
MESSAGE_CONNECT_TO -> {
|
||||
msg.data.getString("ip")?.let { ip ->
|
||||
Thread {
|
||||
try {
|
||||
val socket = SocketChannel.open()
|
||||
if (socket.connect(InetSocketAddress(ip, Constants.port))) {
|
||||
handleNewSocket(socket, true)
|
||||
val addr = InetSocketAddress(ip, Constants.port)
|
||||
if (addr.isUnresolved) {
|
||||
uiCallbacks?.onConnectFailed(ip, getString(R.string.invalid_ip))
|
||||
} else {
|
||||
try {
|
||||
val socket = SocketChannel.open()
|
||||
if (socket.connect(addr)) {
|
||||
handleNewSocket(socket, true)
|
||||
}
|
||||
} catch (e: NoRouteToHostException) {
|
||||
uiCallbacks?.onConnectFailed(ip, e.message)
|
||||
} catch (e: ConnectException) {
|
||||
uiCallbacks?.onConnectFailed(ip, e.message)
|
||||
}
|
||||
} catch (e: ConnectException) {
|
||||
Log.w("Connect failed", "$ip: "+e.message)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import sushi.hardcore.aira.AIRADatabase
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.*
|
||||
import java.nio.channels.spi.SelectorProvider
|
||||
@ -42,8 +43,6 @@ class Session(private val socket: SocketChannel, val outgoing: Boolean): Selecta
|
||||
}
|
||||
|
||||
private val prng = SecureRandom()
|
||||
private val handshakeSentBuff = ByteArrayOutputStream(handshakeBufferLen)
|
||||
private val handshakeRecvBuff = ByteArrayOutputStream(handshakeBufferLen)
|
||||
private val peerCipher = Cipher.getInstance(CIPHER_TYPE)
|
||||
private val localCipher = Cipher.getInstance(CIPHER_TYPE)
|
||||
private var peerCounter = 0L
|
||||
@ -52,42 +51,37 @@ class Session(private val socket: SocketChannel, val outgoing: Boolean): Selecta
|
||||
lateinit var peerPublicKey: ByteArray
|
||||
val ip: String = socket.socket().inetAddress.hostAddress
|
||||
|
||||
private fun handshakeWrite(buffer: ByteArray) {
|
||||
private fun handshakeWrite(buffer: ByteArray, handshakeSentBuff: OutputStream) {
|
||||
writeAll(buffer)
|
||||
handshakeSentBuff.write(buffer)
|
||||
}
|
||||
|
||||
private fun handshakeRead(buffer: ByteBuffer): Boolean {
|
||||
return if (socket.read(buffer) == buffer.position()) {
|
||||
private fun handshakeRead(buffer: ByteBuffer, handshakeRecvBuff: OutputStream): Boolean {
|
||||
return if (readAll(buffer)) {
|
||||
handshakeRecvBuff.write(buffer.array())
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
private fun handshakeRead(buffer: ByteArray): Boolean {
|
||||
return handshakeRead(ByteBuffer.wrap(buffer))
|
||||
}
|
||||
|
||||
private fun hashHandshake(iAmBob: Boolean): ByteArray {
|
||||
private fun hashHandshake(iAmBob: Boolean, handshakeSentBuff: ByteArray, handshakeRecvBuff: ByteArray): ByteArray {
|
||||
MessageDigest.getInstance("SHA-384").apply {
|
||||
if (iAmBob) {
|
||||
update(handshakeSentBuff.toByteArray())
|
||||
update(handshakeRecvBuff.toByteArray())
|
||||
update(handshakeSentBuff)
|
||||
update(handshakeRecvBuff)
|
||||
} else {
|
||||
update(handshakeRecvBuff.toByteArray())
|
||||
update(handshakeSentBuff.toByteArray())
|
||||
update(handshakeRecvBuff)
|
||||
update(handshakeSentBuff)
|
||||
}
|
||||
return digest()
|
||||
}
|
||||
}
|
||||
|
||||
private fun amIBob(): Boolean {
|
||||
val s = handshakeSentBuff.toByteArray()
|
||||
val r = handshakeRecvBuff.toByteArray()
|
||||
for (i in s.indices) {
|
||||
if (s[i] != r[i]) {
|
||||
return s[i].toInt() and 0xff < r[i].toInt() and 0xff
|
||||
private fun amIBob(handshakeSentBuff: ByteArray, handshakeRecvBuff: ByteArray): Boolean {
|
||||
for (i in handshakeSentBuff.indices) {
|
||||
if (handshakeSentBuff[i] != handshakeRecvBuff[i]) {
|
||||
return handshakeSentBuff[i].toInt() and 0xff < handshakeRecvBuff[i].toInt() and 0xff
|
||||
}
|
||||
}
|
||||
throw SecurityException("Handshake buffers are identical")
|
||||
@ -102,64 +96,62 @@ class Session(private val socket: SocketChannel, val outgoing: Boolean): Selecta
|
||||
}
|
||||
|
||||
fun doHandshake(): Boolean {
|
||||
val handshakeSentBuff = ByteArrayOutputStream(handshakeBufferLen)
|
||||
val handshakeRecvBuff = ByteArrayOutputStream(handshakeBufferLen)
|
||||
|
||||
val randomBuffer = ByteArray(RANDOM_LEN)
|
||||
prng.nextBytes(randomBuffer)
|
||||
val curve25519Cipher = Curve25519.getInstance(Curve25519.BEST)
|
||||
val keypair = curve25519Cipher.generateKeyPair()
|
||||
handshakeWrite(randomBuffer+keypair.publicKey)
|
||||
handshakeWrite(randomBuffer+keypair.publicKey, handshakeSentBuff)
|
||||
|
||||
var recvBuffer = ByteBuffer.allocate(RANDOM_LEN+PUBLIC_KEY_LEN)
|
||||
if (handshakeRead(recvBuffer)) {
|
||||
if (handshakeRead(recvBuffer, handshakeRecvBuff)) {
|
||||
val peerEphemeralPublicKey = recvBuffer.array().sliceArray(RANDOM_LEN until recvBuffer.capacity())
|
||||
val sharedSecret = curve25519Cipher.calculateAgreement(peerEphemeralPublicKey, keypair.privateKey)
|
||||
val iAmBob = amIBob() //mutual consensus for keys attribution
|
||||
var handshakeHash = hashHandshake(iAmBob)
|
||||
val iAmBob = amIBob(handshakeSentBuff.toByteArray(), handshakeRecvBuff.toByteArray()) //mutual consensus for keys attribution
|
||||
var handshakeHash = hashHandshake(iAmBob, handshakeSentBuff.toByteArray(), handshakeRecvBuff.toByteArray())
|
||||
val handshakeKeys = deriveHandshakeKeys(sharedSecret, handshakeHash, iAmBob)
|
||||
|
||||
prng.nextBytes(randomBuffer)
|
||||
handshakeWrite(randomBuffer)
|
||||
if (handshakeRead(randomBuffer)) {
|
||||
val localCipher = Cipher.getInstance(CIPHER_TYPE)
|
||||
localCipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(handshakeKeys.localKey, "AES"), GCMParameterSpec(AES_TAG_LEN*8, ivToNonce(handshakeKeys.localIv, 0)))
|
||||
handshakeWrite(localCipher.doFinal(AIRADatabase.getIdentityPublicKey()+sign(keypair.publicKey)))
|
||||
val localCipher = Cipher.getInstance(CIPHER_TYPE)
|
||||
localCipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(handshakeKeys.localKey, "AES"), GCMParameterSpec(AES_TAG_LEN*8, handshakeKeys.localIv))
|
||||
handshakeWrite(localCipher.doFinal(randomBuffer+AIRADatabase.getIdentityPublicKey()+sign(keypair.publicKey)), handshakeSentBuff)
|
||||
|
||||
recvBuffer = ByteBuffer.allocate(PUBLIC_KEY_LEN+SIGNATURE_LEN+AES_TAG_LEN)
|
||||
if (handshakeRead(recvBuffer)) {
|
||||
val peerCipher = Cipher.getInstance(CIPHER_TYPE)
|
||||
peerCipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(handshakeKeys.peerKey, "AES"), GCMParameterSpec(AES_TAG_LEN*8, ivToNonce(handshakeKeys.peerIv, 0)))
|
||||
val plainText: ByteArray
|
||||
try {
|
||||
plainText = peerCipher.doFinal(recvBuffer.array())
|
||||
} catch (e: BadPaddingException) {
|
||||
Log.w("BadPaddingException", ip)
|
||||
return false
|
||||
} catch (e: AEADBadTagException) {
|
||||
Log.w("AEADBadTagException", ip)
|
||||
return false
|
||||
}
|
||||
peerPublicKey = plainText.sliceArray(0 until PUBLIC_KEY_LEN)
|
||||
val signature = plainText.sliceArray(PUBLIC_KEY_LEN until plainText.size)
|
||||
recvBuffer = ByteBuffer.allocate(RANDOM_LEN+PUBLIC_KEY_LEN+SIGNATURE_LEN+AES_TAG_LEN)
|
||||
if (handshakeRead(recvBuffer, handshakeRecvBuff)) {
|
||||
val peerCipher = Cipher.getInstance(CIPHER_TYPE)
|
||||
peerCipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(handshakeKeys.peerKey, "AES"), GCMParameterSpec(AES_TAG_LEN*8, handshakeKeys.peerIv))
|
||||
val plainText: ByteArray
|
||||
try {
|
||||
plainText = peerCipher.doFinal(recvBuffer.array())
|
||||
} catch (e: BadPaddingException) {
|
||||
Log.w("BadPaddingException", ip)
|
||||
return false
|
||||
} catch (e: AEADBadTagException) {
|
||||
Log.w("AEADBadTagException", ip)
|
||||
return false
|
||||
}
|
||||
peerPublicKey = plainText.sliceArray(RANDOM_LEN until RANDOM_LEN+PUBLIC_KEY_LEN)
|
||||
val signature = plainText.sliceArray(RANDOM_LEN+PUBLIC_KEY_LEN until plainText.size)
|
||||
|
||||
val edDSAEngine = EdDSAEngine().apply {
|
||||
initVerify(EdDSAPublicKey(EdDSAPublicKeySpec(peerPublicKey, EdDSANamedCurveTable.ED_25519_CURVE_SPEC)))
|
||||
}
|
||||
if (edDSAEngine.verifyOneShot(peerEphemeralPublicKey, signature)) {
|
||||
handshakeHash = hashHandshake(iAmBob)
|
||||
val handshakeFinished = computeHandshakeFinished(handshakeKeys.localHandshakeTrafficSecret, handshakeHash)
|
||||
writeAll(handshakeFinished)
|
||||
val peerHandshakeFinished = ByteBuffer.allocate(HASH_OUTPUT_LEN)
|
||||
socket.read(peerHandshakeFinished)
|
||||
if (verifyHandshakeFinished(peerHandshakeFinished.array(), handshakeKeys.peerHandshakeTrafficSecret, handshakeHash)){
|
||||
applicationKeys = deriveApplicationKeys(handshakeKeys.handshakeSecret, handshakeHash, iAmBob)
|
||||
handshakeSentBuff.reset()
|
||||
handshakeRecvBuff.reset()
|
||||
return true
|
||||
} else {
|
||||
Log.w("Handshake", "Final verification failed")
|
||||
}
|
||||
val edDSAEngine = EdDSAEngine().apply {
|
||||
initVerify(EdDSAPublicKey(EdDSAPublicKeySpec(peerPublicKey, EdDSANamedCurveTable.ED_25519_CURVE_SPEC)))
|
||||
}
|
||||
if (edDSAEngine.verifyOneShot(peerEphemeralPublicKey, signature)) {
|
||||
handshakeHash = hashHandshake(iAmBob, handshakeSentBuff.toByteArray(), handshakeRecvBuff.toByteArray())
|
||||
val handshakeFinished = computeHandshakeFinished(handshakeKeys.localHandshakeTrafficSecret, handshakeHash)
|
||||
writeAll(handshakeFinished)
|
||||
val peerHandshakeFinished = ByteBuffer.allocate(HASH_OUTPUT_LEN)
|
||||
socket.read(peerHandshakeFinished)
|
||||
if (verifyHandshakeFinished(peerHandshakeFinished.array(), handshakeKeys.peerHandshakeTrafficSecret, handshakeHash)){
|
||||
applicationKeys = deriveApplicationKeys(handshakeKeys.handshakeSecret, handshakeHash, iAmBob)
|
||||
return true
|
||||
} else {
|
||||
Log.w("Handshake", "Signature verification failed")
|
||||
Log.w("Handshake", "Final verification failed")
|
||||
}
|
||||
} else {
|
||||
Log.w("Handshake", "Signature verification failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,14 +34,14 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/image_trust_level"/>
|
||||
app:layout_constraintEnd_toStartOf="@id/bottom_image_trust_level"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_trust_level"
|
||||
android:id="@+id/bottom_image_trust_level"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_warning"
|
||||
android:contentDescription="@string/trust_level_indicator"
|
||||
android:layout_marginEnd="5dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/button_attach"
|
||||
@ -53,10 +53,11 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/message_hint"
|
||||
android:autofillHints="message"
|
||||
android:inputType="text"
|
||||
style="@style/EditText"
|
||||
android:inputType="textShortMessage|textAutoCorrect|textCapSentences"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/image_trust_level"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_image_trust_level"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_send"/>
|
||||
|
||||
<ImageButton
|
||||
|
@ -69,6 +69,7 @@
|
||||
android:imeOptions="actionGo"
|
||||
android:hint="@string/add_peer_ip"
|
||||
android:autofillHints="ip"
|
||||
style="@style/EditText"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
15
app/src/main/res/layout/dialog_edit_text.xml
Normal file
15
app/src/main/res/layout/dialog_edit_text.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="30dp"
|
||||
android:autofillHints="name"
|
||||
android:hint="@string/name"
|
||||
android:inputType="textPersonName"/>
|
||||
|
||||
</RelativeLayout>
|
@ -25,8 +25,16 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/toolbar_image_trust_level"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
@ -3,10 +3,10 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/delete_conversation"
|
||||
android:id="@+id/session_info"
|
||||
app:showAsAction="ifRoom"
|
||||
android:icon="@drawable/ic_delete_conversation"
|
||||
android:title="@string/delete_conversation" />
|
||||
android:icon="@drawable/ic_info"
|
||||
android:title="@string/details"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/verify"
|
||||
@ -27,10 +27,10 @@
|
||||
android:title="@string/remove_contact"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/session_info"
|
||||
android:id="@+id/delete_conversation"
|
||||
app:showAsAction="ifRoom"
|
||||
android:icon="@drawable/ic_info"
|
||||
android:title="@string/details"/>
|
||||
android:icon="@drawable/ic_delete_conversation"
|
||||
android:title="@string/delete_conversation" />
|
||||
|
||||
<item
|
||||
android:id="@+id/refresh_profile"
|
||||
|
@ -94,6 +94,8 @@
|
||||
<string name="choose_avatar">Choose avatar</string>
|
||||
<string name="message_hint">Send a message…</string>
|
||||
<string name="no_name_error">This session has no name !</string>
|
||||
<string name="invalid_ip">Invalid IP address</string>
|
||||
<string name="unable_to_connect_to">Unable to connect to %s</string>
|
||||
|
||||
<!--accessibility strings-->
|
||||
<string name="send_file">Send file</string>
|
||||
@ -102,4 +104,5 @@
|
||||
<string name="show_your_ips">Show your IPs</string>
|
||||
<string name="clickable_indicator">Clickable indicator</string>
|
||||
<string name="avatar">Avatar</string>
|
||||
</resources>
|
||||
<string name="name">Name</string>
|
||||
</resources>
|
@ -18,6 +18,10 @@
|
||||
<item name="android:scaleType">fitXY</item>
|
||||
<item name="android:layout_margin">5dp</item>
|
||||
</style>
|
||||
<style name="EditText" parent="Widget.AppCompat.EditText">
|
||||
<item name="android:background">@color/transparent</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
</style>
|
||||
<style name="Theme.AIRA.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
|
@ -9,7 +9,7 @@
|
||||
android:summary="The avatar of your identity. Shown to all active sessions."
|
||||
android:icon="@drawable/ic_face"/>
|
||||
|
||||
<EditTextPreference
|
||||
<Preference
|
||||
android:key="identityName"
|
||||
android:title="@string/identity_name"
|
||||
android:summary="@string/summary_name"
|
||||
|
Loading…
Reference in New Issue
Block a user