package com.vgmlr.jamb
import android.Manifest
import android.content.ComponentName
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
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.material3.Button
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import android.content.Intent
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.IntentFilter
import android.os.BatteryManager
import android.view.WindowManager
import androidx.compose.runtime.collectAsState
class MainActivity : ComponentActivity() {
private val intentUri = mutableStateOf<String?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!isTaskRoot &&
intent.hasCategory(Intent.CATEGORY_LAUNCHER) &&
Intent.ACTION_MAIN == intent.action
) {
finish()
return
}
intentUri.value = audioUriFrom(intent)
enableEdgeToEdge(
navigationBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT)
)
setContent {
JambTheme {
Surface(modifier = Modifier.fillMaxSize(), color = JambColors.Background) {
JambRoot(
intentUri = intentUri.value,
onIntentConsumed = { intentUri.value = null }
)
}
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
audioUriFrom(intent)?.let { intentUri.value = it }
}
private fun audioUriFrom(intent: Intent?): String? {
if (intent == null || intent.action != Intent.ACTION_VIEW) return null
return intent.data?.toString()
}
}
@Composable
fun JambRoot(
vm: JambViewModel = viewModel(),
intentUri: String? = null,
onIntentConsumed: () -> Unit = {}
) {
val context = LocalContext.current
val audioPerm = if (Build.VERSION.SDK_INT >= 33)
Manifest.permission.READ_MEDIA_AUDIO
else
Manifest.permission.READ_EXTERNAL_STORAGE
var granted by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(context, audioPerm) ==
PackageManager.PERMISSION_GRANTED
)
}
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
granted = result[audioPerm] ?: granted
vm.setPermission(granted)
}
LaunchedEffect(Unit) {
if (granted) {
vm.setPermission(true)
} else {
val perms = buildList {
add(audioPerm)
if (Build.VERSION.SDK_INT >= 33) add(Manifest.permission.POST_NOTIFICATIONS)
}
launcher.launch(perms.toTypedArray())
}
}
DisposableEffect(Unit) {
val token = SessionToken(context, ComponentName(context, JambPlayback::class.java))
val future = MediaController.Builder(context, token).buildAsync()
future.addListener(
{ runCatching { vm.setController(future.get()) } },
ContextCompat.getMainExecutor(context)
)
onDispose {
MediaController.releaseFuture(future)
vm.clearController()
}
}
LaunchedEffect(intentUri) {
intentUri?.let {
vm.setPendingUri(it)
onIntentConsumed()
}
}
KeepAwakeController()
if (granted) {
PlayerScreen(vm)
} else {
PermissionScreen {
launcher.launch(arrayOf(audioPerm))
}
}
}
@Composable
private fun PermissionScreen(onRequest: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize().padding(32.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Jamb needs access to your audio files.", color = JambColors.Title)
Button(onClick = onRequest, modifier = Modifier.padding(top = 16.dp)) {
Text("Grant access")
}
}
}
@Composable
private fun KeepAwakeController() {
val context = LocalContext.current
val activity = context as? Activity ?: return
val prefs = remember { JambPrefs(context) }
val mode by prefs.keepAwake.collectAsState(initial = JambPrefs.AWAKE_NONE)
var charging by remember { mutableStateOf(isCharging(context)) }
DisposableEffect(Unit) {
val filter = IntentFilter().apply {
addAction(Intent.ACTION_POWER_CONNECTED)
addAction(Intent.ACTION_POWER_DISCONNECTED)
}
val receiver = object : BroadcastReceiver() {
override fun onReceive(c: Context?, i: Intent?) { charging = isCharging(context) }
}
charging = isCharging(context)
ContextCompat.registerReceiver(
context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED
)
onDispose { context.unregisterReceiver(receiver) }
}
LaunchedEffect(mode, charging) {
val on = when (mode) {
JambPrefs.AWAKE_ALWAYS -> true
JambPrefs.AWAKE_CHARGING -> charging
else -> false
}
if (on) activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
else activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
private fun isCharging(context: Context): Boolean {
val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
return bm.isCharging
}