mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-10 19:52:24 +00:00
android: disable audio profiles when not in ear; add a debug screen
This commit is contained in:
@@ -6,6 +6,12 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED"
|
||||||
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
||||||
|
tools:ignore="ProtectedPermissions" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -36,6 +42,18 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:foregroundServiceType="connectedDevice"
|
android:foregroundServiceType="connectedDevice"
|
||||||
android:permission="android.permission.BLUETOOTH_CONNECT" />
|
android:permission="android.permission.BLUETOOTH_CONNECT" />
|
||||||
</application>
|
|
||||||
|
|
||||||
|
<!-- <receiver android:name=".StartupReceiver"-->
|
||||||
|
<!-- android:exported="true">-->
|
||||||
|
<!-- <intent-filter>-->
|
||||||
|
<!-- <action android:name="android.bluetooth.device.action.ACL_CONNECTED" />-->
|
||||||
|
<!-- <action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" />-->
|
||||||
|
<!-- <action android:name="android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED" />-->
|
||||||
|
<!-- <action android:name="android.bluetooth.device.action.BOND_STATE_CHANGED" />-->
|
||||||
|
<!-- <action android:name="android.bluetooth.device.action.NAME_CHANGED" />-->
|
||||||
|
<!-- <action android:name="android.intent.action.BOOT_COMPLETED" />-->
|
||||||
|
<!-- <action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />-->
|
||||||
|
<!-- </intent-filter>-->
|
||||||
|
<!-- </receiver>-->
|
||||||
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -6,6 +6,9 @@ import android.app.NotificationChannel
|
|||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothHeadset
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothProfile
|
||||||
import android.bluetooth.BluetoothSocket
|
import android.bluetooth.BluetoothSocket
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -23,6 +26,16 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
|
|
||||||
|
private const val VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV = "+IPHONEACCEV"
|
||||||
|
private const val VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL = 1
|
||||||
|
private const val APPLE = 0x004C
|
||||||
|
const val ACTION_BATTERY_LEVEL_CHANGED = "android.bluetooth.device.action.BATTERY_LEVEL_CHANGED"
|
||||||
|
const val EXTRA_BATTERY_LEVEL = "android.bluetooth.device.extra.BATTERY_LEVEL"
|
||||||
|
private const val PACKAGE_ASI = "com.google.android.settings.intelligence"
|
||||||
|
private const val ACTION_ASI_UPDATE_BLUETOOTH_DATA = "batterywidget.impl.action.update_bluetooth_data"
|
||||||
|
//private const val COMPANION_TYPE_NONE = "COMPANION_NONE"
|
||||||
|
//const val VENDOR_RESULT_CODE_COMMAND_ANDROID = "+ANDROID"
|
||||||
|
|
||||||
class AirPodsService : Service() {
|
class AirPodsService : Service() {
|
||||||
inner class LocalBinder : Binder() {
|
inner class LocalBinder : Binder() {
|
||||||
fun getService(): AirPodsService = this@AirPodsService
|
fun getService(): AirPodsService = this@AirPodsService
|
||||||
@@ -93,7 +106,7 @@ class AirPodsService : Service() {
|
|||||||
fun getANC(): Int {
|
fun getANC(): Int {
|
||||||
return ancNotification.status
|
return ancNotification.status
|
||||||
}
|
}
|
||||||
|
//
|
||||||
// private fun buildBatteryText(battery: List<Battery>): String {
|
// private fun buildBatteryText(battery: List<Battery>): String {
|
||||||
// val left = battery[0]
|
// val left = battery[0]
|
||||||
// val right = battery[1]
|
// val right = battery[1]
|
||||||
@@ -118,6 +131,165 @@ class AirPodsService : Service() {
|
|||||||
return notificationBuilder.build()
|
return notificationBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun disconnectAudio(context: Context, device: BluetoothDevice?) {
|
||||||
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
|
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||||
|
if (profile == BluetoothProfile.A2DP) {
|
||||||
|
try {
|
||||||
|
val method = proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
|
||||||
|
method.invoke(proxy, device)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(profile: Int) { }
|
||||||
|
}, BluetoothProfile.A2DP)
|
||||||
|
|
||||||
|
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||||
|
if (profile == BluetoothProfile.HEADSET) {
|
||||||
|
try {
|
||||||
|
val method = proxy.javaClass.getMethod("disconnect", BluetoothDevice::class.java)
|
||||||
|
method.invoke(proxy, device)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(profile: Int) { }
|
||||||
|
}, BluetoothProfile.HEADSET)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connectAudio(context: Context, device: BluetoothDevice?) {
|
||||||
|
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
|
||||||
|
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||||
|
if (profile == BluetoothProfile.A2DP) {
|
||||||
|
try {
|
||||||
|
val method = proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
|
||||||
|
method.invoke(proxy, device)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(profile: Int) { }
|
||||||
|
}, BluetoothProfile.A2DP)
|
||||||
|
|
||||||
|
bluetoothAdapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||||
|
if (profile == BluetoothProfile.HEADSET) {
|
||||||
|
try {
|
||||||
|
val method = proxy.javaClass.getMethod("connect", BluetoothDevice::class.java)
|
||||||
|
method.invoke(proxy, device)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} finally {
|
||||||
|
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(profile: Int) { }
|
||||||
|
}, BluetoothProfile.HEADSET)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePodsStatus(device: BluetoothDevice, batteryList: List<Battery>) {
|
||||||
|
var batteryUnified = 0
|
||||||
|
var batteryUnifiedArg = 0
|
||||||
|
|
||||||
|
// Handle each Battery object from batteryList
|
||||||
|
// batteryList.forEach { battery ->
|
||||||
|
// when (battery.getComponentName()) {
|
||||||
|
// "LEFT" -> {
|
||||||
|
// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 10, battery.level.toString().toByteArray())
|
||||||
|
// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 13, battery.getStatusName()?.uppercase()?.toByteArray())
|
||||||
|
// }
|
||||||
|
// "RIGHT" -> {
|
||||||
|
// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 11, battery.level.toString().toByteArray())
|
||||||
|
// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 14, battery.getStatusName()?.uppercase()?.toByteArray())
|
||||||
|
// }
|
||||||
|
// "CASE" -> {
|
||||||
|
// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 12, battery.level.toString().toByteArray())
|
||||||
|
// HiddenApiBypass.invoke(BluetoothDevice::class.java, device, "setMetadata", 15, battery.getStatusName()?.uppercase()?.toByteArray())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// Sending broadcast for battery update
|
||||||
|
broadcastVendorSpecificEventIntent(
|
||||||
|
VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV,
|
||||||
|
APPLE,
|
||||||
|
BluetoothHeadset.AT_CMD_TYPE_SET,
|
||||||
|
batteryUnified,
|
||||||
|
batteryUnifiedArg,
|
||||||
|
device
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("SameParameterValue")
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
private fun broadcastVendorSpecificEventIntent(
|
||||||
|
command: String,
|
||||||
|
companyId: Int,
|
||||||
|
commandType: Int,
|
||||||
|
batteryUnified: Int,
|
||||||
|
batteryUnifiedArg: Int,
|
||||||
|
device: BluetoothDevice
|
||||||
|
) {
|
||||||
|
val arguments = arrayOf(
|
||||||
|
1, // Number of key(IndicatorType)/value pairs
|
||||||
|
VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL, // IndicatorType: Battery Level
|
||||||
|
batteryUnifiedArg // Battery Level
|
||||||
|
)
|
||||||
|
|
||||||
|
val intent = Intent(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT).apply {
|
||||||
|
putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD, command)
|
||||||
|
putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE, commandType)
|
||||||
|
putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS, arguments)
|
||||||
|
putExtra(BluetoothDevice.EXTRA_DEVICE, device)
|
||||||
|
putExtra(BluetoothDevice.EXTRA_NAME, device.name)
|
||||||
|
addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "." + companyId.toString())
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
|
|
||||||
|
val batteryIntent = Intent(ACTION_BATTERY_LEVEL_CHANGED).apply {
|
||||||
|
putExtra(BluetoothDevice.EXTRA_DEVICE, device)
|
||||||
|
putExtra(EXTRA_BATTERY_LEVEL, batteryUnified)
|
||||||
|
}
|
||||||
|
sendBroadcast(batteryIntent)
|
||||||
|
|
||||||
|
val statusIntent = Intent(ACTION_ASI_UPDATE_BLUETOOTH_DATA).setPackage(PACKAGE_ASI).apply {
|
||||||
|
putExtra(ACTION_BATTERY_LEVEL_CHANGED, intent)
|
||||||
|
}
|
||||||
|
sendBroadcast(statusIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun setName(name: String) {
|
||||||
|
val nameBytes = name.toByteArray()
|
||||||
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x1a, 0x00, 0x01,
|
||||||
|
nameBytes.size.toByte(), 0x00) + nameBytes
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
val hex = bytes.joinToString(" ") { "%02X".format(it) }
|
||||||
|
Log.d("AirPodsService", "setName: $name, sent packet: $hex")
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission", "InlinedApi")
|
@SuppressLint("MissingPermission", "InlinedApi")
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
|
||||||
@@ -133,6 +305,7 @@ class AirPodsService : Service() {
|
|||||||
|
|
||||||
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
|
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
|
||||||
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||||
|
|
||||||
socket = HiddenApiBypass.newInstance(BluetoothSocket::class.java, 3, true, true, device, 0x1001, uuid) as BluetoothSocket?
|
socket = HiddenApiBypass.newInstance(BluetoothSocket::class.java, 3, true, true, device, 0x1001, uuid) as BluetoothSocket?
|
||||||
try {
|
try {
|
||||||
socket?.connect()
|
socket?.connect()
|
||||||
@@ -150,8 +323,9 @@ class AirPodsService : Service() {
|
|||||||
MediaController.initialize(audioManager)
|
MediaController.initialize(audioManager)
|
||||||
val buffer = ByteArray(1024)
|
val buffer = ByteArray(1024)
|
||||||
val bytesRead = it.inputStream.read(buffer)
|
val bytesRead = it.inputStream.read(buffer)
|
||||||
val data = buffer.copyOfRange(0, bytesRead)
|
var data: ByteArray = byteArrayOf()
|
||||||
if (bytesRead > 0) {
|
if (bytesRead > 0) {
|
||||||
|
data = buffer.copyOfRange(0, bytesRead)
|
||||||
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply {
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply {
|
||||||
putExtra("data", buffer.copyOfRange(0, bytesRead))
|
putExtra("data", buffer.copyOfRange(0, bytesRead))
|
||||||
})
|
})
|
||||||
@@ -159,6 +333,15 @@ class AirPodsService : Service() {
|
|||||||
val formattedHex = bytes.joinToString(" ") { "%02X".format(it) }
|
val formattedHex = bytes.joinToString(" ") { "%02X".format(it) }
|
||||||
Log.d("AirPods Data", "Data received: $formattedHex")
|
Log.d("AirPods Data", "Data received: $formattedHex")
|
||||||
}
|
}
|
||||||
|
else if (bytesRead == -1) {
|
||||||
|
Log.d("AirPods Service", "Socket closed (bytesRead = -1)")
|
||||||
|
this@AirPodsService.stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
socket?.close()
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
var inEar = false
|
||||||
|
var inEarData = listOf<Boolean>()
|
||||||
if (earDetectionNotification.isEarDetectionData(data)) {
|
if (earDetectionNotification.isEarDetectionData(data)) {
|
||||||
earDetectionNotification.setStatus(data)
|
earDetectionNotification.setStatus(data)
|
||||||
sendBroadcast(Intent(AirPodsNotifications.EAR_DETECTION_DATA).apply {
|
sendBroadcast(Intent(AirPodsNotifications.EAR_DETECTION_DATA).apply {
|
||||||
@@ -169,7 +352,7 @@ class AirPodsService : Service() {
|
|||||||
putExtra("data", bytes)
|
putExtra("data", bytes)
|
||||||
})
|
})
|
||||||
Log.d("AirPods Parser", "Ear Detection: ${earDetectionNotification.status[0]} ${earDetectionNotification.status[1]}")
|
Log.d("AirPods Parser", "Ear Detection: ${earDetectionNotification.status[0]} ${earDetectionNotification.status[1]}")
|
||||||
var inEar = false
|
var justEnabledA2dp = false
|
||||||
val earReceiver = object : BroadcastReceiver() {
|
val earReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val data = intent.getByteArrayExtra("data")
|
val data = intent.getByteArrayExtra("data")
|
||||||
@@ -179,10 +362,52 @@ class AirPodsService : Service() {
|
|||||||
} else {
|
} else {
|
||||||
data[0] == 0x00.toByte() && data[1] == 0x00.toByte()
|
data[0] == 0x00.toByte() && data[1] == 0x00.toByte()
|
||||||
}
|
}
|
||||||
if (inEar) {
|
|
||||||
MediaController.sendPlay()
|
val newInEarData = listOf(data[0] == 0x00.toByte(), data[1] == 0x00.toByte())
|
||||||
|
if (newInEarData.contains(true) && inEarData == listOf(false, false)) {
|
||||||
|
connectAudio(this@AirPodsService, device)
|
||||||
|
justEnabledA2dp = true
|
||||||
|
val bluetoothAdapter = this@AirPodsService.getSystemService(BluetoothManager::class.java).adapter
|
||||||
|
bluetoothAdapter.getProfileProxy(
|
||||||
|
this@AirPodsService, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceConnected(
|
||||||
|
profile: Int,
|
||||||
|
proxy: BluetoothProfile
|
||||||
|
) {
|
||||||
|
if (profile == BluetoothProfile.A2DP) {
|
||||||
|
val connectedDevices =
|
||||||
|
proxy.connectedDevices
|
||||||
|
if (connectedDevices.isNotEmpty()) {
|
||||||
|
MediaController.sendPlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bluetoothAdapter.closeProfileProxy(
|
||||||
|
profile,
|
||||||
|
proxy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(
|
||||||
|
profile: Int
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
,BluetoothProfile.A2DP
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else if (newInEarData == listOf(false, false)){
|
||||||
|
disconnectAudio(this@AirPodsService, device)
|
||||||
|
}
|
||||||
|
|
||||||
|
inEarData = newInEarData
|
||||||
|
|
||||||
|
if (inEar == true) {
|
||||||
|
if (!justEnabledA2dp) {
|
||||||
|
justEnabledA2dp = false
|
||||||
|
MediaController.sendPlay()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
MediaController.sendPause()
|
MediaController.sendPause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,18 +434,21 @@ class AirPodsService : Service() {
|
|||||||
for (battery in batteryNotification.getBattery()) {
|
for (battery in batteryNotification.getBattery()) {
|
||||||
Log.d("AirPods Parser", "${battery.getComponentName()}: ${battery.getStatusName()} at ${battery.level}% ")
|
Log.d("AirPods Parser", "${battery.getComponentName()}: ${battery.getStatusName()} at ${battery.level}% ")
|
||||||
}
|
}
|
||||||
|
// updatePodsStatus(device!!, batteryNotification.getBattery())
|
||||||
}
|
}
|
||||||
else if (conversationAwarenessNotification.isConversationalAwarenessData(data)) {
|
else if (conversationAwarenessNotification.isConversationalAwarenessData(data)) {
|
||||||
conversationAwarenessNotification.setData(data)
|
conversationAwarenessNotification.setData(data)
|
||||||
sendBroadcast(Intent(AirPodsNotifications.CA_DATA).apply {
|
sendBroadcast(Intent(AirPodsNotifications.CA_DATA).apply {
|
||||||
putExtra("data", conversationAwarenessNotification.status)
|
putExtra("data", conversationAwarenessNotification.status)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
if (conversationAwarenessNotification.status == 1.toByte() || conversationAwarenessNotification.status == 2.toByte()) {
|
if (conversationAwarenessNotification.status == 1.toByte() || conversationAwarenessNotification.status == 2.toByte()) {
|
||||||
MediaController.startSpeaking()
|
MediaController.startSpeaking()
|
||||||
}
|
} else if (conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) {
|
||||||
else if (conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) {
|
|
||||||
MediaController.stopSpeaking()
|
MediaController.stopSpeaking()
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("AirPods Parser", "Conversation Awareness: ${conversationAwarenessNotification.status}")
|
Log.d("AirPods Parser", "Conversation Awareness: ${conversationAwarenessNotification.status}")
|
||||||
}
|
}
|
||||||
else { }
|
else { }
|
||||||
@@ -228,6 +456,9 @@ class AirPodsService : Service() {
|
|||||||
}
|
}
|
||||||
Log.d("AirPods Service", "Socket closed")
|
Log.d("AirPods Service", "Socket closed")
|
||||||
isRunning = false
|
isRunning = false
|
||||||
|
this@AirPodsService.stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
|
socket?.close()
|
||||||
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import androidx.compose.ui.draw.alpha
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
@@ -153,8 +154,9 @@ fun BatteryView() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?, service: AirPodsService?,
|
fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?, service: AirPodsService?,
|
||||||
navController: NavController) {
|
navController: NavController) {
|
||||||
var deviceName by remember { mutableStateOf(TextFieldValue(device?.name ?: "AirPods Pro (fallback, should never show up)")) }
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
var deviceName by remember { mutableStateOf(TextFieldValue(sharedPreferences.getString("name", device?.name ?: "") ?: "")) }
|
||||||
|
// 4B 61 76 69 73 68 E2 80 99 73 20 41 69 72 50 6F 64 73 20 50 72 6F
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -176,25 +178,29 @@ fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BatteryView()
|
|
||||||
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
if (service != null) {
|
if (service != null) {
|
||||||
|
BatteryView()
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
StyledTextField(
|
StyledTextField(
|
||||||
name = "Name",
|
name = "Name",
|
||||||
value = deviceName.text,
|
value = deviceName.text,
|
||||||
onValueChange = { deviceName = TextFieldValue(it) }
|
onValueChange = {
|
||||||
|
deviceName = TextFieldValue(it)
|
||||||
|
sharedPreferences.edit().putString("name", it).apply()
|
||||||
|
service.setName(it)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
NoiseControlSettings(service = service)
|
NoiseControlSettings(service = service)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
AudioSettings(service = service, sharedPreferences = sharedPreferences)
|
AudioSettings(service = service, sharedPreferences = sharedPreferences)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
IndependentToggle(name = "Automatic Ear Detection", service = service, functionName = "setEarDetection", sharedPreferences = sharedPreferences, true)
|
IndependentToggle(name = "Automatic Ear Detection", service = service, functionName = "setEarDetection", sharedPreferences = sharedPreferences, true)
|
||||||
|
|
||||||
// Spacer(modifier = Modifier.height(16.dp))
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -217,7 +223,6 @@ fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Row (
|
Row (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF1C1B20) else Color(0xFFFFFFFF), RoundedCornerShape(14.dp))
|
.background(if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF1C1B20) else Color(0xFFFFFFFF), RoundedCornerShape(14.dp))
|
||||||
@@ -767,11 +772,17 @@ fun StyledTextField(
|
|||||||
value: String,
|
value: String,
|
||||||
onValueChange: (String) -> Unit
|
onValueChange: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
|
var isFocused by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1B20) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1B20) else Color(0xFFFFFFFF)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val cursorColor = if (isDarkTheme) Color.White else Color.Black
|
val cursorColor = if (isFocused) { // Show cursor only when focused
|
||||||
|
if (isDarkTheme) Color.White else Color.Black
|
||||||
|
} else {
|
||||||
|
Color.Transparent // Hide cursor when not focused
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -781,14 +792,14 @@ fun StyledTextField(
|
|||||||
.background(
|
.background(
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
RoundedCornerShape(14.dp)
|
RoundedCornerShape(14.dp)
|
||||||
) // Dynamic background based on theme
|
)
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = name,
|
text = name,
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
color = textColor // Text color based on theme
|
color = textColor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -796,10 +807,10 @@ fun StyledTextField(
|
|||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
color = textColor, // Dynamic text color
|
color = textColor,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
),
|
),
|
||||||
cursorBrush = SolidColor(cursorColor), // Dynamic cursor color
|
cursorBrush = SolidColor(cursorColor), // Dynamic cursor color based on focus
|
||||||
decorationBox = { innerTextField ->
|
decorationBox = { innerTextField ->
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -809,8 +820,11 @@ fun StyledTextField(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth() // Ensures text field takes remaining available space
|
.fillMaxWidth()
|
||||||
.padding(start = 8.dp), // Padding to adjust spacing between text field and icon,
|
.padding(start = 8.dp)
|
||||||
|
.onFocusChanged { focusState ->
|
||||||
|
isFocused = focusState.isFocused // Update focus state
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ fun DebugScreen(navController: NavController) {
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color(0xFF1C1B20)),
|
.background(if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF1C1B20) else Color(0xFFF2F2F7)),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
val packet = remember { mutableStateOf(TextFieldValue("")) }
|
val packet = remember { mutableStateOf(TextFieldValue("")) }
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
package me.kavishdevar.aln
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.bluetooth.BluetoothProfile
|
import android.bluetooth.BluetoothProfile
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
@@ -30,6 +34,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -53,8 +58,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
|
val topAppBarTitle = remember { mutableStateOf("AirPods Pro") }
|
||||||
ALNTheme {
|
ALNTheme {
|
||||||
Scaffold (
|
Scaffold (
|
||||||
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
|
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
|
||||||
@@ -66,7 +71,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = "AirPods Pro Settings",
|
text = topAppBarTitle.value,
|
||||||
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black,
|
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -80,7 +85,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Main(innerPadding)
|
Main(innerPadding, topAppBarTitle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +95,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Main(paddingValues: PaddingValues) {
|
fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
|
||||||
val bluetoothConnectPermissionState = rememberPermissionState(
|
val bluetoothConnectPermissionState = rememberPermissionState(
|
||||||
permission = "android.permission.BLUETOOTH_CONNECT"
|
permission = "android.permission.BLUETOOTH_CONNECT"
|
||||||
)
|
)
|
||||||
@@ -100,38 +105,21 @@ fun Main(paddingValues: PaddingValues) {
|
|||||||
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||||
val bluetoothManager = getSystemService(context, BluetoothManager::class.java)
|
val bluetoothManager = getSystemService(context, BluetoothManager::class.java)
|
||||||
val bluetoothAdapter = bluetoothManager?.adapter
|
val bluetoothAdapter = bluetoothManager?.adapter
|
||||||
val devices = bluetoothAdapter?.bondedDevices
|
|
||||||
val airpodsDevice = remember { mutableStateOf<BluetoothDevice?>(null) }
|
val airpodsDevice = remember { mutableStateOf<BluetoothDevice?>(null) }
|
||||||
|
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
if (devices != null) {
|
val disconnectReceiver = object : BroadcastReceiver() {
|
||||||
for (device in devices) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
if (device.uuids.contains(uuid)) {
|
navController.navigate("notConnected")
|
||||||
bluetoothAdapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
|
||||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
|
||||||
if (profile == BluetoothProfile.A2DP) {
|
|
||||||
val connectedDevices = proxy.connectedDevices
|
|
||||||
if (connectedDevices.isNotEmpty()) {
|
|
||||||
airpodsDevice.value = device
|
|
||||||
if (context.getSystemService(AirPodsService::class.java) == null || context.getSystemService(AirPodsService::class.java)?.isRunning != true) {
|
|
||||||
context.startService(Intent(context, AirPodsService::class.java).apply {
|
|
||||||
putExtra("device", device)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bluetoothAdapter.closeProfileProxy(profile, proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(profile: Int) { }
|
|
||||||
}, BluetoothProfile.A2DP)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.registerReceiver(disconnectReceiver, IntentFilter(AirPodsNotifications.AIRPODS_DISCONNECTED),
|
||||||
|
Context.RECEIVER_NOT_EXPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
// Service connection for AirPodsService
|
||||||
|
|
||||||
val serviceConnection = object : ServiceConnection {
|
val serviceConnection = object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||||
val binder = service as AirPodsService.LocalBinder
|
val binder = service as AirPodsService.LocalBinder
|
||||||
@@ -144,16 +132,88 @@ fun Main(paddingValues: PaddingValues) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val intent = Intent(context, AirPodsService::class.java)
|
// Function to check if AirPods are connected
|
||||||
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
fun checkIfAirPodsConnected() {
|
||||||
|
val devices = bluetoothAdapter?.bondedDevices
|
||||||
|
devices?.forEach { device ->
|
||||||
|
if (device.uuids.contains(uuid)) {
|
||||||
|
bluetoothAdapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
||||||
|
if (profile == BluetoothProfile.A2DP) {
|
||||||
|
val connectedDevices = proxy.connectedDevices
|
||||||
|
if (connectedDevices.isNotEmpty()) {
|
||||||
|
airpodsDevice.value = device
|
||||||
|
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
topAppBarTitle.value = sharedPreferences.getString("name", device.name) ?: device.name
|
||||||
|
// Start AirPods service if not running
|
||||||
|
if (context.getSystemService(AirPodsService::class.java)?.isRunning != true) {
|
||||||
|
context.startService(Intent(context, AirPodsService::class.java).apply {
|
||||||
|
putExtra("device", device)
|
||||||
|
})
|
||||||
|
context.bindService(Intent(context, AirPodsService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
airpodsDevice.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bluetoothAdapter.closeProfileProxy(profile, proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(profile: Int) {}
|
||||||
|
}, BluetoothProfile.A2DP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BroadcastReceiver to listen for connection state changes
|
||||||
|
val bluetoothReceiver = remember {
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
val action = intent?.action
|
||||||
|
val device = intent?.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
|
||||||
|
if (action == BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED) {
|
||||||
|
when (intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1)) {
|
||||||
|
BluetoothAdapter.STATE_CONNECTED -> {
|
||||||
|
if (device?.uuids?.contains(uuid) == true) {
|
||||||
|
airpodsDevice.value = device
|
||||||
|
checkIfAirPodsConnected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BluetoothAdapter.STATE_DISCONNECTED -> {
|
||||||
|
if (device?.uuids?.contains(uuid) == true) {
|
||||||
|
airpodsDevice.value = null
|
||||||
|
// Show not connected screen when AirPods disconnect
|
||||||
|
navController.navigate("notConnected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the receiver in LaunchedEffect
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
val filter = IntentFilter().apply {
|
||||||
|
addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.registerReceiver(bluetoothReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial check for AirPods connection
|
||||||
|
checkIfAirPodsConnected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI logic
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = "notConnected",
|
startDestination = "notConnected",
|
||||||
enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) }, // Slide in from the right
|
enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) },
|
||||||
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) }, // Slide out to the left
|
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) },
|
||||||
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) }, // Slide in from the left
|
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) },
|
||||||
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(300)) } // Slide out to the right
|
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(300)) }
|
||||||
){
|
) {
|
||||||
composable("notConnected") {
|
composable("notConnected") {
|
||||||
Text("Not Connected...")
|
Text("Not Connected...")
|
||||||
}
|
}
|
||||||
@@ -169,17 +229,17 @@ fun Main(paddingValues: PaddingValues) {
|
|||||||
DebugScreen(navController = navController)
|
DebugScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatically navigate to settings screen if AirPods are connected
|
||||||
if (airpodsDevice.value != null) {
|
if (airpodsDevice.value != null) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
navController.navigate("settings") {
|
navController.navigate("settings") {
|
||||||
popUpTo("notConnected") { inclusive = true }
|
popUpTo("notConnected") { inclusive = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
Text("No AirPods connected")
|
Text("No AirPods connected")
|
||||||
}
|
}
|
||||||
return
|
|
||||||
} else {
|
} else {
|
||||||
// Permission is not granted, request it
|
// Permission is not granted, request it
|
||||||
Column (
|
Column (
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class AirPodsNotifications {
|
|||||||
const val ANC_DATA = "me.kavishdevar.aln.ANC_DATA"
|
const val ANC_DATA = "me.kavishdevar.aln.ANC_DATA"
|
||||||
const val BATTERY_DATA = "me.kavishdevar.aln.BATTERY_DATA"
|
const val BATTERY_DATA = "me.kavishdevar.aln.BATTERY_DATA"
|
||||||
const val CA_DATA = "me.kavishdevar.aln.CA_DATA"
|
const val CA_DATA = "me.kavishdevar.aln.CA_DATA"
|
||||||
|
const val AIRPODS_DISCONNECTED = "me.kavishdevar.aln.AIRPODS_DISCONNECTED"
|
||||||
}
|
}
|
||||||
|
|
||||||
class EarDetection {
|
class EarDetection {
|
||||||
|
|||||||
Reference in New Issue
Block a user