Compare commits
2 Commits
v1.40.2-ty
...
v1.40.2-ty
| Author | SHA1 | Date | |
|---|---|---|---|
| 61f3424de5 | |||
| 45ac29e8a7 |
17
CLAUDE.md
17
CLAUDE.md
@@ -87,7 +87,7 @@ state in `ui/settings/clockwidget/ClockWidgetSettingsScreenVM.kt`. New strings a
|
||||
|
||||
On the desktop:
|
||||
|
||||
JAVA_HOME=~/jdk21 ANDROID_HOME=~/android-sdk \
|
||||
JAVA_HOME=/usr/lib/jvm/java-21-openjdk ANDROID_HOME=~/Android/Sdk \
|
||||
./gradlew :app:app:assembleDefaultRelease
|
||||
|
||||
Result: `app/app/build/outputs/apk/default/release/app-default-release.apk`, applicationId
|
||||
@@ -99,6 +99,21 @@ in the repository root (git-ignored) or the `KEYSTORE_FILE`, `KEYSTORE_PASSWORD`
|
||||
`~/android-keystores/kvaesitso-release.jks`; the phone has its own copy under
|
||||
`~/android-keystores/` in the Termux home.
|
||||
|
||||
The desktop's checkout (jonas@192.168.1.86) is `~/sources/Kvaesitso`. Its SDK is `~/Android/Sdk`
|
||||
(platforms android-35/37.0, build-tools 34/36/37), so `local.properties` there has
|
||||
`sdk.dir=/home/jonas/Android/Sdk`, and `keystore.properties` points at the copy of the release key
|
||||
in `~/android-keystores/`. It has no `~/jdk21`; it uses Arch's `java-21-openjdk`.
|
||||
|
||||
After the APK is built (on either machine), the release is tagged and published from the phone:
|
||||
|
||||
git tag -a v1.40.2-typing.N -m "Kvaesitso 1.40.2 type-to-search and agenda, Nth build"
|
||||
git push origin main v1.40.2-typing.N
|
||||
|
||||
and a Gitea release for that tag is created with the APK attached as
|
||||
`Kvaesitso-v<versionName>-Signed.apk` (the token is in `~/.config/gitea/token`; the API is
|
||||
`POST /api/v1/repos/jonas/Kvaesitso/releases` and then `.../releases/<id>/assets?name=<asset>` with
|
||||
`-F attachment=@<apk>`).
|
||||
|
||||
### Building on the phone itself (Termux)
|
||||
|
||||
The phone builds this project without a desktop. `scripts/ondevice.sh` wraps the loop:
|
||||
|
||||
@@ -34,8 +34,8 @@ android {
|
||||
applicationId = "de.mm20.launcher2"
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091006
|
||||
versionName = "1.40.2-typing.7"
|
||||
versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091201
|
||||
versionName = "1.40.2-typing.8"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,13 @@ data class WeatherSettingsData(
|
||||
val lastLocation: LatLon? = null,
|
||||
val lastUpdate: Long = 0L,
|
||||
val providerSettings: Map<String, ProviderSettings> = emptyMap(),
|
||||
|
||||
/**
|
||||
* Whether the selected provider resolves its own location (e.g. Breezy Weather, which is
|
||||
* configured with a [WeatherLocation.Managed]). The launcher must then not request a location
|
||||
* for it.
|
||||
*/
|
||||
val managedLocation: Boolean = false,
|
||||
)
|
||||
|
||||
class WeatherSettings internal constructor(
|
||||
@@ -53,6 +60,7 @@ class WeatherSettings internal constructor(
|
||||
lastLocation = it.weatherLastLocation,
|
||||
lastUpdate = it.weatherLastUpdate,
|
||||
providerSettings = it.weatherProviderSettings,
|
||||
managedLocation = it.weatherProviderSettings[it.weatherProvider]?.managedLocation == true,
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
) {
|
||||
|
||||
@@ -12,11 +12,15 @@ import de.mm20.launcher2.search.SearchableRepository
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.timeout
|
||||
@@ -32,7 +36,7 @@ internal class LocationsRepository(
|
||||
private val permissionsManager: PermissionsManager,
|
||||
) : SearchableRepository<Location> {
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
override fun search(
|
||||
query: String,
|
||||
allowNetwork: Boolean
|
||||
@@ -40,50 +44,71 @@ internal class LocationsRepository(
|
||||
if (query.isBlank() || query.length <= 1) {
|
||||
return flowOf(persistentListOf())
|
||||
}
|
||||
return combineTransform(
|
||||
poseProvider
|
||||
.getLocation(minTimeMs = 2000, minDistanceM = 50.0f)
|
||||
// 1st location: lastCachedLocation of poseProvider, if available
|
||||
// 2nd location: LocationManager.getLastKnownLocation(), if available and better than lastCachedLocation
|
||||
// 3rd location: live location from LocationManager.requestLocationUpdates() that is better than any of the previous
|
||||
.take(3)
|
||||
// only request locations for 30 seconds
|
||||
.timeout(30.seconds),
|
||||
return combine(
|
||||
permissionsManager.hasPermission(PermissionGroup.Location),
|
||||
settings.data
|
||||
) { userLocation, hasPermission, settingsData ->
|
||||
emit(persistentListOf())
|
||||
|
||||
if (!hasPermission || settingsData.providers.isEmpty()) {
|
||||
return@combineTransform
|
||||
}
|
||||
|
||||
val providers = settingsData.providers.map {
|
||||
when (it) {
|
||||
"openstreetmaps" -> OsmLocationProvider(context, settings)
|
||||
else -> PluginLocationProvider(context, it)
|
||||
}
|
||||
}
|
||||
|
||||
supervisorScope {
|
||||
val result = MutableStateFlow(persistentListOf<Location>())
|
||||
|
||||
for (provider in providers) {
|
||||
launch {
|
||||
val r = provider.search(
|
||||
query,
|
||||
userLocation,
|
||||
allowNetwork,
|
||||
settingsData.searchRadius,
|
||||
settingsData.hideUncategorized
|
||||
)
|
||||
result.update {
|
||||
(it + r).toPersistentList()
|
||||
}
|
||||
}
|
||||
}
|
||||
emitAll(result)
|
||||
) { hasPermission, settingsData ->
|
||||
!hasPermission || settingsData.providers.isEmpty()
|
||||
}.distinctUntilChanged().flatMapLatest { unusable ->
|
||||
if (unusable) {
|
||||
// Bail out before building the location flow below. As an argument to
|
||||
// combineTransform it is always collected, which registered GPS and network
|
||||
// listeners for every search query even when location search was off or not
|
||||
// permitted.
|
||||
flowOf(persistentListOf())
|
||||
} else {
|
||||
locationSearchFlow(query, allowNetwork)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun locationSearchFlow(
|
||||
query: String,
|
||||
allowNetwork: Boolean,
|
||||
): Flow<ImmutableList<Location>> = combineTransform(
|
||||
poseProvider
|
||||
.getLocation(minTimeMs = 2000, minDistanceM = 50.0f)
|
||||
// 1st location: lastCachedLocation of poseProvider, if available
|
||||
// 2nd location: LocationManager.getLastKnownLocation(), if available and better than lastCachedLocation
|
||||
// 3rd location: live location from LocationManager.requestLocationUpdates() that is better than any of the previous
|
||||
.take(3)
|
||||
// only request locations for 30 seconds
|
||||
.timeout(30.seconds),
|
||||
permissionsManager.hasPermission(PermissionGroup.Location),
|
||||
settings.data
|
||||
) { userLocation, hasPermission, settingsData ->
|
||||
emit(persistentListOf())
|
||||
|
||||
if (!hasPermission || settingsData.providers.isEmpty()) {
|
||||
return@combineTransform
|
||||
}
|
||||
|
||||
val providers = settingsData.providers.map {
|
||||
when (it) {
|
||||
"openstreetmaps" -> OsmLocationProvider(context, settings)
|
||||
else -> PluginLocationProvider(context, it)
|
||||
}
|
||||
}
|
||||
|
||||
supervisorScope {
|
||||
val result = MutableStateFlow(persistentListOf<Location>())
|
||||
|
||||
for (provider in providers) {
|
||||
launch {
|
||||
val r = provider.search(
|
||||
query,
|
||||
userLocation,
|
||||
allowNetwork,
|
||||
settingsData.searchRadius,
|
||||
settingsData.hideUncategorized
|
||||
)
|
||||
result.update {
|
||||
(it + r).toPersistentList()
|
||||
}
|
||||
}
|
||||
}
|
||||
emitAll(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,12 @@ internal class WeatherRepositoryImpl(
|
||||
}
|
||||
scope.launch {
|
||||
settings.collectLatest {
|
||||
if (it.managedLocation) {
|
||||
// The provider resolves its own location and pushes weather updates, so there
|
||||
// is nothing to poll for.
|
||||
WorkManager.getInstance(context).cancelUniqueWork("weather")
|
||||
return@collectLatest
|
||||
}
|
||||
val provider = WeatherProvider.getInstance(it.provider)
|
||||
val weatherRequest =
|
||||
PeriodicWorkRequestBuilder<WeatherUpdateWorker>(Duration.ofMillis(provider.getUpdateInterval()))
|
||||
@@ -212,6 +218,18 @@ class WeatherUpdateWorker(
|
||||
override suspend fun doWork(): Result {
|
||||
Log.d("WeatherUpdateWorker", "Requesting weather data")
|
||||
val settingsData = settings.first()
|
||||
|
||||
// A provider with a managed location resolves its position itself and pushes weather
|
||||
// updates instead of being polled (Breezy Weather does both). There is nothing to pull,
|
||||
// and its getWeatherData() always returns null - which used to make this worker request a
|
||||
// location (holding GPS and network listeners for up to ten minutes at a time) and then
|
||||
// return Result.retry() forever, without ever setting lastUpdate, so the update interval
|
||||
// check below could never short-circuit it either.
|
||||
if (settingsData.managedLocation) {
|
||||
Log.d("WeatherUpdateWorker", "Provider manages its own location, nothing to pull")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val provider = WeatherProvider.getInstance(settingsData.provider)
|
||||
|
||||
val updateInterval = provider.getUpdateInterval()
|
||||
@@ -254,7 +272,10 @@ class WeatherUpdateWorker(
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private suspend fun getLastKnownLocation(): LatLon? = locationProvider.getLocation(skipCache = true)
|
||||
.timeout(10.minutes)
|
||||
// Weather does not need a metre-accurate fix; giving up after two minutes instead of ten
|
||||
// keeps the GPS and network listeners (and the wakelocks they hold) short. The caller
|
||||
// falls back to the last known location.
|
||||
.timeout(2.minutes)
|
||||
.firstOrNull()
|
||||
.or { locationProvider.lastCachedLocation }
|
||||
?.let { LatLon(it.latitude, it.longitude) }
|
||||
|
||||
Reference in New Issue
Block a user