add support for peer discovery over BLE

This commit is contained in:
Alex Akselrod 2023-09-21 12:34:48 -07:00
parent 9df80c0612
commit 39eba06f0c
No known key found for this signature in database
GPG key ID: 57D7612D178AA487
11 changed files with 196 additions and 12 deletions

View file

@ -4,13 +4,13 @@ plugins {
}
android {
compileSdkVersion 29
compileSdkVersion 31
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "eu.neilalexander.yggdrasil"
minSdkVersion 21
targetSdkVersion 29
targetSdkVersion 31
versionCode 13
versionName "0.1-013"
@ -37,6 +37,7 @@ android {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig = signingConfigs.getByName("yggdrasil")
matchingFallbacks = ['release']
}
}
compileOptions {
@ -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.6.4'
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'
}
}
}

View file

@ -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"
@ -41,6 +45,7 @@
<service
android:name=".PacketTunnelProvider"
android:foregroundServiceType="location"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="true">
<intent-filter>

View file

@ -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()

View file

@ -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 {
@ -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)

View file

@ -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
@ -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
@ -43,6 +46,46 @@ 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),
getString(R.string.ok),
getString(R.string.cancel),
)
}
.onForwardToSettings { scope, deniedList ->
scope.showForwardToSettingsDialog(
deniedList,
getString(R.string.manual_perms),
getString(R.string.ok),
getString(R.string.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)
@ -65,6 +108,7 @@ class MainActivity : AppCompatActivity() {
enabledSwitch.setOnCheckedChangeListener { _, isChecked ->
when (isChecked) {
true -> {
checkBLEPermissions()
val vpnIntent = VpnService.prepare(this)
if (vpnIntent != null) {
startVpnActivity.launch(vpnIntent)

View file

@ -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
@ -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)
@ -175,6 +181,13 @@ 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() {
@ -182,6 +195,9 @@ open class PacketTunnelProvider: VpnService() {
return
}
bleService?.stop()
bleService = null
yggdrasil.stop()
readerStream?.let {
@ -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)
}
}

View file

@ -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
@ -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?) {
@ -51,8 +55,28 @@ 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 {
@ -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)

View file

@ -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

View file

@ -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>

View file

@ -57,8 +57,12 @@
<string name="peer_connectivity_title">Peer Connectivity</string>
<string name="discoverable_over_multicast">Discoverable over multicast</string>
<string name="search_for_multicast_peers">Search for multicast peers</string>
<string name="enable_ble">Search for peers over Bluetooth LE</string>
<string name="enable_coded_phy">Use BLE Coded PHY</string>
<string name="explain_perms">Bluetooth peering requires Nearby Devices and Location permissions</string>
<string name="manual_perms">To use Bluetooth peering, enable Nearby Devices and Location permissions manually</string>
<string name="configured_peers_hint">Yggdrasil will automatically attempt to connect to configured peers when started. If you configure more than one peer, your device may carry traffic on behalf of other network nodes. Avoid this by configuring only a single peer.</string>
<string name="peer_connectivity_hint">Multicast peers will be discovered on the same Wi-Fi network or via USB. Data charges may apply when using mobile data. You can prevent data usage in the device settings.</string>
<string name="peer_connectivity_hint">Multicast peers will be discovered on the same Wi-Fi network or via USB or BLE (Android 12+). Data charges may apply when using mobile data. You can prevent data usage in the device settings.</string>
<string name="node_info">Node Info</string>
<string name="device_name">Device Name</string>
<string name="tap_to_edit">Tap to edit</string>

View file

@ -1,2 +1,8 @@
rootProject.name = "Yggdrasil"
include ':app'
sourceControl {
gitRepository("https://codeberg.org/aakselrod/blemesh-android.git") {
producesModule("org.akselrod.blemesh:lib")
}
}