implement music play/pause from ear detection

This commit is contained in:
Kavish Devar
2024-10-11 13:24:05 +05:30
parent 0487ea1f69
commit 81d07a7795
9 changed files with 196 additions and 20 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "me.kavishdevar.aln",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 28
}

View File

@@ -4,16 +4,22 @@ import android.annotation.SuppressLint
import android.app.Service import android.app.Service
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothSocket import android.bluetooth.BluetoothSocket
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.os.Binder import android.os.Binder
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.ParcelUuid import android.os.ParcelUuid
import android.util.Log import android.util.Log
import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.lsposed.hiddenapibypass.HiddenApiBypass import org.lsposed.hiddenapibypass.HiddenApiBypass
import kotlin.experimental.or
class AirPodsService : Service() { class AirPodsService : Service() {
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
@@ -27,6 +33,12 @@ class AirPodsService : Service() {
var isRunning: Boolean = false var isRunning: Boolean = false
private var socket: BluetoothSocket? = null private var socket: BluetoothSocket? = null
fun sendPacket(packet: String) {
val fromHex = packet.split(" ").map { it.toInt(16).toByte() }
socket?.outputStream?.write(fromHex.toByteArray())
socket?.outputStream?.flush()
}
fun setANCMode(mode: Int) { fun setANCMode(mode: Int) {
when (mode) { when (mode) {
1 -> { 1 -> {
@@ -50,14 +62,12 @@ class AirPodsService : Service() {
} }
fun setAdaptiveStrength(strength: Int) { fun setAdaptiveStrength(strength: Int) {
val bytes = byteArrayOf(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)
val hexString = bytes.joinToString(" ") { "%02X".format(it) }
Log.d("AirPodsService", "Adaptive Strength: $hexString")
socket?.outputStream?.write(bytes) socket?.outputStream?.write(bytes)
socket?.outputStream?.flush() socket?.outputStream?.flush()
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission", "InlinedApi")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (isRunning) { if (isRunning) {
return START_STICKY return START_STICKY
@@ -107,6 +117,33 @@ 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]}")
val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
val mediaController = MediaController(audioManager)
var inEar = false
val earReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val data = intent.getByteArrayExtra("data")
if (data != null) {
inEar = if (data.find { it == 0x02.toByte() } != null || data.find { it == 0x03.toByte() } != null) {
data[0] == 0x00.toByte() || data[1] == 0x00.toByte()
} else {
data[0] == 0x00.toByte() && data[1] == 0x00.toByte()
}
Log.d("AirPods Parser", "In Ear: $inEar")
if (inEar) {
mediaController.sendPlay()
}
else {
mediaController.sendPause()
}
}
}
}
val earIntentFilter = IntentFilter(Notifications.EAR_DETECTION_DATA)
this@AirPodsService.registerReceiver(earReceiver, earIntentFilter,
RECEIVER_EXPORTED
)
} }
else if (ancNotification.isANCData(data)) { else if (ancNotification.isANCData(data)) {
ancNotification.setStatus(data) ancNotification.setStatus(data)
@@ -129,6 +166,16 @@ class AirPodsService : Service() {
sendBroadcast(Intent(Notifications.CA_DATA).apply { sendBroadcast(Intent(Notifications.CA_DATA).apply {
putExtra("data", conversationAwarenessNotification.status) putExtra("data", conversationAwarenessNotification.status)
}) })
if (conversationAwarenessNotification.status == 1.toByte() or 2.toByte()) {
val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
val mediaController = MediaController(audioManager)
mediaController.startSpeaking()
}
else if (conversationAwarenessNotification.status == 9.toByte() or 8.toByte()) {
val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
val mediaController = MediaController(audioManager)
mediaController.stopSpeaking()
}
Log.d("AirPods Parser", "Conversation Awareness: ${conversationAwarenessNotification.status}") Log.d("AirPods Parser", "Conversation Awareness: ${conversationAwarenessNotification.status}")
} }
else { } else { }

View File

@@ -10,6 +10,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.ServiceConnection import android.content.ServiceConnection
import android.media.AudioManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
@@ -45,12 +46,14 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.VerticalDivider import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -63,6 +66,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha 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.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
@@ -86,6 +90,7 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale import com.google.accompanist.permissions.shouldShowRationale
import me.kavishdevar.aln.ui.theme.ALNTheme import me.kavishdevar.aln.ui.theme.ALNTheme
import kotlin.math.roundToInt
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -397,11 +402,15 @@ fun BatteryView() {
} }
else { else {
Row { Row {
Text(text = "\uDBC6\uDCE5", fontFamily = FontFamily(Font(R.font.sf_pro))) if (left?.status != BatteryStatus.DISCONNECTED) {
BatteryIndicator(left?.level ?: 0, left?.status == BatteryStatus.CHARGING) Text(text = "\uDBC6\uDCE5", fontFamily = FontFamily(Font(R.font.sf_pro)))
Spacer(modifier = Modifier.width(16.dp)) BatteryIndicator(left?.level ?: 0, left?.status == BatteryStatus.CHARGING)
Text(text = "\uDBC6\uDCE8", fontFamily = FontFamily(Font(R.font.sf_pro))) Spacer(modifier = Modifier.width(16.dp))
BatteryIndicator(right?.level ?: 0, right?.status == BatteryStatus.CHARGING) }
if (right?.status != BatteryStatus.DISCONNECTED) {
Text(text = "\uDBC6\uDCE8", fontFamily = FontFamily(Font(R.font.sf_pro)))
BatteryIndicator(right?.level ?: 0, right?.status == BatteryStatus.CHARGING)
}
} }
} }
} }
@@ -420,7 +429,6 @@ fun BatteryView() {
.fillMaxWidth() .fillMaxWidth()
) )
BatteryIndicator(case?.level ?: 0) BatteryIndicator(case?.level ?: 0)
} }
} }
} }
@@ -454,6 +462,7 @@ fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NoiseControlSlider(service: AirPodsService) { fun NoiseControlSlider(service: AirPodsService) {
val sliderValue = remember { mutableStateOf(0f) } val sliderValue = remember { mutableStateOf(0f) }
@@ -475,17 +484,37 @@ fun NoiseControlSlider(service: AirPodsService) {
value = sliderValue.value, value = sliderValue.value,
onValueChange = { onValueChange = {
sliderValue.value = it sliderValue.value = it
service.setAdaptiveStrength(it.toInt()) service.setAdaptiveStrength(100 - it.toInt())
}, },
valueRange = 0f..100f, valueRange = 0f..100f,
steps = 3, onValueChangeFinished = {
// Round the value when the user stops sliding
sliderValue.value = sliderValue.value.roundToInt().toFloat()
},
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth()
.height(36.dp), // Adjust height to ensure thumb fits well
colors = SliderDefaults.colors( colors = SliderDefaults.colors(
thumbColor = thumbColor, thumbColor = thumbColor,
activeTrackColor = activeTrackColor, activeTrackColor = activeTrackColor,
inactiveTrackColor = trackColor inactiveTrackColor = trackColor
) ),
thumb = {
Box(
modifier = Modifier
.size(24.dp) // Circular thumb size
.shadow(4.dp, CircleShape) // Apply shadow to the thumb
.background(thumbColor, CircleShape) // Circular thumb
)
},
track = {
Box(
modifier = Modifier
.fillMaxWidth()
.height(12.dp)
.background(trackColor, RoundedCornerShape(6.dp))
)
}
) )
// Labels // Labels
@@ -599,7 +628,30 @@ fun AudioSettings(service: AirPodsService) {
color = textColor.copy(alpha = 0.6f) color = textColor.copy(alpha = 0.6f)
) )
) )
NoiseControlSlider(service = service) NoiseControlSlider(service = service)
val packet = remember { mutableStateOf ("") }
Row (
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
TextField(
value = packet.value,
onValueChange = { packet.value = it },
modifier = Modifier.fillMaxWidth(0.75f),
)
Button(onClick = {
service.sendPacket(packet.value)
},
modifier = Modifier
.padding(start = 8.dp)
.fillMaxWidth()
) {
Text(text = "Send")
}
}
} }
} }
} }

View File

@@ -0,0 +1,36 @@
package me.kavishdevar.aln
import android.media.AudioManager
import android.view.KeyEvent
class MediaController (private val audioManager: AudioManager){
fun sendPause() {
if (audioManager.isMusicActive) {
audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE))
audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PAUSE))
}
}
fun sendPlay() {
if (!audioManager.isMusicActive) {
audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY))
audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY))
}
}
var initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
fun startSpeaking() {
if (!audioManager.isMusicActive) {
// reduce volume to 10% of initial volume
initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (initialVolume * 0.1).toInt(), 0)
}
}
fun stopSpeaking() {
if (!audioManager.isMusicActive) {
// restore initial volume
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, initialVolume, 0)
}
}
}

View File

@@ -135,7 +135,11 @@ class Notifications {
fun setBattery(data: ByteArray) { fun setBattery(data: ByteArray) {
first = Battery(data[7].toInt(), data[9].toInt(), data[10].toInt()) first = Battery(data[7].toInt(), data[9].toInt(), data[10].toInt())
second = Battery(data[12].toInt(), data[14].toInt(), data[15].toInt()) second = Battery(data[12].toInt(), data[14].toInt(), data[15].toInt())
case = Battery(data[17].toInt(), data[19].toInt(), data[20].toInt()) case = if (data[20].toInt() == BatteryStatus.DISCONNECTED && case.status != BatteryStatus.DISCONNECTED) {
Battery(data[17].toInt(), case.level, data[20].toInt())
} else {
Battery(data[17].toInt(), data[19].toInt(), data[20].toInt())
}
} }
fun getBattery(): List<Battery> { fun getBattery(): List<Battery> {

View File

@@ -1,16 +1,16 @@
[versions] [versions]
accompanistPermissions = "0.36.0" accompanistPermissions = "0.36.0"
agp = "8.7.0-rc01" agp = "8.7.0"
hiddenapibypass = "4.3" hiddenapibypass = "4.3"
kotlin = "2.0.0" kotlin = "2.0.0"
coreKtx = "1.13.1" coreKtx = "1.13.1"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.2.1" junitVersion = "1.2.1"
espressoCore = "3.6.1" espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.5" lifecycleRuntimeKtx = "2.8.6"
activityCompose = "1.9.2" activityCompose = "1.9.2"
composeBom = "2024.04.01" composeBom = "2024.09.03"
annotations = "15.0" annotations = "26.0.0"
[libraries] [libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }

View File

@@ -1,6 +1,6 @@
#Mon Oct 07 22:30:36 IST 2024 #Mon Oct 07 22:30:36 IST 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists