Files
librepods/android/app/src/main/java/me/kavishdevar/aln/MainActivity.kt

286 lines
13 KiB
Kotlin

package me.kavishdevar.aln
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.os.ParcelUuid
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import me.kavishdevar.aln.ui.theme.ALNTheme
@ExperimentalMaterial3Api
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val topAppBarTitle = remember { mutableStateOf("AirPods Pro") }
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 (
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
0xFF000000
) else Color(
0xFFF2F2F7
),
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
text = topAppBarTitle.value,
color = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color.White else Color.Black,
)
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = if (MaterialTheme.colorScheme.surface.luminance() < 0.5) Color(
0xFF000000
) else Color(
0xFFF2F2F7
),
)
)
}
) { innerPadding ->
Main(innerPadding, topAppBarTitle)
}
}
}
}
}
@SuppressLint("MissingPermission")
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun Main(paddingValues: PaddingValues, topAppBarTitle: MutableState<String>) {
val bluetoothConnectPermissionState = rememberPermissionState(
permission = "android.permission.BLUETOOTH_CONNECT"
)
if (bluetoothConnectPermissionState.status.isGranted) {
val context = LocalContext.current
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
val bluetoothManager = getSystemService(context, BluetoothManager::class.java)
val bluetoothAdapter = bluetoothManager?.adapter
val airpodsDevice = remember { mutableStateOf<BluetoothDevice?>(null) }
val airPodsService = remember { mutableStateOf<AirPodsService?>(null) }
val navController = rememberNavController()
val disconnectReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
navController.navigate("notConnected")
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(disconnectReceiver, IntentFilter(AirPodsNotifications.AIRPODS_DISCONNECTED),
Context.RECEIVER_NOT_EXPORTED)
}
// Service connection for AirPodsService
val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as AirPodsService.LocalBinder
airPodsService.value = binder.getService()
Log.d("AirPodsService", "Service connected")
}
override fun onServiceDisconnected(name: ComponentName) {
airPodsService.value = null
}
}
// Function to check if AirPods are connected
fun checkIfAirPodsConnected() {
val devices = bluetoothAdapter?.bondedDevices
devices?.forEach { device ->
if (device.uuids.contains(uuid)) {
bluetoothAdapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.A2DP) {
val connectedDevices = proxy.connectedDevices
if (connectedDevices.isNotEmpty()) {
airpodsDevice.value = device
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
topAppBarTitle.value = sharedPreferences.getString("name", device.name) ?: device.name
// Start AirPods service if not running
if (context.getSystemService(AirPodsService::class.java)?.isConnected != true) {
context.startService(Intent(context, AirPodsService::class.java).apply {
putExtra("device", device)
})
context.bindService(Intent(context, AirPodsService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
}
} else {
airpodsDevice.value = null
}
}
bluetoothAdapter.closeProfileProxy(profile, proxy)
}
override fun onServiceDisconnected(profile: Int) {}
}, BluetoothProfile.A2DP)
}
}
}
// Register the receiver in LaunchedEffect
LaunchedEffect(Unit) {
// Initial check for AirPods connection
checkIfAirPodsConnected()
}
// UI logic
NavHost(
navController = navController,
startDestination = "notConnected",
enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(300)) },
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }, animationSpec = tween(300)) },
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }, animationSpec = tween(300)) },
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }, animationSpec = tween(300)) }
) {
composable("notConnected") {
Text("Not Connected...")
}
composable("settings") {
AirPodsSettingsScreen(
paddingValues,
airpodsDevice.value,
service = airPodsService.value,
navController = navController
)
}
composable("debug") {
DebugScreen(navController = navController)
}
}
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
if (airpodsDevice.value != null) {
LaunchedEffect(Unit) {
navController.navigate("settings") {
popUpTo("notConnected") { inclusive = true }
}
}
} else {
Text("No AirPods connected")
}
} else {
// Permission is not granted, request it
Column (
modifier = Modifier.padding(24.dp),
){
val textToShow = if (bluetoothConnectPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but not permanently, explain why it's needed.
"The BLUETOOTH_CONNECT permission is important for this app. Please grant it to proceed."
} else {
// If the user has permanently denied the permission, inform them to enable it in settings.
"BLUETOOTH_CONNECT permission required for this feature. Please enable it in settings."
}
Text(textToShow)
Button(onClick = { bluetoothConnectPermissionState.launchPermissionRequest() }) {
Text("Request permission")
}
}
}
}
@PreviewLightDark
@Composable
fun PreviewAirPodsSettingsScreen() {
AirPodsSettingsScreen(paddingValues = PaddingValues(0.dp), device = null, service = null, navController = rememberNavController())
}