1
1
package com.coderGtm.yantra.commands.weather
2
2
3
+ import android.content.pm.PackageManager
3
4
import android.graphics.Typeface
4
- import com.android.volley.NoConnectionError
5
- import com.android.volley.VolleyError
5
+ import androidx.appcompat.app.AppCompatDelegate
6
6
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
9
21
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)
52
59
}
53
- } catch (e: Exception ) {
54
- command.output(command.terminal.activity.getString(R .string.an_error_occurred, e.message.toString()))
55
60
}
61
+ }
56
62
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
+ }
58
91
}
59
92
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
63
103
}
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)
66
125
}
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
69
136
}
70
137
}
71
138
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
+ }
81
174
}
175
+
176
+ command.output(" -------------------------" )
82
177
}
0 commit comments