Skip to content

Move weather fetching and SVG generation to native Kotlin - #7

Merged
timbortnik merged 1 commit into
mainfrom
native-svg-generator
Jan 17, 2026
Merged

Move weather fetching and SVG generation to native Kotlin#7
timbortnik merged 1 commit into
mainfrom
native-svg-generator

Conversation

@timbortnik

Copy link
Copy Markdown
Owner
  • 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

This network call does not implement certificate pinning. (no explicitly trusted domains)

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:

  1. Replace the direct use of HttpURLConnection with okhttp3.OkHttpClient.
  2. Configure an okhttp3.CertificatePinner with the expected host and SHA-256 pin(s) of its leaf or intermediate certificate(s).
  3. Use this pinned OkHttpClient to perform the GET request and read the response body into a string, then parse it into JSONObject as 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) using URL(...).
    • Construct a CertificatePinner with .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.)
  • Rewrite fetchFromApi to:
    • Build the URL string via buildUrl.
    • Build a Request with that URL.
    • Get a pinned OkHttpClient instance.
    • Execute client.newCall(request).execute(), validate isSuccessful (or status code 200), read the body as string (response.body?.string()), and return JSONObject of that string.
    • Catch exceptions and log as before.

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.


Suggested changeset 2
android/app/src/main/kotlin/org/bortnik/meteogram/WeatherFetcher.kt

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/android/app/src/main/kotlin/org/bortnik/meteogram/WeatherFetcher.kt b/android/app/src/main/kotlin/org/bortnik/meteogram/WeatherFetcher.kt
--- a/android/app/src/main/kotlin/org/bortnik/meteogram/WeatherFetcher.kt
+++ b/android/app/src/main/kotlin/org/bortnik/meteogram/WeatherFetcher.kt
@@ -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()
         }
     }
 
EOF
@@ -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()
}
}

android/app/build.gradle.kts
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -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")
+
 }
EOF
@@ -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")

}
This fix introduces these dependencies
Package Version Security advisories
com.squareup.okhttp3:okhttp (maven) 5.3.2 None
Copilot is powered by AI and may make mistakes. Always verify output.

// 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

Invoking
AppWidgetManager.notifyAppWidgetViewDataChanged
should be avoided because it has been deprecated.

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.


Suggested changeset 1
android/app/src/main/kotlin/org/bortnik/meteogram/WidgetUtils.kt

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/android/app/src/main/kotlin/org/bortnik/meteogram/WidgetUtils.kt b/android/app/src/main/kotlin/org/bortnik/meteogram/WidgetUtils.kt
--- a/android/app/src/main/kotlin/org/bortnik/meteogram/WidgetUtils.kt
+++ b/android/app/src/main/kotlin/org/bortnik/meteogram/WidgetUtils.kt
@@ -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()}")
EOF
@@ -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()}")
Copilot is powered by AI and may make mistakes. Always verify output.
@timbortnik
timbortnik merged commit daecf01 into main Jan 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants