Local network secure P2P communications
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

371 lines
15 KiB

package sushi.hardcore.aira
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
import android.os.Bundle
import android.os.IBinder
import android.view.Menu
import android.view.MenuItem
import android.widget.AbsListView
import android.widget.AdapterView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
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
import sushi.hardcore.aira.utils.StringUtils
import java.net.NetworkInterface
class MainActivity : ServiceBoundActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var onlineSessionAdapter: SessionAdapter
private var offlineSessionAdapter: SessionAdapter? = null
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 {
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)
}
}
override fun onSessionDisconnect(sessionId: Int) {
runOnUiThread {
onlineSessionAdapter.remove(sessionId)?.let { session ->
if (session.isContact) {
offlineSessionAdapter?.add(session)
}
}
}
}
override fun onNameTold(sessionId: Int, name: String) {
runOnUiThread {
onlineSessionAdapter.setName(sessionId, name)
}
}
override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
runOnUiThread {
onlineSessionAdapter.setAvatar(sessionId, avatar)
}
}
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))
}
return true
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar.toolbar)
val openedToShareFile = intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE
onlineSessionAdapter = SessionAdapter(this)
binding.onlineSessions.apply {
adapter = onlineSessionAdapter
onItemClickListener = if (openedToShareFile) {
onSessionsItemClickSendFile
} else {
AdapterView.OnItemClickListener { _, _, position, _ ->
if (isSelecting()) {
changeSelection(onlineSessionAdapter, position)
} else {
launchChatActivity(onlineSessionAdapter.getItem(position))
}
}
}
onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, position, _ ->
changeSelection(onlineSessionAdapter, position)
true
}
setOnScrollListener(onSessionsScrollListener)
}
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)
}
if (intent.action == NotificationBroadcastReceiver.ACTION_LOGOUT) {
askLogOut()
}
serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
val binder = service as AIRAService.AIRABinder
airaService = binder.getService()
airaService.uiCallbacks = uiCallbacks
airaService.isAppInBackground = false
refreshSessions()
if (!AIRAService.isServiceRunning) {
startService(serviceIntent)
}
initToolbar(airaService.identityName)
}
override fun onServiceDisconnected(name: ComponentName?) {}
}
binding.refresher.setOnRefreshListener {
if (isServiceInitialized()) {
airaService.restartDiscovery()
}
binding.refresher.isRefreshing = false
}
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, R.style.CustomAlertDialog)
.setTitle(R.string.your_addresses)
.setView(dialogBinding.root)
.setPositiveButton(R.string.ok, null)
.show()
}
binding.editPeerIp.setOnEditorActionListener { _, _, _ ->
if (isServiceInitialized()){
airaService.connectTo(binding.editPeerIp.text.toString())
}
binding.editPeerIp.text.clear()
true
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_activity, menu)
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
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.settings -> {
startActivity(Intent(this, SettingsActivity::class.java))
true
}
R.id.close -> {
if (isServiceInitialized()) {
askLogOut()
}
true
}
R.id.remove_contact -> {
AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.warning)
.setMessage(R.string.ask_remove_contacts)
.setPositiveButton(R.string.delete) { _, _ ->
Thread {
for (sessionId in onlineSessionAdapter.getSelectedSessionIds()) {
airaService.removeContact(sessionId)
}
offlineSessionAdapter?.let {
for (sessionId in it.getSelectedSessionIds()) {
airaService.removeContact(sessionId)
}
}
runOnUiThread {
unSelectAll()
refreshSessions()
}
}.start()
}
.setNegativeButton(R.string.cancel, null)
.show()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onPause() {
super.onPause()
if (isServiceInitialized()) {
airaService.isAppInBackground = true
}
}
override fun onBackPressed() {
if (isSelecting()) {
unSelectAll()
} else {
super.onBackPressed()
}
}
private fun initToolbar(identityName: String) {
val avatar = AIRADatabase.getIdentityAvatar(Constants.getDatabaseFolder(this))
if (avatar == null) {
binding.toolbar.avatar.setTextAvatar(identityName)
} else {
binding.toolbar.avatar.setImageAvatar(avatar)
}
binding.toolbar.title.text = identityName
}
private fun refreshSessions() {
onlineSessionAdapter.reset()
offlineSessionAdapter?.reset()
loadContacts()
loadSessions()
}
private fun unSelectAll() {
onlineSessionAdapter.unSelectAll()
offlineSessionAdapter?.unSelectAll()
invalidateOptionsMenu()
}
private fun changeSelection(adapter: SessionAdapter, position: Int) {
val wasSelecting = adapter.selectedItems.isNotEmpty()
adapter.onSelectionChanged(position)
val isSelecting = adapter.selectedItems.isNotEmpty()
if (wasSelecting != isSelecting) {
invalidateOptionsMenu()
}
}
private fun isSelecting(): Boolean {
return onlineSessionAdapter.selectedItems.isNotEmpty() || offlineSessionAdapter?.selectedItems?.isNotEmpty() == true
}
private fun loadContacts() {
if (offlineSessionAdapter != null) {
for ((sessionId, contact) in airaService.contacts) {
offlineSessionAdapter!!.add(Session(sessionId, true, contact.verified, contact.seen, null, contact.name, AIRADatabase.loadAvatar(contact.avatar)))
}
}
}
private fun loadSessions() {
for ((sessionId, session) in airaService.sessions) {
handleNewSession(sessionId, session.ip)
}
}
private fun handleNewSession(sessionId: Int, ip: String) {
val seen = !airaService.notSeen.contains(sessionId)
val contact = airaService.contacts[sessionId]
if (contact == null) {
onlineSessionAdapter.add(Session(sessionId, false, false, seen, ip, airaService.savedNames[sessionId], AIRADatabase.loadAvatar(airaService.savedAvatars[sessionId])))
} else {
onlineSessionAdapter.add(Session(sessionId, true, contact.verified, seen, ip, contact.name, AIRADatabase.loadAvatar(contact.avatar)))
offlineSessionAdapter?.remove(sessionId)
}
}
private fun launchChatActivity(session: Session) {
startActivity(Intent(this, ChatActivity::class.java).apply {
putExtra("sessionId", session.sessionId)
})
}
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) {
Intent.ACTION_SEND -> intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)?.let {
uris = arrayListOf(it)
}
Intent.ACTION_SEND_MULTIPLE -> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
}
if (uris == null) {
Toast.makeText(this, R.string.open_uri_failed, Toast.LENGTH_SHORT).show()
} else {
val msg = if (uris!!.size == 1) {
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 {
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)
}
AlertDialog.Builder(this, R.style.CustomAlertDialog)
.setTitle(R.string.warning)
.setMessage(msg)
.setPositiveButton(R.string.yes) { _, _ ->
airaService.sendFilesFromUris(session.sessionId, uris!!)
finish()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
}