tawasuyu

sergio/humanoid

HTML 38Markdown 1TOML 10 Definiciones

⎇ 608145b4b469bb0f8b58179bb8338053430cbdb6 launcher/src/main/java/com/rizoma/orbita/ui/OrbitaViewModel.kt 153 líneas · 5.4 KB Historia crudo

Órbita: quitar apps del átomo (sin desinstalar) + fondo según hora y clima 608145b4b · sergio ·

1package com.rizoma.orbita.ui
2
3import android.app.Application
4import android.content.BroadcastReceiver
5import android.content.ComponentName
6import android.content.Context
7import android.content.Intent
8import android.content.IntentFilter
9import android.net.Uri
10import android.provider.Settings
11import androidx.lifecycle.AndroidViewModel
12import androidx.lifecycle.viewModelScope
13import com.rizoma.orbita.data.AppRepository
14import com.rizoma.orbita.data.SkyRepository
15import com.rizoma.orbita.data.UsageStore
16import com.rizoma.orbita.data.Weather
17import com.rizoma.orbita.model.AppNode
18import kotlinx.coroutines.Dispatchers
19import kotlinx.coroutines.delay
20import kotlinx.coroutines.flow.MutableStateFlow
21import kotlinx.coroutines.flow.SharingStarted
22import kotlinx.coroutines.flow.StateFlow
23import kotlinx.coroutines.flow.combine
24import kotlinx.coroutines.flow.flow
25import kotlinx.coroutines.flow.stateIn
26import kotlinx.coroutines.launch
27import java.util.Calendar
28
29class OrbitaViewModel(app: Application) : AndroidViewModel(app) {
30
31    private val repo = AppRepository(app)
32    private val usage = UsageStore(app)
33
34    private val rawApps = MutableStateFlow<List<AppNode>>(emptyList())
35
36    /** Nodos ya ponderados y ordenados: fijados primero, luego por calor, luego alfabético. */
37    val nodes: StateFlow<List<AppNode>> = combine(
38        rawApps,
39        usage.counts(),
40        usage.pinned(),
41        usage.penalties(),
42        usage.hidden(),
43    ) { apps, counts, pinned, penalties, hidden ->
44        apps.map { node ->
45            node.copy(
46                score = (counts[node.key] ?: 0).toFloat(),
47                pinned = node.key in pinned,
48                hidden = node.key in hidden,
49            )
50        }.sortedWith(
51            // El orden define la órbita: fijados al centro, luego por calor efectivo
52            // (calor menos penalización por "alejar"), luego alfabético.
53            compareByDescending<AppNode> { it.pinned }
54                .thenByDescending { it.score - (penalties[it.key] ?: 0).toFloat() }
55                .thenBy { it.label.lowercase() }
56        )
57    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
58
59    // --- Cielo del fondo: hora del día + clima local. ---
60
61    private val weather = MutableStateFlow<Weather?>(null)
62
63    /** Marca la hora actual (con fracción) y se reemite cada minuto. */
64    private val clock = flow {
65        while (true) {
66            emit(currentHourFraction())
67            delay(60_000L)
68        }
69    }
70
71    val sky: StateFlow<SkyPalette> = combine(clock, weather) { hour, w ->
72        skyPaletteFor(hour, w)
73    }.stateIn(
74        viewModelScope,
75        SharingStarted.WhileSubscribed(5_000),
76        skyPaletteFor(currentHourFraction(), null),
77    )
78
79    /** Refresca el clima local (best-effort). Llamar tras conceder ubicación. */
80    fun refreshWeather() {
81        viewModelScope.launch(Dispatchers.IO) {
82            SkyRepository(getApplication()).fetch()?.let { weather.value = it }
83        }
84    }
85
86    private fun currentHourFraction(): Float {
87        val c = Calendar.getInstance()
88        return c.get(Calendar.HOUR_OF_DAY) + c.get(Calendar.MINUTE) / 60f
89    }
90
91    private val pkgReceiver = object : BroadcastReceiver() {
92        override fun onReceive(context: Context?, intent: Intent?) = refresh()
93    }
94
95    init {
96        refresh()
97        val filter = IntentFilter().apply {
98            addAction(Intent.ACTION_PACKAGE_ADDED)
99            addAction(Intent.ACTION_PACKAGE_REMOVED)
100            addAction(Intent.ACTION_PACKAGE_CHANGED)
101            addDataScheme("package")
102        }
103        getApplication<Application>().registerReceiver(pkgReceiver, filter)
104    }
105
106    fun refresh() {
107        viewModelScope.launch(Dispatchers.IO) {
108            rawApps.value = repo.loadApps()
109        }
110    }
111
112    fun launch(node: AppNode) {
113        val intent = Intent(Intent.ACTION_MAIN)
114            .addCategory(Intent.CATEGORY_LAUNCHER)
115            .setComponent(ComponentName(node.packageName, node.activityName))
116            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
117        runCatching { getApplication<Application>().startActivity(intent) }
118        viewModelScope.launch { usage.record(node.key) }
119    }
120
121    fun togglePin(node: AppNode) {
122        viewModelScope.launch { usage.setPinned(node.key, !node.pinned) }
123    }
124
125    /** Retira el nodo del átomo (o lo devuelve). No desinstala: la app sigue ahí. */
126    fun toggleHidden(node: AppNode) {
127        viewModelScope.launch { usage.setHidden(node.key, !node.hidden) }
128    }
129
130    /** Aleja el nodo a órbitas externas (usa su calor actual como referencia). */
131    fun pushAway(node: AppNode) {
132        viewModelScope.launch { usage.pushAway(node.key, node.score.toInt()) }
133    }
134
135    fun openInfo(node: AppNode) {
136        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
137            .setData(Uri.fromParts("package", node.packageName, null))
138            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
139        runCatching { getApplication<Application>().startActivity(intent) }
140    }
141
142    fun uninstall(node: AppNode) {
143        @Suppress("DEPRECATION")
144        val intent = Intent(Intent.ACTION_DELETE)
145            .setData(Uri.fromParts("package", node.packageName, null))
146            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
147        runCatching { getApplication<Application>().startActivity(intent) }
148    }
149
150    override fun onCleared() {
151        runCatching { getApplication<Application>().unregisterReceiver(pkgReceiver) }
152    }
153}