diff options
Diffstat (limited to 'src/com/cyanogenmod/lockclock/weather')
8 files changed, 963 insertions, 128 deletions
diff --git a/src/com/cyanogenmod/lockclock/weather/ForecastActivity.java b/src/com/cyanogenmod/lockclock/weather/ForecastActivity.java new file mode 100644 index 0000000..2409cb8 --- /dev/null +++ b/src/com/cyanogenmod/lockclock/weather/ForecastActivity.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2013 David van Tonder + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyanogenmod.lockclock.weather; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.KeyguardManager; +import android.app.WallpaperManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.Window; +import android.view.WindowManager; +import android.view.animation.Animation; +import android.view.animation.LinearInterpolator; +import android.view.animation.RotateAnimation; +import android.widget.ImageView; + +import com.cyanogenmod.lockclock.misc.Constants; +import com.cyanogenmod.lockclock.misc.Preferences; +import com.cyanogenmod.lockclock.misc.WidgetUtils; +import com.cyanogenmod.lockclock.R; + +public class ForecastActivity extends Activity implements OnClickListener { + private static final String TAG = "ForecastActivity"; + + private BroadcastReceiver mUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Stop the animation + ImageView view = (ImageView) findViewById(R.id.weather_refresh); + view.setAnimation(null); + + if (!intent.getBooleanExtra(WeatherUpdateService.EXTRA_UPDATE_CANCELLED, false)) { + updateForecastPanel(); + } + } + }; + + @SuppressLint("InlinedApi") + @Override + public void onCreate(Bundle savedInstanceState) { + // If we are in keyguard, override the default transparent theme + KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); + boolean locked = km.isKeyguardLocked(); + if (locked) { + if (WidgetUtils.isTranslucencyAvailable()) { + setTheme(android.R.style.Theme_Holo_NoActionBar_TranslucentDecor); + } else { + setTheme(android.R.style.Theme_Holo_NoActionBar); + } + } + super.onCreate(savedInstanceState); + + // Get the window ready + Window window = getWindow(); + requestWindowFeature(Window.FEATURE_NO_TITLE); + if (locked) { + window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + final WallpaperManager wallpaperManager = WallpaperManager.getInstance(this); + final Drawable wallpaperDrawable = wallpaperManager.getFastDrawable(); + window.setBackgroundDrawable(wallpaperDrawable); + } else if (WidgetUtils.isTranslucencyAvailable()) { + window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, + WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + + registerReceiver(mUpdateReceiver, new IntentFilter(WeatherUpdateService.ACTION_UPDATE_FINISHED)); + updateForecastPanel(); + } + + @Override + protected void onDestroy() { + unregisterReceiver(mUpdateReceiver); + super.onDestroy(); + } + + @Override + protected void onUserLeaveHint() { + super.onUserLeaveHint(); + finish(); + } + + private void updateForecastPanel() { + // Get the forecasts data + WeatherInfo weather = Preferences.getCachedWeatherInfo(this); + if (weather == null) { + Log.e(TAG, "Error retrieving forecast data, exiting"); + finish(); + return; + } + + View fullLayout = ForecastBuilder.buildFullPanel(this, R.layout.forecast_activity, weather); + setContentView(fullLayout); + fullLayout.requestFitSystemWindows(); + + // Register an onClickListener on Weather refresh + findViewById(R.id.weather_refresh).setOnClickListener(this); + + // Register an onClickListener on the fake done button + findViewById(R.id.button).setOnClickListener(this); + } + + @Override + public void onClick(View v) { + if (v.getId() != R.id.button) { + // Setup anim with desired properties and start the animation + ImageView view = (ImageView) findViewById(R.id.weather_refresh); + RotateAnimation anim = new RotateAnimation(0.0f, 360.0f, + Animation.RELATIVE_TO_SELF, 0.5f, + Animation.RELATIVE_TO_SELF, 0.5f); + anim.setInterpolator(new LinearInterpolator()); + anim.setRepeatCount(Animation.INFINITE); + anim.setDuration(700); + view.startAnimation(anim); + + Intent i = new Intent(this, WeatherUpdateService.class); + i.setAction(WeatherUpdateService.ACTION_FORCE_UPDATE); + startService(i); + } else { + finish(); + } + } +} diff --git a/src/com/cyanogenmod/lockclock/weather/ForecastBuilder.java b/src/com/cyanogenmod/lockclock/weather/ForecastBuilder.java new file mode 100644 index 0000000..6f8da53 --- /dev/null +++ b/src/com/cyanogenmod/lockclock/weather/ForecastBuilder.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2013 David van Tonder + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use context file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyanogenmod.lockclock.weather; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.text.format.DateFormat; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.webkit.WebView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.cyanogenmod.lockclock.misc.IconUtils; +import com.cyanogenmod.lockclock.misc.Preferences; +import com.cyanogenmod.lockclock.misc.WidgetUtils; +import com.cyanogenmod.lockclock.weather.WeatherInfo.DayForecast; +import com.cyanogenmod.lockclock.R; + +public class ForecastBuilder { + private static final String TAG = "ForecastBuilder"; + + /** + * This method is used to build the full current conditions and horizontal forecasts + * panels + * + * @param context + * @param w = the Weather info object that contains the forecast data + * @return = a built view that can be displayed + */ + @SuppressLint("SetJavaScriptEnabled") + public static View buildFullPanel(Context context, int resourceId, WeatherInfo w) { + + // Load some basic settings + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + int color = Preferences.weatherFontColor(context); + boolean invertLowHigh = Preferences.invertLowHighTemperature(context); + + View view = inflater.inflate(resourceId, null); + + // Set the weather source + TextView weatherSource = (TextView) view.findViewById(R.id.weather_source); + weatherSource.setText(Preferences.weatherProvider(context).getNameResourceId()); + + // Set the current conditions + // Weather Image + ImageView weatherImage = (ImageView) view.findViewById(R.id.weather_image); + String iconsSet = Preferences.getWeatherIconSet(context); + weatherImage.setImageBitmap(w.getConditionBitmap(iconsSet, color, + IconUtils.getNextHigherDensity(context))); + + // Weather Condition + TextView weatherCondition = (TextView) view.findViewById(R.id.weather_condition); + weatherCondition.setText(w.getCondition()); + + // Weather Temps + TextView weatherTemp = (TextView) view.findViewById(R.id.weather_temp); + weatherTemp.setText(w.getFormattedTemperature()); + + // City + TextView city = (TextView) view.findViewById(R.id.weather_city); + city.setText(w.getCity()); + + // Weather Update Time + Date lastUpdate = w.getTimestamp(); + StringBuilder sb = new StringBuilder(); + sb.append(DateFormat.format("E", lastUpdate)); + sb.append(" "); + sb.append(DateFormat.getTimeFormat(context).format(lastUpdate)); + TextView updateTime = (TextView) view.findViewById(R.id.update_time); + updateTime.setText(sb.toString()); + updateTime.setVisibility(Preferences.showWeatherTimestamp(context) ? View.VISIBLE : View.GONE); + + // Weather Temps Panel additional items + final String low = w.getFormattedLow(); + final String high = w.getFormattedHigh(); + TextView weatherLowHigh = (TextView) view.findViewById(R.id.weather_low_high); + weatherLowHigh.setText(invertLowHigh ? high + " | " + low : low + " | " + high); + + // Get things ready + LinearLayout forecastView = (LinearLayout) view.findViewById(R.id.forecast_view); + final View progressIndicator = view.findViewById(R.id.progress_indicator); + + // Build the forecast panel + if (buildSmallPanel(context, forecastView, w)) { + // Success, hide the progress container + progressIndicator.setVisibility(View.GONE); + } + + return view; + } + + /** + * This method is used to build the small, horizontal forecasts panel + * @param context + * @param smallPanel = a horizontal linearlayout that will contain the forecasts + * @param w = the Weather info object that contains the forecast data + */ + public static boolean buildSmallPanel(Context context, LinearLayout smallPanel, WeatherInfo w) { + if (smallPanel == null) { + Log.d(TAG, "Invalid view passed"); + return false; + } + + // Get things ready + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + int color = Preferences.weatherFontColor(context); + boolean invertLowHigh = Preferences.invertLowHighTemperature(context); + + ArrayList<DayForecast> forecasts = w.getForecasts(); + if (forecasts == null || forecasts.size() <= 1) { + smallPanel.setVisibility(View.GONE); + return false; + } + + TimeZone MyTimezone = TimeZone.getDefault(); + Calendar calendar = new GregorianCalendar(MyTimezone); + + // Iterate through the forecasts + for (DayForecast d : forecasts) { + // Load the views + View forecastItem = inflater.inflate(R.layout.forecast_item, null); + + // The day of the week + TextView day = (TextView) forecastItem.findViewById(R.id.forecast_day); + day.setText(calendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, Locale.getDefault())); + calendar.roll(Calendar.DAY_OF_WEEK, true); + + // Weather Image + ImageView image = (ImageView) forecastItem.findViewById(R.id.weather_image); + String iconsSet = Preferences.getWeatherIconSet(context); + int resId = d.getConditionResource(context, iconsSet); + if (resId != 0) { + image.setImageResource(resId); + } else { + image.setImageBitmap(d.getConditionBitmap(context, iconsSet, color)); + } + + // Temperatures + String dayLow = d.getFormattedLow(); + String dayHigh = d.getFormattedHigh(); + TextView temps = (TextView) forecastItem.findViewById(R.id.weather_temps); + temps.setText(invertLowHigh ? dayHigh + " " + dayLow : dayLow + " " + dayHigh); + + // Add the view + smallPanel.addView(forecastItem, + new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1)); + } + return true; + } +} diff --git a/src/com/cyanogenmod/lockclock/weather/HttpRetriever.java b/src/com/cyanogenmod/lockclock/weather/HttpRetriever.java index 957d6dc..60723fa 100755 --- a/src/com/cyanogenmod/lockclock/weather/HttpRetriever.java +++ b/src/com/cyanogenmod/lockclock/weather/HttpRetriever.java @@ -38,7 +38,7 @@ public class HttpRetriever { return EntityUtils.toString(entity); } } catch (IOException e) { - Log.e(TAG, "Couldn't retrieve data", e); + Log.e(TAG, "Couldn't retrieve data from url " + url, e); } return null; } diff --git a/src/com/cyanogenmod/lockclock/weather/OpenWeatherMapProvider.java b/src/com/cyanogenmod/lockclock/weather/OpenWeatherMapProvider.java new file mode 100644 index 0000000..808077c --- /dev/null +++ b/src/com/cyanogenmod/lockclock/weather/OpenWeatherMapProvider.java @@ -0,0 +1,311 @@ +package com.cyanogenmod.lockclock.weather; + +import java.util.*; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.content.Context; +import android.location.Location; +import android.net.Uri; +import android.util.Log; + +import com.cyanogenmod.lockclock.weather.WeatherInfo.DayForecast; +import com.cyanogenmod.lockclock.R; + +public class OpenWeatherMapProvider implements WeatherProvider { + private static final String TAG = "OpenWeatherMapProvider"; + + private static final int FORECAST_DAYS = 5; + private static final String SELECTION_LOCATION = "lat=%f&lon=%f"; + private static final String SELECTION_ID = "id=%s"; + + private static final String URL_LOCATION = + "http://api.openweathermap.org/data/2.5/find?q=%s&mode=json&lang=%s"; + private static final String URL_WEATHER = + "http://api.openweathermap.org/data/2.5/weather?%s&mode=json&units=%s&lang=%s"; + private static final String URL_FORECAST = + "http://api.openweathermap.org/data/2.5/forecast/daily?" + + "%s&mode=json&units=%s&lang=%s&cnt=" + FORECAST_DAYS; + + private Context mContext; + + public OpenWeatherMapProvider(Context context) { + mContext = context; + } + + @Override + public int getNameResourceId() { + return R.string.weather_source_openweathermap; + } + + @Override + public List<LocationResult> getLocations(String input) { + String url = String.format(URL_LOCATION, Uri.encode(input), getLanguageCode()); + String response = HttpRetriever.retrieve(url); + if (response == null) { + return null; + } + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "URL = " + url + " returning a response of " + response); + } + + try { + JSONArray jsonResults = new JSONObject(response).getJSONArray("list"); + ArrayList<LocationResult> results = new ArrayList<LocationResult>(); + int count = jsonResults.length(); + + for (int i = 0; i < count; i++) { + JSONObject result = jsonResults.getJSONObject(i); + LocationResult location = new LocationResult(); + + location.id = result.getString("id"); + location.city = result.getString("name"); + location.countryId = result.getJSONObject("sys").getString("country"); + results.add(location); + } + + return results; + } catch (JSONException e) { + Log.w(TAG, "Received malformed location data (input=" + input + ")", e); + } + + return null; + } + + public WeatherInfo getWeatherInfo(String id, String localizedCityName, boolean metric) { + String selection = String.format(Locale.US, SELECTION_ID, id); + return handleWeatherRequest(selection, localizedCityName, metric); + } + + public WeatherInfo getWeatherInfo(Location location, boolean metric) { + String selection = String.format(Locale.US, SELECTION_LOCATION, + location.getLatitude(), location.getLongitude()); + return handleWeatherRequest(selection, null, metric); + } + + private WeatherInfo handleWeatherRequest(String selection, + String localizedCityName, boolean metric) { + String units = metric ? "metric" : "imperial"; + String locale = getLanguageCode(); + String conditionUrl = String.format(Locale.US, URL_WEATHER, selection, units, locale); + String conditionResponse = HttpRetriever.retrieve(conditionUrl); + if (conditionResponse == null) { + return null; + } + + String forecastUrl = String.format(Locale.US, URL_FORECAST, selection, units, locale); + String forecastResponse = HttpRetriever.retrieve(forecastUrl); + if (forecastResponse == null) { + return null; + } + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "URL = " + conditionUrl + " returning a response of " + conditionResponse); + } + + try { + JSONObject conditions = new JSONObject(conditionResponse); + JSONObject weather = conditions.getJSONArray("weather").getJSONObject(0); + JSONObject conditionData = conditions.getJSONObject("main"); + JSONObject windData = conditions.getJSONObject("wind"); + ArrayList<DayForecast> forecasts = + parseForecasts(new JSONObject(forecastResponse).getJSONArray("list")); + int speedUnitResId = metric ? R.string.weather_kph : R.string.weather_mph; + if (localizedCityName == null) { + localizedCityName = conditions.getString("name"); + } + + WeatherInfo w = new WeatherInfo(mContext, conditions.getString("id"), localizedCityName, + /* condition */ weather.getString("main"), + /* conditionCode */ mapConditionIconToCode( + weather.getString("icon"), weather.getInt("id")), + /* temperature */ (float) conditionData.getDouble("temp"), + /* tempUnit */ metric ? "C" : "F", + /* humidity */ (float) conditionData.getDouble("humidity"), + /* wind */ (float) windData.getDouble("speed"), + /* windDir */ windData.getInt("deg"), + /* speedUnit */ mContext.getString(speedUnitResId), + forecasts, + System.currentTimeMillis()); + + Log.d(TAG, "Weather updated: " + w); + return w; + } catch (JSONException e) { + Log.w(TAG, "Received malformed weather data (selection = " + selection + + ", lang = " + locale + ")", e); + } + + return null; + } + + private ArrayList<DayForecast> parseForecasts(JSONArray forecasts) throws JSONException { + ArrayList<DayForecast> result = new ArrayList<DayForecast>(); + int count = forecasts.length(); + + if (count == 0) { + throw new JSONException("Empty forecasts array"); + } + for (int i = 0; i < count; i++) { + JSONObject forecast = forecasts.getJSONObject(i); + JSONObject temperature = forecast.getJSONObject("temp"); + JSONObject data = forecast.getJSONArray("weather").getJSONObject(0); + DayForecast item = new DayForecast( + /* low */ (float) temperature.getDouble("min"), + /* high */ (float) temperature.getDouble("max"), + /* condition */ data.getString("main"), + /* conditionCode */ mapConditionIconToCode( + data.getString("icon"), data.getInt("id"))); + result.add(item); + } + + return result; + } + + private static final HashMap<String, Integer> ICON_MAPPING = new HashMap<String, Integer>(); + static { + ICON_MAPPING.put("01d", 32); + ICON_MAPPING.put("01n", 31); + ICON_MAPPING.put("02d", 30); + ICON_MAPPING.put("02n", 29); + ICON_MAPPING.put("03d", 26); + ICON_MAPPING.put("03n", 26); + ICON_MAPPING.put("04d", 28); + ICON_MAPPING.put("04n", 27); + ICON_MAPPING.put("09d", 12); + ICON_MAPPING.put("09n", 11); + ICON_MAPPING.put("10d", 40); + ICON_MAPPING.put("10n", 45); + ICON_MAPPING.put("11d", 4); + ICON_MAPPING.put("11n", 4); + ICON_MAPPING.put("13d", 16); + ICON_MAPPING.put("13n", 16); + ICON_MAPPING.put("50d", 21); + ICON_MAPPING.put("50n", 20); + } + + private int mapConditionIconToCode(String icon, int conditionId) { + + // First, use condition ID for specific cases + switch (conditionId) { + // Thunderstorms + case 202: // thunderstorm with heavy rain + case 232: // thunderstorm with heavy drizzle + case 211: // thunderstorm + return 4; + case 212: // heavy thunderstorm + return 3; + case 221: // ragged thunderstorm + case 231: // thunderstorm with drizzle + case 201: // thunderstorm with rain + return 38; + case 230: // thunderstorm with light drizzle + case 200: // thunderstorm with light rain + case 210: // light thunderstorm + return 37; + + // Drizzle + case 300: // light intensity drizzle + case 301: // drizzle + case 302: // heavy intensity drizzle + case 310: // light intensity drizzle rain + case 311: // drizzle rain + case 312: // heavy intensity drizzle rain + case 313: // shower rain and drizzle + case 314: // heavy shower rain and drizzle + case 321: // shower drizzle + return 9; + + // Rain + case 500: // light rain + case 501: // moderate rain + case 520: // light intensity shower rain + case 521: // shower rain + case 531: // ragged shower rain + return 11; + case 502: // heavy intensity rain + case 503: // very heavy rain + case 504: // extreme rain + case 522: // heavy intensity shower rain + return 12; + case 511: // freezing rain + return 10; + + // Snow + case 600: case 620: return 14; // light snow + case 601: case 621: return 16; // snow + case 602: case 622: return 41; // heavy snow + case 611: case 612: return 18; // sleet + case 615: case 616: return 5; // rain and snow + + // Atmosphere + case 741: // fog + return 20; + case 711: // smoke + case 762: // volcanic ash + return 22; + case 701: // mist + case 721: // haze + return 21; + case 731: // sand/dust whirls + case 751: // sand + case 761: // dust + return 19; + case 771: // squalls + return 23; + case 781: // tornado + return 0; + + // Extreme + case 900: return 0; // tornado + case 901: return 1; // tropical storm + case 902: return 2; // hurricane + case 903: return 25; // cold + case 904: return 36; // hot + case 905: return 24; // windy + case 906: return 17; // hail + } + + // Not yet handled - Use generic icon mapping + Integer condition = ICON_MAPPING.get(icon); + if (condition != null) { + return condition; + } + + return -1; + } + + private static final HashMap<String, String> LANGUAGE_CODE_MAPPING = new HashMap<String, String>(); + static { + LANGUAGE_CODE_MAPPING.put("bg-", "bg"); + LANGUAGE_CODE_MAPPING.put("de-", "de"); + LANGUAGE_CODE_MAPPING.put("es-", "sp"); + LANGUAGE_CODE_MAPPING.put("fi-", "fi"); + LANGUAGE_CODE_MAPPING.put("fr-", "fr"); + LANGUAGE_CODE_MAPPING.put("it-", "it"); + LANGUAGE_CODE_MAPPING.put("nl-", "nl"); + LANGUAGE_CODE_MAPPING.put("pl-", "pl"); + LANGUAGE_CODE_MAPPING.put("pt-", "pt"); + LANGUAGE_CODE_MAPPING.put("ro-", "ro"); + LANGUAGE_CODE_MAPPING.put("ru-", "ru"); + LANGUAGE_CODE_MAPPING.put("se-", "se"); + LANGUAGE_CODE_MAPPING.put("tr-", "tr"); + LANGUAGE_CODE_MAPPING.put("uk-", "ua"); + LANGUAGE_CODE_MAPPING.put("zh-CN", "zh_cn"); + LANGUAGE_CODE_MAPPING.put("zh-TW", "zh_tw"); + } + private String getLanguageCode() { + Locale locale = mContext.getResources().getConfiguration().locale; + String selector = locale.getLanguage() + "-" + locale.getCountry(); + + for (Map.Entry<String, String> entry : LANGUAGE_CODE_MAPPING.entrySet()) { + if (selector.startsWith(entry.getKey())) { + return entry.getValue(); + } + } + + return "en"; + } +} diff --git a/src/com/cyanogenmod/lockclock/weather/WeatherInfo.java b/src/com/cyanogenmod/lockclock/weather/WeatherInfo.java index a857734..b7c09d5 100755 --- a/src/com/cyanogenmod/lockclock/weather/WeatherInfo.java +++ b/src/com/cyanogenmod/lockclock/weather/WeatherInfo.java @@ -21,9 +21,10 @@ import android.content.res.Resources; import android.graphics.Bitmap; import com.cyanogenmod.lockclock.R; -import com.cyanogenmod.lockclock.misc.WidgetUtils; +import com.cyanogenmod.lockclock.misc.IconUtils; import java.text.DecimalFormat; +import java.util.ArrayList; import java.util.Date; public class WeatherInfo { @@ -33,27 +34,24 @@ public class WeatherInfo { private String id; private String city; - private String forecastDate; private String condition; private int conditionCode; private float temperature; - private float lowTemperature; - private float highTemperature; private String tempUnit; private float humidity; private float wind; private int windDirection; private String speedUnit; private long timestamp; + private ArrayList<DayForecast> forecasts; public WeatherInfo(Context context, String id, - String city, String fdate, String condition, int conditionCode, - float temp, float low, float high, String tempUnit, float humidity, - float wind, int windDir, String speedUnit, long timestamp) { + String city, String condition, int conditionCode, float temp, + String tempUnit, float humidity, float wind, int windDir, + String speedUnit, ArrayList<DayForecast> forecasts, long timestamp) { this.mContext = context.getApplicationContext(); this.id = id; this.city = city; - this.forecastDate = fdate; this.condition = condition; this.conditionCode = conditionCode; this.humidity = humidity; @@ -62,27 +60,57 @@ public class WeatherInfo { this.speedUnit = speedUnit; this.timestamp = timestamp; this.temperature = temp; - this.lowTemperature = low; - this.highTemperature = high; this.tempUnit = tempUnit; + this.forecasts = forecasts; } - public int getConditionResource() { - final Resources res = mContext.getResources(); - final int resId = res.getIdentifier("weather2_" + conditionCode, "drawable", mContext.getPackageName()); - if (resId != 0) { - return resId; + public static class DayForecast { + public final float low, high; + public final int conditionCode; + public final String condition; + + public DayForecast(float low, float high, String condition, int conditionCode) { + this.low = low; + this.high = high; + this.condition = condition; + this.conditionCode = conditionCode; + } + + public String getFormattedLow() { + return getFormattedValue(low, "\u00b0"); } - return R.drawable.weather2_na; - } - public Bitmap getConditionBitmap(int color) { - final Resources res = mContext.getResources(); - int resId = res.getIdentifier("weather_" + conditionCode, "drawable", mContext.getPackageName()); - if (resId == 0) { - resId = R.drawable.weather_na; + public String getFormattedHigh() { + return getFormattedValue(high, "\u00b0"); } - return WidgetUtils.getOverlaidBitmap(mContext, resId, color); + + public int getConditionResource(Context context, String set) { + return IconUtils.getWeatherIconResource(context, set, conditionCode); + } + + public Bitmap getConditionBitmap(Context context, String set, int color) { + return IconUtils.getWeatherIconBitmap(context, set, color, conditionCode); + } + + public Bitmap getConditionBitmap(Context context, String set, int color, int density) { + return IconUtils.getWeatherIconBitmap(context, set, color, conditionCode, density); + } + + public String getCondition(Context context) { + return WeatherInfo.getCondition(context, conditionCode, condition); + } + } + + public int getConditionResource(String set) { + return IconUtils.getWeatherIconResource(mContext, set, conditionCode); + } + + public Bitmap getConditionBitmap(String set, int color) { + return IconUtils.getWeatherIconBitmap(mContext, set, color, conditionCode); + } + + public Bitmap getConditionBitmap(String set, int color, int density) { + return IconUtils.getWeatherIconBitmap(mContext, set, color, conditionCode, density); } public String getId() { @@ -94,8 +122,12 @@ public class WeatherInfo { } public String getCondition() { - final Resources res = mContext.getResources(); - final int resId = res.getIdentifier("weather_" + conditionCode, "string", mContext.getPackageName()); + return getCondition(mContext, conditionCode, condition); + } + + private static String getCondition(Context context, int conditionCode, String condition) { + final Resources res = context.getResources(); + final int resId = res.getIdentifier("weather_" + conditionCode, "string", context.getPackageName()); if (resId != 0) { return res.getString(resId); } @@ -106,23 +138,27 @@ public class WeatherInfo { return new Date(timestamp); } - private String getFormattedValue(float value, String unit) { - if (Float.isNaN(highTemperature)) { + private static String getFormattedValue(float value, String unit) { + if (Float.isNaN(value)) { return "-"; } - return sNoDigitsFormat.format(value) + unit; + String formatted = sNoDigitsFormat.format(value); + if (formatted.equals("-0")) { + formatted = "0"; + } + return formatted + unit; } public String getFormattedTemperature() { - return getFormattedValue(temperature, "°" + tempUnit); + return getFormattedValue(temperature, "\u00b0" + tempUnit); } public String getFormattedLow() { - return getFormattedValue(lowTemperature, "°"); + return forecasts.get(0).getFormattedLow(); } public String getFormattedHigh() { - return getFormattedValue(highTemperature, "°"); + return forecasts.get(0).getFormattedHigh(); } public String getFormattedHumidity() { @@ -153,6 +189,10 @@ public class WeatherInfo { return mContext.getString(resId); } + public ArrayList<DayForecast> getForecasts() { + return forecasts; + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); @@ -178,6 +218,20 @@ public class WeatherInfo { builder.append(getFormattedWindSpeed()); builder.append(" at "); builder.append(getWindDirection()); + if (forecasts.size() > 0) { + builder.append(", forecasts:"); + } + for (int i = 0; i < forecasts.size(); i++) { + DayForecast d = forecasts.get(i); + if (i != 0) { + builder.append(";"); + } + builder.append(" day ").append(i + 1).append(": "); + builder.append("high ").append(d.getFormattedHigh()); + builder.append(", low ").append(d.getFormattedLow()); + builder.append(", ").append(d.condition); + builder.append("(").append(d.conditionCode).append(")"); + } return builder.toString(); } @@ -185,52 +239,88 @@ public class WeatherInfo { StringBuilder builder = new StringBuilder(); builder.append(id).append('|'); builder.append(city).append('|'); - builder.append(forecastDate).append('|'); builder.append(condition).append('|'); builder.append(conditionCode).append('|'); builder.append(temperature).append('|'); - builder.append(lowTemperature).append('|'); - builder.append(highTemperature).append('|'); builder.append(tempUnit).append('|'); builder.append(humidity).append('|'); builder.append(wind).append('|'); builder.append(windDirection).append('|'); builder.append(speedUnit).append('|'); - builder.append(timestamp); + builder.append(timestamp).append('|'); + serializeForecasts(builder); return builder.toString(); } + private void serializeForecasts(StringBuilder builder) { + builder.append(forecasts.size()); + for (DayForecast d : forecasts) { + builder.append(';'); + builder.append(d.high).append(';'); + builder.append(d.low).append(';'); + builder.append(d.condition).append(';'); + builder.append(d.conditionCode); + } + } + public static WeatherInfo fromSerializedString(Context context, String input) { if (input == null) { return null; } String[] parts = input.split("\\|"); - if (parts == null || parts.length != 14) { + if (parts == null || parts.length != 12) { return null; } int conditionCode, windDirection; long timestamp; - float temperature, low, high, humidity, wind; + float temperature, humidity, wind; + String[] forecastParts = parts[11].split(";"); + int forecastItems; + ArrayList<DayForecast> forecasts = new ArrayList<DayForecast>(); + // Parse the core data try { - conditionCode = Integer.parseInt(parts[4]); - temperature = Float.parseFloat(parts[5]); - low = Float.parseFloat(parts[6]); - high = Float.parseFloat(parts[7]); - humidity = Float.parseFloat(parts[9]); - wind = Float.parseFloat(parts[10]); - windDirection = Integer.parseInt(parts[11]); - timestamp = Long.parseLong(parts[13]); + conditionCode = Integer.parseInt(parts[3]); + temperature = Float.parseFloat(parts[4]); + humidity = Float.parseFloat(parts[6]); + wind = Float.parseFloat(parts[7]); + windDirection = Integer.parseInt(parts[8]); + timestamp = Long.parseLong(parts[10]); + forecastItems = forecastParts == null ? 0 : Integer.parseInt(forecastParts[0]); } catch (NumberFormatException e) { return null; } + if (forecastItems == 0 || forecastParts.length != 4 * forecastItems + 1) { + return null; + } + + // Parse the forecast data + try { + for (int item = 0; item < forecastItems; item ++) { + int offset = item * 4 + 1; + DayForecast day = new DayForecast( + /* low */ Float.parseFloat(forecastParts[offset + 1]), + /* high */ Float.parseFloat(forecastParts[offset]), + /* condition */ forecastParts[offset + 2], + /* conditionCode */ Integer.parseInt(forecastParts[offset + 3])); + if (!Float.isNaN(day.low) && !Float.isNaN(day.high) && day.conditionCode >= 0) { + forecasts.add(day); + } + } + } catch (NumberFormatException ignored) { + } + + if (forecasts.isEmpty()) { + return null; + } + return new WeatherInfo(context, - /* id */ parts[0], /* city */ parts[1], /* date */ parts[2], - /* condition */ parts[3], conditionCode, temperature, low, high, - /* tempUnit */ parts[8], humidity, wind, windDirection, - /* speedUnit */ parts[12], timestamp); + /* id */ parts[0], /* city */ parts[1], /* condition */ parts[2], + conditionCode, temperature, /* tempUnit */ parts[5], + humidity, wind, windDirection, /* speedUnit */ parts[9], + /* forecasts */ forecasts, timestamp); } } diff --git a/src/com/cyanogenmod/lockclock/weather/WeatherProvider.java b/src/com/cyanogenmod/lockclock/weather/WeatherProvider.java index 15c8aff..70fbf42 100644 --- a/src/com/cyanogenmod/lockclock/weather/WeatherProvider.java +++ b/src/com/cyanogenmod/lockclock/weather/WeatherProvider.java @@ -27,11 +27,13 @@ public interface WeatherProvider { public String postal; public String countryId; public String country; - }; + } List<LocationResult> getLocations(String input); - WeatherInfo getWeatherInfo(String id, String localizedCityName); + WeatherInfo getWeatherInfo(String id, String localizedCityName, boolean metricUnits); - WeatherInfo getWeatherInfo(Location location); -}; + WeatherInfo getWeatherInfo(Location location, boolean metricUnits); + + int getNameResourceId(); +} diff --git a/src/com/cyanogenmod/lockclock/weather/WeatherUpdateService.java b/src/com/cyanogenmod/lockclock/weather/WeatherUpdateService.java index 94fca71..f38c046 100755 --- a/src/com/cyanogenmod/lockclock/weather/WeatherUpdateService.java +++ b/src/com/cyanogenmod/lockclock/weather/WeatherUpdateService.java @@ -26,19 +26,19 @@ import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.location.LocationProvider; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; +import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import com.cyanogenmod.lockclock.ClockWidgetProvider; import com.cyanogenmod.lockclock.misc.Constants; import com.cyanogenmod.lockclock.misc.Preferences; +import com.cyanogenmod.lockclock.misc.WidgetUtils; import java.util.Date; @@ -47,6 +47,15 @@ public class WeatherUpdateService extends Service { private static final boolean D = Constants.DEBUG; public static final String ACTION_FORCE_UPDATE = "com.cyanogenmod.lockclock.action.FORCE_WEATHER_UPDATE"; + private static final String ACTION_CANCEL_LOCATION_UPDATE = + "com.cyanogenmod.lockclock.action.CANCEL_LOCATION_UPDATE"; + + // Broadcast action for end of update + public static final String ACTION_UPDATE_FINISHED = "com.cyanogenmod.lockclock.action.WEATHER_UPDATE_FINISHED"; + public static final String EXTRA_UPDATE_CANCELLED = "update_cancelled"; + + private static final long LOCATION_REQUEST_TIMEOUT = 5L * 60L * 1000L; // request for at most 5 minutes + private static final long OUTDATED_LOCATION_THRESHOLD_MILLIS = 10L * 60L * 1000L; // 10 minutes private WeatherUpdateTask mTask; @@ -62,18 +71,26 @@ public class WeatherUpdateService extends Service { public int onStartCommand(Intent intent, int flags, int startId) { if (D) Log.v(TAG, "Got intent " + intent); - if (mTask != null && mTask.getStatus() != AsyncTask.Status.FINISHED) { + boolean active = mTask != null && mTask.getStatus() != AsyncTask.Status.FINISHED; + + if (ACTION_CANCEL_LOCATION_UPDATE.equals(intent.getAction())) { + WeatherLocationListener.cancel(this); + if (!active) { + stopSelf(); + } + return START_NOT_STICKY; + } + + if (active) { if (D) Log.v(TAG, "Weather update is still active, not starting new update"); return START_REDELIVER_INTENT; } boolean force = ACTION_FORCE_UPDATE.equals(intent.getAction()); - if (force) { - Preferences.setCachedWeatherInfo(this, 0, null); - } if (!shouldUpdate(force)) { Log.d(TAG, "Service started, but shouldn't update ... stopping"); stopSelf(); + sendCancelledBroadcast(); return START_NOT_STICKY; } @@ -83,6 +100,12 @@ public class WeatherUpdateService extends Service { return START_REDELIVER_INTENT; } + private void sendCancelledBroadcast() { + Intent finishedIntent = new Intent(ACTION_UPDATE_FINISHED); + finishedIntent.putExtra(EXTRA_UPDATE_CANCELLED, true); + sendBroadcast(finishedIntent); + } + @Override public IBinder onBind(Intent intent) { return null; @@ -97,25 +120,16 @@ public class WeatherUpdateService extends Service { } private boolean shouldUpdate(boolean force) { - ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); - NetworkInfo info = cm.getActiveNetworkInfo(); - - if (info == null || !info.isConnected()) { - if (D) Log.d(TAG, "No network connection is available for weather update"); - return false; - } - - if (!Preferences.showWeather(this)) { - if (D) Log.v(TAG, "Weather isn't shown, skip update"); - return false; - } - long interval = Preferences.weatherRefreshIntervalInMs(this); if (interval == 0 && !force) { if (D) Log.v(TAG, "Interval set to manual and update not forced, skip update"); return false; } + if (force) { + Preferences.setCachedWeatherInfo(this, 0, null); + } + long now = System.currentTimeMillis(); long lastUpdate = Preferences.lastWeatherUpdateTimestamp(this); long due = lastUpdate + interval; @@ -127,7 +141,7 @@ public class WeatherUpdateService extends Service { return false; } - return true; + return WidgetUtils.isNetworkAvailable(this); } private class WeatherUpdateTask extends AsyncTask<Void, Void, WeatherInfo> { @@ -138,6 +152,7 @@ public class WeatherUpdateService extends Service { if (D) Log.d(TAG, "Starting weather update task"); PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mWakeLock.setReferenceCounted(false); mContext = WeatherUpdateService.this; } @@ -151,12 +166,32 @@ public class WeatherUpdateService extends Service { LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE); Location location = lm.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER); if (D) Log.v(TAG, "Current location is " + location); + + // If lastKnownLocation is not present (because none of the apps in the + // device has requested the current location to the system yet) or outdated, + // then try to get the current location use the provider that best matches the criteria. + boolean needsUpdate = location == null; + if (location != null) { + long delta = System.currentTimeMillis() - location.getTime(); + needsUpdate = delta > OUTDATED_LOCATION_THRESHOLD_MILLIS; + } + if (needsUpdate) { + if (D) Log.d(TAG, "Getting best location provider"); + String locationProvider = lm.getBestProvider(sLocationCriteria, true); + if (TextUtils.isEmpty(locationProvider)) { + Log.e(TAG, "No available location providers matching criteria."); + } else { + WeatherLocationListener.registerIfNeeded(mContext, locationProvider); + } + } + return location; } @Override protected WeatherInfo doInBackground(Void... params) { - WeatherProvider provider = new YahooWeatherProvider(mContext); + WeatherProvider provider = Preferences.weatherProvider(mContext); + boolean metric = Preferences.useMetricUnits(mContext); String customLocationId = null, customLocationName = null; if (Preferences.useCustomWeatherLocation(mContext)) { @@ -165,31 +200,22 @@ public class WeatherUpdateService extends Service { } if (customLocationId != null) { - return provider.getWeatherInfo(customLocationId, customLocationName); + return provider.getWeatherInfo(customLocationId, customLocationName, metric); } Location location = getCurrentLocation(); if (location != null) { - WeatherInfo info = provider.getWeatherInfo(location); + WeatherInfo info = provider.getWeatherInfo(location, metric); if (info != null) { return info; } } + // work with cached location from last request for now + // a listener to update it is already scheduled if possible WeatherInfo cachedInfo = Preferences.getCachedWeatherInfo(mContext); if (cachedInfo != null) { - return provider.getWeatherInfo(cachedInfo.getId(), cachedInfo.getCity()); - } - // If lastKnownLocation is not present because none of the apps in the - // device has requested the current location to the system yet, then try to - // get the current location use the provider that best matches the criteria. - if (D) Log.d(TAG, "Getting best location provider"); - LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE); - String locationProvider = lm.getBestProvider(sLocationCriteria, true); - if (TextUtils.isEmpty(locationProvider)) { - Log.e(TAG, "No available location providers matching criteria."); - } else { - WeatherLocationListener.registerIfNeeded(mContext, locationProvider); + return provider.getWeatherInfo(cachedInfo.getId(), cachedInfo.getCity(), metric); } return null; @@ -224,6 +250,10 @@ public class WeatherUpdateService extends Service { scheduleUpdate(mContext, interval, false); } + Intent finishedIntent = new Intent(ACTION_UPDATE_FINISHED); + finishedIntent.putExtra(EXTRA_UPDATE_CANCELLED, result == null); + sendBroadcast(finishedIntent); + if (D) Log.d(TAG, "RELEASING WAKELOCK"); mWakeLock.release(); stopSelf(); @@ -232,6 +262,7 @@ public class WeatherUpdateService extends Service { private static class WeatherLocationListener implements LocationListener { private Context mContext; + private PendingIntent mTimeoutIntent; private static WeatherLocationListener sInstance = null; static void registerIfNeeded(Context context, String provider) { @@ -248,35 +279,79 @@ public class WeatherUpdateService extends Service { // Check whether the provider is supported. // NOTE!!! Actually only WeatherUpdateService class is calling this function // with the NETWORK_PROVIDER, so setting the instance is safe. We must - // change this if this call receive differents providers + // change this if this call receive different providers LocationProvider lp = locationManager.getProvider(provider); if (lp != null) { if (D) Log.d(TAG, "LocationManager - Requesting single update"); locationManager.requestSingleUpdate(provider, sInstance, appContext.getMainLooper()); + sInstance.setTimeoutAlarm(); } } } } + static void cancel(Context context) { + synchronized (WeatherLocationListener.class) { + if (sInstance != null) { + final Context appContext = context.getApplicationContext(); + final LocationManager locationManager = + (LocationManager) appContext.getSystemService(Context.LOCATION_SERVICE); + if (D) Log.d(TAG, "Aborting location request after timeout"); + locationManager.removeUpdates(sInstance); + sInstance.cancelTimeoutAlarm(); + sInstance = null; + } + } + } + private WeatherLocationListener(Context context) { super(); mContext = context; } + private void setTimeoutAlarm() { + Intent intent = new Intent(mContext, WeatherUpdateService.class); + intent.setAction(ACTION_CANCEL_LOCATION_UPDATE); + + mTimeoutIntent = PendingIntent.getService(mContext, 0, intent, + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT); + + AlarmManager am = (AlarmManager) mContext.getSystemService(ALARM_SERVICE); + long elapseTime = SystemClock.elapsedRealtime() + LOCATION_REQUEST_TIMEOUT; + am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, elapseTime, mTimeoutIntent); + } + + private void cancelTimeoutAlarm() { + if (mTimeoutIntent != null) { + AlarmManager am = (AlarmManager) mContext.getSystemService(ALARM_SERVICE); + am.cancel(mTimeoutIntent); + mTimeoutIntent = null; + } + } + @Override public void onLocationChanged(Location location) { // Now, we have a location to use. Schedule a weather update right now. if (D) Log.d(TAG, "The location has changed, schedule an update "); synchronized (WeatherLocationListener.class) { WeatherUpdateService.scheduleUpdate(mContext, 0, true); + cancelTimeoutAlarm(); sInstance = null; } } @Override public void onStatusChanged(String provider, int status, Bundle extras) { - // Not used + // Now, we have a location to use. Schedule a weather update right now. + if (D) Log.d(TAG, "The location service has become available, schedule an update "); + if (status == LocationProvider.AVAILABLE) { + synchronized (WeatherLocationListener.class) { + WeatherUpdateService.scheduleUpdate(mContext, 0, true); + cancelTimeoutAlarm(); + sInstance = null; + } + } } @Override @@ -298,9 +373,9 @@ public class WeatherUpdateService extends Service { am.set(AlarmManager.RTC_WAKEUP, due, getUpdateIntent(context, force)); } - public static void scheduleNextUpdate(Context context) { + public static void scheduleNextUpdate(Context context, boolean force) { long lastUpdate = Preferences.lastWeatherUpdateTimestamp(context); - if (lastUpdate == 0) { + if (lastUpdate == 0 || force) { scheduleUpdate(context, 0, true); } else { long interval = Preferences.weatherRefreshIntervalInMs(context); @@ -320,5 +395,6 @@ public class WeatherUpdateService extends Service { AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); am.cancel(getUpdateIntent(context, true)); am.cancel(getUpdateIntent(context, false)); + WeatherLocationListener.cancel(context); } } diff --git a/src/com/cyanogenmod/lockclock/weather/YahooWeatherProvider.java b/src/com/cyanogenmod/lockclock/weather/YahooWeatherProvider.java index dc714cb..5abe35d 100644 --- a/src/com/cyanogenmod/lockclock/weather/YahooWeatherProvider.java +++ b/src/com/cyanogenmod/lockclock/weather/YahooWeatherProvider.java @@ -19,9 +19,12 @@ package com.cyanogenmod.lockclock.weather; import android.content.Context; import android.location.Location; import android.net.Uri; +import android.text.Html; +import android.text.TextUtils; import android.util.Log; -import com.cyanogenmod.lockclock.misc.Preferences; +import com.cyanogenmod.lockclock.weather.WeatherInfo.DayForecast; +import com.cyanogenmod.lockclock.R; import org.json.JSONArray; import org.json.JSONException; @@ -51,7 +54,7 @@ public class YahooWeatherProvider implements WeatherProvider { Uri.encode("select woeid, postal, admin1, admin2, admin3, " + "locality1, locality2, country from geo.places where " + "(placetype = 7 or placetype = 8 or placetype = 9 " + - "or placetype = 10 or placetype = 11) and text ="); + "or placetype = 10 or placetype = 11 or placetype = 20) and text ="); private static final String URL_PLACEFINDER = "http://query.yahooapis.com/v1/public/yql?format=json&q=" + Uri.encode("select woeid, city from geo.placefinder where gflags=\"R\" and text ="); @@ -67,9 +70,14 @@ public class YahooWeatherProvider implements WeatherProvider { } @Override + public int getNameResourceId() { + return R.string.weather_source_yahoo; + } + + @Override public List<LocationResult> getLocations(String input) { - String locale = mContext.getResources().getConfiguration().locale.getCountry(); - String params = "\"" + input + "\" and lang = \"" + locale + "\""; + String language = getLanguage(); + String params = "\"" + input + "\" and lang = \"" + language + "\""; String url = URL_LOCATION + Uri.encode(params); JSONObject jsonResults = fetchResults(url); if (jsonResults == null) { @@ -93,14 +101,14 @@ public class YahooWeatherProvider implements WeatherProvider { } return results; } catch (JSONException e) { - Log.e(TAG, "Received malformed places data", e); + Log.e(TAG, "Received malformed places data (input=" + input + ", lang=" + language + ")", e); } return null; } - public WeatherInfo getWeatherInfo(String id, String localizedCityName) { - String unit = Preferences.useMetricUnits(mContext) ? "c" : "f"; - String url = String.format(URL_WEATHER, id, unit); + @Override + public WeatherInfo getWeatherInfo(String id, String localizedCityName, boolean metric) { + String url = String.format(URL_WEATHER, id, metric ? "c" : "f"); String response = HttpRetriever.retrieve(url); if (response == null) { @@ -115,22 +123,31 @@ public class YahooWeatherProvider implements WeatherProvider { parser.parse(new InputSource(reader), handler); if (handler.isComplete()) { + // There are cases where the current condition is unknown, but the forecast + // is not - using the (inaccurate) forecast is probably better than showing + // the question mark + if (handler.conditionCode == 3200) { + handler.condition = handler.forecasts.get(0).condition; + handler.conditionCode = handler.forecasts.get(0).conditionCode; + } + WeatherInfo w = new WeatherInfo(mContext, id, - localizedCityName != null ? localizedCityName : handler.city, null, + localizedCityName != null ? localizedCityName : handler.city, handler.condition, handler.conditionCode, handler.temperature, - handler.forecasts.get(0).low, handler.forecasts.get(0).high, handler.temperatureUnit, handler.humidity, handler.windSpeed, - handler.windDirection, handler.speedUnit, + handler.windDirection, handler.speedUnit, handler.forecasts, System.currentTimeMillis()); Log.d(TAG, "Weather updated: " + w); return w; + } else { + Log.w(TAG, "Received incomplete weather XML (id=" + id + ")"); } } catch (ParserConfigurationException e) { Log.e(TAG, "Could not create XML parser", e); } catch (SAXException e) { - Log.e(TAG, "Could not parse weather XML", e); + Log.e(TAG, "Could not parse weather XML (id=" + id + ")", e); } catch (IOException e) { - Log.e(TAG, "Could not parse weather XML", e); + Log.e(TAG, "Could not parse weather XML (id=" + id + ")", e); } return null; @@ -144,12 +161,6 @@ public class YahooWeatherProvider implements WeatherProvider { String condition; ArrayList<DayForecast> forecasts = new ArrayList<DayForecast>(); - private static class DayForecast { - float low, high; - int conditionCode; - String condition; - } - @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { @@ -168,11 +179,11 @@ public class YahooWeatherProvider implements WeatherProvider { conditionCode = (int) stringToFloat(attributes.getValue("code"), -1); temperature = stringToFloat(attributes.getValue("temp"), Float.NaN); } else if (qName.equals("yweather:forecast")) { - DayForecast day = new DayForecast(); - day.low = stringToFloat(attributes.getValue("low"), Float.NaN); - day.high = stringToFloat(attributes.getValue("high"), Float.NaN); - day.condition = attributes.getValue("text"); - day.conditionCode = (int) stringToFloat(attributes.getValue("code"), -1); + DayForecast day = new DayForecast( + /* low */ stringToFloat(attributes.getValue("low"), Float.NaN), + /* high */ stringToFloat(attributes.getValue("high"), Float.NaN), + /* condition */ attributes.getValue("text"), + /* conditionCode */ (int) stringToFloat(attributes.getValue("code"), -1)); if (!Float.isNaN(day.low) && !Float.isNaN(day.high) && day.conditionCode >= 0) { forecasts.add(day); } @@ -194,10 +205,11 @@ public class YahooWeatherProvider implements WeatherProvider { } } - public WeatherInfo getWeatherInfo(Location location) { - String locale = mContext.getResources().getConfiguration().locale.getCountry(); - String params = String.format(Locale.US, "\"%f %f\" and lang=\"%s\"", - location.getLatitude(), location.getLongitude(), locale); + @Override + public WeatherInfo getWeatherInfo(Location location, boolean metric) { + String language = getLanguage(); + String params = String.format(Locale.US, "\"%f %f\" and locale=\"%s\"", + location.getLatitude(), location.getLongitude(), language); String url = URL_PLACEFINDER + Uri.encode(params); JSONObject results = fetchResults(url); if (results == null) { @@ -209,17 +221,24 @@ public class YahooWeatherProvider implements WeatherProvider { String woeid = result.getString("woeid"); String city = result.getString("city"); + if (city == null) { + city = result.getString("neighborhood"); + } + + // The city name in the placefinder result is HTML encoded :-( + if (city != null) { + city = Html.fromHtml(city).toString(); + } + Log.d(TAG, "Resolved location " + location + " to " + city + " (" + woeid + ")"); - WeatherInfo info = getWeatherInfo(woeid, city); + WeatherInfo info = getWeatherInfo(woeid, city, metric); if (info != null) { - // cache the result for potential reuse - // (the placefinder service API is rate limited) - Preferences.setCachedLocationId(mContext, woeid); return info; } } catch (JSONException e) { - Log.e(TAG, "Received malformed placefinder data", e); + Log.e(TAG, "Received malformed placefinder data (location=" + + location + ", lang=" + language + ")", e); } return null; @@ -243,6 +262,11 @@ public class YahooWeatherProvider implements WeatherProvider { } } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "JSON data " + place.toString() + " -> id=" + result.id + + ", city=" + result.city + ", country=" + result.countryId); + } + if (result.id == null || result.city == null || result.countryId == null) { return null; } @@ -264,9 +288,20 @@ public class YahooWeatherProvider implements WeatherProvider { JSONObject rootObject = new JSONObject(response); return rootObject.getJSONObject("query").getJSONObject("results"); } catch (JSONException e) { - Log.w(TAG, "Received malformed places data", e); + Log.w(TAG, "Received malformed places data (url=" + url + ")", e); } return null; } -}; + + private String getLanguage() { + Locale locale = mContext.getResources().getConfiguration().locale; + String country = locale.getCountry(); + String language = locale.getLanguage(); + + if (TextUtils.isEmpty(country)) { + return language; + } + return language + "-" + country; + } +} |