mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-04-30 18:16:42 +00:00
prepare for first release?
This commit is contained in:
@@ -52,6 +52,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.ui.tooling.preview)
|
implementation(libs.androidx.ui.tooling.preview)
|
||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.annotations)
|
implementation(libs.annotations)
|
||||||
|
implementation(libs.androidx.navigation.compose)
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -13,7 +16,10 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.ALN"
|
android:theme="@style/Theme.ALN"
|
||||||
tools:targetApi="31">
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
tools:targetApi="31"
|
||||||
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -23,13 +29,13 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".AirPodsService"
|
android:name=".AirPodsService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:foregroundServiceType="connectedDevice"
|
android:foregroundServiceType="connectedDevice"
|
||||||
android:permission="android.permission.BLUETOOTH_CONNECT">
|
android:permission="android.permission.BLUETOOTH_CONNECT" />
|
||||||
</service>
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package me.kavishdevar.aln
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothSocket
|
import android.bluetooth.BluetoothSocket
|
||||||
@@ -14,12 +17,11 @@ 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 androidx.core.app.NotificationCompat
|
||||||
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() {
|
||||||
@@ -67,8 +69,61 @@ class AirPodsService : Service() {
|
|||||||
socket?.outputStream?.flush()
|
socket?.outputStream?.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val earDetectionNotification = AirPodsNotifications.EarDetection()
|
||||||
|
val ancNotification = AirPodsNotifications.ANC()
|
||||||
|
val batteryNotification = AirPodsNotifications.BatteryNotification()
|
||||||
|
val conversationAwarenessNotification = AirPodsNotifications.ConversationalAwarenessNotification()
|
||||||
|
|
||||||
|
var earDetectionEnabled = true
|
||||||
|
|
||||||
|
fun setCaseChargingSounds(enabled: Boolean) {
|
||||||
|
val bytes = byteArrayOf(0x12, 0x3a, 0x00, 0x01, 0x00, 0x08, if (enabled) 0x00 else 0x01)
|
||||||
|
socket?.outputStream?.write(bytes)
|
||||||
|
socket?.outputStream?.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEarDetection(enabled: Boolean) {
|
||||||
|
earDetectionEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBattery(): List<Battery> {
|
||||||
|
return batteryNotification.getBattery()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getANC(): Int {
|
||||||
|
return ancNotification.status
|
||||||
|
}
|
||||||
|
|
||||||
|
// private fun buildBatteryText(battery: List<Battery>): String {
|
||||||
|
// val left = battery[0]
|
||||||
|
// val right = battery[1]
|
||||||
|
// val case = battery[2]
|
||||||
|
//
|
||||||
|
// return "Left: ${left.level}% ${left.getStatusName()}, Right: ${right.level}% ${right.getStatusName()}, Case: ${case.level}% ${case.getStatusName()}"
|
||||||
|
// }
|
||||||
|
|
||||||
|
private fun createNotification(): Notification {
|
||||||
|
val channelId = "battery"
|
||||||
|
val notificationBuilder = NotificationCompat.Builder(this, channelId)
|
||||||
|
.setSmallIcon(R.drawable.pro_2_buds)
|
||||||
|
.setContentTitle("AirPods Connected")
|
||||||
|
.setOngoing(true)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
|
||||||
|
val channel =
|
||||||
|
NotificationChannel(channelId, "Battery Notification", NotificationManager.IMPORTANCE_LOW)
|
||||||
|
|
||||||
|
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
return notificationBuilder.build()
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission", "InlinedApi")
|
@SuppressLint("MissingPermission", "InlinedApi")
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
|
||||||
|
val notification = createNotification()
|
||||||
|
startForeground(1, notification)
|
||||||
|
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
@@ -85,22 +140,19 @@ class AirPodsService : Service() {
|
|||||||
it.outputStream.write(Enums.HANDSHAKE.value)
|
it.outputStream.write(Enums.HANDSHAKE.value)
|
||||||
it.outputStream.write(Enums.SET_SPECIFIC_FEATURES.value)
|
it.outputStream.write(Enums.SET_SPECIFIC_FEATURES.value)
|
||||||
it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value)
|
it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value)
|
||||||
sendBroadcast(Intent(Notifications.AIRPODS_CONNECTED))
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_CONNECTED))
|
||||||
it.outputStream.flush()
|
it.outputStream.flush()
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val earDetectionNotification = Notifications.EarDetection()
|
|
||||||
val ancNotification = Notifications.ANC()
|
|
||||||
val batteryNotification = Notifications.BatteryNotification()
|
|
||||||
val conversationAwarenessNotification = Notifications.ConversationalAwarenessNotification()
|
|
||||||
|
|
||||||
while (socket?.isConnected == true) {
|
while (socket?.isConnected == true) {
|
||||||
socket?.let {
|
socket?.let {
|
||||||
|
val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
|
||||||
|
MediaController.initialize(audioManager)
|
||||||
val buffer = ByteArray(1024)
|
val buffer = ByteArray(1024)
|
||||||
val bytesRead = it.inputStream.read(buffer)
|
val bytesRead = it.inputStream.read(buffer)
|
||||||
val data = buffer.copyOfRange(0, bytesRead)
|
val data = buffer.copyOfRange(0, bytesRead)
|
||||||
if (bytesRead > 0) {
|
if (bytesRead > 0) {
|
||||||
sendBroadcast(Intent(Notifications.AIRPODS_DATA).apply {
|
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DATA).apply {
|
||||||
putExtra("data", buffer.copyOfRange(0, bytesRead))
|
putExtra("data", buffer.copyOfRange(0, bytesRead))
|
||||||
})
|
})
|
||||||
val bytes = buffer.copyOfRange(0, bytesRead)
|
val bytes = buffer.copyOfRange(0, bytesRead)
|
||||||
@@ -109,7 +161,7 @@ class AirPodsService : Service() {
|
|||||||
}
|
}
|
||||||
if (earDetectionNotification.isEarDetectionData(data)) {
|
if (earDetectionNotification.isEarDetectionData(data)) {
|
||||||
earDetectionNotification.setStatus(data)
|
earDetectionNotification.setStatus(data)
|
||||||
sendBroadcast(Intent(Notifications.EAR_DETECTION_DATA).apply {
|
sendBroadcast(Intent(AirPodsNotifications.EAR_DETECTION_DATA).apply {
|
||||||
val list = earDetectionNotification.status
|
val list = earDetectionNotification.status
|
||||||
val bytes = ByteArray(2)
|
val bytes = ByteArray(2)
|
||||||
bytes[0] = list[0]
|
bytes[0] = list[0]
|
||||||
@@ -117,44 +169,41 @@ 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
|
var inEar = false
|
||||||
val earReceiver = object : BroadcastReceiver() {
|
val earReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val data = intent.getByteArrayExtra("data")
|
val data = intent.getByteArrayExtra("data")
|
||||||
if (data != null) {
|
if (data != null && earDetectionEnabled) {
|
||||||
inEar = if (data.find { it == 0x02.toByte() } != null || data.find { it == 0x03.toByte() } != null) {
|
inEar = if (data.find { it == 0x02.toByte() } != null || data.find { it == 0x03.toByte() } != null) {
|
||||||
data[0] == 0x00.toByte() || data[1] == 0x00.toByte()
|
data[0] == 0x00.toByte() || data[1] == 0x00.toByte()
|
||||||
} else {
|
} else {
|
||||||
data[0] == 0x00.toByte() && data[1] == 0x00.toByte()
|
data[0] == 0x00.toByte() && data[1] == 0x00.toByte()
|
||||||
}
|
}
|
||||||
Log.d("AirPods Parser", "In Ear: $inEar")
|
|
||||||
if (inEar) {
|
if (inEar) {
|
||||||
mediaController.sendPlay()
|
MediaController.sendPlay()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
mediaController.sendPause()
|
MediaController.sendPause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val earIntentFilter = IntentFilter(Notifications.EAR_DETECTION_DATA)
|
val earIntentFilter = IntentFilter(AirPodsNotifications.EAR_DETECTION_DATA)
|
||||||
this@AirPodsService.registerReceiver(earReceiver, earIntentFilter,
|
this@AirPodsService.registerReceiver(earReceiver, earIntentFilter,
|
||||||
RECEIVER_EXPORTED
|
RECEIVER_EXPORTED
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else if (ancNotification.isANCData(data)) {
|
else if (ancNotification.isANCData(data)) {
|
||||||
ancNotification.setStatus(data)
|
ancNotification.setStatus(data)
|
||||||
sendBroadcast(Intent(Notifications.ANC_DATA).apply {
|
sendBroadcast(Intent(AirPodsNotifications.ANC_DATA).apply {
|
||||||
putExtra("data", ancNotification.status)
|
putExtra("data", ancNotification.status)
|
||||||
})
|
})
|
||||||
Log.d("AirPods Parser", "ANC: ${ancNotification.status}")
|
Log.d("AirPods Parser", "ANC: ${ancNotification.status}")
|
||||||
}
|
}
|
||||||
else if (batteryNotification.isBatteryData(data)) {
|
else if (batteryNotification.isBatteryData(data)) {
|
||||||
batteryNotification.setBattery(data)
|
batteryNotification.setBattery(data)
|
||||||
sendBroadcast(Intent(Notifications.BATTERY_DATA).apply {
|
sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
||||||
putParcelableArrayListExtra("data", ArrayList(batteryNotification.getBattery()))
|
putParcelableArrayListExtra("data", ArrayList(batteryNotification.getBattery()))
|
||||||
})
|
})
|
||||||
for (battery in batteryNotification.getBattery()) {
|
for (battery in batteryNotification.getBattery()) {
|
||||||
@@ -163,18 +212,14 @@ class AirPodsService : Service() {
|
|||||||
}
|
}
|
||||||
else if (conversationAwarenessNotification.isConversationalAwarenessData(data)) {
|
else if (conversationAwarenessNotification.isConversationalAwarenessData(data)) {
|
||||||
conversationAwarenessNotification.setData(data)
|
conversationAwarenessNotification.setData(data)
|
||||||
sendBroadcast(Intent(Notifications.CA_DATA).apply {
|
sendBroadcast(Intent(AirPodsNotifications.CA_DATA).apply {
|
||||||
putExtra("data", conversationAwarenessNotification.status)
|
putExtra("data", conversationAwarenessNotification.status)
|
||||||
})
|
})
|
||||||
if (conversationAwarenessNotification.status == 1.toByte() or 2.toByte()) {
|
if (conversationAwarenessNotification.status == 1.toByte() || conversationAwarenessNotification.status == 2.toByte()) {
|
||||||
val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
|
MediaController.startSpeaking()
|
||||||
val mediaController = MediaController(audioManager)
|
|
||||||
mediaController.startSpeaking()
|
|
||||||
}
|
}
|
||||||
else if (conversationAwarenessNotification.status == 9.toByte() or 8.toByte()) {
|
else if (conversationAwarenessNotification.status == 8.toByte() || conversationAwarenessNotification.status == 9.toByte()) {
|
||||||
val audioManager = this@AirPodsService.getSystemService(AUDIO_SERVICE) as AudioManager
|
MediaController.stopSpeaking()
|
||||||
val mediaController = MediaController(audioManager)
|
|
||||||
mediaController.stopSpeaking()
|
|
||||||
}
|
}
|
||||||
Log.d("AirPods Parser", "Conversation Awareness: ${conversationAwarenessNotification.status}")
|
Log.d("AirPods Parser", "Conversation Awareness: ${conversationAwarenessNotification.status}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,894 @@
|
|||||||
|
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
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
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
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.imageResource
|
||||||
|
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.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@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(AirPodsNotifications.BATTERY_DATA)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
@Composable
|
||||||
|
fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?, service: AirPodsService?,
|
||||||
|
navController: NavController) {
|
||||||
|
var deviceName by remember { mutableStateOf(TextFieldValue(device?.name ?: "AirPods Pro (fallback, should never show up)")) }
|
||||||
|
|
||||||
|
val verticalScrollState = rememberScrollState()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.padding(vertical = 24.dp, horizontal = 12.dp)
|
||||||
|
.verticalScroll(
|
||||||
|
state = verticalScrollState,
|
||||||
|
enabled = true,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
LaunchedEffect(service) {
|
||||||
|
service?.let {
|
||||||
|
it.sendBroadcast(Intent(AirPodsNotifications.BATTERY_DATA).apply {
|
||||||
|
putParcelableArrayListExtra("data", ArrayList(it.getBattery()))
|
||||||
|
})
|
||||||
|
it.sendBroadcast(Intent(AirPodsNotifications.ANC_DATA).apply {
|
||||||
|
putExtra("data", it.getANC())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BatteryView()
|
||||||
|
val sharedPreferences = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
if (service != null) {
|
||||||
|
StyledTextField(
|
||||||
|
name = "Name",
|
||||||
|
value = deviceName.text,
|
||||||
|
onValueChange = { deviceName = TextFieldValue(it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
|
NoiseControlSettings(service = service)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
AudioSettings(service = service, sharedPreferences = sharedPreferences)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
IndependentToggle(name = "Automatic Ear Detection", service = service, functionName = "setEarDetection", sharedPreferences = sharedPreferences, true)
|
||||||
|
|
||||||
|
// Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
// val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
// localstorage stuff
|
||||||
|
// TODO: localstorage and call the setButtons() with previous configuration and new configuration
|
||||||
|
// Box (
|
||||||
|
// modifier = Modifier
|
||||||
|
// .padding(vertical = 8.dp)
|
||||||
|
// .background(
|
||||||
|
// if (isDarkTheme) Color(0xFF1C1B20) else Color(0xFFFFFFFF),
|
||||||
|
// RoundedCornerShape(14.dp)
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// {
|
||||||
|
// // TODO: A Column Rows with text at start and a check mark if ticked
|
||||||
|
// }
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row (
|
||||||
|
modifier = Modifier
|
||||||
|
.background(if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF1C1B20) else Color(0xFFFFFFFF), RoundedCornerShape(14.dp))
|
||||||
|
.height(55.dp)
|
||||||
|
.clickable {
|
||||||
|
navController.navigate("debug")
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = "Debug", modifier = Modifier.padding(16.dp), color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
IconButton(
|
||||||
|
onClick = { navController.navigate("debug") },
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black ),
|
||||||
|
modifier = Modifier.padding(start = 16.dp).fillMaxHeight()
|
||||||
|
) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
Icon(imageVector = Icons.Default.KeyboardArrowRight, contentDescription = "Debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun NoiseControlSlider(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
|
val sliderValue = remember { mutableFloatStateOf(0f) }
|
||||||
|
LaunchedEffect(sliderValue) {
|
||||||
|
if (sharedPreferences.contains("adaptive_strength")) {
|
||||||
|
sliderValue.floatValue = sharedPreferences.getInt("adaptive_strength", 0).toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(sliderValue.floatValue) {
|
||||||
|
sharedPreferences.edit().putInt("adaptive_strength", sliderValue.floatValue.toInt()).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
|
||||||
|
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9)
|
||||||
|
val activeTrackColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFF007AFF)
|
||||||
|
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFFFFFFFF)
|
||||||
|
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
// Slider
|
||||||
|
Slider(
|
||||||
|
value = sliderValue.floatValue,
|
||||||
|
onValueChange = {
|
||||||
|
sliderValue.floatValue = it
|
||||||
|
service.setAdaptiveStrength(100 - it.toInt())
|
||||||
|
},
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
onValueChangeFinished = {
|
||||||
|
// Round the value when the user stops sliding
|
||||||
|
sliderValue.floatValue = sliderValue.floatValue.roundToInt().toFloat()
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.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
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Less Noise",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = labelTextColor
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(start = 4.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "More Noise",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = labelTextColor
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun Preview() {
|
||||||
|
IndependentToggle("Case Charging Sounds", AirPodsService(), "setCaseChargingSounds", LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun IndependentToggle(name: String, service: AirPodsService, functionName: String, sharedPreferences: SharedPreferences, default: Boolean = false) {
|
||||||
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
// Standardize the key
|
||||||
|
val snakeCasedName = name.replace(Regex("[\\W\\s]+"), "_").lowercase()
|
||||||
|
|
||||||
|
// State for the toggle
|
||||||
|
var checked by remember { mutableStateOf(default) }
|
||||||
|
|
||||||
|
// Load initial state from SharedPreferences
|
||||||
|
LaunchedEffect(sharedPreferences) {
|
||||||
|
checked = sharedPreferences.getBoolean(snakeCasedName, true)
|
||||||
|
}
|
||||||
|
Box (
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.background(
|
||||||
|
if (isDarkTheme) Color(0xFF1C1B20) else Color(0xFFFFFFFF),
|
||||||
|
RoundedCornerShape(14.dp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(55.dp)
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
.clickable {
|
||||||
|
// Toggle checked state and save to SharedPreferences
|
||||||
|
checked = !checked
|
||||||
|
sharedPreferences
|
||||||
|
.edit()
|
||||||
|
.putBoolean(snakeCasedName, checked)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
// Call the corresponding method in the service
|
||||||
|
val method = service::class.java.getMethod(functionName, Boolean::class.java)
|
||||||
|
method.invoke(service, checked)
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = name, modifier = Modifier.weight(1f), fontSize = 16.sp, color = textColor)
|
||||||
|
StyledSwitch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = {
|
||||||
|
checked = it
|
||||||
|
sharedPreferences.edit().putBoolean(snakeCasedName, it).apply()
|
||||||
|
|
||||||
|
// Call the corresponding method in the service
|
||||||
|
val method = service::class.java.getMethod(functionName, Boolean::class.java)
|
||||||
|
method.invoke(service, it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AudioSettings(service: AirPodsService, sharedPreferences: SharedPreferences) {
|
||||||
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
// Load the conversational awareness state from sharedPreferences
|
||||||
|
var conversationalAwarenessEnabled by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
sharedPreferences.getBoolean("conversational_awareness", true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the service when the toggle is changed
|
||||||
|
fun updateConversationalAwareness(enabled: Boolean) {
|
||||||
|
conversationalAwarenessEnabled = enabled
|
||||||
|
sharedPreferences.edit().putBoolean("conversational_awareness", enabled).apply()
|
||||||
|
service.setCAEnabled(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "AUDIO",
|
||||||
|
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(0xFF1C1B20) else Color(0xFFFFFFFF)
|
||||||
|
val isPressed = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
|
.padding(top = 2.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp)
|
||||||
|
.pointerInput(Unit) { // Detect press state for iOS-like effect
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = {
|
||||||
|
isPressed.value = true
|
||||||
|
tryAwaitRelease() // Wait until release
|
||||||
|
isPressed.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.clickable(
|
||||||
|
indication = null, // Disable ripple effect
|
||||||
|
interactionSource = remember { MutableInteractionSource() } // Required for clickable
|
||||||
|
) {
|
||||||
|
// Toggle the conversational awareness value
|
||||||
|
updateConversationalAwareness(!conversationalAwarenessEnabled)
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Conversational Awareness",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
|
||||||
|
StyledSwitch(
|
||||||
|
checked = conversationalAwarenessEnabled,
|
||||||
|
onCheckedChange = {
|
||||||
|
updateConversationalAwareness(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp, vertical = 10.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Adaptive Audio",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp, bottom = 2.dp, start = 2.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise.",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp, top = 2.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = textColor.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
NoiseControlSlider(service = service, sharedPreferences = sharedPreferences)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
|
@Composable
|
||||||
|
fun NoiseControlSettings(service: AirPodsService) {
|
||||||
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1B20) else Color(0xFFE3E3E8)
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
val textColorSelected = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
val selectedBackground = if (isDarkTheme) Color(0xFF5C5A5F) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
|
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
|
||||||
|
val noiseControlIntentFilter = IntentFilter(AirPodsNotifications.ANC_DATA)
|
||||||
|
context.registerReceiver(noiseControlReceiver, noiseControlIntentFilter, Context.RECEIVER_EXPORTED)
|
||||||
|
|
||||||
|
// val paddingAnim by animateDpAsState(
|
||||||
|
// targetValue = when (noiseControlMode.value) {
|
||||||
|
// NoiseControlMode.OFF -> 0.dp
|
||||||
|
// NoiseControlMode.TRANSPARENCY -> 150.dp
|
||||||
|
// NoiseControlMode.ADAPTIVE -> 250.dp
|
||||||
|
// NoiseControlMode.NOISE_CANCELLATION -> 350.dp
|
||||||
|
// }, label = ""
|
||||||
|
// )
|
||||||
|
|
||||||
|
val d1a = remember { mutableFloatStateOf(0f) }
|
||||||
|
val d2a = remember { mutableFloatStateOf(0f) }
|
||||||
|
val d3a = remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
|
fun onModeSelected(mode: NoiseControlMode) {
|
||||||
|
noiseControlMode.value = mode
|
||||||
|
service.setANCMode(mode.ordinal+1)
|
||||||
|
when (mode) {
|
||||||
|
NoiseControlMode.NOISE_CANCELLATION -> {
|
||||||
|
d1a.floatValue = 1f
|
||||||
|
d2a.floatValue = 1f
|
||||||
|
d3a.floatValue = 0f
|
||||||
|
}
|
||||||
|
NoiseControlMode.OFF -> {
|
||||||
|
d1a.floatValue = 0f
|
||||||
|
d2a.floatValue = 1f
|
||||||
|
d3a.floatValue = 1f
|
||||||
|
}
|
||||||
|
NoiseControlMode.ADAPTIVE -> {
|
||||||
|
d1a.floatValue = 1f
|
||||||
|
d2a.floatValue = 0f
|
||||||
|
d3a.floatValue = 0f
|
||||||
|
}
|
||||||
|
NoiseControlMode.TRANSPARENCY -> {
|
||||||
|
d1a.floatValue = 0f
|
||||||
|
d2a.floatValue = 0f
|
||||||
|
d3a.floatValue = 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "NOISE CONTROL",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Light,
|
||||||
|
color = textColor.copy(alpha = 0.6f)
|
||||||
|
),
|
||||||
|
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(75.dp)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
// Box(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .fillMaxHeight()
|
||||||
|
// .width(80.dp)
|
||||||
|
// .offset(x = paddingAnim)
|
||||||
|
// .background(selectedBackground, RoundedCornerShape(8.dp))
|
||||||
|
// )
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor, RoundedCornerShape(14.dp))
|
||||||
|
) {
|
||||||
|
NoiseControlButton(
|
||||||
|
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
||||||
|
onClick = { onModeSelected(NoiseControlMode.OFF) },
|
||||||
|
textColor = if (noiseControlMode.value == NoiseControlMode.OFF) textColorSelected else textColor,
|
||||||
|
backgroundColor = if (noiseControlMode.value == NoiseControlMode.OFF) selectedBackground else Color.Transparent,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
VerticalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 10.dp)
|
||||||
|
.alpha(d1a.floatValue),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
)
|
||||||
|
NoiseControlButton(
|
||||||
|
icon = ImageBitmap.imageResource(R.drawable.transparency),
|
||||||
|
onClick = { onModeSelected(NoiseControlMode.TRANSPARENCY) },
|
||||||
|
textColor = if (noiseControlMode.value == NoiseControlMode.TRANSPARENCY) textColorSelected else textColor,
|
||||||
|
backgroundColor = if (noiseControlMode.value == NoiseControlMode.TRANSPARENCY) selectedBackground else Color.Transparent,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
VerticalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 10.dp)
|
||||||
|
.alpha(d2a.floatValue),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
)
|
||||||
|
NoiseControlButton(
|
||||||
|
icon = ImageBitmap.imageResource(R.drawable.adaptive),
|
||||||
|
onClick = { onModeSelected(NoiseControlMode.ADAPTIVE) },
|
||||||
|
textColor = if (noiseControlMode.value == NoiseControlMode.ADAPTIVE) textColorSelected else textColor,
|
||||||
|
backgroundColor = if (noiseControlMode.value == NoiseControlMode.ADAPTIVE) selectedBackground else Color.Transparent,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
VerticalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 10.dp)
|
||||||
|
.alpha(d3a.floatValue),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
)
|
||||||
|
NoiseControlButton(
|
||||||
|
icon = ImageBitmap.imageResource(R.drawable.noise_cancellation),
|
||||||
|
onClick = { onModeSelected(NoiseControlMode.NOISE_CANCELLATION) },
|
||||||
|
textColor = if (noiseControlMode.value == NoiseControlMode.NOISE_CANCELLATION) textColorSelected else textColor,
|
||||||
|
backgroundColor = if (noiseControlMode.value == NoiseControlMode.NOISE_CANCELLATION) selectedBackground else Color.Transparent,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.padding(top = 1.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Off",
|
||||||
|
style = TextStyle(fontSize = 12.sp, color = textColor),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Transparency",
|
||||||
|
style = TextStyle(fontSize = 12.sp, color = textColor),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Adaptive",
|
||||||
|
style = TextStyle(fontSize = 12.sp, color = textColor),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Noise Cancellation",
|
||||||
|
style = TextStyle(fontSize = 12.sp, color = textColor),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NoiseControlButton(
|
||||||
|
icon: ImageBitmap,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
textColor: Color,
|
||||||
|
backgroundColor: Color,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(horizontal = 4.dp, vertical = 4.dp)
|
||||||
|
.background(color = backgroundColor, shape = RoundedCornerShape(11.dp))
|
||||||
|
.clickable(
|
||||||
|
onClick = onClick,
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
bitmap = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = textColor,
|
||||||
|
modifier = Modifier.size(40.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class NoiseControlMode {
|
||||||
|
OFF, NOISE_CANCELLATION, TRANSPARENCY, ADAPTIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StyledSwitch(
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
|
||||||
|
val thumbColor = Color.White
|
||||||
|
val trackColor = if (checked) Color(0xFF34C759) else if (isDarkTheme) Color(0xFF5B5B5E) else Color(0xFFD1D1D6)
|
||||||
|
|
||||||
|
// Animate the horizontal offset of the thumb
|
||||||
|
val thumbOffsetX by animateDpAsState(targetValue = if (checked) 20.dp else 0.dp, label = "Test")
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(51.dp)
|
||||||
|
.height(31.dp)
|
||||||
|
.clip(RoundedCornerShape(15.dp))
|
||||||
|
.background(trackColor) // Dynamic track background
|
||||||
|
.padding(horizontal = 3.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(x = thumbOffsetX) // Animate the offset for smooth transition
|
||||||
|
.size(27.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(thumbColor) // Dynamic thumb color
|
||||||
|
.clickable { onCheckedChange(!checked) } // Make the switch clickable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StyledTextField(
|
||||||
|
name: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
||||||
|
|
||||||
|
val backgroundColor = if (isDarkTheme) Color(0xFF1C1B20) else Color(0xFFFFFFFF)
|
||||||
|
val textColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
val cursorColor = if (isDarkTheme) Color.White else Color.Black
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(55.dp)
|
||||||
|
.background(
|
||||||
|
backgroundColor,
|
||||||
|
RoundedCornerShape(14.dp)
|
||||||
|
) // Dynamic background based on theme
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor // Text color based on theme
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
BasicTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = textColor, // Dynamic text color
|
||||||
|
fontSize = 16.sp,
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(cursorColor), // Dynamic cursor color
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth() // Ensures text field takes remaining available space
|
||||||
|
.padding(start = 8.dp), // Padding to adjust spacing between text field and icon,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) {
|
||||||
|
val batteryOutlineColor = Color(0xFFBFBFBF) // Light gray outline
|
||||||
|
val batteryFillColor = if (batteryPercentage > 30) Color(0xFF30D158) else Color(0xFFFC3C3C)
|
||||||
|
val batteryTextColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
|
||||||
|
// Battery indicator dimensions
|
||||||
|
val batteryWidth = 30.dp
|
||||||
|
val batteryHeight = 15.dp
|
||||||
|
val batteryCornerRadius = 4.dp
|
||||||
|
val tipWidth = 5.dp
|
||||||
|
val tipHeight = batteryHeight * 0.3f
|
||||||
|
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
// Row for battery icon and tip
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp) // Padding between icon and percentage text
|
||||||
|
) {
|
||||||
|
// Battery Icon
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(batteryWidth)
|
||||||
|
.height(batteryHeight)
|
||||||
|
.border(1.dp, batteryOutlineColor, RoundedCornerShape(batteryCornerRadius))
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(2.dp)
|
||||||
|
.width(batteryWidth * (batteryPercentage / 100f))
|
||||||
|
.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)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(tipWidth)
|
||||||
|
.height(tipHeight)
|
||||||
|
.padding(start = 1.dp)
|
||||||
|
.background(
|
||||||
|
batteryOutlineColor,
|
||||||
|
RoundedCornerShape(
|
||||||
|
topStart = 0.dp,
|
||||||
|
topEnd = 12.dp,
|
||||||
|
bottomStart = 0.dp,
|
||||||
|
bottomEnd = 12.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Battery Percentage Text
|
||||||
|
Text(
|
||||||
|
text = "$batteryPercentage%",
|
||||||
|
color = batteryTextColor,
|
||||||
|
style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
225
android/app/src/main/java/me/kavishdevar/aln/DebugScreen.kt
Normal file
225
android/app/src/main/java/me/kavishdevar/aln/DebugScreen.kt
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
|
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.IBinder
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Send
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun DebugScreen(navController: NavController) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Debug") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF000000)
|
||||||
|
else Color(0xFFF2F2F7),
|
||||||
|
) { paddingValues ->
|
||||||
|
|
||||||
|
val text = remember { mutableStateListOf<String>("Log Start") }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
val receiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val data = intent.getByteArrayExtra("data")
|
||||||
|
data?.let {
|
||||||
|
text.add(">" + it.joinToString(" ") { byte -> "%02X".format(byte) }) // Use ">" for received packets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(context) {
|
||||||
|
val intentFilter = IntentFilter(AirPodsNotifications.AIRPODS_DATA)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(text.size) {
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
listState.animateScrollToItem(text.size - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.imePadding(), // Ensures padding for keyboard visibility
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
content = {
|
||||||
|
items(text.size) { index ->
|
||||||
|
val message = text[index]
|
||||||
|
val isSent = message.startsWith(">")
|
||||||
|
val backgroundColor = if (isSent) Color(0xFFE1FFC7) else Color(0xFFD1D1D1)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
.background(backgroundColor, RoundedCornerShape(12.dp))
|
||||||
|
.padding(12.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
if (!isSent) {
|
||||||
|
Text("<", color = Color(0xFF00796B), fontSize = 16.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = if (isSent) message.substring(1) else message, // Remove the ">" from sent packets
|
||||||
|
fontFamily = FontFamily(Font(R.font.hack)),
|
||||||
|
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF000000)
|
||||||
|
else Color(0xFF000000),
|
||||||
|
modifier = Modifier.weight(1f) // Allows text to take available space
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isSent) {
|
||||||
|
Text(">", color = Color(0xFF00796B), fontSize = 16.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
HorizontalDivider()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color(0xFF1C1B20)),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
val packet = remember { mutableStateOf(TextFieldValue("")) }
|
||||||
|
TextField(
|
||||||
|
value = packet.value,
|
||||||
|
onValueChange = { packet.value = it },
|
||||||
|
label = { Text("Packet") },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp), // Padding for the input field
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
airPodsService.value?.sendPacket(packet.value.text)
|
||||||
|
text.add(packet.value.text) // Add sent message directly without prefix
|
||||||
|
packet.value = TextFieldValue("") // Clear input field after sending
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
Icon(Icons.Filled.Send, contentDescription = "Send")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF1C1B20) else Color(0xFFF2F2F7),
|
||||||
|
unfocusedContainerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF1C1B20) else Color(0xFFF2F2F7),
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
focusedTextColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black,
|
||||||
|
unfocusedTextColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black.copy(alpha = 0.6f),
|
||||||
|
focusedLabelColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White.copy(alpha = 0.6f) else Color.Black,
|
||||||
|
unfocusedLabelColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.6f),
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,10 @@ import android.annotation.SuppressLint
|
|||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.bluetooth.BluetoothProfile
|
import android.bluetooth.BluetoothProfile
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.media.AudioManager
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
@@ -19,79 +15,40 @@ 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.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
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.Button
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
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.SliderDefaults
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
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
|
||||||
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
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
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.clip
|
|
||||||
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.SolidColor
|
|
||||||
import androidx.compose.ui.graphics.luminance
|
import androidx.compose.ui.graphics.luminance
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.imageResource
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
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.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
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.core.content.ContextCompat.getSystemService
|
import androidx.core.content.ContextCompat.getSystemService
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
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
|
|
||||||
|
|
||||||
|
@ExperimentalMaterial3Api
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -100,7 +57,28 @@ class MainActivity : ComponentActivity() {
|
|||||||
setContent {
|
setContent {
|
||||||
ALNTheme {
|
ALNTheme {
|
||||||
Scaffold (
|
Scaffold (
|
||||||
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(0xFF000000) else Color(0xFFFFFFFF)
|
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
|
||||||
|
0xFF000000
|
||||||
|
) else Color(
|
||||||
|
0xFFF2F2F7
|
||||||
|
),
|
||||||
|
topBar = {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "AirPods Pro Settings",
|
||||||
|
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||||
|
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
|
||||||
|
0xFF000000
|
||||||
|
) else Color(
|
||||||
|
0xFFF2F2F7
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Main(innerPadding)
|
Main(innerPadding)
|
||||||
}
|
}
|
||||||
@@ -109,172 +87,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UseOfNonLambdaOffsetOverload")
|
|
||||||
@Composable
|
|
||||||
fun StyledSwitch(
|
|
||||||
checked: Boolean,
|
|
||||||
onCheckedChange: (Boolean) -> Unit
|
|
||||||
) {
|
|
||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
|
||||||
|
|
||||||
val thumbColor = Color.White
|
|
||||||
val trackColor = if (checked) Color(0xFF34C759) else if (isDarkTheme) Color(0xFF262629) else Color(0xFFD1D1D6)
|
|
||||||
|
|
||||||
// Animate the horizontal offset of the thumb
|
|
||||||
val thumbOffsetX by animateDpAsState(targetValue = if (checked) 20.dp else 0.dp, label = "Test")
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(51.dp)
|
|
||||||
.height(31.dp)
|
|
||||||
.clip(RoundedCornerShape(15.dp))
|
|
||||||
.background(trackColor) // Dynamic track background
|
|
||||||
.padding(horizontal = 3.dp),
|
|
||||||
contentAlignment = Alignment.CenterStart
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.offset(x = thumbOffsetX) // Animate the offset for smooth transition
|
|
||||||
.size(27.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(thumbColor) // Dynamic thumb color
|
|
||||||
.clickable { onCheckedChange(!checked) } // Make the switch clickable
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun StyledTextField(
|
|
||||||
name: String,
|
|
||||||
value: String,
|
|
||||||
onValueChange: (String) -> Unit
|
|
||||||
) {
|
|
||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
|
||||||
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF252525) else Color(0xFFFFFFFF)
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
val cursorColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
backgroundColor,
|
|
||||||
RoundedCornerShape(10.dp)
|
|
||||||
) // Dynamic background based on theme
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = name,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = textColor // Text color based on theme
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicTextField(
|
|
||||||
value = value,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
textStyle = TextStyle(
|
|
||||||
color = textColor, // Dynamic text color
|
|
||||||
fontSize = 16.sp,
|
|
||||||
),
|
|
||||||
cursorBrush = SolidColor(cursorColor), // Dynamic cursor color
|
|
||||||
decorationBox = { innerTextField ->
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.End
|
|
||||||
) {
|
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth() // Ensures text field takes remaining available space
|
|
||||||
.padding(start = 8.dp) // Padding to adjust spacing between text field and icon
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BatteryIndicator(batteryPercentage: Int, charging: Boolean = false) {
|
|
||||||
val batteryOutlineColor = Color(0xFFBFBFBF) // Light gray outline
|
|
||||||
val batteryFillColor = if (batteryPercentage > 30) Color(0xFF30D158) else Color(0xFFFC3C3C)
|
|
||||||
val batteryTextColor = MaterialTheme.colorScheme.onSurface
|
|
||||||
|
|
||||||
// Battery indicator dimensions
|
|
||||||
val batteryWidth = 30.dp
|
|
||||||
val batteryHeight = 15.dp
|
|
||||||
val batteryCornerRadius = 4.dp
|
|
||||||
val tipWidth = 5.dp
|
|
||||||
val tipHeight = batteryHeight * 0.3f
|
|
||||||
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
// Row for battery icon and tip
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
|
||||||
modifier = Modifier.padding(bottom = 4.dp) // Padding between icon and percentage text
|
|
||||||
) {
|
|
||||||
// Battery Icon
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(batteryWidth)
|
|
||||||
.height(batteryHeight)
|
|
||||||
.border(1.dp, batteryOutlineColor, RoundedCornerShape(batteryCornerRadius))
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.padding(2.dp)
|
|
||||||
.width(batteryWidth * (batteryPercentage / 100f))
|
|
||||||
.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)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.width(tipWidth)
|
|
||||||
.height(tipHeight)
|
|
||||||
.padding(start = 1.dp)
|
|
||||||
.background(
|
|
||||||
batteryOutlineColor,
|
|
||||||
RoundedCornerShape(
|
|
||||||
topStart = 0.dp,
|
|
||||||
topEnd = 12.dp,
|
|
||||||
bottomStart = 0.dp,
|
|
||||||
bottomEnd = 12.dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Battery Percentage Text
|
|
||||||
Text(
|
|
||||||
text = "$batteryPercentage%",
|
|
||||||
color = batteryTextColor,
|
|
||||||
style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -290,6 +102,9 @@ fun Main(paddingValues: PaddingValues) {
|
|||||||
val bluetoothAdapter = bluetoothManager?.adapter
|
val bluetoothAdapter = bluetoothManager?.adapter
|
||||||
val devices = bluetoothAdapter?.bondedDevices
|
val devices = bluetoothAdapter?.bondedDevices
|
||||||
val airpodsDevice = remember { mutableStateOf<BluetoothDevice?>(null) }
|
val airpodsDevice = remember { mutableStateOf<BluetoothDevice?>(null) }
|
||||||
|
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
if (devices != null) {
|
if (devices != null) {
|
||||||
for (device in devices) {
|
for (device in devices) {
|
||||||
if (device.uuids.contains(uuid)) {
|
if (device.uuids.contains(uuid)) {
|
||||||
@@ -328,16 +143,38 @@ fun Main(paddingValues: PaddingValues) {
|
|||||||
airPodsService.value = null
|
airPodsService.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val intent = Intent(context, AirPodsService::class.java)
|
val intent = Intent(context, AirPodsService::class.java)
|
||||||
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
NavHost(
|
||||||
if (airpodsDevice.value != null)
|
navController = navController,
|
||||||
{
|
startDestination = "notConnected",
|
||||||
AirPodsSettingsScreen(
|
enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) }, // Slide in from the right
|
||||||
paddingValues,
|
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) }, // Slide out to the left
|
||||||
airpodsDevice.value,
|
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) }, // Slide in from the left
|
||||||
service = airPodsService.value
|
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(300)) } // Slide out to the right
|
||||||
)
|
){
|
||||||
|
composable("notConnected") {
|
||||||
|
Text("Not Connected...")
|
||||||
|
}
|
||||||
|
composable("settings") {
|
||||||
|
AirPodsSettingsScreen(
|
||||||
|
paddingValues,
|
||||||
|
airpodsDevice.value,
|
||||||
|
service = airPodsService.value,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composable("debug") {
|
||||||
|
DebugScreen(navController = navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (airpodsDevice.value != null) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
navController.navigate("settings") {
|
||||||
|
popUpTo("notConnected") { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Text("No AirPods connected")
|
Text("No AirPods connected")
|
||||||
@@ -363,519 +200,8 @@ fun Main(paddingValues: PaddingValues) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@PreviewLightDark
|
||||||
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 {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
@Composable
|
|
||||||
fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?, service: AirPodsService?) {
|
|
||||||
var deviceName by remember { mutableStateOf(TextFieldValue(device?.name ?: "AirPods Pro (fallback, should never show up)")) }
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(paddingValues)
|
|
||||||
.padding(vertical = 24.dp, horizontal = 12.dp)
|
|
||||||
) {
|
|
||||||
BatteryView()
|
|
||||||
if (service != null) {
|
|
||||||
StyledTextField(
|
|
||||||
name = "Name",
|
|
||||||
value = deviceName.text,
|
|
||||||
onValueChange = { deviceName = TextFieldValue(it) }
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
NoiseControlSettings(service = service)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
AudioSettings(service = service)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun NoiseControlSlider(service: AirPodsService) {
|
|
||||||
val sliderValue = remember { mutableStateOf(0f) }
|
|
||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
|
||||||
|
|
||||||
val trackColor = if (isDarkTheme) Color(0xFFB3B3B3) else Color(0xFFD9D9D9)
|
|
||||||
val activeTrackColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFF007AFF)
|
|
||||||
val thumbColor = if (isDarkTheme) Color(0xFFFFFFFF) else Color(0xFF007AFF)
|
|
||||||
val labelTextColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
// Slider
|
|
||||||
Slider(
|
|
||||||
value = sliderValue.value,
|
|
||||||
onValueChange = {
|
|
||||||
sliderValue.value = it
|
|
||||||
service.setAdaptiveStrength(100 - it.toInt())
|
|
||||||
},
|
|
||||||
valueRange = 0f..100f,
|
|
||||||
onValueChangeFinished = {
|
|
||||||
// Round the value when the user stops sliding
|
|
||||||
sliderValue.value = sliderValue.value.roundToInt().toFloat()
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.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
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Less Noise",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
color = labelTextColor
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(start = 4.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "More Noise",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
color = labelTextColor
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(end = 4.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AudioSettings(service: AirPodsService) {
|
|
||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
var conversationalAwarenessEnabled by remember { mutableStateOf(true) }
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "AUDIO",
|
|
||||||
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(0xFF252525) else Color(0xFFFFFFFF)
|
|
||||||
val isPressed = remember { mutableStateOf(false) }
|
|
||||||
Column (
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(backgroundColor, RoundedCornerShape(12.dp))
|
|
||||||
.padding(top = 2.dp)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = if (isPressed.value) Color(0xFFE0E0E0) else Color.Transparent
|
|
||||||
)
|
|
||||||
.padding(horizontal = 12.dp, vertical = 12.dp)
|
|
||||||
.pointerInput(Unit) { // Detect press state for iOS-like effect
|
|
||||||
detectTapGestures(
|
|
||||||
onPress = {
|
|
||||||
isPressed.value = true
|
|
||||||
tryAwaitRelease() // Wait until release
|
|
||||||
isPressed.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.clickable(
|
|
||||||
indication = null, // Disable ripple effect
|
|
||||||
interactionSource = remember { MutableInteractionSource() } // Required for clickable
|
|
||||||
) {
|
|
||||||
conversationalAwarenessEnabled = !conversationalAwarenessEnabled
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(text = "Conversational Awareness", modifier = Modifier.weight(1f), fontSize = 16.sp, color = textColor)
|
|
||||||
|
|
||||||
StyledSwitch(
|
|
||||||
checked = conversationalAwarenessEnabled,
|
|
||||||
onCheckedChange = {
|
|
||||||
conversationalAwarenessEnabled = it
|
|
||||||
service.setCAEnabled(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Column (
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp, vertical = 10.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Adaptive Audio",
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 8.dp, bottom = 2.dp, start = 2.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = textColor
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Adaptive audio dynamically responds to your environment and cancels or allows external noise. You can customize Adaptive Audio to allow more or less noise",
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(8.dp, top = 2.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 12.sp,
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NoiseControlSettings(service: AirPodsService) {
|
|
||||||
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
|
|
||||||
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7)
|
|
||||||
val textColor = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
val textColorSelected = if (isDarkTheme) Color.White else Color.Black
|
|
||||||
val selectedBackground = if (isDarkTheme) Color(0xFF090909) else Color(0xFFFFFFFF)
|
|
||||||
|
|
||||||
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(
|
|
||||||
// targetValue = when (noiseControlMode.value) {
|
|
||||||
// NoiseControlMode.OFF -> 0.dp
|
|
||||||
// NoiseControlMode.TRANSPARENCY -> 150.dp
|
|
||||||
// NoiseControlMode.ADAPTIVE -> 250.dp
|
|
||||||
// NoiseControlMode.NOISE_CANCELLATION -> 350.dp
|
|
||||||
// }, label = ""
|
|
||||||
// )
|
|
||||||
|
|
||||||
val d1a = remember { mutableStateOf(0f) }
|
|
||||||
val d2a = remember { mutableStateOf(0f) }
|
|
||||||
val d3a = remember { mutableStateOf(0f) }
|
|
||||||
|
|
||||||
fun onModeSelected(mode: NoiseControlMode) {
|
|
||||||
noiseControlMode.value = mode
|
|
||||||
service.setANCMode(mode.ordinal+1)
|
|
||||||
when (mode) {
|
|
||||||
NoiseControlMode.NOISE_CANCELLATION -> {
|
|
||||||
d1a.value = 1f
|
|
||||||
d2a.value = 1f
|
|
||||||
d3a.value = 0f
|
|
||||||
}
|
|
||||||
NoiseControlMode.OFF -> {
|
|
||||||
d1a.value = 0f
|
|
||||||
d2a.value = 1f
|
|
||||||
d3a.value = 1f
|
|
||||||
}
|
|
||||||
NoiseControlMode.ADAPTIVE -> {
|
|
||||||
d1a.value = 1f
|
|
||||||
d2a.value = 0f
|
|
||||||
d3a.value = 0f
|
|
||||||
}
|
|
||||||
NoiseControlMode.TRANSPARENCY -> {
|
|
||||||
d1a.value = 0f
|
|
||||||
d2a.value = 0f
|
|
||||||
d3a.value = 1f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "NOISE CONTROL",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Light,
|
|
||||||
color = textColor.copy(alpha = 0.6f)
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(8.dp, bottom = 2.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(65.dp)
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
// Box(
|
|
||||||
// modifier = Modifier
|
|
||||||
// .fillMaxHeight()
|
|
||||||
// .width(80.dp)
|
|
||||||
// .offset(x = paddingAnim)
|
|
||||||
// .background(selectedBackground, RoundedCornerShape(8.dp))
|
|
||||||
// )
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(backgroundColor, RoundedCornerShape(8.dp))
|
|
||||||
) {
|
|
||||||
NoiseControlButton(
|
|
||||||
icon = Icons.Default.Person, // Replace with your icon
|
|
||||||
onClick = { onModeSelected(NoiseControlMode.OFF) },
|
|
||||||
textColor = if (noiseControlMode.value == NoiseControlMode.OFF) textColorSelected else textColor,
|
|
||||||
backgroundColor = if (noiseControlMode.value == NoiseControlMode.OFF) selectedBackground else Color.Transparent,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
VerticalDivider(
|
|
||||||
thickness = 1.dp,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 10.dp)
|
|
||||||
.alpha(d1a.value),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
|
||||||
)
|
|
||||||
NoiseControlButton(
|
|
||||||
icon = Icons.Default.Person, // Replace with your icon
|
|
||||||
onClick = { onModeSelected(NoiseControlMode.TRANSPARENCY) },
|
|
||||||
textColor = if (noiseControlMode.value == NoiseControlMode.TRANSPARENCY) textColorSelected else textColor,
|
|
||||||
backgroundColor = if (noiseControlMode.value == NoiseControlMode.TRANSPARENCY) selectedBackground else Color.Transparent,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
VerticalDivider(
|
|
||||||
thickness = 1.dp,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 10.dp)
|
|
||||||
.alpha(d2a.value),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
|
||||||
)
|
|
||||||
NoiseControlButton(
|
|
||||||
icon = Icons.Default.Person, // Replace with your icon
|
|
||||||
onClick = { onModeSelected(NoiseControlMode.ADAPTIVE) },
|
|
||||||
textColor = if (noiseControlMode.value == NoiseControlMode.ADAPTIVE) textColorSelected else textColor,
|
|
||||||
backgroundColor = if (noiseControlMode.value == NoiseControlMode.ADAPTIVE) selectedBackground else Color.Transparent,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
VerticalDivider(
|
|
||||||
thickness = 1.dp,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 10.dp)
|
|
||||||
.alpha(d3a.value),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
|
||||||
)
|
|
||||||
NoiseControlButton(
|
|
||||||
icon = Icons.Default.Person, // Replace with your icon
|
|
||||||
onClick = { onModeSelected(NoiseControlMode.NOISE_CANCELLATION) },
|
|
||||||
textColor = if (noiseControlMode.value == NoiseControlMode.NOISE_CANCELLATION) textColorSelected else textColor,
|
|
||||||
backgroundColor = if (noiseControlMode.value == NoiseControlMode.NOISE_CANCELLATION) selectedBackground else Color.Transparent,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.padding(top = 1.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Off",
|
|
||||||
style = TextStyle(fontSize = 12.sp, color = textColor),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Transparency",
|
|
||||||
style = TextStyle(fontSize = 12.sp, color = textColor),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Adaptive",
|
|
||||||
style = TextStyle(fontSize = 12.sp, color = textColor),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Noise Cancellation",
|
|
||||||
style = TextStyle(fontSize = 12.sp, color = textColor),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun NoiseControlButton(
|
|
||||||
icon: ImageVector,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
textColor: Color,
|
|
||||||
backgroundColor: Color,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.padding(horizontal = 4.dp, vertical = 4.dp)
|
|
||||||
.background(color = backgroundColor, shape = RoundedCornerShape(6.dp))
|
|
||||||
.clickable(
|
|
||||||
onClick = onClick,
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = textColor,
|
|
||||||
modifier = Modifier.size(32.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class NoiseControlMode {
|
|
||||||
OFF, NOISE_CANCELLATION, TRANSPARENCY, ADAPTIVE
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewAirPodsSettingsScreen() {
|
fun PreviewAirPodsSettingsScreen() {
|
||||||
BatteryIndicator(100, true)
|
AirPodsSettingsScreen(paddingValues = PaddingValues(0.dp), device = null, service = null, navController = rememberNavController())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
package me.kavishdevar.aln
|
package me.kavishdevar.aln
|
||||||
|
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
|
||||||
class MediaController (private val audioManager: AudioManager){
|
object MediaController {
|
||||||
|
private var initialVolume: Int? = null // Nullable to track the unset state
|
||||||
|
private lateinit var audioManager: AudioManager // Declare AudioManager
|
||||||
|
|
||||||
|
// Initialize the singleton with the AudioManager instance
|
||||||
|
fun initialize(audioManager: AudioManager) {
|
||||||
|
this.audioManager = audioManager
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
fun sendPause() {
|
fun sendPause() {
|
||||||
if (audioManager.isMusicActive) {
|
if (audioManager.isMusicActive) {
|
||||||
audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE))
|
audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE))
|
||||||
@@ -11,26 +21,35 @@ class MediaController (private val audioManager: AudioManager){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
fun sendPlay() {
|
fun sendPlay() {
|
||||||
if (!audioManager.isMusicActive) {
|
if (!audioManager.isMusicActive) {
|
||||||
audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY))
|
audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY))
|
||||||
audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY))
|
audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
|
||||||
|
@Synchronized
|
||||||
fun startSpeaking() {
|
fun startSpeaking() {
|
||||||
if (!audioManager.isMusicActive) {
|
Log.d("MediaController", "Starting speaking")
|
||||||
// reduce volume to 10% of initial volume
|
if (initialVolume == null) {
|
||||||
initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
initialVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (initialVolume * 0.1).toInt(), 0)
|
Log.d("MediaController", "Initial Volume Set: $initialVolume")
|
||||||
|
audioManager.setStreamVolume(
|
||||||
|
AudioManager.STREAM_MUSIC,
|
||||||
|
1, // Set to a lower volume when speaking starts
|
||||||
|
0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
Log.d("MediaController", "Initial Volume: $initialVolume")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
fun stopSpeaking() {
|
fun stopSpeaking() {
|
||||||
if (!audioManager.isMusicActive) {
|
Log.d("MediaController", "Stopping speaking, initialVolume: $initialVolume")
|
||||||
// restore initial volume
|
initialVolume?.let { volume ->
|
||||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, initialVolume, 0)
|
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0)
|
||||||
|
initialVolume = null // Reset to null after restoring the volume
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ data class Battery(val component: Int, val level: Int, val status: Int) : Parcel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Notifications {
|
class AirPodsNotifications {
|
||||||
companion object {
|
companion object {
|
||||||
const val AIRPODS_CONNECTED = "me.kavishdevar.aln.AIRPODS_CONNECTED"
|
const val AIRPODS_CONNECTED = "me.kavishdevar.aln.AIRPODS_CONNECTED"
|
||||||
const val AIRPODS_DATA = "me.kavishdevar.aln.AIRPODS_DATA"
|
const val AIRPODS_DATA = "me.kavishdevar.aln.AIRPODS_DATA"
|
||||||
|
|||||||
BIN
android/app/src/main/res/drawable/adaptive.png
Normal file
BIN
android/app/src/main/res/drawable/adaptive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
BIN
android/app/src/main/res/drawable/noise_cancellation.png
Normal file
BIN
android/app/src/main/res/drawable/noise_cancellation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
android/app/src/main/res/drawable/transparency.png
Normal file
BIN
android/app/src/main/res/drawable/transparency.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/font/hack.ttf
Normal file
BIN
android/app/src/main/res/font/hack.ttf
Normal file
Binary file not shown.
@@ -1,3 +1,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">ALN</string>
|
<string name="app_name">ALN</string>
|
||||||
|
<string name="title_activity_debug">DebugActivity</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -11,6 +11,7 @@ lifecycleRuntimeKtx = "2.8.6"
|
|||||||
activityCompose = "1.9.2"
|
activityCompose = "1.9.2"
|
||||||
composeBom = "2024.09.03"
|
composeBom = "2024.09.03"
|
||||||
annotations = "26.0.0"
|
annotations = "26.0.0"
|
||||||
|
navigationCompose = "2.8.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
|
||||||
@@ -30,6 +31,7 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
|
|||||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" }
|
annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" }
|
||||||
|
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user