Skip to content

Commit 1bb9570

Browse files
authored
Merge pull request #428 from sebastinto/feature/weather-fields
Feature/weather fields
2 parents c22c8bb + 283eeff commit 1bb9570

File tree

23 files changed

+2651
-146
lines changed

23 files changed

+2651
-146
lines changed

app/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins {
22
id 'com.android.application'
33
id 'org.jetbrains.kotlin.android'
4+
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.0'
45
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
56
}
67

@@ -92,8 +93,11 @@ dependencies {
9293
implementation 'org.luaj:luaj-jse:3.0.1'
9394
implementation 'com.github.amitshekhariitbhu.Fast-Android-Networking:android-networking:1.0.4'
9495
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
96+
implementation libs.bundles.ktor
9597

9698
testImplementation 'junit:junit:4.13.2'
99+
testImplementation libs.bundles.mockito
100+
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
97101
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
98102
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
99103
}

app/src/androidTest/java/com/coderGtm/yantra/ExampleInstrumentedTest.kt

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
package com.coderGtm.yantra.commands.weather
22

3-
import android.content.pm.PackageManager
4-
import android.graphics.Typeface
5-
import androidx.appcompat.app.AppCompatDelegate
6-
import com.android.volley.Request
7-
import com.android.volley.toolbox.StringRequest
8-
import com.android.volley.toolbox.Volley
93
import com.coderGtm.yantra.R
104
import com.coderGtm.yantra.blueprints.BaseCommand
115
import com.coderGtm.yantra.models.CommandMetadata
@@ -18,26 +12,18 @@ class Command(terminal: Terminal) : BaseCommand(terminal) {
1812
description = terminal.activity.getString(R.string.cmd_weather_help)
1913
)
2014

15+
2116
override fun execute(command: String) {
22-
val args = command.split(" ")
23-
if (args.size < 2) {
24-
output(terminal.activity.getString(R.string.please_specify_a_location), terminal.theme.errorTextColor)
25-
return
17+
when (val parseResult = parseWeatherCommand(command, this.terminal.activity)) {
18+
is ParseResult.MissingLocation -> handleMissingLocation(this)
19+
is ParseResult.ValidationError -> handleValidationError(
20+
parseResult.formatErrors,
21+
parseResult.invalidFields,
22+
this
23+
)
24+
25+
is ParseResult.ListCommand -> showAvailableFields(this)
26+
is ParseResult.Success -> fetchWeatherData(parseResult.args, this)
2627
}
27-
val location = command.trim().removePrefix(args[0]).trim()
28-
val langCode = AppCompatDelegate.getApplicationLocales().toLanguageTags()
29-
output(terminal.activity.getString(R.string.fetching_weather_report_of, location), terminal.theme.resultTextColor, Typeface.ITALIC)
30-
val apiKey = terminal.activity.packageManager.getApplicationInfo(terminal.activity.packageName, PackageManager.GET_META_DATA).metaData.getString("WEATHER_API_KEY")
31-
val url = "https://api.weatherapi.com/v1/forecast.json?key=$apiKey&q=$location&lang=$langCode&aqi=yes"
32-
val queue = Volley.newRequestQueue(terminal.activity)
33-
val stringRequest = StringRequest(
34-
Request.Method.GET, url,
35-
{ response ->
36-
handleResponse(response, this@Command)
37-
},
38-
{ error ->
39-
handleError(error, this@Command)
40-
})
41-
queue.add(stringRequest)
4228
}
43-
}
29+
}
Lines changed: 160 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,177 @@
11
package com.coderGtm.yantra.commands.weather
22

3+
import android.content.pm.PackageManager
34
import android.graphics.Typeface
4-
import com.android.volley.NoConnectionError
5-
import com.android.volley.VolleyError
5+
import androidx.appcompat.app.AppCompatDelegate
66
import com.coderGtm.yantra.R
7-
import org.json.JSONObject
8-
import kotlin.math.roundToInt
7+
import com.coderGtm.yantra.blueprints.BaseCommand
8+
import com.coderGtm.yantra.network.HttpClientProvider
9+
import io.ktor.client.call.body
10+
import io.ktor.client.plugins.ClientRequestException
11+
import io.ktor.client.request.get
12+
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.Dispatchers
14+
import kotlinx.coroutines.Job
15+
import kotlinx.coroutines.ensureActive
16+
import kotlinx.coroutines.launch
17+
import kotlinx.coroutines.withContext
18+
import java.net.ConnectException
19+
import java.net.UnknownHostException
20+
import kotlin.coroutines.cancellation.CancellationException
921

10-
fun handleResponse(response: String, command: Command) {
11-
command.output("-------------------------")
12-
val json = JSONObject(response)
13-
try {
14-
val weather_location = json.getJSONObject("location").getString("name") + ", " + json.getJSONObject("location").getString("country")
15-
val current = json.getJSONObject("current")
16-
val condition = current.getJSONObject("condition").getString("text")
17-
val temp_c = current.getDouble("temp_c")
18-
val temp_f = current.getDouble("temp_f")
19-
val feelslike_c = current.getDouble("feelslike_c")
20-
val feelslike_f = current.getDouble("feelslike_f")
21-
val wind_kph = current.getDouble("wind_kph")
22-
val wind_mph = current.getDouble("wind_mph")
23-
val wind_dir = current.getString("wind_dir")
24-
val humidity = current.getDouble("humidity")
25-
val air_quality = current.getJSONObject("air_quality")
26-
val air_quality_index = air_quality.getInt("us-epa-index")
27-
val forecast = json.getJSONObject("forecast")
28-
val forecastDay = forecast.getJSONArray("forecastday").getJSONObject(0)
29-
val day = forecastDay.getJSONObject("day")
30-
val maxtemp_c = day.getDouble("maxtemp_c")
31-
val mintemp_c = day.getDouble("mintemp_c")
32-
val maxtemp_f = day.getDouble("maxtemp_f")
33-
val mintemp_f = day.getDouble("mintemp_f")
34-
val will_it_rain = day.getInt("daily_will_it_rain")
35-
val will_it_snow = day.getInt("daily_will_it_snow")
36-
val precipitation_chance = day.getInt("daily_chance_of_rain")
37-
val snow_chance = day.getInt("daily_chance_of_snow")
38-
command.output(command.terminal.activity.getString(R.string.weather_report_of, weather_location), command.terminal.theme.successTextColor, Typeface.BOLD)
39-
command.output("=> $condition")
40-
command.output(command.terminal.activity.getString(R.string.weather_temperature_c_f, temp_c, temp_f))
41-
command.output(command.terminal.activity.getString(R.string.weather_feels_like_c_f, feelslike_c, feelslike_f))
42-
command.output(command.terminal.activity.getString(R.string.weather_min_c_f, mintemp_c, mintemp_f))
43-
command.output(command.terminal.activity.getString(R.string.weather_max_c_f, maxtemp_c, maxtemp_f))
44-
command.output(command.terminal.activity.getString(R.string.weather_humidity, humidity.roundToInt()))
45-
command.output(command.terminal.activity.getString(R.string.weather_wind, wind_kph, wind_mph, wind_dir))
46-
command.output(command.terminal.activity.getString(R.string.weather_air_quality, getAqiText(air_quality_index)))
47-
if (will_it_rain == 1) {
48-
command.output(command.terminal.activity.getString(R.string.precipitation_chance, precipitation_chance))
49-
}
50-
if (will_it_snow == 1) {
51-
command.output(command.terminal.activity.getString(R.string.snow_chance, snow_chance))
22+
private var weatherJob: Job? = null
23+
24+
/**
25+
* Fetches weather data from the WeatherAPI for the specified location.
26+
*
27+
* @param args The [WeatherCommandArgs] containing the location for which to fetch weather data.
28+
* @param command The [BaseCommand] instance.
29+
*/
30+
fun fetchWeatherData(args: WeatherCommandArgs, command: BaseCommand) {
31+
val location = args.location
32+
33+
val langCode = AppCompatDelegate.getApplicationLocales().toLanguageTags()
34+
command.output(
35+
command.terminal.activity.getString(R.string.fetching_weather_report_of, location),
36+
command.terminal.theme.resultTextColor,
37+
Typeface.ITALIC
38+
)
39+
40+
val apiKey = command.terminal.activity.packageManager.getApplicationInfo(
41+
command.terminal.activity.packageName,
42+
PackageManager.GET_META_DATA
43+
).metaData.getString("WEATHER_API_KEY")
44+
45+
val url =
46+
"https://api.weatherapi.com/v1/forecast.json?key=$apiKey&q=$location&lang=$langCode&aqi=yes"
47+
48+
weatherJob?.cancel()
49+
weatherJob = CoroutineScope(Dispatchers.Main).launch {
50+
try {
51+
ensureActive()
52+
val weather = withContext(Dispatchers.IO) {
53+
HttpClientProvider.client.get(url).body<WeatherResponse>()
54+
}
55+
handleResponse(weather, args, command)
56+
} catch (e: Exception) {
57+
if (e is CancellationException) return@launch
58+
handleKtorError(e, command)
5259
}
53-
} catch (e: Exception) {
54-
command.output(command.terminal.activity.getString(R.string.an_error_occurred, e.message.toString()))
5560
}
61+
}
5662

57-
command.output("-------------------------")
63+
/**
64+
* Handles the error response from the WeatherAPI.
65+
*/
66+
internal suspend fun handleKtorError(error: Exception, command: BaseCommand) {
67+
when (error) {
68+
is ClientRequestException -> {
69+
val apiError = parseErrorResponse(error)
70+
val stringRes = getWeatherApiErrorStringRes(apiError, error.response.status.value)
71+
command.output(
72+
command.terminal.activity.getString(stringRes),
73+
command.terminal.theme.errorTextColor
74+
)
75+
}
76+
77+
is ConnectException, is UnknownHostException -> {
78+
command.output(
79+
command.terminal.activity.getString(R.string.no_internet_connection),
80+
command.terminal.theme.errorTextColor
81+
)
82+
}
83+
84+
else -> {
85+
command.output(
86+
command.terminal.activity.getString(R.string.an_error_occurred_no_reason),
87+
command.terminal.theme.errorTextColor
88+
)
89+
}
90+
}
5891
}
5992

60-
fun handleError(error: VolleyError, command: Command) {
61-
if (error is NoConnectionError) {
62-
command.output(command.terminal.activity.getString(R.string.no_internet_connection), command.terminal.theme.errorTextColor)
93+
/**
94+
* Convenience function to parse the error response from the WeatherAPI.
95+
*/
96+
internal suspend fun parseErrorResponse(
97+
exception: ClientRequestException
98+
): WeatherApiError? = withContext(Dispatchers.IO) {
99+
try {
100+
exception.response.body<WeatherErrorResponse>().error
101+
} catch (_: Exception) {
102+
null
63103
}
64-
else if (error.networkResponse.statusCode == 400) {
65-
command.output(command.terminal.activity.getString(R.string.location_not_found), command.terminal.theme.warningTextColor)
104+
}
105+
106+
/**
107+
* Convenience function to get the appropriate error string resource based on the API error code.
108+
*/
109+
internal fun getWeatherApiErrorStringRes(
110+
apiError: WeatherApiError?,
111+
statusCode: Int
112+
): Int = when (apiError?.code) {
113+
1002 -> R.string.weather_api_key_not_provided
114+
1003 -> R.string.weather_location_parameter_missing
115+
1005 -> R.string.weather_api_request_invalid
116+
1006 -> R.string.weather_location_not_found
117+
2006 -> R.string.weather_api_key_invalid
118+
2007 -> R.string.weather_quota_exceeded
119+
2008 -> R.string.weather_api_disabled
120+
2009 -> R.string.weather_api_access_restricted
121+
9000 -> R.string.weather_bulk_request_invalid
122+
9001 -> R.string.weather_bulk_too_many_locations
123+
9999 -> R.string.weather_internal_error
124+
else -> getGenericErrorForStatus(statusCode)
66125
}
67-
else {
68-
command.output(command.terminal.activity.getString(R.string.an_error_occurred_no_reason),command.terminal.theme.errorTextColor)
126+
127+
/**
128+
* Convenience function to get the appropriate error string resource based on the HTTP status code.
129+
*/
130+
private fun getGenericErrorForStatus(statusCode: Int): Int {
131+
return when (statusCode) {
132+
400 -> R.string.weather_location_not_found
133+
401 -> R.string.weather_api_key_invalid
134+
403 -> R.string.weather_quota_exceeded
135+
else -> R.string.weather_unknown_error
69136
}
70137
}
71138

72-
fun getAqiText(index: Int): String {
73-
return when (index) {
74-
1 -> "Good"
75-
2 -> "Moderate"
76-
3 -> "Unhealthy for sensitive group"
77-
4 -> "Unhealthy"
78-
5 -> "Very Unhealthy"
79-
6 -> "Hazardous"
80-
else -> "Unknown"
139+
/**
140+
* Handles the successful response from the WeatherAPI.
141+
*/
142+
private fun handleResponse(
143+
weather: WeatherResponse,
144+
args: WeatherCommandArgs,
145+
command: BaseCommand,
146+
) {
147+
command.output("-------------------------")
148+
with(command.terminal.activity) {
149+
try {
150+
val location = "${weather.location.name}, ${weather.location.country}"
151+
command.output(
152+
getString(R.string.weather_report_of, location),
153+
command.terminal.theme.successTextColor,
154+
Typeface.BOLD
155+
)
156+
157+
val fieldsToShow = if (args.showDefaultFields) {
158+
DEFAULT_WEATHER_FIELDS
159+
} else {
160+
args.requestedFields
161+
}
162+
163+
fieldsToShow.forEach { fieldKey ->
164+
WEATHER_FIELD_MAP[fieldKey]?.renderer?.invoke(weather, command)
165+
}
166+
} catch (e: Exception) {
167+
command.output(
168+
getString(
169+
R.string.an_error_occurred,
170+
e.message.toString()
171+
)
172+
)
173+
}
81174
}
175+
176+
command.output("-------------------------")
82177
}

0 commit comments

Comments
 (0)