From 912874fd0c962b92e27d3bafed744dbc0d9bc362 Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Tue, 7 Jul 2026 16:55:27 +0200 Subject: [PATCH] feat(platform): add device info and network summary support This adds DeviceInfo to the Platform interface and implements it for Android, iOS, and JVM, including battery level and active network summary where available. Co-authored-by: Junie --- .../kotlin/com/vnidrop/app/MainActivity.kt | 1 + .../com/vnidrop/app/Platform.android.kt | 55 +++++++++++++++++++ .../kotlin/com/vnidrop/app/Platform.kt | 9 +++ .../kotlin/com/vnidrop/app/Platform.ios.kt | 20 ++++++- .../kotlin/com/vnidrop/app/Platform.jvm.kt | 26 +++++++++ 5 files changed, 109 insertions(+), 2 deletions(-) diff --git a/androidApp/src/main/kotlin/com/vnidrop/app/MainActivity.kt b/androidApp/src/main/kotlin/com/vnidrop/app/MainActivity.kt index 6fe4dbf..c1bef0a 100644 --- a/androidApp/src/main/kotlin/com/vnidrop/app/MainActivity.kt +++ b/androidApp/src/main/kotlin/com/vnidrop/app/MainActivity.kt @@ -13,6 +13,7 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() super.onCreate(savedInstanceState) attachAndroidFilePickerContext(this) + attachAndroidPlatformContext(this) setContent { App() diff --git a/shared/src/androidMain/kotlin/com/vnidrop/app/Platform.android.kt b/shared/src/androidMain/kotlin/com/vnidrop/app/Platform.android.kt index 3c414bf..34faa26 100644 --- a/shared/src/androidMain/kotlin/com/vnidrop/app/Platform.android.kt +++ b/shared/src/androidMain/kotlin/com/vnidrop/app/Platform.android.kt @@ -1,6 +1,11 @@ package com.vnidrop.app +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.BatteryManager import android.os.Build +import java.net.NetworkInterface class AndroidPlatform : Platform { override val name: String = "Android ${Build.VERSION.SDK_INT}" @@ -8,6 +13,56 @@ class AndroidPlatform : Platform { System.getProperty("java.io.tmpdir") ?: "/data/local/tmp/vnidrop" override val defaultReceiveDir: String = System.getProperty("java.io.tmpdir") ?: "/data/local/tmp/vnidrop-receive" + override val deviceInfo: DeviceInfo = DeviceInfo( + deviceName = Build.DEVICE, + deviceModel = listOf(Build.MANUFACTURER, Build.MODEL) + .filter { it.isNotBlank() } + .joinToString(" ") + .ifBlank { null }, + operatingSystem = "Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})", + network = activeNetworkSummary(), + batteryLevel = batteryLevel(), + ) } actual fun getPlatform(): Platform = AndroidPlatform() + +fun attachAndroidPlatformContext(context: Context) { + AndroidPlatformContextHolder.context = context.applicationContext +} + +private object AndroidPlatformContextHolder { + var context: Context? = null +} + +private fun activeNetworkSummary(): String? = + runCatching { + val context = AndroidPlatformContextHolder.context ?: return@runCatching networkInterfaceName() + val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager + ?: return@runCatching networkInterfaceName() + val activeNetwork = manager.activeNetwork ?: return@runCatching networkInterfaceName() + val capabilities = manager.getNetworkCapabilities(activeNetwork) ?: return@runCatching networkInterfaceName() + + when { + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "Wi-Fi" + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "Mobile" + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "Ethernet" + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> "VPN" + else -> networkInterfaceName() + } + }.getOrNull() + +private fun batteryLevel(): String? = + runCatching { + val context = AndroidPlatformContextHolder.context ?: return@runCatching null + val manager = context.getSystemService(BatteryManager::class.java) + val level = manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) + level.takeIf { it >= 0 }?.let { "$it%" } + }.getOrNull() + +private fun networkInterfaceName(): String? = + NetworkInterface.getNetworkInterfaces() + .asSequence() + .filter { it.isUp && !it.isLoopback } + .map { it.displayName } + .firstOrNull() diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/Platform.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/Platform.kt index 963133a..cee02ae 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/Platform.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/Platform.kt @@ -4,6 +4,15 @@ interface Platform { val name: String val defaultCoreDataDir: String val defaultReceiveDir: String + val deviceInfo: DeviceInfo } +data class DeviceInfo( + val deviceName: String?, + val deviceModel: String?, + val operatingSystem: String, + val network: String?, + val batteryLevel: String?, +) + expect fun getPlatform(): Platform diff --git a/shared/src/iosMain/kotlin/com/vnidrop/app/Platform.ios.kt b/shared/src/iosMain/kotlin/com/vnidrop/app/Platform.ios.kt index c84b5b4..7934fb8 100644 --- a/shared/src/iosMain/kotlin/com/vnidrop/app/Platform.ios.kt +++ b/shared/src/iosMain/kotlin/com/vnidrop/app/Platform.ios.kt @@ -1,12 +1,28 @@ package com.vnidrop.app -import platform.UIKit.UIDevice import platform.Foundation.NSTemporaryDirectory +import platform.UIKit.UIDevice class IOSPlatform : Platform { - override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion + private val device: UIDevice = UIDevice.currentDevice + + override val name: String = device.systemName() + " " + device.systemVersion override val defaultCoreDataDir: String = NSTemporaryDirectory() + "vnidrop" override val defaultReceiveDir: String = NSTemporaryDirectory() + "vnidrop-receive" + override val deviceInfo: DeviceInfo = DeviceInfo( + deviceName = device.name, + deviceModel = device.model, + operatingSystem = device.systemName() + " " + device.systemVersion, + network = null, + batteryLevel = batteryLevel(device), + ) } actual fun getPlatform(): Platform = IOSPlatform() + +private fun batteryLevel(device: UIDevice): String? = + runCatching { + device.batteryMonitoringEnabled = true + val level = device.batteryLevel + if (level >= 0.0) "${(level * 100).toInt()}%" else null + }.getOrNull() diff --git a/shared/src/jvmMain/kotlin/com/vnidrop/app/Platform.jvm.kt b/shared/src/jvmMain/kotlin/com/vnidrop/app/Platform.jvm.kt index b98cfc1..085fa32 100644 --- a/shared/src/jvmMain/kotlin/com/vnidrop/app/Platform.jvm.kt +++ b/shared/src/jvmMain/kotlin/com/vnidrop/app/Platform.jvm.kt @@ -1,11 +1,37 @@ package com.vnidrop.app +import java.net.NetworkInterface + class JVMPlatform : Platform { override val name: String = "Java ${System.getProperty("java.version")}" override val defaultCoreDataDir: String = System.getProperty("user.home") + "/.vnidrop" override val defaultReceiveDir: String = System.getProperty("user.home") + "/Downloads" + override val deviceInfo: DeviceInfo = DeviceInfo( + deviceName = System.getenv("COMPUTERNAME") + ?: System.getenv("HOSTNAME") + ?: System.getProperty("user.name"), + deviceModel = listOfNotNull( + System.getProperty("os.arch"), + System.getProperty("java.vm.name"), + ).joinToString(" | ").ifBlank { null }, + operatingSystem = listOfNotNull( + System.getProperty("os.name"), + System.getProperty("os.version"), + ).joinToString(" ").ifBlank { name }, + network = activeNetworkSummary(), + batteryLevel = null, + ) } actual fun getPlatform(): Platform = JVMPlatform() + +private fun activeNetworkSummary(): String? = + runCatching { + NetworkInterface.getNetworkInterfaces() + .asSequence() + .filter { it.isUp && !it.isLoopback } + .map { it.displayName } + .firstOrNull() + }.getOrNull()