mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-28 22:01:50 +00:00
implement music play/pause from ear detection
This commit is contained in:
BIN
android/app/release/baselineProfiles/0/app-release.dm
Normal file
BIN
android/app/release/baselineProfiles/0/app-release.dm
Normal file
Binary file not shown.
BIN
android/app/release/baselineProfiles/1/app-release.dm
Normal file
BIN
android/app/release/baselineProfiles/1/app-release.dm
Normal file
Binary file not shown.
37
android/app/release/output-metadata.json
Normal file
37
android/app/release/output-metadata.json
Normal 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
|
||||
}
|
||||
@@ -4,16 +4,22 @@ import android.annotation.SuppressLint
|
||||
import android.app.Service
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.media.AudioManager
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||
import kotlin.experimental.or
|
||||
|
||||
class AirPodsService : Service() {
|
||||
inner class LocalBinder : Binder() {
|
||||
@@ -27,6 +33,12 @@ class AirPodsService : Service() {
|
||||
var isRunning: Boolean = false
|
||||
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) {
|
||||
when (mode) {
|
||||
1 -> {
|
||||
@@ -50,14 +62,12 @@ class AirPodsService : Service() {
|
||||
}
|
||||
|
||||
fun setAdaptiveStrength(strength: Int) {
|
||||
val bytes = byteArrayOf(0x00, 0x04, 0x00, 0x09, 0x00, 0x2E, strength.toByte(), 0x00, 0x00, 0x00)
|
||||
val hexString = bytes.joinToString(" ") { "%02X".format(it) }
|
||||
Log.d("AirPodsService", "Adaptive Strength: $hexString")
|
||||
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2E, strength.toByte(), 0x00, 0x00, 0x00)
|
||||
socket?.outputStream?.write(bytes)
|
||||
socket?.outputStream?.flush()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@SuppressLint("MissingPermission", "InlinedApi")
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (isRunning) {
|
||||
return START_STICKY
|
||||
@@ -107,6 +117,33 @@ class AirPodsService : Service() {
|
||||
putExtra("data", bytes)
|
||||
})
|
||||
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)) {
|
||||
ancNotification.setStatus(data)
|
||||
@@ -129,6 +166,16 @@ class AirPodsService : Service() {
|
||||
sendBroadcast(Intent(Notifications.CA_DATA).apply {
|
||||
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}")
|
||||
}
|
||||
else { }
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
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.filled.Person
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
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.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
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.shouldShowRationale
|
||||
import me.kavishdevar.aln.ui.theme.ALNTheme
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -397,11 +402,15 @@ fun BatteryView() {
|
||||
}
|
||||
else {
|
||||
Row {
|
||||
Text(text = "\uDBC6\uDCE5", fontFamily = FontFamily(Font(R.font.sf_pro)))
|
||||
BatteryIndicator(left?.level ?: 0, left?.status == BatteryStatus.CHARGING)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(text = "\uDBC6\uDCE8", fontFamily = FontFamily(Font(R.font.sf_pro)))
|
||||
BatteryIndicator(right?.level ?: 0, right?.status == BatteryStatus.CHARGING)
|
||||
if (left?.status != BatteryStatus.DISCONNECTED) {
|
||||
Text(text = "\uDBC6\uDCE5", fontFamily = FontFamily(Font(R.font.sf_pro)))
|
||||
BatteryIndicator(left?.level ?: 0, left?.status == BatteryStatus.CHARGING)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
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()
|
||||
)
|
||||
BatteryIndicator(case?.level ?: 0)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,6 +462,7 @@ fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NoiseControlSlider(service: AirPodsService) {
|
||||
val sliderValue = remember { mutableStateOf(0f) }
|
||||
@@ -475,17 +484,37 @@ fun NoiseControlSlider(service: AirPodsService) {
|
||||
value = sliderValue.value,
|
||||
onValueChange = {
|
||||
sliderValue.value = it
|
||||
service.setAdaptiveStrength(it.toInt())
|
||||
service.setAdaptiveStrength(100 - it.toInt())
|
||||
},
|
||||
valueRange = 0f..100f,
|
||||
steps = 3,
|
||||
onValueChangeFinished = {
|
||||
// Round the value when the user stops sliding
|
||||
sliderValue.value = sliderValue.value.roundToInt().toFloat()
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.height(36.dp), // Adjust height to ensure thumb fits well
|
||||
colors = SliderDefaults.colors(
|
||||
thumbColor = thumbColor,
|
||||
activeTrackColor = activeTrackColor,
|
||||
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
|
||||
@@ -599,7 +628,30 @@ fun AudioSettings(service: AirPodsService) {
|
||||
color = textColor.copy(alpha = 0.6f)
|
||||
)
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,11 @@ class Notifications {
|
||||
fun setBattery(data: ByteArray) {
|
||||
first = Battery(data[7].toInt(), data[9].toInt(), data[10].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> {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
[versions]
|
||||
accompanistPermissions = "0.36.0"
|
||||
agp = "8.7.0-rc01"
|
||||
agp = "8.7.0"
|
||||
hiddenapibypass = "4.3"
|
||||
kotlin = "2.0.0"
|
||||
coreKtx = "1.13.1"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.2.1"
|
||||
espressoCore = "3.6.1"
|
||||
lifecycleRuntimeKtx = "2.8.5"
|
||||
lifecycleRuntimeKtx = "2.8.6"
|
||||
activityCompose = "1.9.2"
|
||||
composeBom = "2024.04.01"
|
||||
annotations = "15.0"
|
||||
composeBom = "2024.09.03"
|
||||
annotations = "26.0.0"
|
||||
|
||||
[libraries]
|
||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#Mon Oct 07 22:30:36 IST 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Reference in New Issue
Block a user