android: clean up a lot of stuff

This commit is contained in:
Kavish Devar
2025-09-10 12:38:27 +05:30
parent aecbb066b5
commit fa00620b5b
39 changed files with 269 additions and 263 deletions

View File

@@ -35,8 +35,6 @@
<uses-permission android:name="android.permission.LOCAL_MAC_ADDRESS" <uses-permission android:name="android.permission.LOCAL_MAC_ADDRESS"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<protected-broadcast android:name="batterywidget.impl.action.update_bluetooth_data" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@@ -95,7 +93,7 @@
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="librepods" <data android:scheme="librepods"
android:host="add-magic-keys" /> android:host="add-magic-keys" />
</intent-filter> --> </intent-filter> -->
</activity> </activity>

View File

@@ -1,24 +1,23 @@
/* /*
* LibrePods - AirPods liberated from Apples ecosystem * LibrePods - AirPods liberated from Apples ecosystem
* *
* Copyright (C) 2025 LibrePods contributors * Copyright (C) 2025 LibrePods contributors
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published * it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License. * by the Free Software Foundation, either version 3 of the License.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package me.kavishdevar.librepods package me.kavishdevar.librepods
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
@@ -29,26 +28,12 @@ import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresPermission import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.foundation.layout.Arrangement import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import me.kavishdevar.librepods.screens.AccessibilitySettingsScreen
import me.kavishdevar.librepods.screens.EqualizerSettingsScreen
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
import org.lsposed.hiddenapibypass.HiddenApiBypass
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -56,36 +41,40 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import me.kavishdevar.librepods.screens.AccessibilitySettingsScreen
import me.kavishdevar.librepods.screens.EqualizerSettingsScreen
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
import org.lsposed.hiddenapibypass.HiddenApiBypass
import java.io.IOException import java.io.IOException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
@Suppress("PrivatePropertyName")
class CustomDevice : ComponentActivity() { class CustomDevice : ComponentActivity() {
private val TAG = "AirPodsAccessibilitySettings" private val TAG = "AirPodsAccessibilitySettings"
private var socket: BluetoothSocket? = null private var socket: BluetoothSocket? = null
private val deviceAddress = "28:2D:7F:C2:05:5B" private val deviceAddress = "28:2D:7F:C2:05:5B"
private val psm = 31
private val uuid: ParcelUuid = ParcelUuid.fromString("00000000-0000-0000-0000-00000000000") private val uuid: ParcelUuid = ParcelUuid.fromString("00000000-0000-0000-0000-00000000000")
// Data states // Data states
private val isConnected = mutableStateOf(false) private val isConnected = mutableStateOf(false)
private val leftAmplification = mutableStateOf(1.0f) private val leftAmplification = mutableFloatStateOf(1.0f)
private val leftTone = mutableStateOf(1.0f) private val leftTone = mutableFloatStateOf(1.0f)
private val leftAmbientNoiseReduction = mutableStateOf(0.5f) private val leftAmbientNoiseReduction = mutableFloatStateOf(0.5f)
private val leftConversationBoost = mutableStateOf(false) private val leftConversationBoost = mutableStateOf(false)
private val leftEQ = mutableStateOf(FloatArray(8) { 50.0f }) private val leftEQ = mutableStateOf(FloatArray(8) { 50.0f })
private val rightAmplification = mutableStateOf(1.0f) private val rightAmplification = mutableFloatStateOf(1.0f)
private val rightTone = mutableStateOf(1.0f) private val rightTone = mutableFloatStateOf(1.0f)
private val rightAmbientNoiseReduction = mutableStateOf(0.5f) private val rightAmbientNoiseReduction = mutableFloatStateOf(0.5f)
private val rightConversationBoost = mutableStateOf(false) private val rightConversationBoost = mutableStateOf(false)
private val rightEQ = mutableStateOf(FloatArray(8) { 50.0f }) private val rightEQ = mutableStateOf(FloatArray(8) { 50.0f })
private val singleMode = mutableStateOf(false) private val singleMode = mutableStateOf(false)
private val amplification = mutableStateOf(1.0f) private val amplification = mutableFloatStateOf(1.0f)
private val balance = mutableStateOf(0.5f) private val balance = mutableFloatStateOf(0.5f)
private val retryCount = mutableStateOf(0) private val retryCount = mutableIntStateOf(0)
private val showRetryButton = mutableStateOf(false) private val showRetryButton = mutableStateOf(false)
private val maxRetries = 3 private val maxRetries = 3
@@ -146,18 +135,19 @@ class CustomDevice : ComponentActivity() {
socket?.close() socket?.close()
} }
@SuppressLint("MissingPermission")
private suspend fun connectL2CAP() { private suspend fun connectL2CAP() {
retryCount.value = 0 retryCount.intValue = 0
// Close any existing socket // Close any existing socket
socket?.close() socket?.close()
socket = null socket = null
while (retryCount.value < maxRetries) { while (retryCount.intValue < maxRetries) {
try { try {
Log.d(TAG, "Starting L2CAP connection setup, attempt ${retryCount.value + 1}") Log.d(TAG, "Starting L2CAP connection setup, attempt ${retryCount.intValue + 1}")
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;") HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
val manager = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager val manager = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
val device: BluetoothDevice = manager.adapter.getRemoteDevice(deviceAddress) val device: BluetoothDevice = manager.adapter.getRemoteDevice(deviceAddress)
socket = createBluetoothSocket(device, psm) socket = createBluetoothSocket(device)
withTimeout(5000L) { withTimeout(5000L) {
socket?.connect() socket?.connect()
@@ -177,9 +167,9 @@ class CustomDevice : ComponentActivity() {
return return
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to connect, attempt ${retryCount.value + 1}: ${e.message}") Log.e(TAG, "Failed to connect, attempt ${retryCount.intValue + 1}: ${e.message}")
retryCount.value++ retryCount.intValue++
if (retryCount.value < maxRetries) { if (retryCount.intValue < maxRetries) {
delay(2000) // Wait 2 seconds before retry delay(2000) // Wait 2 seconds before retry
} }
} }
@@ -193,7 +183,7 @@ class CustomDevice : ComponentActivity() {
} }
} }
private fun createBluetoothSocket(device: BluetoothDevice, psm: Int): BluetoothSocket { private fun createBluetoothSocket(device: BluetoothDevice): BluetoothSocket {
val type = 3 // L2CAP val type = 3 // L2CAP
val constructorSpecs = listOf( val constructorSpecs = listOf(
arrayOf(device, type, true, true, 31, uuid), arrayOf(device, type, true, true, 31, uuid),
@@ -300,18 +290,18 @@ class CustomDevice : ComponentActivity() {
leftEQ.value = newLeftEQ leftEQ.value = newLeftEQ
if (singleMode.value) rightEQ.value = newLeftEQ if (singleMode.value) rightEQ.value = newLeftEQ
leftAmplification.value = buffer.float leftAmplification.floatValue = buffer.float
Log.d(TAG, "Parsed left amplification: ${leftAmplification.value}") Log.d(TAG, "Parsed left amplification: ${leftAmplification.floatValue}")
leftTone.value = buffer.float leftTone.floatValue = buffer.float
Log.d(TAG, "Parsed left tone: ${leftTone.value}") Log.d(TAG, "Parsed left tone: ${leftTone.floatValue}")
if (singleMode.value) rightTone.value = leftTone.value if (singleMode.value) rightTone.floatValue = leftTone.floatValue
val leftConvFloat = buffer.float val leftConvFloat = buffer.float
leftConversationBoost.value = leftConvFloat > 0.5f leftConversationBoost.value = leftConvFloat > 0.5f
Log.d(TAG, "Parsed left conversation boost: $leftConvFloat (${leftConversationBoost.value})") Log.d(TAG, "Parsed left conversation boost: $leftConvFloat (${leftConversationBoost.value})")
if (singleMode.value) rightConversationBoost.value = leftConversationBoost.value if (singleMode.value) rightConversationBoost.value = leftConversationBoost.value
leftAmbientNoiseReduction.value = buffer.float leftAmbientNoiseReduction.floatValue = buffer.float
Log.d(TAG, "Parsed left ambient noise reduction: ${leftAmbientNoiseReduction.value}") Log.d(TAG, "Parsed left ambient noise reduction: ${leftAmbientNoiseReduction.floatValue}")
if (singleMode.value) rightAmbientNoiseReduction.value = leftAmbientNoiseReduction.value if (singleMode.value) rightAmbientNoiseReduction.floatValue = leftAmbientNoiseReduction.floatValue
// Right bud // Right bud
val newRightEQ = rightEQ.value.copyOf() val newRightEQ = rightEQ.value.copyOf()
@@ -321,24 +311,24 @@ class CustomDevice : ComponentActivity() {
} }
rightEQ.value = newRightEQ rightEQ.value = newRightEQ
rightAmplification.value = buffer.float rightAmplification.floatValue = buffer.float
Log.d(TAG, "Parsed right amplification: ${rightAmplification.value}") Log.d(TAG, "Parsed right amplification: ${rightAmplification.floatValue}")
rightTone.value = buffer.float rightTone.floatValue = buffer.float
Log.d(TAG, "Parsed right tone: ${rightTone.value}") Log.d(TAG, "Parsed right tone: ${rightTone.floatValue}")
val rightConvFloat = buffer.float val rightConvFloat = buffer.float
rightConversationBoost.value = rightConvFloat > 0.5f rightConversationBoost.value = rightConvFloat > 0.5f
Log.d(TAG, "Parsed right conversation boost: $rightConvFloat (${rightConversationBoost.value})") Log.d(TAG, "Parsed right conversation boost: $rightConvFloat (${rightConversationBoost.value})")
rightAmbientNoiseReduction.value = buffer.float rightAmbientNoiseReduction.floatValue = buffer.float
Log.d(TAG, "Parsed right ambient noise reduction: ${rightAmbientNoiseReduction.value}") Log.d(TAG, "Parsed right ambient noise reduction: ${rightAmbientNoiseReduction.floatValue}")
Log.d(TAG, "Settings parsed successfully") Log.d(TAG, "Settings parsed successfully")
// Update single mode values if in single mode // Update single mode values if in single mode
if (singleMode.value) { if (singleMode.value) {
val avg = (leftAmplification.value + rightAmplification.value) / 2 val avg = (leftAmplification.floatValue + rightAmplification.floatValue) / 2
amplification.value = avg.coerceIn(0f, 1f) amplification.floatValue = avg.coerceIn(0f, 1f)
val diff = rightAmplification.value - leftAmplification.value val diff = rightAmplification.floatValue - leftAmplification.floatValue
balance.value = (0.5f + diff / (2 * avg)).coerceIn(0f, 1f) balance.floatValue = (0.5f + diff / (2 * avg)).coerceIn(0f, 1f)
} }
} }
@@ -363,19 +353,19 @@ class CustomDevice : ComponentActivity() {
for (eq in leftEQ.value) { for (eq in leftEQ.value) {
buffer.putFloat(eq) buffer.putFloat(eq)
} }
buffer.putFloat(leftAmplification.value) buffer.putFloat(leftAmplification.floatValue)
buffer.putFloat(leftTone.value) buffer.putFloat(leftTone.floatValue)
buffer.putFloat(if (leftConversationBoost.value) 1.0f else 0.0f) buffer.putFloat(if (leftConversationBoost.value) 1.0f else 0.0f)
buffer.putFloat(leftAmbientNoiseReduction.value) buffer.putFloat(leftAmbientNoiseReduction.floatValue)
// Right bud // Right bud
for (eq in rightEQ.value) { for (eq in rightEQ.value) {
buffer.putFloat(eq) buffer.putFloat(eq)
} }
buffer.putFloat(rightAmplification.value) buffer.putFloat(rightAmplification.floatValue)
buffer.putFloat(rightTone.value) buffer.putFloat(rightTone.floatValue)
buffer.putFloat(if (rightConversationBoost.value) 1.0f else 0.0f) buffer.putFloat(if (rightConversationBoost.value) 1.0f else 0.0f)
buffer.putFloat(rightAmbientNoiseReduction.value) buffer.putFloat(rightAmbientNoiseReduction.floatValue)
val packet = buffer.array() val packet = buffer.array()
Log.d(TAG, "Packet length: ${packet.size}") Log.d(TAG, "Packet length: ${packet.size}")
@@ -393,4 +383,4 @@ class CustomDevice : ComponentActivity() {
} }
} }
} }
} }

View File

@@ -97,6 +97,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
@@ -104,6 +106,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import me.kavishdevar.librepods.constants.AirPodsNotifications import me.kavishdevar.librepods.constants.AirPodsNotifications
import me.kavishdevar.librepods.screens.AirPodsSettingsScreen import me.kavishdevar.librepods.screens.AirPodsSettingsScreen
import me.kavishdevar.librepods.screens.AppSettingsScreen import me.kavishdevar.librepods.screens.AppSettingsScreen
@@ -123,6 +126,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi
lateinit var serviceConnection: ServiceConnection lateinit var serviceConnection: ServiceConnection
lateinit var connectionStatusReceiver: BroadcastReceiver lateinit var connectionStatusReceiver: BroadcastReceiver
@ExperimentalHazeMaterialsApi
@ExperimentalMaterial3Api @ExperimentalMaterial3Api
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
companion object { companion object {
@@ -137,8 +141,10 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
LibrePodsTheme { LibrePodsTheme {
getSharedPreferences("settings", MODE_PRIVATE).edit().putLong("textColor", getSharedPreferences("settings", MODE_PRIVATE).edit {
MaterialTheme.colorScheme.onSurface.toArgb().toLong()).apply() putLong(
"textColor",
MaterialTheme.colorScheme.onSurface.toArgb().toLong())}
Main() Main()
} }
} }
@@ -207,8 +213,7 @@ class MainActivity : ComponentActivity() {
} }
private fun handleAddMagicKeys(uri: Uri) { private fun handleAddMagicKeys(uri: Uri) {
val context = this val sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
val sharedPreferences = getSharedPreferences("settings", Context.MODE_PRIVATE)
val irkHex = uri.getQueryParameter("irk") val irkHex = uri.getQueryParameter("irk")
val encKeyHex = uri.getQueryParameter("enc_key") val encKeyHex = uri.getQueryParameter("enc_key")
@@ -217,13 +222,13 @@ class MainActivity : ComponentActivity() {
if (irkHex != null && validateHexInput(irkHex)) { if (irkHex != null && validateHexInput(irkHex)) {
val irkBytes = hexStringToByteArray(irkHex) val irkBytes = hexStringToByteArray(irkHex)
val irkBase64 = Base64.encode(irkBytes) val irkBase64 = Base64.encode(irkBytes)
sharedPreferences.edit().putString("IRK", irkBase64).apply() sharedPreferences.edit {putString("IRK", irkBase64)}
} }
if (encKeyHex != null && validateHexInput(encKeyHex)) { if (encKeyHex != null && validateHexInput(encKeyHex)) {
val encKeyBytes = hexStringToByteArray(encKeyHex) val encKeyBytes = hexStringToByteArray(encKeyHex)
val encKeyBase64 = Base64.encode(encKeyBytes) val encKeyBase64 = Base64.encode(encKeyBytes)
sharedPreferences.edit().putString("ENC_KEY", encKeyBase64).apply() sharedPreferences.edit { putString("ENC_KEY", encKeyBase64)}
} }
Toast.makeText(this, "Magic keys added successfully!", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Magic keys added successfully!", Toast.LENGTH_SHORT).show()
@@ -247,6 +252,7 @@ class MainActivity : ComponentActivity() {
} }
} }
@ExperimentalHazeMaterialsApi
@SuppressLint("MissingPermission", "InlinedApi", "UnspecifiedRegisterReceiverFlag") @SuppressLint("MissingPermission", "InlinedApi", "UnspecifiedRegisterReceiverFlag")
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
@@ -404,6 +410,7 @@ fun Main() {
} }
} }
@ExperimentalHazeMaterialsApi
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun PermissionsScreen( fun PermissionsScreen(
@@ -586,7 +593,7 @@ fun PermissionsScreen(
onClick = { onClick = {
val intent = Intent( val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${context.packageName}") "package:${context.packageName}".toUri()
) )
context.startActivity(intent) context.startActivity(intent)
onOverlaySettingsReturn() onOverlaySettingsReturn()
@@ -616,9 +623,9 @@ fun PermissionsScreen(
Button( Button(
onClick = { onClick = {
val editor = context.getSharedPreferences("settings", MODE_PRIVATE).edit() context.getSharedPreferences("settings", MODE_PRIVATE).edit {
editor.putBoolean("overlay_permission_skipped", true) putBoolean("overlay_permission_skipped", true)
editor.apply() }
val intent = Intent(context, MainActivity::class.java) val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)

View File

@@ -133,7 +133,7 @@ class QuickSettingsDialogActivity : ComponentActivity() {
window.setGravity(Gravity.BOTTOM) window.setGravity(Gravity.BOTTOM)
Intent(this, AirPodsService::class.java).also { intent -> Intent(this, AirPodsService::class.java).also { intent ->
bindService(intent, connection, Context.BIND_AUTO_CREATE) bindService(intent, connection, BIND_AUTO_CREATE)
} }
setContent { setContent {

View File

@@ -134,7 +134,7 @@ fun AccessibilitySettings() {
textColor = textColor textColor = textColor
) )
val volumeSwipeSpeedOptions = mapOf<Byte, String>( val volumeSwipeSpeedOptions = mapOf(
1.toByte() to "Default", 1.toByte() to "Default",
2.toByte() to "Longer", 2.toByte() to "Longer",
3.toByte() to "Longest" 3.toByte() to "Longest"

View File

@@ -23,10 +23,8 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -135,4 +133,4 @@ fun AccessibilitySliderPreview() {
onValueChange = {}, onValueChange = {},
valueRange = 0f..2f valueRange = 0f..2f
) )
} }

View File

@@ -99,7 +99,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
batteryStatus.value = service.getBattery() batteryStatus.value = service.getBattery()
if (preview) { if (preview) {
batteryStatus.value = listOf<Battery>( batteryStatus.value = listOf(
Battery(BatteryComponent.LEFT, 100, BatteryStatus.CHARGING), Battery(BatteryComponent.LEFT, 100, BatteryStatus.CHARGING),
Battery(BatteryComponent.RIGHT, 50, BatteryStatus.NOT_CHARGING), Battery(BatteryComponent.RIGHT, 50, BatteryStatus.NOT_CHARGING),
Battery(BatteryComponent.CASE, 5, BatteryStatus.CHARGING) Battery(BatteryComponent.CASE, 5, BatteryStatus.CHARGING)

View File

@@ -15,7 +15,9 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
@file:Suppress("unused")
package me.kavishdevar.librepods.composables package me.kavishdevar.librepods.composables
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState

View File

@@ -50,6 +50,7 @@ import androidx.compose.ui.unit.sp
import me.kavishdevar.librepods.services.AirPodsService import me.kavishdevar.librepods.services.AirPodsService
import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.AACPManager
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
import androidx.core.content.edit
@Composable @Composable
fun IndependentToggle(name: String, service: AirPodsService? = null, functionName: String? = null, sharedPreferences: SharedPreferences, default: Boolean = false, controlCommandIdentifier: AACPManager.Companion.ControlCommandIdentifiers? = null) { fun IndependentToggle(name: String, service: AirPodsService? = null, functionName: String? = null, sharedPreferences: SharedPreferences, default: Boolean = false, controlCommandIdentifier: AACPManager.Companion.ControlCommandIdentifiers? = null) {
@@ -70,7 +71,7 @@ fun IndependentToggle(name: String, service: AirPodsService? = null, functionNam
fun cb() { fun cb() {
if (controlCommandIdentifier == null) { if (controlCommandIdentifier == null) {
sharedPreferences.edit().putBoolean(snakeCasedName, checked).apply() sharedPreferences.edit { putBoolean(snakeCasedName, checked) }
} }
if (functionName != null && service != null) { if (functionName != null && service != null) {
val method = val method =
@@ -127,4 +128,4 @@ fun IndependentToggle(name: String, service: AirPodsService? = null, functionNam
@Composable @Composable
fun IndependentTogglePreview() { fun IndependentTogglePreview() {
IndependentToggle("Test", AirPodsService(), "test", LocalContext.current.getSharedPreferences("preview", 0), true) IndependentToggle("Test", AirPodsService(), "test", LocalContext.current.getSharedPreferences("preview", 0), true)
} }

View File

@@ -244,7 +244,7 @@ fun isHeadTrackingData(data: ByteArray): Boolean {
) )
for (i in prefixPattern.indices) { for (i in prefixPattern.indices) {
if (data[i] != prefixPattern[i].toByte()) return false if (data[i] != prefixPattern[i]) return false
} }
if (data[10] != 0x44.toByte() && data[10] != 0x45.toByte()) return false if (data[10] != 0x44.toByte() && data[10] != 0x45.toByte()) return false

View File

@@ -18,7 +18,6 @@
package me.kavishdevar.librepods.constants package me.kavishdevar.librepods.constants
import me.kavishdevar.librepods.constants.StemAction.entries
import me.kavishdevar.librepods.utils.AACPManager import me.kavishdevar.librepods.utils.AACPManager
enum class StemAction { enum class StemAction {

View File

@@ -18,7 +18,6 @@
package me.kavishdevar.librepods.screens package me.kavishdevar.librepods.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -469,4 +468,4 @@ fun AccessibilitySettingsScreen(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
} }
} }
} }

View File

@@ -57,8 +57,6 @@ import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -86,6 +84,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.edit
import androidx.navigation.NavController import androidx.navigation.NavController
import dev.chrisbanes.haze.HazeEffectScope import dev.chrisbanes.haze.HazeEffectScope
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
@@ -186,11 +185,11 @@ fun AppSettingsScreen(navController: NavController) {
var bleOnlyMode by remember { var bleOnlyMode by remember {
mutableStateOf(sharedPreferences.getBoolean("ble_only_mode", false)) mutableStateOf(sharedPreferences.getBoolean("ble_only_mode", false))
} }
// Ensure the default value is properly set if not exists // Ensure the default value is properly set if not exists
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (!sharedPreferences.contains("ble_only_mode")) { if (!sharedPreferences.contains("ble_only_mode")) {
sharedPreferences.edit().putBoolean("ble_only_mode", false).apply() sharedPreferences.edit { putBoolean("ble_only_mode", false) }
} }
} }
@@ -312,7 +311,7 @@ fun AppSettingsScreen(navController: NavController) {
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
showPhoneBatteryInWidget = !showPhoneBatteryInWidget showPhoneBatteryInWidget = !showPhoneBatteryInWidget
sharedPreferences.edit().putBoolean("show_phone_battery_in_widget", showPhoneBatteryInWidget).apply() sharedPreferences.edit { putBoolean("show_phone_battery_in_widget", showPhoneBatteryInWidget)}
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -340,7 +339,7 @@ fun AppSettingsScreen(navController: NavController) {
checked = showPhoneBatteryInWidget, checked = showPhoneBatteryInWidget,
onCheckedChange = { onCheckedChange = {
showPhoneBatteryInWidget = it showPhoneBatteryInWidget = it
sharedPreferences.edit().putBoolean("show_phone_battery_in_widget", it).apply() sharedPreferences.edit { putBoolean("show_phone_battery_in_widget", it)}
} }
) )
} }
@@ -376,7 +375,7 @@ fun AppSettingsScreen(navController: NavController) {
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
bleOnlyMode = !bleOnlyMode bleOnlyMode = !bleOnlyMode
sharedPreferences.edit().putBoolean("ble_only_mode", bleOnlyMode).apply() sharedPreferences.edit { putBoolean("ble_only_mode", bleOnlyMode)}
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -403,7 +402,7 @@ fun AppSettingsScreen(navController: NavController) {
checked = bleOnlyMode, checked = bleOnlyMode,
onCheckedChange = { onCheckedChange = {
bleOnlyMode = it bleOnlyMode = it
sharedPreferences.edit().putBoolean("ble_only_mode", it).apply() sharedPreferences.edit { putBoolean("ble_only_mode", it)}
} }
) )
} }
@@ -440,12 +439,12 @@ fun AppSettingsScreen(navController: NavController) {
fun updateConversationalAwarenessPauseMusic(enabled: Boolean) { fun updateConversationalAwarenessPauseMusic(enabled: Boolean) {
conversationalAwarenessPauseMusicEnabled = enabled conversationalAwarenessPauseMusicEnabled = enabled
sharedPreferences.edit().putBoolean("conversational_awareness_pause_music", enabled).apply() sharedPreferences.edit { putBoolean("conversational_awareness_pause_music", enabled)}
} }
fun updateRelativeConversationalAwarenessVolume(enabled: Boolean) { fun updateRelativeConversationalAwarenessVolume(enabled: Boolean) {
relativeConversationalAwarenessVolumeEnabled = enabled relativeConversationalAwarenessVolumeEnabled = enabled
sharedPreferences.edit().putBoolean("relative_conversational_awareness_volume", enabled).apply() sharedPreferences.edit { putBoolean("relative_conversational_awareness_volume", enabled)}
} }
Row( Row(
@@ -541,7 +540,7 @@ fun AppSettingsScreen(navController: NavController) {
value = sliderValue.floatValue, value = sliderValue.floatValue,
onValueChange = { onValueChange = {
sliderValue.floatValue = it sliderValue.floatValue = it
sharedPreferences.edit().putInt("conversational_awareness_volume", it.toInt()).apply() sharedPreferences.edit { putInt("conversational_awareness_volume", it.toInt())}
}, },
valueRange = 10f..85f, valueRange = 10f..85f,
onValueChangeFinished = { onValueChangeFinished = {
@@ -639,7 +638,7 @@ fun AppSettingsScreen(navController: NavController) {
) { ) {
fun updateQsClickBehavior(enabled: Boolean) { fun updateQsClickBehavior(enabled: Boolean) {
openDialogForControlling = enabled openDialogForControlling = enabled
sharedPreferences.edit().putString("qs_click_behavior", if (enabled) "dialog" else "cycle").apply() sharedPreferences.edit { putString("qs_click_behavior", if (enabled) "dialog" else "cycle")}
} }
Row( Row(
@@ -708,7 +707,7 @@ fun AppSettingsScreen(navController: NavController) {
) { ) {
fun updateDisconnectWhenNotWearing(enabled: Boolean) { fun updateDisconnectWhenNotWearing(enabled: Boolean) {
disconnectWhenNotWearing = enabled disconnectWhenNotWearing = enabled
sharedPreferences.edit().putBoolean("disconnect_when_not_wearing", enabled).apply() sharedPreferences.edit { putBoolean("disconnect_when_not_wearing", enabled)}
} }
Row( Row(
@@ -789,7 +788,7 @@ fun AppSettingsScreen(navController: NavController) {
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
takeoverWhenDisconnected = !takeoverWhenDisconnected takeoverWhenDisconnected = !takeoverWhenDisconnected
sharedPreferences.edit().putBoolean("takeover_when_disconnected", takeoverWhenDisconnected).apply() sharedPreferences.edit { putBoolean("takeover_when_disconnected", takeoverWhenDisconnected)}
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -817,7 +816,7 @@ fun AppSettingsScreen(navController: NavController) {
checked = takeoverWhenDisconnected, checked = takeoverWhenDisconnected,
onCheckedChange = { onCheckedChange = {
takeoverWhenDisconnected = it takeoverWhenDisconnected = it
sharedPreferences.edit().putBoolean("takeover_when_disconnected", it).apply() sharedPreferences.edit { putBoolean("takeover_when_disconnected", it)}
} }
) )
} }
@@ -830,7 +829,7 @@ fun AppSettingsScreen(navController: NavController) {
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
takeoverWhenIdle = !takeoverWhenIdle takeoverWhenIdle = !takeoverWhenIdle
sharedPreferences.edit().putBoolean("takeover_when_idle", takeoverWhenIdle).apply() sharedPreferences.edit { putBoolean("takeover_when_idle", takeoverWhenIdle)}
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -858,7 +857,7 @@ fun AppSettingsScreen(navController: NavController) {
checked = takeoverWhenIdle, checked = takeoverWhenIdle,
onCheckedChange = { onCheckedChange = {
takeoverWhenIdle = it takeoverWhenIdle = it
sharedPreferences.edit().putBoolean("takeover_when_idle", it).apply() sharedPreferences.edit { putBoolean("takeover_when_idle", it)}
} }
) )
} }
@@ -871,7 +870,7 @@ fun AppSettingsScreen(navController: NavController) {
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
takeoverWhenMusic = !takeoverWhenMusic takeoverWhenMusic = !takeoverWhenMusic
sharedPreferences.edit().putBoolean("takeover_when_music", takeoverWhenMusic).apply() sharedPreferences.edit { putBoolean("takeover_when_music", takeoverWhenMusic)}
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -899,7 +898,7 @@ fun AppSettingsScreen(navController: NavController) {
checked = takeoverWhenMusic, checked = takeoverWhenMusic,
onCheckedChange = { onCheckedChange = {
takeoverWhenMusic = it takeoverWhenMusic = it
sharedPreferences.edit().putBoolean("takeover_when_music", it).apply() sharedPreferences.edit { putBoolean("takeover_when_music", it)}
} }
) )
} }
@@ -912,7 +911,7 @@ fun AppSettingsScreen(navController: NavController) {
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
takeoverWhenCall = !takeoverWhenCall takeoverWhenCall = !takeoverWhenCall
sharedPreferences.edit().putBoolean("takeover_when_call", takeoverWhenCall).apply() sharedPreferences.edit { putBoolean("takeover_when_call", takeoverWhenCall)}
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -940,7 +939,7 @@ fun AppSettingsScreen(navController: NavController) {
checked = takeoverWhenCall, checked = takeoverWhenCall,
onCheckedChange = { onCheckedChange = {
takeoverWhenCall = it takeoverWhenCall = it
sharedPreferences.edit().putBoolean("takeover_when_call", it).apply() sharedPreferences.edit { putBoolean("takeover_when_call", it)}
} }
) )
} }
@@ -963,7 +962,7 @@ fun AppSettingsScreen(navController: NavController) {
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
takeoverWhenRingingCall = !takeoverWhenRingingCall takeoverWhenRingingCall = !takeoverWhenRingingCall
sharedPreferences.edit().putBoolean("takeover_when_ringing_call", takeoverWhenRingingCall).apply() sharedPreferences.edit { putBoolean("takeover_when_ringing_call", takeoverWhenRingingCall)}
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -991,7 +990,7 @@ fun AppSettingsScreen(navController: NavController) {
checked = takeoverWhenRingingCall, checked = takeoverWhenRingingCall,
onCheckedChange = { onCheckedChange = {
takeoverWhenRingingCall = it takeoverWhenRingingCall = it
sharedPreferences.edit().putBoolean("takeover_when_ringing_call", it).apply() sharedPreferences.edit { putBoolean("takeover_when_ringing_call", it)}
} }
) )
} }
@@ -1004,7 +1003,7 @@ fun AppSettingsScreen(navController: NavController) {
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
takeoverWhenMediaStart = !takeoverWhenMediaStart takeoverWhenMediaStart = !takeoverWhenMediaStart
sharedPreferences.edit().putBoolean("takeover_when_media_start", takeoverWhenMediaStart).apply() sharedPreferences.edit { putBoolean("takeover_when_media_start", takeoverWhenMediaStart)}
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -1032,7 +1031,7 @@ fun AppSettingsScreen(navController: NavController) {
checked = takeoverWhenMediaStart, checked = takeoverWhenMediaStart,
onCheckedChange = { onCheckedChange = {
takeoverWhenMediaStart = it takeoverWhenMediaStart = it
sharedPreferences.edit().putBoolean("takeover_when_media_start", it).apply() sharedPreferences.edit { putBoolean("takeover_when_media_start", it)}
} }
) )
} }
@@ -1126,7 +1125,10 @@ fun AppSettingsScreen(navController: NavController) {
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
useAlternateHeadTrackingPackets = !useAlternateHeadTrackingPackets useAlternateHeadTrackingPackets = !useAlternateHeadTrackingPackets
sharedPreferences.edit().putBoolean("use_alternate_head_tracking_packets", useAlternateHeadTrackingPackets).apply() sharedPreferences.edit {
putBoolean(
"use_alternate_head_tracking_packets",
useAlternateHeadTrackingPackets)}
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -1154,7 +1156,7 @@ fun AppSettingsScreen(navController: NavController) {
checked = useAlternateHeadTrackingPackets, checked = useAlternateHeadTrackingPackets,
onCheckedChange = { onCheckedChange = {
useAlternateHeadTrackingPackets = it useAlternateHeadTrackingPackets = it
sharedPreferences.edit().putBoolean("use_alternate_head_tracking_packets", it).apply() sharedPreferences.edit { putBoolean("use_alternate_head_tracking_packets", it)}
} }
) )
} }
@@ -1348,7 +1350,7 @@ fun AppSettingsScreen(navController: NavController) {
} }
val base64Value = Base64.encode(hexBytes) val base64Value = Base64.encode(hexBytes)
sharedPreferences.edit().putString(AACPManager.Companion.ProximityKeyType.IRK.name, base64Value).apply() sharedPreferences.edit { putString(AACPManager.Companion.ProximityKeyType.IRK.name, base64Value)}
Toast.makeText(context, "IRK has been set successfully", Toast.LENGTH_SHORT).show() Toast.makeText(context, "IRK has been set successfully", Toast.LENGTH_SHORT).show()
showIrkDialog = false showIrkDialog = false
@@ -1437,7 +1439,7 @@ fun AppSettingsScreen(navController: NavController) {
} }
val base64Value = Base64.encode(hexBytes) val base64Value = Base64.encode(hexBytes)
sharedPreferences.edit().putString(AACPManager.Companion.ProximityKeyType.ENC_KEY.name, base64Value).apply() sharedPreferences.edit { putString(AACPManager.Companion.ProximityKeyType.ENC_KEY.name, base64Value)}
Toast.makeText(context, "Encryption key has been set successfully", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Encryption key has been set successfully", Toast.LENGTH_SHORT).show()
showEncKeyDialog = false showEncKeyDialog = false

View File

@@ -74,6 +74,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@@ -344,11 +345,11 @@ fun DebugScreen(navController: NavController) {
val packetLogs = airPodsService?.packetLogsFlow?.collectAsState(emptySet())?.value ?: emptySet() val packetLogs = airPodsService?.packetLogsFlow?.collectAsState(emptySet())?.value ?: emptySet()
val shouldScrollToBottom = remember { mutableStateOf(true) } val shouldScrollToBottom = remember { mutableStateOf(true) }
val refreshTrigger = remember { mutableStateOf(0) } val refreshTrigger = remember { mutableIntStateOf(0) }
LaunchedEffect(refreshTrigger.value) { LaunchedEffect(refreshTrigger.intValue) {
while(true) { while(true) {
delay(1000) delay(1000)
refreshTrigger.value = refreshTrigger.value + 1 refreshTrigger.intValue = refreshTrigger.intValue + 1
} }
} }
@@ -361,7 +362,7 @@ fun DebugScreen(navController: NavController) {
Toast.makeText(context, "Packet copied to clipboard", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Packet copied to clipboard", Toast.LENGTH_SHORT).show()
} }
LaunchedEffect(packetLogs.size, refreshTrigger.value) { LaunchedEffect(packetLogs.size, refreshTrigger.intValue) {
if (shouldScrollToBottom.value && packetLogs.isNotEmpty()) { if (shouldScrollToBottom.value && packetLogs.isNotEmpty()) {
listState.animateScrollToItem(packetLogs.size - 1) listState.animateScrollToItem(packetLogs.size - 1)
} }

View File

@@ -20,7 +20,6 @@
package me.kavishdevar.librepods.screens package me.kavishdevar.librepods.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -36,7 +35,6 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@@ -44,9 +42,8 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -64,7 +61,6 @@ import kotlinx.coroutines.launch
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.composables.AccessibilitySlider import me.kavishdevar.librepods.composables.AccessibilitySlider
import me.kavishdevar.librepods.services.ServiceManager import me.kavishdevar.librepods.services.ServiceManager
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -301,4 +297,4 @@ fun EqualizerSettingsScreen(
} }
} }
} }
} }

View File

@@ -84,6 +84,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.utils.RadareOffsetFinder import me.kavishdevar.librepods.utils.RadareOffsetFinder
import androidx.core.content.edit
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -528,7 +529,7 @@ fun Onboarding(navController: NavController, activityContext: Context) {
onClick = { onClick = {
showSkipDialog = false showSkipDialog = false
RadareOffsetFinder.clearHookOffsets() RadareOffsetFinder.clearHookOffsets()
sharedPreferences.edit().putBoolean("skip_setup", true).apply() sharedPreferences.edit { putBoolean("skip_setup", true) }
navController.navigate("settings") { navController.navigate("settings") {
popUpTo("onboarding") { inclusive = true } popUpTo("onboarding") { inclusive = true }
} }
@@ -665,6 +666,3 @@ fun OnboardingPreview() {
Onboarding(navController = NavController(LocalContext.current), activityContext = LocalContext.current) Onboarding(navController = NavController(LocalContext.current), activityContext = LocalContext.current)
} }
private suspend fun delay(timeMillis: Long) {
kotlinx.coroutines.delay(timeMillis)
}

View File

@@ -66,6 +66,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.edit
import androidx.navigation.NavController import androidx.navigation.NavController
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.constants.StemAction import me.kavishdevar.librepods.constants.StemAction
@@ -178,7 +179,7 @@ fun LongPress(navController: NavController, name: String) {
selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES, selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES,
onClick = { onClick = {
longPressAction = StemAction.CYCLE_NOISE_CONTROL_MODES longPressAction = StemAction.CYCLE_NOISE_CONTROL_MODES
sharedPreferences.edit().putString(prefKey, StemAction.CYCLE_NOISE_CONTROL_MODES.name).apply() sharedPreferences.edit { putString(prefKey, StemAction.CYCLE_NOISE_CONTROL_MODES.name)}
}, },
isFirst = true, isFirst = true,
isLast = false isLast = false
@@ -189,7 +190,7 @@ fun LongPress(navController: NavController, name: String) {
selected = longPressAction == StemAction.DIGITAL_ASSISTANT, selected = longPressAction == StemAction.DIGITAL_ASSISTANT,
onClick = { onClick = {
longPressAction = StemAction.DIGITAL_ASSISTANT longPressAction = StemAction.DIGITAL_ASSISTANT
sharedPreferences.edit().putString(prefKey, StemAction.DIGITAL_ASSISTANT.name).apply() sharedPreferences.edit { putString(prefKey, StemAction.DIGITAL_ASSISTANT.name)}
}, },
isFirst = false, isFirst = false,
isLast = true isLast = true
@@ -271,7 +272,9 @@ fun LongPressElement(name: String, enabled: Boolean = true, resourceId: Int, isF
it.identifier == AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS it.identifier == AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS
}?.value?.takeIf { it.isNotEmpty() }?.get(0) }?.value?.takeIf { it.isNotEmpty() }?.get(0)
val savedByte = context.getSharedPreferences("settings", Context.MODE_PRIVATE).getInt("long_press_byte", 0b0101.toInt()) val savedByte = context.getSharedPreferences("settings", Context.MODE_PRIVATE).getInt("long_press_byte",
0b0101
)
val byteValue = currentByteValue ?: (savedByte and 0xFF).toByte() val byteValue = currentByteValue ?: (savedByte and 0xFF).toByte()
val isChecked = (byteValue.toInt() and bit) != 0 val isChecked = (byteValue.toInt() and bit) != 0
@@ -331,8 +334,8 @@ fun LongPressElement(name: String, enabled: Boolean = true, resourceId: Int, isF
updatedByte updatedByte
) )
context.getSharedPreferences("settings", Context.MODE_PRIVATE).edit() context.getSharedPreferences("settings", Context.MODE_PRIVATE).edit {
.putInt("long_press_byte", newValue).apply() putInt("long_press_byte", newValue)}
checked.value = false checked.value = false
Log.d("PressAndHoldSettingsScreen", "Updated: $name, enabled: false, byte: ${updatedByte.toInt() and 0xFF}, bits: ${Integer.toBinaryString(updatedByte.toInt() and 0xFF)}") Log.d("PressAndHoldSettingsScreen", "Updated: $name, enabled: false, byte: ${updatedByte.toInt() and 0xFF}, bits: ${Integer.toBinaryString(updatedByte.toInt() and 0xFF)}")
@@ -345,8 +348,9 @@ fun LongPressElement(name: String, enabled: Boolean = true, resourceId: Int, isF
updatedByte updatedByte
) )
context.getSharedPreferences("settings", Context.MODE_PRIVATE).edit() context.getSharedPreferences("settings", Context.MODE_PRIVATE).edit {
.putInt("long_press_byte", newValue).apply() putInt("long_press_byte", newValue)
}
checked.value = true checked.value = true
Log.d("PressAndHoldSettingsScreen", "Updated: $name, enabled: true, byte: ${updatedByte.toInt() and 0xFF}, bits: ${newValue.toString(2)}") Log.d("PressAndHoldSettingsScreen", "Updated: $name, enabled: true, byte: ${updatedByte.toInt() and 0xFF}, bits: ${newValue.toString(2)}")

View File

@@ -69,6 +69,7 @@ import androidx.navigation.NavController
import me.kavishdevar.librepods.R import me.kavishdevar.librepods.R
import me.kavishdevar.librepods.services.ServiceManager import me.kavishdevar.librepods.services.ServiceManager
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
import androidx.core.content.edit
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -153,7 +154,7 @@ fun RenameScreen(navController: NavController) {
value = name.value, value = name.value,
onValueChange = { onValueChange = {
name.value = it name.value = it
sharedPreferences.edit().putString("name", it.text).apply() sharedPreferences.edit {putString("name", it.text)}
ServiceManager.getService()?.setName(it.text) ServiceManager.getService()?.setName(it.text)
}, },
textStyle = TextStyle( textStyle = TextStyle(
@@ -175,7 +176,7 @@ fun RenameScreen(navController: NavController) {
IconButton( IconButton(
onClick = { onClick = {
name.value = TextFieldValue("") name.value = TextFieldValue("")
sharedPreferences.edit().putString("name", "").apply() sharedPreferences.edit { putString("name", "") }
ServiceManager.getService()?.setName("") ServiceManager.getService()?.setName("")
} }
) { ) {

View File

@@ -27,7 +27,6 @@ import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
@@ -46,17 +45,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -65,7 +59,6 @@ import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -91,10 +84,7 @@ import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -102,7 +92,6 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -199,7 +188,7 @@ fun TroubleshootingScreen(navController: NavController) {
val buttonBgColor = if (isSystemInDarkTheme()) Color(0xFF333333) else Color(0xFFDDDDDD) val buttonBgColor = if (isSystemInDarkTheme()) Color(0xFF333333) else Color(0xFFDDDDDD)
var instructionText by remember { mutableStateOf("") } var instructionText by remember { mutableStateOf("") }
var isDarkTheme = isSystemInDarkTheme() val isDarkTheme = isSystemInDarkTheme()
var mDensity by remember { mutableFloatStateOf(0f) } var mDensity by remember { mutableFloatStateOf(0f) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {

View File

@@ -753,6 +753,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} }
} }
@Suppress("unused")
fun cameraClosed() { fun cameraClosed() {
cameraActive = false cameraActive = false
setupStemActions() setupStemActions()
@@ -894,7 +895,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
this@AirPodsService, this@AirPodsService,
(batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0), (batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0),
IslandType.MOVED_TO_OTHER_DEVICE, IslandType.MOVED_TO_OTHER_DEVICE,
reversed = reasonReverseTapped reversed = true
) )
} }
if (!aacpManager.owns) { if (!aacpManager.owns) {
@@ -909,12 +910,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} }
override fun onShowNearbyUI() { override fun onShowNearbyUI() {
// showIsland( showIsland(
// this@AirPodsService, this@AirPodsService,
// (batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0), (batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0),
// IslandType.MOVED_TO_OTHER_DEVICE, IslandType.MOVED_TO_OTHER_DEVICE,
// reversed = false reversed = false
// ) )
} }
override fun onDeviceMetadataReceived(deviceMetadata: ByteArray) { override fun onDeviceMetadataReceived(deviceMetadata: ByteArray) {
@@ -1462,7 +1463,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
} }
fun setBatteryMetadata() { fun setBatteryMetadata() {
device?.let { device?.let { it ->
SystemApisUtils.setMetadata( SystemApisUtils.setMetadata(
it, it,
it.METADATA_UNTETHERED_CASE_BATTERY, it.METADATA_UNTETHERED_CASE_BATTERY,
@@ -1502,7 +1503,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val componentName = ComponentName(this, BatteryWidget::class.java) val componentName = ComponentName(this, BatteryWidget::class.java)
val widgetIds = appWidgetManager.getAppWidgetIds(componentName) val widgetIds = appWidgetManager.getAppWidgetIds(componentName)
val remoteViews = RemoteViews(packageName, R.layout.battery_widget).also { val remoteViews = RemoteViews(packageName, R.layout.battery_widget).also { it ->
val openActivityIntent = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) val openActivityIntent = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
it.setOnClickPendingIntent(R.id.battery_widget, openActivityIntent) it.setOnClickPendingIntent(R.id.battery_widget, openActivityIntent)
@@ -1569,7 +1570,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
if (widgetMobileBatteryEnabled) View.VISIBLE else View.GONE if (widgetMobileBatteryEnabled) View.VISIBLE else View.GONE
) )
if (widgetMobileBatteryEnabled) { if (widgetMobileBatteryEnabled) {
val batteryManager = getSystemService<BatteryManager>(BatteryManager::class.java) val batteryManager = getSystemService(BatteryManager::class.java)
val batteryLevel = val batteryLevel =
batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
val charging = val charging =
@@ -1606,7 +1607,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
val appWidgetManager = AppWidgetManager.getInstance(this) val appWidgetManager = AppWidgetManager.getInstance(this)
val componentName = ComponentName(this, NoiseControlWidget::class.java) val componentName = ComponentName(this, NoiseControlWidget::class.java)
val widgetIds = appWidgetManager.getAppWidgetIds(componentName) val widgetIds = appWidgetManager.getAppWidgetIds(componentName)
val remoteViews = RemoteViews(packageName, R.layout.noise_control_widget).also { val remoteViews = RemoteViews(packageName, R.layout.noise_control_widget).also { it ->
val ancStatus = ancNotification.status val ancStatus = ancNotification.status
val allowOffModeValue = aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION } val allowOffModeValue = aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION }
val allowOffMode = allowOffModeValue?.value?.takeIf { it.isNotEmpty() }?.get(0) == 0x01.toByte() val allowOffMode = allowOffModeValue?.value?.takeIf { it.isNotEmpty() }?.get(0) == 0x01.toByte()
@@ -2198,7 +2199,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
Log.d("AirPodsService", macAddress) Log.d("AirPodsService", macAddress)
sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false) } sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false) }
device = getSystemService<BluetoothManager>(BluetoothManager::class.java).adapter.bondedDevices.find { device = getSystemService(BluetoothManager::class.java).adapter.bondedDevices.find {
it.address == macAddress it.address == macAddress
} }
@@ -2335,7 +2336,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
setupStemActions() setupStemActions()
while (socket.isConnected) { while (socket.isConnected) {
socket.let { socket.let { it ->
val buffer = ByteArray(1024) val buffer = ByteArray(1024)
val bytesRead = it.inputStream.read(buffer) val bytesRead = it.inputStream.read(buffer)
var data: ByteArray var data: ByteArray

View File

@@ -423,7 +423,7 @@ class AACPManager {
) )
Log.d( Log.d(
TAG, "Control command list is now: ${ TAG, "Control command list is now: ${
controlCommandStatusList.joinToString(", ") { controlCommandStatusList.joinToString(", ") { it ->
"${it.identifier.name} (${it.identifier.value.toHexString()}) - ${ "${it.identifier.name} (${it.identifier.value.toHexString()}) - ${
it.value.joinToString( it.value.joinToString(
" " " "
@@ -692,8 +692,8 @@ class AACPManager {
if (selfMacAddress.length != 17 || !selfMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")) || targetMacAddress.length != 17 || !targetMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}"))) { if (selfMacAddress.length != 17 || !selfMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")) || targetMacAddress.length != 17 || !targetMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}"))) {
throw IllegalArgumentException("MAC address must be 6 bytes") throw IllegalArgumentException("MAC address must be 6 bytes")
} }
Log.d(TAG, "SELFMAC: ${selfMacAddress}, TARGETMAC: ${targetMacAddress}") Log.d(TAG, "SELFMAC: ${selfMacAddress}, TARGETMAC: $targetMacAddress")
Log.d(TAG, "Sending Media Information packet to ${targetMacAddress}") Log.d(TAG, "Sending Media Information packet to $targetMacAddress")
return sendDataPacket(createMediaInformationNewDevicePacket(selfMacAddress, targetMacAddress)) return sendDataPacket(createMediaInformationNewDevicePacket(selfMacAddress, targetMacAddress))
} }
@@ -775,7 +775,7 @@ class AACPManager {
if (selfMacAddress.length != 17 || !selfMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}"))) { if (selfMacAddress.length != 17 || !selfMacAddress.matches(Regex("([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}"))) {
throw IllegalArgumentException("MAC address must be 6 bytes") throw IllegalArgumentException("MAC address must be 6 bytes")
} }
Log.d(TAG, "SELFMAC: ${selfMacAddress}") Log.d(TAG, "SELFMAC: $selfMacAddress")
val targetMac = connectedDevices.find { it.mac != selfMacAddress }?.mac val targetMac = connectedDevices.find { it.mac != selfMacAddress }?.mac
Log.d(TAG, "Sending Media Information packet to ${targetMac ?: "unknown device"}") Log.d(TAG, "Sending Media Information packet to ${targetMac ?: "unknown device"}")
return sendDataPacket( return sendDataPacket(
@@ -842,7 +842,7 @@ class AACPManager {
fun createSmartRoutingShowUIPacket(targetMacAddress: String): ByteArray { fun createSmartRoutingShowUIPacket(targetMacAddress: String): ByteArray {
val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00) val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00)
val buffer = ByteBuffer.allocate(134) val buffer = ByteBuffer.allocate(134)
buffer.put( buffer.put(
targetMacAddress.split(":").map { it.toInt(16).toByte() }.toByteArray().reversedArray() targetMacAddress.split(":").map { it.toInt(16).toByte() }.toByteArray().reversedArray()
) )

View File

@@ -30,7 +30,6 @@ import android.content.SharedPreferences
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import me.kavishdevar.librepods.services.ServiceManager
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
@@ -223,12 +222,13 @@ class BLEManager(private val context: Context) {
} }
} }
@SuppressLint("GetInstance")
private fun decryptLastBytes(data: ByteArray, key: ByteArray): ByteArray? { private fun decryptLastBytes(data: ByteArray, key: ByteArray): ByteArray? {
return try { return try {
if (data.size < 16) { if (data.size < 16) {
return null return null
} }
val block = data.copyOfRange(data.size - 16, data.size) val block = data.copyOfRange(data.size - 16, data.size)
val cipher = Cipher.getInstance("AES/ECB/NoPadding") val cipher = Cipher.getInstance("AES/ECB/NoPadding")
val secretKey = SecretKeySpec(key, "AES") val secretKey = SecretKeySpec(key, "AES")
@@ -302,7 +302,7 @@ class BLEManager(private val context: Context) {
if (previousGlobalState != parsedStatus.lidOpen) { if (previousGlobalState != parsedStatus.lidOpen) {
listener.onLidStateChanged(parsedStatus.lidOpen) listener.onLidStateChanged(parsedStatus.lidOpen)
Log.d(TAG, "Lid state changed from ${previousGlobalState} to ${parsedStatus.lidOpen}") Log.d(TAG, "Lid state changed from $previousGlobalState to ${parsedStatus.lidOpen}")
} }
} }
@@ -348,13 +348,13 @@ class BLEManager(private val context: Context) {
val isRightInEar = if (xorFactor) (status and 0x02) != 0 else (status and 0x08) != 0 val isRightInEar = if (xorFactor) (status and 0x02) != 0 else (status and 0x08) != 0
val isFlipped = !primaryLeft val isFlipped = !primaryLeft
val leftByteIndex = if (isFlipped) 2 else 1 val leftByteIndex = if (isFlipped) 2 else 1
val rightByteIndex = if (isFlipped) 1 else 2 val rightByteIndex = if (isFlipped) 1 else 2
val (isLeftCharging, leftBattery) = formatBattery(decrypted[leftByteIndex].toInt() and 0xFF) val (isLeftCharging, leftBattery) = formatBattery(decrypted[leftByteIndex].toInt() and 0xFF)
val (isRightCharging, rightBattery) = formatBattery(decrypted[rightByteIndex].toInt() and 0xFF) val (isRightCharging, rightBattery) = formatBattery(decrypted[rightByteIndex].toInt() and 0xFF)
val rawCaseBatteryByte = decrypted[3].toInt() and 0xFF val rawCaseBatteryByte = decrypted[3].toInt() and 0xFF
val (isCaseCharging, rawCaseBattery) = formatBattery(rawCaseBatteryByte) val (isCaseCharging, rawCaseBattery) = formatBattery(rawCaseBatteryByte)
@@ -442,10 +442,10 @@ class BLEManager(private val context: Context) {
val isRightInEar = if (xorFactor) (status and 0x02) != 0 else (status and 0x08) != 0 val isRightInEar = if (xorFactor) (status and 0x02) != 0 else (status and 0x08) != 0
val isFlipped = !primaryLeft val isFlipped = !primaryLeft
val leftBatteryNibble = if (isFlipped) (podsBattery shr 4) and 0x0F else podsBattery and 0x0F val leftBatteryNibble = if (isFlipped) (podsBattery shr 4) and 0x0F else podsBattery and 0x0F
val rightBatteryNibble = if (isFlipped) podsBattery and 0x0F else (podsBattery shr 4) and 0x0F val rightBatteryNibble = if (isFlipped) podsBattery and 0x0F else (podsBattery shr 4) and 0x0F
val caseBattery = flagsCase and 0x0F val caseBattery = flagsCase and 0x0F
val flags = (flagsCase shr 4) and 0x0F val flags = (flagsCase shr 4) and 0x0F

View File

@@ -18,6 +18,7 @@
package me.kavishdevar.librepods.utils package me.kavishdevar.librepods.utils
import android.annotation.SuppressLint
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@@ -26,10 +27,10 @@ import javax.crypto.spec.SecretKeySpec
* verifying Resolvable Private Addresses (RPA) used by AirPods. * verifying Resolvable Private Addresses (RPA) used by AirPods.
*/ */
object BluetoothCryptography { object BluetoothCryptography {
/** /**
* Verifies if the provided Bluetooth address is an RPA that matches the given Identity Resolving Key (IRK) * Verifies if the provided Bluetooth address is an RPA that matches the given Identity Resolving Key (IRK)
* *
* @param addr The Bluetooth address to verify * @param addr The Bluetooth address to verify
* @param irk The Identity Resolving Key to use for verification * @param irk The Identity Resolving Key to use for verification
* @return true if the address is verified as an RPA matching the IRK * @return true if the address is verified as an RPA matching the IRK
@@ -44,11 +45,12 @@ object BluetoothCryptography {
/** /**
* Performs E function (AES-128) as specified in Bluetooth Core Specification * Performs E function (AES-128) as specified in Bluetooth Core Specification
* *
* @param key The key for encryption * @param key The key for encryption
* @param data The data to encrypt * @param data The data to encrypt
* @return The encrypted data * @return The encrypted data
*/ */
@SuppressLint("GetInstance")
fun e(key: ByteArray, data: ByteArray): ByteArray { fun e(key: ByteArray, data: ByteArray): ByteArray {
val swappedKey = key.reversedArray() val swappedKey = key.reversedArray()
val swappedData = data.reversedArray() val swappedData = data.reversedArray()
@@ -60,7 +62,7 @@ object BluetoothCryptography {
/** /**
* Performs the ah function as specified in Bluetooth Core Specification * Performs the ah function as specified in Bluetooth Core Specification
* *
* @param k The IRK key * @param k The IRK key
* @param r The random part of the address * @param r The random part of the address
* @return The hash part of the address * @return The hash part of the address

View File

@@ -34,6 +34,7 @@ import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.ParcelUuid import android.os.ParcelUuid
import android.util.Log import android.util.Log
import androidx.core.content.edit
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -76,7 +77,7 @@ object CrossDevice {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
Log.d("CrossDevice", "Initializing CrossDevice") Log.d("CrossDevice", "Initializing CrossDevice")
sharedPreferences = context.getSharedPreferences("packet_logs", Context.MODE_PRIVATE) sharedPreferences = context.getSharedPreferences("packet_logs", Context.MODE_PRIVATE)
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply() sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false)}
this@CrossDevice.bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter this@CrossDevice.bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
this@CrossDevice.bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser this@CrossDevice.bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser
// startAdvertising() // startAdvertising()
@@ -111,7 +112,7 @@ object CrossDevice {
} }
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission", "unused")
private fun startAdvertising() { private fun startAdvertising() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val settings = AdvertiseSettings.Builder() val settings = AdvertiseSettings.Builder()
@@ -147,7 +148,7 @@ object CrossDevice {
fun setAirPodsConnected(connected: Boolean) { fun setAirPodsConnected(connected: Boolean) {
if (connected) { if (connected) {
isAvailable = false isAvailable = false
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply() sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false)}
clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_CONNECTED.packet) clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_CONNECTED.packet)
} else { } else {
clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_DISCONNECTED.packet) clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_DISCONNECTED.packet)
@@ -168,7 +169,7 @@ object CrossDevice {
val logEntry = "$source: $packetHex" val logEntry = "$source: $packetHex"
val logs = sharedPreferences.getStringSet(PACKET_LOG_KEY, mutableSetOf())?.toMutableSet() ?: mutableSetOf() val logs = sharedPreferences.getStringSet(PACKET_LOG_KEY, mutableSetOf())?.toMutableSet() ?: mutableSetOf()
logs.add(logEntry) logs.add(logEntry)
sharedPreferences.edit().putStringSet(PACKET_LOG_KEY, logs).apply() sharedPreferences.edit { putStringSet(PACKET_LOG_KEY, logs)}
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@@ -207,10 +208,10 @@ object CrossDevice {
} }
} else if (packet.contentEquals(CrossDevicePackets.AIRPODS_CONNECTED.packet)) { } else if (packet.contentEquals(CrossDevicePackets.AIRPODS_CONNECTED.packet)) {
isAvailable = true isAvailable = true
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", true).apply() sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", true)}
} else if (packet.contentEquals(CrossDevicePackets.AIRPODS_DISCONNECTED.packet)) { } else if (packet.contentEquals(CrossDevicePackets.AIRPODS_DISCONNECTED.packet)) {
isAvailable = false isAvailable = false
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply() sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false)}
} else if (packet.contentEquals(CrossDevicePackets.REQUEST_BATTERY_BYTES.packet)) { } else if (packet.contentEquals(CrossDevicePackets.REQUEST_BATTERY_BYTES.packet)) {
Log.d("CrossDevice", "Received battery request, battery data: ${batteryBytes.joinToString("") { "%02x".format(it) }}") Log.d("CrossDevice", "Received battery request, battery data: ${batteryBytes.joinToString("") { "%02x".format(it) }}")
sendRemotePacket(batteryBytes) sendRemotePacket(batteryBytes)
@@ -223,7 +224,7 @@ object CrossDevice {
} else { } else {
if (packet.sliceArray(0..3).contentEquals(CrossDevicePackets.AIRPODS_DATA_HEADER.packet)) { if (packet.sliceArray(0..3).contentEquals(CrossDevicePackets.AIRPODS_DATA_HEADER.packet)) {
isAvailable = true isAvailable = true
sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", true).apply() sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", true) }
if (packet.size % 2 == 0) { if (packet.size % 2 == 0) {
val half = packet.size / 2 val half = packet.size / 2
if (packet.sliceArray(0 until half).contentEquals(packet.sliceArray(half until packet.size))) { if (packet.sliceArray(0 until half).contentEquals(packet.sliceArray(half until packet.size))) {

View File

@@ -113,7 +113,7 @@ class IslandWindow(private val context: Context) {
intent.getParcelableArrayListExtra("data", Battery::class.java) intent.getParcelableArrayListExtra("data", Battery::class.java)
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
intent.getParcelableArrayListExtra<Battery>("data") intent.getParcelableArrayListExtra("data")
} }
updateBatteryDisplay(batteryList) updateBatteryDisplay(batteryList)
} else if (intent?.action == AirPodsNotifications.DISCONNECT_RECEIVERS) { } else if (intent?.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {

View File

@@ -1,5 +1,6 @@
package me.kavishdevar.librepods.utils package me.kavishdevar.librepods.utils
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
@@ -17,6 +18,7 @@ import android.widget.FrameLayout
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.net.toUri
import io.github.libxposed.api.XposedInterface import io.github.libxposed.api.XposedInterface
import io.github.libxposed.api.XposedInterface.AfterHookCallback import io.github.libxposed.api.XposedInterface.AfterHookCallback
import io.github.libxposed.api.XposedModule import io.github.libxposed.api.XposedModule
@@ -27,7 +29,7 @@ import io.github.libxposed.api.annotations.XposedHooker
private const val TAG = "AirPodsHook" private const val TAG = "AirPodsHook"
private lateinit var module: KotlinModule private lateinit var module: KotlinModule
@SuppressLint("DiscouragedApi", "PrivateApi")
class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModule(base, param) { class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModule(base, param) {
init { init {
Log.i(TAG, "AirPodsHook module initialized at :: ${param.processName}") Log.i(TAG, "AirPodsHook module initialized at :: ${param.processName}")
@@ -60,7 +62,7 @@ class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModul
val updateIconMethod = headerControllerClass.getDeclaredMethod( val updateIconMethod = headerControllerClass.getDeclaredMethod(
"updateIcon", "updateIcon",
android.widget.ImageView::class.java, ImageView::class.java,
String::class.java) String::class.java)
hook(updateIconMethod, BluetoothIconHooker::class.java) hook(updateIconMethod, BluetoothIconHooker::class.java)
@@ -89,7 +91,7 @@ class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModul
val updateIconMethod = headerControllerClass.getDeclaredMethod( val updateIconMethod = headerControllerClass.getDeclaredMethod(
"updateIcon", "updateIcon",
android.widget.ImageView::class.java, ImageView::class.java,
String::class.java) String::class.java)
hook(updateIconMethod, BluetoothIconHooker::class.java) hook(updateIconMethod, BluetoothIconHooker::class.java)
@@ -209,7 +211,7 @@ class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModul
val imageView = callback.args[0] as ImageView val imageView = callback.args[0] as ImageView
val iconUri = callback.args[1] as String val iconUri = callback.args[1] as String
val uri = android.net.Uri.parse(iconUri) val uri = iconUri.toUri()
if (uri.toString().startsWith("android.resource://me.kavishdevar.librepods")) { if (uri.toString().startsWith("android.resource://me.kavishdevar.librepods")) {
Log.i(TAG, "Handling AirPods icon URI: $uri") Log.i(TAG, "Handling AirPods icon URI: $uri")
@@ -571,10 +573,10 @@ class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModul
addView(icon) addView(icon)
if (isSelected) { background = if (isSelected) {
background = createSelectedBackground(context) createSelectedBackground(context)
} else { } else {
background = null null
} }
setOnClickListener { setOnClickListener {

View File

@@ -19,8 +19,6 @@
package me.kavishdevar.librepods.utils package me.kavishdevar.librepods.utils
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.BufferedReader import java.io.BufferedReader
@@ -30,7 +28,7 @@ import java.io.InputStreamReader
class LogCollector(private val context: Context) { class LogCollector(private val context: Context) {
private var isCollecting = false private var isCollecting = false
private var logProcess: Process? = null private var logProcess: Process? = null
suspend fun openXposedSettings(context: Context) { suspend fun openXposedSettings(context: Context) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val command = if (android.os.Build.VERSION.SDK_INT >= 29) { val command = if (android.os.Build.VERSION.SDK_INT >= 29) {
@@ -38,42 +36,42 @@ class LogCollector(private val context: Context) {
} else { } else {
"am broadcast -a android.provider.Telephony.SECRET_CODE -d android_secret_code://5776733 android" "am broadcast -a android.provider.Telephony.SECRET_CODE -d android_secret_code://5776733 android"
} }
executeRootCommand(command) executeRootCommand(command)
} }
} }
suspend fun clearLogs() { suspend fun clearLogs() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
executeRootCommand("logcat -c") executeRootCommand("logcat -c")
} }
} }
suspend fun killBluetoothService() { suspend fun killBluetoothService() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
executeRootCommand("killall com.android.bluetooth") executeRootCommand("killall com.android.bluetooth")
} }
} }
private suspend fun getPackageUIDs(): Pair<String?, String?> { private suspend fun getPackageUIDs(): Pair<String?, String?> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val btUid = executeRootCommand("dumpsys package com.android.bluetooth | grep -m 1 \"uid=\" | sed -E 's/.*uid=([0-9]+).*/\\1/'") val btUid = executeRootCommand("dumpsys package com.android.bluetooth | grep -m 1 \"uid=\" | sed -E 's/.*uid=([0-9]+).*/\\1/'")
.trim() .trim()
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
val appUid = executeRootCommand("dumpsys package me.kavishdevar.librepods | grep -m 1 \"uid=\" | sed -E 's/.*uid=([0-9]+).*/\\1/'") val appUid = executeRootCommand("dumpsys package me.kavishdevar.librepods | grep -m 1 \"uid=\" | sed -E 's/.*uid=([0-9]+).*/\\1/'")
.trim() .trim()
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
Pair(btUid, appUid) Pair(btUid, appUid)
} }
} }
suspend fun startLogCollection(listener: (String) -> Unit, connectionDetectedCallback: () -> Unit): String { suspend fun startLogCollection(listener: (String) -> Unit, connectionDetectedCallback: () -> Unit): String {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
isCollecting = true isCollecting = true
val (btUid, appUid) = getPackageUIDs() val (btUid, appUid) = getPackageUIDs()
val uidFilter = buildString { val uidFilter = buildString {
if (!btUid.isNullOrEmpty() && !appUid.isNullOrEmpty()) { if (!btUid.isNullOrEmpty() && !appUid.isNullOrEmpty()) {
append("$btUid,$appUid") append("$btUid,$appUid")
@@ -83,33 +81,33 @@ class LogCollector(private val context: Context) {
append(appUid) append(appUid)
} }
} }
val command = if (uidFilter.isNotEmpty()) { val command = if (uidFilter.isNotEmpty()) {
"su -c logcat --uid=$uidFilter -v threadtime" "su -c logcat --uid=$uidFilter -v threadtime"
} else { } else {
"su -c logcat -v threadtime" "su -c logcat -v threadtime"
} }
val logs = StringBuilder() val logs = StringBuilder()
try { try {
logProcess = Runtime.getRuntime().exec(command) logProcess = Runtime.getRuntime().exec(command)
val reader = BufferedReader(InputStreamReader(logProcess!!.inputStream)) val reader = BufferedReader(InputStreamReader(logProcess!!.inputStream))
var line: String? = null var line: String? = null
var connectionDetected = false var connectionDetected = false
while (isCollecting && reader.readLine().also { line = it } != null) { while (isCollecting && reader.readLine().also { line = it } != null) {
line?.let { line?.let {
if (it.contains("<LogCollector:")) { if (it.contains("<LogCollector:")) {
logs.append("\n=============\n") logs.append("\n=============\n")
} }
logs.append(it).append("\n") logs.append(it).append("\n")
listener(it) listener(it)
if (it.contains("<LogCollector:")) { if (it.contains("<LogCollector:")) {
logs.append("=============\n\n") logs.append("=============\n\n")
} }
if (!connectionDetected) { if (!connectionDetected) {
if (it.contains("<LogCollector:Complete:Success>")) { if (it.contains("<LogCollector:Complete:Success>")) {
connectionDetected = true connectionDetected = true
@@ -118,7 +116,7 @@ class LogCollector(private val context: Context) {
connectionDetected = true connectionDetected = true
connectionDetectedCallback() connectionDetectedCallback()
} else if (it.contains("<LogCollector:Start>")) { } else if (it.contains("<LogCollector:Start>")) {
} }
else if (it.contains("AirPodsService") && it.contains("Connected to device")) { else if (it.contains("AirPodsService") && it.contains("Connected to device")) {
connectionDetected = true connectionDetected = true
connectionDetectedCallback() connectionDetectedCallback()
@@ -139,17 +137,17 @@ class LogCollector(private val context: Context) {
logs.append("Error collecting logs: ${e.message}").append("\n") logs.append("Error collecting logs: ${e.message}").append("\n")
e.printStackTrace() e.printStackTrace()
} }
logs.toString() logs.toString()
} }
} }
fun stopLogCollection() { fun stopLogCollection() {
isCollecting = false isCollecting = false
logProcess?.destroy() logProcess?.destroy()
logProcess = null logProcess = null
} }
suspend fun saveLogToInternalStorage(fileName: String, content: String): File? { suspend fun saveLogToInternalStorage(fileName: String, content: String): File? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
try { try {
@@ -157,7 +155,7 @@ class LogCollector(private val context: Context) {
if (!logsDir.exists()) { if (!logsDir.exists()) {
logsDir.mkdir() logsDir.mkdir()
} }
val file = File(logsDir, fileName) val file = File(logsDir, fileName)
file.writeText(content) file.writeText(content)
return@withContext file return@withContext file
@@ -167,31 +165,31 @@ class LogCollector(private val context: Context) {
} }
} }
} }
suspend fun addLogMarker(markerType: LogMarkerType, details: String = "") { suspend fun addLogMarker(markerType: LogMarkerType, details: String = "") {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val timestamp = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", java.util.Locale.US) val timestamp = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", java.util.Locale.US)
.format(java.util.Date()) .format(java.util.Date())
val marker = when (markerType) { val marker = when (markerType) {
LogMarkerType.START -> "<LogCollector:Start> [$timestamp] Beginning connection test" LogMarkerType.START -> "<LogCollector:Start> [$timestamp] Beginning connection test"
LogMarkerType.SUCCESS -> "<LogCollector:Complete:Success> [$timestamp] Connection test completed successfully" LogMarkerType.SUCCESS -> "<LogCollector:Complete:Success> [$timestamp] Connection test completed successfully"
LogMarkerType.FAILURE -> "<LogCollector:Complete:Failed> [$timestamp] Connection test failed" LogMarkerType.FAILURE -> "<LogCollector:Complete:Failed> [$timestamp] Connection test failed"
LogMarkerType.CUSTOM -> "<LogCollector:Custom:$details> [$timestamp]" LogMarkerType.CUSTOM -> "<LogCollector:Custom:$details> [$timestamp]"
} }
val command = "log -t AirPodsService \"$marker\"" val command = "log -t AirPodsService \"$marker\""
executeRootCommand(command) executeRootCommand(command)
} }
} }
enum class LogMarkerType { enum class LogMarkerType {
START, START,
SUCCESS, SUCCESS,
FAILURE, FAILURE,
CUSTOM CUSTOM
} }
private suspend fun executeRootCommand(command: String): String { private suspend fun executeRootCommand(command: String): String {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
try { try {
@@ -199,11 +197,11 @@ class LogCollector(private val context: Context) {
val reader = BufferedReader(InputStreamReader(process.inputStream)) val reader = BufferedReader(InputStreamReader(process.inputStream))
val output = StringBuilder() val output = StringBuilder()
var line: String? var line: String?
while (reader.readLine().also { line = it } != null) { while (reader.readLine().also { line = it } != null) {
output.append(line).append("\n") output.append(line).append("\n")
} }
process.waitFor() process.waitFor()
output.toString() output.toString()
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -275,7 +275,7 @@ object MediaController {
} else { } else {
initialVolume!! initialVolume!!
} }
smoothVolumeTransition(initialVolume!!, targetVolume.toInt()) smoothVolumeTransition(initialVolume!!, targetVolume)
if (conversationalAwarenessPauseMusic) { if (conversationalAwarenessPauseMusic) {
sendPause(force = true) sendPause(force = true)
} }
@@ -311,4 +311,4 @@ object MediaController {
} }
}) })
} }
} }

View File

@@ -49,7 +49,6 @@ import me.kavishdevar.librepods.constants.AirPodsNotifications
import me.kavishdevar.librepods.constants.Battery import me.kavishdevar.librepods.constants.Battery
import me.kavishdevar.librepods.constants.BatteryComponent import me.kavishdevar.librepods.constants.BatteryComponent
import me.kavishdevar.librepods.constants.BatteryStatus import me.kavishdevar.librepods.constants.BatteryStatus
import kotlin.collections.find
@SuppressLint("InflateParams", "ClickableViewAccessibility") @SuppressLint("InflateParams", "ClickableViewAccessibility")
class PopupWindow( class PopupWindow(
@@ -172,7 +171,12 @@ class PopupWindow(
batteryUpdateReceiver = object : BroadcastReceiver() { batteryUpdateReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == AirPodsNotifications.BATTERY_DATA) { if (intent?.action == AirPodsNotifications.BATTERY_DATA) {
val batteryList = intent.getParcelableArrayListExtra<Battery>("data") val batteryList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableArrayListExtra("data", Battery::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableArrayListExtra("data")
}
if (batteryList != null) { if (batteryList != null) {
updateBatteryStatusFromList(batteryList) updateBatteryStatusFromList(batteryList)
} }
@@ -272,7 +276,4 @@ class PopupWindow(
onCloseCallback() onCloseCallback()
} }
} }
val isShowing: Boolean
get() = mView.parent != null && !isClosing
} }

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><!--
Background for widgets to make the rounded corners based on the
appWidgetRadius attribute value
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="?attr/appWidgetRadius" />
<solid android:color="?android:attr/colorBackground" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?><!--
Background for views inside widgets to make the rounded corners based on the
appWidgetInnerRadius attribute value
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="?attr/appWidgetInnerRadius" />
<solid android:color="?android:attr/colorAccent" />
</shape>

View File

@@ -113,7 +113,7 @@
android:layout_gravity="center" android:layout_gravity="center"
android:translationX="-12dp" android:translationX="-12dp"
android:background="@drawable/ic_undo_button_bg" android:background="@drawable/ic_undo_button_bg"
android:contentDescription="Undo button" android:contentDescription="@string/undo"
android:scaleType="centerInside" android:scaleType="centerInside"
android:src="@drawable/ic_undo" android:src="@drawable/ic_undo"
android:tint="@android:color/white" android:tint="@android:color/white"
@@ -121,4 +121,4 @@
android:translationZ="8dp" android:translationZ="8dp"
android:visibility="gone" /> android:visibility="gone" />
</FrameLayout> </FrameLayout>
</LinearLayout> </LinearLayout>

View File

@@ -4,12 +4,14 @@
android:id="@+id/noise_control_widget" android:id="@+id/noise_control_widget"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:theme="@style/Theme.LibrePods.AppWidgetContainer"> android:theme="@style/Theme.LibrePods.AppWidgetContainer"
tools:ignore="ContentDescription,NestedWeights">
<LinearLayout <LinearLayout
android:id="@android:id/background" android:id="@android:id/background"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal"> android:orientation="horizontal">
<LinearLayout <LinearLayout
@@ -70,7 +72,8 @@
android:shadowRadius="12" android:shadowRadius="12"
android:text="@string/transparency" android:text="@string/transparency"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="12sp" /> android:textSize="12sp"
tools:ignore="NestedWeights" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -102,7 +105,8 @@
android:shadowRadius="12" android:shadowRadius="12"
android:text="@string/adaptive" android:text="@string/adaptive"
android:textColor="@color/white" android:textColor="@color/white"
android:textSize="12sp" /> android:textSize="12sp"
tools:ignore="NestedWeights" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_monochrome" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

View File

@@ -1,14 +0,0 @@
<resources>
<style name="Widget.LibrePods.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item>
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_background</item>
</style>
<style name="Widget.LibrePods.AppWidget.InnerView" parent="android:Widget">
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_inner_view_background</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
</resources>

View File

@@ -48,7 +48,7 @@
<string name="island_taking_over_text">Connected</string> <string name="island_taking_over_text">Connected</string>
<string name="island_moved_to_remote_text">Moved to Linux</string> <string name="island_moved_to_remote_text">Moved to Linux</string>
<string name="island_moved_to_other_device_text">Moved to other device</string> <string name="island_moved_to_other_device_text">Moved to other device</string>
<string name="island_moved_to_other_device_reversed_text">Reconnect from notification</string> <string name="island_moved_to_other_device_reversed_text">Reconnect from notification</string>
<string name="head_tracking">Head Tracking</string> <string name="head_tracking">Head Tracking</string>
<string name="head_gestures_details">Nod to answer calls, and shake your head to decline.</string> <string name="head_gestures_details">Nod to answer calls, and shake your head to decline.</string>
<string name="general_settings_header">General</string> <string name="general_settings_header">General</string>
@@ -81,4 +81,5 @@
<string name="takeover_ringing_call_desc">Your phone starts ringing</string> <string name="takeover_ringing_call_desc">Your phone starts ringing</string>
<string name="takeover_media_start">Starting media playback</string> <string name="takeover_media_start">Starting media playback</string>
<string name="takeover_media_start_desc">Your phone starts playing media</string> <string name="takeover_media_start_desc">Your phone starts playing media</string>
<string name="undo">Undo</string>
</resources> </resources>

View File

@@ -1,12 +1,6 @@
<resources> <resources>
<style name="Widget.LibrePods.AppWidget.Container" parent="android:Widget"> <style name="Widget.LibrePods.AppWidget.Container" parent="android:Widget"></style>
<item name="android:id">@android:id/background</item>
<item name="android:background">?android:attr/colorBackground</item>
</style>
<style name="Widget.LibrePods.AppWidget.InnerView" parent="android:Widget"> <style name="Widget.LibrePods.AppWidget.InnerView" parent="android:Widget"></style>
<item name="android:background">?android:attr/colorBackground</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
</resources> </resources>