mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-27 00:23:30 +00:00
enable noise control functionality, and battery
This commit is contained in:
@@ -2,6 +2,7 @@ plugins {
|
|||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -10,8 +11,8 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "me.kavishdevar.aln"
|
applicationId = "me.kavishdevar.aln"
|
||||||
minSdk = 22
|
minSdk = 28
|
||||||
targetSdk = 34
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
@@ -40,7 +41,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(libs.accompanist.permissions)
|
||||||
|
implementation(libs.hiddenapibypass)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|||||||
@@ -20,10 +20,16 @@
|
|||||||
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>
|
||||||
|
<service
|
||||||
|
android:name=".AirPodsService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:foregroundServiceType="connectedDevice"
|
||||||
|
android:permission="android.permission.BLUETOOTH_CONNECT">
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
153
android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt
Normal file
153
android/app/src/main/java/me/kavishdevar/aln/AirPodsService.kt
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Service
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothSocket
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Binder
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.ParcelUuid
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
|
|
||||||
|
class AirPodsService : Service() {
|
||||||
|
inner class LocalBinder : Binder() {
|
||||||
|
fun getService(): AirPodsService = this@AirPodsService
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder {
|
||||||
|
return LocalBinder()
|
||||||
|
}
|
||||||
|
|
||||||
|
var isRunning: Boolean = false
|
||||||
|
private var socket: BluetoothSocket? = null
|
||||||
|
|
||||||
|
fun setANCMode(mode: Int) {
|
||||||
|
when (mode) {
|
||||||
|
1 -> {
|
||||||
|
socket?.outputStream?.write(Enums.NOISE_CANCELLATION_OFF.value)
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
socket?.outputStream?.write(Enums.NOISE_CANCELLATION_ON.value)
|
||||||
|
}
|
||||||
|
3 -> {
|
||||||
|
socket?.outputStream?.write(Enums.NOISE_CANCELLATION_TRANSPARENCY.value)
|
||||||
|
}
|
||||||
|
4 -> {
|
||||||
|
socket?.outputStream?.write(Enums.NOISE_CANCELLATION_ADAPTIVE.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCAEnabled(enabled: Boolean) {
|
||||||
|
socket?.outputStream?.write(if (enabled) Enums.SET_CONVERSATION_AWARENESS_ON.value else Enums.SET_CONVERSATION_AWARENESS_OFF.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (isRunning) {
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
isRunning = true
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION") val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) intent?.getParcelableExtra("device", BluetoothDevice::class.java) else intent?.getParcelableExtra("device")
|
||||||
|
|
||||||
|
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
|
||||||
|
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||||
|
socket = HiddenApiBypass.newInstance(BluetoothSocket::class.java, 3, true, true, device, 0x1001, uuid) as BluetoothSocket?
|
||||||
|
try {
|
||||||
|
socket?.connect()
|
||||||
|
socket?.let { it ->
|
||||||
|
it.outputStream.write(Enums.HANDSHAKE.value)
|
||||||
|
it.outputStream.write(Enums.SET_SPECIFIC_FEATURES.value)
|
||||||
|
it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value)
|
||||||
|
sendBroadcast(Intent(Notifications.AIRPODS_CONNECTED))
|
||||||
|
it.outputStream.flush()
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val earDetectionNotification = Notifications.EarDetection()
|
||||||
|
val ancNotification = Notifications.ANC()
|
||||||
|
val batteryNotification = Notifications.BatteryNotification()
|
||||||
|
val conversationAwarenessNotification = Notifications.ConversationalAwarenessNotification()
|
||||||
|
|
||||||
|
while (socket?.isConnected == true) {
|
||||||
|
socket?.let {
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
val bytesRead = it.inputStream.read(buffer)
|
||||||
|
val data = buffer.copyOfRange(0, bytesRead)
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
sendBroadcast(Intent(Notifications.AIRPODS_DATA).apply {
|
||||||
|
putExtra("data", buffer.copyOfRange(0, bytesRead))
|
||||||
|
})
|
||||||
|
val bytes = buffer.copyOfRange(0, bytesRead)
|
||||||
|
val formattedHex = bytes.joinToString(" ") { "%02X".format(it) }
|
||||||
|
Log.d("AirPods Data", "Data received: $formattedHex")
|
||||||
|
}
|
||||||
|
if (earDetectionNotification.isEarDetectionData(data)) {
|
||||||
|
earDetectionNotification.setStatus(data)
|
||||||
|
sendBroadcast(Intent(Notifications.EAR_DETECTION_DATA).apply {
|
||||||
|
val list = earDetectionNotification.status
|
||||||
|
val bytes = ByteArray(2)
|
||||||
|
bytes[0] = list[0]
|
||||||
|
bytes[1] = list[1]
|
||||||
|
putExtra("data", bytes)
|
||||||
|
})
|
||||||
|
Log.d("AirPods Parser", "Ear Detection: ${earDetectionNotification.status[0]} ${earDetectionNotification.status[1]}")
|
||||||
|
}
|
||||||
|
else if (ancNotification.isANCData(data)) {
|
||||||
|
ancNotification.setStatus(data)
|
||||||
|
sendBroadcast(Intent(Notifications.ANC_DATA).apply {
|
||||||
|
putExtra("data", ancNotification.status)
|
||||||
|
})
|
||||||
|
Log.d("AirPods Parser", "ANC: ${ancNotification.status}")
|
||||||
|
}
|
||||||
|
else if (batteryNotification.isBatteryData(data)) {
|
||||||
|
batteryNotification.setBattery(data)
|
||||||
|
sendBroadcast(Intent(Notifications.BATTERY_DATA).apply {
|
||||||
|
putParcelableArrayListExtra("data", ArrayList(batteryNotification.getBattery()))
|
||||||
|
})
|
||||||
|
for (battery in batteryNotification.getBattery()) {
|
||||||
|
Log.d("AirPods Parser", "${battery.getComponentName()}: ${battery.getStatusName()} at ${battery.level}% ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (conversationAwarenessNotification.isConversationalAwarenessData(data)) {
|
||||||
|
conversationAwarenessNotification.setData(data)
|
||||||
|
sendBroadcast(Intent(Notifications.CA_DATA).apply {
|
||||||
|
putExtra("data", conversationAwarenessNotification.status)
|
||||||
|
})
|
||||||
|
Log.d("AirPods Parser", "Conversation Awareness: ${conversationAwarenessNotification.status}")
|
||||||
|
}
|
||||||
|
else { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d("AirPods Service", "Socket closed")
|
||||||
|
isRunning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e: Exception) {
|
||||||
|
Log.e("AirPodsSettingsScreen", "Error connecting to device: ${e.message}")
|
||||||
|
}
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
socket?.close()
|
||||||
|
isRunning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,21 @@ package me.kavishdevar.aln
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.content.res.Configuration
|
import android.bluetooth.BluetoothProfile
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.content.ServiceConnection
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -37,6 +44,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.text.BasicTextField
|
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.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
|
||||||
@@ -45,6 +53,7 @@ import androidx.compose.material3.SliderDefaults
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
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.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -53,43 +62,49 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
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.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
|
||||||
import androidx.compose.ui.graphics.luminance
|
import androidx.compose.ui.graphics.luminance
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.imageResource
|
import androidx.compose.ui.res.imageResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
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 me.kavishdevar.aln.ui.theme.ALNTheme
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
val address = mutableStateOf("28:2D:7F:C2:05:5B")
|
|
||||||
val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
|
|
||||||
val bluetoothAdapter = bluetoothManager.adapter
|
|
||||||
val device = bluetoothAdapter.getRemoteDevice(address.value)
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
ALNTheme {
|
ALNTheme {
|
||||||
Scaffold { innerPadding ->
|
Scaffold (
|
||||||
AirPodsSettingsScreen(innerPadding, device)
|
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF000000) else Color(0xFFFFFFFF)
|
||||||
|
) { innerPadding ->
|
||||||
|
Main(innerPadding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UseOfNonLambdaOffsetOverload")
|
||||||
@Composable
|
@Composable
|
||||||
fun StyledSwitch(
|
fun StyledSwitch(
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
@@ -131,7 +146,7 @@ fun StyledTextField(
|
|||||||
) {
|
) {
|
||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF0E0E0E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF252525) 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 (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
@@ -178,7 +193,7 @@ fun StyledTextField(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BatteryIndicator(batteryPercentage: Int) {
|
fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) {
|
||||||
val batteryOutlineColor = Color(0xFFBFBFBF) // Light gray outline
|
val batteryOutlineColor = Color(0xFFBFBFBF) // Light gray outline
|
||||||
val batteryFillColor = if (batteryPercentage > 30) Color(0xFF30D158) else Color(0xFFFC3C3C)
|
val batteryFillColor = if (batteryPercentage > 30) Color(0xFF30D158) else Color(0xFFFC3C3C)
|
||||||
val batteryTextColor = MaterialTheme.colorScheme.onSurface
|
val batteryTextColor = MaterialTheme.colorScheme.onSurface
|
||||||
@@ -187,7 +202,7 @@ fun BatteryIndicator(batteryPercentage: Int) {
|
|||||||
val batteryWidth = 30.dp
|
val batteryWidth = 30.dp
|
||||||
val batteryHeight = 15.dp
|
val batteryHeight = 15.dp
|
||||||
val batteryCornerRadius = 4.dp
|
val batteryCornerRadius = 4.dp
|
||||||
val tipWidth = 3.dp
|
val tipWidth = 5.dp
|
||||||
val tipHeight = batteryHeight * 0.3f
|
val tipHeight = batteryHeight * 0.3f
|
||||||
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
@@ -204,7 +219,6 @@ fun BatteryIndicator(batteryPercentage: Int) {
|
|||||||
.height(batteryHeight)
|
.height(batteryHeight)
|
||||||
.border(1.dp, batteryOutlineColor, RoundedCornerShape(batteryCornerRadius))
|
.border(1.dp, batteryOutlineColor, RoundedCornerShape(batteryCornerRadius))
|
||||||
) {
|
) {
|
||||||
// Battery Fill
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
@@ -212,6 +226,21 @@ fun BatteryIndicator(batteryPercentage: Int) {
|
|||||||
.width(batteryWidth * (batteryPercentage / 100f))
|
.width(batteryWidth * (batteryPercentage / 100f))
|
||||||
.background(batteryFillColor, RoundedCornerShape(2.dp))
|
.background(batteryFillColor, RoundedCornerShape(2.dp))
|
||||||
)
|
)
|
||||||
|
if (charging) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(), // Take up the entire size of the outer Box
|
||||||
|
contentAlignment = Alignment.Center // Center the charging bolt within the Box
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "\uDBC0\uDEE6",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Battery Tip (Protrusion)
|
// Battery Tip (Protrusion)
|
||||||
@@ -224,9 +253,9 @@ fun BatteryIndicator(batteryPercentage: Int) {
|
|||||||
batteryOutlineColor,
|
batteryOutlineColor,
|
||||||
RoundedCornerShape(
|
RoundedCornerShape(
|
||||||
topStart = 0.dp,
|
topStart = 0.dp,
|
||||||
topEnd = 5.dp,
|
topEnd = 12.dp,
|
||||||
bottomStart = 5.dp,
|
bottomStart = 0.dp,
|
||||||
bottomEnd = 4.dp
|
bottomEnd = 12.dp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -241,41 +270,165 @@ fun BatteryIndicator(batteryPercentage: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
@Composable
|
||||||
|
fun Main(paddingValues: PaddingValues) {
|
||||||
|
val bluetoothConnectPermissionState = rememberPermissionState(
|
||||||
|
permission = "android.permission.BLUETOOTH_CONNECT"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (bluetoothConnectPermissionState.status.isGranted) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||||
|
val bluetoothManager = getSystemService(context, BluetoothManager::class.java)
|
||||||
|
val bluetoothAdapter = bluetoothManager?.adapter
|
||||||
|
val devices = bluetoothAdapter?.bondedDevices
|
||||||
|
val airpodsDevice = remember { mutableStateOf<BluetoothDevice?>(null) }
|
||||||
|
if (devices != null) {
|
||||||
|
for (device in devices) {
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
|
||||||
|
|
||||||
|
val serviceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||||
|
val binder = service as AirPodsService.LocalBinder
|
||||||
|
airPodsService.value = binder.getService()
|
||||||
|
Log.d("AirPodsService", "Service connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName) {
|
||||||
|
airPodsService.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val intent = Intent(context, AirPodsService::class.java)
|
||||||
|
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
|
||||||
|
if (airpodsDevice.value != null)
|
||||||
|
{
|
||||||
|
AirPodsSettingsScreen(
|
||||||
|
paddingValues,
|
||||||
|
airpodsDevice.value,
|
||||||
|
service = airPodsService.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Text("No AirPods connected")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// Permission is not granted, request it
|
||||||
|
Column (
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
){
|
||||||
|
val textToShow = if (bluetoothConnectPermissionState.status.shouldShowRationale) {
|
||||||
|
// If the user has denied the permission but not permanently, explain why it's needed.
|
||||||
|
"The BLUETOOTH_CONNECT permission is important for this app. Please grant it to proceed."
|
||||||
|
} else {
|
||||||
|
// If the user has permanently denied the permission, inform them to enable it in settings.
|
||||||
|
"BLUETOOTH_CONNECT permission required for this feature. Please enable it in settings."
|
||||||
|
}
|
||||||
|
Text(textToShow)
|
||||||
|
Button(onClick = { bluetoothConnectPermissionState.launchPermissionRequest() }) {
|
||||||
|
Text("Request permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BatteryView() {
|
||||||
|
val batteryStatus = remember { mutableStateOf<List<Battery>>(listOf()) }
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION") val batteryReceiver = remember {
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
batteryStatus.value = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableArrayListExtra("data", Battery::class.java) } else { intent.getParcelableArrayListExtra("data") }?.toList() ?: listOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
LaunchedEffect(context) {
|
||||||
|
val batteryIntentFilter = IntentFilter(Notifications.BATTERY_DATA)
|
||||||
|
context.registerReceiver(batteryReceiver, batteryIntentFilter, Context.RECEIVER_EXPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
Column (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.5f),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Image (
|
||||||
|
bitmap = ImageBitmap.imageResource(R.drawable.pro_2_buds),
|
||||||
|
contentDescription = "Buds",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.scale(0.50f)
|
||||||
|
)
|
||||||
|
val left = batteryStatus.value.find { it.component == BatteryComponent.LEFT }
|
||||||
|
val right = batteryStatus.value.find { it.component == BatteryComponent.RIGHT }
|
||||||
|
if ((right?.status == BatteryStatus.CHARGING && left?.status == BatteryStatus.CHARGING) || (left?.status == BatteryStatus.NOT_CHARGING && right?.status == BatteryStatus.NOT_CHARGING))
|
||||||
|
{
|
||||||
|
BatteryIndicator(right.level.let { left.level.coerceAtMost(it) }, left.status == BatteryStatus.CHARGING)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column (
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
val case = batteryStatus.value.find { it.component == BatteryComponent.CASE }
|
||||||
|
|
||||||
|
Image(
|
||||||
|
bitmap = ImageBitmap.imageResource(R.drawable.pro_2_case),
|
||||||
|
contentDescription = "Case",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
BatteryIndicator(case?.level ?: 0)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission", "NewApi")
|
@SuppressLint("MissingPermission", "NewApi")
|
||||||
@Composable
|
@Composable
|
||||||
fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?) {
|
fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?, service: AirPodsService?) {
|
||||||
var deviceName by remember { mutableStateOf(TextFieldValue(device?.name ?: "Kavish's AirPods Pro (Fallback)")) }
|
var deviceName by remember { mutableStateOf(TextFieldValue(device?.name ?: "AirPods Pro (fallback, should never show up)")) }
|
||||||
val channel = device?.createL2capChannel(0x1001)
|
|
||||||
val connected = remember { mutableStateOf(false) }
|
|
||||||
try {
|
|
||||||
channel?.connect()
|
|
||||||
channel?.let { it ->
|
|
||||||
var message = "00 00 04 00 01 00 02 00 00 00 00 00 00 00 00 00"
|
|
||||||
var bytes = message.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
|
||||||
it.outputStream.write(bytes)
|
|
||||||
Log.d("AirPodsSettingsScreen", "Message sent: $message")
|
|
||||||
|
|
||||||
message = "04 00 04 00 4d 00 ff 00 00 00 00 00 00 00"
|
|
||||||
bytes = message.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
|
||||||
it.outputStream.write(bytes)
|
|
||||||
Log.d("AirPodsSettingsScreen", "Message sent: $message")
|
|
||||||
|
|
||||||
message = "04 00 04 00 0F 00 FF FF FE FF"
|
|
||||||
bytes = message.split(" ").map { it.toInt(16).toByte() }.toByteArray()
|
|
||||||
it.outputStream.write(bytes)
|
|
||||||
Log.d("AirPodsSettingsScreen", "Message sent: $message")
|
|
||||||
connected.value = true
|
|
||||||
it.outputStream.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e: Exception) {
|
|
||||||
Log.e("AirPodsSettingsScreen", "Error connecting to device: ${e.message}")
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
channel?.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(text = "Connected ${connected.value}")
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -283,29 +436,8 @@ fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.padding(vertical = 24.dp, horizontal = 12.dp)
|
.padding(vertical = 24.dp, horizontal = 12.dp)
|
||||||
) {
|
) {
|
||||||
Row {
|
BatteryView()
|
||||||
Column (
|
if (service != null) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
){
|
|
||||||
// using this temporarily until i can find an image of only the buds
|
|
||||||
Image(
|
|
||||||
bitmap = ImageBitmap.imageResource(R.drawable.pro_2),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.fillMaxWidth(0.5f)
|
|
||||||
)
|
|
||||||
BatteryIndicator(batteryPercentage = 10)
|
|
||||||
}
|
|
||||||
Column (
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
){
|
|
||||||
Image(
|
|
||||||
bitmap = ImageBitmap.imageResource(R.drawable.pro_2),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
BatteryIndicator(batteryPercentage = 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StyledTextField(
|
StyledTextField(
|
||||||
name = "Name",
|
name = "Name",
|
||||||
value = deviceName.text,
|
value = deviceName.text,
|
||||||
@@ -313,14 +445,17 @@ fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?
|
|||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
NoiseControlSettings()
|
|
||||||
|
NoiseControlSettings(service = service)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
AudioSettings()
|
AudioSettings(service = service)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NoiseControlSlider() {
|
fun NoiseControlSlider(service: AirPodsService) {
|
||||||
val sliderValue = remember { mutableStateOf(0f) }
|
val sliderValue = remember { mutableStateOf(0f) }
|
||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
|
||||||
@@ -338,7 +473,10 @@ fun NoiseControlSlider() {
|
|||||||
// Slider
|
// Slider
|
||||||
Slider(
|
Slider(
|
||||||
value = sliderValue.value,
|
value = sliderValue.value,
|
||||||
onValueChange = { sliderValue.value = it },
|
onValueChange = {
|
||||||
|
sliderValue.value = it
|
||||||
|
service.setAdaptiveStrength(it.toInt())
|
||||||
|
},
|
||||||
valueRange = 0f..100f,
|
valueRange = 0f..100f,
|
||||||
steps = 99,
|
steps = 99,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -378,7 +516,7 @@ fun NoiseControlSlider() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AudioSettings() {
|
fun AudioSettings(service: AirPodsService) {
|
||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
var conversationalAwarenessEnabled by remember { mutableStateOf(true) }
|
var conversationalAwarenessEnabled by remember { mutableStateOf(true) }
|
||||||
@@ -392,7 +530,7 @@ fun AudioSettings() {
|
|||||||
),
|
),
|
||||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||||
)
|
)
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF0E0E0E) else Color(0xFFFFFFFF)
|
val backgroundColor = if (isDarkTheme) Color(0xFF252525) else Color(0xFFFFFFFF)
|
||||||
val isPressed = remember { mutableStateOf(false) }
|
val isPressed = remember { mutableStateOf(false) }
|
||||||
Column (
|
Column (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -425,10 +563,14 @@ fun AudioSettings() {
|
|||||||
},
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(text = "Conversational Awareness", modifier = Modifier.weight(1f), fontSize = 16.sp)
|
Text(text = "Conversational Awareness", modifier = Modifier.weight(1f), fontSize = 16.sp, color = textColor)
|
||||||
|
|
||||||
StyledSwitch(
|
StyledSwitch(
|
||||||
checked = conversationalAwarenessEnabled,
|
checked = conversationalAwarenessEnabled,
|
||||||
onCheckedChange = { conversationalAwarenessEnabled = it },
|
onCheckedChange = {
|
||||||
|
conversationalAwarenessEnabled = it
|
||||||
|
service.setCAEnabled(it)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column (
|
Column (
|
||||||
@@ -457,15 +599,14 @@ fun AudioSettings() {
|
|||||||
color = textColor.copy(alpha = 0.6f)
|
color = textColor.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
NoiseControlSlider()
|
NoiseControlSlider(service = service)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NoiseControlSettings() {
|
fun NoiseControlSettings(service: AirPodsService) {
|
||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7)
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7)
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
val textColorSelected = if (isDarkTheme) Color.White else Color.Black
|
val textColorSelected = if (isDarkTheme) Color.White else Color.Black
|
||||||
@@ -473,6 +614,19 @@ fun NoiseControlSettings() {
|
|||||||
|
|
||||||
val noiseControlMode = remember { mutableStateOf(NoiseControlMode.OFF) }
|
val noiseControlMode = remember { mutableStateOf(NoiseControlMode.OFF) }
|
||||||
|
|
||||||
|
val noiseControlReceiver = remember {
|
||||||
|
object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
noiseControlMode.value = NoiseControlMode.entries.toTypedArray()[intent.getIntExtra("data", 3) - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val context = LocalContext.current
|
||||||
|
LaunchedEffect(context) {
|
||||||
|
val noiseControlIntentFilter = IntentFilter(Notifications.ANC_DATA)
|
||||||
|
context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter, Context.RECEIVER_EXPORTED)
|
||||||
|
}
|
||||||
|
|
||||||
// val paddingAnim by animateDpAsState(
|
// val paddingAnim by animateDpAsState(
|
||||||
// targetValue = when (noiseControlMode.value) {
|
// targetValue = when (noiseControlMode.value) {
|
||||||
// NoiseControlMode.OFF -> 0.dp
|
// NoiseControlMode.OFF -> 0.dp
|
||||||
@@ -488,6 +642,7 @@ fun NoiseControlSettings() {
|
|||||||
|
|
||||||
fun onModeSelected(mode: NoiseControlMode) {
|
fun onModeSelected(mode: NoiseControlMode) {
|
||||||
noiseControlMode.value = mode
|
noiseControlMode.value = mode
|
||||||
|
service.setANCMode(mode.ordinal+1)
|
||||||
when (mode) {
|
when (mode) {
|
||||||
NoiseControlMode.NOISE_CANCELLATION -> {
|
NoiseControlMode.NOISE_CANCELLATION -> {
|
||||||
d1a.value = 1f
|
d1a.value = 1f
|
||||||
@@ -664,14 +819,11 @@ fun NoiseControlButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class NoiseControlMode {
|
enum class NoiseControlMode {
|
||||||
OFF, TRANSPARENCY, ADAPTIVE, NOISE_CANCELLATION
|
OFF, NOISE_CANCELLATION, TRANSPARENCY, ADAPTIVE
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true, name = "AirPods Settings",
|
@Preview
|
||||||
uiMode = Configuration.UI_MODE_NIGHT_YES, showSystemUi = true,
|
|
||||||
device = "spec:width=411dp,height=891dp"
|
|
||||||
)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewAirPodsSettingsScreen() {
|
fun PreviewAirPodsSettingsScreen() {
|
||||||
AirPodsSettingsScreen(PaddingValues(8.dp), null)
|
BatteryIndicator(100, true)
|
||||||
}
|
}
|
||||||
|
|||||||
188
android/app/src/main/java/me/kavishdevar/aln/Packets.kt
Normal file
188
android/app/src/main/java/me/kavishdevar/aln/Packets.kt
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
enum class Enums(val value: ByteArray) {
|
||||||
|
NOISE_CANCELLATION(Capabilities.NOISE_CANCELLATION),
|
||||||
|
CONVERSATION_AWARENESS(Capabilities.CONVERSATION_AWARENESS),
|
||||||
|
CUSTOMIZABLE_ADAPTIVE_TRANSPARENCY(Capabilities.CUSTOMIZABLE_ADAPTIVE_TRANSPARENCY),
|
||||||
|
PREFIX(byteArrayOf(0x04, 0x00, 0x04, 0x00)),
|
||||||
|
SETTINGS(byteArrayOf(0x09, 0x00)),
|
||||||
|
SUFFIX(byteArrayOf(0x00, 0x00, 0x00)),
|
||||||
|
NOTIFICATION_FILTER(byteArrayOf(0x0f)),
|
||||||
|
HANDSHAKE(byteArrayOf(0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)),
|
||||||
|
SPECIFIC_FEATURES(byteArrayOf(0x4d)),
|
||||||
|
SET_SPECIFIC_FEATURES(PREFIX.value + SPECIFIC_FEATURES.value + byteArrayOf(0x00,
|
||||||
|
0xff.toByte(), 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)),
|
||||||
|
REQUEST_NOTIFICATIONS(PREFIX.value + NOTIFICATION_FILTER.value + byteArrayOf(0x00, 0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte())),
|
||||||
|
NOISE_CANCELLATION_PREFIX(PREFIX.value + SETTINGS.value + NOISE_CANCELLATION.value),
|
||||||
|
NOISE_CANCELLATION_OFF(NOISE_CANCELLATION_PREFIX.value + Capabilities.NoiseCancellation.OFF.value + SUFFIX.value),
|
||||||
|
NOISE_CANCELLATION_ON(NOISE_CANCELLATION_PREFIX.value + Capabilities.NoiseCancellation.ON.value + SUFFIX.value),
|
||||||
|
NOISE_CANCELLATION_TRANSPARENCY(NOISE_CANCELLATION_PREFIX.value + Capabilities.NoiseCancellation.TRANSPARENCY.value + SUFFIX.value),
|
||||||
|
NOISE_CANCELLATION_ADAPTIVE(NOISE_CANCELLATION_PREFIX.value + Capabilities.NoiseCancellation.ADAPTIVE.value + SUFFIX.value),
|
||||||
|
SET_CONVERSATION_AWARENESS_OFF(PREFIX.value + SETTINGS.value + CONVERSATION_AWARENESS.value + Capabilities.ConversationAwareness.OFF.value + SUFFIX.value),
|
||||||
|
SET_CONVERSATION_AWARENESS_ON(PREFIX.value + SETTINGS.value + CONVERSATION_AWARENESS.value + Capabilities.ConversationAwareness.ON.value + SUFFIX.value),
|
||||||
|
CONVERSATION_AWARENESS_RECEIVE_PREFIX(PREFIX.value + byteArrayOf(0x4b, 0x00, 0x02, 0x00));
|
||||||
|
}
|
||||||
|
|
||||||
|
object BatteryComponent {
|
||||||
|
const val LEFT = 4
|
||||||
|
const val RIGHT = 2
|
||||||
|
const val CASE = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
object BatteryStatus {
|
||||||
|
const val CHARGING = 1
|
||||||
|
const val NOT_CHARGING = 2
|
||||||
|
const val DISCONNECTED = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Battery(val component: Int, val level: Int, val status: Int) : Parcelable {
|
||||||
|
fun getComponentName(): String? {
|
||||||
|
return when (component) {
|
||||||
|
BatteryComponent.LEFT -> "LEFT"
|
||||||
|
BatteryComponent.RIGHT -> "RIGHT"
|
||||||
|
BatteryComponent.CASE -> "CASE"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStatusName(): String? {
|
||||||
|
return when (status) {
|
||||||
|
BatteryStatus.CHARGING -> "CHARGING"
|
||||||
|
BatteryStatus.NOT_CHARGING -> "NOT_CHARGING"
|
||||||
|
BatteryStatus.DISCONNECTED -> "DISCONNECTED"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Notifications {
|
||||||
|
companion object {
|
||||||
|
const val AIRPODS_CONNECTED = "me.kavishdevar.aln.AIRPODS_CONNECTED"
|
||||||
|
const val AIRPODS_DATA = "me.kavishdevar.aln.AIRPODS_DATA"
|
||||||
|
const val EAR_DETECTION_DATA = "me.kavishdevar.aln.EAR_DETECTION_DATA"
|
||||||
|
const val ANC_DATA = "me.kavishdevar.aln.ANC_DATA"
|
||||||
|
const val BATTERY_DATA = "me.kavishdevar.aln.BATTERY_DATA"
|
||||||
|
const val CA_DATA = "me.kavishdevar.aln.CA_DATA"
|
||||||
|
}
|
||||||
|
|
||||||
|
class EarDetection {
|
||||||
|
private val notificationBit = Capabilities.EAR_DETECTION
|
||||||
|
private val notificationPrefix = Enums.PREFIX.value + notificationBit
|
||||||
|
|
||||||
|
var status: List<Byte> = listOf(0x01, 0x01)
|
||||||
|
|
||||||
|
fun setStatus(data: ByteArray) {
|
||||||
|
status = listOf(data[6], data[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEarDetectionData(data: ByteArray): Boolean {
|
||||||
|
if (data.size != 8) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val prefixHex = notificationPrefix.joinToString("") { "%02x".format(it) }
|
||||||
|
val dataHex = data.joinToString("") { "%02x".format(it) }
|
||||||
|
return dataHex.startsWith(prefixHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ANC {
|
||||||
|
private val notificationPrefix = Enums.NOISE_CANCELLATION_PREFIX.value
|
||||||
|
|
||||||
|
var status: Int = 1
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun isANCData(data: ByteArray): Boolean {
|
||||||
|
if (data.size != 11) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val prefixHex = notificationPrefix.joinToString("") { "%02x".format(it) }
|
||||||
|
val dataHex = data.joinToString("") { "%02x".format(it) }
|
||||||
|
return dataHex.startsWith(prefixHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStatus(data: ByteArray) {
|
||||||
|
status = data[7].toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val name: String =
|
||||||
|
when (status) {
|
||||||
|
1 -> "OFF"
|
||||||
|
2 -> "ON"
|
||||||
|
3 -> "TRANSPARENCY"
|
||||||
|
4 -> "ADAPTIVE"
|
||||||
|
else -> "UNKNOWN"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class BatteryNotification {
|
||||||
|
private var first: Battery = Battery(BatteryComponent.LEFT, 0, BatteryStatus.DISCONNECTED)
|
||||||
|
private var second: Battery = Battery(BatteryComponent.RIGHT, 0, BatteryStatus.DISCONNECTED)
|
||||||
|
private var case: Battery = Battery(BatteryComponent.CASE, 0, BatteryStatus.DISCONNECTED)
|
||||||
|
|
||||||
|
fun isBatteryData(data: ByteArray): Boolean {
|
||||||
|
if (data.size != 22) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return data[0] == 0x04.toByte() && data[1] == 0x00.toByte() && data[2] == 0x04.toByte() &&
|
||||||
|
data[3] == 0x00.toByte() && data[4] == 0x04.toByte() && data[5] == 0x00.toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBattery(): List<Battery> {
|
||||||
|
val left = if (first.component == BatteryComponent.LEFT) first else second
|
||||||
|
val right = if (first.component == BatteryComponent.LEFT) second else first
|
||||||
|
return listOf(left, right, case)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConversationalAwarenessNotification {
|
||||||
|
private val NOTIFICATION_PREFIX = Enums.CONVERSATION_AWARENESS_RECEIVE_PREFIX.value
|
||||||
|
|
||||||
|
var status: Byte = 0
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun isConversationalAwarenessData(data: ByteArray): Boolean {
|
||||||
|
if (data.size != 10) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val prefixHex = NOTIFICATION_PREFIX.joinToString("") { "%02x".format(it) }
|
||||||
|
val dataHex = data.joinToString("") { "%02x".format(it) }
|
||||||
|
return dataHex.startsWith(prefixHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setData(data: ByteArray) {
|
||||||
|
status = data[9]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Capabilities {
|
||||||
|
companion object {
|
||||||
|
val NOISE_CANCELLATION = byteArrayOf(0x0d)
|
||||||
|
val CONVERSATION_AWARENESS = byteArrayOf(0x28)
|
||||||
|
val CUSTOMIZABLE_ADAPTIVE_TRANSPARENCY = byteArrayOf(0x01, 0x02)
|
||||||
|
val EAR_DETECTION = byteArrayOf(0x06)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class NoiseCancellation(val value: ByteArray) {
|
||||||
|
OFF(byteArrayOf(0x01)),
|
||||||
|
ON(byteArrayOf(0x02)),
|
||||||
|
TRANSPARENCY(byteArrayOf(0x03)),
|
||||||
|
ADAPTIVE(byteArrayOf(0x04));
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ConversationAwareness(val value: ByteArray) {
|
||||||
|
OFF(byteArrayOf(0x02)),
|
||||||
|
ON(byteArrayOf(0x01));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package me.kavishdevar.aln.ui.theme
|
package me.kavishdevar.aln.ui.theme
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|||||||
BIN
android/app/src/main/res/drawable/pro_2_buds.png
Normal file
BIN
android/app/src/main/res/drawable/pro_2_buds.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 752 KiB |
BIN
android/app/src/main/res/drawable/pro_2_case.png
Normal file
BIN
android/app/src/main/res/drawable/pro_2_case.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
android/app/src/main/res/font/sf_pro.ttf
Executable file
BIN
android/app/src/main/res/font/sf_pro.ttf
Executable file
Binary file not shown.
@@ -1,5 +1,7 @@
|
|||||||
[versions]
|
[versions]
|
||||||
|
accompanistPermissions = "0.36.0"
|
||||||
agp = "8.7.0-beta01"
|
agp = "8.7.0-beta01"
|
||||||
|
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"
|
||||||
@@ -11,7 +13,9 @@ composeBom = "2024.04.01"
|
|||||||
annotations = "15.0"
|
annotations = "15.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapibypass" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||||
|
|||||||
Reference in New Issue
Block a user