Display local IP addresses & Safe messages parsing
This commit is contained in:
parent
19ccc94453
commit
490673052d
@ -1,5 +1,5 @@
|
|||||||
# AIRA Android
|
# AIRA Android
|
||||||
AIRA is peer-to-peer encrypted communication tool for local networks built on the [PSEC protocol](https://forge.chapril.org/hardcoresushi/PSEC). It allows to securely send text messages and files without any server or Internet access.
|
AIRA is peer-to-peer encrypted communication tool for local networks built on the [PSEC protocol](https://forge.chapril.org/hardcoresushi/PSEC). It allows to securely send text messages and files without any server or Internet access. AIRA automatically discovers and connects to other peers on your network, so you don't need any prior configuration to start communicating.
|
||||||
|
|
||||||
Here is the Android version. You can find the original AIRA desktop version [here](https://forge.chapril.org/hardcoresushi/AIRA).
|
Here is the Android version. You can find the original AIRA desktop version [here](https://forge.chapril.org/hardcoresushi/AIRA).
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.3.2'
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation "androidx.fragment:fragment-ktx:1.3.2"
|
implementation "androidx.fragment:fragment-ktx:1.3.3"
|
||||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||||
implementation 'com.google.android.material:material:1.3.0'
|
implementation 'com.google.android.material:material:1.3.0'
|
||||||
//implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
//implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="sushi.hardcore.aira">
|
package="sushi.hardcore.aira"
|
||||||
|
android:installLocation="auto">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
@ -7,8 +7,6 @@ import android.provider.OpenableColumns
|
|||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
@ -21,6 +19,8 @@ import sushi.hardcore.aira.background_service.AIRAService
|
|||||||
import sushi.hardcore.aira.background_service.Protocol
|
import sushi.hardcore.aira.background_service.Protocol
|
||||||
import sushi.hardcore.aira.background_service.ReceiveFileTransfer
|
import sushi.hardcore.aira.background_service.ReceiveFileTransfer
|
||||||
import sushi.hardcore.aira.databinding.ActivityChatBinding
|
import sushi.hardcore.aira.databinding.ActivityChatBinding
|
||||||
|
import sushi.hardcore.aira.databinding.DialogFingerprintsBinding
|
||||||
|
import sushi.hardcore.aira.databinding.DialogInfoBinding
|
||||||
import sushi.hardcore.aira.utils.FileUtils
|
import sushi.hardcore.aira.utils.FileUtils
|
||||||
import sushi.hardcore.aira.utils.StringUtils
|
import sushi.hardcore.aira.utils.StringUtils
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -36,7 +36,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
private var lastLoadedMessageOffset = 0
|
private var lastLoadedMessageOffset = 0
|
||||||
private var isActivityInForeground = false
|
private var isActivityInForeground = false
|
||||||
private val filePicker = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
private val filePicker = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||||
if (::airaService.isInitialized) {
|
if (::airaService.isInitialized && uri != null) {
|
||||||
contentResolver.query(uri, null, null, null, null)?.let { cursor ->
|
contentResolver.query(uri, null, null, null, null)?.let { cursor ->
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
contentResolver.openInputStream(uri)?.let { inputStream ->
|
contentResolver.openInputStream(uri)?.let { inputStream ->
|
||||||
@ -238,13 +238,14 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
val contact = airaService.contacts[sessionId]
|
val contact = airaService.contacts[sessionId]
|
||||||
val session = airaService.sessions[sessionId]
|
val session = airaService.sessions[sessionId]
|
||||||
val publicKey = contact?.publicKey ?: session?.peerPublicKey
|
val publicKey = contact?.publicKey ?: session?.peerPublicKey
|
||||||
val dialogView = layoutInflater.inflate(R.layout.dialog_info, null)
|
val dialogBinding = DialogInfoBinding.inflate(layoutInflater)
|
||||||
dialogView.findViewById<TextView>(R.id.text_fingerprint).text = StringUtils.beautifyFingerprint(generateFingerprint(publicKey!!))
|
dialogBinding.textAvatar.setLetterFrom(sessionName)
|
||||||
|
dialogBinding.textFingerprint.text = StringUtils.beautifyFingerprint(generateFingerprint(publicKey!!))
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
dialogView.findViewById<LinearLayout>(R.id.online_fields).visibility = View.GONE
|
dialogBinding.onlineFields.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
dialogView.findViewById<TextView>(R.id.text_ip).text = session.ip
|
dialogBinding.textIp.text = session.ip
|
||||||
dialogView.findViewById<TextView>(R.id.text_outgoing).text = getString(if (session.outgoing) {
|
dialogBinding.textOutgoing.text = getString(if (session.outgoing) {
|
||||||
R.string.outgoing
|
R.string.outgoing
|
||||||
} else {
|
} else {
|
||||||
R.string.incoming
|
R.string.incoming
|
||||||
@ -252,7 +253,7 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(sessionName)
|
.setTitle(sessionName)
|
||||||
.setView(dialogView)
|
.setView(dialogBinding.root)
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
true
|
true
|
||||||
@ -282,12 +283,12 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
airaService.contacts[sessionId]?.let { contact ->
|
airaService.contacts[sessionId]?.let { contact ->
|
||||||
val localFingerprint = StringUtils.beautifyFingerprint(AIRADatabase.getIdentityFingerprint())
|
val localFingerprint = StringUtils.beautifyFingerprint(AIRADatabase.getIdentityFingerprint())
|
||||||
val peerFingerprint = StringUtils.beautifyFingerprint(generateFingerprint(contact.publicKey))
|
val peerFingerprint = StringUtils.beautifyFingerprint(generateFingerprint(contact.publicKey))
|
||||||
val dialogView = layoutInflater.inflate(R.layout.dialog_fingerprints, null)
|
val dialogBinding = DialogFingerprintsBinding.inflate(layoutInflater)
|
||||||
dialogView.findViewById<TextView>(R.id.text_local_fingerprint).text = localFingerprint
|
dialogBinding.textLocalFingerprint.text = localFingerprint
|
||||||
dialogView.findViewById<TextView>(R.id.text_peer_fingerprint).text = peerFingerprint
|
dialogBinding.textPeerFingerprint.text = peerFingerprint
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(R.string.verifying_contact)
|
.setTitle(R.string.verifying_contact)
|
||||||
.setView(dialogView)
|
.setView(dialogBinding.root)
|
||||||
.setPositiveButton(R.string.they_match) { _, _ ->
|
.setPositiveButton(R.string.they_match) { _, _ ->
|
||||||
if (airaService.setVerified(sessionId)) {
|
if (airaService.setVerified(sessionId)) {
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
|
@ -10,6 +10,7 @@ import android.os.IBinder
|
|||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.widget.AbsListView
|
||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
@ -19,13 +20,31 @@ import sushi.hardcore.aira.adapters.SessionAdapter
|
|||||||
import sushi.hardcore.aira.background_service.AIRAService
|
import sushi.hardcore.aira.background_service.AIRAService
|
||||||
import sushi.hardcore.aira.background_service.ReceiveFileTransfer
|
import sushi.hardcore.aira.background_service.ReceiveFileTransfer
|
||||||
import sushi.hardcore.aira.databinding.ActivityMainBinding
|
import sushi.hardcore.aira.databinding.ActivityMainBinding
|
||||||
|
import sushi.hardcore.aira.databinding.DialogIpAddressesBinding
|
||||||
import sushi.hardcore.aira.utils.FileUtils
|
import sushi.hardcore.aira.utils.FileUtils
|
||||||
|
import sushi.hardcore.aira.utils.StringUtils
|
||||||
|
import java.lang.StringBuilder
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
private lateinit var airaService: AIRAService
|
private lateinit var airaService: AIRAService
|
||||||
private lateinit var onlineSessionAdapter: SessionAdapter
|
private lateinit var onlineSessionAdapter: SessionAdapter
|
||||||
private lateinit var offlineSessionAdapter: SessionAdapter
|
private lateinit var offlineSessionAdapter: SessionAdapter
|
||||||
|
private val onSessionsItemClick = AdapterView.OnItemClickListener { adapter, _, position, _ ->
|
||||||
|
launchChatActivity(adapter.getItemAtPosition(position) as Session)
|
||||||
|
}
|
||||||
|
private val onSessionsItemClickSendFile = AdapterView.OnItemClickListener { adapter, _, position, _ ->
|
||||||
|
askShareFileTo(adapter.getItemAtPosition(position) as Session)
|
||||||
|
}
|
||||||
|
private val onSessionsScrollListener = object : AbsListView.OnScrollListener {
|
||||||
|
override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {}
|
||||||
|
override fun onScroll(listView: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {
|
||||||
|
if (listView.getChildAt(0) != null) {
|
||||||
|
binding.refresher.isEnabled = listView.firstVisiblePosition == 0 && listView.getChildAt(0).top == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
private val uiCallbacks = object : AIRAService.UiCallbacks {
|
private val uiCallbacks = object : AIRAService.UiCallbacks {
|
||||||
override fun onNewSession(sessionId: Int, ip: String) {
|
override fun onNewSession(sessionId: Int, ip: String) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
@ -75,27 +94,21 @@ class MainActivity : AppCompatActivity() {
|
|||||||
binding.onlineSessions.apply {
|
binding.onlineSessions.apply {
|
||||||
adapter = onlineSessionAdapter
|
adapter = onlineSessionAdapter
|
||||||
onItemClickListener = if (openedToShareFile) {
|
onItemClickListener = if (openedToShareFile) {
|
||||||
AdapterView.OnItemClickListener { _, _, position, _ ->
|
onSessionsItemClickSendFile
|
||||||
askShareFileTo(onlineSessionAdapter.getItem(position))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
AdapterView.OnItemClickListener { _, _, position, _ ->
|
onSessionsItemClick
|
||||||
launchChatActivity(onlineSessionAdapter.getItem(position))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
setOnScrollListener(onSessionsScrollListener)
|
||||||
}
|
}
|
||||||
offlineSessionAdapter = SessionAdapter(this)
|
offlineSessionAdapter = SessionAdapter(this)
|
||||||
binding.offlineSessions.apply {
|
binding.offlineSessions.apply {
|
||||||
adapter = offlineSessionAdapter
|
adapter = offlineSessionAdapter
|
||||||
onItemClickListener = if (openedToShareFile) {
|
onItemClickListener = if (openedToShareFile) {
|
||||||
AdapterView.OnItemClickListener { _, _, position, _ ->
|
onSessionsItemClickSendFile
|
||||||
askShareFileTo(offlineSessionAdapter.getItem(position))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
AdapterView.OnItemClickListener { _, _, position, _ ->
|
onSessionsItemClick
|
||||||
launchChatActivity(offlineSessionAdapter.getItem(position))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
setOnScrollListener(onSessionsScrollListener)
|
||||||
}
|
}
|
||||||
Intent(this, AIRAService::class.java).also { serviceIntent ->
|
Intent(this, AIRAService::class.java).also { serviceIntent ->
|
||||||
bindService(serviceIntent, object : ServiceConnection {
|
bindService(serviceIntent, object : ServiceConnection {
|
||||||
@ -121,6 +134,23 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}, Context.BIND_AUTO_CREATE)
|
}, Context.BIND_AUTO_CREATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.buttonShowIp.setOnClickListener {
|
||||||
|
val ipAddresses = StringBuilder()
|
||||||
|
for (iface in NetworkInterface.getNetworkInterfaces()) {
|
||||||
|
for (addr in iface.inetAddresses) {
|
||||||
|
if (!addr.isLoopbackAddress) {
|
||||||
|
ipAddresses.appendLine(StringUtils.getIpFromInetAddress(addr)+" ("+iface.displayName+')')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val dialogBinding = DialogIpAddressesBinding.inflate(layoutInflater)
|
||||||
|
dialogBinding.textIpAddresses.text = ipAddresses.substring(0, ipAddresses.length-1) //remove last LF
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.your_addresses)
|
||||||
|
.setView(dialogBinding.root)
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
binding.editPeerIp.setOnEditorActionListener { _, _, _ ->
|
binding.editPeerIp.setOnEditorActionListener { _, _, _ ->
|
||||||
if (::airaService.isInitialized){
|
if (::airaService.isInitialized){
|
||||||
airaService.connectTo(binding.editPeerIp.text.toString())
|
airaService.connectTo(binding.editPeerIp.text.toString())
|
||||||
|
@ -31,14 +31,15 @@ class SessionAdapter(context: Context): BaseAdapter() {
|
|||||||
val view: View = convertView ?: inflater.inflate(R.layout.adapter_session, parent, false)
|
val view: View = convertView ?: inflater.inflate(R.layout.adapter_session, parent, false)
|
||||||
val currentSession = getItem(position)
|
val currentSession = getItem(position)
|
||||||
view.findViewById<TextView>(R.id.text_name).apply {
|
view.findViewById<TextView>(R.id.text_name).apply {
|
||||||
if (currentSession.name == null) {
|
view.findViewById<TextAvatar>(R.id.text_avatar).setLetterFrom(if (currentSession.name == null) {
|
||||||
text = currentSession.ip
|
text = currentSession.ip
|
||||||
setTextColor(Color.RED)
|
setTextColor(Color.RED)
|
||||||
|
"?"
|
||||||
} else {
|
} else {
|
||||||
text = currentSession.name
|
text = currentSession.name
|
||||||
setTextColor(Color.WHITE)
|
setTextColor(Color.WHITE)
|
||||||
}
|
currentSession.name!!
|
||||||
view.findViewById<TextAvatar>(R.id.text_avatar).setLetterFrom(text.toString())
|
})
|
||||||
}
|
}
|
||||||
view.findViewById<ImageView>(R.id.image_trust_level).apply {
|
view.findViewById<ImageView>(R.id.image_trust_level).apply {
|
||||||
if (currentSession.isVerified) {
|
if (currentSession.isVerified) {
|
||||||
|
@ -610,9 +610,8 @@ class AIRAService : Service() {
|
|||||||
}
|
}
|
||||||
Protocol.ASK_LARGE_FILE -> {
|
Protocol.ASK_LARGE_FILE -> {
|
||||||
if (receiveFileTransfers[sessionId] == null) {
|
if (receiveFileTransfers[sessionId] == null) {
|
||||||
val fileSize = ByteBuffer.wrap(buffer.sliceArray(1..8)).long
|
Protocol.parseAskFile(buffer)?.let { fileInfo ->
|
||||||
val fileName = buffer.sliceArray(9 until buffer.size).decodeToString()
|
val fileTransfer = ReceiveFileTransfer(fileInfo.fileName, fileInfo.fileSize, { fileTransfer ->
|
||||||
val fileTransfer = ReceiveFileTransfer(fileName, fileSize, { fileTransfer ->
|
|
||||||
createFileTransferNotification(sessionId, fileTransfer)
|
createFileTransferNotification(sessionId, fileTransfer)
|
||||||
sendTo(sessionId, Protocol.acceptLargeFile())
|
sendTo(sessionId, Protocol.acceptLargeFile())
|
||||||
}, {
|
}, {
|
||||||
@ -633,7 +632,7 @@ class AIRAService : Service() {
|
|||||||
.setCategory(NotificationCompat.CATEGORY_EVENT)
|
.setCategory(NotificationCompat.CATEGORY_EVENT)
|
||||||
.setSmallIcon(R.drawable.ic_launcher)
|
.setSmallIcon(R.drawable.ic_launcher)
|
||||||
.setContentTitle(getString(R.string.download_file_request))
|
.setContentTitle(getString(R.string.download_file_request))
|
||||||
.setContentText(getString(R.string.want_to_send_a_file, name, ": $fileName"))
|
.setContentText(getString(R.string.want_to_send_a_file, name, ": "+fileInfo.fileName))
|
||||||
.setOngoing(true) //not cancelable
|
.setOngoing(true) //not cancelable
|
||||||
.setContentIntent(
|
.setContentIntent(
|
||||||
PendingIntent.getActivity(this, 0, Intent(this, ChatActivity::class.java).apply {
|
PendingIntent.getActivity(this, 0, Intent(this, ChatActivity::class.java).apply {
|
||||||
@ -649,17 +648,21 @@ class AIRAService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
when (buffer[0]){
|
when (buffer[0]){
|
||||||
Protocol.MESSAGE -> buffer
|
Protocol.MESSAGE -> buffer
|
||||||
Protocol.FILE -> {
|
Protocol.FILE -> {
|
||||||
val filenameLen = ByteBuffer.wrap(ByteArray(2) +buffer.sliceArray(1..2)).int
|
val smallFile = Protocol.parseSmallFile(buffer)
|
||||||
val filename = buffer.sliceArray(3 until 3+filenameLen)
|
if (smallFile == null) {
|
||||||
val rawFileUuid = AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer.sliceArray(3+filenameLen until buffer.size))
|
null
|
||||||
|
} else {
|
||||||
|
val rawFileUuid = AIRADatabase.storeFile(contacts[sessionId]?.uuid, smallFile.fileContent)
|
||||||
if (rawFileUuid == null) {
|
if (rawFileUuid == null) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
byteArrayOf(Protocol.FILE)+rawFileUuid+filename
|
byteArrayOf(Protocol.FILE)+rawFileUuid+smallFile.rawFileName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -45,5 +45,30 @@ class Protocol {
|
|||||||
fun ackChunk(): ByteArray {
|
fun ackChunk(): ByteArray {
|
||||||
return byteArrayOf(ACK_CHUNK)
|
return byteArrayOf(ACK_CHUNK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SmallFile(val rawFileName: ByteArray, val fileContent: ByteArray)
|
||||||
|
|
||||||
|
fun parseSmallFile(buffer: ByteArray): SmallFile? {
|
||||||
|
if (buffer.size > 3) {
|
||||||
|
val filenameLen = ByteBuffer.wrap(ByteArray(2) +buffer.sliceArray(1..2)).int
|
||||||
|
if (buffer.size > 3+filenameLen) {
|
||||||
|
val rawFileName = buffer.sliceArray(3 until 3+filenameLen)
|
||||||
|
return SmallFile(rawFileName, buffer.sliceArray(3+filenameLen until buffer.size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileInfo(val fileName: String, val fileSize: Long)
|
||||||
|
|
||||||
|
fun parseAskFile(buffer: ByteArray): FileInfo? {
|
||||||
|
return if (buffer.size > 9) {
|
||||||
|
val fileSize = ByteBuffer.wrap(buffer.sliceArray(1..8)).long
|
||||||
|
val fileName = buffer.sliceArray(9 until buffer.size).decodeToString()
|
||||||
|
FileInfo(fileName, fileSize)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -46,7 +46,7 @@ object FileUtils {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@Suppress("Deprecation")
|
@Suppress("Deprecation")
|
||||||
File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), datedFilename).outputStream()
|
File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), File(datedFilename).name).outputStream()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package sushi.hardcore.aira.utils
|
package sushi.hardcore.aira.utils
|
||||||
|
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
object StringUtils {
|
object StringUtils {
|
||||||
fun beautifyFingerprint(fingerprint: String): String {
|
fun beautifyFingerprint(fingerprint: String): String {
|
||||||
val newFingerprint = StringBuilder(fingerprint.length+7)
|
val newFingerprint = StringBuilder(fingerprint.length+7)
|
||||||
@ -9,4 +11,14 @@ object StringUtils {
|
|||||||
newFingerprint.append(fingerprint.slice(fingerprint.length-4 until fingerprint.length))
|
newFingerprint.append(fingerprint.slice(fingerprint.length-4 until fingerprint.length))
|
||||||
return newFingerprint.toString()
|
return newFingerprint.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getIpFromInetAddress(addr: InetAddress): String {
|
||||||
|
val rawIp = addr.hostAddress
|
||||||
|
val i = rawIp.lastIndexOf('%')
|
||||||
|
return if (i == -1) {
|
||||||
|
rawIp
|
||||||
|
} else {
|
||||||
|
rawIp.substring(0, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -33,6 +33,8 @@ class TextAvatar @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setLetterFrom(name: String) {
|
fun setLetterFrom(name: String) {
|
||||||
|
if (name.isNotEmpty()) {
|
||||||
view.findViewById<TextView>(R.id.text_letter).text = name[0].toUpperCase().toString()
|
view.findViewById<TextView>(R.id.text_letter).text = name[0].toUpperCase().toString()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -22,7 +22,7 @@
|
|||||||
<ListView
|
<ListView
|
||||||
android:id="@+id/online_sessions"
|
android:id="@+id/online_sessions"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_online_sessions"/>
|
app:layout_constraintTop_toBottomOf="@id/text_online_sessions"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@ -36,19 +36,46 @@
|
|||||||
<ListView
|
<ListView
|
||||||
android:id="@+id/offline_sessions"
|
android:id="@+id/offline_sessions"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_offline_sessions"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/bottom_panel"/>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/bottom_panel"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toBottomOf="@id/text_offline_sessions"/>
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginHorizontal="20dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/edit_peer_ip"
|
android:id="@+id/edit_peer_ip"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:imeOptions="actionGo"
|
android:imeOptions="actionGo"
|
||||||
android:hint="@string/add_peer_ip"
|
android:hint="@string/add_peer_ip"
|
||||||
android:layout_margin="30dp"
|
android:layout_marginEnd="10dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/button_show_ip"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/button_show_ip"
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:src="@drawable/ic_info"
|
||||||
|
android:scaleType="fitXY"
|
||||||
|
android:background="#00000000"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/edit_peer_ip"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
@ -1,9 +1,18 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:orientation="vertical" android:layout_width="match_parent"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:paddingHorizontal="20dp">
|
android:paddingHorizontal="20dp">
|
||||||
|
|
||||||
|
<sushi.hardcore.aira.widgets.TextAvatar
|
||||||
|
android:id="@+id/text_avatar"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
app:textSize="20sp"
|
||||||
|
android:layout_gravity="center_horizontal"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
18
app/src/main/res/layout/dialog_ip_addresses.xml
Normal file
18
app/src/main/res/layout/dialog_ip_addresses.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?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="match_parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/your_addresses"
|
||||||
|
style="@style/Label"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_ip_addresses"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="center"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -70,4 +70,5 @@
|
|||||||
<string name="add_contact">Add contact</string>
|
<string name="add_contact">Add contact</string>
|
||||||
<string name="remove_contact">Remove contact</string>
|
<string name="remove_contact">Remove contact</string>
|
||||||
<string name="details">Details</string>
|
<string name="details">Details</string>
|
||||||
|
<string name="your_addresses">Your IP addresses:</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = "1.4.21"
|
ext.kotlin_version = "1.4.32"
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
Loading…
Reference in New Issue
Block a user