mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-02 16:19:10 +00:00
android: clean up a lot of stuff
This commit is contained in:
@@ -35,8 +35,6 @@
|
||||
<uses-permission android:name="android.permission.LOCAL_MAC_ADDRESS"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<protected-broadcast android:name="batterywidget.impl.action.update_bluetooth_data" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@@ -95,7 +93,7 @@
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="librepods"
|
||||
<data android:scheme="librepods"
|
||||
android:host="add-magic-keys" />
|
||||
</intent-filter> -->
|
||||
</activity>
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -134,7 +134,7 @@ fun AccessibilitySettings() {
|
||||
textColor = textColor
|
||||
)
|
||||
|
||||
val volumeSwipeSpeedOptions = mapOf<Byte, String>(
|
||||
val volumeSwipeSpeedOptions = mapOf(
|
||||
1.toByte() to "Default",
|
||||
2.toByte() to "Longer",
|
||||
3.toByte() to "Longest"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ fun BatteryView(service: AirPodsService, preview: Boolean = false) {
|
||||
batteryStatus.value = service.getBattery()
|
||||
|
||||
if (preview) {
|
||||
batteryStatus.value = listOf<Battery>(
|
||||
batteryStatus.value = listOf(
|
||||
Battery(BatteryComponent.LEFT, 100, BatteryStatus.CHARGING),
|
||||
Battery(BatteryComponent.RIGHT, 50, BatteryStatus.NOT_CHARGING),
|
||||
Battery(BatteryComponent.CASE, 5, BatteryStatus.CHARGING)
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
@file:Suppress("unused")
|
||||
|
||||
package me.kavishdevar.librepods.composables
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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("")
|
||||
}
|
||||
) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>(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>(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
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
@@ -113,7 +113,7 @@ class IslandWindow(private val context: Context) {
|
||||
intent.getParcelableArrayListExtra("data", Battery::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableArrayListExtra<Battery>("data")
|
||||
intent.getParcelableArrayListExtra("data")
|
||||
}
|
||||
updateBatteryDisplay(batteryList)
|
||||
} else if (intent?.action == AirPodsNotifications.DISCONNECT_RECEIVERS) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String?, String?> {
|
||||
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("<LogCollector:")) {
|
||||
logs.append("\n=============\n")
|
||||
}
|
||||
|
||||
|
||||
logs.append(it).append("\n")
|
||||
listener(it)
|
||||
|
||||
|
||||
if (it.contains("<LogCollector:")) {
|
||||
logs.append("=============\n\n")
|
||||
}
|
||||
|
||||
|
||||
if (!connectionDetected) {
|
||||
if (it.contains("<LogCollector:Complete:Success>")) {
|
||||
connectionDetected = true
|
||||
@@ -118,7 +116,7 @@ class LogCollector(private val context: Context) {
|
||||
connectionDetected = true
|
||||
connectionDetectedCallback()
|
||||
} else if (it.contains("<LogCollector:Start>")) {
|
||||
}
|
||||
}
|
||||
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 -> "<LogCollector:Start> [$timestamp] Beginning connection test"
|
||||
LogMarkerType.SUCCESS -> "<LogCollector:Complete:Success> [$timestamp] Connection test completed successfully"
|
||||
LogMarkerType.FAILURE -> "<LogCollector:Complete:Failed> [$timestamp] Connection test failed"
|
||||
LogMarkerType.CUSTOM -> "<LogCollector:Custom:$details> [$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) {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Battery>("data")
|
||||
val batteryList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableArrayListExtra("data", Battery::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableArrayListExtra("data")
|
||||
}
|
||||
if (batteryList != null) {
|
||||
updateBatteryStatusFromList(batteryList)
|
||||
}
|
||||
@@ -272,7 +276,4 @@ class PopupWindow(
|
||||
onCloseCallback()
|
||||
}
|
||||
}
|
||||
|
||||
val isShowing: Boolean
|
||||
get() = mView.parent != null && !isClosing
|
||||
}
|
||||
|
||||
10
android/app/src/main/res/drawable/app_widget_background.xml
Normal file
10
android/app/src/main/res/drawable/app_widget_background.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Background for widgets to make the rounded corners based on the
|
||||
appWidgetRadius attribute value
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<corners android:radius="?attr/appWidgetRadius" />
|
||||
<solid android:color="?android:attr/colorBackground" />
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Background for views inside widgets to make the rounded corners based on the
|
||||
appWidgetInnerRadius attribute value
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="?attr/appWidgetInnerRadius" />
|
||||
<solid android:color="?android:attr/colorAccent" />
|
||||
</shape>
|
||||
@@ -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" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -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">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@android:id/background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
@@ -70,7 +72,8 @@
|
||||
android:shadowRadius="12"
|
||||
android:text="@string/transparency"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp" />
|
||||
android:textSize="12sp"
|
||||
tools:ignore="NestedWeights" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
@@ -102,7 +105,8 @@
|
||||
android:shadowRadius="12"
|
||||
android:text="@string/adaptive"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp" />
|
||||
android:textSize="12sp"
|
||||
tools:ignore="NestedWeights" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
||||
6
android/app/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal file
6
android/app/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
Binary file not shown.
@@ -1,14 +0,0 @@
|
||||
<resources>
|
||||
|
||||
<style name="Widget.LibrePods.AppWidget.Container" parent="android:Widget">
|
||||
<item name="android:id">@android:id/background</item>
|
||||
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||
<item name="android:background">@drawable/app_widget_background</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.LibrePods.AppWidget.InnerView" parent="android:Widget">
|
||||
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||
<item name="android:background">@drawable/app_widget_inner_view_background</item>
|
||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -48,7 +48,7 @@
|
||||
<string name="island_taking_over_text">Connected</string>
|
||||
<string name="island_moved_to_remote_text">Moved to Linux</string>
|
||||
<string name="island_moved_to_other_device_text">Moved to other device</string>
|
||||
<string name="island_moved_to_other_device_reversed_text">Reconnect from notification</string>
|
||||
<string name="island_moved_to_other_device_reversed_text">Reconnect from notification</string>
|
||||
<string name="head_tracking">Head Tracking</string>
|
||||
<string name="head_gestures_details">Nod to answer calls, and shake your head to decline.</string>
|
||||
<string name="general_settings_header">General</string>
|
||||
@@ -81,4 +81,5 @@
|
||||
<string name="takeover_ringing_call_desc">Your phone starts ringing</string>
|
||||
<string name="takeover_media_start">Starting media playback</string>
|
||||
<string name="takeover_media_start_desc">Your phone starts playing media</string>
|
||||
<string name="undo">Undo</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<resources>
|
||||
|
||||
<style name="Widget.LibrePods.AppWidget.Container" parent="android:Widget">
|
||||
<item name="android:id">@android:id/background</item>
|
||||
<item name="android:background">?android:attr/colorBackground</item>
|
||||
</style>
|
||||
<style name="Widget.LibrePods.AppWidget.Container" parent="android:Widget"></style>
|
||||
|
||||
<style name="Widget.LibrePods.AppWidget.InnerView" parent="android:Widget">
|
||||
<item name="android:background">?android:attr/colorBackground</item>
|
||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||
</style>
|
||||
<style name="Widget.LibrePods.AppWidget.InnerView" parent="android:Widget"></style>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user