Move weather fetching and SVG generation to native Kotlin - #7
Conversation
timbortnik
commented
Jan 17, 2026
- Add WeatherFetcher.kt for native HTTP requests to Open-Meteo API
- Add WeatherDataParser.kt for parsing cached weather JSON
- Add SvgChartGenerator.kt as the single source of SVG generation
- Add NativeSvgService for Dart↔Kotlin method channel communication
- Remove Dart implementations: weather_service.dart, background_service.dart, svg_chart_generator.dart, native_svg_renderer.dart, locale_utils.dart
- Remove obsolete constants (ChartConstants, AlarmConstants)
- Update home_widget to 0.9.0 and remove version workaround docs
- Force Java 17 for all subprojects to eliminate deprecation warnings
- Clean up dead code in widget_service.dart and units_service.dart
- Add WeatherFetcher.kt for native HTTP requests to Open-Meteo API - Add WeatherDataParser.kt for parsing cached weather JSON - Add SvgChartGenerator.kt as the single source of SVG generation - Add NativeSvgService for Dart↔Kotlin method channel communication - Remove Dart implementations: weather_service.dart, background_service.dart, svg_chart_generator.dart, native_svg_renderer.dart, locale_utils.dart - Remove obsolete constants (ChartConstants, AlarmConstants) - Update home_widget to 0.9.0 and remove version workaround docs - Force Java 17 for all subprojects to eliminate deprecation warnings - Clean up dead code in widget_service.dart and units_service.dart Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| var connection: HttpURLConnection? = null | ||
|
|
||
| return try { | ||
| connection = url.openConnection() as HttpURLConnection |
Check warning
Code scanning / CodeQL
Android missing certificate pinning Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 8 months ago
In general, to fix missing certificate pinning you must ensure that HTTPS connections only trust a specific certificate or public key instead of the full system CA set. In this code, the most direct fix (without altering higher-level app behavior) is to configure certificate pinning at the HTTP client level and use that client for the request.
The cleanest way within this snippet, and without changing the overall functionality of fetchFromApi, is:
- Replace the direct use of
HttpURLConnectionwithokhttp3.OkHttpClient. - Configure an
okhttp3.CertificatePinnerwith the expected host and SHA-256 pin(s) of its leaf or intermediate certificate(s). - Use this pinned
OkHttpClientto perform the GET request and read the response body into a string, then parse it intoJSONObjectas before.
Concretely in android/app/src/main/kotlin/org/bortnik/meteogram/WeatherFetcher.kt:
- Add imports for OkHttp types (
OkHttpClient,Request,CertificatePinner,Call,Response) at the top of the file. - Add a private helper method (or static property) that builds a pinned
OkHttpClient. It should:- Extract the host from
buildUrl(latitude, longitude)usingURL(...). - Construct a
CertificatePinnerwith.add(host, "<your sha256 pin>"). (The actual pin value needs to be filled with the real one for the Open-Meteo endpoint used; I will place a placeholder for now.)
- Extract the host from
- Rewrite
fetchFromApito:- Build the URL string via
buildUrl. - Build a
Requestwith that URL. - Get a pinned
OkHttpClientinstance. - Execute
client.newCall(request).execute(), validateisSuccessful(or status code 200), read the body as string (response.body?.string()), and returnJSONObjectof that string. - Catch exceptions and log as before.
- Build the URL string via
This preserves existing behavior (same URL, same JSON parsing, same timeouts if we configure them similarly via OkHttp’s builder) but adds certificate pinning for that domain.
| @@ -12,6 +12,10 @@ | ||
| import java.util.Locale | ||
| import java.util.TimeZone | ||
| import java.util.concurrent.Executors | ||
| import okhttp3.CertificatePinner | ||
| import okhttp3.OkHttpClient | ||
| import okhttp3.Request | ||
| import okhttp3.Response | ||
|
|
||
| /** | ||
| * Native weather fetcher for Open-Meteo API. | ||
| @@ -103,35 +107,33 @@ | ||
| * @return JSON response or null on failure | ||
| */ | ||
| private fun fetchFromApi(latitude: Double, longitude: Double): JSONObject? { | ||
| val url = URL(buildUrl(latitude, longitude)) | ||
| var connection: HttpURLConnection? = null | ||
|
|
||
| val urlString = buildUrl(latitude, longitude) | ||
| return try { | ||
| connection = url.openConnection() as HttpURLConnection | ||
| connection.requestMethod = "GET" | ||
| connection.connectTimeout = TIMEOUT_MS | ||
| connection.readTimeout = TIMEOUT_MS | ||
| val url = URL(urlString) | ||
| val client = createPinnedClient(url.host) | ||
|
|
||
| val responseCode = connection.responseCode | ||
| if (responseCode != HttpURLConnection.HTTP_OK) { | ||
| Log.e(TAG, "API returned $responseCode") | ||
| return null | ||
| } | ||
| val request = Request.Builder() | ||
| .url(urlString) | ||
| .get() | ||
| .build() | ||
|
|
||
| val reader = BufferedReader(InputStreamReader(connection.inputStream)) | ||
| val response = StringBuilder() | ||
| var line: String? | ||
| while (reader.readLine().also { line = it } != null) { | ||
| response.append(line) | ||
| } | ||
| reader.close() | ||
| client.newCall(request).execute().use { response: Response -> | ||
| if (!response.isSuccessful) { | ||
| Log.e(TAG, "API returned ${response.code}") | ||
| return null | ||
| } | ||
|
|
||
| JSONObject(response.toString()) | ||
| val bodyString = response.body?.string() | ||
| if (bodyString.isNullOrEmpty()) { | ||
| Log.e(TAG, "Empty response body") | ||
| return null | ||
| } | ||
|
|
||
| JSONObject(bodyString) | ||
| } | ||
| } catch (e: Exception) { | ||
| Log.e(TAG, "Network error", e) | ||
| null | ||
| } finally { | ||
| connection?.disconnect() | ||
| } | ||
| } | ||
|
|
| @@ -75,4 +75,6 @@ | ||
| androidTestImplementation("androidx.test.ext:junit:1.1.5") | ||
| androidTestImplementation("androidx.test:runner:1.5.2") | ||
| androidTestImplementation("androidx.test:rules:1.5.0") | ||
| implementation("com.squareup.okhttp3:okhttp:5.3.2") | ||
|
|
||
| } |
| Package | Version | Security advisories |
| com.squareup.okhttp3:okhttp (maven) | 5.3.2 | None |
|
|
||
| // notifyAppWidgetViewDataChanged triggers onUpdate | ||
| for (widgetId in widgetIds) { | ||
| appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, android.R.id.list) |
Check notice
Code scanning / CodeQL
Deprecated method or constructor invocation Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 8 months ago
In general, deprecated API usages should be replaced with their recommended alternatives. For AppWidgetManager.notifyAppWidgetViewDataChanged, the Android framework recommends calling the overload that accepts an array of widget IDs instead of the deprecated single-ID overload.
In this method we already have widgetIds: IntArray, so we can avoid the deprecated overload entirely by making a single call to notifyAppWidgetViewDataChanged(widgetIds, android.R.id.list) instead of looping and calling the deprecated version per ID. This keeps functionality identical (all the same widgets’ list views are notified to refresh) and uses the non-deprecated API. No new imports are required because AppWidgetManager and android.R are already available. The only changes needed are within WidgetUtils.rerenderAllWidgetsNative around lines 154–157: remove the for (widgetId in widgetIds) loop that calls the deprecated method and replace it with one call to the non-deprecated overload.
| @@ -151,16 +151,18 @@ | ||
|
|
||
| Log.d(TAG, "Triggering native update for ${widgetIds.size} widgets") | ||
|
|
||
| // notifyAppWidgetViewDataChanged triggers onUpdate | ||
| for (widgetId in widgetIds) { | ||
| appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, android.R.id.list) | ||
| } | ||
| // notifyAppWidgetViewDataChanged triggers onUpdate for all widget IDs | ||
| appWidgetManager.notifyAppWidgetViewDataChanged(widgetIds, android.R.id.list) | ||
|
|
||
| // Also send explicit update intent | ||
| val intent = android.content.Intent(context, MeteogramWidgetProvider::class.java).apply { | ||
| action = AppWidgetManager.ACTION_APPWIDGET_UPDATE | ||
| putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds) | ||
| } | ||
| val intent = android.content.Intent(context, MeteogramWidgetProvider::class.java).apply { | ||
| action = AppWidgetManager.ACTION_APPWIDGET_UPDATE | ||
| putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds) | ||
| } | ||
| context.sendBroadcast(intent) | ||
|
|
||
| Log.d(TAG, "Native widget update triggered for ${widgetIds.joinToString()}") |