Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44d7e81801 | |||
| 553d1802c7 |
24
CLAUDE.md
24
CLAUDE.md
@@ -38,7 +38,7 @@ export JAVA_HOME=~/jdk21 # Gradle 8.13, compileSdk/targetSdk 36, minSdk 2
|
|||||||
## Release flow (see git history for the v0.3.x pattern)
|
## Release flow (see git history for the v0.3.x pattern)
|
||||||
|
|
||||||
1. Bump `versionCode`/`versionName` in `app/build.gradle.kts`
|
1. Bump `versionCode`/`versionName` in `app/build.gradle.kts`
|
||||||
(currently 35 / 0.3.5).
|
(currently 38 / 0.3.8).
|
||||||
2. Build both flavors, then sign each:
|
2. Build both flavors, then sign each:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -46,9 +46,9 @@ PASS=$(cat ~/android-keystores/easylauncher-release.pass)
|
|||||||
~/android-sdk/build-tools/36.0.0/apksigner sign --v4-signing-enabled true \
|
~/android-sdk/build-tools/36.0.0/apksigner sign --v4-signing-enabled true \
|
||||||
--ks ~/android-keystores/easylauncher-release.jks --ks-key-alias easylauncher \
|
--ks ~/android-keystores/easylauncher-release.jks --ks-key-alias easylauncher \
|
||||||
--ks-pass "pass:$PASS" --key-pass "pass:$PASS" \
|
--ks-pass "pass:$PASS" --key-pass "pass:$PASS" \
|
||||||
--out dist/EasyLauncher-Internet-v0.3.5-Signed.apk \
|
--out dist/EasyLauncher-Internet-v0.3.8-Signed.apk \
|
||||||
app/build/outputs/apk/withInternet/release/app.easy.launcher_v0.3.5-Release.apk
|
app/build/outputs/apk/withInternet/release/app.easy.launcher_v0.3.7-Release.apk
|
||||||
# repeat for withoutInternet -> dist/EasyLauncher-v0.3.5-Signed.apk
|
# repeat for withoutInternet -> dist/EasyLauncher-v0.3.8-Signed.apk
|
||||||
```
|
```
|
||||||
|
|
||||||
3. `apksigner verify --print-certs` should show the keystore SHA-256.
|
3. `apksigner verify --print-certs` should show the keystore SHA-256.
|
||||||
@@ -124,11 +124,17 @@ Prefs live in `/data/data/app.easy.launcher/shared_prefs/`. To change them:
|
|||||||
|
|
||||||
### Home current-weather element (MET/Yr)
|
### Home current-weather element (MET/Yr)
|
||||||
|
|
||||||
- Source: `https://api.met.no/weatherapi/locationforecast/2.0/compact` — **no API
|
- Fetched via the **shared `met-weather` library** (`dk.haugesenspil:met-weather`
|
||||||
key**, but REQUIRES a descriptive `User-Agent` header (see `MetApiService`).
|
from the Gitea maven registry, source in `~/sources/met-weather`):
|
||||||
Verified working from PC and phone network.
|
`MetWeatherClient(`met-weather 0.1.0`)` hits
|
||||||
- `AppHelper.fetchMetWeather(context, lat, lon)` returns Celsius (rounded) +
|
`https://api.met.no/weatherapi/locationforecast/2.0/compact` — **no API
|
||||||
MET `symbol_code`; 15-minute cache in `met_weather_prefs`.
|
key**, but REQUIRES a descriptive `User-Agent` header (passed in
|
||||||
|
`AppHelper.MET_USER_AGENT`). Same library feeds `met-weather-provider` and
|
||||||
|
therefore the GadgetBridge/Skagen watch.
|
||||||
|
- `AppHelper.fetchMetWeather(context, lat, lon)` (suspend) returns Celsius
|
||||||
|
(rounded) + MET `symbol_code` via `MetWeatherClient`; 15-minute cache in
|
||||||
|
`met_weather_prefs`. Bump `dk.haugesenspil:met-weather` version here when the
|
||||||
|
library is updated.
|
||||||
- The icon is a **Nerd Font weather glyph** (`nf-weather-*`, U+E300+ PUA),
|
- The icon is a **Nerd Font weather glyph** (`nf-weather-*`, U+E300+ PUA),
|
||||||
mapped from the symbol code in `MetSymbolMapper`. It renders with the bundled
|
mapped from the symbol code in `MetSymbolMapper`. It renders with the bundled
|
||||||
`R.font.jetbrains_mono_nf_weather` — a ~95KB fontTools subset of
|
`R.font.jetbrains_mono_nf_weather` — a ~95KB fontTools subset of
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ android {
|
|||||||
applicationId = "app.easy.launcher"
|
applicationId = "app.easy.launcher"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 37
|
versionCode = 38
|
||||||
versionName = "0.3.7"
|
versionName = "0.3.8"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
manifestPlaceholders["internetPermission"] = "android.permission.INTERNET"
|
manifestPlaceholders["internetPermission"] = "android.permission.INTERNET"
|
||||||
|
|||||||
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@@ -26,5 +26,8 @@
|
|||||||
-keep class com.github.droidworksstudio.launcher.helper.weather.Main { *; }
|
-keep class com.github.droidworksstudio.launcher.helper.weather.Main { *; }
|
||||||
-keep class com.github.droidworksstudio.launcher.helper.weather.Weather { *; }
|
-keep class com.github.droidworksstudio.launcher.helper.weather.Weather { *; }
|
||||||
|
|
||||||
|
# Keep the shared met-weather library classes (Gson DTOs must keep field names)
|
||||||
|
-keep class dk.haugesenspil.metweather.** { *; }
|
||||||
|
|
||||||
# Keep all model classes and their fields that Gson might use for serialization/deserialization
|
# Keep all model classes and their fields that Gson might use for serialization/deserialization
|
||||||
-keep class com.github.droidworksstudio.launcher.helper.weather.** { *; }
|
-keep class com.github.droidworksstudio.launcher.helper.weather.** { *; }
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class AppHelper @Inject constructor() {
|
|||||||
private companion object {
|
private companion object {
|
||||||
/** MET requires a descriptive User-Agent identifying the app + contact. */
|
/** MET requires a descriptive User-Agent identifying the app + contact. */
|
||||||
const val MET_USER_AGENT = "app.easy.launcher (https://gitea.haugesenspil.dk/jonas/EasyLauncher)"
|
const val MET_USER_AGENT = "app.easy.launcher (https://gitea.haugesenspil.dk/jonas/EasyLauncher)"
|
||||||
|
private const val FOUR_HOURS_MILLIS = 4L * 60 * 60 * 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("WrongConstant", "PrivateApi")
|
@SuppressLint("WrongConstant", "PrivateApi")
|
||||||
@@ -562,7 +563,13 @@ class AppHelper @Inject constructor() {
|
|||||||
data class CachedWeatherData(val timestamp: Long, val weatherResponse: WeatherResponse)
|
data class CachedWeatherData(val timestamp: Long, val weatherResponse: WeatherResponse)
|
||||||
|
|
||||||
sealed class MetWeatherResult {
|
sealed class MetWeatherResult {
|
||||||
data class Success(val temperature: Int, val symbolCode: String) : MetWeatherResult()
|
data class Success(
|
||||||
|
val temperature: Int,
|
||||||
|
val symbolCode: String,
|
||||||
|
val temperature4h: Int? = null,
|
||||||
|
val symbol4h: String? = null,
|
||||||
|
) : MetWeatherResult()
|
||||||
|
|
||||||
data class Failure(val errorMessage: String) : MetWeatherResult()
|
data class Failure(val errorMessage: String) : MetWeatherResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,9 +599,15 @@ class AppHelper @Inject constructor() {
|
|||||||
return try {
|
return try {
|
||||||
val weather = MetWeatherClient(MET_USER_AGENT)
|
val weather = MetWeatherClient(MET_USER_AGENT)
|
||||||
.fetch(latitude.toDouble(), longitude.toDouble())
|
.fetch(latitude.toDouble(), longitude.toDouble())
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val plus4 = weather.hourly.minByOrNull {
|
||||||
|
kotlin.math.abs(it.epochMillis - (now + FOUR_HOURS_MILLIS))
|
||||||
|
}
|
||||||
val result = MetWeatherResult.Success(
|
val result = MetWeatherResult.Success(
|
||||||
weather.temperatureC.roundToInt(),
|
temperature = weather.temperatureC.roundToInt(),
|
||||||
weather.symbolCode,
|
symbolCode = weather.symbolCode,
|
||||||
|
temperature4h = plus4?.temperatureC?.roundToInt(),
|
||||||
|
symbol4h = plus4?.symbolCode,
|
||||||
)
|
)
|
||||||
context.cacheMetWeather(result)
|
context.cacheMetWeather(result)
|
||||||
result
|
result
|
||||||
@@ -605,11 +618,21 @@ class AppHelper @Inject constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the home-screen weather text: nerd-font weather glyph followed
|
* Builds the home-screen weather text: the current nerd-font weather glyph
|
||||||
* by the temperature in Celsius, e.g. "\uE312 16°".
|
* and Celsius value, plus the reading ~4 hours out when available, e.g.
|
||||||
|
* "\uE302 16° → \uE31D 18°".
|
||||||
*/
|
*/
|
||||||
fun buildCurrentWeatherText(temperature: Int, symbolCode: String): String {
|
fun buildCurrentWeatherText(
|
||||||
return "${MetSymbolMapper.glyphFor(symbolCode)} $temperature°"
|
temperature: Int,
|
||||||
|
symbolCode: String,
|
||||||
|
temperature4h: Int? = null,
|
||||||
|
symbol4h: String? = null,
|
||||||
|
): String {
|
||||||
|
val current = "${MetSymbolMapper.glyphFor(symbolCode)} $temperature°"
|
||||||
|
if (temperature4h != null && symbol4h != null) {
|
||||||
|
return "$current → ${MetSymbolMapper.glyphFor(symbol4h)} $temperature4h°"
|
||||||
|
}
|
||||||
|
return current
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Context.cacheMetWeather(result: MetWeatherResult.Success) {
|
private fun Context.cacheMetWeather(result: MetWeatherResult.Success) {
|
||||||
@@ -618,6 +641,8 @@ class AppHelper @Inject constructor() {
|
|||||||
.putLong(Constants.MET_WEATHER_TIMESTAMP, System.currentTimeMillis())
|
.putLong(Constants.MET_WEATHER_TIMESTAMP, System.currentTimeMillis())
|
||||||
.putInt(Constants.MET_WEATHER_TEMPERATURE, result.temperature)
|
.putInt(Constants.MET_WEATHER_TEMPERATURE, result.temperature)
|
||||||
.putString(Constants.MET_WEATHER_SYMBOL, result.symbolCode)
|
.putString(Constants.MET_WEATHER_SYMBOL, result.symbolCode)
|
||||||
|
.putInt(Constants.MET_WEATHER_TEMPERATURE_4H, result.temperature4h ?: Int.MIN_VALUE)
|
||||||
|
.putString(Constants.MET_WEATHER_SYMBOL_4H, result.symbol4h ?: "")
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,6 +652,14 @@ class AppHelper @Inject constructor() {
|
|||||||
if (timestamp == -1L) return null
|
if (timestamp == -1L) return null
|
||||||
val temperature = sharedPreferences.getInt(Constants.MET_WEATHER_TEMPERATURE, 0)
|
val temperature = sharedPreferences.getInt(Constants.MET_WEATHER_TEMPERATURE, 0)
|
||||||
val symbol = sharedPreferences.getString(Constants.MET_WEATHER_SYMBOL, "cloudy") ?: "cloudy"
|
val symbol = sharedPreferences.getString(Constants.MET_WEATHER_SYMBOL, "cloudy") ?: "cloudy"
|
||||||
return timestamp to MetWeatherResult.Success(temperature, symbol)
|
val temp4h = sharedPreferences.getInt(Constants.MET_WEATHER_TEMPERATURE_4H, Int.MIN_VALUE)
|
||||||
|
val symbol4h = sharedPreferences.getString(Constants.MET_WEATHER_SYMBOL_4H, "") ?: ""
|
||||||
|
val result = MetWeatherResult.Success(
|
||||||
|
temperature = temperature,
|
||||||
|
symbolCode = symbol,
|
||||||
|
temperature4h = temp4h.takeIf { it != Int.MIN_VALUE },
|
||||||
|
symbol4h = symbol4h.takeIf { it.isNotEmpty() },
|
||||||
|
)
|
||||||
|
return timestamp to result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -393,7 +393,9 @@ class HomeFragment : Fragment(),
|
|||||||
is AppHelper.MetWeatherResult.Success -> {
|
is AppHelper.MetWeatherResult.Success -> {
|
||||||
binding.currentWeather.text = appHelper.buildCurrentWeatherText(
|
binding.currentWeather.text = appHelper.buildCurrentWeatherText(
|
||||||
result.temperature,
|
result.temperature,
|
||||||
result.symbolCode
|
result.symbolCode,
|
||||||
|
result.temperature4h,
|
||||||
|
result.symbol4h,
|
||||||
)
|
)
|
||||||
binding.currentWeather.visibility = View.VISIBLE
|
binding.currentWeather.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ object Constants {
|
|||||||
const val MET_WEATHER_TIMESTAMP = "MET_WEATHER_TIMESTAMP"
|
const val MET_WEATHER_TIMESTAMP = "MET_WEATHER_TIMESTAMP"
|
||||||
const val MET_WEATHER_TEMPERATURE = "MET_WEATHER_TEMPERATURE"
|
const val MET_WEATHER_TEMPERATURE = "MET_WEATHER_TEMPERATURE"
|
||||||
const val MET_WEATHER_SYMBOL = "MET_WEATHER_SYMBOL"
|
const val MET_WEATHER_SYMBOL = "MET_WEATHER_SYMBOL"
|
||||||
|
const val MET_WEATHER_TEMPERATURE_4H = "MET_WEATHER_TEMPERATURE_4H"
|
||||||
|
const val MET_WEATHER_SYMBOL_4H = "MET_WEATHER_SYMBOL_4H"
|
||||||
const val WEATHER_RESPONSE = "WEATHER_RESPONSE"
|
const val WEATHER_RESPONSE = "WEATHER_RESPONSE"
|
||||||
const val WEATHER_UNITS = "WEATHER_UNITS"
|
const val WEATHER_UNITS = "WEATHER_UNITS"
|
||||||
const val LATITUDE = "LATITUDE"
|
const val LATITUDE = "LATITUDE"
|
||||||
|
|||||||
BIN
dist/EasyLauncher-Internet-v0.3.8-Signed.apk
vendored
Normal file
BIN
dist/EasyLauncher-Internet-v0.3.8-Signed.apk
vendored
Normal file
Binary file not shown.
BIN
dist/EasyLauncher-Internet-v0.3.8-Signed.apk.idsig
vendored
Normal file
BIN
dist/EasyLauncher-Internet-v0.3.8-Signed.apk.idsig
vendored
Normal file
Binary file not shown.
BIN
dist/EasyLauncher-v0.3.8-Signed.apk
vendored
Normal file
BIN
dist/EasyLauncher-v0.3.8-Signed.apk
vendored
Normal file
Binary file not shown.
BIN
dist/EasyLauncher-v0.3.8-Signed.apk.idsig
vendored
Normal file
BIN
dist/EasyLauncher-v0.3.8-Signed.apk.idsig
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user