Skip to content

Commit 55768be

Browse files
committed
android: improve connection handling
1 parent cea09b2 commit 55768be

File tree

8 files changed

+115
-48
lines changed

8 files changed

+115
-48
lines changed

android/app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ android {
1515
minSdk = 28
1616
targetSdk = 36
1717
versionCode = 8
18-
versionName = "0.2.0-alpha"
18+
versionName = "0.2.0-beta.1"
1919
}
2020

2121
buildTypes {
@@ -86,4 +86,4 @@ aboutLibraries {
8686
excludeFields = listOf("generated")
8787
outputFile = file("src/main/res/raw/aboutlibraries.json")
8888
}
89-
}
89+
}

android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ package me.kavishdevar.librepods.screens
2020

2121
import android.annotation.SuppressLint
2222
import android.util.Log
23-
import androidx.compose.animation.animateColorAsState
24-
import androidx.compose.animation.core.tween
2523
import androidx.compose.foundation.background
2624
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
2725
import androidx.compose.foundation.gestures.detectTapGestures
@@ -35,17 +33,10 @@ import androidx.compose.foundation.layout.fillMaxSize
3533
import androidx.compose.foundation.layout.fillMaxWidth
3634
import androidx.compose.foundation.layout.height
3735
import androidx.compose.foundation.layout.padding
38-
import androidx.compose.foundation.layout.size
3936
import androidx.compose.foundation.rememberScrollState
40-
import androidx.compose.foundation.shape.CircleShape
4137
import androidx.compose.foundation.shape.RoundedCornerShape
4238
import androidx.compose.foundation.verticalScroll
43-
import androidx.compose.material3.Checkbox
44-
import androidx.compose.material3.CheckboxDefaults
4539
import androidx.compose.material3.ExperimentalMaterial3Api
46-
import androidx.compose.material3.HorizontalDivider
47-
import androidx.compose.material3.Slider
48-
import androidx.compose.material3.SliderDefaults
4940
import androidx.compose.material3.Text
5041
import androidx.compose.runtime.Composable
5142
import androidx.compose.runtime.DisposableEffect
@@ -59,8 +50,6 @@ import androidx.compose.runtime.setValue
5950
import androidx.compose.ui.Alignment
6051
import androidx.compose.ui.Modifier
6152
import androidx.compose.ui.draw.clip
62-
import androidx.compose.ui.draw.scale
63-
import androidx.compose.ui.draw.shadow
6453
import androidx.compose.ui.geometry.Offset
6554
import androidx.compose.ui.graphics.Color
6655
import androidx.compose.ui.input.pointer.pointerInput
@@ -88,7 +77,6 @@ import kotlinx.coroutines.launch
8877
import me.kavishdevar.librepods.R
8978
import me.kavishdevar.librepods.composables.NavigationButton
9079
import me.kavishdevar.librepods.composables.StyledDropdown
91-
import me.kavishdevar.librepods.composables.StyledIconButton
9280
import me.kavishdevar.librepods.composables.StyledScaffold
9381
import me.kavishdevar.librepods.composables.StyledSlider
9482
import me.kavishdevar.librepods.composables.StyledToggle

android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ import androidx.compose.ui.unit.dp
6767
import androidx.compose.ui.unit.sp
6868
import androidx.navigation.NavController
6969
import androidx.navigation.compose.rememberNavController
70-
import com.kyant.backdrop.backdrops.layerBackdrop
7170
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
7271
import com.kyant.backdrop.drawBackdrop
7372
import com.kyant.backdrop.highlight.Highlight
@@ -231,7 +230,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
231230
val instance = service.airpodsInstance
232231
if (instance == null) {
233232
Text("Error: AirPods instance is null")
234-
return@StyledScaffold
233+
return@StyledScaffold
235234
}
236235
val capabilities = instance.model.capabilities
237236
LazyColumn(
@@ -267,7 +266,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
267266
item(key = "spacer_noise") { Spacer(modifier = Modifier.height(16.dp)) }
268267
item(key = "noise_control") { NoiseControlSettings(service = service) }
269268
}
270-
269+
271270
if (capabilities.contains(Capability.STEM_CONFIG)) {
272271
item(key = "spacer_press_hold") { Spacer(modifier = Modifier.height(16.dp)) }
273272
item(key = "press_hold") { PressAndHoldSettings(navController = navController) }
@@ -370,15 +369,36 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
370369
Spacer(Modifier.height(32.dp))
371370
StyledButton(
372371
onClick = { navController.navigate("troubleshooting") },
373-
backdrop = backdrop
372+
backdrop = backdrop,
373+
modifier = Modifier
374+
.fillMaxWidth(0.9f)
374375
) {
375376
Text(
376377
text = "Troubleshoot Connection",
377378
style = TextStyle(
378379
fontSize = 16.sp,
379380
fontWeight = FontWeight.Medium,
380381
fontFamily = FontFamily(Font(R.font.sf_pro)),
381-
color = if (isSystemInDarkTheme()) Color.White else Color.Black
382+
color = if (isSystemInDarkTheme()) Color.White else Color.Black
383+
)
384+
)
385+
}
386+
Spacer(Modifier.height(16.dp))
387+
StyledButton(
388+
onClick = {
389+
service.reconnectFromSavedMac()
390+
},
391+
backdrop = backdrop,
392+
modifier = Modifier
393+
.fillMaxWidth(0.9f)
394+
) {
395+
Text(
396+
text = stringResource(R.string.reconnect_to_last_device),
397+
style = TextStyle(
398+
fontSize = 16.sp,
399+
fontWeight = FontWeight.Medium,
400+
fontFamily = FontFamily(Font(R.font.sf_pro)),
401+
color = if (isSystemInDarkTheme()) Color.White else Color.Black
382402
)
383403
)
384404
}

android/app/src/main/java/me/kavishdevar/librepods/screens/HearingAidScreen.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ import androidx.compose.ui.unit.sp
5454
import androidx.navigation.NavController
5555
import com.kyant.backdrop.backdrops.layerBackdrop
5656
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
57+
import dev.chrisbanes.haze.HazeState
5758
import dev.chrisbanes.haze.hazeSource
5859
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
59-
import dev.chrisbanes.haze.rememberHazeState
6060
import kotlinx.coroutines.CoroutineScope
6161
import kotlinx.coroutines.Dispatchers
6262
import kotlinx.coroutines.launch
@@ -97,7 +97,7 @@ fun HearingAidScreen(navController: NavController) {
9797
mutableStateOf((aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte()))
9898
}
9999

100-
var hazeStateS = rememberHazeState()
100+
val hazeStateS = remember { mutableStateOf(HazeState()) } // dont question this. i could possibly use something other than initializing it with an empty state and then replacing it with the the one provided by the scaffold
101101

102102
StyledScaffold(
103103
title = stringResource(R.string.hearing_aid),
@@ -112,7 +112,7 @@ fun HearingAidScreen(navController: NavController) {
112112
.padding(horizontal = 16.dp),
113113
verticalArrangement = Arrangement.spacedBy(8.dp)
114114
) {
115-
hazeStateS = hazeState
115+
hazeStateS.value = hazeState
116116
Spacer(modifier = Modifier.height(spacerHeight))
117117

118118
val hearingAidListener = remember {
@@ -288,7 +288,7 @@ fun HearingAidScreen(navController: NavController) {
288288
}
289289
}
290290
},
291-
hazeState = hazeStateS,
291+
hazeState = hazeStateS.value,
292292
// backdrop = backdrop
293293
)
294294
}

android/app/src/main/java/me/kavishdevar/librepods/screens/TroubleshootingScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ fun TroubleshootingScreen(navController: NavController) {
193193
LaunchedEffect(currentStep) {
194194
instructionText = when (currentStep) {
195195
0 -> "First, let's ensure Xposed module is properly configured. Tap the button below to check Xposed scope settings."
196-
1 -> "Please put your AirPods in the case and close it, so they disconnect completely."
196+
1 -> "Please put your AirPods in the case and close it, so they disconnectForCD completely."
197197
2 -> "Preparing to collect logs... Please wait."
198198
3 -> "Now, open the AirPods case and connect your AirPods. Logs are being collected. Connection will be detected automatically, or you can manually stop logging when you're done."
199199
4 -> "Log collection complete! You can now save or share the logs."

android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ import me.kavishdevar.librepods.constants.StemAction
8888
import me.kavishdevar.librepods.constants.isHeadTrackingData
8989
import me.kavishdevar.librepods.utils.AACPManager
9090
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType
91+
import me.kavishdevar.librepods.utils.ATTManager
9192
import me.kavishdevar.librepods.utils.AirPodsInstance
9293
import me.kavishdevar.librepods.utils.AirPodsModels
93-
import me.kavishdevar.librepods.utils.ATTManager
9494
import me.kavishdevar.librepods.utils.BLEManager
9595
import me.kavishdevar.librepods.utils.BluetoothConnectionManager
9696
import me.kavishdevar.librepods.utils.CrossDevice
@@ -229,6 +229,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
229229
private var handleIncomingCallOnceConnected = false
230230

231231
lateinit var bleManager: BLEManager
232+
233+
private lateinit var socket: BluetoothSocket
234+
232235
private val bleStatusListener = object : BLEManager.AirPodsStatusListener {
233236
@SuppressLint("NewApi")
234237
override fun onDeviceStatusChanged(
@@ -973,7 +976,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
973976
config.airpodsVersion3 = deviceInformation.version3
974977
config.airpodsHardwareRevision = deviceInformation.hardwareRevision
975978
config.airpodsUpdaterIdentifier = deviceInformation.updaterIdentifier
976-
979+
977980
val model = AirPodsModels.getModelByModelNumber(config.airpodsModelNumber)
978981
if (model != null) {
979982
airpodsInstance = AirPodsInstance(
@@ -1032,8 +1035,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
10321035
byteArrayOf(0x00)
10331036
)
10341037
// this also means that the other device has start playing the audio, and if that's true, we can again start listening for audio config changes
1035-
Log.d(TAG, "Another device started playing audio, listening for audio config changes again")
1036-
MediaController.pausedForOtherDevice = false
1038+
// Log.d(TAG, "Another device started playing audio, listening for audio config changes again")
1039+
// MediaController.pausedForOtherDevice = false
1040+
// future me: what the heck is this? this just means it will not be taking over again if audio source doesn't change???
10371041
}
10381042
}
10391043

@@ -1842,7 +1846,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
18421846
notificationManager.notify(1, updatedNotification)
18431847
notificationManager.cancel(2)
18441848
} else if (!config.bleOnlyMode && !socket.isConnected && isConnectedLocally) {
1845-
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
18461849
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
18471850
}
18481851
}
@@ -2135,10 +2138,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
21352138
val name = context?.getSharedPreferences("settings", MODE_PRIVATE)
21362139
?.getString("name", bluetoothDevice?.name)
21372140
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
2138-
Log.d(TAG, "Received bluetooth connection broadcast")
2141+
Log.d(TAG, "Received bluetooth connection broadcast: action=$action")
21392142
if (ServiceManager.getService()?.isConnectedLocally == true) {
2140-
Log.d(TAG, "Device is already connected locally, ignoring broadcast")
2141-
ServiceManager.getService()?.manuallyCheckForAudioSource()
2143+
Log.d(TAG, "Device is already connected locally, checking if we should keep audio connected")
2144+
if (ServiceManager.getService()?.socket?.isConnected == true) ServiceManager.getService()?.manuallyCheckForAudioSource() else Log.d(TAG, "We're not connected, ignoring")
21422145
return
21432146
}
21442147
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
@@ -2175,14 +2178,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
21752178
return START_STICKY
21762179
}
21772180

2178-
private lateinit var socket: BluetoothSocket
2179-
21802181
fun manuallyCheckForAudioSource() {
2181-
val shouldResume = MediaController.getMusicActive()
2182-
if ((earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) || disconnectedBecauseReversed || otherDeviceTookOver) {
2182+
val shouldResume = MediaController.getMusicActive() // todo: for some reason we lose this info after disconnecting, probably android dispatches some event. haven't investigated yet.
2183+
if (airpodsInstance == null) return
2184+
Log.d(TAG, "disconnectedBecauseReversed: $disconnectedBecauseReversed, otherDeviceTookOver: $otherDeviceTookOver")
2185+
if ((earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) || disconnectedBecauseReversed || otherDeviceTookOver) {
21832186
Log.d(
21842187
TAG,
2185-
"For some reason, Android connected to the audio profile itself even after disconnecting. Disconnecting audio profile again!"
2188+
"For some reason, Android connected to the audio profile itself even after disconnecting. Disconnecting audio profile again! I will resume: $shouldResume"
21862189
)
21872190
disconnectAudio(this, device, shouldResume = shouldResume)
21882191
}
@@ -2378,7 +2381,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
23782381
}
23792382

23802383
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
2381-
fun connectToSocket(device: BluetoothDevice) {
2384+
fun connectToSocket(device: BluetoothDevice, manual: Boolean = false) {
23822385
Log.d(TAG, "<LogCollector:Start> Connecting to socket")
23832386
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
23842387
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
@@ -2387,7 +2390,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
23872390
createBluetoothSocket(device, uuid)
23882391
} catch (e: Exception) {
23892392
Log.e(TAG, "Failed to create BluetoothSocket: ${e.message}")
2390-
showSocketConnectionFailureNotification("Failed to create Bluetooth socket: ${e.message}")
2393+
showSocketConnectionFailureNotification("Failed to create Bluetooth socket: ${e.localizedMessage}")
23912394
return
23922395
}
23932396

@@ -2431,15 +2434,29 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
24312434
)
24322435
Log.d(TAG, "<LogCollector:Complete:Success> Socket connected")
24332436
} catch (e: Exception) {
2434-
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
2435-
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
2436-
throw e
2437+
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected, ${e.message}")
2438+
if (manual) {
2439+
sendToast(
2440+
"Couldn't connect to socket: ${e.localizedMessage}"
2441+
)
2442+
} else {
2443+
showSocketConnectionFailureNotification("Couldn't connect to socket: ${e.localizedMessage}")
2444+
}
2445+
return@withTimeout
2446+
// throw e // lol how did i not catch this before... gonna comment this line instead of removing to preserve history
24372447
}
24382448
}
2439-
if (!socket.isConnected) {
2440-
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
2441-
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
2449+
}
2450+
if (!socket.isConnected) {
2451+
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
2452+
if (manual) {
2453+
sendToast(
2454+
"Couldn't connect to socket: timeout."
2455+
)
2456+
} else {
2457+
showSocketConnectionFailureNotification("Couldn't connect to socket: Timeout")
24422458
}
2459+
return
24432460
}
24442461
this@AirPodsService.device = device
24452462
socket.let {
@@ -2519,15 +2536,15 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
25192536
} catch (e: Exception) {
25202537
e.printStackTrace()
25212538
Log.d(TAG, "Failed to connect to socket: ${e.message}")
2522-
showSocketConnectionFailureNotification("Failed to establish connection: ${e.message}")
2539+
showSocketConnectionFailureNotification("Failed to establish connection: ${e.localizedMessage}")
25232540
isConnectedLocally = false
25242541
this@AirPodsService.device = device
25252542
updateNotificationContent(false)
25262543
}
25272544
}
25282545
}
25292546

2530-
fun disconnect() {
2547+
fun disconnectForCD() {
25312548
if (!this::socket.isInitialized) return
25322549
socket.close()
25332550
MediaController.pausedWhileTakingOver = false
@@ -2552,6 +2569,33 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
25522569
CrossDevice.isAvailable = true
25532570
}
25542571

2572+
fun disconnectAirPods() {
2573+
if (!this::socket.isInitialized) return
2574+
socket.close()
2575+
isConnectedLocally = false
2576+
aacpManager.disconnected()
2577+
attManager?.disconnect()
2578+
updateNotificationContent(false)
2579+
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
2580+
2581+
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
2582+
bluetoothAdapter.getProfileProxy(this, object : BluetoothProfile.ServiceListener {
2583+
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
2584+
if (profile == BluetoothProfile.A2DP) {
2585+
val connectedDevices = proxy.connectedDevices
2586+
if (connectedDevices.isNotEmpty()) {
2587+
MediaController.sendPause()
2588+
}
2589+
}
2590+
bluetoothAdapter.closeProfileProxy(profile, proxy)
2591+
}
2592+
2593+
override fun onServiceDisconnected(profile: Int) {}
2594+
}, BluetoothProfile.A2DP)
2595+
Log.d(TAG, "Disconnected AirPods upon user request")
2596+
2597+
}
2598+
25552599
val earDetectionNotification = AirPodsNotifications.EarDetection()
25562600
val ancNotification = AirPodsNotifications.ANC()
25572601
val batteryNotification = AirPodsNotifications.BatteryNotification()
@@ -2750,6 +2794,19 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
27502794
isHeadTrackingActive = false
27512795
}
27522796

2797+
@SuppressLint("MissingPermission")
2798+
fun reconnectFromSavedMac(){
2799+
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
2800+
device = bluetoothAdapter.bondedDevices.find {
2801+
it.address == macAddress
2802+
}
2803+
if (device != null) {
2804+
CoroutineScope(Dispatchers.IO).launch {
2805+
connectToSocket(device!!, manual = true)
2806+
}
2807+
}
2808+
}
2809+
27532810
}
27542811

27552812
private fun Int.dpToPx(): Int {

0 commit comments

Comments
 (0)