Pith - jamb
jamb/app/src/main/java/com/vgmlr/jamb/MainActivity.kt [6.7 kb]
Modified: 20:33:38 112 026 (09 Jul 026)
16 Days Ago
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
}
Updates
Wedge - Android 126.026
Wedge - Linux 124.026
Shim - Android 117.026
Miter - 114.026
Kerf - Android 112.026
Dev
TVShow (227) 'CSA'
TVShow (228) 'APT'
TVProgram (83) 'BXT'
Miter Update(s)
Peen (Messaging)

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)