@@ -34,6 +34,7 @@ object SaicWeather {
3434 private const val CALLBACK_DESCRIPTOR = " com.saicmotor.mapservice.ICallBack"
3535
3636 private const val TX_QUERY_WEATHER_BY_POSITION = 2
37+ private const val TX_QUERY_FORECAST_BY_POSITION = 30
3738 private const val TX_CALLBACK_ON_RESULT = 1
3839
3940 /* *
@@ -42,6 +43,7 @@ object SaicWeather {
4243 * nothing.
4344 */
4445 private const val DATA_OBJECT_WEATHER = " data_object_weather"
46+ private const val DATA_OBJECT_WEATHER_FUTURE = " data_object_weather_future"
4547
4648 /* *
4749 * How long a reading may hold the rule cycle.
@@ -52,6 +54,12 @@ object SaicWeather {
5254 */
5355 private const val TIMEOUT_MS = 2_000L
5456
57+ /* *
58+ * A daily outlook is a handful of days. A larger count means the parcel is not what this
59+ * expects, and reading on would consume whatever follows it as strings.
60+ */
61+ private const val MAX_FORECAST_DAYS = 16
62+
5563 private val service = SaicService .byComponent(PACKAGE , CLASS , " weather" )
5664
5765 val isAvailable: Boolean get() = binder() != null
@@ -66,17 +74,59 @@ object SaicWeather {
6674 * [languageCode] reaches the provider and decides the language of [Reading.text], which is
6775 * the field a rule compares against — so it is the caller's, not a constant here.
6876 */
69- fun currentAt (latitude : Double , longitude : Double , languageCode : String ): Reading ? {
77+ fun currentAt (latitude : Double , longitude : Double , languageCode : String ): Reading ? =
78+ // Longitude first, then latitude, and both as strings — the query takes them in that
79+ // order, so swapping them would ask about a plausible-looking wrong place rather than
80+ // failing outright.
81+ query(TX_QUERY_WEATHER_BY_POSITION , ::readCurrent) { target, callback ->
82+ SaicAidl .callVoid(
83+ target, DESCRIPTOR , TX_QUERY_WEATHER_BY_POSITION ,
84+ longitude.toString(), latitude.toString(), languageCode, callback
85+ )
86+ }
87+
88+ /* *
89+ * The days ahead, today first.
90+ *
91+ * **Days, not hours.** The service answers a daily outlook — a phrase and a high/low per
92+ * day — so "will it rain tomorrow" is answerable here and "will it rain in three hours" is
93+ * not. A rule that wants to close the windows before a shower is not served by this, and
94+ * nothing in the catalogue pretends otherwise.
95+ *
96+ * The position arrives as doubles on this transaction where the current-conditions query
97+ * takes strings. That is the interface, not a choice.
98+ */
99+ fun forecastAt (latitude : Double , longitude : Double , languageCode : String ): List <Day >? =
100+ query(TX_QUERY_FORECAST_BY_POSITION , ::readForecast) { target, callback ->
101+ SaicAidl .callVoid(
102+ target, DESCRIPTOR , TX_QUERY_FORECAST_BY_POSITION ,
103+ longitude, latitude, languageCode, callback
104+ )
105+ }
106+
107+ /* *
108+ * One query, made synchronous.
109+ *
110+ * The service takes a callback binder and answers on it, so every reading here is a send
111+ * plus a bounded wait. A binder that never calls back leaves the latch untripped and the
112+ * reading null, which the catalogue treats as "cannot tell" — the same as any other
113+ * unreadable signal.
114+ */
115+ private fun <T > query (
116+ code : Int ,
117+ decode : (Parcel ) -> T ? ,
118+ send : (IBinder , Binder ) -> Boolean
119+ ): T ? {
70120 val target = binder() ? : return null
71- var reading : Reading ? = null
121+ var answer : T ? = null
72122 val answered = CountDownLatch (1 )
73123
74124 val callback = object : Binder () {
75- override fun onTransact (code : Int , data : Parcel , reply : Parcel ? , flags : Int ): Boolean {
76- if (code != TX_CALLBACK_ON_RESULT ) return super .onTransact(code , data, reply, flags)
125+ override fun onTransact (tx : Int , data : Parcel , reply : Parcel ? , flags : Int ): Boolean {
126+ if (tx != TX_CALLBACK_ON_RESULT ) return super .onTransact(tx , data, reply, flags)
77127 data.enforceInterface(CALLBACK_DESCRIPTOR )
78- reading = runCatching { readResult (data) }
79- .onFailure { AppLogger .d(TAG , " weather: undecodable answer: ${it.message} " ) }
128+ answer = runCatching { decode (data) }
129+ .onFailure { AppLogger .d(TAG , " weather# $code : undecodable answer: ${it.message} " ) }
80130 .getOrNull()
81131 answered.countDown()
82132 reply?.writeNoException()
@@ -86,15 +136,8 @@ object SaicWeather {
86136 override fun getInterfaceDescriptor (): String = CALLBACK_DESCRIPTOR
87137 }
88138
89- // Longitude first, then latitude: the query takes them in that order, and both as
90- // strings — swapping them would ask about a plausible-looking wrong place rather
91- // than failing.
92- val sent = SaicAidl .callVoid(
93- target, DESCRIPTOR , TX_QUERY_WEATHER_BY_POSITION ,
94- longitude.toString(), latitude.toString(), languageCode, callback
95- )
96- if (! sent) return null
97- return if (answered.await(TIMEOUT_MS , TimeUnit .MILLISECONDS )) reading else null
139+ if (! send(target, callback)) return null
140+ return if (answered.await(TIMEOUT_MS , TimeUnit .MILLISECONDS )) answer else null
98141 }
99142
100143 /* *
@@ -104,11 +147,8 @@ object SaicWeather {
104147 * boot classpath — the same reason every transaction here is written out rather than
105148 * called through an SDK.
106149 */
107- private fun readResult (parcel : Parcel ): Reading ? {
108- parcel.readInt() // code — the payload's presence is the real answer
109- parcel.readString() // message
110- if (parcel.readString() != DATA_OBJECT_WEATHER ) return null
111- parcel.readString() // the parcelable's own class name
150+ private fun readCurrent (parcel : Parcel ): Reading ? {
151+ if (! openPayload(parcel, DATA_OBJECT_WEATHER )) return null
112152 val text = parcel.readString()
113153 val icon = parcel.readString()
114154 parcel.readByte() // daytime
@@ -125,15 +165,71 @@ object SaicWeather {
125165 )
126166 }
127167
168+ /* *
169+ * The daily outlook: an envelope, a place, then the list of days.
170+ *
171+ * The nine strings before the list are the place and the provider's headline — read past
172+ * rather than skipped, because a parcel has no field names and the only way to reach the
173+ * list is to consume exactly what precedes it.
174+ */
175+ private fun readForecast (parcel : Parcel ): List <Day >? {
176+ if (! openPayload(parcel, DATA_OBJECT_WEATHER_FUTURE )) return null
177+ parcel.readInt() // the payload's own code
178+ parcel.readString() // the nested parcelable's class name
179+ repeat(9 ) { parcel.readString() } // place, coordinates and headline
180+ val days = parcel.readInt()
181+ // A negative or absurd count is a parcel that does not hold what this expects; reading
182+ // on would consume whatever follows as strings.
183+ if (days <= 0 || days > MAX_FORECAST_DAYS ) return null
184+ return (0 until days).map {
185+ val date = parcel.readString().orEmpty()
186+ val maxValue = parcel.readString()
187+ parcel.readString() // night icon
188+ parcel.readString() // day icon
189+ parcel.readString() // epoch date
190+ val nightPhrase = parcel.readString().orEmpty()
191+ val maxUnit = parcel.readString()
192+ val minUnit = parcel.readString()
193+ parcel.readString() // max unit type
194+ val minValue = parcel.readString()
195+ parcel.readString() // min unit type
196+ val dayPhrase = parcel.readString().orEmpty()
197+ Day (
198+ date = date,
199+ dayText = dayPhrase,
200+ nightText = nightPhrase,
201+ highCelsius = toCelsius(maxValue?.toDoubleOrNull(), maxUnit),
202+ lowCelsius = toCelsius(minValue?.toDoubleOrNull(), minUnit)
203+ )
204+ }
205+ }
206+
207+ /* *
208+ * Reads the `Result` envelope and stops unless it holds the expected payload.
209+ *
210+ * A code, a message, a tag naming what follows, then the tagged object. Another tag is
211+ * another shape, and answering from a half-read parcel would be worse than saying nothing.
212+ */
213+ private fun openPayload (parcel : Parcel , expectedTag : String ): Boolean {
214+ parcel.readInt() // code — the payload's presence is the real answer
215+ parcel.readString() // message
216+ if (parcel.readString() != expectedTag) return false
217+ parcel.readString() // the parcelable's own class name
218+ return true
219+ }
220+
128221 /* *
129222 * The service answers in whatever unit it was configured with and names it, so the name is
130223 * what decides. Anything it does not name is left alone rather than assumed Celsius: a
131224 * Fahrenheit value passed through unconverted would read as a mild summer day in winter.
132225 */
133- private fun toCelsius (value : Double , unit : String? ): Double? = when (unit?.trim()?.uppercase()) {
134- " C" , " °C" -> value
135- " F" , " °F" -> (value - 32.0 ) * 5.0 / 9.0
136- else -> null
226+ private fun toCelsius (value : Double? , unit : String? ): Double? {
227+ if (value == null ) return null
228+ return when (unit?.trim()?.uppercase()) {
229+ " C" , " °C" -> value
230+ " F" , " °F" -> (value - 32.0 ) * 5.0 / 9.0
231+ else -> null
232+ }
137233 }
138234
139235 /* *
@@ -142,6 +238,20 @@ object SaicWeather {
142238 * [text] is the provider's own description ("Rain", "Partly cloudy"), which is what a rule
143239 * compares against; [temperatureCelsius] is null when the answer named no unit.
144240 */
241+ /* *
242+ * One day of the outlook.
243+ *
244+ * [dayText] and [nightText] are the provider's phrases, in the language the query asked
245+ * for. The temperatures are null when the answer named no unit — see [toCelsius].
246+ */
247+ data class Day (
248+ val date : String ,
249+ val dayText : String ,
250+ val nightText : String ,
251+ val highCelsius : Double? ,
252+ val lowCelsius : Double?
253+ )
254+
145255 data class Reading (
146256 val text : String ,
147257 val icon : String ,
0 commit comments