mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-06-13 12:03:08 +00:00
android: add custom EQ settings (ios27)
will be released into stable as soon as I implement capability parsing
This commit is contained in:
@@ -130,6 +130,7 @@ import me.kavishdevar.librepods.presentation.screens.AirPodsSettingsScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.AppSettingsScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.CameraControlScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.DebugScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.EqualizerScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.HeadTrackingScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.HearingAidAdjustmentsScreen
|
||||
import me.kavishdevar.librepods.presentation.screens.HearingAidScreen
|
||||
@@ -479,6 +480,9 @@ fun Main() {
|
||||
val purchaseViewModel: PurchaseViewModel = viewModel()
|
||||
PurchaseScreen(purchaseViewModel, navController)
|
||||
}
|
||||
composable("equalizer_screen") {
|
||||
if (airPodsViewModel != null) EqualizerScreen(airPodsViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
package me.kavishdevar.librepods.bluetooth
|
||||
|
||||
import android.util.Log
|
||||
import me.kavishdevar.librepods.data.Capability
|
||||
import me.kavishdevar.librepods.data.CustomEq
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
@@ -47,7 +49,7 @@ class AACPManager {
|
||||
const val PROXIMITY_KEYS_REQ: Byte = 0x30
|
||||
const val PROXIMITY_KEYS_RSP: Byte = 0x31
|
||||
const val STEM_PRESS: Byte = 0x19
|
||||
const val EQ_DATA: Byte = 0x53
|
||||
const val HEADPHONE_ACCOMMODATION: Byte = 0x53
|
||||
const val CONNECTED_DEVICES: Byte = 0x2E // TiPi 1
|
||||
const val AUDIO_SOURCE: Byte = 0x0E // TiPi 2
|
||||
const val SMART_ROUTING: Byte = 0x10
|
||||
@@ -55,6 +57,7 @@ class AACPManager {
|
||||
const val SMART_ROUTING_RESP: Byte = 0x11
|
||||
const val SEND_CONNECTED_MAC: Byte = 0x14
|
||||
const val AUDIO_SOURCE_2: Byte = 0x0C // seems redundant?
|
||||
const val CUSTOM_EQ: Byte = 0x63
|
||||
}
|
||||
|
||||
private val HEADER_BYTES = byteArrayOf(0x04, 0x00, 0x04, 0x00)
|
||||
@@ -199,6 +202,11 @@ class AACPManager {
|
||||
var eqOnMedia: Boolean = false
|
||||
private set
|
||||
|
||||
var customEq: CustomEq = CustomEq(state = 1, low = 50, mid = 50, high = 50)
|
||||
private set
|
||||
|
||||
var customEqCallback: ((CustomEq) -> Unit)? = null
|
||||
|
||||
fun getControlCommandStatus(identifier: ControlCommandIdentifiers): ControlCommandStatus? {
|
||||
return controlCommandStatusList.find { it.identifier == identifier }
|
||||
}
|
||||
@@ -235,7 +243,9 @@ class AACPManager {
|
||||
fun onConnectedDevicesReceived(connectedDevices: List<ConnectedDevice>)
|
||||
fun onOwnershipToFalseRequest(sender: String, reasonReverseTapped: Boolean)
|
||||
fun onShowNearbyUI(sender: String)
|
||||
fun onEQPacketReceived(eqData: FloatArray)
|
||||
fun onHeadphoneAccommodationReceived(eqData: FloatArray)
|
||||
fun onCustomEqReceived(customEq: CustomEq)
|
||||
fun onCapabilitiesReceived(capabilities: List<Capability>)
|
||||
}
|
||||
|
||||
fun parseStemPressResponse(data: ByteArray): Pair<StemPressType, StemPressBudType> {
|
||||
@@ -548,18 +558,18 @@ class AACPManager {
|
||||
}
|
||||
}
|
||||
|
||||
Opcodes.EQ_DATA -> {
|
||||
Opcodes.HEADPHONE_ACCOMMODATION -> {
|
||||
if (packet.size != 140) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Received EQ_DATA packet of unexpected size: ${packet.size}, expected 140"
|
||||
"Received HEADPHONE_ACCOMMODATION packet of unexpected size: ${packet.size}, expected 140"
|
||||
)
|
||||
return
|
||||
}
|
||||
if (packet[6] != 0x84.toByte()) {
|
||||
Log.w(
|
||||
TAG,
|
||||
"Received EQ_DATA packet with unexpected identifier: ${packet[6].toHexString()}, expected 0x84"
|
||||
"Received HEADPHONE_ACCOMMODATION packet with unexpected identifier: ${packet[6].toHexString()}, expected 0x84"
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -582,7 +592,7 @@ class AACPManager {
|
||||
"EQ Data set to: ${eqData.toList()}, eqOnPhone: $eqOnPhone, eqOnMedia: $eqOnMedia"
|
||||
)
|
||||
|
||||
callback?.onEQPacketReceived(eqData)
|
||||
callback?.onHeadphoneAccommodationReceived(eqData)
|
||||
}
|
||||
|
||||
Opcodes.INFORMATION -> {
|
||||
@@ -591,6 +601,13 @@ class AACPManager {
|
||||
callback?.onDeviceInformationReceived(information)
|
||||
}
|
||||
|
||||
Opcodes.CUSTOM_EQ -> {
|
||||
Log.d(TAG, "Parsing CUSTOM_EQ: ${packet.toHexString()}")
|
||||
customEq = parseCustomEqPacket(packet)
|
||||
customEqCallback?.invoke(customEq)
|
||||
callback?.onCustomEqReceived(customEq)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.d(TAG, "Unhandled opcode received: ${opcode.toHexString()}")
|
||||
callback?.onUnknownPacketReceived(packet)
|
||||
@@ -1296,4 +1313,38 @@ class AACPManager {
|
||||
version3 = strings.getOrNull(10) ?: "",
|
||||
)
|
||||
}
|
||||
|
||||
fun sendCustomEqPacket(customEq: CustomEq): Boolean {
|
||||
return sendDataPacket(customEq.toPacket())
|
||||
}
|
||||
|
||||
fun parseCustomEqPacket(packet: ByteArray): CustomEq {
|
||||
val data = packet.sliceArray(6 until packet.size)
|
||||
|
||||
if (data.size < 7) {
|
||||
Log.e(TAG, "custom EQ packet length less than 7, returning default")
|
||||
return CustomEq(1, 50, 50, 50)
|
||||
}
|
||||
|
||||
val lengthLow = data[0].toInt() and 0xFF
|
||||
val lengthHigh = data[1].toInt() and 0xFF
|
||||
|
||||
val length = (lengthHigh shl 8) or lengthLow
|
||||
|
||||
if (length != 5) {
|
||||
Log.w(TAG, "parseCustomEqPacket: unexpected length ($length). parsing normally")
|
||||
}
|
||||
|
||||
val state = data[3].toInt()
|
||||
val low = data[4].toInt()
|
||||
val mid = data[5].toInt()
|
||||
val high = data[6].toInt()
|
||||
|
||||
return CustomEq(
|
||||
state,
|
||||
low,
|
||||
mid,
|
||||
high
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package me.kavishdevar.librepods.data
|
||||
|
||||
import me.kavishdevar.librepods.bluetooth.AACPManager
|
||||
|
||||
enum class CustomEqBand { LOW, MID, HIGH }
|
||||
|
||||
data class CustomEq(val state: Int, val low: Int, val mid: Int, val high: Int) {
|
||||
|
||||
fun isEnabled(): Boolean {
|
||||
return state == 2
|
||||
}
|
||||
|
||||
fun toPacket(): ByteArray {
|
||||
return byteArrayOf(
|
||||
AACPManager.Companion.Opcodes.CUSTOM_EQ, 0x00,
|
||||
0x05, 0x00, // length (LE)
|
||||
0x01, state.toByte(),
|
||||
low.toByte(), mid.toByte(), high.toByte()
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
require(low in 0..100) { "low must be between 0 and 100, was $low" }
|
||||
require(mid in 0..100) { "mid must be between 0 and 100, was $mid" }
|
||||
require(high in 0..100) { "high must be between 0 and 100, was $high" }
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,10 @@ import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
// TODO: Remove everything but Battery-related stuff
|
||||
|
||||
enum class Enums(val value: ByteArray) {
|
||||
NOISE_CANCELLATION(Capabilities.NOISE_CANCELLATION),
|
||||
NOISE_CANCELLATION(byteArrayOf(0x0d)),
|
||||
PREFIX(byteArrayOf(0x04, 0x00, 0x04, 0x00)),
|
||||
SETTINGS(byteArrayOf(0x09, 0x00)),
|
||||
NOISE_CANCELLATION_PREFIX(PREFIX.value + SETTINGS.value + NOISE_CANCELLATION.value),
|
||||
@@ -81,12 +83,12 @@ class AirPodsNotifications {
|
||||
const val AIRPODS_DISCONNECTED = "me.kavishdevar.librepods.AIRPODS_DISCONNECTED"
|
||||
const val AIRPODS_CONNECTION_DETECTED = "me.kavishdevar.librepods.AIRPODS_CONNECTION_DETECTED"
|
||||
const val DISCONNECT_RECEIVERS = "me.kavishdevar.librepods.DISCONNECT_RECEIVERS"
|
||||
const val EQ_DATA = "me.kavishdevar.librepods.EQ_DATA"
|
||||
const val EQ_DATA = "me.kavishdevar.librepods.HEADPHONE_ACCOMMODATION"
|
||||
const val AIRPODS_INFORMATION_UPDATED = "me.kavishdevar.librepods.AIRPODS_INFORMATION_UPDATED"
|
||||
}
|
||||
|
||||
class EarDetection {
|
||||
private val notificationBit = Capabilities.EAR_DETECTION
|
||||
private val notificationBit = 6.toByte()
|
||||
private val notificationPrefix = Enums.PREFIX.value + notificationBit
|
||||
|
||||
var status: List<Byte> = listOf(0x01, 0x01)
|
||||
@@ -243,13 +245,6 @@ class AirPodsNotifications {
|
||||
}
|
||||
}
|
||||
|
||||
class Capabilities {
|
||||
companion object {
|
||||
val NOISE_CANCELLATION = byteArrayOf(0x0d)
|
||||
val EAR_DETECTION = byteArrayOf(0x06)
|
||||
}
|
||||
}
|
||||
|
||||
fun isHeadTrackingData(data: ByteArray): Boolean {
|
||||
if (data.size <= 60) return false
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ fun AudioSettings(
|
||||
conversationalAwarenessCapability: Boolean,
|
||||
loudSoundReductionCapability: Boolean,
|
||||
adaptiveAudioCapability: Boolean,
|
||||
customEqCapability: Boolean,
|
||||
|
||||
adaptiveVolumeChecked: Boolean,
|
||||
onAdaptiveVolumeCheckedChange: (Boolean) -> Unit,
|
||||
@@ -157,6 +158,20 @@ fun AudioSettings(
|
||||
navController = navController,
|
||||
independent = false
|
||||
)
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = Color(0x40888888),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
)
|
||||
}
|
||||
if (customEqCapability) {
|
||||
NavigationButton(
|
||||
to = "equalizer_screen",
|
||||
name = stringResource(R.string.equalizer),
|
||||
navController = navController,
|
||||
independent = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +185,7 @@ fun AudioSettingsPreview() {
|
||||
conversationalAwarenessCapability = true,
|
||||
loudSoundReductionCapability = true,
|
||||
adaptiveAudioCapability = true,
|
||||
customEqCapability = true,
|
||||
adaptiveVolumeChecked = true,
|
||||
onAdaptiveVolumeCheckedChange = { },
|
||||
conversationalAwarenessChecked = true,
|
||||
|
||||
@@ -140,7 +140,7 @@ half4 main(float2 coord) {
|
||||
}
|
||||
drawRect(color)
|
||||
} else {
|
||||
if (isPressed) {
|
||||
if (isPressed && enabled) {
|
||||
drawRect(Color.Black.copy(alpha = 0.4f))
|
||||
drawRect(Color.White.copy(alpha = 0.2f))
|
||||
}
|
||||
@@ -264,29 +264,38 @@ half4 main(float2 coord) {
|
||||
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
||||
val offsetAnimationSpec = spring(1f, 300f, Offset.VisibilityThreshold)
|
||||
val onDragStop: () -> Unit = {
|
||||
scope.launch {
|
||||
launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) }
|
||||
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
||||
launch {
|
||||
offsetAnimation.animateTo(
|
||||
Offset.Zero,
|
||||
offsetAnimationSpec
|
||||
)
|
||||
if (enabled) {
|
||||
scope.launch {
|
||||
launch { haptics.performHapticFeedback(HapticFeedbackType.Reject) }
|
||||
launch {
|
||||
progressAnimation.animateTo(
|
||||
0f,
|
||||
progressAnimationSpec
|
||||
)
|
||||
}
|
||||
launch {
|
||||
offsetAnimation.animateTo(
|
||||
Offset.Zero,
|
||||
offsetAnimationSpec
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
inspectDragGestures(
|
||||
onDragStart = { down ->
|
||||
pressStartPosition = down.position
|
||||
scope.launch {
|
||||
launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) }
|
||||
launch {
|
||||
progressAnimation.animateTo(
|
||||
1f,
|
||||
progressAnimationSpec
|
||||
)
|
||||
if (enabled) {
|
||||
scope.launch {
|
||||
launch { haptics.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) }
|
||||
launch {
|
||||
progressAnimation.animateTo(
|
||||
1f,
|
||||
progressAnimationSpec
|
||||
)
|
||||
}
|
||||
launch { offsetAnimation.snapTo(Offset.Zero) }
|
||||
}
|
||||
launch { offsetAnimation.snapTo(Offset.Zero) }
|
||||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
@@ -294,11 +303,13 @@ half4 main(float2 coord) {
|
||||
},
|
||||
onDragCancel = onDragStop
|
||||
) { _, dragAmount ->
|
||||
scope.launch {
|
||||
if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback(
|
||||
HapticFeedbackType.SegmentFrequentTick
|
||||
)
|
||||
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
|
||||
if (enabled) {
|
||||
scope.launch {
|
||||
if (dragAmount.getDistanceSquared() > 350) haptics.performHapticFeedback(
|
||||
HapticFeedbackType.SegmentFrequentTick
|
||||
)
|
||||
offsetAnimation.snapTo(offsetAnimation.value + dragAmount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +365,7 @@ fun AirPodsSettingsScreen(viewModel: AirPodsViewModel, navController: NavControl
|
||||
conversationalAwarenessCapability = conversationalAwarenessCapability,
|
||||
loudSoundReductionCapability = loudSoundReductionCapability,
|
||||
adaptiveAudioCapability = adaptiveAudioCapability,
|
||||
customEqCapability = true,
|
||||
adaptiveVolumeChecked = adaptiveVolumeChecked,
|
||||
onAdaptiveVolumeCheckedChange = { checked ->
|
||||
viewModel.setControlCommandBoolean(
|
||||
|
||||
@@ -0,0 +1,658 @@
|
||||
/*
|
||||
LibrePods - AirPods liberated from Apple’s ecosystem
|
||||
Copyright (C) 2025 LibrePods contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.kavishdevar.librepods.presentation.screens
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.visible
|
||||
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.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.style.TextAlign
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import com.kyant.backdrop.drawBackdrop
|
||||
import com.kyant.backdrop.effects.lens
|
||||
import com.kyant.backdrop.highlight.Highlight
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import me.kavishdevar.librepods.R
|
||||
import me.kavishdevar.librepods.presentation.components.SelectItem
|
||||
import me.kavishdevar.librepods.presentation.components.StyledButton
|
||||
import me.kavishdevar.librepods.presentation.components.StyledScaffold
|
||||
import me.kavishdevar.librepods.presentation.components.StyledSelectList
|
||||
import me.kavishdevar.librepods.presentation.viewmodel.AirPodsViewModel
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
fun EqualizerScreen(viewModel: AirPodsViewModel) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
val customEq = state.customEq
|
||||
val enabled = customEq.isEnabled()
|
||||
|
||||
val recommendedString = stringResource(R.string.recommended)
|
||||
val customString = stringResource(R.string.custom)
|
||||
|
||||
val eqStateOptions = remember(state.customEq) {
|
||||
listOf(
|
||||
SelectItem(
|
||||
name = recommendedString,
|
||||
selected = !enabled,
|
||||
onClick = { viewModel.setCustomEqEnabled(false) }
|
||||
),
|
||||
SelectItem(
|
||||
name = customString,
|
||||
selected = enabled,
|
||||
onClick = { viewModel.setCustomEqEnabled(true) }
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
StyledScaffold(
|
||||
title = stringResource(R.string.equalizer)
|
||||
) { spacerHeight ->
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
|
||||
val height = 200.dp
|
||||
val maxOffset = with(LocalDensity.current) { height.toPx() } / 2
|
||||
|
||||
val offsets = remember(state.customEq) {
|
||||
listOf(
|
||||
mutableFloatStateOf(lerp(maxOffset, -maxOffset, customEq.low.toFloat() / 100)),
|
||||
mutableFloatStateOf(lerp(maxOffset, -maxOffset, customEq.mid.toFloat() / 100)),
|
||||
mutableFloatStateOf(lerp(maxOffset, -maxOffset, customEq.high.toFloat() / 100))
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(spacerHeight))
|
||||
StyledSelectList(items = eqStateOptions)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
val backgroundColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
|
||||
|
||||
Crossfade (
|
||||
customEq.isEnabled()
|
||||
) { visible ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.visible(visible),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(28.dp))
|
||||
) {
|
||||
val dashColor =
|
||||
if (isSystemInDarkTheme()) Color(0x80AAAAAA) else Color(0x809D9D9D)
|
||||
// LaunchedEffect(offsets[0].floatValue, offsets[1].floatValue, offsets[2].floatValue) {
|
||||
// val low = ((offsets[0].floatValue / (2 * maxOffset) + 0.5f) * 100).roundToInt()
|
||||
// val mid = ((offsets[1].floatValue / (2 * maxOffset) + 0.5f) * 100).roundToInt()
|
||||
// val high = ((offsets[2].floatValue / (2 * maxOffset) + 0.5f) * 100).roundToInt()
|
||||
// Log.d("EqualizerScreen", "$low, $mid, $high")
|
||||
// viewModel.setCustomEq(
|
||||
// low = low,
|
||||
// mid = mid,
|
||||
// high = high
|
||||
// )
|
||||
// }
|
||||
|
||||
LaunchedEffect(offsets) {
|
||||
snapshotFlow {
|
||||
Triple(
|
||||
offsets[0].floatValue,
|
||||
offsets[1].floatValue,
|
||||
offsets[2].floatValue
|
||||
)
|
||||
}
|
||||
.debounce(100.milliseconds) // cool, should've been using this since the very beginning
|
||||
.collect { (lowF, midF, highF) ->
|
||||
val low =
|
||||
100 - ((lowF / (2 * maxOffset) + 0.5f) * 100).roundToInt()
|
||||
val mid =
|
||||
100 - ((midF / (2 * maxOffset) + 0.5f) * 100).roundToInt()
|
||||
val high =
|
||||
100 - ((highF / (2 * maxOffset) + 0.5f) * 100).roundToInt()
|
||||
|
||||
viewModel.setCustomEq(low, mid, high)
|
||||
}
|
||||
}
|
||||
|
||||
val backdrop = rememberLayerBackdrop()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(backgroundColor, RoundedCornerShape(28.dp))
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(42.dp))
|
||||
// Row(
|
||||
// modifier = Modifier
|
||||
// .fillMaxWidth()
|
||||
// .padding(18.dp),
|
||||
// verticalAlignment = Alignment.CenterVertically,
|
||||
// horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
// ) {
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .size(64.dp)
|
||||
// .background(if (isSystemInDarkTheme()) Color.DarkGray else Color.LightGray, RoundedCornerShape(12.dp))
|
||||
// )
|
||||
// Column(
|
||||
// modifier = Modifier
|
||||
// .weight(1f),
|
||||
// verticalArrangement = Arrangement.Center
|
||||
// ) {
|
||||
// Text(
|
||||
// text = "Written into Changes",
|
||||
// style = TextStyle(
|
||||
// fontSize = 16.sp,
|
||||
// fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
// fontWeight = FontWeight.Bold,
|
||||
// color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||
// )
|
||||
// )
|
||||
// Spacer(modifier = Modifier.height(4.dp))
|
||||
// Text(
|
||||
// text = "Avalon Emerson",
|
||||
// style = TextStyle(
|
||||
// fontSize = 14.sp,
|
||||
// fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
// fontWeight = FontWeight.Normal,
|
||||
// color = if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// val paused = remember { mutableStateOf(false) }
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .size(48.dp)
|
||||
// .background(Color(0x600091FF), CircleShape)
|
||||
// .clickable(
|
||||
// interactionSource = remember { MutableInteractionSource() },
|
||||
// indication = null,
|
||||
// ) {
|
||||
// paused.value = !paused.value
|
||||
// },
|
||||
// contentAlignment = Alignment.Center
|
||||
// ) {
|
||||
// Crossfade(
|
||||
// targetState = paused.value,
|
||||
// label = "media_icon"
|
||||
// ) { p ->
|
||||
// Text(
|
||||
// text = if (p) "" else "",
|
||||
// style = TextStyle(
|
||||
// fontSize = 24.sp,
|
||||
// fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
// fontWeight = FontWeight.Normal,
|
||||
// color = Color(0xFF0091FF),
|
||||
// textAlign = TextAlign.Center
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// HorizontalDivider(
|
||||
// thickness = 1.dp,
|
||||
// color = Color(0x40888888),
|
||||
// modifier = Modifier
|
||||
// .padding(horizontal = 20.dp)
|
||||
// .padding(bottom = 16.dp)
|
||||
// )
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
fun colorFromY(y: Float): Color {
|
||||
val f = ((y + maxOffset) / (2f * maxOffset)).coerceIn(0f, 1f)
|
||||
val stops = listOf(
|
||||
0.0f to Color(0xFFFFA300),
|
||||
0.25f to Color(0xFFFCE600),
|
||||
0.5f to Color(0xFF00FAAF),
|
||||
0.75f to Color(0xFF00FAFF),
|
||||
1.0f to Color(0xFF00B5FF)
|
||||
)
|
||||
val (start, end) = stops.zipWithNext()
|
||||
.first { f <= it.second.first }
|
||||
val c = (f - start.first) / (end.first - start.first)
|
||||
return lerp(start.second, end.second, c)
|
||||
}
|
||||
|
||||
fun pathBrush(
|
||||
startY: Float,
|
||||
endY: Float,
|
||||
): Brush {
|
||||
val stops = (0..20).map { i ->
|
||||
val t = i / 20f
|
||||
val y = lerp(startY, endY, t)
|
||||
t to colorFromY(y)
|
||||
}
|
||||
|
||||
return Brush.linearGradient(
|
||||
colorStops = stops.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().layerBackdrop(backdrop)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(height)
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
val dashCount = (height / 10.dp).toInt()
|
||||
repeat(3) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.weight(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
for (i in 1..(dashCount)) {
|
||||
val t = i.toFloat() / dashCount
|
||||
val centerDistance = abs(0.5f - t)
|
||||
val alpha = 1f - (centerDistance * 2f)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(9.dp)
|
||||
.width(0.75.dp)
|
||||
.background(
|
||||
dashColor.copy(alpha),
|
||||
RoundedCornerShape(28.dp)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
val canvasWidth = size.width
|
||||
|
||||
drawLine(
|
||||
color = backgroundColor,
|
||||
start = Offset(
|
||||
x = 0f,
|
||||
y = offsets[0].floatValue + maxOffset
|
||||
),
|
||||
end = Offset(
|
||||
x = 1 / 6f * canvasWidth,
|
||||
y = offsets[0].floatValue + maxOffset
|
||||
),
|
||||
strokeWidth = 10f
|
||||
)
|
||||
drawLine(
|
||||
color = colorFromY(offsets[0].floatValue),
|
||||
start = Offset(
|
||||
x = 0f,
|
||||
y = offsets[0].floatValue + maxOffset
|
||||
),
|
||||
end = Offset(
|
||||
x = 1 / 6f * canvasWidth,
|
||||
y = offsets[0].floatValue + maxOffset
|
||||
),
|
||||
strokeWidth = 8f
|
||||
)
|
||||
|
||||
val lowToMidPath = Path()
|
||||
lowToMidPath.moveTo(
|
||||
x = 1 / 6f * canvasWidth,
|
||||
y = offsets[0].floatValue + maxOffset
|
||||
)
|
||||
lowToMidPath.cubicTo(
|
||||
x1 = canvasWidth * 1 / 6f + 108.dp.value,
|
||||
y1 = offsets[0].floatValue + maxOffset,
|
||||
x2 = canvasWidth * 0.5f - 108.dp.value,
|
||||
y2 = offsets[1].floatValue + maxOffset,
|
||||
x3 = canvasWidth * 0.5f,
|
||||
y3 = offsets[1].floatValue + maxOffset
|
||||
)
|
||||
drawPath(
|
||||
color = backgroundColor,
|
||||
path = lowToMidPath,
|
||||
style = Stroke(width = 10f)
|
||||
)
|
||||
drawPath(
|
||||
brush = pathBrush(
|
||||
offsets[0].floatValue,
|
||||
offsets[1].floatValue
|
||||
),
|
||||
path = lowToMidPath,
|
||||
style = Stroke(width = 8f)
|
||||
)
|
||||
|
||||
val midToHighPath = Path()
|
||||
midToHighPath.moveTo(
|
||||
x = 0.5f * canvasWidth,
|
||||
y = offsets[1].floatValue + maxOffset
|
||||
)
|
||||
midToHighPath.cubicTo(
|
||||
x1 = canvasWidth * 0.5f + 108.dp.value,
|
||||
y1 = offsets[1].floatValue + maxOffset,
|
||||
x2 = canvasWidth * 5 / 6f - 108.dp.value,
|
||||
y2 = offsets[2].floatValue + maxOffset,
|
||||
x3 = canvasWidth * 5 / 6f,
|
||||
y3 = offsets[2].floatValue + maxOffset
|
||||
)
|
||||
drawPath(
|
||||
color = backgroundColor,
|
||||
path = midToHighPath,
|
||||
style = Stroke(width = 10f)
|
||||
)
|
||||
drawPath(
|
||||
brush = pathBrush(
|
||||
offsets[1].floatValue,
|
||||
offsets[2].floatValue
|
||||
),
|
||||
path = midToHighPath,
|
||||
style = Stroke(width = 8f)
|
||||
)
|
||||
drawLine(
|
||||
color = backgroundColor,
|
||||
start = Offset(
|
||||
x = 5 / 6f * canvasWidth,
|
||||
y = offsets[2].floatValue + maxOffset
|
||||
),
|
||||
end = Offset(
|
||||
x = 1f * canvasWidth,
|
||||
y = offsets[2].floatValue + maxOffset
|
||||
),
|
||||
strokeWidth = 10f
|
||||
)
|
||||
drawLine(
|
||||
color = colorFromY(offsets[2].floatValue),
|
||||
start = Offset(
|
||||
x = 5 / 6f * canvasWidth,
|
||||
y = offsets[2].floatValue + maxOffset
|
||||
),
|
||||
end = Offset(
|
||||
x = 1f * canvasWidth,
|
||||
y = offsets[2].floatValue + maxOffset
|
||||
),
|
||||
strokeWidth = 8f
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp, horizontal = 20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = "Low".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(
|
||||
0.2f
|
||||
),
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = "Mid".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(
|
||||
0.2f
|
||||
),
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = "High".uppercase(),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = (if (isSystemInDarkTheme()) Color.White else Color.Black).copy(
|
||||
0.2f
|
||||
),
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(height)
|
||||
.padding(horizontal = 20.dp),
|
||||
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
for (i in 0..2) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
val pressed = remember { mutableStateOf(false) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset {
|
||||
IntOffset(
|
||||
x = 0,
|
||||
y = offsets[i].floatValue.roundToInt()
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Crossfade(
|
||||
pressed.value
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.then(
|
||||
if (it) {
|
||||
Modifier.drawBackdrop(
|
||||
backdrop = backdrop,
|
||||
shape = { CircleShape },
|
||||
highlight = {
|
||||
Highlight.Ambient
|
||||
},
|
||||
onDrawSurface = {
|
||||
drawCircle(
|
||||
color = Color.White.copy(
|
||||
0.2f
|
||||
),
|
||||
radius = size.height
|
||||
)
|
||||
drawCircle(
|
||||
color = colorFromY(
|
||||
offsets[i].floatValue
|
||||
),
|
||||
style = Stroke(2.dp.value),
|
||||
radius = size.height / 2
|
||||
)
|
||||
},
|
||||
effects = {
|
||||
lens(
|
||||
refractionHeight = 32f.dp.value,
|
||||
refractionAmount = size.height
|
||||
)
|
||||
}
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.background(
|
||||
colorFromY(offsets[i].floatValue),
|
||||
CircleShape
|
||||
)
|
||||
.border(
|
||||
2.5.dp,
|
||||
backgroundColor,
|
||||
CircleShape
|
||||
)
|
||||
.draggable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberDraggableState { delta ->
|
||||
offsets[i].floatValue =
|
||||
(offsets[i].floatValue + delta).coerceIn(
|
||||
-maxOffset,
|
||||
maxOffset
|
||||
)
|
||||
},
|
||||
onDragStarted = {
|
||||
pressed.value = true
|
||||
},
|
||||
onDragStopped = {
|
||||
pressed.value = false
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val resetButtonEnabled = remember { derivedStateOf { !offsets.all { it.floatValue == 0f } } }
|
||||
|
||||
StyledButton(
|
||||
onClick = {
|
||||
offsets[0].floatValue = 0f
|
||||
offsets[1].floatValue = 0f
|
||||
offsets[2].floatValue = 0f
|
||||
},
|
||||
backdrop = rememberLayerBackdrop(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isInteractive = false,
|
||||
surfaceColor = backgroundColor,
|
||||
enabled = resetButtonEnabled.value
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.reset),
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily(Font(R.font.sf_pro)),
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = if (!offsets.all { it.floatValue == 0f }) Color(0xFF0093FF) else Color.Gray
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ import me.kavishdevar.librepods.data.BatteryComponent
|
||||
import me.kavishdevar.librepods.data.BatteryStatus
|
||||
import me.kavishdevar.librepods.data.Capability
|
||||
import me.kavishdevar.librepods.data.ControlCommandRepository
|
||||
import me.kavishdevar.librepods.data.CustomEq
|
||||
import me.kavishdevar.librepods.data.StemAction
|
||||
import me.kavishdevar.librepods.data.XposedRemotePrefProvider
|
||||
import me.kavishdevar.librepods.services.AirPodsService
|
||||
@@ -97,7 +98,9 @@ data class AirPodsUiState(
|
||||
val dynamicEndOfCharge: Boolean = false,
|
||||
|
||||
val connectionSuccessful: Boolean = false,
|
||||
val timeUntilFOSSPremiumExpiry: Long = 0L
|
||||
val timeUntilFOSSPremiumExpiry: Long = 0L,
|
||||
|
||||
val customEq: CustomEq = CustomEq(1, 50, 50, 50) // disabled
|
||||
)
|
||||
|
||||
class AirPodsViewModel(
|
||||
@@ -140,13 +143,36 @@ class AirPodsViewModel(
|
||||
_cameraAction.value = action
|
||||
}
|
||||
|
||||
fun setCustomEq(low: Int, mid: Int, high: Int) {
|
||||
require(low in 0..100)
|
||||
require(mid in 0..100)
|
||||
require(high in 0..100)
|
||||
val updatedEq = _uiState.value.customEq.copy(low = low, mid = mid, high = high)
|
||||
service.aacpManager.sendCustomEqPacket(updatedEq)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
customEq = updatedEq
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCustomEqEnabled(enabled: Boolean) {
|
||||
service.aacpManager.sendCustomEqPacket(_uiState.value.customEq.copy(state = if (enabled) 2 else 1))
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
customEq = it.customEq.copy(state = if (enabled) 2 else 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
observeBroadcasts()
|
||||
loadName()
|
||||
loadInstance()
|
||||
loadSharedPreferences()
|
||||
setupControlObservers()
|
||||
observeAACP()
|
||||
loadControlList()
|
||||
loadEq()
|
||||
loadATT()
|
||||
observeATT()
|
||||
observeSharedPreferences()
|
||||
@@ -158,7 +184,7 @@ class AirPodsViewModel(
|
||||
listeners.forEach { (id, listener) ->
|
||||
controlRepo.remove(id, listener)
|
||||
}
|
||||
|
||||
service.aacpManager.customEqCallback = null
|
||||
appContext.unregisterReceiver(broadcastReceiver)
|
||||
|
||||
super.onCleared()
|
||||
@@ -315,7 +341,7 @@ class AirPodsViewModel(
|
||||
}
|
||||
|
||||
// I'm lazy, sorry.
|
||||
fun setupControlObservers() {
|
||||
fun observeAACP() {
|
||||
val identifiersList = listOf(
|
||||
ControlCommandIdentifiers.MIC_MODE,
|
||||
ControlCommandIdentifiers.DOUBLE_CLICK_INTERVAL,
|
||||
@@ -347,6 +373,9 @@ class AirPodsViewModel(
|
||||
for (identifier in identifiersList) {
|
||||
observeControl(identifier)
|
||||
}
|
||||
service.aacpManager.customEqCallback = { customEq ->
|
||||
_uiState.update { it.copy(customEq = customEq) }
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshInitialData() {
|
||||
@@ -479,6 +508,14 @@ class AirPodsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadEq() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
customEq = service.aacpManager.customEq
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadInstance() {
|
||||
val instance = service.airpodsInstance ?: AirPodsInstance(
|
||||
name = "AirPods",
|
||||
|
||||
@@ -96,6 +96,8 @@ import me.kavishdevar.librepods.data.AirPodsNotifications
|
||||
import me.kavishdevar.librepods.data.Battery
|
||||
import me.kavishdevar.librepods.data.BatteryComponent
|
||||
import me.kavishdevar.librepods.data.BatteryStatus
|
||||
import me.kavishdevar.librepods.data.Capability
|
||||
import me.kavishdevar.librepods.data.CustomEq
|
||||
import me.kavishdevar.librepods.data.StemAction
|
||||
import me.kavishdevar.librepods.data.XposedRemotePrefProvider
|
||||
import me.kavishdevar.librepods.data.isHeadTrackingData
|
||||
@@ -1159,13 +1161,21 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEQPacketReceived(eqData: FloatArray) {
|
||||
override fun onHeadphoneAccommodationReceived(eqData: FloatArray) {
|
||||
sendBroadcast(
|
||||
Intent(AirPodsNotifications.EQ_DATA).putExtra("eqData", eqData).apply {
|
||||
setPackage(packageName)
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCustomEqReceived(customEq: CustomEq) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override fun onCapabilitiesReceived(capabilities: List<Capability>) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override fun onUnknownPacketReceived(packet: ByteArray) {
|
||||
Log.d(
|
||||
"AACPManager",
|
||||
@@ -2839,7 +2849,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
||||
}
|
||||
|
||||
} else if (bytesRead == -1) {
|
||||
Log.d("AirPods Service", "BluetoothConnectionManager.getAACPSocket()? closed (bytesRead = -1)")
|
||||
Log.d("AirPodsService", "socket closed (bytesRead = -1)")
|
||||
sendBroadcast(Intent(AirPodsNotifications.AIRPODS_DISCONNECTED).apply {
|
||||
setPackage(packageName)
|
||||
})
|
||||
|
||||
@@ -277,4 +277,6 @@
|
||||
<string name="optimized_charging_description">AirPods can learn from your daily usage and determine when to charge to an optmized limit and when to allow or full charge. This limit adapts to your daily usage and preserves your battery lifespan over time.\nThis setting may not affect unsupported AirPods, or AirPods on an older firmware version.</string>
|
||||
<string name="enable_app_in_xposed_or_update_device">Enable LibrePods in Xposed or update your device to proceed.</string>
|
||||
<string name="play_foss_premium_banner">Due to an error in billing, premium access will expire in %1$d days. If you already upgraded the app, please click on this message to email billing@kavish.xyz to restore or verify access. Apologies for the inconvenience.</string>
|
||||
<string name="custom">Custom</string>
|
||||
<string name="recommended">Recommended</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user