/*
* AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
*
* Copyright (C) 2024 Kavish Devar
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package me.kavishdevar.aln
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Context.RECEIVER_EXPORTED
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.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.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
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.rememberMultiplePermissionsState
import me.kavishdevar.aln.screens.AirPodsSettingsScreen
import me.kavishdevar.aln.screens.DebugScreen
import me.kavishdevar.aln.screens.LongPress
import me.kavishdevar.aln.services.AirPodsService
import me.kavishdevar.aln.ui.theme.ALNTheme
import me.kavishdevar.aln.utils.AirPodsNotifications
lateinit var serviceConnection: ServiceConnection
lateinit var connectionStatusReceiver: BroadcastReceiver
@ExperimentalMaterial3Api
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ALNTheme {
getSharedPreferences("settings", MODE_PRIVATE).edit().putLong("textColor",
MaterialTheme.colorScheme.onSurface.toArgb().toLong()).apply()
Main()
startService(Intent(this, AirPodsService::class.java))
}
}
}
override fun onDestroy() {
try {
unbindService(serviceConnection)
Log.d("MainActivity", "Unbound service")
} catch (e: Exception) {
Log.e("MainActivity", "Error while unbinding service: $e")
}
try {
unregisterReceiver(connectionStatusReceiver)
Log.d("MainActivity", "Unregistered receiver")
} catch (e: Exception) {
Log.e("MainActivity", "Error while unregistering receiver: $e")
}
sendBroadcast(Intent(AirPodsNotifications.DISCONNECT_RECEIVERS))
super.onDestroy()
}
override fun onStop() {
try {
unbindService(serviceConnection)
Log.d("MainActivity", "Unbound service")
} catch (e: Exception) {
Log.e("MainActivity", "Error while unbinding service: $e")
}
try {
unregisterReceiver(connectionStatusReceiver)
Log.d("MainActivity", "Unregistered receiver")
} catch (e: Exception) {
Log.e("MainActivity", "Error while unregistering receiver: $e")
}
super.onStop()
}
}
@SuppressLint("MissingPermission", "InlinedApi", "UnspecifiedRegisterReceiverFlag")
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun Main() {
val isConnected = remember { mutableStateOf(false) }
val permissionState = rememberMultiplePermissionsState(
permissions = listOf(
"android.permission.BLUETOOTH_CONNECT",
"android.permission.POST_NOTIFICATIONS"
)
)
val airPodsService = remember { mutableStateOf(null) }
if (permissionState.allPermissionsGranted) {
val context = LocalContext.current
val navController = rememberNavController()
connectionStatusReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == AirPodsNotifications.AIRPODS_CONNECTED) {
Log.d("MainActivity", "AirPods Connected intent received")
isConnected.value = true
}
else if (intent.action == AirPodsNotifications.AIRPODS_DISCONNECTED) {
Log.d("MainActivity", "AirPods Disconnected intent received")
isConnected.value = false
}
}
}
val filter = IntentFilter().apply {
addAction(AirPodsNotifications.AIRPODS_CONNECTED)
addAction(AirPodsNotifications.AIRPODS_DISCONNECTED)
addAction(AirPodsNotifications.AIRPODS_CONNECTION_DETECTED)
}
Log.d("MainActivity", "Registering Receiver")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
context.registerReceiver(connectionStatusReceiver, filter, RECEIVER_EXPORTED)
} else {
context.registerReceiver(connectionStatusReceiver, filter)
}
Log.d("MainActivity", "Registered Receiver")
NavHost(
navController = navController,
startDestination = "settings",
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("settings") {
if (airPodsService.value != null) {
AirPodsSettingsScreen(
dev = airPodsService.value?.device,
service = airPodsService.value!!,
navController = navController,
isConnected = isConnected.value
)
}
}
composable("debug") {
DebugScreen(navController = navController)
}
composable("long_press/{bud}") { navBackStackEntry ->
LongPress(
navController = navController,
name = navBackStackEntry.arguments?.getString("bud")!!
)
}
}
serviceConnection = remember {
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as AirPodsService.LocalBinder
airPodsService.value = binder.getService()
}
override fun onServiceDisconnected(name: ComponentName?) {
airPodsService.value = null
}
}
}
context.bindService(Intent(context, AirPodsService::class.java), serviceConnection, Context.BIND_AUTO_CREATE)
if (airPodsService.value?.isConnected == true) {
isConnected.value = true
}
} else {
// Permission is not granted, request it
Column (
modifier = Modifier.padding(24.dp),
){
val textToShow = if (permissionState.shouldShowRationale) {
// If the user has denied the permission but not permanently, explain why it's needed.
"Please enable Bluetooth and Notification permissions to use the app. The Nearby Devices is required to connect to your AirPods, and the notification is required to show the AirPods battery status."
} else {
// If the user has permanently denied the permission, inform them to enable it in settings.
"Please enable Bluetooth and Notification permissions in the app settings to use the app."
}
Text(textToShow)
Button(onClick = { permissionState.launchMultiplePermissionRequest() }) {
Text("Request permission")
}
}
}
}