Files
Ambient-Launcher/app/src/main/java/com/ambient/launcher/LauncherViewModel.kt
T
2026-07-25 14:25:23 -07:00

160 lines
6.3 KiB
Kotlin

package com.ambient.launcher
import android.app.Application
import android.content.ComponentName
import android.content.Intent
import android.content.pm.PackageManager
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
data class LauncherUiState(
val settings: LauncherSettings = LauncherSettings(),
val apps: List<LauncherApp> = emptyList(),
val localImages: List<Any> = emptyList(),
val weather: WeatherNow? = null,
val weatherLoading: Boolean = false,
val weatherError: String? = null
)
class LauncherViewModel(application: Application) : AndroidViewModel(application) {
private val store = SettingsStore(application)
private val weatherRepository = WeatherRepository()
private val _uiState = MutableStateFlow(LauncherUiState())
val uiState: StateFlow<LauncherUiState> = _uiState.asStateFlow()
init {
loadApps()
viewModelScope.launch {
store.settings.collectLatest { settings ->
val old = _uiState.value.settings
_uiState.value = _uiState.value.copy(settings = settings)
if (settings.localFolderUri != old.localFolderUri) loadLocalImages(settings.localFolderUri)
if (
settings.useFahrenheit != old.useFahrenheit ||
_uiState.value.weather == null
) {
refreshWeather()
}
}
}
}
fun updateSettings(transform: (LauncherSettings) -> LauncherSettings) {
val updated = transform(_uiState.value.settings)
_uiState.value = _uiState.value.copy(settings = updated)
viewModelScope.launch { store.save(updated) }
}
fun togglePinned(app: LauncherApp) {
updateSettings { settings ->
val id = app.component.flattenToString()
val next = settings.pinnedComponents.toMutableSet().apply {
if (!add(id)) remove(id)
}
settings.copy(pinnedComponents = next)
}
}
fun setLocalFolder(uri: String) {
updateSettings { it.copy(localFolderUri = uri) }
loadLocalImages(uri)
}
fun refreshWeather() {
val settings = _uiState.value.settings
val lat = settings.latitude.toDoubleOrNull()
val lon = settings.longitude.toDoubleOrNull()
if (lat == null || lon == null || lat !in -90.0..90.0 || lon !in -180.0..180.0) {
_uiState.value = _uiState.value.copy(
weatherLoading = false,
weatherError = "Enter valid latitude and longitude."
)
return
}
viewModelScope.launch {
_uiState.value = _uiState.value.copy(weatherLoading = true, weatherError = null)
runCatching { weatherRepository.load(lat, lon, settings.useFahrenheit) }
.onSuccess {
_uiState.value = _uiState.value.copy(
weather = it,
weatherLoading = false,
weatherError = null
)
}
.onFailure {
_uiState.value = _uiState.value.copy(
weatherLoading = false,
weatherError = "Weather unavailable. Check the connection."
)
}
}
}
fun launch(app: LauncherApp): Boolean {
val intent = Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setComponent(app.component)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
return runCatching {
getApplication<Application>().startActivity(intent)
}.isSuccess
}
private fun loadApps() {
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
val context = getApplication<Application>()
val pm = context.packageManager
val intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
val flags = if (android.os.Build.VERSION.SDK_INT >= 33) {
PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL.toLong())
} else null
val resolved = if (flags != null) {
pm.queryIntentActivities(intent, flags)
} else {
@Suppress("DEPRECATION")
pm.queryIntentActivities(intent, PackageManager.MATCH_ALL)
}
resolved.mapNotNull { info ->
val activity = info.activityInfo ?: return@mapNotNull null
if (activity.packageName == context.packageName) return@mapNotNull null
LauncherApp(
label = info.loadLabel(pm).toString(),
component = ComponentName(activity.packageName, activity.name),
icon = info.loadIcon(pm)
)
}.distinctBy { it.component }.sortedBy { it.label.lowercase() }
}
_uiState.value = _uiState.value.copy(apps = result)
}
}
private fun loadLocalImages(uri: String) {
viewModelScope.launch {
val images = withContext(Dispatchers.IO) {
if (uri.isBlank()) return@withContext emptyList()
val context = getApplication<Application>()
runCatching {
DocumentFile.fromTreeUri(context, android.net.Uri.parse(uri))
?.listFiles()
?.asSequence()
?.filter { it.isFile && it.type?.startsWith("image/") == true }
?.mapNotNull { it.uri }
?.take(250)
?.toList()
?: emptyList()
}.getOrDefault(emptyList())
}
_uiState.value = _uiState.value.copy(localImages = images)
}
}
}