mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-29 06:10:52 +00:00
android: improve connection handling
This commit is contained in:
@@ -15,7 +15,7 @@ android {
|
||||
minSdk = 28
|
||||
targetSdk = 36
|
||||
versionCode = 8
|
||||
versionName = "0.2.0-alpha"
|
||||
versionName = "0.2.0-beta.1"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -86,4 +86,4 @@ aboutLibraries {
|
||||
excludeFields = listOf("generated")
|
||||
outputFile = file("src/main/res/raw/aboutlibraries.json")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ package me.kavishdevar.librepods.screens
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
@@ -35,17 +33,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
@@ -59,8 +50,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
@@ -88,7 +77,6 @@ import kotlinx.coroutines.launch
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.composables.NavigationButton
|
||||
import me.kavishdevar.librepods.composables.StyledDropdown
|
||||
import me.kavishdevar.librepods.composables.StyledIconButton
|
||||
import me.kavishdevar.librepods.composables.StyledScaffold
|
||||
import me.kavishdevar.librepods.composables.StyledSlider
|
||||
import me.kavishdevar.librepods.composables.StyledToggle
|
||||
|
||||
@@ -67,7 +67,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import com.kyant.backdrop.drawBackdrop
|
||||
import com.kyant.backdrop.highlight.Highlight
|
||||
@@ -231,7 +230,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
val instance = service.airpodsInstance
|
||||
if (instance == null) {
|
||||
Text("Error: AirPods instance is null")
|
||||
return@StyledScaffold
|
||||
return@StyledScaffold
|
||||
}
|
||||
val capabilities = instance.model.capabilities
|
||||
LazyColumn(
|
||||
@@ -267,7 +266,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
item(key = "spacer_noise") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
item(key = "noise_control") { NoiseControlSettings(service = service) }
|
||||
}
|
||||
|
||||
|
||||
if (capabilities.contains(Capability.STEM_CONFIG)) {
|
||||
item(key = "spacer_press_hold") { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
item(key = "press_hold") { PressAndHoldSettings(navController = navController) }
|
||||
@@ -370,7 +369,9 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
Spacer(Modifier.height(32.dp))
|
||||
StyledButton(
|
||||
onClick = { navController.navigate("troubleshooting") },
|
||||
backdrop = backdrop
|
||||
backdrop = backdrop,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
) {
|
||||
Text(
|
||||
text = "Troubleshoot Connection",
|
||||
@@ -378,7 +379,26 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||
)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
StyledButton(
|
||||
onClick = {
|
||||
service.reconnectFromSavedMac()
|
||||
},
|
||||
backdrop = backdrop,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.reconnect_to_last_device),
|
||||
style = TextStyle(
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -97,7 +97,7 @@ fun HearingAidScreen(navController: NavController) {
|
||||
mutableStateOf((aidStatus?.value?.getOrNull(1) == 0x01.toByte()) && (assistStatus?.value?.getOrNull(0) == 0x01.toByte()))
|
||||
}
|
||||
|
||||
var hazeStateS = rememberHazeState()
|
||||
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
|
||||
|
||||
StyledScaffold(
|
||||
title = stringResource(R.string.hearing_aid),
|
||||
@@ -112,7 +112,7 @@ fun HearingAidScreen(navController: NavController) {
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
hazeStateS = hazeState
|
||||
hazeStateS.value = hazeState
|
||||
Spacer(modifier = Modifier.height(spacerHeight))
|
||||
|
||||
val hearingAidListener = remember {
|
||||
@@ -288,7 +288,7 @@ fun HearingAidScreen(navController: NavController) {
|
||||
}
|
||||
}
|
||||
},
|
||||
hazeState = hazeStateS,
|
||||
hazeState = hazeStateS.value,
|
||||
// backdrop = backdrop
|
||||
)
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ fun TroubleshootingScreen(navController: NavController) {
|
||||
LaunchedEffect(currentStep) {
|
||||
instructionText = when (currentStep) {
|
||||
0 -> "First, let's ensure Xposed module is properly configured. Tap the button below to check Xposed scope settings."
|
||||
1 -> "Please put your AirPods in the case and close it, so they disconnect completely."
|
||||
1 -> "Please put your AirPods in the case and close it, so they disconnectForCD completely."
|
||||
2 -> "Preparing to collect logs... Please wait."
|
||||
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."
|
||||
4 -> "Log collection complete! You can now save or share the logs."
|
||||
|
||||
@@ -88,9 +88,9 @@ import me.kavishdevar.librepods.constants.StemAction
|
||||
import me.kavishdevar.librepods.constants.isHeadTrackingData
|
||||
import me.kavishdevar.librepods.utils.AACPManager
|
||||
import me.kavishdevar.librepods.utils.AACPManager.Companion.StemPressType
|
||||
import me.kavishdevar.librepods.utils.ATTManager
|
||||
import me.kavishdevar.librepods.utils.AirPodsInstance
|
||||
import me.kavishdevar.librepods.utils.AirPodsModels
|
||||
import me.kavishdevar.librepods.utils.ATTManager
|
||||
import me.kavishdevar.librepods.utils.BLEManager
|
||||
import me.kavishdevar.librepods.utils.BluetoothConnectionManager
|
||||
import me.kavishdevar.librepods.utils.CrossDevice
|
||||
@@ -229,6 +229,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
private var handleIncomingCallOnceConnected = false
|
||||
|
||||
lateinit var bleManager: BLEManager
|
||||
|
||||
private lateinit var socket: BluetoothSocket
|
||||
|
||||
private val bleStatusListener = object : BLEManager.AirPodsStatusListener {
|
||||
@SuppressLint("NewApi")
|
||||
override fun onDeviceStatusChanged(
|
||||
@@ -973,7 +976,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
config.airpodsVersion3 = deviceInformation.version3
|
||||
config.airpodsHardwareRevision = deviceInformation.hardwareRevision
|
||||
config.airpodsUpdaterIdentifier = deviceInformation.updaterIdentifier
|
||||
|
||||
|
||||
val model = AirPodsModels.getModelByModelNumber(config.airpodsModelNumber)
|
||||
if (model != null) {
|
||||
airpodsInstance = AirPodsInstance(
|
||||
@@ -1032,8 +1035,9 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
byteArrayOf(0x00)
|
||||
)
|
||||
// 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
|
||||
Log.d(TAG, "Another device started playing audio, listening for audio config changes again")
|
||||
MediaController.pausedForOtherDevice = false
|
||||
// Log.d(TAG, "Another device started playing audio, listening for audio config changes again")
|
||||
// MediaController.pausedForOtherDevice = false
|
||||
// future me: what the heck is this? this just means it will not be taking over again if audio source doesn't change???
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1842,7 +1846,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
notificationManager.notify(1, updatedNotification)
|
||||
notificationManager.cancel(2)
|
||||
} else if (!config.bleOnlyMode && !socket.isConnected && isConnectedLocally) {
|
||||
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
|
||||
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
||||
}
|
||||
}
|
||||
@@ -2135,10 +2138,10 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
val name = context?.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
?.getString("name", bluetoothDevice?.name)
|
||||
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
|
||||
Log.d(TAG, "Received bluetooth connection broadcast")
|
||||
Log.d(TAG, "Received bluetooth connection broadcast: action=$action")
|
||||
if (ServiceManager.getService()?.isConnectedLocally == true) {
|
||||
Log.d(TAG, "Device is already connected locally, ignoring broadcast")
|
||||
ServiceManager.getService()?.manuallyCheckForAudioSource()
|
||||
Log.d(TAG, "Device is already connected locally, checking if we should keep audio connected")
|
||||
if (ServiceManager.getService()?.socket?.isConnected == true) ServiceManager.getService()?.manuallyCheckForAudioSource() else Log.d(TAG, "We're not connected, ignoring")
|
||||
return
|
||||
}
|
||||
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
|
||||
@@ -2175,14 +2178,14 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private lateinit var socket: BluetoothSocket
|
||||
|
||||
fun manuallyCheckForAudioSource() {
|
||||
val shouldResume = MediaController.getMusicActive()
|
||||
if ((earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) || disconnectedBecauseReversed || otherDeviceTookOver) {
|
||||
val shouldResume = MediaController.getMusicActive() // todo: for some reason we lose this info after disconnecting, probably android dispatches some event. haven't investigated yet.
|
||||
if (airpodsInstance == null) return
|
||||
Log.d(TAG, "disconnectedBecauseReversed: $disconnectedBecauseReversed, otherDeviceTookOver: $otherDeviceTookOver")
|
||||
if ((earDetectionNotification.status[0] != 0.toByte() && earDetectionNotification.status[1] != 0.toByte()) || disconnectedBecauseReversed || otherDeviceTookOver) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"For some reason, Android connected to the audio profile itself even after disconnecting. Disconnecting audio profile again!"
|
||||
"For some reason, Android connected to the audio profile itself even after disconnecting. Disconnecting audio profile again! I will resume: $shouldResume"
|
||||
)
|
||||
disconnectAudio(this, device, shouldResume = shouldResume)
|
||||
}
|
||||
@@ -2378,7 +2381,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||
fun connectToSocket(device: BluetoothDevice) {
|
||||
fun connectToSocket(device: BluetoothDevice, manual: Boolean = false) {
|
||||
Log.d(TAG, "<LogCollector:Start> Connecting to socket")
|
||||
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
|
||||
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||
@@ -2387,7 +2390,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
createBluetoothSocket(device, uuid)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to create BluetoothSocket: ${e.message}")
|
||||
showSocketConnectionFailureNotification("Failed to create Bluetooth socket: ${e.message}")
|
||||
showSocketConnectionFailureNotification("Failed to create Bluetooth socket: ${e.localizedMessage}")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2431,15 +2434,29 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
)
|
||||
Log.d(TAG, "<LogCollector:Complete:Success> Socket connected")
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
|
||||
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
||||
throw e
|
||||
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected, ${e.message}")
|
||||
if (manual) {
|
||||
sendToast(
|
||||
"Couldn't connect to socket: ${e.localizedMessage}"
|
||||
)
|
||||
} else {
|
||||
showSocketConnectionFailureNotification("Couldn't connect to socket: ${e.localizedMessage}")
|
||||
}
|
||||
return@withTimeout
|
||||
// throw e // lol how did i not catch this before... gonna comment this line instead of removing to preserve history
|
||||
}
|
||||
}
|
||||
if (!socket.isConnected) {
|
||||
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
|
||||
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
||||
}
|
||||
if (!socket.isConnected) {
|
||||
Log.d(TAG, "<LogCollector:Complete:Failed> Socket not connected")
|
||||
if (manual) {
|
||||
sendToast(
|
||||
"Couldn't connect to socket: timeout."
|
||||
)
|
||||
} else {
|
||||
showSocketConnectionFailureNotification("Couldn't connect to socket: Timeout")
|
||||
}
|
||||
return
|
||||
}
|
||||
this@AirPodsService.device = device
|
||||
socket.let {
|
||||
@@ -2519,7 +2536,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.d(TAG, "Failed to connect to socket: ${e.message}")
|
||||
showSocketConnectionFailureNotification("Failed to establish connection: ${e.message}")
|
||||
showSocketConnectionFailureNotification("Failed to establish connection: ${e.localizedMessage}")
|
||||
isConnectedLocally = false
|
||||
this@AirPodsService.device = device
|
||||
updateNotificationContent(false)
|
||||
@@ -2527,7 +2544,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
fun disconnectForCD() {
|
||||
if (!this::socket.isInitialized) return
|
||||
socket.close()
|
||||
MediaController.pausedWhileTakingOver = false
|
||||
@@ -2552,6 +2569,33 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
CrossDevice.isAvailable = true
|
||||
}
|
||||
|
||||
fun disconnectAirPods() {
|
||||
if (!this::socket.isInitialized) return
|
||||
socket.close()
|
||||
isConnectedLocally = false
|
||||
aacpManager.disconnected()
|
||||
attManager?.disconnect()
|
||||
updateNotificationContent(false)
|
||||
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
||||
|
||||
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
|
||||
bluetoothAdapter.getProfileProxy(this, object : BluetoothProfile.ServiceListener {
|
||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||
if (profile == BluetoothProfile.A2DP) {
|
||||
val connectedDevices = proxy.connectedDevices
|
||||
if (connectedDevices.isNotEmpty()) {
|
||||
MediaController.sendPause()
|
||||
}
|
||||
}
|
||||
bluetoothAdapter.closeProfileProxy(profile, proxy)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(profile: Int) {}
|
||||
}, BluetoothProfile.A2DP)
|
||||
Log.d(TAG, "Disconnected AirPods upon user request")
|
||||
|
||||
}
|
||||
|
||||
val earDetectionNotification = AirPodsNotifications.EarDetection()
|
||||
val ancNotification = AirPodsNotifications.ANC()
|
||||
val batteryNotification = AirPodsNotifications.BatteryNotification()
|
||||
@@ -2750,6 +2794,19 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
isHeadTrackingActive = false
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun reconnectFromSavedMac(){
|
||||
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
|
||||
device = bluetoothAdapter.bondedDevices.find {
|
||||
it.address == macAddress
|
||||
}
|
||||
if (device != null) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
connectToSocket(device!!, manual = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun Int.dpToPx(): Int {
|
||||
|
||||
@@ -200,7 +200,7 @@ object CrossDevice {
|
||||
notifyAirPodsDisconnectedRemotely(ServiceManager.getService()?.applicationContext!!)
|
||||
break
|
||||
} else if (packet.contentEquals(CrossDevicePackets.REQUEST_DISCONNECT.packet) || packet.contentEquals(CrossDevicePackets.REQUEST_DISCONNECT.packet + CrossDevicePackets.AIRPODS_DATA_HEADER.packet)) {
|
||||
ServiceManager.getService()?.disconnect()
|
||||
ServiceManager.getService()?.disconnectForCD()
|
||||
disconnectionRequested = true
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
delay(1000)
|
||||
|
||||
@@ -204,4 +204,6 @@
|
||||
<string name="ppe">EN 352 Protection</string>
|
||||
<string name="workspace_use_description">EN 352 Protection limits the maximum level of media to 82 dBA, and meets applicable EN 352 Standard requirements for personal hearing protection.</string>
|
||||
<string name="environmental_noise">Environmental Noise</string>
|
||||
<string name="reconnect_to_last_device">Reconnect to last connected device</string>
|
||||
<string name="disconnect">Disconnect</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user