try to add automatic device connection detection; add "Off Listening Mode" toggle

This commit is contained in:
Kavish Devar
2024-11-27 00:38:45 +05:30
parent c360c21305
commit 58de49d1b1
9 changed files with 354 additions and 59 deletions

View File

@@ -7,31 +7,44 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" <uses-permission
android:name="android.permission.BLUETOOTH_PRIVILEGED"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" <uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.ALN" android:theme="@style/Theme.ALN"
android:enableOnBackInvokedCallback="true" tools:ignore="UnusedAttribute"
tools:targetApi="31" tools:targetApi="31">
tools:ignore="UnusedAttribute"> <activity
android:name=".CustomDevice"
android:exported="true"
android:label="@string/title_activity_custom_device"
android:theme="@style/Theme.ALN">
<intent-filter>
<!-- <action android:name="android.intent.action.MAIN" />-->
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.ALN"> android:theme="@style/Theme.ALN">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
@@ -42,7 +55,6 @@
android:exported="true" android:exported="true"
android:foregroundServiceType="connectedDevice" android:foregroundServiceType="connectedDevice"
android:permission="android.permission.BLUETOOTH_CONNECT" /> android:permission="android.permission.BLUETOOTH_CONNECT" />
<service <service
android:name=".AirPodsQSService" android:name=".AirPodsQSService"
android:exported="true" android:exported="true"
@@ -53,18 +65,6 @@
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>
</service> </service>
<!-- <receiver android:name=".StartupReceiver"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.bluetooth.device.action.ACL_CONNECTED" />-->
<!-- <action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" />-->
<!-- <action android:name="android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED" />-->
<!-- <action android:name="android.bluetooth.device.action.BOND_STATE_CHANGED" />-->
<!-- <action android:name="android.bluetooth.device.action.NAME_CHANGED" />-->
<!-- <action android:name="android.intent.action.BOOT_COMPLETED" />-->
<!-- <action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
</application> </application>
</manifest> </manifest>

View File

@@ -10,8 +10,8 @@ import android.service.quicksettings.TileService
import android.util.Log import android.util.Log
class AirPodsQSService: TileService() { class AirPodsQSService: TileService() {
private val ancModes = listOf(NoiseControlMode.OFF.name, NoiseControlMode.NOISE_CANCELLATION.name, NoiseControlMode.TRANSPARENCY.name, NoiseControlMode.ADAPTIVE.name) private val ancModes = listOf(NoiseControlMode.NOISE_CANCELLATION.name, NoiseControlMode.TRANSPARENCY.name, NoiseControlMode.ADAPTIVE.name)
private var currentModeIndex = 3 private var currentModeIndex = 2
private lateinit var ancStatusReceiver: BroadcastReceiver private lateinit var ancStatusReceiver: BroadcastReceiver
private lateinit var availabilityReceiver: BroadcastReceiver private lateinit var availabilityReceiver: BroadcastReceiver
@@ -63,8 +63,24 @@ class AirPodsQSService: TileService() {
override fun onStopListening() { override fun onStopListening() {
super.onStopListening() super.onStopListening()
unregisterReceiver(ancStatusReceiver) try {
unregisterReceiver(availabilityReceiver) unregisterReceiver(ancStatusReceiver)
}
catch (
e: IllegalArgumentException
)
{
Log.e("QuickSettingTileService", "Receiver not registered")
}
try {
unregisterReceiver(availabilityReceiver)
}
catch (
e: IllegalArgumentException
)
{
Log.e("QuickSettingTileService", "Receiver not registered")
}
} }
override fun onClick() { override fun onClick() {

View File

@@ -1,3 +1,5 @@
@file:Suppress("unused")
package me.kavishdevar.aln package me.kavishdevar.aln
import android.annotation.SuppressLint import android.annotation.SuppressLint
@@ -90,6 +92,10 @@ class AirPodsService : Service() {
socket?.outputStream?.write(if (enabled) Enums.SET_CONVERSATION_AWARENESS_ON.value else Enums.SET_CONVERSATION_AWARENESS_OFF.value) socket?.outputStream?.write(if (enabled) Enums.SET_CONVERSATION_AWARENESS_ON.value else Enums.SET_CONVERSATION_AWARENESS_OFF.value)
} }
fun setOffListeningMode(enabled: Boolean) {
socket?.outputStream?.write(byteArrayOf(0x04, 0x00 ,0x04, 0x00, 0x09, 0x00, 0x34, if (enabled) 0x01 else 0x02, 0x00, 0x00, 0x00))
}
fun setAdaptiveStrength(strength: Int) { fun setAdaptiveStrength(strength: Int) {
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2E, strength.toByte(), 0x00, 0x00, 0x00) val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x09, 0x00, 0x2E, strength.toByte(), 0x00, 0x00, 0x00)
socket?.outputStream?.write(bytes) socket?.outputStream?.write(bytes)

View File

@@ -150,6 +150,33 @@ fun BatteryView() {
} }
} }
@Composable
fun AccessibilitySettings(service: AirPodsService, sharedPreferences: SharedPreferences) {
val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5
val textColor = if (isDarkTheme) Color.White else Color.Black
Text(
text = "ACCESSIBILITY",
style = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Light,
color = textColor.copy(alpha = 0.6f)
),
modifier = Modifier.padding(8.dp, bottom = 2.dp)
)
val backgroundColor = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFFFFFFF)
Column(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor, RoundedCornerShape(14.dp))
.padding(top = 2.dp)
) {
//
}
}
@SuppressLint("MissingPermission", "NewApi") @SuppressLint("MissingPermission", "NewApi")
@Composable @Composable
fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?, service: AirPodsService?, fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?, service: AirPodsService?,
@@ -203,6 +230,11 @@ fun AirPodsSettingsScreen(paddingValues: PaddingValues, device: BluetoothDevice?
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
IndependentToggle(name = "Automatic Ear Detection", service = service, functionName = "setEarDetection", sharedPreferences = sharedPreferences, true) IndependentToggle(name = "Automatic Ear Detection", service = service, functionName = "setEarDetection", sharedPreferences = sharedPreferences, true)
Spacer(modifier = Modifier.height(16.dp))
IndependentToggle(name = "Off Listening Mode", service = service, functionName = "setOffListeningMode", sharedPreferences = sharedPreferences, false)
Spacer(modifier = Modifier.height(16.dp))
AccessibilitySettings(service = service, sharedPreferences = sharedPreferences)
// Spacer(modifier = Modifier.height(16.dp)) // Spacer(modifier = Modifier.height(16.dp))
// val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5 // val isDarkTheme = MaterialTheme.colorScheme.surface.luminance() < 0.5

View File

@@ -0,0 +1,159 @@
package me.kavishdevar.aln
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothDevice.TRANSPORT_LE
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothManager
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
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 me.kavishdevar.aln.ui.theme.ALNTheme
import org.lsposed.hiddenapibypass.HiddenApiBypass
import java.util.UUID
class CustomDevice : ComponentActivity() {
@SuppressLint("MissingPermission", "CoroutineCreationDuringComposition", "NewApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ALNTheme {
val connect = remember { mutableStateOf(false) }
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Custom Device", style = MaterialTheme.typography.titleLarge)
}
}
) { innerPadding ->
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
val manager = getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
// val device: BluetoothDevice = manager.adapter.getRemoteDevice("EC:D6:F4:3D:89:B8")
val device: BluetoothDevice = manager.adapter.getRemoteDevice("E0:90:8F:D9:94:73")
// val socket = device.createInsecureL2capChannel(31)
// socket.outputStream.write(byteArrayOf(0x12,0x3B,0x00,0x02, 0x00))
// socket.outputStream.write(byteArrayOf(0x12, 0x3A, 0x00, 0x01, 0x00, 0x08,0x01))
val gatt = device.connectGatt(this, true, object: BluetoothGattCallback() {
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// Step 2: Iterate through the services and characteristics
gatt.services.forEach { service ->
Log.d("GATT", "Service UUID: ${service.uuid}")
service.characteristics.forEach { characteristic ->
Log.d("GATT", " Characteristic UUID: ${characteristic.uuid}")
}
}
}
}
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothGatt.STATE_CONNECTED) {
Log.d("GATT", "Connected to GATT server")
gatt.discoverServices() // Discover services after connection
}
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d("BLE", "Write successful for UUID: ${characteristic.uuid}")
} else {
Log.e("BLE", "Write failed for UUID: ${characteristic.uuid}, status: $status")
}
}
}, TRANSPORT_LE, 1)
if (connect.value) {
try {
gatt.connect()
}
catch (e: Exception) {
e.printStackTrace()
}
connect.value = false
}
Column (
modifier = Modifier.padding(innerPadding),
verticalArrangement = Arrangement.spacedBy(16.dp)
)
{
Button(
onClick = { connect.value = true }
)
{
Text("Connect")
}
Button(onClick = {
val characteristicUuid = "4f860002-943b-49ef-bed4-2f730304427a"
val value = byteArrayOf(0x01, 0x00, 0x02)
sendWriteRequest(gatt, characteristicUuid, value)
}) {
Text("Play Sound")
}
}
}
}
}
}
}
@SuppressLint("MissingPermission", "NewApi")
fun sendWriteRequest(
gatt: BluetoothGatt,
characteristicUuid: String,
value: ByteArray
) {
// Retrieve the service containing the characteristic
val service = gatt.services.find { service ->
service.characteristics.any { it.uuid.toString() == characteristicUuid }
}
if (service == null) {
Log.e("GATT", "Service containing characteristic UUID $characteristicUuid not found.")
return
}
// Retrieve the characteristic
val characteristic = service.getCharacteristic(UUID.fromString(characteristicUuid))
if (characteristic == null) {
Log.e("GATT", "Characteristic with UUID $characteristicUuid not found.")
return
}
// Send the write request
val success = gatt.writeCharacteristic(characteristic, value, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
Log.d("GATT", "Write request sent $success to UUID: $characteristicUuid")
}

View File

@@ -1,7 +1,6 @@
package me.kavishdevar.aln package me.kavishdevar.aln
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
@@ -43,6 +42,7 @@ import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.ContextCompat.getSystemService
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
@@ -61,6 +61,43 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
val topAppBarTitle = remember { mutableStateOf("AirPods Pro") } val topAppBarTitle = remember { mutableStateOf("AirPods Pro") }
ALNTheme { ALNTheme {
val navController = rememberNavController()
registerReceiver(object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
val bluetoothDevice =
intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE", BluetoothDevice::class.java)
val action = intent.action
// Airpods filter
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
Log.d("BluetoothReceiver", "Received broadcast")
// Airpods connected, show notification.
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
val uuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
if (bluetoothDevice.uuids.contains(uuid)) {
topAppBarTitle.value = bluetoothDevice.name
}
// start service
startService(Intent(context, AirPodsService::class.java).apply {
putExtra("device", bluetoothDevice)
})
Log.d("AirPodsService", "Service started")
context?.sendBroadcast(Intent(AirPodsNotifications.AIRPODS_CONNECTED))
}
// Airpods disconnected, remove notification but leave the scanner going.
if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action
|| BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action
) {
topAppBarTitle.value = "AirPods Pro"
// stop service
stopService(Intent(context, AirPodsService::class.java))
Log.d("AirPodsService", "Service stopped")
}
}
}
}, BluetoothReceiver.buildFilter())
Scaffold ( Scaffold (
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color( containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
0xFF000000 0xFF000000
@@ -109,6 +146,7 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) } val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
val navController = rememberNavController() val navController = rememberNavController()
val disconnectReceiver = object : BroadcastReceiver() { val disconnectReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
navController.navigate("notConnected") navController.navigate("notConnected")
@@ -165,42 +203,8 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
} }
} }
// BroadcastReceiver to listen for connection state changes
val bluetoothReceiver = remember {
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val action = intent?.action
val device = intent?.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
if (action == BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED) {
when (intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1)) {
BluetoothAdapter.STATE_CONNECTED -> {
if (device?.uuids?.contains(uuid) == true) {
airpodsDevice.value = device
checkIfAirPodsConnected()
}
}
BluetoothAdapter.STATE_DISCONNECTED -> {
if (device?.uuids?.contains(uuid) == true) {
airpodsDevice.value = null
// Show not connected screen when AirPods disconnect
navController.navigate("notConnected")
}
}
}
}
}
}
}
// Register the receiver in LaunchedEffect // Register the receiver in LaunchedEffect
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
val filter = IntentFilter().apply {
addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(bluetoothReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
}
// Initial check for AirPods connection // Initial check for AirPods connection
checkIfAirPodsConnected() checkIfAirPodsConnected()
} }
@@ -230,6 +234,21 @@ fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
} }
} }
ContextCompat.registerReceiver(
context,
object : BroadcastReceiver() {
@SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onReceive(context: Context?, intent: Intent) {
Log.d("PLEASE NAVIGATE", "TO SETTINGS")
navController.navigate("settings") {
popUpTo("notConnected") { inclusive = true }
}
}
},
IntentFilter(AirPodsNotifications.AIRPODS_CONNECTED),
ContextCompat.RECEIVER_NOT_EXPORTED
)
// Automatically navigate to settings screen if AirPods are connected // Automatically navigate to settings screen if AirPods are connected
if (airpodsDevice.value != null) { if (airpodsDevice.value != null) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {

View File

@@ -0,0 +1,62 @@
package me.kavishdevar.aln
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
class BluetoothReceiver : BroadcastReceiver() {
fun onConnect(bluetoothDevice: BluetoothDevice?) {
}
fun onDisconnect(bluetoothDevice: BluetoothDevice?) {
}
@SuppressLint("NewApi")
override fun onReceive(context: Context?, intent: Intent) {
val bluetoothDevice =
intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE", BluetoothDevice::class.java)
val action = intent.action
// Airpods filter
if (bluetoothDevice != null && action != null && !action.isEmpty()) {
// Airpods connected, show notification.
if (BluetoothDevice.ACTION_ACL_CONNECTED == action) {
onConnect(bluetoothDevice)
}
// Airpods disconnected, remove notification but leave the scanner going.
if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action
|| BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED == action
) {
onDisconnect(bluetoothDevice)
}
}
}
companion object {
/**
* When the service is created, we register to get as many bluetooth and airpods related events as possible.
* ACL_CONNECTED and ACL_DISCONNECTED should have been enough, but you never know with android these days.
*/
fun buildFilter(): IntentFilter {
val intentFilter = IntentFilter()
intentFilter.addAction("android.bluetooth.device.action.ACL_CONNECTED")
intentFilter.addAction("android.bluetooth.device.action.ACL_DISCONNECTED")
intentFilter.addAction("android.bluetooth.device.action.BOND_STATE_CHANGED")
intentFilter.addAction("android.bluetooth.device.action.NAME_CHANGED")
intentFilter.addAction("android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED")
intentFilter.addAction("android.bluetooth.adapter.action.STATE_CHANGED")
intentFilter.addAction("android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED")
intentFilter.addAction("android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT")
intentFilter.addAction("android.bluetooth.a2dp.profile.action.CONNECTION_STATE_CHANGED")
intentFilter.addAction("android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED")
intentFilter.addCategory("android.bluetooth.headset.intent.category.companyid.76")
return intentFilter
}
}
}

View File

@@ -1,4 +1,5 @@
<resources> <resources>
<string name="app_name">ALN</string> <string name="app_name">ALN</string>
<string name="title_activity_debug">DebugActivity</string> <string name="title_activity_debug">DebugActivity</string>
<string name="title_activity_custom_device">CustomDevice</string>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
[versions] [versions]
accompanistPermissions = "0.36.0" accompanistPermissions = "0.36.0"
agp = "8.7.0" agp = "8.7.2"
hiddenapibypass = "4.3" hiddenapibypass = "4.3"
kotlin = "2.0.0" kotlin = "2.0.0"
coreKtx = "1.13.1" coreKtx = "1.13.1"