diff --git a/app/build.gradle b/app/build.gradle index 854ae6a..f21a625 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' + } + } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e5231bb..23eb924 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,10 @@ + + + + @@ -67,4 +72,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/ConfigurationProxy.kt b/app/src/main/java/eu/neilalexander/yggdrasil/ConfigurationProxy.kt index e5dd0ba..514bd49 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/ConfigurationProxy.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/ConfigurationProxy.kt @@ -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() @@ -94,4 +95,4 @@ object ConfigurationProxy { (json.getJSONArray("MulticastInterfaces").get(0) as JSONObject).put("Beacon", value) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt b/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt index b2f02b4..ffc4cec 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/GlobalApplication.kt @@ -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) @@ -119,4 +121,4 @@ private fun createNotificationChannels(context: Context) { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt index af70595..d2ceca3 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/MainActivity.kt @@ -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) diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt b/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt index 36d3b56..9cbe14b 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/PacketTunnelProvider.kt @@ -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? { + 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) + } + + } diff --git a/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt b/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt index a3f4e08..8e5ff83 100644 --- a/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt +++ b/app/src/main/java/eu/neilalexander/yggdrasil/PeersActivity.kt @@ -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,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(R.id.enableMulticastBeaconPanel) multicastBeaconPanel.setOnClickListener { multicastBeaconSwitch.toggle() @@ -63,6 +87,19 @@ class PeersActivity : AppCompatActivity() { multicastListenSwitch.toggle() } + val enableBLEPanel = findViewById(R.id.enableBLEPanel) + val enableCodedPHYPanel = findViewById(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) @@ -181,4 +218,4 @@ class PeersActivity : AppCompatActivity() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/activity_peers.xml b/app/src/main/res/layout/activity_peers.xml index 88724c2..d6e144c 100644 --- a/app/src/main/res/layout/activity_peers.xml +++ b/app/src/main/res/layout/activity_peers.xml @@ -217,6 +217,50 @@ + + + + + + + + + + + + + + + + + + + + Подключения пиров Находимый через multicast Искать пиров через multicast + Искать пиров через Bluetooth LE + Использовать BLE Coded PHY + Для поиска Bluetooth пиров, разрешите Nearby Devices и Location + Для поиска Bluetooth пиров, разрешите Nearby Devices и Location в настройках Yggdrasil будет пытаться подключаться к этим пирам автоматически. Если вы добавите несколько пиров, ваше устройство может быть использовано для переноса данных между другими узлами сети. Чтобы этого избежать настройте только один пир. - Пиры могут быть найдены с помощью Multicast если они находятся в той же Wi-Fi сети, либо через USB. Трафик в мобильной сети может быть платным. Вы можете отключить мобильные данные в настройках устройства. + Пиры могут быть найдены с помощью Multicast если они находятся в той же Wi-Fi сети, либо через USB или BLE (Android 12+). Трафик в мобильной сети может быть платным. Вы можете отключить мобильные данные в настройках устройства. Об узле Название устройства Нажмите для изменения @@ -83,4 +87,4 @@ Приватный ключ: Установить свои ключи Сохранить - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 96e6545..22ca57e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,8 +57,12 @@ Peer Connectivity Discoverable over multicast Search for multicast peers + Search for peers over Bluetooth LE + Use BLE Coded PHY + Bluetooth peering requires Nearby Devices and Location permissions + To use Bluetooth peering, enable Nearby Devices and Location permissions manually 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. - 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. + 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. Node Info Device Name Tap to edit @@ -83,4 +87,4 @@ Private key: Set your own keys Save - \ No newline at end of file + diff --git a/settings.gradle b/settings.gradle index 8413a98..7726caa 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,8 @@ rootProject.name = "Yggdrasil" include ':app' + +sourceControl { + gitRepository("https://codeberg.org/aakselrod/blemesh-android.git") { + producesModule("org.akselrod.blemesh:lib") + } +}