diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 57599da..e01ed6f 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -7,31 +7,44 @@
-
-
+ tools:ignore="UnusedAttribute"
+ tools:targetApi="31">
+
+
+
+
+
+
+
@@ -42,7 +55,6 @@
android:exported="true"
android:foregroundServiceType="connectedDevice"
android:permission="android.permission.BLUETOOTH_CONNECT" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/android/app/src/main/java/me/kavishdevar/aln/AirPodsQSService.kt b/android/app/src/main/java/me/kavishdevar/aln/AirPodsQSService.kt
index c7562fc..12fe4bf 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/AirPodsQSService.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/AirPodsQSService.kt
@@ -10,8 +10,8 @@ import android.service.quicksettings.TileService
import android.util.Log
class AirPodsQSService: TileService() {
- private val ancModes = listOf(NoiseControlMode.OFF.name, NoiseControlMode.NOISE_CANCELLATION.name, NoiseControlMode.TRANSPARENCY.name, NoiseControlMode.ADAPTIVE.name)
- private var currentModeIndex = 3
+ private val ancModes = listOf(NoiseControlMode.NOISE_CANCELLATION.name, NoiseControlMode.TRANSPARENCY.name, NoiseControlMode.ADAPTIVE.name)
+ private var currentModeIndex = 2
private lateinit var ancStatusReceiver: BroadcastReceiver
private lateinit var availabilityReceiver: BroadcastReceiver
@@ -63,8 +63,24 @@ class AirPodsQSService: TileService() {
override fun onStopListening() {
super.onStopListening()
- unregisterReceiver(ancStatusReceiver)
- unregisterReceiver(availabilityReceiver)
+ try {
+ 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() {
diff --git a/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt
index 97aaf9f..ed7e839 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt
@@ -1,3 +1,5 @@
+@file:Suppress("unused")
+
package me.kavishdevar.aln
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)
}
+ 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) {
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2E, strength.toByte(), 0x00, 0x00, 0x00)
socket?.outputStream?.write(bytes)
diff --git a/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt
index 52ead13..35e2de5 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/AirPodsSettingsScreen.kt
@@ -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")
@Composable
fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?, service: AirPodsService?,
@@ -203,6 +230,11 @@ fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?
Spacer(modifier = Modifier.height(16.dp))
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))
// val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
diff --git a/android/app/src/main/java/me/kavishdevar/aln/CustomDevice.kt b/android/app/src/main/java/me/kavishdevar/aln/CustomDevice.kt
new file mode 100644
index 0000000..f88783a
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/aln/CustomDevice.kt
@@ -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")
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt
index 153d7e6..db84c3e 100644
--- a/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt
+++ b/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt
@@ -1,7 +1,6 @@
package me.kavishdevar.aln
import android.annotation.SuppressLint
-import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
@@ -43,6 +42,7 @@ import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
@@ -61,6 +61,43 @@ class MainActivity : ComponentActivity() {
setContent {
val topAppBarTitle = remember { mutableStateOf("AirPods Pro") }
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 (
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
0xFF000000
@@ -109,6 +146,7 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState) {
val airPodsService = remember { mutableStateOf(null) }
val navController = rememberNavController()
+
val disconnectReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
navController.navigate("notConnected")
@@ -165,42 +203,8 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState) {
}
}
- // 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.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()
}
@@ -230,6 +234,21 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState) {
}
}
+ 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
if (airpodsDevice.value != null) {
LaunchedEffect(Unit) {
diff --git a/android/app/src/main/java/me/kavishdevar/aln/receiver.kt b/android/app/src/main/java/me/kavishdevar/aln/receiver.kt
new file mode 100644
index 0000000..ab96720
--- /dev/null
+++ b/android/app/src/main/java/me/kavishdevar/aln/receiver.kt
@@ -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
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 03f6736..3e00038 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -1,4 +1,5 @@
ALN
DebugActivity
+ CustomDevice
\ No newline at end of file
diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml
index 0059329..e99426a 100644
--- a/android/gradle/libs.versions.toml
+++ b/android/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
[versions]
accompanistPermissions = "0.36.0"
-agp = "8.7.0"
+agp = "8.7.2"
hiddenapibypass = "4.3"
kotlin = "2.0.0"
coreKtx = "1.13.1"