Initial commit: LCK Control Android app

Multi-module Android app (app/shared/sdk) with backend-driven auth,
Quest Platform SDK login, YouTube/Twitch OAuth linking, stream
management via AIDL service. Compose UI with Hilt DI.
This commit is contained in:
2026-02-24 12:03:43 +01:00
commit 82aa207f9a
101 changed files with 4723 additions and 0 deletions

23
sdk/build.gradle.kts Normal file
View File

@@ -0,0 +1,23 @@
plugins {
alias(libs.plugins.android.library)
}
android {
namespace = "com.omixlab.lckcontrol.sdk"
compileSdk = 36
defaultConfig {
minSdk = 33
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
api(project(":shared"))
implementation(libs.androidx.core.ktx)
implementation(libs.kotlinx.coroutines.android)
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -0,0 +1,122 @@
package com.omixlab.lckcontrol.sdk
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import com.omixlab.lckcontrol.shared.ILckControlCallback
import com.omixlab.lckcontrol.shared.ILckControlService
import com.omixlab.lckcontrol.shared.LinkedAccount
import com.omixlab.lckcontrol.shared.StreamPlan
import com.omixlab.lckcontrol.shared.StreamPlanConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class LckControlClient(private val context: Context) {
companion object {
private const val SERVICE_PACKAGE = "com.omixlab.lckcontrol"
private const val SERVICE_CLASS = "$SERVICE_PACKAGE.service.LckControlService"
private const val PERMISSION = "$SERVICE_PACKAGE.permission.USE_LCK_CONTROL"
}
private var service: ILckControlService? = null
private var clientId: String? = null
private val _connected = MutableStateFlow(false)
val connected: StateFlow<Boolean> = _connected.asStateFlow()
private val _streamPlans = MutableStateFlow<List<StreamPlan>>(emptyList())
val streamPlans: StateFlow<List<StreamPlan>> = _streamPlans.asStateFlow()
private val callback = object : ILckControlCallback.Stub() {
override fun onStreamPlansChanged(plans: List<StreamPlan>) {
_streamPlans.value = plans
}
override fun onStreamPlanUpdated(plan: StreamPlan) {
_streamPlans.value = _streamPlans.value.map {
if (it.planId == plan.planId) plan else it
}
}
override fun onClientRegistered(id: String) {}
override fun onClientUnregistered(id: String) {}
}
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
service = ILckControlService.Stub.asInterface(binder)
service?.registerCallback(callback)
_connected.value = true
}
override fun onServiceDisconnected(name: ComponentName?) {
service = null
clientId = null
_connected.value = false
}
}
fun bind(): Boolean {
val intent = Intent().apply {
component = ComponentName(SERVICE_PACKAGE, SERVICE_CLASS)
}
return context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
fun unbind() {
service?.let { svc ->
clientId?.let { svc.unregisterClient(it) }
svc.unregisterCallback(callback)
}
try {
context.unbindService(connection)
} catch (_: IllegalArgumentException) {
// Already unbound
}
service = null
clientId = null
_connected.value = false
}
fun registerAsClient(clientName: String, packageName: String): String? {
val id = service?.registerClient(clientName, packageName)
clientId = id
return id
}
fun getLinkedAccounts(): List<LinkedAccount> {
return service?.linkedAccounts ?: emptyList()
}
fun getStreamPlans(): List<StreamPlan> {
return service?.streamPlans ?: emptyList()
}
fun getStreamPlan(planId: String): StreamPlan? {
return service?.getStreamPlan(planId)
}
fun createStreamPlan(config: StreamPlanConfig): StreamPlan? {
return service?.createStreamPlan(config)
}
fun prepareStreamPlan(planId: String): StreamPlan? {
return service?.prepareStreamPlan(planId)
}
fun startStreamPlan(planId: String): Boolean {
return service?.startStreamPlan(planId) ?: false
}
fun endStreamPlan(planId: String): Boolean {
return service?.endStreamPlan(planId) ?: false
}
fun setClientActivePlan(planId: String) {
clientId?.let { service?.setClientActivePlan(it, planId) }
}
}