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:
@@ -0,0 +1,203 @@
|
||||
package com.omixlab.lckcontrol.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteCallbackList
|
||||
import com.omixlab.lckcontrol.R
|
||||
import com.omixlab.lckcontrol.data.repository.AccountRepository
|
||||
import com.omixlab.lckcontrol.data.repository.StreamPlanRepository
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LckControlService : Service() {
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "lck_control_service"
|
||||
private const val NOTIFICATION_ID = 1
|
||||
}
|
||||
|
||||
@Inject lateinit var accountRepository: AccountRepository
|
||||
@Inject lateinit var streamPlanRepository: StreamPlanRepository
|
||||
|
||||
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
private val clientTracker = ClientTracker()
|
||||
private val callbacks = RemoteCallbackList<ILckControlCallback>()
|
||||
|
||||
private val binder = object : ILckControlService.Stub() {
|
||||
|
||||
override fun getLinkedAccounts(): List<LinkedAccount> = runBlocking {
|
||||
accountRepository.getAccounts()
|
||||
}
|
||||
|
||||
override fun createStreamPlan(config: StreamPlanConfig): StreamPlan = runBlocking {
|
||||
val plan = streamPlanRepository.createPlan(config.name, config.destinations)
|
||||
broadcastPlansChanged()
|
||||
plan
|
||||
}
|
||||
|
||||
override fun prepareStreamPlan(planId: String): StreamPlan = runBlocking {
|
||||
streamPlanRepository.preparePlan(planId)
|
||||
val plan = streamPlanRepository.getPlan(planId) ?: error("Plan not found after prepare")
|
||||
broadcastPlanUpdated(plan)
|
||||
plan
|
||||
}
|
||||
|
||||
override fun getStreamPlans(): List<StreamPlan> = runBlocking {
|
||||
streamPlanRepository.getPlans()
|
||||
}
|
||||
|
||||
override fun getStreamPlan(planId: String): StreamPlan? = runBlocking {
|
||||
streamPlanRepository.getPlan(planId)
|
||||
}
|
||||
|
||||
override fun startStreamPlan(planId: String): Boolean = runBlocking {
|
||||
val plan = streamPlanRepository.getPlan(planId) ?: return@runBlocking false
|
||||
if (plan.status != "READY") return@runBlocking false
|
||||
streamPlanRepository.startPlan(planId)
|
||||
val updated = streamPlanRepository.getPlan(planId)
|
||||
if (updated != null) broadcastPlanUpdated(updated)
|
||||
true
|
||||
}
|
||||
|
||||
override fun endStreamPlan(planId: String): Boolean = runBlocking {
|
||||
val plan = streamPlanRepository.getPlan(planId) ?: return@runBlocking false
|
||||
if (plan.status != "LIVE") return@runBlocking false
|
||||
streamPlanRepository.endPlan(planId)
|
||||
val updated = streamPlanRepository.getPlan(planId)
|
||||
if (updated != null) broadcastPlanUpdated(updated)
|
||||
true
|
||||
}
|
||||
|
||||
override fun registerClient(clientName: String, packageName: String): String {
|
||||
val clientId = clientTracker.register(clientName, packageName)
|
||||
broadcastClientRegistered(clientId)
|
||||
return clientId
|
||||
}
|
||||
|
||||
override fun unregisterClient(clientId: String) {
|
||||
clientTracker.unregister(clientId)
|
||||
broadcastClientUnregistered(clientId)
|
||||
}
|
||||
|
||||
override fun setClientActivePlan(clientId: String, planId: String) {
|
||||
clientTracker.setActivePlan(clientId, planId)
|
||||
}
|
||||
|
||||
override fun registerCallback(callback: ILckControlCallback) {
|
||||
callbacks.register(callback)
|
||||
}
|
||||
|
||||
override fun unregisterCallback(callback: ILckControlCallback) {
|
||||
callbacks.unregister(callback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
buildNotification(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder = binder
|
||||
|
||||
override fun onDestroy() {
|
||||
serviceScope.cancel()
|
||||
callbacks.kill()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"LCK Control Service",
|
||||
NotificationManager.IMPORTANCE_LOW,
|
||||
).apply {
|
||||
description = "Manages connected game clients and stream plans"
|
||||
}
|
||||
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun buildNotification(): Notification =
|
||||
Notification.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("LCK Control")
|
||||
.setContentText("Managing stream connections")
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
|
||||
private fun broadcastPlansChanged() {
|
||||
serviceScope.launch {
|
||||
val plans = streamPlanRepository.getPlans()
|
||||
val count = callbacks.beginBroadcast()
|
||||
try {
|
||||
for (i in 0 until count) {
|
||||
try {
|
||||
callbacks.getBroadcastItem(i).onStreamPlansChanged(plans)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
} finally {
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastPlanUpdated(plan: StreamPlan) {
|
||||
val count = callbacks.beginBroadcast()
|
||||
try {
|
||||
for (i in 0 until count) {
|
||||
try {
|
||||
callbacks.getBroadcastItem(i).onStreamPlanUpdated(plan)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
} finally {
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastClientRegistered(clientId: String) {
|
||||
val count = callbacks.beginBroadcast()
|
||||
try {
|
||||
for (i in 0 until count) {
|
||||
try {
|
||||
callbacks.getBroadcastItem(i).onClientRegistered(clientId)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
} finally {
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcastClientUnregistered(clientId: String) {
|
||||
val count = callbacks.beginBroadcast()
|
||||
try {
|
||||
for (i in 0 until count) {
|
||||
try {
|
||||
callbacks.getBroadcastItem(i).onClientUnregistered(clientId)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
} finally {
|
||||
callbacks.finishBroadcast()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user