Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ plugins {
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.sqldelight)
alias(libs.plugins.kotlin.serialization)
id("com.google.devtools.ksp")
}

kotlin {
Expand Down Expand Up @@ -60,4 +61,17 @@ dependencies {
implementation(libs.androidx.tv.material)
implementation(libs.coil.compose)
debugImplementation(libs.androidx.compose.ui.tooling)

// ✅ Room with KSP
val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
}

tasks.named("kspDebugKotlin") {
dependsOn("generateDebugDatabaseInterface")
}
tasks.named("kspReleaseKotlin") {
dependsOn("generateReleaseDatabaseInterface")
}
6 changes: 1 addition & 5 deletions app/src/main/kotlin/nl/ndat/tvlauncher/LauncherActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ import nl.ndat.tvlauncher.data.repository.ChannelRepository
import nl.ndat.tvlauncher.data.repository.InputRepository
import nl.ndat.tvlauncher.ui.AppBase
import nl.ndat.tvlauncher.util.DefaultLauncherHelper
import org.koin.android.ext.android.getKoin
import org.koin.android.ext.android.inject
import org.koin.compose.KoinContext

@SuppressLint("RestrictedApi")
val PERMISSION_READ_CHANNELS = TvContractCompat.PERMISSION_READ_TV_LISTINGS
Expand All @@ -42,9 +40,7 @@ class LauncherActivity : ComponentActivity() {
super.onCreate(savedInstanceState)

setContent {
KoinContext(getKoin()) {
AppBase()
}
AppBase()
}

validateDefaultLauncher()
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/kotlin/nl/ndat/tvlauncher/LauncherApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import nl.ndat.tvlauncher.data.repository.InputRepository
import nl.ndat.tvlauncher.data.resolver.AppResolver
import nl.ndat.tvlauncher.data.resolver.ChannelResolver
import nl.ndat.tvlauncher.data.resolver.InputResolver
import nl.ndat.tvlauncher.service.AutoStartService
import nl.ndat.tvlauncher.ui.tab.apps.AppsTabViewModel
import nl.ndat.tvlauncher.ui.tab.home.HomeTabViewModel
import nl.ndat.tvlauncher.util.DefaultLauncherHelper
Expand All @@ -34,6 +35,8 @@ private val launcherModule = module {
single { InputRepository(get(), get(), get()) }
single { InputResolver() }

single { AutoStartService() }

viewModel { HomeTabViewModel(get(), get()) }
viewModel { AppsTabViewModel(get()) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import nl.ndat.tvlauncher.data.repository.AppRepository
import nl.ndat.tvlauncher.service.AutoStartService
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

Expand All @@ -21,20 +22,28 @@ class PackageChangeReceiver : BroadcastReceiver(), KoinComponent {
}

private val appRepository: AppRepository by inject()
private val autoStartService: AutoStartService by inject()

override fun onReceive(context: Context, intent: Intent) {
val pendingIntent = goAsync()

@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch {
try {
val packageName = when {
intent.action in packageActions && intent.data?.scheme == "package" -> intent.data?.schemeSpecificPart
else -> null
}
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED -> {
// 系统启动完成,启动自启动应用
autoStartService.startAutoStartApps(context)
}
in packageActions -> {
val packageName = if (intent.data?.scheme == "package") {
intent.data?.schemeSpecificPart
} else null

if (packageName != null) appRepository.refreshApplication(packageName)
else appRepository.refreshAllApplications()
if (packageName != null) appRepository.refreshApplication(packageName)
else appRepository.refreshAllApplications()
}
}
} finally {
pendingIntent.finish()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,14 @@ class AppRepository(

fun getApps() = database.apps.getAll().executeAsListFlow()
fun getFavoriteApps() = database.apps.getAllFavorites(::App).executeAsListFlow()
fun getAutoStartApps() = database.apps.getAllAutoStartApps(::App).executeAsListFlow()
suspend fun getByPackageName(packageName: String) = withContext(Dispatchers.IO) { database.apps.getByPackageName(packageName).awaitAsOneOrNull() }

suspend fun favorite(id: String) = withContext(Dispatchers.IO) { database.apps.updateFavoriteAdd(id) }
suspend fun unfavorite(id: String) = withContext(Dispatchers.IO) { database.apps.updateFavoriteRemove(id) }
suspend fun updateFavoriteOrder(id: String, order: Int) = withContext(Dispatchers.IO) { database.apps.updateFavoriteOrder(id, order.toLong()) }

suspend fun addAutoStart(id: String) = withContext(Dispatchers.IO) { database.apps.updateAutoStartAdd(id) }
suspend fun removeAutoStart(id: String) = withContext(Dispatchers.IO) { database.apps.updateAutoStartRemove(id) }
suspend fun updateAutoStartOrder(id: String, order: Int) = withContext(Dispatchers.IO) { database.apps.updateAutoStartOrder(id, order.toLong()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,6 @@ class AppResolver {
launchIntentUriLeanback = packageManager.getLeanbackLaunchIntentForPackage(activityInfo.packageName)?.toUri(0),

favoriteOrder = null,
autoStartOrder = null,
)
}
76 changes: 76 additions & 0 deletions app/src/main/kotlin/nl/ndat/tvlauncher/service/AutoStartService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package nl.ndat.tvlauncher.service

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import nl.ndat.tvlauncher.data.repository.AppRepository
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

class AutoStartService : KoinComponent {
companion object {
private const val TAG = "AutoStartService"
private const val STARTUP_DELAY_MS = 3000L // 3秒延迟启动
private const val APP_START_INTERVAL_MS = 1000L // 应用间启动间隔1秒
}

private val appRepository: AppRepository by inject()

@OptIn(DelicateCoroutinesApi::class)
fun startAutoStartApps(context: Context) {
Log.d(TAG, "开始启动自启动应用")

GlobalScope.launch {
try {
// 等待系统完全启动
delay(STARTUP_DELAY_MS)

val autoStartApps = appRepository.getAutoStartApps().collect { apps ->
Log.d(TAG, "找到 ${apps.size} 个自启动应用")

apps.forEachIndexed { index, app ->
try {
Log.d(TAG, "启动应用: ${app.displayName} (${app.packageName})")

val intent = when {
app.launchIntentUriLeanback != null -> {
Intent.parseUri(app.launchIntentUriLeanback, 0)
}
app.launchIntentUriDefault != null -> {
Intent.parseUri(app.launchIntentUriDefault, 0)
}
else -> {
context.packageManager.getLaunchIntentForPackage(app.packageName)
}
}

if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
Log.d(TAG, "成功启动应用: ${app.displayName}")
} else {
Log.w(TAG, "无法启动应用: ${app.displayName} - 没有找到启动Intent")
}

// 应用间启动间隔
if (index < apps.size - 1) {
delay(APP_START_INTERVAL_MS)
}
} catch (e: Exception) {
Log.e(TAG, "启动应用失败: ${app.displayName}", e)
}
}
}
} catch (e: Exception) {
Log.e(TAG, "自启动服务执行失败", e)
}
}
}
}


17 changes: 16 additions & 1 deletion app/src/main/kotlin/nl/ndat/tvlauncher/ui/tab/apps/AppPopup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -18,6 +20,8 @@ import androidx.tv.material3.IconButtonDefaults
fun AppPopup(
isFavorite: Boolean,
onToggleFavorite: (favorite: Boolean) -> Unit,
isAutoStart: Boolean,
onToggleAutoStart: (autoStart: Boolean) -> Unit,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
Expand All @@ -28,7 +32,18 @@ fun AppPopup(
) {
Icon(
imageVector = if (isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
contentDescription = null,
contentDescription = if (isFavorite) "取消收藏" else "添加到收藏",
modifier = Modifier.size(IconButtonDefaults.SmallIconSize)
)
}

IconButton(
modifier = Modifier.size(IconButtonDefaults.SmallButtonSize),
onClick = { onToggleAutoStart(!isAutoStart) }
) {
Icon(
imageVector = if (isAutoStart) Icons.Default.PlayArrow else Icons.Outlined.PlayArrow,
contentDescription = if (isAutoStart) "取消开机自启动" else "设置开机自启动",
modifier = Modifier.size(IconButtonDefaults.SmallIconSize)
)
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/kotlin/nl/ndat/tvlauncher/ui/tab/apps/AppsTab.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ fun AppsTab(
isFavorite = app.favoriteOrder != null,
onToggleFavorite = { favorite ->
viewModel.favoriteApp(app, favorite)
},
isAutoStart = app.autoStartOrder != null,
onToggleAutoStart = { autoStart ->
viewModel.toggleAutoStart(app, autoStart)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,9 @@ class AppsTabViewModel(
if (favorite) appRepository.favorite(app.id)
else appRepository.unfavorite(app.id)
}

fun toggleAutoStart(app: App, autoStart: Boolean) = viewModelScope.launch {
if (autoStart) appRepository.addAutoStart(app.id)
else appRepository.removeAutoStart(app.id)
}
}
22 changes: 19 additions & 3 deletions app/src/main/kotlin/nl/ndat/tvlauncher/ui/tab/home/AppPopup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -22,6 +24,8 @@ fun AppPopup(
isLast: Boolean,
isFavorite: Boolean,
onToggleFavorite: (favorite: Boolean) -> Unit,
isAutoStart: Boolean,
onToggleAutoStart: (autoStart: Boolean) -> Unit,
onMove: (relativePosition: Int) -> Unit,
) {
Row(
Expand All @@ -34,7 +38,7 @@ fun AppPopup(
) {
Icon(
imageVector = Icons.AutoMirrored.Default.KeyboardArrowLeft,
contentDescription = null,
contentDescription = "向左移动",
modifier = Modifier.size(IconButtonDefaults.SmallIconSize)
)
}
Expand All @@ -45,7 +49,18 @@ fun AppPopup(
) {
Icon(
imageVector = if (isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
contentDescription = null,
contentDescription = if (isFavorite) "取消收藏" else "添加到收藏",
modifier = Modifier.size(IconButtonDefaults.SmallIconSize)
)
}

IconButton(
modifier = Modifier.size(IconButtonDefaults.SmallButtonSize),
onClick = { onToggleAutoStart(!isAutoStart) }
) {
Icon(
imageVector = if (isAutoStart) Icons.Default.PlayArrow else Icons.Outlined.PlayArrow,
contentDescription = if (isAutoStart) "取消开机自启动" else "设置开机自启动",
modifier = Modifier.size(IconButtonDefaults.SmallIconSize)
)
}
Expand All @@ -56,7 +71,8 @@ fun AppPopup(
onClick = { onMove(+1) },
) {
Icon(
imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight, contentDescription = null,
imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
contentDescription = "向右移动",
modifier = Modifier.size(IconButtonDefaults.SmallIconSize)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,9 @@ class HomeTabViewModel(
if (app.favoriteOrder == null) appRepository.favorite(app.id)
appRepository.updateFavoriteOrder(app.id, order)
}

fun toggleAutoStart(app: App, autoStart: Boolean) = viewModelScope.launch {
if (autoStart) appRepository.addAutoStart(app.id)
else appRepository.removeAutoStart(app.id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ fun AppCardRow(
onToggleFavorite = { favorite ->
viewModel.favoriteApp(app, favorite)
},
isAutoStart = app.autoStartOrder != null,
onToggleAutoStart = { autoStart ->
viewModel.toggleAutoStart(app, autoStart)
},
onMove = { relativePosition ->
val newIndex = index + relativePosition
viewModel.setFavoriteOrder(app, newIndex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.sp
import androidx.core.os.ConfigurationCompat
Expand All @@ -21,8 +22,9 @@ import java.util.Calendar
@Composable
fun ToolbarClock() {
val context = LocalContext.current
val pattern = remember(context) {
val locale = requireNotNull(ConfigurationCompat.getLocales(context.resources.configuration)[0])
val configuration = LocalConfiguration.current
val pattern = remember(configuration) {
val locale = requireNotNull(ConfigurationCompat.getLocales(configuration)[0])
val is24HourFormat = DateFormat.is24HourFormat(context)
val pattern = when {
is24HourFormat -> "Hm"
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/sqldelight/migrations/1.sqm
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
ALTER TABLE App ADD COLUMN favoriteOrder INTEGER;
CREATE INDEX appFavoriteOrder ON App(favoriteOrder);

ALTER TABLE App ADD COLUMN autoStartOrder INTEGER;
CREATE INDEX appAutoStartOrder ON App(autoStartOrder);
Loading
Loading