From fa00620b5b411962bf39197365e96820de718ef4 Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Wed, 10 Sep 2025 12:38:27 +0530 Subject: [PATCH] android: clean up a lot of stuff --- android/app/src/main/AndroidManifest.xml | 4 +- .../librepods/CustomDeviceActivity.kt | 118 ++++++++---------- .../me/kavishdevar/librepods/MainActivity.kt | 27 ++-- .../librepods/QuickSettingsDialogActivity.kt | 2 +- .../composables/AccessibilitySettings.kt | 2 +- .../composables/AccessibilitySlider.kt | 4 +- .../librepods/composables/BatteryView.kt | 2 +- .../composables/ControlCenterButton.kt | 4 +- .../composables/IndependentToggle.kt | 5 +- .../librepods/constants/Packets.kt | 2 +- .../librepods/constants/StemAction.kt | 1 - .../screens/AccessibilitySettingsScreen.kt | 3 +- .../librepods/screens/AppSettingsScreen.kt | 60 ++++----- .../librepods/screens/DebugScreen.kt | 9 +- .../screens/EqualizerSettingsScreen.kt | 8 +- .../librepods/screens/Onboarding.kt | 6 +- .../screens/PressAndHoldSettingsScreen.kt | 18 +-- .../librepods/screens/RenameScreen.kt | 5 +- .../screens/TroubleshootingScreen.kt | 13 +- .../librepods/services/AirPodsService.kt | 27 ++-- .../librepods/utils/AACPManager.kt | 10 +- .../kavishdevar/librepods/utils/BLEManager.kt | 16 +-- .../librepods/utils/BluetoothCryptography.kt | 10 +- .../librepods/utils/CrossDevice.kt | 15 +-- .../librepods/utils/IslandWindow.kt | 2 +- .../librepods/utils/KotlinModule.kt | 16 +-- .../librepods/utils/LogCollector.kt | 56 ++++----- .../librepods/utils/MediaController.kt | 4 +- .../librepods/utils/PopupWindow.kt | 11 +- .../res/drawable/app_widget_background.xml | 10 ++ .../app_widget_inner_view_background.xml | 9 ++ .../app/src/main/res/layout/island_window.xml | 4 +- .../main/res/layout/noise_control_widget.xml | 10 +- .../main/res/mipmap-anydpi/ic_launcher.xml | 6 + .../res/mipmap-anydpi/ic_launcher_round.xml | 6 + android/app/src/main/res/raw/blip_yes.wav | Bin 33444 -> 0 bytes .../app/src/main/res/values-v21/styles.xml | 14 --- android/app/src/main/res/values/strings.xml | 3 +- android/app/src/main/res/values/styles.xml | 10 +- 39 files changed, 269 insertions(+), 263 deletions(-) create mode 100644 android/app/src/main/res/drawable/app_widget_background.xml create mode 100644 android/app/src/main/res/drawable/app_widget_inner_view_background.xml create mode 100644 android/app/src/main/res/mipmap-anydpi/ic_launcher.xml create mode 100644 android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml delete mode 100644 android/app/src/main/res/values-v21/styles.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e7f26a1..c960427 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -35,8 +35,6 @@ - - - --> diff --git a/android/app/src/main/java/me/kavishdevar/librepods/CustomDeviceActivity.kt b/android/app/src/main/java/me/kavishdevar/librepods/CustomDeviceActivity.kt index 46b6415..27e4679 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/CustomDeviceActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/CustomDeviceActivity.kt @@ -1,24 +1,23 @@ /* * LibrePods - AirPods liberated from Apple’s ecosystem - * + * * Copyright (C) 2025 LibrePods contributors - * + * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package me.kavishdevar.librepods -import android.Manifest import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager @@ -29,26 +28,12 @@ import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.annotation.RequiresPermission -import androidx.compose.foundation.layout.Arrangement -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.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf 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.composable 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.Dispatchers import kotlinx.coroutines.Job @@ -56,36 +41,40 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext 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.nio.ByteBuffer import java.nio.ByteOrder +@Suppress("PrivatePropertyName") class CustomDevice : ComponentActivity() { private val TAG = "AirPodsAccessibilitySettings" private var socket: BluetoothSocket? = null private val deviceAddress = "28:2D:7F:C2:05:5B" - private val psm = 31 private val uuid: ParcelUuid = ParcelUuid.fromString("00000000-0000-0000-0000-00000000000") // Data states private val isConnected = mutableStateOf(false) - private val leftAmplification = mutableStateOf(1.0f) - private val leftTone = mutableStateOf(1.0f) - private val leftAmbientNoiseReduction = mutableStateOf(0.5f) + private val leftAmplification = mutableFloatStateOf(1.0f) + private val leftTone = mutableFloatStateOf(1.0f) + private val leftAmbientNoiseReduction = mutableFloatStateOf(0.5f) private val leftConversationBoost = mutableStateOf(false) private val leftEQ = mutableStateOf(FloatArray(8) { 50.0f }) - private val rightAmplification = mutableStateOf(1.0f) - private val rightTone = mutableStateOf(1.0f) - private val rightAmbientNoiseReduction = mutableStateOf(0.5f) + private val rightAmplification = mutableFloatStateOf(1.0f) + private val rightTone = mutableFloatStateOf(1.0f) + private val rightAmbientNoiseReduction = mutableFloatStateOf(0.5f) private val rightConversationBoost = mutableStateOf(false) private val rightEQ = mutableStateOf(FloatArray(8) { 50.0f }) private val singleMode = mutableStateOf(false) - private val amplification = mutableStateOf(1.0f) - private val balance = mutableStateOf(0.5f) + private val amplification = mutableFloatStateOf(1.0f) + private val balance = mutableFloatStateOf(0.5f) - private val retryCount = mutableStateOf(0) + private val retryCount = mutableIntStateOf(0) private val showRetryButton = mutableStateOf(false) private val maxRetries = 3 @@ -146,18 +135,19 @@ class CustomDevice : ComponentActivity() { socket?.close() } + @SuppressLint("MissingPermission") private suspend fun connectL2CAP() { - retryCount.value = 0 + retryCount.intValue = 0 // Close any existing socket socket?.close() socket = null - while (retryCount.value < maxRetries) { + while (retryCount.intValue < maxRetries) { 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;") val manager = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager val device: BluetoothDevice = manager.adapter.getRemoteDevice(deviceAddress) - socket = createBluetoothSocket(device, psm) + socket = createBluetoothSocket(device) withTimeout(5000L) { socket?.connect() @@ -177,9 +167,9 @@ class CustomDevice : ComponentActivity() { return } catch (e: Exception) { - Log.e(TAG, "Failed to connect, attempt ${retryCount.value + 1}: ${e.message}") - retryCount.value++ - if (retryCount.value < maxRetries) { + Log.e(TAG, "Failed to connect, attempt ${retryCount.intValue + 1}: ${e.message}") + retryCount.intValue++ + if (retryCount.intValue < maxRetries) { 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 constructorSpecs = listOf( arrayOf(device, type, true, true, 31, uuid), @@ -300,18 +290,18 @@ class CustomDevice : ComponentActivity() { leftEQ.value = newLeftEQ if (singleMode.value) rightEQ.value = newLeftEQ - leftAmplification.value = buffer.float - Log.d(TAG, "Parsed left amplification: ${leftAmplification.value}") - leftTone.value = buffer.float - Log.d(TAG, "Parsed left tone: ${leftTone.value}") - if (singleMode.value) rightTone.value = leftTone.value + leftAmplification.floatValue = buffer.float + Log.d(TAG, "Parsed left amplification: ${leftAmplification.floatValue}") + leftTone.floatValue = buffer.float + Log.d(TAG, "Parsed left tone: ${leftTone.floatValue}") + if (singleMode.value) rightTone.floatValue = leftTone.floatValue val leftConvFloat = buffer.float leftConversationBoost.value = leftConvFloat > 0.5f Log.d(TAG, "Parsed left conversation boost: $leftConvFloat (${leftConversationBoost.value})") if (singleMode.value) rightConversationBoost.value = leftConversationBoost.value - leftAmbientNoiseReduction.value = buffer.float - Log.d(TAG, "Parsed left ambient noise reduction: ${leftAmbientNoiseReduction.value}") - if (singleMode.value) rightAmbientNoiseReduction.value = leftAmbientNoiseReduction.value + leftAmbientNoiseReduction.floatValue = buffer.float + Log.d(TAG, "Parsed left ambient noise reduction: ${leftAmbientNoiseReduction.floatValue}") + if (singleMode.value) rightAmbientNoiseReduction.floatValue = leftAmbientNoiseReduction.floatValue // Right bud val newRightEQ = rightEQ.value.copyOf() @@ -321,24 +311,24 @@ class CustomDevice : ComponentActivity() { } rightEQ.value = newRightEQ - rightAmplification.value = buffer.float - Log.d(TAG, "Parsed right amplification: ${rightAmplification.value}") - rightTone.value = buffer.float - Log.d(TAG, "Parsed right tone: ${rightTone.value}") + rightAmplification.floatValue = buffer.float + Log.d(TAG, "Parsed right amplification: ${rightAmplification.floatValue}") + rightTone.floatValue = buffer.float + Log.d(TAG, "Parsed right tone: ${rightTone.floatValue}") val rightConvFloat = buffer.float rightConversationBoost.value = rightConvFloat > 0.5f Log.d(TAG, "Parsed right conversation boost: $rightConvFloat (${rightConversationBoost.value})") - rightAmbientNoiseReduction.value = buffer.float - Log.d(TAG, "Parsed right ambient noise reduction: ${rightAmbientNoiseReduction.value}") + rightAmbientNoiseReduction.floatValue = buffer.float + Log.d(TAG, "Parsed right ambient noise reduction: ${rightAmbientNoiseReduction.floatValue}") Log.d(TAG, "Settings parsed successfully") // Update single mode values if in single mode if (singleMode.value) { - val avg = (leftAmplification.value + rightAmplification.value) / 2 - amplification.value = avg.coerceIn(0f, 1f) - val diff = rightAmplification.value - leftAmplification.value - balance.value = (0.5f + diff / (2 * avg)).coerceIn(0f, 1f) + val avg = (leftAmplification.floatValue + rightAmplification.floatValue) / 2 + amplification.floatValue = avg.coerceIn(0f, 1f) + val diff = rightAmplification.floatValue - leftAmplification.floatValue + balance.floatValue = (0.5f + diff / (2 * avg)).coerceIn(0f, 1f) } } @@ -363,19 +353,19 @@ class CustomDevice : ComponentActivity() { for (eq in leftEQ.value) { buffer.putFloat(eq) } - buffer.putFloat(leftAmplification.value) - buffer.putFloat(leftTone.value) + buffer.putFloat(leftAmplification.floatValue) + buffer.putFloat(leftTone.floatValue) buffer.putFloat(if (leftConversationBoost.value) 1.0f else 0.0f) - buffer.putFloat(leftAmbientNoiseReduction.value) + buffer.putFloat(leftAmbientNoiseReduction.floatValue) // Right bud for (eq in rightEQ.value) { buffer.putFloat(eq) } - buffer.putFloat(rightAmplification.value) - buffer.putFloat(rightTone.value) + buffer.putFloat(rightAmplification.floatValue) + buffer.putFloat(rightTone.floatValue) buffer.putFloat(if (rightConversationBoost.value) 1.0f else 0.0f) - buffer.putFloat(rightAmbientNoiseReduction.value) + buffer.putFloat(rightAmbientNoiseReduction.floatValue) val packet = buffer.array() Log.d(TAG, "Packet length: ${packet.size}") @@ -393,4 +383,4 @@ class CustomDevice : ComponentActivity() { } } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt index 1d35813..9dba146 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/MainActivity.kt @@ -97,6 +97,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp 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.composable 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.isGranted import com.google.accompanist.permissions.rememberMultiplePermissionsState +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import me.kavishdevar.librepods.constants.AirPodsNotifications import me.kavishdevar.librepods.screens.AirPodsSettingsScreen import me.kavishdevar.librepods.screens.AppSettingsScreen @@ -123,6 +126,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi lateinit var serviceConnection: ServiceConnection lateinit var connectionStatusReceiver: BroadcastReceiver +@ExperimentalHazeMaterialsApi @ExperimentalMaterial3Api class MainActivity : ComponentActivity() { companion object { @@ -137,8 +141,10 @@ class MainActivity : ComponentActivity() { setContent { LibrePodsTheme { - getSharedPreferences("settings", MODE_PRIVATE).edit().putLong("textColor", - MaterialTheme.colorScheme.onSurface.toArgb().toLong()).apply() + getSharedPreferences("settings", MODE_PRIVATE).edit { + putLong( + "textColor", + MaterialTheme.colorScheme.onSurface.toArgb().toLong())} Main() } } @@ -207,8 +213,7 @@ class MainActivity : ComponentActivity() { } private fun handleAddMagicKeys(uri: Uri) { - val context = this - val sharedPreferences = getSharedPreferences("settings", Context.MODE_PRIVATE) + val sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE) val irkHex = uri.getQueryParameter("irk") val encKeyHex = uri.getQueryParameter("enc_key") @@ -217,13 +222,13 @@ class MainActivity : ComponentActivity() { if (irkHex != null && validateHexInput(irkHex)) { val irkBytes = hexStringToByteArray(irkHex) val irkBase64 = Base64.encode(irkBytes) - sharedPreferences.edit().putString("IRK", irkBase64).apply() + sharedPreferences.edit {putString("IRK", irkBase64)} } if (encKeyHex != null && validateHexInput(encKeyHex)) { val encKeyBytes = hexStringToByteArray(encKeyHex) 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() @@ -247,6 +252,7 @@ class MainActivity : ComponentActivity() { } } +@ExperimentalHazeMaterialsApi @SuppressLint("MissingPermission", "InlinedApi", "UnspecifiedRegisterReceiverFlag") @OptIn(ExperimentalPermissionsApi::class) @Composable @@ -404,6 +410,7 @@ fun Main() { } } +@ExperimentalHazeMaterialsApi @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @Composable fun PermissionsScreen( @@ -586,7 +593,7 @@ fun PermissionsScreen( onClick = { val intent = Intent( Settings.ACTION_MANAGE_OVERLAY_PERMISSION, - Uri.parse("package:${context.packageName}") + "package:${context.packageName}".toUri() ) context.startActivity(intent) onOverlaySettingsReturn() @@ -616,9 +623,9 @@ fun PermissionsScreen( Button( onClick = { - val editor = context.getSharedPreferences("settings", MODE_PRIVATE).edit() - editor.putBoolean("overlay_permission_skipped", true) - editor.apply() + context.getSharedPreferences("settings", MODE_PRIVATE).edit { + putBoolean("overlay_permission_skipped", true) + } val intent = Intent(context, MainActivity::class.java) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt b/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt index d8a79d2..b30c3ed 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/QuickSettingsDialogActivity.kt @@ -133,7 +133,7 @@ class QuickSettingsDialogActivity : ComponentActivity() { window.setGravity(Gravity.BOTTOM) Intent(this, AirPodsService::class.java).also { intent -> - bindService(intent, connection, Context.BIND_AUTO_CREATE) + bindService(intent, connection, BIND_AUTO_CREATE) } setContent { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/AccessibilitySettings.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/AccessibilitySettings.kt index 75cbd2a..42c942b 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/AccessibilitySettings.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/AccessibilitySettings.kt @@ -134,7 +134,7 @@ fun AccessibilitySettings() { textColor = textColor ) - val volumeSwipeSpeedOptions = mapOf( + val volumeSwipeSpeedOptions = mapOf( 1.toByte() to "Default", 2.toByte() to "Longer", 3.toByte() to "Longest" diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/AccessibilitySlider.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/AccessibilitySlider.kt index d111c0a..abd8d14 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/AccessibilitySlider.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/AccessibilitySlider.kt @@ -23,10 +23,8 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -135,4 +133,4 @@ fun AccessibilitySliderPreview() { onValueChange = {}, valueRange = 0f..2f ) -} \ No newline at end of file +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryView.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryView.kt index c4740d7..4f90662 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryView.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/BatteryView.kt @@ -99,7 +99,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) { batteryStatus.value = service.getBattery() if (preview) { - batteryStatus.value = listOf( + batteryStatus.value = listOf( Battery(BatteryComponent.LEFT, 100, BatteryStatus.CHARGING), Battery(BatteryComponent.RIGHT, 50, BatteryStatus.NOT_CHARGING), Battery(BatteryComponent.CASE, 5, BatteryStatus.CHARGING) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/ControlCenterButton.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/ControlCenterButton.kt index 2262682..6de2876 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/ControlCenterButton.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/ControlCenterButton.kt @@ -15,7 +15,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ - + +@file:Suppress("unused") + package me.kavishdevar.librepods.composables import androidx.compose.animation.animateColorAsState diff --git a/android/app/src/main/java/me/kavishdevar/librepods/composables/IndependentToggle.kt b/android/app/src/main/java/me/kavishdevar/librepods/composables/IndependentToggle.kt index f3e320b..2cb6e46 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/composables/IndependentToggle.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/composables/IndependentToggle.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.unit.sp import me.kavishdevar.librepods.services.AirPodsService import me.kavishdevar.librepods.utils.AACPManager import kotlin.io.encoding.ExperimentalEncodingApi +import androidx.core.content.edit @Composable 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() { if (controlCommandIdentifier == null) { - sharedPreferences.edit().putBoolean(snakeCasedName, checked).apply() + sharedPreferences.edit { putBoolean(snakeCasedName, checked) } } if (functionName != null && service != null) { val method = @@ -127,4 +128,4 @@ fun IndependentToggle(name: String, service: AirPodsService? = null, functionNam @Composable fun IndependentTogglePreview() { IndependentToggle("Test", AirPodsService(), "test", LocalContext.current.getSharedPreferences("preview", 0), true) -} \ No newline at end of file +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/constants/Packets.kt b/android/app/src/main/java/me/kavishdevar/librepods/constants/Packets.kt index 6c8d661..91f79f4 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/constants/Packets.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/constants/Packets.kt @@ -244,7 +244,7 @@ fun isHeadTrackingData(data: ByteArray): Boolean { ) 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 diff --git a/android/app/src/main/java/me/kavishdevar/librepods/constants/StemAction.kt b/android/app/src/main/java/me/kavishdevar/librepods/constants/StemAction.kt index 3c5be49..fabe01a 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/constants/StemAction.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/constants/StemAction.kt @@ -18,7 +18,6 @@ package me.kavishdevar.librepods.constants -import me.kavishdevar.librepods.constants.StemAction.entries import me.kavishdevar.librepods.utils.AACPManager enum class StemAction { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt index 824098c..f6d1fc2 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AccessibilitySettingsScreen.kt @@ -18,7 +18,6 @@ package me.kavishdevar.librepods.screens -import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -469,4 +468,4 @@ fun AccessibilitySettingsScreen( Spacer(modifier = Modifier.height(16.dp)) } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt index 308c280..d49021e 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/AppSettingsScreen.kt @@ -57,8 +57,6 @@ import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable 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.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.edit import androidx.navigation.NavController import dev.chrisbanes.haze.HazeEffectScope import dev.chrisbanes.haze.HazeState @@ -186,11 +185,11 @@ fun AppSettingsScreen(navController: NavController) { var bleOnlyMode by remember { mutableStateOf(sharedPreferences.getBoolean("ble_only_mode", false)) } - + // Ensure the default value is properly set if not exists LaunchedEffect(Unit) { 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() } ) { showPhoneBatteryInWidget = !showPhoneBatteryInWidget - sharedPreferences.edit().putBoolean("show_phone_battery_in_widget", showPhoneBatteryInWidget).apply() + sharedPreferences.edit { putBoolean("show_phone_battery_in_widget", showPhoneBatteryInWidget)} }, verticalAlignment = Alignment.CenterVertically ) { @@ -340,7 +339,7 @@ fun AppSettingsScreen(navController: NavController) { checked = showPhoneBatteryInWidget, onCheckedChange = { 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() } ) { bleOnlyMode = !bleOnlyMode - sharedPreferences.edit().putBoolean("ble_only_mode", bleOnlyMode).apply() + sharedPreferences.edit { putBoolean("ble_only_mode", bleOnlyMode)} }, verticalAlignment = Alignment.CenterVertically ) { @@ -403,7 +402,7 @@ fun AppSettingsScreen(navController: NavController) { checked = bleOnlyMode, onCheckedChange = { 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) { conversationalAwarenessPauseMusicEnabled = enabled - sharedPreferences.edit().putBoolean("conversational_awareness_pause_music", enabled).apply() + sharedPreferences.edit { putBoolean("conversational_awareness_pause_music", enabled)} } fun updateRelativeConversationalAwarenessVolume(enabled: Boolean) { relativeConversationalAwarenessVolumeEnabled = enabled - sharedPreferences.edit().putBoolean("relative_conversational_awareness_volume", enabled).apply() + sharedPreferences.edit { putBoolean("relative_conversational_awareness_volume", enabled)} } Row( @@ -541,7 +540,7 @@ fun AppSettingsScreen(navController: NavController) { value = sliderValue.floatValue, onValueChange = { sliderValue.floatValue = it - sharedPreferences.edit().putInt("conversational_awareness_volume", it.toInt()).apply() + sharedPreferences.edit { putInt("conversational_awareness_volume", it.toInt())} }, valueRange = 10f..85f, onValueChangeFinished = { @@ -639,7 +638,7 @@ fun AppSettingsScreen(navController: NavController) { ) { fun updateQsClickBehavior(enabled: Boolean) { 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( @@ -708,7 +707,7 @@ fun AppSettingsScreen(navController: NavController) { ) { fun updateDisconnectWhenNotWearing(enabled: Boolean) { disconnectWhenNotWearing = enabled - sharedPreferences.edit().putBoolean("disconnect_when_not_wearing", enabled).apply() + sharedPreferences.edit { putBoolean("disconnect_when_not_wearing", enabled)} } Row( @@ -789,7 +788,7 @@ fun AppSettingsScreen(navController: NavController) { interactionSource = remember { MutableInteractionSource() } ) { takeoverWhenDisconnected = !takeoverWhenDisconnected - sharedPreferences.edit().putBoolean("takeover_when_disconnected", takeoverWhenDisconnected).apply() + sharedPreferences.edit { putBoolean("takeover_when_disconnected", takeoverWhenDisconnected)} }, verticalAlignment = Alignment.CenterVertically ) { @@ -817,7 +816,7 @@ fun AppSettingsScreen(navController: NavController) { checked = takeoverWhenDisconnected, onCheckedChange = { 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() } ) { takeoverWhenIdle = !takeoverWhenIdle - sharedPreferences.edit().putBoolean("takeover_when_idle", takeoverWhenIdle).apply() + sharedPreferences.edit { putBoolean("takeover_when_idle", takeoverWhenIdle)} }, verticalAlignment = Alignment.CenterVertically ) { @@ -858,7 +857,7 @@ fun AppSettingsScreen(navController: NavController) { checked = takeoverWhenIdle, onCheckedChange = { 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() } ) { takeoverWhenMusic = !takeoverWhenMusic - sharedPreferences.edit().putBoolean("takeover_when_music", takeoverWhenMusic).apply() + sharedPreferences.edit { putBoolean("takeover_when_music", takeoverWhenMusic)} }, verticalAlignment = Alignment.CenterVertically ) { @@ -899,7 +898,7 @@ fun AppSettingsScreen(navController: NavController) { checked = takeoverWhenMusic, onCheckedChange = { 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() } ) { takeoverWhenCall = !takeoverWhenCall - sharedPreferences.edit().putBoolean("takeover_when_call", takeoverWhenCall).apply() + sharedPreferences.edit { putBoolean("takeover_when_call", takeoverWhenCall)} }, verticalAlignment = Alignment.CenterVertically ) { @@ -940,7 +939,7 @@ fun AppSettingsScreen(navController: NavController) { checked = takeoverWhenCall, onCheckedChange = { 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() } ) { takeoverWhenRingingCall = !takeoverWhenRingingCall - sharedPreferences.edit().putBoolean("takeover_when_ringing_call", takeoverWhenRingingCall).apply() + sharedPreferences.edit { putBoolean("takeover_when_ringing_call", takeoverWhenRingingCall)} }, verticalAlignment = Alignment.CenterVertically ) { @@ -991,7 +990,7 @@ fun AppSettingsScreen(navController: NavController) { checked = takeoverWhenRingingCall, onCheckedChange = { 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() } ) { takeoverWhenMediaStart = !takeoverWhenMediaStart - sharedPreferences.edit().putBoolean("takeover_when_media_start", takeoverWhenMediaStart).apply() + sharedPreferences.edit { putBoolean("takeover_when_media_start", takeoverWhenMediaStart)} }, verticalAlignment = Alignment.CenterVertically ) { @@ -1032,7 +1031,7 @@ fun AppSettingsScreen(navController: NavController) { checked = takeoverWhenMediaStart, onCheckedChange = { 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() } ) { useAlternateHeadTrackingPackets = !useAlternateHeadTrackingPackets - sharedPreferences.edit().putBoolean("use_alternate_head_tracking_packets", useAlternateHeadTrackingPackets).apply() + sharedPreferences.edit { + putBoolean( + "use_alternate_head_tracking_packets", + useAlternateHeadTrackingPackets)} }, verticalAlignment = Alignment.CenterVertically ) { @@ -1154,7 +1156,7 @@ fun AppSettingsScreen(navController: NavController) { checked = useAlternateHeadTrackingPackets, onCheckedChange = { 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) - 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() showIrkDialog = false @@ -1437,7 +1439,7 @@ fun AppSettingsScreen(navController: NavController) { } 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() showEncKeyDialog = false diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/DebugScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/DebugScreen.kt index 6529cbe..94fff3c 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/DebugScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/DebugScreen.kt @@ -74,6 +74,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -344,11 +345,11 @@ fun DebugScreen(navController: NavController) { val packetLogs = airPodsService?.packetLogsFlow?.collectAsState(emptySet())?.value ?: emptySet() val shouldScrollToBottom = remember { mutableStateOf(true) } - val refreshTrigger = remember { mutableStateOf(0) } - LaunchedEffect(refreshTrigger.value) { + val refreshTrigger = remember { mutableIntStateOf(0) } + LaunchedEffect(refreshTrigger.intValue) { while(true) { 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() } - LaunchedEffect(packetLogs.size, refreshTrigger.value) { + LaunchedEffect(packetLogs.size, refreshTrigger.intValue) { if (shouldScrollToBottom.value && packetLogs.isNotEmpty()) { listState.animateScrollToItem(packetLogs.size - 1) } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/EqualizerSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/EqualizerSettingsScreen.kt index 8197c2d..5b77ebb 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/EqualizerSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/EqualizerSettingsScreen.kt @@ -20,7 +20,6 @@ package me.kavishdevar.librepods.screens -import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -36,7 +35,6 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -44,9 +42,8 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.remember import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color @@ -64,7 +61,6 @@ import kotlinx.coroutines.launch import me.kavishdevar.librepods.R import me.kavishdevar.librepods.composables.AccessibilitySlider import me.kavishdevar.librepods.services.ServiceManager - import kotlin.io.encoding.ExperimentalEncodingApi @OptIn(ExperimentalMaterial3Api::class) @@ -301,4 +297,4 @@ fun EqualizerSettingsScreen( } } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/Onboarding.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/Onboarding.kt index dc7a540..3459266 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/Onboarding.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/Onboarding.kt @@ -84,6 +84,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.kavishdevar.librepods.R import me.kavishdevar.librepods.utils.RadareOffsetFinder +import androidx.core.content.edit @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -528,7 +529,7 @@ fun Onboarding(navController: NavController, activityContext: Context) { onClick = { showSkipDialog = false RadareOffsetFinder.clearHookOffsets() - sharedPreferences.edit().putBoolean("skip_setup", true).apply() + sharedPreferences.edit { putBoolean("skip_setup", true) } navController.navigate("settings") { popUpTo("onboarding") { inclusive = true } } @@ -665,6 +666,3 @@ fun OnboardingPreview() { Onboarding(navController = NavController(LocalContext.current), activityContext = LocalContext.current) } -private suspend fun delay(timeMillis: Long) { - kotlinx.coroutines.delay(timeMillis) -} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/PressAndHoldSettingsScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/PressAndHoldSettingsScreen.kt index 4d2f2c8..eb884c9 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/PressAndHoldSettingsScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/PressAndHoldSettingsScreen.kt @@ -66,6 +66,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.content.edit import androidx.navigation.NavController import me.kavishdevar.librepods.R import me.kavishdevar.librepods.constants.StemAction @@ -178,7 +179,7 @@ fun LongPress(navController: NavController, name: String) { selected = longPressAction == StemAction.CYCLE_NOISE_CONTROL_MODES, onClick = { 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, isLast = false @@ -189,7 +190,7 @@ fun LongPress(navController: NavController, name: String) { selected = longPressAction == StemAction.DIGITAL_ASSISTANT, onClick = { longPressAction = StemAction.DIGITAL_ASSISTANT - sharedPreferences.edit().putString(prefKey, StemAction.DIGITAL_ASSISTANT.name).apply() + sharedPreferences.edit { putString(prefKey, StemAction.DIGITAL_ASSISTANT.name)} }, isFirst = false, isLast = true @@ -271,7 +272,9 @@ fun LongPressElement(name: String, enabled: Boolean = true, resourceId: Int, isF it.identifier == AACPManager.Companion.ControlCommandIdentifiers.LISTENING_MODE_CONFIGS }?.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 isChecked = (byteValue.toInt() and bit) != 0 @@ -331,8 +334,8 @@ fun LongPressElement(name: String, enabled: Boolean = true, resourceId: Int, isF updatedByte ) - context.getSharedPreferences("settings", Context.MODE_PRIVATE).edit() - .putInt("long_press_byte", newValue).apply() + context.getSharedPreferences("settings", Context.MODE_PRIVATE).edit { + putInt("long_press_byte", newValue)} checked.value = false 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 ) - context.getSharedPreferences("settings", Context.MODE_PRIVATE).edit() - .putInt("long_press_byte", newValue).apply() + context.getSharedPreferences("settings", Context.MODE_PRIVATE).edit { + putInt("long_press_byte", newValue) + } checked.value = true Log.d("PressAndHoldSettingsScreen", "Updated: $name, enabled: true, byte: ${updatedByte.toInt() and 0xFF}, bits: ${newValue.toString(2)}") diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/RenameScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/RenameScreen.kt index 9601e93..bcb4dc5 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/RenameScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/RenameScreen.kt @@ -69,6 +69,7 @@ import androidx.navigation.NavController import me.kavishdevar.librepods.R import me.kavishdevar.librepods.services.ServiceManager import kotlin.io.encoding.ExperimentalEncodingApi +import androidx.core.content.edit @OptIn(ExperimentalMaterial3Api::class) @@ -153,7 +154,7 @@ fun RenameScreen(navController: NavController) { value = name.value, onValueChange = { name.value = it - sharedPreferences.edit().putString("name", it.text).apply() + sharedPreferences.edit {putString("name", it.text)} ServiceManager.getService()?.setName(it.text) }, textStyle = TextStyle( @@ -175,7 +176,7 @@ fun RenameScreen(navController: NavController) { IconButton( onClick = { name.value = TextFieldValue("") - sharedPreferences.edit().putString("name", "").apply() + sharedPreferences.edit { putString("name", "") } ServiceManager.getService()?.setName("") } ) { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/screens/TroubleshootingScreen.kt b/android/app/src/main/java/me/kavishdevar/librepods/screens/TroubleshootingScreen.kt index 747ed32..64bf6ff 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/screens/TroubleshootingScreen.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/screens/TroubleshootingScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween -import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut 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.padding 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.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons 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.PlayArrow import androidx.compose.material.icons.filled.Share import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button @@ -65,7 +59,6 @@ import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet 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.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor 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.res.painterResource 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.FontFamily 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.unit.dp import androidx.compose.ui.unit.sp @@ -199,7 +188,7 @@ fun TroubleshootingScreen(navController: NavController) { val buttonBgColor = if (isSystemInDarkTheme()) Color(0xFF333333) else Color(0xFFDDDDDD) var instructionText by remember { mutableStateOf("") } - var isDarkTheme = isSystemInDarkTheme() + val isDarkTheme = isSystemInDarkTheme() var mDensity by remember { mutableFloatStateOf(0f) } LaunchedEffect(Unit) { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt index f2d3a9f..71374d6 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt @@ -753,6 +753,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } } + @Suppress("unused") fun cameraClosed() { cameraActive = false setupStemActions() @@ -894,7 +895,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList this@AirPodsService, (batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0), IslandType.MOVED_TO_OTHER_DEVICE, - reversed = reasonReverseTapped + reversed = true ) } if (!aacpManager.owns) { @@ -909,12 +910,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } override fun onShowNearbyUI() { - // showIsland( - // this@AirPodsService, - // (batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0), - // IslandType.MOVED_TO_OTHER_DEVICE, - // reversed = false - // ) + showIsland( + this@AirPodsService, + (batteryNotification.getBattery().find { it.component == BatteryComponent.LEFT}?.level?: 0).coerceAtMost(batteryNotification.getBattery().find { it.component == BatteryComponent.RIGHT}?.level?: 0), + IslandType.MOVED_TO_OTHER_DEVICE, + reversed = false + ) } override fun onDeviceMetadataReceived(deviceMetadata: ByteArray) { @@ -1462,7 +1463,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } fun setBatteryMetadata() { - device?.let { + device?.let { it -> SystemApisUtils.setMetadata( it, it.METADATA_UNTETHERED_CASE_BATTERY, @@ -1502,7 +1503,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList val componentName = ComponentName(this, BatteryWidget::class.java) 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) 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) { - val batteryManager = getSystemService(BatteryManager::class.java) + val batteryManager = getSystemService(BatteryManager::class.java) val batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) val charging = @@ -1606,7 +1607,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList val appWidgetManager = AppWidgetManager.getInstance(this) val componentName = ComponentName(this, NoiseControlWidget::class.java) 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 allowOffModeValue = aacpManager.controlCommandStatusList.find { it.identifier == AACPManager.Companion.ControlCommandIdentifiers.ALLOW_OFF_OPTION } val allowOffMode = allowOffModeValue?.value?.takeIf { it.isNotEmpty() }?.get(0) == 0x01.toByte() @@ -2198,7 +2199,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList Log.d("AirPodsService", macAddress) sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false) } - device = getSystemService(BluetoothManager::class.java).adapter.bondedDevices.find { + device = getSystemService(BluetoothManager::class.java).adapter.bondedDevices.find { it.address == macAddress } @@ -2335,7 +2336,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList setupStemActions() while (socket.isConnected) { - socket.let { + socket.let { it -> val buffer = ByteArray(1024) val bytesRead = it.inputStream.read(buffer) var data: ByteArray diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/AACPManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/AACPManager.kt index 0a43e93..342db7a 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/AACPManager.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/AACPManager.kt @@ -423,7 +423,7 @@ class AACPManager { ) Log.d( TAG, "Control command list is now: ${ - controlCommandStatusList.joinToString(", ") { + controlCommandStatusList.joinToString(", ") { it -> "${it.identifier.name} (${it.identifier.value.toHexString()}) - ${ 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}"))) { throw IllegalArgumentException("MAC address must be 6 bytes") } - Log.d(TAG, "SELFMAC: ${selfMacAddress}, TARGETMAC: ${targetMacAddress}") - Log.d(TAG, "Sending Media Information packet to ${targetMacAddress}") + Log.d(TAG, "SELFMAC: ${selfMacAddress}, TARGETMAC: $targetMacAddress") + Log.d(TAG, "Sending Media Information packet to $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}"))) { 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 Log.d(TAG, "Sending Media Information packet to ${targetMac ?: "unknown device"}") return sendDataPacket( @@ -842,7 +842,7 @@ class AACPManager { fun createSmartRoutingShowUIPacket(targetMacAddress: String): ByteArray { val opcode = byteArrayOf(Opcodes.SMART_ROUTING, 0x00) - val buffer = ByteBuffer.allocate(134) + val buffer = ByteBuffer.allocate(134) buffer.put( targetMacAddress.split(":").map { it.toInt(16).toByte() }.toByteArray().reversedArray() ) diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/BLEManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/BLEManager.kt index c62b24a..a047d02 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/BLEManager.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/BLEManager.kt @@ -30,7 +30,6 @@ import android.content.SharedPreferences import android.os.Handler import android.os.Looper import android.util.Log -import me.kavishdevar.librepods.services.ServiceManager import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec 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? { return try { if (data.size < 16) { return null } - + val block = data.copyOfRange(data.size - 16, data.size) val cipher = Cipher.getInstance("AES/ECB/NoPadding") val secretKey = SecretKeySpec(key, "AES") @@ -302,7 +302,7 @@ class BLEManager(private val context: Context) { if (previousGlobalState != 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 isFlipped = !primaryLeft - + val leftByteIndex = if (isFlipped) 2 else 1 val rightByteIndex = if (isFlipped) 1 else 2 - + val (isLeftCharging, leftBattery) = formatBattery(decrypted[leftByteIndex].toInt() and 0xFF) val (isRightCharging, rightBattery) = formatBattery(decrypted[rightByteIndex].toInt() and 0xFF) - + val rawCaseBatteryByte = decrypted[3].toInt() and 0xFF 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 isFlipped = !primaryLeft - + 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 caseBattery = flagsCase and 0x0F val flags = (flagsCase shr 4) and 0x0F diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/BluetoothCryptography.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/BluetoothCryptography.kt index 145c89f..4f0ed98 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/BluetoothCryptography.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/BluetoothCryptography.kt @@ -18,6 +18,7 @@ package me.kavishdevar.librepods.utils +import android.annotation.SuppressLint import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec @@ -26,10 +27,10 @@ import javax.crypto.spec.SecretKeySpec * verifying Resolvable Private Addresses (RPA) used by AirPods. */ object BluetoothCryptography { - + /** * 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 irk The Identity Resolving Key to use for verification * @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 - * + * * @param key The key for encryption * @param data The data to encrypt * @return The encrypted data */ + @SuppressLint("GetInstance") fun e(key: ByteArray, data: ByteArray): ByteArray { val swappedKey = key.reversedArray() val swappedData = data.reversedArray() @@ -60,7 +62,7 @@ object BluetoothCryptography { /** * Performs the ah function as specified in Bluetooth Core Specification - * + * * @param k The IRK key * @param r The random part of the address * @return The hash part of the address diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/CrossDevice.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/CrossDevice.kt index f5130ea..8c9ee97 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/CrossDevice.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/CrossDevice.kt @@ -34,6 +34,7 @@ import android.content.Intent import android.content.SharedPreferences import android.os.ParcelUuid import android.util.Log +import androidx.core.content.edit import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -76,7 +77,7 @@ object CrossDevice { CoroutineScope(Dispatchers.IO).launch { Log.d("CrossDevice", "Initializing CrossDevice") 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.bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser // startAdvertising() @@ -111,7 +112,7 @@ object CrossDevice { } } - @SuppressLint("MissingPermission") + @SuppressLint("MissingPermission", "unused") private fun startAdvertising() { CoroutineScope(Dispatchers.IO).launch { val settings = AdvertiseSettings.Builder() @@ -147,7 +148,7 @@ object CrossDevice { fun setAirPodsConnected(connected: Boolean) { if (connected) { isAvailable = false - sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply() + sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false)} clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_CONNECTED.packet) } else { clientSocket?.outputStream?.write(CrossDevicePackets.AIRPODS_DISCONNECTED.packet) @@ -168,7 +169,7 @@ object CrossDevice { val logEntry = "$source: $packetHex" val logs = sharedPreferences.getStringSet(PACKET_LOG_KEY, mutableSetOf())?.toMutableSet() ?: mutableSetOf() logs.add(logEntry) - sharedPreferences.edit().putStringSet(PACKET_LOG_KEY, logs).apply() + sharedPreferences.edit { putStringSet(PACKET_LOG_KEY, logs)} } @SuppressLint("MissingPermission") @@ -207,10 +208,10 @@ object CrossDevice { } } else if (packet.contentEquals(CrossDevicePackets.AIRPODS_CONNECTED.packet)) { isAvailable = true - sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", true).apply() + sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", true)} } else if (packet.contentEquals(CrossDevicePackets.AIRPODS_DISCONNECTED.packet)) { isAvailable = false - sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", false).apply() + sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false)} } else if (packet.contentEquals(CrossDevicePackets.REQUEST_BATTERY_BYTES.packet)) { Log.d("CrossDevice", "Received battery request, battery data: ${batteryBytes.joinToString("") { "%02x".format(it) }}") sendRemotePacket(batteryBytes) @@ -223,7 +224,7 @@ object CrossDevice { } else { if (packet.sliceArray(0..3).contentEquals(CrossDevicePackets.AIRPODS_DATA_HEADER.packet)) { isAvailable = true - sharedPreferences.edit().putBoolean("CrossDeviceIsAvailable", true).apply() + sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", true) } if (packet.size % 2 == 0) { val half = packet.size / 2 if (packet.sliceArray(0 until half).contentEquals(packet.sliceArray(half until packet.size))) { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/IslandWindow.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/IslandWindow.kt index 769f04d..d8ef502 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/IslandWindow.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/IslandWindow.kt @@ -113,7 +113,7 @@ class IslandWindow(private val context: Context) { intent.getParcelableArrayListExtra("data", Battery::class.java) } else { @Suppress("DEPRECATION") - intent.getParcelableArrayListExtra("data") + intent.getParcelableArrayListExtra("data") } updateBatteryDisplay(batteryList) } else if (intent?.action == AirPodsNotifications.DISCONNECT_RECEIVERS) { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/KotlinModule.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/KotlinModule.kt index 1fc4d8a..e2d5046 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/KotlinModule.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/KotlinModule.kt @@ -1,5 +1,6 @@ package me.kavishdevar.librepods.utils +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo @@ -17,6 +18,7 @@ import android.widget.FrameLayout import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout +import androidx.core.net.toUri import io.github.libxposed.api.XposedInterface import io.github.libxposed.api.XposedInterface.AfterHookCallback import io.github.libxposed.api.XposedModule @@ -27,7 +29,7 @@ import io.github.libxposed.api.annotations.XposedHooker private const val TAG = "AirPodsHook" private lateinit var module: KotlinModule - +@SuppressLint("DiscouragedApi", "PrivateApi") class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModule(base, param) { init { Log.i(TAG, "AirPodsHook module initialized at :: ${param.processName}") @@ -60,7 +62,7 @@ class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModul val updateIconMethod = headerControllerClass.getDeclaredMethod( "updateIcon", - android.widget.ImageView::class.java, + ImageView::class.java, String::class.java) hook(updateIconMethod, BluetoothIconHooker::class.java) @@ -89,7 +91,7 @@ class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModul val updateIconMethod = headerControllerClass.getDeclaredMethod( "updateIcon", - android.widget.ImageView::class.java, + ImageView::class.java, String::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 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")) { Log.i(TAG, "Handling AirPods icon URI: $uri") @@ -571,10 +573,10 @@ class KotlinModule(base: XposedInterface, param: ModuleLoadedParam): XposedModul addView(icon) - if (isSelected) { - background = createSelectedBackground(context) + background = if (isSelected) { + createSelectedBackground(context) } else { - background = null + null } setOnClickListener { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/LogCollector.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/LogCollector.kt index 8ce65ab..cc59214 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/LogCollector.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/LogCollector.kt @@ -19,8 +19,6 @@ package me.kavishdevar.librepods.utils import android.content.Context -import android.content.Intent -import android.net.Uri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.BufferedReader @@ -30,7 +28,7 @@ import java.io.InputStreamReader class LogCollector(private val context: Context) { private var isCollecting = false private var logProcess: Process? = null - + suspend fun openXposedSettings(context: Context) { withContext(Dispatchers.IO) { val command = if (android.os.Build.VERSION.SDK_INT >= 29) { @@ -38,42 +36,42 @@ class LogCollector(private val context: Context) { } else { "am broadcast -a android.provider.Telephony.SECRET_CODE -d android_secret_code://5776733 android" } - + executeRootCommand(command) } } - + suspend fun clearLogs() { withContext(Dispatchers.IO) { executeRootCommand("logcat -c") } } - + suspend fun killBluetoothService() { withContext(Dispatchers.IO) { executeRootCommand("killall com.android.bluetooth") } } - + private suspend fun getPackageUIDs(): Pair { return withContext(Dispatchers.IO) { val btUid = executeRootCommand("dumpsys package com.android.bluetooth | grep -m 1 \"uid=\" | sed -E 's/.*uid=([0-9]+).*/\\1/'") .trim() .takeIf { it.isNotEmpty() } - + val appUid = executeRootCommand("dumpsys package me.kavishdevar.librepods | grep -m 1 \"uid=\" | sed -E 's/.*uid=([0-9]+).*/\\1/'") .trim() .takeIf { it.isNotEmpty() } - + Pair(btUid, appUid) } } - + suspend fun startLogCollection(listener: (String) -> Unit, connectionDetectedCallback: () -> Unit): String { return withContext(Dispatchers.IO) { isCollecting = true val (btUid, appUid) = getPackageUIDs() - + val uidFilter = buildString { if (!btUid.isNullOrEmpty() && !appUid.isNullOrEmpty()) { append("$btUid,$appUid") @@ -83,33 +81,33 @@ class LogCollector(private val context: Context) { append(appUid) } } - + val command = if (uidFilter.isNotEmpty()) { "su -c logcat --uid=$uidFilter -v threadtime" } else { "su -c logcat -v threadtime" } - + val logs = StringBuilder() try { logProcess = Runtime.getRuntime().exec(command) val reader = BufferedReader(InputStreamReader(logProcess!!.inputStream)) var line: String? = null var connectionDetected = false - + while (isCollecting && reader.readLine().also { line = it } != null) { line?.let { if (it.contains("")) { connectionDetected = true @@ -118,7 +116,7 @@ class LogCollector(private val context: Context) { connectionDetected = true connectionDetectedCallback() } else if (it.contains("")) { - } + } else if (it.contains("AirPodsService") && it.contains("Connected to device")) { connectionDetected = true connectionDetectedCallback() @@ -139,17 +137,17 @@ class LogCollector(private val context: Context) { logs.append("Error collecting logs: ${e.message}").append("\n") e.printStackTrace() } - + logs.toString() } } - + fun stopLogCollection() { isCollecting = false logProcess?.destroy() logProcess = null } - + suspend fun saveLogToInternalStorage(fileName: String, content: String): File? { return withContext(Dispatchers.IO) { try { @@ -157,7 +155,7 @@ class LogCollector(private val context: Context) { if (!logsDir.exists()) { logsDir.mkdir() } - + val file = File(logsDir, fileName) file.writeText(content) return@withContext file @@ -167,31 +165,31 @@ class LogCollector(private val context: Context) { } } } - + suspend fun addLogMarker(markerType: LogMarkerType, details: String = "") { withContext(Dispatchers.IO) { val timestamp = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", java.util.Locale.US) .format(java.util.Date()) - + val marker = when (markerType) { LogMarkerType.START -> " [$timestamp] Beginning connection test" LogMarkerType.SUCCESS -> " [$timestamp] Connection test completed successfully" LogMarkerType.FAILURE -> " [$timestamp] Connection test failed" LogMarkerType.CUSTOM -> " [$timestamp]" } - + val command = "log -t AirPodsService \"$marker\"" executeRootCommand(command) } } - + enum class LogMarkerType { START, SUCCESS, FAILURE, CUSTOM } - + private suspend fun executeRootCommand(command: String): String { return withContext(Dispatchers.IO) { try { @@ -199,11 +197,11 @@ class LogCollector(private val context: Context) { val reader = BufferedReader(InputStreamReader(process.inputStream)) val output = StringBuilder() var line: String? - + while (reader.readLine().also { line = it } != null) { output.append(line).append("\n") } - + process.waitFor() output.toString() } catch (e: Exception) { diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/MediaController.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/MediaController.kt index aa55c15..631673b 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/MediaController.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/MediaController.kt @@ -275,7 +275,7 @@ object MediaController { } else { initialVolume!! } - smoothVolumeTransition(initialVolume!!, targetVolume.toInt()) + smoothVolumeTransition(initialVolume!!, targetVolume) if (conversationalAwarenessPauseMusic) { sendPause(force = true) } @@ -311,4 +311,4 @@ object MediaController { } }) } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/PopupWindow.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/PopupWindow.kt index d050cba..1d54aa9 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/PopupWindow.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/PopupWindow.kt @@ -49,7 +49,6 @@ import me.kavishdevar.librepods.constants.AirPodsNotifications import me.kavishdevar.librepods.constants.Battery import me.kavishdevar.librepods.constants.BatteryComponent import me.kavishdevar.librepods.constants.BatteryStatus -import kotlin.collections.find @SuppressLint("InflateParams", "ClickableViewAccessibility") class PopupWindow( @@ -172,7 +171,12 @@ class PopupWindow( batteryUpdateReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action == AirPodsNotifications.BATTERY_DATA) { - val batteryList = intent.getParcelableArrayListExtra("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) { updateBatteryStatusFromList(batteryList) } @@ -272,7 +276,4 @@ class PopupWindow( onCloseCallback() } } - - val isShowing: Boolean - get() = mView.parent != null && !isClosing } diff --git a/android/app/src/main/res/drawable/app_widget_background.xml b/android/app/src/main/res/drawable/app_widget_background.xml new file mode 100644 index 0000000..785445c --- /dev/null +++ b/android/app/src/main/res/drawable/app_widget_background.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/app_widget_inner_view_background.xml b/android/app/src/main/res/drawable/app_widget_inner_view_background.xml new file mode 100644 index 0000000..11a09f9 --- /dev/null +++ b/android/app/src/main/res/drawable/app_widget_inner_view_background.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/island_window.xml b/android/app/src/main/res/layout/island_window.xml index 5cb1e14..c804737 100644 --- a/android/app/src/main/res/layout/island_window.xml +++ b/android/app/src/main/res/layout/island_window.xml @@ -113,7 +113,7 @@ android:layout_gravity="center" android:translationX="-12dp" android:background="@drawable/ic_undo_button_bg" - android:contentDescription="Undo button" + android:contentDescription="@string/undo" android:scaleType="centerInside" android:src="@drawable/ic_undo" android:tint="@android:color/white" @@ -121,4 +121,4 @@ android:translationZ="8dp" android:visibility="gone" /> - \ No newline at end of file + diff --git a/android/app/src/main/res/layout/noise_control_widget.xml b/android/app/src/main/res/layout/noise_control_widget.xml index 6b53bb1..baa2f4b 100644 --- a/android/app/src/main/res/layout/noise_control_widget.xml +++ b/android/app/src/main/res/layout/noise_control_widget.xml @@ -4,12 +4,14 @@ android:id="@+id/noise_control_widget" android:layout_width="match_parent" android:layout_height="match_parent" - android:theme="@style/Theme.LibrePods.AppWidgetContainer"> + android:theme="@style/Theme.LibrePods.AppWidgetContainer" + tools:ignore="ContentDescription,NestedWeights"> + android:textSize="12sp" + tools:ignore="NestedWeights" /> + android:textSize="12sp" + tools:ignore="NestedWeights" /> + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..8fde456 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/raw/blip_yes.wav b/android/app/src/main/res/raw/blip_yes.wav index b796b599876f2c211243c8c0e3b0be3bbec77b21..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 33444 zcmWJscQ{vV7`8JSMoBcJ5ZYP4cPJ^9s8lo+mC~S8zNSKTb9v(zU8F!?6A?Dpad{q@qM1A z;$s@TYY$uf=oCAIIkmnR2Hqt}vj1=a!#%f{=)@YK@$H|`Cr_JFr)kV;ydu~b?vCO^ zE+QrP82TTX2dcN1tAvWJJZu2bsp1NyOz*N2( zbGJTRRVr>GbQ*0k?X`l*U4JE$Ija>-{*#h7X_g&p67g=diU0jECY6N>CTH)dm`uE{ zZlX3_$0Uz0Fez^nm>jvc#AM3cWhSaK%}iRSo0!}?WoQx{Jjdiu`b?8I7L!fl4vshJ z|31bf{jjXboJtv!-zQ~Fbby?RQo5YUz729FYb9k&>Y~L>WM2Km9Sh&#KP#VL$*Fg6 zpH>hSYjVW}dspGT3Oe}vR3$9gFM;EGz9E@iZ&7JuAu3rMjZ{L+(cf%UxaH*}Hhd?W znFlEF8<$t|^q;5s4}}n}`R*p4^)#K2nx4Zuv?-U-&*xePAMwp)C0uIDOMZ99TfYDO zC;r`JkdL>O&R-X=ke|>vDPJv4J^$}P&HP(6+WBs++WDbKD_^8>M*dv;>G?hrrsWqT zPRT!AG9_Q_+_d~N3pMihZPm;d2G7c$HBK*o<}v59jJ|Lvszj(pQBI=t~?JRDqW$J;nu7g*dV~4ciH0@nx&4 zc)#R%ydcyb*SRdi66$)`KK0fDd-{bH~!JJf@|K?~88XJ)U*^(d1|R zXYV6^d`BLSzL~*KrYG~yD4zTGMR2kym^Tev8o7Ut8;I@a5p&jYmpXmEEO;V!JN%95 zCTBD6I5)QImlSK;E>5lVkHV8Cib(AGE>vX~fv$fmK+b2n(8GVSxVA?F=N?7apx+j2 z-gd=XtG%(YatL-Zj={^n+{QiAGjMWmHr9@ySoXg{Z27qugZ1V3&5!5U_+J%1X!jaV z=xV~5)@}F{{)n?IyK()DFZl2IKKx)+AKnzwi^D=cPM*+_F72-<=9^zA@?%*l=<1u;^j*oBg!|Jl`xC}Vrt`Al? z{kRd1P|?6+`51gn?+0?*{2YaLCZYISr;vQ01`7SpA6h+WC$H9;%xmCJy7?G$3asrh(UP7$82TZ**~J;h49UgBdP zt8i;WEj~TrH7-5%1|R(K2CwjZga6EajpY^Vu=&PnyhgPWd;31aI>IvC&5N z`g9en@>l|&9o>dPt}x{AFdW%#+>Vyo$e{>(dl+@<6RCLrky>B1WHlQ?m~}a4CntYo zS6!re$y+rZWHy&K-CfLe=B?xX{X6+x!^2!=(mCF{!-wC#7Qolv4dbqUH+Z|+E#9*< zg?G24^T>vWJWDl~CtM?Z$~w-EZ!8$m(L%mB`VpU{T*T!T7x84xBJTbC5nnm)5mz=Y z31I<_(h8E=HAseM8^r%b(i)$$10v}DRQEP-PTUO!6C$?eReTT68>9hD} zu{WMpbOo2p55<$mN8*_$VzJ8p1U%z!GLAD&!#=a`|)+1UOvK) zocdXO#SCLUKgFDTcy8oVUpnw<87_Rh;ThhMb%FcDU*_uTuX2_f#;s{2Z!(YN-R8IW zowOw0awLUUh27zY)zi3#Mmpzd>AdZEI$sl-&exAh=cneT@l>h1+|@gk?~Y35^#)1& zx5X_kRUOOgpGNV1!)si2axkBE$)BJ0_vZ6l&+)m-T=}$nyZN#wHr({;3SJ~57s-u~l=tY_|C;~i16X2}ZlhAOXJsKw- ziZWkiq4>A82$}vu#^dF%uu>JT-=d4Zr3>WFncj$v=)fn_ay@U2#V z?DRJXSBHe-6&aEEpluAc4~fUN&I$N@U($%qr{F+|J2>OoUA%jQo9Q>xaqUn#4y#Yc z6Q`x)GnIGohn`ft(IXkVg(u<=lbg6dAO?>Ljl_Fx!*O#%5FV?31&>zu#j2H_c%9`* zEVt+ou35bkYeZXPzlCOawvIklNt}tR%g5uX#S&P5PZtVUT8a}3}0HHQDwish@u$8rf0!zYZ7;a-!X`P=*`p5GtAd-ASv_kW?hm<93m z!&i8C#U&o4bCDz4bKIuz7@zRaiI*+2<5Aw$T>j*8-lrzuy$5FVhc?rA+fxPJQY6mJ z4t!$rOP{e>TIuZe>HwDgVhh{3M}@sEN~dcS?~#^Wx-hW58otn(g;v`;qKuj#M5FGZ zUn)nGHvksfvjPWQ*?`LjcVPcJ`?1N@WBBO3vpDIJ z7tY)5gE#hF#`5|BIB#AMKJqREFT>%uVDWW4*cgHT$wpyX6opMiZ{X&VC|s!$h0R1G z@%GEt@l0VjUKJUNBj*I;Ot(Osde|Qas$9ZK2QOmTT^{)7*Av)qs|#)s-Gi%tZN`^Z zTVV$!Q@nDIKAyN)8|O=@V*iVy@u`AA^wFplHS8%t4|MOK)`J1)kh(oGR-1_$tDnPF z@iXAHlXr=xb~61croei(Sg~!o7unX5f}S>d{G?2@Py_rI*jMITQc(Q{os z;y(HGgQi?o(~@u9Y0H<(*z-t*{Tw;D@U{sEli2k?LK0o-eD0C#n`!sQnF^X*ZW`Hu)+eqo+Brw(5Hq2oFJ+VTW9 z_BzZDj@!pOM0fBd`#18l8!h;+>cw2*hd#H?)8TPbr}D8g6#4ahN#6eYE3@2G&t?u& z=G+m-BK0n?((cu)xLYq`#QQ&ML+E`PK zzKrcfJ>BAXU8FoVkeGrMrs!bZDt)|3aS7IaYJs}>F8Jl38+Nun zi)Z}wz|IFRV8w86Y;wXEZ&tdDgZBI3OK$%7$^0ugJmd8>N$=0C1^>I_%gc%J{!^yD)Qyf|t0;=y_s zc(&#R?oj8&zb*6PKaP8HT_+Fj2G4OljWgWq#0kDc?-*~N6eiN(0ms$Mok>sfDpAsaADWq(jG)12=* zlCezK!+Kd3v@eYcr%TY)S8sx+A|bH3^AF5 zc*T}oI7itL2f}?=?S&K8soRf5HXXp**B!u93P&^{(Fy1L-HVOs9$eJqfU~ylz*0I} z@#Ox1%4Q zE78+nin3ypP=xALq@{NRDX%p}t6CM1yN1NZ;l$g|Emt4GG+mn zNjKu9Wta!sn(+q~EBT^$OTJ~>I_~GOfzOq<<@#ec^K{QGyzcWJ5-a$&Tc%t+0rFur1O8#gT<-Kri*FQ6En60f9tI^No&6M%yDyN5!Fv?)=^L8S zAd0IV%ivml1^l0p3V!{1D(=(P#O-tSaP6o0SklrE?-W36aeXnKWNn6jZZXHx(^g`? zRTj7guEwI*SK~J4)mZ+O1y+B)3jgP{64%`{$KPF-;ihs^JmrQ7#ti@~xf|j|9t*Hz zyB=1~)Wp$0r{Y!l6Y(KcdHhRN8fV`gM&(JpD9gMFO^SSqj-(2as!K95tPMlwo_eCX z^V^Z4gFX^lA%~1_6~XI*ZE$;|DcI!toeT{GQLFQ9^q9CR6J2e}z>R$@<)jbWbT5K^ z^0>nihA7ir_?)eu-pmYnFWdQPh?Sa+;umtqa#=coXBtoDlIj{<+)0~vE6nCAb>{Qg z1&g?Ghava9FW`%2BW`AI!s9KLaP1aTes`Q1_bD>tbuDIm-6k`>P<1J<*IdH)cw)ZR z1n{6EhTKwTAy;;v!+pGF@yV-ZaLd-;3SwB{hYv8k8Yq%rDT_MYhvr7yD`sSt%UPEV>$~@AN>b7$ZS*&t4Y1*{TU;q@rO% z!3P-OrHqU`04h1Y10B`!M7Spe%{-lmWa>ghh(}P zRGaV!Sg3vwp6gSi+XAoBwguIcE*Z_fZJx(ct=6*}hmSKIPd_G^9mz6pC$ozdS?q(b zfSJy(V24C&81ZOg$A5OPU;cf}-r_F{kQ3wM8l<@XJ~^&_X&jGMP~xXsCh)wbiM&Z{ z692Vp60e&vi9a}|!lk?>^3;>c{O_>|Jl118-x{IBixL$1=?e;c_gHxj)yDGCescWd zGHL#1nFLRpG0gfd{a|yYyO`NvBeS8EY~H0}b}u52eUVOQr7v$XQ*w=+xZ=;Q$a*mM zj(yBodkr(ZvyfTsp1^c}cT@M0G}?aNmL?|OCLy}|Kv6IYUb6^fCrgpm!7V6W%oQaiTts+cFmfo0L7j*0psw;9)aX})T1ILUt{>}=i`qM+-PevH zPJc$bK7T={gT5o1;$Nur`UvJX`$IPqL%2pk2nH=B1k=ny z$(pB=Y1y;mv~b!TdPcpOb|}a(hp<`fY_mC=Ea}Mh|M6gAYXaGI%_#P7XCecm(^>G| zEcWUpVdL%>vS_Cg_R-=QdmmKALT%o#>%J{4x2uzl)P`8u^PlXh`#*N$^)Oq!UX<_b z962&Y`PP>ryrX=GwOamThduh4cJg<2qWcTWa_DB?e|%(i?(Ho7>3e1u)5I30zh)l_ ztC(2EQ?}s1W7Z%=Soxg{w*5jP)2)hNW}mOHs41SzcEdqdzu$(9^IgK8oSV(!+Em!b z8InxY_B~a7n@we9eCe1;I&^HZ8@Ur=2_m0q!s)U8uxWA;oG1qx8+ z7YnrV(l#`4+94Ea=Z+HVeNk;hFiPpTfmA$hqmO^@qnrkUPGmkt*NvW{hla1v+lRGC z^WAG?CI1#hB{!mv63u9m`#Yq*{5=wLYD4J-?I>UTBf3=Ag+i-7p^u55&>4j;)H~FU z7N2~N#t7dcwW+lzd-Mym;B5)YC@DaqeR;^=<{?sja~HXpB%u-j_&L#c{^$bF>0 z@t@vtG}FZnIjvZZGIVDnhcrc$ocj$fHhKh0cLYP-&*remWN={B1Y3FWj4noxI9&9V3{GLp<~MNM-}w z>Fj%5CUZL~WIKEFS-`$xcDT2k9Xj=rxwY3Yhu?45&4bNs+~9jwb@T(PNNZ=~liFFI z%LjJaxRu>q@s7P{f6I1ke$6!A*07=ruh>QWf(5r!FvW$X>}zKci)Wl|K9$SXir!~& zTaww>v>4V0LfOJ?mza9xX%@Q6k=?3Y$Lv;MCg-QiGK^H19hGGApFYs1HI%-+aFx#V zUQA16vxs^B8Nn>$eh^u^7iO6z!}!^+p&SuInI|WrvEI5!)&!wlT2|<8r5&1>>4Ls0 zc%agmm(kX+Qjoe2LAHGkTLx``FSE=DNN zM;)aE%Ak~Q?QpFuf%aZkVVu7uG?o7iLh2TPi-Iv^)rODc)n{{BpcF_GT7|UXLI>?g zm0>#N)7aNF2F&-{Dwg17$2N5zW(8p$tTO%*Gt&rS3uaws)z4#?m`)bEjNL+i|u*h>}z_(A{u3bhobq7WZVsrf?s~3=ARP zSp)hP>VU^fw1n{|wvxrq7#Ub2MQ`OSq}dbq(GTe%ROE6Foj9(Uo@*0l+gerF!28+E z?TrbeW7jb4XFHU z%wbnJYyT0!KJK}}`p!l(IzO6iz7fUxe_v-Ax5C)re?hEZTL2T2^ka{v`m*m2FEXoS zFP6+bnBE3=*1GB>bNlSd_O$G0ZT&l$oWVx+_l-Gw?r6-O?$cop?Nr$|D#r-a<`0s)->wjoH5$Ub78}7Nlg~gZ#Q@e>AA@VX zu0grc4`A2Ka(J+?8AfP)gE!hl(SMU<(XUa8C~9yL@>-~Y@C#j}*0KnF-HuW6s1@kQ z#dRoR;}(?k%N`9%>_e~g527!JoYCZ`&S+8UAtdH`0GZV6Lx)uzQPbmHNK@MZWp1)Z zo%8IGn%T%RlJ*F-?m*>mJNkHY6LRiYhs>w1M28<>sN4z~5zECyTq?n0@d>riB;2XsAS0o$LAg;D3@fx>}6K}oSDdATZ| z{B#>j6Ydz%E6v+zsH8h}eiBGWHO0~3%!gF+ekqO4Z>FcV^wZ&`(#$@5JR7g9&IT=X z*|pCL+33##R^hgoO=&k{*{Lg7qVa0B?B5y|yl?}1)3=H3(6M8Kal4pO^FH>%>JVFS z=rH^C<_K%pc7zQ|x-hAE2iW@Ejx2q>J)0S}js3l}iOt@=k^R?i&5Zu6W!syr*o!Hv znZEuCR>Vx1r!Qb*6Bmr&s?Cn+PiA-X?qIpEVdSy+E^0W|Pl2j?%}4^OzC zgHoxN;q#s0@YIC_cswf$UjJ4G_k_KJi3j_kz56J1ZM7m=w^t2`Zqq;yrL~dc4INZ+ zLKlTC(?tnF9Ta_02kkP@MM|?~qf>|Hp@WYWp`u`8RAqqCoUcoeoPrtJs%D1%^)5lf z_Ye}vH$>kb%tOOQv(Tp>>L|up6)ou*j~4sNqYD$ppx;`uXp#OX)R!fWdd>{N=)P~z zb@vAtJ5U8rn-xIiu2gt~gu?30XW+Ku*09b*4@x=6z->YGKzdyQ2rk|WqylAtg}SC7 zKH{t}a^E7NYZpbX^|lbPj0yDp5=fUE-AU&-d(ctqu2LPtX!=1vnMx<#r)F|Oy0^BF zrq3;-(^kKvPRHt~dT|pyB;8Jx20v3XpKtW>`QNlXW03y&Bf`Yu#Mtm-arS7B1oJ#A z!Q#e9u+Njl+3km-Y#?Kp-ZlP5KdAqrbB^@U7M)L2?eGY0j*T>Ld@c2Meoo(PdP1is zGCE~OCbh0eq4rl}=qbBkdP?sit?N2Sw_LWO56K*=xpfRh2@PcF-)K@)w4AK)%oOf< zzQFj?V7*|grX?tvdmW_uRe%RqL||3q6o^+KcpGkquY1qJ8)boTby6%$xs?tl+ERGP zvlLoNRl$J%1{fUP3eWBAf?gthuzx~744OO)E2c}JAblCMJa{bn*QtbTs#VaA-;enlytJ=ya-E0Ed z4wXLf$1g&^ z@Z<1!{x(==jNzdg6`1y^8Mxf?2D~&?Q0bH_)K@u2<_JniQiBLRouxu?ydD*`f%NJG z3mSvB(u=juw0_zJ>i;d6zUsL}we+&-Y2Q*hHT^YBTHZ-*R(+$+^Zw8cqXy_&w|~?r z;5RkA^MlIF|4NINf1&Q#U+Ck?Z*Y1yOQ*X=r_gaLlc=qJB5l-3pb7iqY3%MO8l@CORaSb^ zvy1KM@9c&2&K@ah)0Rt)F5F5IR38hi`V{ganwAUnkK7e}_J1e%VmAi-oru6{cmd3| z&I5BU3;_S7T5yM@HLS@y122CGg5}HNA!)obg6(~{srms-S$rQpFHVR1{!4>NztW+< zNhY*A#~_$k4rS}=;bo~#s9gF3Y8@VilE1}}MyEK^@e@N~oi^(TCJ^fPodZH0L| z-@?Z5dbmKo9(G@R1A7LW;05J2Xk*z4!^U($eZ@{#e6s~!yI%{3K9oQwzijwfH38=9 z2g7s76HZrig62~IsZaHx?~U0|e#Uh8a-%$apYaWRJ)Q>wGxmYCr*8_L zN?sS9p1+4oy^}|r6Mm7Eb0$z-qC*{91@xEg3cCEw25M{IKoulhX@#yAb*BMzL01$_ zo0>{bgyqt+&QECG`&YDoLnCcm_kl*{bzC^q*$36`>6gsHFTrz?K+(5~z4)TO_f zwjFMuXZz}@^I#nnVRckazK*_rQB8Ayy`aj?B{X9br5AG3==;hWRHyMW&E0o|uGL#d zD|+?mtlDYxq>KVJj~_+f1d3AW3;jgxZ#${lQ%)4hW66_Y3!+x-CVXvLDmZw|2h{S{ z0J|%|SpoB*)|r*?o5>a^TC*GeoqGTVwz|OW*~cMNJ_qHt`@n@!K``o444g26`?xg> zx@SFwZ|vT{F^fLHv2H}k%5meVHit-MKA}7sZ7_IgPk_}&B|=8Q@aX% z;5v^E>6_8}eH*CmfCF`HbEe)VCu!At4|+M+o2CT&QG3%Mx;g6_70ZgD6I&B0D!xmt z-aVkWEQi`GAv8jt(cxT16Q?qI8`muD;d8}-0AQ!z#GGe#0v_{{}BH8+An zy|ZBLmvF#iGQs8t^A#!KnZJpCl z{Lx8Rk#rIsGdKe)+&$pd?-yYN@q>?zf}vFIb@=X33{;7@3Gcl~fP%IJ__OgQ{52{T zE*OY_<4=Ub;>m%q*5@)jFy9A`UwIME&bj|0V zDY&uX5TtMH;9h4ds1kzUi=(q)z35aJQK|qn`$oY6OA%;j@&m*=c7en*&0s_5EATPz z5r_}G2P`jL2hT)Lf)8(ufb*310=);51b;V62<`tC3B#jRi2dH>B=N^S5~AiwrsVpQ z+@5f;z{VKbMPIuf*6=U~MN9sfiRgTc%v!|(v`vv-GvLEfW45G!x;j~*fg09Mspfa7; zC>;u+TXO@b%O77_OT4H=fIFSO?i7W3r|9gkvvit-Cl#r`K%dok)1T@-)Kqz-@BH!{ z?Y2BZ4U_Gt-U&GOoG%PZ3x?HeBcb|m3@pkU*<=62!iLajc=tji zJkf9s)&+(`iH+AFIS~olt72i`izN8%NIDF@k_G?!D}+I>7_>}daO_@eEMDCK?!JT>=WP_W;?a^FYJI_k!V> zQv?26aXg+jZHb{{f+A@i2&d0Z z1kp!_{OL9WAL`ZaLA&RkpwA)>(dokuG;QxDy0zGnHV>H6{6RzNeqE2AST&6{hxFo!2GXV;|&V<&Ti(p2%DeTZ&1HTk+hR;tqK%cEn@NA|t z)cfoT_ndKq)tx8d_1kCRhVvd!Rr&(#YVn4Pn=V1UB7YdLED%~M1wqpfLGW@w5S$}+ z6>eE~1-4~gg2(>52&1dc!{QhJL8AEeV&Hk?7Dx!a2#y`y1O~^=2Xn5-169v1!IAb1 zf%thFLFnLktqB0gYe0gihXUMGMWh(X9JU)KvdCofmtSzKrpp|2jQrfATpx zKKCT;+2Bgg_U@;@e(t95s690<+ezQvaiB(P9BGQ*e)??q5Iug>g%iWlYanik`(IDf%`@#TV9hCc+kXJx>O8_FO!NE384 z3P9Fx8}R1cY2flH6r^aT1BEH2K%@H|D8|3QItK~ZwonfKavTSznkm56g9`9-jyx1d z%R_~Q@-Vz!0X|(c0a|OT!vCz*p~Qd|99}#J#@j4}+8ze*JTQblNA#hk^?W#SyDl8u zIRmPI$#8Gn1Q<9l4!#PKgEyU}VS$|#JSrjy3(Lge5h*dKYc>Rq&;JS5Zv6;u^Lnr{ z^)Z+tmJWPgg@Hp&CqcHa1=#gK9jqz+BZ$9SAVAN91#7#u2#%Pk3e3YZjhp6eGMYR) zIq$LbTH$!zOyPr0NfIMrM5doOKr|8q$x^l3WSt!$dd?NZyS<6;)j)%zCFu&{+x_SQW5 zeT5$VeNKyh*VCYqqenQHGKE@An@U3$s?+|>Gw8jCTC{eL4)uGWOKU#sPv(&I~R>wWz+2@;t`AVG1l~zsKv&6cP|za^7i&qwOFm=aPnYqqV&f#}@OCmpIa6Us^Hg}^%oKRE zPZiQ96QR?I@lfot0<^Fk2j6}i1M{uNKv&B#@ce&c;IQo&sK0+SlwBqZ3p1r)!UIt_ zIq4VRwVlAIqaK(%C{4ETOE1f15p3`nR4*xGp*bc)-7*fA?W?7(cG zbz&?SVD$oB+be?e)B26?X^Q6stTYsUDv1;BIov4R3#CcZhN+}Iasl~w!Hg_AZ9^Pd zok(!PIdblXKY6YjNp__s6LI$(GODeZc&b(rP4^~}712Q+ZSEoa|9mC$D}Ipsj=#v! zll?^V+HcZr{g*Vf43Hr;5$Zcij4t^nPOH--srXANYCCll)!I6e!A?j}nTw)y@4A14 zTYn=H6F!pus%DaMyp~v(KO+Zwi^-0coV0w(A>J(+BvU(uT$>R`Zl4GvyLB#+Chz0q z#S0r!Wokes3@DM5_I6>`&fCKDb2h@AGB5MSs>|if-z_xW?rSWVTJ9$hj->*t$XODtLXRUACI*^o?l(|+|u{J??BI>Qs)JmH8T2Ci-;sUbz`&x36>?JFnx)Yr=Khkjf z8u?gvi<}eBAmUwwh`|!__--X>8`nTS;#T6K(M48&>?M{iM(6A91%HBC_!! zG;>Ilel``S^L9zlcjl6G&2veb)htQZ?vkWBDtZQ!^FZj>rVhO*v>^Sptk&UVsX{dQfBd4!Di(1Wy8b z!M*IC;MAc3@Wo6NhJP1_+viKc+!2p0uPp8#*}uZ-aMuLnk_ z-1G9Ts9OnF{f!ZB|4}U*`&XQ-o<4;H1RIcVkyhmA8b>nq-dXbL!(|e;A)Ng4h$nV? zN4{}yHu3E&Amfe8iG0y3q89RoR7AIsXVo1<)TWzg7=I;3=YJAv_=jYk8zAf9Fxj6e zLNEJ{WN3LY+6lyHf|MBjxJ#7oUMWHkc@2`VkUymP?GLi{{1>wMY8T;?+lVP@Bx*ZB8XN<#FVabU2B+f0@`9ogsUc?Ix$>El8=ye6l4}nKUZ> z5nfY#BHV8rEG!!Y!ZD_|^Wt4^8C}fkH;y{AN6?dXTOcv|tzc8mC?IH`4wM@pc%Wzl zY)(4^%?n=Odr1J03Wxw>Ufu$0Z=?Z-?rh-8^TGVdr9edCCHNCm2S{=gpw=J2=wqKi zp==+>eE9=(Jo*iOj~W0roFc(ZqRrGFjJrpA5+n zQh&3EY)`EqNV_VdmFsFE8or2>Y)~Vqcf^QzeYG%r z!YyHc#ZIBQ0R;S=!l>;$$&ZQzH) zJ79O90Vp=tfP42}fQ0^1V47YCG#}@IJ3H=!CqBtw%(_?*LBc@uV?Q7`?*TSs90aV@ z27F6d3^wM^0$XhqKx+3lfva53z(IY9fOZRwBXe7GbX4l{B3@Yu_pXT$F307< zs?mRhE?VQs-EDeAQe`Qb?XsDOLl?5^fhP(3>Q74Ehm+$c;z(#j3K_fX0ckiWB$bmN zk$6(eP0+blQ$Qy|sc2?Oi}}qEt!$HgU2jyI%OvB1Kp^*HxG= zswkWgyehAHsgd!#nPUZomz)JjBDV$SYia~Z|A~Sd%T)jkp9_-pmVv|bwt%Hk&OrUP z2QW481L1a|;L3z(VC$L)!dImMgQQGwHI#swpM}7zwFI2o@C@V|yaL?47W`Cy1M1Zp zfkb#S*zC~)tjpd5g>$W-^Fk|#YOEd5t(dF-fZ@|Sdb>O%}75KgRIp|X^ z1KpNI;NlreMdHHR`Tep>yc8g-tHz%LeCFK&|r2E8nPAYNOaFZ0sMUqdO zgUHcUzQn%k4ADB{M4a|-Bt`X$N&Pcj(s6JCDY611Fge-A9v%mNdILhzB~gFi-(f!vBxu<}I(Fv)ukW++sG zs0E9lUDAef=+BiI{fAox9s8~5cl8UmS{dFvFkg~4&|!fExX!cdbMp_}B8@Yoe4 zf-}igh4sB$upJe@G-{(}oNVWF|vn4KqPoXD4>=`G}Qn3ldUoZzph(4I$r2%dijRPb9ISIZ8RSA~3 z-4)nrUlhpN8VeRJeraqZl4*1wAI$q6DJPs1yG=NASGZ8&GZR+4ZWA^POAw2giDb=h z9dht9B!_lek@Y4!NYr*0^0(EUWbO1JaXSOZxY1!FwTmcnY+XEYew;+6y}CnY?am-8 zu4IyXD|3nGDMEbbauRA^Kte`8B7vqwL}9RqY+mq~1lJc4+i#DE>(xRsoSRQ-?HKVs zDtJH`h0(4^zb4n^E*w_G!Bw4Gq;f` zBVK|R3&_povxpj2CNJ8=$s@Z~;h6#^^csI%7#`{%T%s{vIA?u8-q+O)hF6+XjJ>s| z3y!@xBrq0_7PweG5@h^r7u@ZU0Kcy*gICYA!Q6eupylLBV4<=ZtXA0vKJ9k{jRBtE zq1+{KZ&o0%uLuRb5|LozhZtb8>K6EJa2v!vO&!TvX`ugd29QX90M1xv0x!=j@NYze zoz7%~wFcS1{YVyAG&>XIyFLKp4Ku*U&@^!9NGiDBkOaon-vo~xVt~t~2w*tkx8B4B zfP*)Efw`$C=o#k*ZuIX3TGpFEpZY4`dR73QF4F&c{{6(g_?05!lP#rg_GZw3Zu$C371@xBGh&wdGb)3 zD8?F*ovJH{lbkJCyL=CE9B?H*vggT;``+a58GmB(Er=+Nx<+hbqsZU4v1H8u?Y#$7 zRL!<7iWo?O3W8z+5fKBK?p0L+f`9@FihzI^$cRc15HKMb2_lFY!HgLstXfrth#+9Z z96>PyM$D-E;%onS{?t9B`P0c zkF$@kcPYo%>;5Io&iFW!eR7=XjV@((+mExg+Q*srRWainjJ2u`Jvdfpove_@y zndu=pHeCHZE%v`czq%CD=B=4@$~mv;R4g&WJ2#} z$6&J6W$6B)115fuK`3(&(ks?MPOc&p9Ab^O^mj&Cdh^kWor}6(#L{VA4z~#F4A?;TM>De7{>B?AWzDx{vOSOZEV{C!< z)@&iZ>JZVC8AOYAc+egfvgpX}qg3(8Mf&>fOPV*MH)}N+$hwbdF>#S03+ZXWR(*6} zkt^I;$pSxSxFM8%QC`ksRwS^?y;7Njbp{&&vf0ydumh4k z+5?Lc*-6`#Y)RjxY?5Xmt2i}>byPaB{koHxw~>f(m1CIKWpy?oPJzvi|4I$BnyI#^ ziaM?>q!#*X=-auLv}Aq{iqezGqx1ndHz^W0*>-|y9D=_LiG@FQZ-+8R3C!2H1;2N{ zhuVcQ$k9g?<&|im{)q;tkE$6melrzW%etW5Y#tg#f{>;1Qsi)HIa2G2M`O#9QH<1D zl-ikrytc1H$F8hLJNDkEF)CB^MXS!aBVR>Fq;$dxZF($1MaRY> z+3&-U>$84HF{TiXh&ExQKZXV2#T>_uYGlVsH&q3r!f3W?5HxBJwNesq) zBsLZz8lDqI^>=Ti6V*zo_Jxa7Nw1yW&*;Huyb@~|K9tS(9m8~VO<4IN3${bYp1C@@ zu&pg~nRZhEYq1GszsN+hjf$&T&x!lch}MOE7B+_F>L; z9?YW5iOD3{upUn!n_8vM)+>)OhqmoDTpPfphwlosCa)8nl&~Nsn3o_Grd-!#lA5p z>H1Q1_TXZ)=7~Qt4V#CoJlv4++3Dy&tu@jZijZxG0h03@jXI)-AhB^j^kqd)l%M_v zT0Fc72llLjN7n6y*$L@zoArFCv2ZNZdDjM%*2e;UCdcjcGbKsN^+YRV5G{K!jq_hth^16kb8#cbGA$@ygaD0VkIhIxNl&f4l$GObsum?&vATe@g9GvBw0 zO%bnT+>qsLnq4$ocQJzXt_o#(eHO8#a(_0gZayQkJS1mdomp7DJzH|jis>ms=G@$ujKI+b^`i{4uTWb%9z;FQ>Yi`84BLB9$(4p%%F#>Ah(!q-f(( z(z;qrXh|InG=>UbO12`*6f}cN1p5Jv-K$C)0 zk&cuG@>0}Af%%5$VKqQft1Z!~V^h&HTPGxTaz*X6p2+yneDqYt55=)Sw4gN@{aCXE zab}_DsB;(+-3UY0r^3+q!C}b$N(fSYvKXn`1tTfhKvar+(e8)y(D)}FX#N}*1ZFy- z6?6*fl{X26b#Z9(M+5X}J=Y0Ck zpp@zrHqiL+W_n>q2c5IC2QyPtV2M2kGUuXUEK6!Mv%Ecl#eO$ob$bwteqza1ZJNS% z6;ES3U1zeO09SV8u{$fgJ%_y&h2wqRoom_ zk>t)6_i|-gZ)dO%*3($2%oH}-!IC{!MC{%|6EnG7Aq_KzOO{LK_t9FS&i z)$iy@hZgE+dY-y%;_2{xyQu%fOv(lvjb{_PVMcYRVL}!|Z zqWhvzsAA|ibR*0Vy|Wb~$?;C4TVRPk_pn79+U!seHyz#n?1U_UGpgvAg&KRiqUqVL zXm^$?THNA-bg$1sFAbg1+4oMUpLjYN`_N7j(``{9wnSIoPDB%rh|$Y9&Z=3c0DAy z=Vi{6UQA*qWGAzt{nqRyvtgen+A;$(TV`|IMzVHWvz0?9vvZ1;EHK}kWk({Wc~Z=z z@{HL^dwrI+YbE4@8^0 z)RD1~2I}mijh6f9B2U{1X!LOd^z?`^T5m2wf;op?lmXOn0-^$YNm}MEIRf!ql0$U@ z^ge|{LZJxtnqq=J#v7sorg}(z(O9$;jYj8oYNCQ2L($!dYDgufKa!p;kF;;}L}60j zpl01mD0lxJoDkRu4ScF#!1JT9kJb)o-<<)c4vd6-;%30J8^=R&qYSJ~Y5=XXVu5?j z8LoYbh2UIfMFx5vBH1h6k?U)R(Og+ex~{>O4vJ5tNMJxd zq=nvl{F2(9{X)~1NVDPcvMkC=k!jm1v%-2cmUn*$JCQho9T7&d*mP|c^H_(ep4MfK zCgYgKgz>C_AJ4wmk7p-+$Fsh<<5;fkSQfN)43qU7%`%u4BS$pYS-oLwl*3@wTWKJx zjqb;sBjwrFe!ZFdk#3rQ;61&e*h)V>xa*#uTyA_e@ z#sTQz+kwcdZ4ioIF$9?$9)?n4N1&T`HBjV5P4vcXq+~4}i5_wz(UDM1G~Gf2{n$Dj ztt}jiZrG_K-DEWs$R2j7Xu{4@r(*;jYcn5FyYlWT0P4L~TMrdYP z1G_$!Lj#>6=pwZhK1oW4Aaxn^NpXien*i(*t3y?#kKlPF56Yh{2KIS(Ib(}F+&g~& zu_{O-MG6fh#$B4$b!gI4)fTkPcrK0l5=)gAWzd^VTWGJI`{@f>N@qN-qPG^Eqg9bN zXj}gWbV{G+^o;&{s(11mRi4y?eZ16*y_b_^zg&}N3Lc8=t;A1869%x!tCiWAMJmj; zRdVm}oeHxkS7B`fRG6BYGTT?upJ_f-Vo|&LvV0kN=B_Ejl3RK*WlJe$yY35}Gx{yH z=-)>5!F`%>{~F!&%Ne?I%L&?b^EmClXCK`zvz31A%AmRHuT=iG1O?0 zJRP3YOcV=tle3ym#Kf&sP`=qFzJEg*=!lYm{+lXrXlN&xS2GCq-EJb`Z3Zm-v=F+u zCcqmLvfHh031y6-SF zpa=SNs3-bZ+Z$Cz$)I7CvS@Oi9FkR(N4`e#$hDU|S|1^Yq~m2#y|E0E&hCwpH}*u4 zlX{^3BfrCII-T%&{9A}DpToN=9>Gif??O$TD=?Kdz{7V=z$CXa*zR@!UY6Sd^<8q{ zUgu;e_bCh>d^j6wq?$pkNm_8-R9SefqY3=r_Jh&Y^T7PJM_h*PI#K)g%Yx%)7b5K5 zN&3vYK^EEdqJx@-)B9~=dgs6ls(m$x#&pEd`U4phtlva8f7wZ$ULB$htIKIz>Ir(Y zy^h*Hx=0(`Z_r=lnrXY)Bl_@uD~)^dlD-@9mX164fzH1Dna;BKN}FDNqqPsZsF(UL z^nFDaT~_>!uI=-cvNNCQ`#T?~cvYMso6$@cRZrv=w`a~;SGvv8mXUj1MR+4 zMb(XY`d$4feVew24$j<4ok2F0&rYHZl6`EKkU2DHl{Hmv8c)s2Rp_&(&!pyHJyD*y zkvQ+QAytO8g4*~Cvjn=N z#KVRa8Bll7MmW>00G=6L1XcGI!>cJ6POq+)uMK@ugO%t?tZGoCQ z9>Iq)Pa&t;3O`D* zUe!oZ)JKh~G>oIym<7#9oJB1j2Ts5f-^#ITvnh(~kJ`DyAcnK~Kk%NJ77*w**hk?45@KKjD#4mke;?PLA zWnKc5zO@#*-CYllE!+&Pj_iOLEBC;y)dyhXrlYX&^Kq!%S^-<#3G{TSgykoz;44Y~ z{;XdOq{C`qQ(-M!<6R5m&ep(_=#xJi|3ZWZO;zFhEMxp z!OmT9?WOHdzH}oTZj=rEjM5;l9uEg)E`@0qeBg)i&T!NsOX%%w023b$hoZL%@Xpi^ zpeg$jaP~L^ejHB%Lj6Q=yXFCBWvRd|T&W{GiGD8ZTsetMXip{@YYvfZw2|x$dPkbS z$Dm(x-=mXsppPTKy%SYO18s+WIW|{YfsZuis3a=H=7x zsXJ-pR_Edm1B}3qrT3y)gDu%yQTEhwZXTt8zx$yn@ zg>b{ONZ2wa4hk(vuzlfLcqb$ay2<50Iwuc~o3>aT3S^+dTPzWy$ z+zH+K?u5#_3!z?p0hC=QiM=m0^NrFL-p!OOV>y2tLUj2Lq?) z0_ltXK-p~!80db3)Bmc^&9d2oYoc}wPX;QHlD7`Tc4-nh{d5;G{ZK_*?3;-1xX;9X zwJg;sSEbVzYEi=seLBqrQEeAnYTZ7AM##*eU;X{4?1&|Fr(P7z@Lx$!^h=;=y2*6F z<5ZeCDV?f~&!k}ovuO97b@a`VY--$ZJ$*T1J>@QC(>W^Hbm{vn$}P{NHV4wFPlsq1AXagNn_(gR6J%JwVgeJN*O3q ztIFPVsKI;U;(LdrU8y4dckdy^ZfRtIj0YKfO@kccE(`Oet%OC-UWyttS8_3dFFB92 z37|W}7u;;f1nE7B!0i>)K<3kJ(BJwUF!AmMtrzx#!=r{kmq*(0$s2um)EB@x^Q@rD zWqa6K<_v*~CtP@80lc?20Iu%67*3U}m+9tFQ08q6?DJ#=e9(I}9J3@2p4DFi4W_Pv zS!d#4`KHxyZtY6g=Dr-}8$?4(`v@4=90DgiUI-Ts_k(Zi=0f=14IbIy1W#O_0^3t9 zV90q9>`a^htE#l%i^{=pu%i;Zw74f6T=W*~Flqv0+G;_m$`SDO%Lb7BBMLnAv<8+3 z2LNhP!yQpF=k(3{^D9fH3-_eY3Ey9+lDLIaNJHr|@}}Pg5~y&9SO-@Vxz#sFvD0%B zVBbxS$jea_rA&)LhSJFEqo~=_@pS%n6MCx#(d%JWbjg~j6uzEL&tGz;!uZ*AWw$3K zrt|2z#s$>mx({77(T}$1`_uCg{&dr4KPvszm#WY7p-YSx(EOmebZ9?Mng!gbPwh@OpIX#v^qyOxw}2_TDI5LrJ@iR@WaFW^cq0rl3wm+s4Oq50c6J)^f=j~Xp-9ot_6&#vx068Ht>_y6nJC#RQSr$4o+HW2j{BW!R*mf;kE0w zu(;bAW^T8F(;6&b^n!^{GnIq6$OI}Z)Q9)SjfER?N5S&w;c#!K8tfGHhf@a0!Q(4? zz?|;)VDHt(V5!D6@UHa~=w(#`){ZX#87niuZ0|5oSz-^4f6@Zk9$&b#vyX638WERf z@Ktok{0mLHkJ~r z2`9)4#Rg(xewldpx1>oS7PPcO`OK{pu!tz8sXfF(mB1U zic%l?(6JBgoZ5$aU+GQhk6!fX)1LHtq%;Loil#Y!C%e7B5VI%miKz7zxiqzv$b4xb zN5^vg9FO771#1iRgJ_Op@lj`?|WV!QT zvc{%ch~9ZY_})r1I?U`T5|c>^_hpdlWodUk=(=GjP5`GN)cPfOzYR zpy~b|Na2v>OBGtMUTLpj7LC2Us4|M5NvU80SWR9z`G}r%wuI;GDwPx z25TPp0Tb~|@U(j(AQ59ge7!R8y4A&TTd#7jJr8hCT4T9@yP90o%njnaeJ=cvx_bP0 z7$-EXOc8E9C>G}Ky)E?G_CtuFDkLXUhq$zHL@c%^8lUHo-Gf8O^QzTkNoXo@DqKhA zM@qi8+}lpx^1H}VaS>7TI6{ISmyoM+6=ZLzK)#%3MAx;7L`|V`8tSYj_rIMtsAmrSq3ewfLl$@zON(K)+NKC%&Ar|2Uq<_X1 za&b&9Dc+Sq%)cd(k4u-6ZJvwBh#PZAvfx1My$~_WA4`U88cga;WXS8H7eYsKgYaB& zuMl%-wV-4x5)#(j!J+>Bu(X?^*ze5^F}K@@>su7VdDse;}a_XYE~P|$We7HnRc0AjUM!Mep6K*J#$w6k0=rEVh- zg13N;iQ7PzQ$Fyyy8|RYEC3d6g}`K1AyDX92y~_vfCEE!0K9QK*tTmc=w-TDqLT+4 zr{{nfSF(WC=e0ofLNbV(91ntTMuX-ZOTe=VACPY}8?;zEfY$fs;Q2@+K;^YT&lEL~ zj%5KE{)v09e206cQpxSyU%(-ycG?=Nx*|hk}@cith^XROy7DF#e7fl;`2r4!yz>Q#}YBocQtQt_^p3S50Q>mSi`{;whP7`p-1Oj&597Mdc0wB{CXdSczHWR0T z%@&TJCDRGKoG=6UPMZNHjGF=C$+0)o8= zq@FVX@1BkY>E}j)!Q$ZnSg3(nQvJZo`7&VGfo|^D``6sKh4;CRh6~)*Pn3JEbdZy` z+Qh9(Oyu|%^Es7afZORafLm&EODxloF21V%S=1fXRv|mZ6b~C$gb${@!@{KB}aD?O>9wFpQk;7(q_Y8cCeyjv^7?w28L74q;~{@1A2w>?<8o<1Z;Yj3(W! zqlnxPEfU0Q5cA%{N&F9Wk`b#$_F`qCaYTtc5XqDD*}chY4Jop<6>;*-ySZJtJGemE zEnG3m<)#kJA#>@rQ%R+cvX)efoG!tI0pD28;hC*x!CzSLO z35P!#33}W0g*#`)3D?qf1g4-RoU$1%s96mb?%YroE@~(WDt0o0vq=x(xXfp)JnlKp zd(n&&>M!BkkQ!`nP=SA(EyBGgZNt}gWa2ZSEAgPa3-E}iHh6*QINaxUKkU%+C0{Nr z@L9Pj{2jBg{J;@86$joNGP&M#*Hoq1N@P4TLj;eNh$gt*7QG7oMbsUsB+ls?EIT#WL%{#D(3_;sJA3i?>B3h*zCT66-Hc5px--Vso=J zv08kZxY;dDe6TQ8oE(uNZtIyWme)%Zmo}{tYrk3{Hk-XnTw5I~Zq*MGpNU)`?yKl7 zKGy6g2F5mG8%-pBXm221*h5>Ko;O&$^+#Xv`wl5_%e?2J$#s`SrRKco==prncd0}X z?KwlF@>X3WUvtLvX)kEn(*3yXeEE`!JE!keSWXzpJC)Ajzf4Ks&5O74MdA{EM?)Qd zY3E)3obwxg#y%;$W|%yFWv+tPC=bQg;&XM_jD0c`7Jfv4D6G}A~+XdK-c;lW!eDF!h`+li67Rh?!>gV&Z zRO&oD=&Bc<|HTupdF+lSrnupZu`YPc<(YW(5+{7-+cdl&!X9tDGX=XT+Thv4t#GWG z1vWa1aJw3ZRV+<#%P0ffRXYy*Tj*e#J`x`w!|+V`!Pt7S3Vv9rgmXLP@LK=gc->Pe z?0f$UFYfH%J!)F{ni==`S((>)otx)))dST$i{kld9!GeOd%O4=vn~9A-PydWe=5J% zeKr5FM;Jd_@Z%krCvP)yCLc4|j=%lIl22KQ_>~h)`D`D3zOz`DAGl-`Uwm=|-)Hv_ z-eL0~e(6J1e(7OVUe#m}|4}rAzp`uuf6G{#zdCq4-`d-Rp9ReLp+Qr4^RF}cIlI01 zqlJO|QmY7l`QbSJ>9Z7meN`6k`6Z8U4anz>KJ4K?CLQL7yeQ=j-x1y>v6@fXR?qiP zyue?(cbPvXcZ1ijyUh=|*~H7+-{N4$^2L%t=gg-lvWoA~~8JD(J@i?>@;#M|9H%1@Q6;N4|T@Z%rW z@k`cS;zv%p%fGUE#`Ejm^T9q+`1S=EoF&PzTvP_(gxV2!oQV$BFxJBcn@zC8EQkjN zSYXvxlku8QQ}F2x4mkIsBlbHx6E8}2!On8C@hfW&T%kP&ccy#cdzN$Y)=_iu4iztK zDdUMhkDrYvC(gp!s!q7G%?=livBnF3m|>5-V%$2w5MP)!7I%4Q;YB_}@wgQOacPDk zj#<_RM;mqX^voOn`Pmk}EcgpG@rR0Q z1&1mw*IQOJexF*NqF83)f7aV{l5?49LBe}e`RKtSBPmX#QtK+}cPd6jz> zrn_AQd!JYrLyWU68qHepPgteAbeRs4cH5z&2Cfb-^-zlj;tBr_D)T zsn;XETTvR9ol?O%dq?B7r5ql8$sT*(@WOs&Avnr?4R)Nl7N6dmgWD6f;?lD_@%Y#x zd@k=Owo@;|Q9m#~IJpvUeQ^?7^smJu@6=&i_cM6R`g6D;^a7UsdJ&&8zJya`E@OxJ zmvKP<%lK8lOZeTwi`ZD_JbpOs3|?{nG=9)ljkheX#EpkA&de;u_r@H>b?b}p-OIc1 z)@R%CnW~LA$T1sl%T2>yljE_raRlyO<%{1`Ib-j(N%;IIJ#2*4aY9RPd~f#?zW>xp zUUOs~U#sT9PcT#D%}lc@6 zI&iAmTWoc%12?q2!C9HFaX9}HOHX-@MWR+*xAQT!Io5)e#!q&X;ZCz+EPL$`7CG<7sRF2a+y2H>n8KG@G@0Uj`N9{y#f7j`K2#Qnq`SgU<=wB~$#*Lg0!zk3e;Wtb=4e$O3m{V^N&j-HL{ zRA=L^b8h%bsvA~yb;E@QZn)`_E7r?$#lihuab&d%9(Uda%b2_3JDP6zdFpI@F3|%! zi@fmR1M{(GkuT2p8i+eLEXJ$XhU4H~(fF|3Dr`3)0SEXeO>|Px5$8` z@ege-@^P)TJP2m|jE`mfIpHY3F!=!g?adzEHK&k&I$%4W6TXReZ_45AXJqmBuB1t1 zlXz8e9N%RV%}1{e<1gL{=D)W2^QLaze9}WNUfa~2_u1#nM^rlSS+gedBh3N7&~*ae zd+rE6%0Y=gsqn3$DeZPeRy?lQpSh*t{Dr8B*Ne?7hVPfFc$`~S9{NzXe7jtok>{a6 zlUGfTOni=vm#huJre0?^nobgU)2DK`O&!K{n(Bwki}d6Mi}JT=i+1D~iVi55i6+)e z5fzur6wMz#N3_SyPgF5)iKuYIGSM8*)uN%%iK5qMQbbo5t`#je%n%)x%M?u>mML15 zkRkdpB3<;cZ<=UnTCym&DnWGR`)bjHN3o)nvQeU*zG0%R&lib~ga?WaYx#Yn`t1r2HUNO2yj{9s-!b1Y0d^BT2mV`#4SaU5O|1jSGBq{%_ zHqg@iRU~S$xt7(>4>Ua-XKGr9MFeO85qRG#OTME=!ZoF85ltb zf5I3W>q#W^BA16o`NZmlMvnRoS_yMz5>9L!k!yrxOhx|cw?FCp)q#_Jg8p9nQ|G4# zmIVLmh;W~X$bg@1GuQf~asLtiFR|1&l%}Ik#NvR6NVA{b zk+kG5iNE>fH=y5J`>V`Pqd!sprub{+KUWNlkeHt36BS?)XbKDs4gb*ntNuUhni>4o z+5eA7u+jXL`TrgXze4=cmA|<8&t3WHx!*YctGoYa(BQJn*KbL1_2Ek;{=tFE|0R^aB>#@#=!on?Bcpsm{Q~T5EF_K74-S?b;1A>seK|iYxfVK(qTlO>V{0{9efj03o^)(cUjrGKV zriOZ^{=R;CK7q!*dM3b+3ltfP1B`sce?b2S^?!h#5fLm|{(P4F2brI(|9@-ZzkvA% zQ+C0TQDG6w{~iK<4T3HKOaET`zZ)HYT>+4f-_HfkBGM;1Ktk`|>i%lfUm^83f#831 z-@nH3KdAq68!-z5LjP9-^*5+rrT#fcWLRKSj88;>Rgh#vSp1e`SS|Ap{&i8;oh+FX zV>OLU^-VQ(9ew;HD??c1!m)p_@ORyRZo_Y(PiRnpzlFiioaW~g`vc&Q5B{_2|7?8! zo6G*&Zv59~;s4cc{NHomZ%+ODivBwHf2)-wx{@s0;LkMsAHu?))AoO|@SjKX|0MRGSpPQiCl&un;oo%q zD=hsf^KZKTq~c#G{F| - - - - - diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 3c3413d..25458ed 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -48,7 +48,7 @@ Connected Moved to Linux Moved to other device - Reconnect from notification + Reconnect from notification Head Tracking Nod to answer calls, and shake your head to decline. General @@ -81,4 +81,5 @@ Your phone starts ringing Starting media playback Your phone starts playing media + Undo diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 30e8f43..df666a4 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -1,12 +1,6 @@ - + - +