Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Courier Sync #29

Merged
merged 6 commits into from
Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ apply from: 'jacoco.gradle'

android {
compileSdkVersion 30
buildToolsVersion "29.0.3"
buildToolsVersion '29.0.3'
ndkVersion '21.3.6528147'

defaultConfig {
applicationId "tech.relaycorp.gateway"
Expand Down Expand Up @@ -100,6 +101,10 @@ dependencies {
implementation 'tech.relaycorp:relaynet:1.15.6'
implementation 'tech.relaycorp:relaynet-cogrpc:1.1.0'

// Android TLS support for Netty
implementation "io.netty:netty-handler:4.1.50.Final"
implementation 'org.conscrypt:conscrypt-android:2.4.0'

// ORM
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,102 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "f4d34def0a467a21b391ecb53e2175d2",
"identityHash": "1eb6cc04dc6143149575f7545f798e49",
"entities": [
{
"tableName": "Message",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"tableName": "Parcel",
gnarea marked this conversation as resolved.
Show resolved Hide resolved
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`recipientAddress` TEXT NOT NULL, `senderAddress` TEXT NOT NULL, `messageId` TEXT NOT NULL, `recipientLocation` TEXT NOT NULL, `creationTimeUtc` INTEGER NOT NULL, `expirationTimeUtc` INTEGER NOT NULL, `storagePath` TEXT NOT NULL, `size` INTEGER NOT NULL, PRIMARY KEY(`recipientAddress`, `senderAddress`, `messageId`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"fieldPath": "recipientAddress",
"columnName": "recipientAddress",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "senderAddress",
"columnName": "senderAddress",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "messageId",
"columnName": "messageId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "recipientLocation",
"columnName": "recipientLocation",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "creationTimeUtc",
"columnName": "creationTimeUtc",
"affinity": "INTEGER",
"notNull": true
},
gnarea marked this conversation as resolved.
Show resolved Hide resolved
{
"fieldPath": "expirationTimeUtc",
"columnName": "expirationTimeUtc",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "storagePath",
"columnName": "storagePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
"recipientAddress",
"senderAddress",
"messageId"
],
"autoGenerate": false
},
"indices": [],
"indices": [
{
"name": "index_Parcel_recipientLocation",
"unique": false,
"columnNames": [
"recipientLocation"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Parcel_recipientLocation` ON `${TABLE_NAME}` (`recipientLocation`)"
},
{
"name": "index_Parcel_creationTimeUtc",
"unique": false,
"columnNames": [
"creationTimeUtc"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Parcel_creationTimeUtc` ON `${TABLE_NAME}` (`creationTimeUtc`)"
},
{
"name": "index_Parcel_expirationTimeUtc",
"unique": false,
"columnNames": [
"expirationTimeUtc"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_Parcel_expirationTimeUtc` ON `${TABLE_NAME}` (`expirationTimeUtc`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f4d34def0a467a21b391ecb53e2175d2')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1eb6cc04dc6143149575f7545f798e49')"
]
}
}
16 changes: 15 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,38 @@
xmlns:tools="http://schemas.android.com/tools"
package="tech.relaycorp.gateway">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

<application
android:name=".App"
android:allowBackup="false"
android:extractNativeLibs="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Gateway"
tools:ignore="GoogleAppIndexingWarning">
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<activity
android:name=".ui.main.MainActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Gateway.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.sync.CourierConnectionActivity"
android:label="@string/sync_courier"
android:screenOrientation="portrait" />
<activity
android:name=".ui.sync.CourierSyncActivity"
android:label="@string/sync_courier"
android:screenOrientation="portrait" />
</application>

</manifest>
7 changes: 7 additions & 0 deletions app/src/main/java/tech/relaycorp/gateway/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package tech.relaycorp.gateway
import android.app.Application
import android.os.Build
import android.os.StrictMode
import org.conscrypt.Conscrypt
import tech.relaycorp.gateway.common.Logging
import tech.relaycorp.gateway.common.di.AppComponent
import tech.relaycorp.gateway.common.di.DaggerAppComponent
import java.security.Security
import java.util.logging.Level
import java.util.logging.LogManager

Expand All @@ -29,6 +31,7 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
component.inject(this)
setupTLSProvider()
setupLogger()
setupStrictMode()
}
Expand Down Expand Up @@ -74,5 +77,9 @@ class App : Application() {
}
}

private fun setupTLSProvider() {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}

enum class Mode { Normal, Test }
}
10 changes: 10 additions & 0 deletions app/src/main/java/tech/relaycorp/gateway/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package tech.relaycorp.gateway

import android.content.Context
import android.content.res.Resources
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
import dagger.Module
import dagger.Provides

Expand All @@ -21,4 +23,12 @@ class AppModule(

@Provides
fun resources(): Resources = app.resources

@Provides
fun connectivityManager() =
app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

@Provides
fun wifiManager() =
app.getSystemService(Context.WIFI_SERVICE) as WifiManager
}
5 changes: 5 additions & 0 deletions app/src/main/java/tech/relaycorp/gateway/background/CogRPC.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package tech.relaycorp.gateway.background

object CogRPC {
const val PORT = 21473
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up: I'll move this to the core lib: relaycorp/awala-jvm#63

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package tech.relaycorp.gateway.background

import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import tech.relaycorp.gateway.common.interval
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.seconds

@Singleton
class CourierConnectionObserver
@Inject constructor(
connectivityManager: ConnectivityManager,
private val wifiManager: WifiManager,
private val pingRemoteServer: PingRemoteServer
) {

private val state =
MutableStateFlow<CourierConnectionState>(CourierConnectionState.Disconnected)

private val networkRequest =
NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()

private val hotspotSourceIpAddress
get() =
wifiManager.dhcpInfo?.serverAddress?.toIpAddressString()

init {
val networkCallback = NetworkCallback()

networkCallback
.isWifiConnected
.flatMapLatest { isWifiConnected ->
if (isWifiConnected) {
interval(SERVER_PING_INTERVAL)
.map { checkConnectedState() }
} else {
flowOf(CourierConnectionState.Disconnected)
}
}
.onEach { state.value = it }
.launchIn(CoroutineScope(Dispatchers.IO))

connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
}

fun observe(): Flow<CourierConnectionState> = state

private suspend fun checkConnectedState(): CourierConnectionState {
val serverAddress = hotspotSourceIpAddress
?: return CourierConnectionState.ConnectedWithUnknown
return if (pingCourierServer(serverAddress)) {
CourierConnectionState.ConnectedWithCourier(serverAddress.toFullServerAddress())
} else {
CourierConnectionState.ConnectedWithUnknown
}
}

private suspend fun pingCourierServer(serverAddress: String) =
pingRemoteServer.ping(serverAddress, CogRPC.PORT)

private fun Int.toIpAddressString() =
String.format(
"%d.%d.%d.%d",
this and 0xff,
this shr 8 and 0xff,
this shr 16 and 0xff,
this shr 24 and 0xff
)

private fun String.toFullServerAddress() = "https://$this:${CogRPC.PORT}"

private class NetworkCallback : ConnectivityManager.NetworkCallback() {
val isWifiConnected = MutableStateFlow(false)

override fun onAvailable(network: Network) {
isWifiConnected.value = true
}

override fun onUnavailable() {
isWifiConnected.value = false
}

override fun onLost(network: Network) {
isWifiConnected.value = false
}
}

companion object {
private val SERVER_PING_INTERVAL = 5.seconds
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package tech.relaycorp.gateway.background

sealed class CourierConnectionState {
data class ConnectedWithCourier(val address: String) : CourierConnectionState()
object ConnectedWithUnknown : CourierConnectionState()
object Disconnected : CourierConnectionState()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package tech.relaycorp.gateway.background

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import tech.relaycorp.gateway.common.Logging.logger
import java.net.ConnectException
import java.net.InetSocketAddress
import java.net.Socket
import java.util.logging.Level
import javax.inject.Inject

class PingRemoteServer
@Inject constructor() {
suspend fun ping(hostname: String, port: Int) =
withContext(Dispatchers.IO) {
val socket = Socket()
try {
socket.connect(InetSocketAddress(hostname, port), 2000)
true
} catch (ce: ConnectException) {
logger.log(Level.INFO, "Could not reach $hostname:$port")
false
} catch (ex: Exception) {
logger.log(Level.INFO, "Could not reach $hostname:$port", ex)
false
} finally {
socket.close()
}
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/tech/relaycorp/gateway/common/FlowUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package tech.relaycorp.gateway.common

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlin.time.Duration

fun interval(duration: Duration) = flow {
while (true) {
emit(Unit)
delay(duration)
}
}
Loading