android: clean up a lot of stuff

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

View File

@@ -35,8 +35,6 @@
<uses-permission android:name="android.permission.LOCAL_MAC_ADDRESS"
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>

View File

@@ -1,24 +1,23 @@
/*
* LibrePods - AirPods liberated from Apples 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() {
}
}
}
}
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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
)
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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))
}
}
}
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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(
}
}
}
}
}

View File

@@ -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)
}

View File

@@ -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)}")

View File

@@ -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("")
}
) {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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()
)

View File

@@ -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

View File

@@ -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

View File

@@ -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))) {

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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 {
}
})
}
}
}

View File

@@ -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
}

View File

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

View File

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

View File

@@ -113,7 +113,7 @@
android:layout_gravity="center"
android: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>

View File

@@ -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

View File

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

View File

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

View File

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

View File

@@ -48,7 +48,7 @@
<string name="island_taking_over_text">Connected</string>
<string name="island_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>

View File

@@ -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>