mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-10 19:52:24 +00:00
try to add automatic device connection detection; add "Off Listening Mode" toggle
This commit is contained in:
@@ -7,31 +7,44 @@
|
|||||||
<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.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED"
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH_PRIVILEGED"
|
||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH_ADMIN"
|
||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.ALN"
|
android:theme="@style/Theme.ALN"
|
||||||
android:enableOnBackInvokedCallback="true"
|
tools:ignore="UnusedAttribute"
|
||||||
tools:targetApi="31"
|
tools:targetApi="31">
|
||||||
tools:ignore="UnusedAttribute">
|
<activity
|
||||||
|
android:name=".CustomDevice"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/title_activity_custom_device"
|
||||||
|
android:theme="@style/Theme.ALN">
|
||||||
|
<intent-filter>
|
||||||
|
<!-- <action android:name="android.intent.action.MAIN" />-->
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.ALN">
|
android:theme="@style/Theme.ALN">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
@@ -42,7 +55,6 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:foregroundServiceType="connectedDevice"
|
android:foregroundServiceType="connectedDevice"
|
||||||
android:permission="android.permission.BLUETOOTH_CONNECT" />
|
android:permission="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".AirPodsQSService"
|
android:name=".AirPodsQSService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -53,18 +65,6 @@
|
|||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<!-- <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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -10,8 +10,8 @@ import android.service.quicksettings.TileService
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
class AirPodsQSService: TileService() {
|
class AirPodsQSService: TileService() {
|
||||||
private val ancModes = listOf(NoiseControlMode.OFF.name, NoiseControlMode.NOISE_CANCELLATION.name, NoiseControlMode.TRANSPARENCY.name, NoiseControlMode.ADAPTIVE.name)
|
private val ancModes = listOf(NoiseControlMode.NOISE_CANCELLATION.name, NoiseControlMode.TRANSPARENCY.name, NoiseControlMode.ADAPTIVE.name)
|
||||||
private var currentModeIndex = 3
|
private var currentModeIndex = 2
|
||||||
private lateinit var ancStatusReceiver: BroadcastReceiver
|
private lateinit var ancStatusReceiver: BroadcastReceiver
|
||||||
private lateinit var availabilityReceiver: BroadcastReceiver
|
private lateinit var availabilityReceiver: BroadcastReceiver
|
||||||
|
|
||||||
@@ -63,8 +63,24 @@ class AirPodsQSService: TileService() {
|
|||||||
|
|
||||||
override fun onStopListening() {
|
override fun onStopListening() {
|
||||||
super.onStopListening()
|
super.onStopListening()
|
||||||
unregisterReceiver(ancStatusReceiver)
|
try {
|
||||||
unregisterReceiver(availabilityReceiver)
|
unregisterReceiver(ancStatusReceiver)
|
||||||
|
}
|
||||||
|
catch (
|
||||||
|
e: IllegalArgumentException
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Log.e("QuickSettingTileService", "Receiver not registered")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unregisterReceiver(availabilityReceiver)
|
||||||
|
}
|
||||||
|
catch (
|
||||||
|
e: IllegalArgumentException
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Log.e("QuickSettingTileService", "Receiver not registered")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
package me.kavishdevar.aln
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -90,6 +92,10 @@ class AirPodsService : Service() {
|
|||||||
socket?.outputStream?.write(if (enabled) Enums.SET_CONVERSATION_AWARENESS_ON.value else Enums.SET_CONVERSATION_AWARENESS_OFF.value)
|
socket?.outputStream?.write(if (enabled) Enums.SET_CONVERSATION_AWARENESS_ON.value else Enums.SET_CONVERSATION_AWARENESS_OFF.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setOffListeningMode(enabled: Boolean) {
|
||||||
|
socket?.outputStream?.write(byteArrayOf(0x04, 0x00 ,0x04, 0x00, 0x09, 0x00, 0x34, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00))
|
||||||
|
}
|
||||||
|
|
||||||
fun setAdaptiveStrength(strength: Int) {
|
fun setAdaptiveStrength(strength: Int) {
|
||||||
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2E, strength.toByte(), 0x00, 0x00, 0x00)
|
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2E, strength.toByte(), 0x00, 0x00, 0x00)
|
||||||
socket?.outputStream?.write(bytes)
|
socket?.outputStream?.write(bytes)
|
||||||
|
|||||||
@@ -150,6 +150,33 @@ fun BatteryView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "ACCESSIBILITY",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f)
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
|
.padding(top = 2.dp)
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission", "NewApi")
|
@SuppressLint("MissingPermission", "NewApi")
|
||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?, service: AirPodsService?,
|
fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?, service: AirPodsService?,
|
||||||
@@ -203,6 +230,11 @@ fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?
|
|||||||
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))
|
||||||
|
IndependentToggle(name = "Off Listening Mode", service = service, functionName = "setOffListeningMode", sharedPreferences = sharedPreferences, false)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
AccessibilitySettings(service = service, sharedPreferences = sharedPreferences)
|
||||||
// Spacer(modifier = Modifier.height(16.dp))
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
// val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
|||||||
159
android/app/src/main/java/me/kavishdevar/aln/CustomDevice.kt
Normal file
159
android/app/src/main/java/me/kavishdevar/aln/CustomDevice.kt
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothDevice.TRANSPORT_LE
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCallback
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import me.kavishdevar.aln.ui.theme.ALNTheme
|
||||||
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class CustomDevice : ComponentActivity() {
|
||||||
|
@SuppressLint("MissingPermission", "CoroutineCreationDuringComposition", "NewApi")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContent {
|
||||||
|
ALNTheme {
|
||||||
|
val connect = remember { mutableStateOf(false) }
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
topBar = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text("Custom Device", style = MaterialTheme.typography.titleLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
|
||||||
|
val manager = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
|
// val device: BluetoothDevice = manager.adapter.getRemoteDevice("EC:D6:F4:3D:89:B8")
|
||||||
|
val device: BluetoothDevice = manager.adapter.getRemoteDevice("E0:90:8F:D9:94:73")
|
||||||
|
// val socket = device.createInsecureL2capChannel(31)
|
||||||
|
|
||||||
|
// socket.outputStream.write(byteArrayOf(0x12,0x3B,0x00,0x02, 0x00))
|
||||||
|
// socket.outputStream.write(byteArrayOf(0x12, 0x3A, 0x00, 0x01, 0x00, 0x08,0x01))
|
||||||
|
|
||||||
|
val gatt = device.connectGatt(this, true, object: BluetoothGattCallback() {
|
||||||
|
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
// Step 2: Iterate through the services and characteristics
|
||||||
|
gatt.services.forEach { service ->
|
||||||
|
Log.d("GATT", "Service UUID: ${service.uuid}")
|
||||||
|
service.characteristics.forEach { characteristic ->
|
||||||
|
Log.d("GATT", " Characteristic UUID: ${characteristic.uuid}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
||||||
|
if (newState == BluetoothGatt.STATE_CONNECTED) {
|
||||||
|
Log.d("GATT", "Connected to GATT server")
|
||||||
|
gatt.discoverServices() // Discover services after connection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCharacteristicWrite(
|
||||||
|
gatt: BluetoothGatt,
|
||||||
|
characteristic: BluetoothGattCharacteristic,
|
||||||
|
status: Int
|
||||||
|
) {
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
Log.d("BLE", "Write successful for UUID: ${characteristic.uuid}")
|
||||||
|
} else {
|
||||||
|
Log.e("BLE", "Write failed for UUID: ${characteristic.uuid}, status: $status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, TRANSPORT_LE, 1)
|
||||||
|
|
||||||
|
if (connect.value) {
|
||||||
|
try {
|
||||||
|
gatt.connect()
|
||||||
|
}
|
||||||
|
catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
connect.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Column (
|
||||||
|
modifier = Modifier.padding(innerPadding),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Button(
|
||||||
|
onClick = { connect.value = true }
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Text("Connect")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(onClick = {
|
||||||
|
val characteristicUuid = "4f860002-943b-49ef-bed4-2f730304427a"
|
||||||
|
val value = byteArrayOf(0x01, 0x00, 0x02)
|
||||||
|
|
||||||
|
sendWriteRequest(gatt, characteristicUuid, value)
|
||||||
|
}) {
|
||||||
|
Text("Play Sound")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission", "NewApi")
|
||||||
|
fun sendWriteRequest(
|
||||||
|
gatt: BluetoothGatt,
|
||||||
|
characteristicUuid: String,
|
||||||
|
value: ByteArray
|
||||||
|
) {
|
||||||
|
// Retrieve the service containing the characteristic
|
||||||
|
val service = gatt.services.find { service ->
|
||||||
|
service.characteristics.any { it.uuid.toString() == characteristicUuid }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (service == null) {
|
||||||
|
Log.e("GATT", "Service containing characteristic UUID $characteristicUuid not found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the characteristic
|
||||||
|
val characteristic = service.getCharacteristic(UUID.fromString(characteristicUuid))
|
||||||
|
if (characteristic == null) {
|
||||||
|
Log.e("GATT", "Characteristic with UUID $characteristicUuid not found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Send the write request
|
||||||
|
val success = gatt.writeCharacteristic(characteristic, value, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
|
||||||
|
Log.d("GATT", "Write request sent $success to UUID: $characteristicUuid")
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
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
|
||||||
@@ -43,6 +42,7 @@ import androidx.compose.ui.graphics.luminance
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
@@ -61,6 +61,43 @@ class MainActivity : ComponentActivity() {
|
|||||||
setContent {
|
setContent {
|
||||||
val topAppBarTitle = remember { mutableStateOf("AirPods Pro") }
|
val topAppBarTitle = remember { mutableStateOf("AirPods Pro") }
|
||||||
ALNTheme {
|
ALNTheme {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
registerReceiver(object: BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent) {
|
||||||
|
val bluetoothDevice =
|
||||||
|
intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE", BluetoothDevice::class.java)
|
||||||
|
val action = intent.action
|
||||||
|
|
||||||
|
// Airpods filter
|
||||||
|
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
|
||||||
|
Log.d("BluetoothReceiver", "Received broadcast")
|
||||||
|
// Airpods connected, show notification.
|
||||||
|
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
|
||||||
|
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||||
|
if (bluetoothDevice.uuids.contains(uuid)) {
|
||||||
|
topAppBarTitle.value = bluetoothDevice.name
|
||||||
|
}
|
||||||
|
// start service
|
||||||
|
startService(Intent(context, AirPodsService::class.java).apply {
|
||||||
|
putExtra("device", bluetoothDevice)
|
||||||
|
})
|
||||||
|
Log.d("AirPodsService", "Service started")
|
||||||
|
context?.sendBroadcast(Intent(AirPodsNotifications.AIRPODS_CONNECTED))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Airpods disconnected, remove notification but leave the scanner going.
|
||||||
|
if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action
|
||||||
|
|| BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action
|
||||||
|
) {
|
||||||
|
topAppBarTitle.value = "AirPods Pro"
|
||||||
|
// stop service
|
||||||
|
stopService(Intent(context, AirPodsService::class.java))
|
||||||
|
Log.d("AirPodsService", "Service stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, BluetoothReceiver.buildFilter())
|
||||||
|
|
||||||
Scaffold (
|
Scaffold (
|
||||||
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
|
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
|
||||||
0xFF000000
|
0xFF000000
|
||||||
@@ -109,6 +146,7 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
|
|||||||
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
|
||||||
val disconnectReceiver = object : BroadcastReceiver() {
|
val disconnectReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
navController.navigate("notConnected")
|
navController.navigate("notConnected")
|
||||||
@@ -165,42 +203,8 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Register the receiver in LaunchedEffect
|
||||||
LaunchedEffect(Unit) {
|
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
|
// Initial check for AirPods connection
|
||||||
checkIfAirPodsConnected()
|
checkIfAirPodsConnected()
|
||||||
}
|
}
|
||||||
@@ -230,6 +234,21 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
context,
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||||
|
override fun onReceive(context: Context?, intent: Intent) {
|
||||||
|
Log.d("PLEASE NAVIGATE", "TO SETTINGS")
|
||||||
|
navController.navigate("settings") {
|
||||||
|
popUpTo("notConnected") { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IntentFilter(AirPodsNotifications.AIRPODS_CONNECTED),
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
|
)
|
||||||
|
|
||||||
// Automatically navigate to settings screen if AirPods are connected
|
// Automatically navigate to settings screen if AirPods are connected
|
||||||
if (airpodsDevice.value != null) {
|
if (airpodsDevice.value != null) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|||||||
62
android/app/src/main/java/me/kavishdevar/aln/receiver.kt
Normal file
62
android/app/src/main/java/me/kavishdevar/aln/receiver.kt
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
|
||||||
|
class BluetoothReceiver : BroadcastReceiver() {
|
||||||
|
fun onConnect(bluetoothDevice: BluetoothDevice?) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDisconnect(bluetoothDevice: BluetoothDevice?) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
override fun onReceive(context: Context?, intent: Intent) {
|
||||||
|
val bluetoothDevice =
|
||||||
|
intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE", BluetoothDevice::class.java)
|
||||||
|
val action = intent.action
|
||||||
|
|
||||||
|
// Airpods filter
|
||||||
|
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
|
||||||
|
// Airpods connected, show notification.
|
||||||
|
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
|
||||||
|
onConnect(bluetoothDevice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Airpods disconnected, remove notification but leave the scanner going.
|
||||||
|
if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action
|
||||||
|
|| BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action
|
||||||
|
) {
|
||||||
|
onDisconnect(bluetoothDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* When the service is created, we register to get as many bluetooth and airpods related events as possible.
|
||||||
|
* ACL_CONNECTED and ACL_DISCONNECTED should have been enough, but you never know with android these days.
|
||||||
|
*/
|
||||||
|
fun buildFilter(): IntentFilter {
|
||||||
|
val intentFilter = IntentFilter()
|
||||||
|
intentFilter.addAction("android.bluetooth.device.action.ACL_CONNECTED")
|
||||||
|
intentFilter.addAction("android.bluetooth.device.action.ACL_DISCONNECTED")
|
||||||
|
intentFilter.addAction("android.bluetooth.device.action.BOND_STATE_CHANGED")
|
||||||
|
intentFilter.addAction("android.bluetooth.device.action.NAME_CHANGED")
|
||||||
|
intentFilter.addAction("android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED")
|
||||||
|
intentFilter.addAction("android.bluetooth.adapter.action.STATE_CHANGED")
|
||||||
|
intentFilter.addAction("android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED")
|
||||||
|
intentFilter.addAction("android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT")
|
||||||
|
intentFilter.addAction("android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED")
|
||||||
|
intentFilter.addAction("android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED")
|
||||||
|
intentFilter.addCategory("android.bluetooth.headset.intent.category.companyid.76")
|
||||||
|
return intentFilter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">ALN</string>
|
<string name="app_name">ALN</string>
|
||||||
<string name="title_activity_debug">DebugActivity</string>
|
<string name="title_activity_debug">DebugActivity</string>
|
||||||
|
<string name="title_activity_custom_device">CustomDevice</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
accompanistPermissions = "0.36.0"
|
accompanistPermissions = "0.36.0"
|
||||||
agp = "8.7.0"
|
agp = "8.7.2"
|
||||||
hiddenapibypass = "4.3"
|
hiddenapibypass = "4.3"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.0"
|
||||||
coreKtx = "1.13.1"
|
coreKtx = "1.13.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user