Skip to content
Open
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
109 changes: 76 additions & 33 deletions app/src/main/java/com/sameerasw/airsync/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,20 @@ object AdbDiscoveryHolder {
}
}

private data class ConnectionLaunchPayload(
val ip: String? = null,
val port: String? = null,
val pcName: String? = null,
val isPlus: Boolean = false,
val symmetricKey: String? = null,
val showConnectionDialog: Boolean = false,
val requestNonce: Long = 0L
)

class MainActivity : ComponentActivity() {
// Flag to keep splash screen visible during app initialization
private var isAppReady = false
private var launchPayload by mutableStateOf(ConnectionLaunchPayload())

// Permission launcher for Android 13+ notification permission
private val notificationPermissionLauncher = registerForActivityResult(
Expand Down Expand Up @@ -340,33 +351,7 @@ class MainActivity : ComponentActivity() {
}
}

val data: android.net.Uri? = intent?.data
val ip = data?.host
val port = data?.port?.takeIf { it != -1 }?.toString()

// Parse QR code parameters
var pcName: String? = null
var isPlus = false
var symmetricKey: String? = null

data?.let { uri ->
val urlString = uri.toString()
val queryPart = urlString.substringAfter('?', "")
if (queryPart.isNotEmpty()) {
val params = queryPart.split('?')
val paramMap = params.associate {
val parts = it.split('=', limit = 2)
val key = parts.getOrNull(0) ?: ""
val value = parts.getOrNull(1) ?: ""
key to value
}
pcName = paramMap["name"]?.let { URLDecoder.decode(it, "UTF-8") }
isPlus = paramMap["plus"]?.toBooleanStrictOrNull() ?: false
symmetricKey = paramMap["key"]
}
}

val isFromQrScan = data != null
launchPayload = parseConnectionLaunch(intent)

setContent {
val viewModel: com.sameerasw.airsync.presentation.viewmodel.AirSyncViewModel =
Expand Down Expand Up @@ -396,12 +381,13 @@ class MainActivity : ComponentActivity() {
) {
composable("main") {
AirSyncMainScreen(
initialIp = ip,
initialPort = port,
showConnectionDialog = isFromQrScan,
pcName = pcName,
isPlus = isPlus,
symmetricKey = symmetricKey
initialIp = launchPayload.ip,
initialPort = launchPayload.port,
showConnectionDialog = launchPayload.showConnectionDialog,
pcName = launchPayload.pcName,
isPlus = launchPayload.isPlus,
symmetricKey = launchPayload.symmetricKey,
requestNonce = launchPayload.requestNonce
)
}
}
Expand Down Expand Up @@ -548,6 +534,7 @@ class MainActivity : ComponentActivity() {

override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)

// Handle Notes Role intent
handleNotesRoleIntent(intent)
Expand All @@ -562,8 +549,14 @@ class MainActivity : ComponentActivity() {
}
startActivity(qrScannerIntent)
finish()
return
}
}

val updatedLaunchPayload = parseConnectionLaunch(intent)
if (updatedLaunchPayload.showConnectionDialog) {
launchPayload = updatedLaunchPayload
}
}

/**
Expand Down Expand Up @@ -647,4 +640,54 @@ class MainActivity : ComponentActivity() {

ContentCaptureManager.launchContentCapture(contentCaptureLauncher)
}

private fun parseConnectionLaunch(intent: Intent?): ConnectionLaunchPayload {
val uri = intent?.data ?: return ConnectionLaunchPayload()
if (uri.scheme != "airsync") return ConnectionLaunchPayload()

var ip = uri.host ?: ""
var port = uri.port.takeIf { it != -1 }?.toString() ?: ""

if (ip.isEmpty() || port.isEmpty()) {
val authority = uri.toString().substringAfter("://").substringBefore("?")
if (authority.contains(":")) {
ip = authority.substringBeforeLast(":")
port = authority.substringAfterLast(":")
} else {
ip = authority
}
}

if (ip.isEmpty() || port.isEmpty()) {
return ConnectionLaunchPayload()
}

var pcName: String? = null
var isPlus = false
var symmetricKey: String? = null

val queryPart = uri.toString().substringAfter('?', "")
if (queryPart.isNotEmpty()) {
val params = queryPart.split('?')
val paramMap = params.associate {
val parts = it.split('=', limit = 2)
val key = parts.getOrNull(0) ?: ""
val value = parts.getOrNull(1) ?: ""
key to value
}
pcName = paramMap["name"]?.let { URLDecoder.decode(it, "UTF-8") }
isPlus = paramMap["plus"]?.toBooleanStrictOrNull() ?: false
symmetricKey = paramMap["key"]
}

return ConnectionLaunchPayload(
ip = ip,
port = port,
pcName = pcName,
isPlus = isPlus,
symmetricKey = symmetricKey,
showConnectionDialog = true,
requestNonce = System.currentTimeMillis()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.sameerasw.airsync.domain.model.NotificationApp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import org.json.JSONArray
import org.json.JSONObject

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "airsync_settings")
Expand Down Expand Up @@ -604,45 +605,61 @@ class DataStoreManager(private val context: Context) {
}
}

private fun parseNetworkConnections(jsonString: String): Map<String, String> {
return try {
val json = JSONObject(jsonString)
buildMap {
json.keys().forEach { key ->
put(key, json.getString(key))
}
}
} catch (_: Exception) {
emptyMap()
}
}

private fun parseStringList(jsonString: String): List<String> {
return try {
val json = JSONArray(jsonString)
List(json.length()) { index -> json.getString(index) }
} catch (_: Exception) {
emptyList()
}
}

private fun stringifyStringList(values: Iterable<String>): String {
return JSONArray(NetworkDeviceConnection.rankIps(values)).toString()
}

// Network-aware device connections
suspend fun saveNetworkDeviceConnection(
deviceName: String,
ourIp: String,
clientIp: String,
candidateIps: List<String> = emptyList(),
port: String,
isPlus: Boolean,
symmetricKey: String?,
model: String? = null,
deviceType: String? = null
) {
context.dataStore.edit { preferences ->
// Load existing connections for this device
val existingConnectionsJson =
preferences[stringPreferencesKey("${NETWORK_CONNECTIONS_PREFIX}${deviceName}")]
?: "{}"
val existingConnections = try {
val json = JSONObject(existingConnectionsJson)
val map = mutableMapOf<String, String>()
json.keys().forEach { key ->
map[key] = json.getString(key)
}
map
} catch (_: Exception) {
mutableMapOf()
}
val existingConnections = parseNetworkConnections(existingConnectionsJson).toMutableMap()

// Add/update the new connection
existingConnections[ourIp] = clientIp

// Convert back to JSON
val updatedJson = JSONObject(existingConnections).toString()

// Save device info
preferences[stringPreferencesKey("${NETWORK_DEVICES_PREFIX}${deviceName}_name")] =
deviceName
preferences[stringPreferencesKey("${NETWORK_DEVICES_PREFIX}${deviceName}_port")] = port
preferences[booleanPreferencesKey("${NETWORK_DEVICES_PREFIX}${deviceName}_plus")] =
isPlus
preferences[stringPreferencesKey("${NETWORK_DEVICES_PREFIX}${deviceName}_candidate_ips")] =
stringifyStringList(existingConnections.values + candidateIps + listOf(clientIp))
symmetricKey?.let {
preferences[stringPreferencesKey("${NETWORK_DEVICES_PREFIX}${deviceName}_symmetric_key")] =
it
Expand Down Expand Up @@ -673,6 +690,9 @@ class DataStoreManager(private val context: Context) {
?: false
val symmetricKey =
preferences[stringPreferencesKey("${NETWORK_DEVICES_PREFIX}${deviceName}_symmetric_key")]
val candidateIpsJson =
preferences[stringPreferencesKey("${NETWORK_DEVICES_PREFIX}${deviceName}_candidate_ips")]
?: "[]"
val model =
preferences[stringPreferencesKey("${NETWORK_DEVICES_PREFIX}${deviceName}_model")]
val deviceType =
Expand All @@ -685,20 +705,10 @@ class DataStoreManager(private val context: Context) {
?: "{}"

if (name != null && port != null) {
val connections = try {
val json = JSONObject(connectionsJson)
val map = mutableMapOf<String, String>()
json.keys().forEach { key ->
map[key] = json.getString(key)
}
map
} catch (_: Exception) {
emptyMap()
}

NetworkDeviceConnection(
deviceName = name,
networkConnections = connections,
networkConnections = parseNetworkConnections(connectionsJson),
candidateIps = parseStringList(candidateIpsJson),
port = port,
lastConnected = lastConnected,
isPlus = isPlus,
Expand Down Expand Up @@ -737,6 +747,9 @@ class DataStoreManager(private val context: Context) {
?: false
val symmetricKey =
preferences[stringPreferencesKey("${NETWORK_DEVICES_PREFIX}${deviceName}_symmetric_key")]
val candidateIpsJson =
preferences[stringPreferencesKey("${NETWORK_DEVICES_PREFIX}${deviceName}_candidate_ips")]
?: "[]"
val model =
preferences[stringPreferencesKey("${NETWORK_DEVICES_PREFIX}${deviceName}_model")]
val deviceType =
Expand All @@ -749,21 +762,11 @@ class DataStoreManager(private val context: Context) {
?: "{}"

if (name != null && port != null) {
val connections = try {
val json = JSONObject(connectionsJson)
val map = mutableMapOf<String, String>()
json.keys().forEach { key ->
map[key] = json.getString(key)
}
map
} catch (_: Exception) {
emptyMap()
}

devices.add(
NetworkDeviceConnection(
deviceName = name,
networkConnections = connections,
networkConnections = parseNetworkConnections(connectionsJson),
candidateIps = parseStringList(candidateIpsJson),
port = port,
lastConnected = lastConnected,
isPlus = isPlus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class AirSyncRepositoryImpl(
deviceName: String,
ourIp: String,
clientIp: String,
candidateIps: List<String>,
port: String,
isPlus: Boolean,
symmetricKey: String?,
Expand All @@ -90,6 +91,7 @@ class AirSyncRepositoryImpl(
deviceName,
ourIp,
clientIp,
candidateIps,
port,
isPlus,
symmetricKey,
Expand Down Expand Up @@ -295,4 +297,4 @@ class AirSyncRepositoryImpl(
override fun isQuickShareEnabled(): Flow<Boolean> {
return dataStoreManager.isQuickShareEnabled()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.sameerasw.airsync.domain.model

enum class ConnectionTransport {
LOCAL,
EXTENDED,
UNKNOWN;

companion object {
fun fromIp(ipAddress: String?): ConnectionTransport {
val ip = ipAddress?.trim().orEmpty()
if (ip.isEmpty()) return UNKNOWN

if (ip.startsWith("192.168.") || ip.startsWith("10.") || ip.startsWith("127.") || ip.startsWith("169.254.")) {
return LOCAL
}

if (ip.startsWith("172.")) {
val parts = ip.split(".")
val secondOctet = parts.getOrNull(1)?.toIntOrNull()
if (secondOctet != null && secondOctet in 16..31) {
return LOCAL
}
}

if (ip.startsWith("100.")) {
return EXTENDED
}

return EXTENDED
}
}
}
Loading