Skip to content

Commit

Permalink
Initial Android TV support (#260)
Browse files Browse the repository at this point in the history
* [ANDROID_TV] Try to create Android TV UI

* [SHARED] Update libs

* [SHARED] Remove magic numbers

* [SHARED] Change native libs icon to button

* [SHARED] Change selection color

* [SHARED] Fix apps action row

* [SHARED] Update kotlin

* [SHARED] Update xcode project structure, disable iOS arm simulator

* [ANDROID] Update native cpuinfo

* [IOS] Preparations for dependencies cleanup

* [SHARED] Update AGP

* [SHARED] Update navigation

* [IOS] Update cpuinfo lib

* [TV] Add TabRow for info section

* [TV] Handle info TabRow content

* [TV] Remove focusable workaround

* [TV] Add banner

* [TV] Adjust colors

* [TV] Adjust info screens
  • Loading branch information
kamgurgul authored Dec 13, 2024
1 parent 54c27ba commit 1d5265a
Show file tree
Hide file tree
Showing 33 changed files with 1,221 additions and 48 deletions.
26 changes: 25 additions & 1 deletion androidApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,20 @@
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />

<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />

<application
android:name=".CpuInfoApp"
android:allowBackup="true"
android:banner="@mipmap/ic_banner"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
Expand All @@ -37,6 +48,19 @@

</activity>

<activity
android:name="com.kgurgul.cpuinfo.TvActivity"
android:exported="true"
android:theme="@style/Theme.Launcher"
android:windowSoftInputMode="adjustResize">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>

</activity>

<service
android:name=".features.cputile.CpuTileService"
android:exported="true"
Expand All @@ -52,4 +76,4 @@

</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.kgurgul.cpuinfo

import android.os.Build
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kgurgul.cpuinfo.features.HostViewModel
import com.kgurgul.cpuinfo.features.TvHostScreen
import com.kgurgul.cpuinfo.ui.shouldUseDarkTheme
import com.kgurgul.cpuinfo.ui.theme.CpuInfoTheme
import com.kgurgul.cpuinfo.ui.theme.DarkColors
import com.kgurgul.cpuinfo.ui.theme.LightColors
import com.kgurgul.cpuinfo.ui.theme.darkPrimary
import com.kgurgul.cpuinfo.ui.theme.lightPrimary
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel

class TvActivity : AppCompatActivity() {

private val viewModel: HostViewModel by viewModel()

override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)

var uiState: HostViewModel.UiState by mutableStateOf(HostViewModel.UiState())
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiStateFlow
.onEach { uiState = it }
.collect()
}
}
splashScreen.setKeepOnScreenCondition { uiState.isLoading }

enableEdgeToEdge()

setContent {
val darkTheme = shouldUseDarkTheme(uiState)
val systemBarScrim = (if (darkTheme) darkPrimary else lightPrimary).toArgb()
DisposableEffect(darkTheme) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(systemBarScrim),
navigationBarStyle = SystemBarStyle.dark(systemBarScrim),
)
onDispose {}
}
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colors = when {
dynamicColor && darkTheme -> {
val dynamicDarkColors = dynamicDarkColorScheme(LocalContext.current)
DarkColors.copy(
secondary = dynamicDarkColors.primaryContainer,
onSecondary = dynamicDarkColors.onPrimaryContainer,
)
}

dynamicColor && !darkTheme -> {
val dynamicLightColors = dynamicLightColorScheme(LocalContext.current)
LightColors.copy(
secondary = dynamicLightColors.secondary,
onSecondary = dynamicLightColors.onSecondary,
)
}

darkTheme -> DarkColors
else -> LightColors
}
CpuInfoTheme(
useDarkTheme = darkTheme,
colors = colors,
) {
TvHostScreen(
viewModel = viewModel,
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package com.kgurgul.cpuinfo.features

import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.kgurgul.cpuinfo.features.applications.ApplicationsScreen
import com.kgurgul.cpuinfo.features.information.TvInfoContainerScreen
import com.kgurgul.cpuinfo.features.settings.SettingsScreen
import com.kgurgul.cpuinfo.features.temperature.TemperatureScreen
import com.kgurgul.cpuinfo.shared.Res
import com.kgurgul.cpuinfo.shared.hardware
import com.kgurgul.cpuinfo.shared.ic_cpu
import com.kgurgul.cpuinfo.shared.ic_settings
import com.kgurgul.cpuinfo.shared.settings
import com.kgurgul.cpuinfo.ui.components.CpuNavigationSuiteScaffold
import com.kgurgul.cpuinfo.ui.components.CpuNavigationSuiteScaffoldDefault
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel

@Composable
fun TvHostScreen(
viewModel: HostViewModel = koinViewModel(),
) {
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
TvHostScreen(
uiState = uiState,
)
}

@Composable
fun TvHostScreen(
uiState: HostViewModel.UiState,
) {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val itemDefaultColors = CpuNavigationSuiteScaffoldDefault.itemDefaultColors()
CpuNavigationSuiteScaffold(
navigationSuiteItems = {
TvHostNavigationItem.bottomNavigationItems(
isApplicationsVisible = uiState.isApplicationSectionVisible,
).forEach { item ->
item(
icon = {
Icon(
painter = painterResource(item.icon),
contentDescription = stringResource(item.label),
)
},
label = {
Text(
text = stringResource(item.label),
style = MaterialTheme.typography.labelSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
selected = currentDestination?.hierarchy
?.any { it.route == item.route } == true,
onClick = {
navController.navigate(item.route) {
navController.graph.findStartDestination().route?.let {
popUpTo(it) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
},
colors = itemDefaultColors,
)
}
},
) {
NavHost(
navController = navController,
startDestination = TvHostScreen.Information.route,
) {
composable(
route = TvHostScreen.Information.route,
enterTransition = { fadeIn() },
exitTransition = { fadeOut() },
popEnterTransition = { fadeIn() },
popExitTransition = { fadeOut() },
) { TvInfoContainerScreen() }
composable(
route = TvHostScreen.Applications.route,
enterTransition = { fadeIn() },
exitTransition = { fadeOut() },
popEnterTransition = { fadeIn() },
popExitTransition = { fadeOut() },
) { ApplicationsScreen() }
composable(
route = TvHostScreen.Temperatures.route,
enterTransition = { fadeIn() },
exitTransition = { fadeOut() },
popEnterTransition = { fadeIn() },
popExitTransition = { fadeOut() },
) { TemperatureScreen() }
composable(
route = TvHostScreen.Settings.route,
enterTransition = { fadeIn() },
exitTransition = { fadeOut() },
popEnterTransition = { fadeIn() },
popExitTransition = { fadeOut() },
) { SettingsScreen() }
}
}
}

sealed class TvHostScreen(val route: String) {
data object Information : TvHostScreen("information_route")
data object Applications : TvHostScreen("applications_route")
data object Temperatures : TvHostScreen("temperatures_route")
data object Settings : TvHostScreen("settings_route")
}

data class TvHostNavigationItem(
val label: StringResource,
val icon: DrawableResource,
val route: String,
) {

companion object {
fun bottomNavigationItems(
isApplicationsVisible: Boolean,
): List<HostNavigationItem> {
return buildList {
add(
HostNavigationItem(
label = Res.string.hardware,
icon = Res.drawable.ic_cpu,
route = HostScreen.Information.route,
),
)
/*if (isApplicationsVisible) {
add(
HostNavigationItem(
label = Res.string.applications,
icon = Res.drawable.ic_android,
route = HostScreen.Applications.route,
),
)
}
add(
HostNavigationItem(
label = Res.string.temp,
icon = Res.drawable.ic_temperature,
route = HostScreen.Temperatures.route,
),
)*/
add(
HostNavigationItem(
label = Res.string.settings,
icon = Res.drawable.ic_settings,
route = HostScreen.Settings.route,
),
)
}
}
}
}
Loading

0 comments on commit 1d5265a

Please sign in to comment.