Skip to content

Commit

Permalink
add support for peer discovery over BLE
Browse files Browse the repository at this point in the history
  • Loading branch information
aakselrod committed Oct 10, 2023
1 parent 9df80c0 commit c457021
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 12 deletions.
12 changes: 10 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ plugins {
}

android {
compileSdkVersion 29
compileSdkVersion 33
buildToolsVersion "30.0.3"

defaultConfig {
applicationId "eu.neilalexander.yggdrasil"
minSdkVersion 21
targetSdkVersion 29
targetSdkVersion 33
versionCode 13
versionName "0.1-013"

Expand All @@ -37,6 +37,7 @@ android {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig = signingConfigs.getByName("yggdrasil")
matchingFallbacks = ['release']
}
}
compileOptions {
Expand All @@ -51,12 +52,19 @@ android {
dependencies {
implementation fileTree(include: ['*.aar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.preference:preference-ktx:1.1.0'
implementation 'com.guolindev.permissionx:permissionx:1.7.1'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation('org.akselrod.blemesh:lib:0.0.1') {
version {
branch = 'main'
}
}
}
7 changes: 6 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

<application
android:name=".GlobalApplication"
Expand Down Expand Up @@ -41,6 +45,7 @@

<service
android:name=".PacketTunnelProvider"
android:foregroundServiceType="location"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="true">
<intent-filter>
Expand All @@ -67,4 +72,4 @@
</receiver>
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ object ConfigurationProxy {
json.put("AdminListen", "none")
json.put("IfName", "none")
json.put("IfMTU", 65535)
json.put("Listen", JSONArray(arrayOf("tcp://127.0.0.1:9004")))

if (json.getJSONArray("MulticastInterfaces").get(0) is String) {
var ar = JSONArray()
Expand Down Expand Up @@ -94,4 +95,4 @@ object ConfigurationProxy {
(json.getJSONArray("MulticastInterfaces").get(0) as JSONObject).put("Beacon", value)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat

const val PREF_KEY_ENABLED = "enabled"
const val BLE_ENABLED = "ble"
const val CODED_PHY_ENABLED = "codedPhy"
const val MAIN_CHANNEL_ID = "Yggdrasil Service"

class GlobalApplication: Application(), YggStateReceiver.StateReceiver {
Expand Down Expand Up @@ -68,7 +70,7 @@ fun createServiceNotification(context: Context, state: State): Notification {
val intent = Intent(context, MainActivity::class.java).apply {
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)

val text = when (state) {
State.Disabled -> context.getText(R.string.tile_disabled)
Expand Down Expand Up @@ -119,4 +121,4 @@ private fun createNotificationChannels(context: Context) {
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
}
34 changes: 34 additions & 0 deletions app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package eu.neilalexander.yggdrasil

import android.Manifest
import android.app.Activity
import android.content.*
import android.graphics.Color
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.widget.Switch
import android.widget.TextView
Expand All @@ -14,6 +16,7 @@ import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.edit
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import com.permissionx.guolindev.PermissionX
import eu.neilalexander.yggdrasil.PacketTunnelProvider.Companion.STATE_INTENT
import mobile.Mobile
import org.json.JSONArray
Expand Down Expand Up @@ -43,6 +46,36 @@ class MainActivity : AppCompatActivity() {
}
}

private fun checkBLEPermissions() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)
val bleEnabled = preferences.getBoolean(BLE_ENABLED, (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S))
if (!bleEnabled) {
return
}

PermissionX.init(this)
.permissions(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.BLUETOOTH_ADVERTISE,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN,
)
.onExplainRequestReason { scope, deniedList ->
scope.showRequestReasonDialog(deniedList, getString(R.string.explain_perms), "OK", "Cancel")
}
.onForwardToSettings { scope, deniedList ->
scope.showForwardToSettingsDialog(deniedList, getString(R.string.manual_perms), "OK", "Cancel")
}
.request { allGranted, _, _ ->
if(!allGranted) {
preferences.edit().apply {
putBoolean(BLE_ENABLED, false)
commit()
}
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Expand All @@ -65,6 +98,7 @@ class MainActivity : AppCompatActivity() {
enabledSwitch.setOnCheckedChangeListener { _, isChecked ->
when (isChecked) {
true -> {
checkBLEPermissions()
val vpnIntent = VpnService.prepare(this)
if (vpnIntent != null) {
startVpnActivity.launch(vpnIntent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import android.util.Log
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import eu.neilalexander.yggdrasil.YggStateReceiver.Companion.YGG_STATE_INTENT
import org.akselrod.blemesh.BLEService
import mobile.Yggdrasil
import org.json.JSONArray
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.net.Socket
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread

Expand Down Expand Up @@ -43,6 +47,8 @@ open class PacketTunnelProvider: VpnService() {
private var readerStream: FileInputStream? = null
private var writerStream: FileOutputStream? = null

private var bleService: BLEService? = null

override fun onCreate() {
super.onCreate()
config = ConfigurationProxy(applicationContext)
Expand Down Expand Up @@ -175,13 +181,23 @@ open class PacketTunnelProvider: VpnService() {
intent = Intent(YGG_STATE_INTENT)
intent.putExtra("state", STATE_ENABLED)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)

if (preferences.getBoolean(BLE_ENABLED, (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S))) {
val publicKey = config.getJSON().getString("PublicKey")
val codedPhy = preferences.getBoolean(CODED_PHY_ENABLED, false)
bleService = BLEService(this.baseContext, publicKey, codedPhy, ::peerConnect)
bleService?.start()
}
}

private fun stop() {
if (!started.compareAndSet(true, false)) {
return
}

bleService?.stop()
bleService = null

yggdrasil.stop()

readerStream?.let {
Expand Down Expand Up @@ -336,4 +352,17 @@ open class PacketTunnelProvider: VpnService() {
readerStream = null
}
}

private fun peerConnect(): Pair<InputStream, OutputStream>? {
var socket: Socket?
try {
socket = Socket("127.0.0.1", 9004)
} catch (e: Exception) {
Log.e(TAG, "Couldn't open peer socket: $e")
return null
}
return Pair(socket.inputStream, socket.outputStream)
}


}
41 changes: 39 additions & 2 deletions app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import android.content.Intent
import android.content.IntentFilter
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Build
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.widget.*
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import com.google.android.material.textfield.TextInputEditText
import org.json.JSONArray
import org.json.JSONObject
Expand All @@ -27,6 +29,8 @@ class PeersActivity : AppCompatActivity() {
private lateinit var configuredTableLabel: TextView
private lateinit var multicastListenSwitch: Switch
private lateinit var multicastBeaconSwitch: Switch
private lateinit var enableBLESwitch: Switch
private lateinit var enableCodedPHYSwitch: Switch
private lateinit var addPeerButton: ImageButton

override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -51,9 +55,29 @@ class PeersActivity : AppCompatActivity() {
multicastBeaconSwitch.setOnCheckedChangeListener { button, _ ->
config.multicastBeacon = button.isChecked
}

val preferences = PreferenceManager.getDefaultSharedPreferences(this.baseContext)

enableBLESwitch = findViewById(R.id.enableBLE)
enableBLESwitch.setOnCheckedChangeListener { button, _ ->
preferences.edit().apply {
putBoolean(BLE_ENABLED, button.isChecked)
commit()
}
}
enableCodedPHYSwitch = findViewById(R.id.enableCodedPHY)
enableCodedPHYSwitch.setOnCheckedChangeListener { button, _ ->
preferences.edit().apply {
putBoolean(CODED_PHY_ENABLED, button.isChecked)
commit()
}
}

multicastListenSwitch.isChecked = config.multicastListen
multicastBeaconSwitch.isChecked = config.multicastBeacon

enableBLESwitch.isChecked = preferences.getBoolean(BLE_ENABLED, (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S))
enableCodedPHYSwitch.isChecked = preferences.getBoolean(CODED_PHY_ENABLED, false)

val multicastBeaconPanel = findViewById<TableRow>(R.id.enableMulticastBeaconPanel)
multicastBeaconPanel.setOnClickListener {
multicastBeaconSwitch.toggle()
Expand All @@ -63,6 +87,19 @@ class PeersActivity : AppCompatActivity() {
multicastListenSwitch.toggle()
}

val enableBLEPanel = findViewById<TableRow>(R.id.enableBLEPanel)
val enableCodedPHYPanel = findViewById<TableRow>(R.id.enableCodedPHYPanel)

enableBLEPanel.isEnabled = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
enableCodedPHYPanel.isEnabled = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)

enableBLEPanel.setOnClickListener {
enableBLESwitch.toggle()
}
enableCodedPHYPanel.setOnClickListener {
enableCodedPHYSwitch.toggle()
}

addPeerButton = findViewById(R.id.addPeerButton)
addPeerButton.setOnClickListener {
val view = inflater.inflate(R.layout.dialog_addpeer, null)
Expand Down Expand Up @@ -181,4 +218,4 @@ class PeersActivity : AppCompatActivity() {
}
}
}
}
}
44 changes: 44 additions & 0 deletions app/src/main/res/layout/activity_peers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,50 @@

</TableRow>

<TableRow
android:id="@+id/enableBLEPanel"
style="@style/SelectableSwitchItemStyle">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enable_ble"
android:textColor="?attr/textDefault" />

<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="2" />

<Switch
android:id="@+id/enableBLE"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</TableRow>

<TableRow
android:id="@+id/enableCodedPHYPanel"
style="@style/SelectableSwitchItemStyle">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/enable_coded_phy"
android:textColor="?attr/textDefault" />

<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="2" />

<Switch
android:id="@+id/enableCodedPHY"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</TableRow>

</TableLayout>

<TextView
Expand Down
8 changes: 6 additions & 2 deletions app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@
<string name="peer_connectivity_title">Подключения пиров</string>
<string name="discoverable_over_multicast">Находимый через multicast</string>
<string name="search_for_multicast_peers">Искать пиров через multicast</string>
<string name="enable_ble">Искать пиров через Bluetooth LE</string>
<string name="enable_coded_phy">Использовать BLE Coded PHY</string>
<string name="explain_perms">Для поиска Bluetooth пиров, разрешите Nearby Devices и Location</string>
<string name="manual_perms">Для поиска Bluetooth пиров, разрешите Nearby Devices и Location в настройках</string>
<string name="configured_peers_hint">Yggdrasil будет пытаться подключаться к этим пирам автоматически. Если вы добавите несколько пиров, ваше устройство может быть использовано для переноса данных между другими узлами сети. Чтобы этого избежать настройте только один пир.</string>
<string name="peer_connectivity_hint">Пиры могут быть найдены с помощью Multicast если они находятся в той же Wi-Fi сети, либо через USB. Трафик в мобильной сети может быть платным. Вы можете отключить мобильные данные в настройках устройства.</string>
<string name="peer_connectivity_hint">Пиры могут быть найдены с помощью Multicast если они находятся в той же Wi-Fi сети, либо через USB или BLE (Android 12+). Трафик в мобильной сети может быть платным. Вы можете отключить мобильные данные в настройках устройства.</string>
<string name="node_info">Об узле</string>
<string name="device_name">Название устройства</string>
<string name="tap_to_edit">Нажмите для изменения</string>
Expand All @@ -83,4 +87,4 @@
<string name="private_key_label">Приватный ключ:</string>
<string name="set_keys">Установить свои ключи</string>
<string name="save">Сохранить</string>
</resources>
</resources>
Loading

0 comments on commit c457021

Please sign in to comment.