diff options
Diffstat (limited to 'src/com/cyanogenmod/lockclock/weather')
11 files changed, 671 insertions, 1446 deletions
diff --git a/src/com/cyanogenmod/lockclock/weather/ForecastActivity.java b/src/com/cyanogenmod/lockclock/weather/ForecastActivity.java index b08a069..70fe61c 100644 --- a/src/com/cyanogenmod/lockclock/weather/ForecastActivity.java +++ b/src/com/cyanogenmod/lockclock/weather/ForecastActivity.java @@ -18,13 +18,10 @@ 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; @@ -35,11 +32,10 @@ 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.R; import com.cyanogenmod.lockclock.misc.Preferences; import com.cyanogenmod.lockclock.misc.WidgetUtils; -import com.cyanogenmod.lockclock.R; +import cyanogenmod.weather.WeatherInfo; public class ForecastActivity extends Activity implements OnClickListener { private static final String TAG = "ForecastActivity"; diff --git a/src/com/cyanogenmod/lockclock/weather/ForecastBuilder.java b/src/com/cyanogenmod/lockclock/weather/ForecastBuilder.java index f5e4e6d..4bb2b84 100644 --- a/src/com/cyanogenmod/lockclock/weather/ForecastBuilder.java +++ b/src/com/cyanogenmod/lockclock/weather/ForecastBuilder.java @@ -16,29 +16,34 @@ 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.graphics.Color; 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.R; 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; +import static cyanogenmod.providers.WeatherContract.WeatherColumns.WindSpeedUnit.MPH; +import static cyanogenmod.providers.WeatherContract.WeatherColumns.WindSpeedUnit.KPH; +import static cyanogenmod.providers.WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT; +import static cyanogenmod.providers.WeatherContract.WeatherColumns.TempUnit.CELSIUS; +import cyanogenmod.weather.CMWeatherManager; +import cyanogenmod.weather.WeatherInfo; +import cyanogenmod.weather.WeatherInfo.DayForecast; +import cyanogenmod.weather.util.WeatherUtils; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; public class ForecastBuilder { private static final String TAG = "ForecastBuilder"; @@ -58,50 +63,82 @@ public class ForecastBuilder { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); int color = Preferences.weatherFontColor(context); boolean invertLowHigh = Preferences.invertLowHighTemperature(context); + final boolean useMetric = Preferences.useMetricUnits(context); + + //Make any conversion needed in case the data was not provided in the desired unit + double temp = w.getTemperature(); + double todaysLow = w.getTodaysLow(); + double todaysHigh = w.getTodaysHigh(); + int tempUnit = w.getTemperatureUnit(); + if (tempUnit == FAHRENHEIT && useMetric) { + temp = WeatherUtils.fahrenheitToCelsius(temp); + todaysLow = WeatherUtils.fahrenheitToCelsius(todaysLow); + todaysHigh = WeatherUtils.fahrenheitToCelsius(todaysHigh); + tempUnit = CELSIUS; + } else if (tempUnit == CELSIUS && !useMetric) { + temp = WeatherUtils.celsiusToFahrenheit(temp); + todaysLow = WeatherUtils.celsiusToFahrenheit(todaysLow); + todaysHigh = WeatherUtils.celsiusToFahrenheit(todaysHigh); + tempUnit = FAHRENHEIT; + } + + double windSpeed = w.getWindSpeed(); + int windSpeedUnit = w.getWindSpeedUnit(); + if (windSpeedUnit == MPH && useMetric) { + windSpeedUnit = KPH; + windSpeed = Utils.milesToKilometers(windSpeed); + } else if (windSpeedUnit == KPH && !useMetric) { + windSpeedUnit = MPH; + windSpeed = Utils.kilometersToMiles(windSpeed); + } 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()); + final CMWeatherManager cmWeatherManager = CMWeatherManager.getInstance(context); + String activeWeatherLabel = cmWeatherManager.getActiveWeatherServiceProviderLabel(); + weatherSource.setText(activeWeatherLabel != null ? activeWeatherLabel : ""); // 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))); + weatherImage.setImageBitmap(IconUtils.getWeatherIconBitmap(context, iconsSet, color, + w.getConditionCode(), IconUtils.getNextHigherDensity(context))); // Weather Condition TextView weatherCondition = (TextView) view.findViewById(R.id.weather_condition); - weatherCondition.setText(w.getCondition()); + weatherCondition.setText(Utils.resolveWeatherCondition(context, w.getConditionCode())); // Weather Temps TextView weatherTemp = (TextView) view.findViewById(R.id.weather_temp); - weatherTemp.setText(w.getFormattedTemperature()); + weatherTemp.setText(WeatherUtils.formatTemperature(temp, tempUnit)); // Humidity and Wind TextView weatherHumWind = (TextView) view.findViewById(R.id.weather_hum_wind); - weatherHumWind.setText(w.getFormattedHumidity() + ", " + w.getFormattedWindSpeed() + " " - + w.getWindDirection()); + weatherHumWind.setText(Utils.formatHumidity(w.getHumidity()) + ", " + + Utils.formatWindSpeed(context, windSpeed, windSpeedUnit) + " " + + Utils.resolveWindDirection(context, w.getWindDirection())); // City TextView city = (TextView) view.findViewById(R.id.weather_city); city.setText(w.getCity()); // Weather Update Time - Date lastUpdate = w.getTimestamp(); + Date lastUpdate = new Date(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); + updateTime.setVisibility( + Preferences.showWeatherTimestamp(context) ? View.VISIBLE : View.GONE); // Weather Temps Panel additional items - final String low = w.getFormattedLow(); - final String high = w.getFormattedHigh(); + final String low = WeatherUtils.formatTemperature(todaysLow, tempUnit); + final String high = WeatherUtils.formatTemperature(todaysHigh, tempUnit); TextView weatherLowHigh = (TextView) view.findViewById(R.id.weather_low_high); weatherLowHigh.setText(invertLowHigh ? high + " | " + low : low + " | " + high); @@ -113,6 +150,9 @@ public class ForecastBuilder { if (buildSmallPanel(context, forecastView, w)) { // Success, hide the progress container progressIndicator.setVisibility(View.GONE); + } else { + // TODO: Display a text notifying the user that the forecast data is not available + // rather than keeping the indicator spinning forever } return view; @@ -125,55 +165,72 @@ public class ForecastBuilder { * @param w = the Weather info object that contains the forecast data */ public static boolean buildSmallPanel(Context context, LinearLayout smallPanel, WeatherInfo w) { - if (smallPanel == null) { + 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); + // Get things ready + LayoutInflater inflater + = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + int color = Preferences.weatherFontColor(context); + boolean invertLowHigh = Preferences.invertLowHighTemperature(context); + final boolean useMetric = Preferences.useMetricUnits(context); - ArrayList<DayForecast> forecasts = w.getForecasts(); - if (forecasts == null || forecasts.size() <= 1) { + List<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) { + } + + TimeZone MyTimezone = TimeZone.getDefault(); + Calendar calendar = new GregorianCalendar(MyTimezone); + int weatherTempUnit = w.getTemperatureUnit(); + // 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); + final int resId = IconUtils.getWeatherIconResource(context, iconsSet, + d.getConditionCode()); + 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, + } else { + image.setImageBitmap(IconUtils.getWeatherIconBitmap(context, iconsSet, + color, d.getConditionCode())); + } + + // Temperatures + double lowTemp = d.getLow(); + double highTemp = d.getHigh(); + int tempUnit = weatherTempUnit; + if (weatherTempUnit == FAHRENHEIT && useMetric) { + lowTemp = WeatherUtils.fahrenheitToCelsius(lowTemp); + highTemp = WeatherUtils.fahrenheitToCelsius(highTemp); + tempUnit = CELSIUS; + } else if (weatherTempUnit == CELSIUS && !useMetric) { + lowTemp = WeatherUtils.celsiusToFahrenheit(lowTemp); + highTemp = WeatherUtils.celsiusToFahrenheit(highTemp); + tempUnit = FAHRENHEIT; + } + String dayLow = WeatherUtils.formatTemperature(lowTemp, tempUnit); + String dayHigh = WeatherUtils.formatTemperature(highTemp, tempUnit); + 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; + } + return true; } } diff --git a/src/com/cyanogenmod/lockclock/weather/HttpRetriever.java b/src/com/cyanogenmod/lockclock/weather/HttpRetriever.java deleted file mode 100755 index 60723fa..0000000 --- a/src/com/cyanogenmod/lockclock/weather/HttpRetriever.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2013 The CyanogenMod Project - * - * 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.util.Log; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.util.EntityUtils; - -import java.io.IOException; - -public class HttpRetriever { - private static final String TAG = "HttpRetriever"; - - public static String retrieve(String url) { - HttpGet request = new HttpGet(url); - try { - HttpResponse response = new DefaultHttpClient().execute(request); - HttpEntity entity = response.getEntity(); - if (entity != null) { - return EntityUtils.toString(entity); - } - } catch (IOException 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 deleted file mode 100644 index ee2f46f..0000000 --- a/src/com/cyanogenmod/lockclock/weather/OpenWeatherMapProvider.java +++ /dev/null @@ -1,330 +0,0 @@ -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 APP_ID = "e2b075d68c39dc43e16995653fcd6fd0"; - - private static final String URL_LOCATION = - "http://api.openweathermap.org/data/2.5/find?q=%s&mode=json&lang=%s&appid=" - + APP_ID; - private static final String URL_WEATHER = - "http://api.openweathermap.org/data/2.5/weather?%s&mode=json&units=%s&lang=%s&appid=" - + APP_ID; - 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 + "&appid=" + APP_ID; - - 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"), metric); - 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 */ sanitizeTemperature(conditionData.getDouble("temp"), metric), - /* 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, boolean metric) 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 */ sanitizeTemperature(temperature.getDouble("min"), metric), - /* high */ sanitizeTemperature(temperature.getDouble("max"), metric), - /* condition */ data.getString("main"), - /* conditionCode */ mapConditionIconToCode( - data.getString("icon"), data.getInt("id"))); - result.add(item); - } - - return result; - } - - // OpenWeatherMap sometimes returns temperatures in Kelvin even if we ask it - // for deg C or deg F. Detect this and convert accordingly. - private static float sanitizeTemperature(double value, boolean metric) { - // threshold chosen to work for both C and F. 170 deg F is hotter - // than the hottest place on earth. - if (value > 170) { - // K -> deg C - value -= 273.15; - if (!metric) { - // deg C -> deg F - value = (value * 1.8) + 32; - } - } - return (float) value; - } - - 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/Utils.java b/src/com/cyanogenmod/lockclock/weather/Utils.java new file mode 100644 index 0000000..411fe29 --- /dev/null +++ b/src/com/cyanogenmod/lockclock/weather/Utils.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * 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.content.Context; +import android.content.res.Resources; +import com.cyanogenmod.lockclock.R; +import cyanogenmod.providers.WeatherContract; + +import static cyanogenmod.providers.WeatherContract.WeatherColumns.WeatherCode.NOT_AVAILABLE; +import static cyanogenmod.providers.WeatherContract.WeatherColumns.WeatherCode.SCATTERED_THUNDERSTORMS; +import static cyanogenmod.providers.WeatherContract.WeatherColumns.WeatherCode.SCATTERED_SNOW_SHOWERS; +import static cyanogenmod.providers.WeatherContract.WeatherColumns.WeatherCode.ISOLATED_THUNDERSHOWERS; + +import java.text.DecimalFormat; + +public final class Utils { + + private static final DecimalFormat sNoDigitsFormat = new DecimalFormat("0"); + + // In doubt? See https://en.wikipedia.org/wiki/Points_of_the_compass + private static final double DIRECTION_NORTH = 23d; + private static final double DIRECTION_NORTH_EAST = 68d; + private static final double DIRECTION_EAST = 113d; + private static final double DIRECTION_SOUTH_EAST = 158d; + private static final double DIRECTION_SOUTH = 203d; + private static final double DIRECTION_SOUTH_WEST = 248d; + private static final double DIRECTION_WEST = 293d; + private static final double DIRECTION_NORTH_WEST = 338d; + + /** + * Returns a localized string of the wind direction + * @param context Application context to access resources + * @param windDirection The wind direction in degrees + * @return + */ + public static String resolveWindDirection(Context context, double windDirection) { + int resId; + + if (windDirection < 0) { + resId = R.string.unknown; + } else if (windDirection < DIRECTION_NORTH) { + resId = R.string.weather_N; + } else if (windDirection < DIRECTION_NORTH_EAST) { + resId = R.string.weather_NE; + } else if (windDirection < DIRECTION_EAST) { + resId = R.string.weather_E; + } else if (windDirection < DIRECTION_SOUTH_EAST) { + resId = R.string.weather_SE; + } else if (windDirection < DIRECTION_SOUTH) { + resId = R.string.weather_S; + } else if (windDirection < DIRECTION_SOUTH_WEST) { + resId = R.string.weather_SW; + } else if (windDirection < DIRECTION_WEST) { + resId = R.string.weather_W; + } else if (windDirection < DIRECTION_NORTH_WEST) { + resId = R.string.weather_NW; + } else { + resId = R.string.weather_N; + } + + return context.getString(resId); + } + + /** + * Returns the resource name associated to the supplied weather condition code + * @param context Application context to access resources + * @param conditionCode The weather condition code + * @return The resource name if a valid condition code is passed, empty string otherwise + */ + public static String resolveWeatherCondition(Context context, int conditionCode) { + final Resources res = context.getResources(); + final int resId = res.getIdentifier("weather_" + + Utils.addOffsetToConditionCodeFromWeatherContract(conditionCode), "string", + context.getPackageName()); + if (resId != 0) { + return res.getString(resId); + } + return ""; + } + + private static String getFormattedValue(double value, String unit) { + if (Double.isNaN(value)) { + return "-"; + } + String formatted = sNoDigitsFormat.format(value); + if (formatted.equals("-0")) { + formatted = "0"; + } + return formatted + unit; + } + + /** + * Returns a string with the format xx% (where xx is the humidity value provided) + * @param humidity The humidity value + * @return The formatted string if a valid value is provided, "-" otherwise. Decimals are + * removed + */ + public static String formatHumidity(double humidity) { + return getFormattedValue(humidity, "%"); + } + + /** + * Returns a localized string of the speed and speed unit + * @param context Application context to access resources + * @param windSpeed The wind speed + * @param windSpeedUnit The speed unit. See + * {@link cyanogenmod.providers.WeatherContract.WeatherColumns.WindSpeedUnit} + * @return The formatted string if a valid speed and speed unit a provided. + * {@link com.cyanogenmod.lockclock.R.string#unknown} otherwise + */ + public static String formatWindSpeed(Context context, double windSpeed, int windSpeedUnit) { + if (windSpeed < 0) { + return context.getString(R.string.unknown); + } + + String localizedSpeedUnit; + switch (windSpeedUnit) { + case WeatherContract.WeatherColumns.WindSpeedUnit.MPH: + localizedSpeedUnit = context.getString(R.string.weather_mph); + break; + case WeatherContract.WeatherColumns.WindSpeedUnit.KPH: + localizedSpeedUnit = context.getString(R.string.weather_kph); + break; + default: + return context.getString(R.string.unknown); + } + return getFormattedValue(windSpeed, localizedSpeedUnit); + } + + /** + * Helper method to convert miles to kilometers + * @param miles The value in miles + * @return The value in kilometers + */ + public static double milesToKilometers(double miles) { + return miles * 1.609344d; + } + + /** + * Helper method to convert kilometers to miles + * @param km The value in kilometers + * @return The value in miles + */ + public static double kilometersToMiles(double km) { + return km * 0.6214d; + } + + /** + * Adds an offset to the condition code reported by the active weather service provider. + * @param conditionCode The condition code from the Weather API + * @return A condition code that correctly maps to our resource IDs + */ + public static int addOffsetToConditionCodeFromWeatherContract(int conditionCode) { + if (conditionCode <= WeatherContract.WeatherColumns.WeatherCode.SHOWERS) { + return conditionCode; + } else if (conditionCode <= SCATTERED_THUNDERSTORMS) { + return conditionCode + 1; + } else if (conditionCode <= SCATTERED_SNOW_SHOWERS) { + return conditionCode + 2; + } else if (conditionCode <= ISOLATED_THUNDERSHOWERS) { + return conditionCode + 3; + } else { + return NOT_AVAILABLE; + } + } +} diff --git a/src/com/cyanogenmod/lockclock/weather/WeatherContentProvider.java b/src/com/cyanogenmod/lockclock/weather/WeatherContentProvider.java deleted file mode 100644 index a2ab385..0000000 --- a/src/com/cyanogenmod/lockclock/weather/WeatherContentProvider.java +++ /dev/null @@ -1,187 +0,0 @@ - -package com.cyanogenmod.lockclock.weather; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.net.Uri; -import android.util.Log; - -import com.cyanogenmod.lockclock.misc.Preferences; -import com.cyanogenmod.lockclock.weather.WeatherInfo.DayForecast; - -public class WeatherContentProvider extends ContentProvider { - - public static final String TAG = WeatherContentProvider.class.getSimpleName(); - private static final boolean DEBUG = false; - - static WeatherInfo sCachedWeatherInfo; - - private static final int URI_TYPE_EVERYTHING = 1; - private static final int URI_TYPE_CURRENT = 2; - private static final int URI_TYPE_FORECAST = 3; - - private static final String COLUMN_CURRENT_CITY_ID = "city_id"; - private static final String COLUMN_CURRENT_CITY = "city"; - private static final String COLUMN_CURRENT_CONDITION = "condition"; - private static final String COLUMN_CURRENT_TEMPERATURE = "temperature"; - private static final String COLUMN_CURRENT_HUMIDITY = "humidity"; - private static final String COLUMN_CURRENT_WIND = "wind"; - private static final String COLUMN_CURRENT_TIME_STAMP = "time_stamp"; - private static final String COLUMN_CURRENT_CONDITION_CODE = "condition_code"; - - private static final String COLUMN_FORECAST_LOW = "forecast_low"; - private static final String COLUMN_FORECAST_HIGH = "forecast_high"; - private static final String COLUMN_FORECAST_CONDITION = "forecast_condition"; - private static final String COLUMN_FORECAST_CONDITION_CODE = "forecast_condition_code"; - - private static final String[] PROJECTION_DEFAULT_CURRENT = new String[] { - COLUMN_CURRENT_CITY_ID, - COLUMN_CURRENT_CITY, - COLUMN_CURRENT_CONDITION, - COLUMN_CURRENT_TEMPERATURE, - COLUMN_CURRENT_HUMIDITY, - COLUMN_CURRENT_WIND, - COLUMN_CURRENT_TIME_STAMP, - COLUMN_CURRENT_CONDITION_CODE - }; - - private static final String[] PROJECTION_DEFAULT_FORECAST = new String[] { - COLUMN_FORECAST_LOW, - COLUMN_FORECAST_HIGH, - COLUMN_FORECAST_CONDITION, - COLUMN_FORECAST_CONDITION_CODE - }; - - private static final String[] PROJECTION_DEFAULT_EVERYTHING = new String[] { - COLUMN_CURRENT_CITY_ID, - COLUMN_CURRENT_CITY, - COLUMN_CURRENT_CONDITION, - COLUMN_CURRENT_TEMPERATURE, - COLUMN_CURRENT_HUMIDITY, - COLUMN_CURRENT_WIND, - COLUMN_CURRENT_TIME_STAMP, - COLUMN_CURRENT_CONDITION_CODE, - - COLUMN_FORECAST_LOW, - COLUMN_FORECAST_HIGH, - COLUMN_FORECAST_CONDITION, - COLUMN_FORECAST_CONDITION_CODE - }; - - public static final String AUTHORITY = "com.cyanogenmod.lockclock.weather.provider"; - - private static final UriMatcher sUriMatcher; - static { - sUriMatcher = new UriMatcher(URI_TYPE_EVERYTHING); - sUriMatcher.addURI(AUTHORITY, "weather", URI_TYPE_EVERYTHING); - sUriMatcher.addURI(AUTHORITY, "weather/current", URI_TYPE_CURRENT); - sUriMatcher.addURI(AUTHORITY, "weather/forecast", URI_TYPE_FORECAST); - } - - private Context mContext; - - @Override - public boolean onCreate() { - mContext = getContext(); - sCachedWeatherInfo = Preferences.getCachedWeatherInfo(mContext); - return true; - } - - @Override - public Cursor query( - Uri uri, - String[] projection, - String selection, - String[] selectionArgs, - String sortOrder) { - - final int projectionType = sUriMatcher.match(uri); - final MatrixCursor result = new MatrixCursor(resolveProjection(projection, projectionType)); - - WeatherInfo weather = sCachedWeatherInfo; - if (weather != null) { - // current - result.newRow() - .add(COLUMN_CURRENT_CITY, weather.getCity()) - .add(COLUMN_CURRENT_CITY_ID, weather.getId()) - .add(COLUMN_CURRENT_CONDITION, weather.getCondition()) - .add(COLUMN_CURRENT_HUMIDITY, weather.getFormattedHumidity()) - .add(COLUMN_CURRENT_WIND, weather.getFormattedWindSpeed() - + " " + weather.getWindDirection()) - .add(COLUMN_CURRENT_TEMPERATURE, weather.getFormattedTemperature()) - .add(COLUMN_CURRENT_TIME_STAMP, weather.getTimestamp().toString()) - .add(COLUMN_CURRENT_CONDITION_CODE, weather.getConditionCode()); - - // forecast - for (DayForecast day : weather.getForecasts()) { - result.newRow() - .add(COLUMN_FORECAST_CONDITION, day.getCondition(mContext)) - .add(COLUMN_FORECAST_LOW, day.getFormattedLow()) - .add(COLUMN_FORECAST_HIGH, day.getFormattedHigh()) - .add(COLUMN_FORECAST_CONDITION_CODE, day.getConditionCode()); - } - return result; - } else { - if (DEBUG) Log.e(TAG, "sCachedWeatherInfo is null"); - Intent updateWeather = new Intent(WeatherUpdateService.ACTION_FORCE_UPDATE); - updateWeather.setClass(mContext, WeatherUpdateService.class); - mContext.startService(updateWeather); - } - return null; - } - - private String[] resolveProjection(String[] projection, int uriType) { - if (projection != null) - return projection; - switch (uriType) { - default: - case URI_TYPE_EVERYTHING: - return PROJECTION_DEFAULT_EVERYTHING; - - case URI_TYPE_CURRENT: - return PROJECTION_DEFAULT_CURRENT; - - case URI_TYPE_FORECAST: - return PROJECTION_DEFAULT_FORECAST; - } - } - - @Override - public String getType(Uri uri) { - return null; - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - return null; - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - return 0; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - return 0; - } - - public static void updateCachedWeatherInfo(Context context, WeatherInfo info) { - if (DEBUG) Log.e(TAG, "updateCachedWeatherInfo()"); - if(info != null) { - if (DEBUG) Log.e(TAG, "set new weather info"); - sCachedWeatherInfo = WeatherInfo.fromSerializedString(context, info.toSerializedString()); - } else { - if(DEBUG) Log.e(TAG, "nulled out cached weather info"); - sCachedWeatherInfo = null; - } - context.getContentResolver().notifyChange( - Uri.parse("content://" + WeatherContentProvider.AUTHORITY + "/weather"), null); - } - -} diff --git a/src/com/cyanogenmod/lockclock/weather/WeatherInfo.java b/src/com/cyanogenmod/lockclock/weather/WeatherInfo.java deleted file mode 100755 index 7ad4339..0000000 --- a/src/com/cyanogenmod/lockclock/weather/WeatherInfo.java +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright (C) 2012 The AOKP Project - * - * 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.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; - -import com.cyanogenmod.lockclock.R; -import com.cyanogenmod.lockclock.misc.IconUtils; - -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.Date; - -public class WeatherInfo { - private static final DecimalFormat sNoDigitsFormat = new DecimalFormat("0"); - - private Context mContext; - - private String id; - private String city; - private String condition; - private int conditionCode; - private float temperature; - 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 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.condition = condition; - this.conditionCode = conditionCode; - this.humidity = humidity; - this.wind = wind; - this.windDirection = windDir; - this.speedUnit = speedUnit; - this.timestamp = timestamp; - this.temperature = temp; - this.tempUnit = tempUnit; - this.forecasts = forecasts; - } - - 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"); - } - - public String getFormattedHigh() { - return getFormattedValue(high, "\u00b0"); - } - - 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 getConditionCode() { - return conditionCode; - } - } - - 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() { - return id; - } - - public String getCity() { - return city; - } - - public String getCondition() { - return getCondition(mContext, conditionCode, condition); - } - - public int getConditionCode() { - return conditionCode; - } - - 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); - } - return condition; - } - - public Date getTimestamp() { - return new Date(timestamp); - } - - private static String getFormattedValue(float value, String unit) { - if (Float.isNaN(value)) { - return "-"; - } - String formatted = sNoDigitsFormat.format(value); - if (formatted.equals("-0")) { - formatted = "0"; - } - return formatted + unit; - } - - public String getFormattedTemperature() { - return getFormattedValue(temperature, "\u00b0" + tempUnit); - } - - public String getFormattedLow() { - return forecasts.get(0).getFormattedLow(); - } - - public String getFormattedHigh() { - return forecasts.get(0).getFormattedHigh(); - } - - public String getFormattedHumidity() { - return getFormattedValue(humidity, "%"); - } - - public String getFormattedWindSpeed() { - if (wind < 0) { - return mContext.getString(R.string.unknown); - } - return getFormattedValue(wind, speedUnit); - } - - public String getWindDirection() { - int resId; - - if (windDirection < 0) resId = R.string.unknown; - else if (windDirection < 23) resId = R.string.weather_N; - else if (windDirection < 68) resId = R.string.weather_NE; - else if (windDirection < 113) resId = R.string.weather_E; - else if (windDirection < 158) resId = R.string.weather_SE; - else if (windDirection < 203) resId = R.string.weather_S; - else if (windDirection < 248) resId = R.string.weather_SW; - else if (windDirection < 293) resId = R.string.weather_W; - else if (windDirection < 338) resId = R.string.weather_NW; - else resId = R.string.weather_N; - - return mContext.getString(resId); - } - - public ArrayList<DayForecast> getForecasts() { - return forecasts; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("WeatherInfo for "); - builder.append(city); - builder.append(" ("); - builder.append(id); - builder.append(") @ "); - builder.append(getTimestamp()); - builder.append(": "); - builder.append(getCondition()); - builder.append("("); - builder.append(conditionCode); - builder.append("), temperature "); - builder.append(getFormattedTemperature()); - builder.append(", low "); - builder.append(getFormattedLow()); - builder.append(", high "); - builder.append(getFormattedHigh()); - builder.append(", humidity "); - builder.append(getFormattedHumidity()); - builder.append(", wind "); - 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(); - } - - public String toSerializedString() { - StringBuilder builder = new StringBuilder(); - builder.append(id).append('|'); - builder.append(city).append('|'); - builder.append(condition).append('|'); - builder.append(conditionCode).append('|'); - builder.append(temperature).append('|'); - builder.append(tempUnit).append('|'); - builder.append(humidity).append('|'); - builder.append(wind).append('|'); - builder.append(windDirection).append('|'); - builder.append(speedUnit).append('|'); - 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 != 12) { - return null; - } - - int conditionCode, windDirection; - long timestamp; - 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[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], /* 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 deleted file mode 100644 index 70fbf42..0000000 --- a/src/com/cyanogenmod/lockclock/weather/WeatherProvider.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2013 The CyanogenMod Project - * - * 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.location.Location; - -import java.util.List; - -public interface WeatherProvider { - public class LocationResult { - public String id; - public String city; - public String postal; - public String countryId; - public String country; - } - - List<LocationResult> getLocations(String input); - - WeatherInfo getWeatherInfo(String id, String localizedCityName, boolean metricUnits); - - WeatherInfo getWeatherInfo(Location location, boolean metricUnits); - - int getNameResourceId(); -} diff --git a/src/com/cyanogenmod/lockclock/weather/WeatherSourceListenerService.java b/src/com/cyanogenmod/lockclock/weather/WeatherSourceListenerService.java new file mode 100644 index 0000000..4bc816a --- /dev/null +++ b/src/com/cyanogenmod/lockclock/weather/WeatherSourceListenerService.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 The CyanogenMod Project + * + * 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.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; +import com.cyanogenmod.lockclock.ClockWidgetService; +import com.cyanogenmod.lockclock.misc.Constants; +import com.cyanogenmod.lockclock.misc.Preferences; +import cyanogenmod.weather.CMWeatherManager; + +public class WeatherSourceListenerService extends Service + implements CMWeatherManager.WeatherServiceProviderChangeListener { + + private static final String TAG = WeatherSourceListenerService.class.getSimpleName(); + private static final boolean D = Constants.DEBUG; + private Context mContext; + + @Override + public void onWeatherServiceProviderChanged(String providerLabel) { + if (D) Log.d(TAG, "Weather Source changed " + providerLabel); + Preferences.setWeatherSource(mContext, providerLabel); + Preferences.setCachedWeatherInfo(mContext, 0, null); + //The data contained in WeatherLocation is tightly coupled to the weather provider + //that generated that data, so we need to clear the cached weather location and let the new + //weather provider regenerate the data if the user decides to use custom location again + Preferences.setCustomWeatherLocationCity(mContext, null); + Preferences.setCustomWeatherLocation(mContext, null); + Preferences.setUseCustomWeatherLocation(mContext, false); + + //Refresh the widget + mContext.startService(new Intent(mContext, ClockWidgetService.class) + .setAction(ClockWidgetService.ACTION_REFRESH)); + + if (providerLabel != null) { + mContext.startService(new Intent(mContext, WeatherUpdateService.class) + .putExtra(WeatherUpdateService.ACTION_FORCE_UPDATE, true)); + } + } + + @Override + public void onCreate() { + mContext = getApplicationContext(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + final CMWeatherManager weatherManager + = CMWeatherManager.getInstance(mContext); + weatherManager.registerWeatherServiceProviderChangeListener(this); + if (D) Log.d(TAG, "Listener registered"); + return START_STICKY; + } + + @Override + public void onDestroy() { + final CMWeatherManager weatherManager = CMWeatherManager.getInstance(mContext); + weatherManager.unregisterWeatherServiceProviderChangeListener(this); + if (D) Log.d(TAG, "Listener unregistered"); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } +} diff --git a/src/com/cyanogenmod/lockclock/weather/WeatherUpdateService.java b/src/com/cyanogenmod/lockclock/weather/WeatherUpdateService.java index ea0b89c..fc652b1 100644 --- a/src/com/cyanogenmod/lockclock/weather/WeatherUpdateService.java +++ b/src/com/cyanogenmod/lockclock/weather/WeatherUpdateService.java @@ -26,24 +26,31 @@ import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.location.LocationProvider; -import android.os.AsyncTask; import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; - +import android.widget.Toast; import com.cyanogenmod.lockclock.ClockWidgetProvider; +import com.cyanogenmod.lockclock.R; import com.cyanogenmod.lockclock.misc.Constants; import com.cyanogenmod.lockclock.misc.Preferences; import com.cyanogenmod.lockclock.misc.WidgetUtils; import com.cyanogenmod.lockclock.preference.WeatherPreferences; - import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesUtil; +import cyanogenmod.weather.CMWeatherManager; +import cyanogenmod.weather.WeatherInfo; +import cyanogenmod.weather.WeatherLocation; +import java.lang.ref.WeakReference; import java.util.Date; public class WeatherUpdateService extends Service { @@ -54,6 +61,10 @@ public class WeatherUpdateService extends Service { private static final String ACTION_CANCEL_LOCATION_UPDATE = "com.cyanogenmod.lockclock.action.CANCEL_LOCATION_UPDATE"; + private static final String ACTION_CANCEL_UPDATE_WEATHER_REQUEST = + "com.cyanogenmod.lockclock.action.CANCEL_UPDATE_WEATHER_REQUEST"; + private static final long WEATHER_UPDATE_REQUEST_TIMEOUT_MS = 30L * 1000L; + // 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"; @@ -62,7 +73,8 @@ public class WeatherUpdateService extends Service { private static final long OUTDATED_LOCATION_THRESHOLD_MILLIS = 10L * 60L * 1000L; // 10 minutes private static final float LOCATION_ACCURACY_THRESHOLD_METERS = 50000; - private WeatherUpdateTask mTask; + private WorkerThread mWorkerThread; + private Handler mHandler; private static final Criteria sLocationCriteria; static { @@ -73,107 +85,243 @@ public class WeatherUpdateService extends Service { } @Override + public void onCreate() { + Log.d(TAG, "onCreate"); + mWorkerThread = new WorkerThread(getApplicationContext()); + mWorkerThread.start(); + mWorkerThread.prepareHandler(); + mHandler = new Handler(Looper.getMainLooper()); + } + + @Override public int onStartCommand(Intent intent, int flags, int startId) { if (D) Log.v(TAG, "Got intent " + intent); - boolean active = mTask != null && mTask.getStatus() != AsyncTask.Status.FINISHED; - if (ACTION_CANCEL_LOCATION_UPDATE.equals(intent.getAction())) { WeatherLocationListener.cancel(this); - if (!active) { + if (!mWorkerThread.isProcessing()) { 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; + if (ACTION_CANCEL_UPDATE_WEATHER_REQUEST.equals(intent.getAction())) { + if (mWorkerThread.isProcessing()) { + mWorkerThread.getHandler().obtainMessage( + WorkerThread.MSG_CANCEL_UPDATE_WEATHER_REQUEST).sendToTarget(); + mHandler.post(new Runnable() { + @Override + public void run() { + final Context context = getApplicationContext(); + final CMWeatherManager weatherManager + = CMWeatherManager.getInstance(context); + final String activeProviderLabel + = weatherManager.getActiveWeatherServiceProviderLabel(); + final String noData + = getString(R.string.weather_cannot_reach_provider, + activeProviderLabel); + Toast.makeText(context, noData, Toast.LENGTH_SHORT).show(); + } + }); + } + stopSelf(); + return START_NOT_STICKY; } boolean force = ACTION_FORCE_UPDATE.equals(intent.getAction()); if (!shouldUpdate(force)) { Log.d(TAG, "Service started, but shouldn't update ... stopping"); - stopSelf(); sendCancelledBroadcast(); + stopSelf(); return START_NOT_STICKY; } - mTask = new WeatherUpdateTask(); - mTask.execute(); + mWorkerThread.getHandler().obtainMessage(WorkerThread.MSG_ON_NEW_WEATHER_REQUEST) + .sendToTarget(); 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; - } - - @Override - public void onDestroy() { - if (mTask != null && mTask.getStatus() != AsyncTask.Status.FINISHED) { - mTask.cancel(true); - mTask = null; + private boolean shouldUpdate(boolean force) { + final CMWeatherManager weatherManager + = CMWeatherManager.getInstance(getApplicationContext()); + if (weatherManager.getActiveWeatherServiceProviderLabel() == null) { + //Why bother if we don't even have an active provider + if (D) Log.d(TAG, "No active weather service provider found, skip"); + return false; } - } - private boolean shouldUpdate(boolean force) { - long interval = Preferences.weatherRefreshIntervalInMs(this); + final long interval = Preferences.weatherRefreshIntervalInMs(this); if (interval == 0 && !force) { - if (D) Log.v(TAG, "Interval set to manual and update not forced, skip update"); + if (D) Log.v(TAG, "Interval set to manual and update not forced, skip"); return false; } if (!WeatherPreferences.hasLocationPermission(this)) { - if (D) Log.v(TAG, "Application does not have the location permission"); + if (D) Log.v(TAG, "Application does not have the location permission, skip"); return false; } - if (force) { - Preferences.setCachedWeatherInfo(this, 0, null); - } - - long now = System.currentTimeMillis(); - long lastUpdate = Preferences.lastWeatherUpdateTimestamp(this); - long due = lastUpdate + interval; - - if (D) Log.d(TAG, "Now " + now + " due " + due + "(" + new Date(due) + ")"); + if (WidgetUtils.isNetworkAvailable(this)) { + if (force) { + if (D) Log.d(TAG, "Forcing weather update"); + return true; + } else { + final long now = SystemClock.elapsedRealtime(); + final long lastUpdate = Preferences.lastWeatherUpdateTimestamp(this); + final long due = lastUpdate + interval; + if (D) { + Log.d(TAG, "Now " + now + " Last update " + lastUpdate + + " interval " + interval); + } - if (lastUpdate != 0 && now < due) { - if (D) Log.v(TAG, "Weather update is not due yet"); + if (lastUpdate == 0 || due - now < 0) { + if (D) Log.d(TAG, "Should update"); + return true; + } else { + if (D) Log.v(TAG, "Next weather update due in " + (due - now) + " ms, skip"); + return false; + } + } + } else { + if (D) Log.d(TAG, "Network is not available, skip"); return false; } - - return WidgetUtils.isNetworkAvailable(this); } - private class WeatherUpdateTask extends AsyncTask<Void, Void, WeatherInfo> { + private static class WorkerThread extends HandlerThread + implements CMWeatherManager.WeatherUpdateRequestListener { + + public static final int MSG_ON_NEW_WEATHER_REQUEST = 1; + public static final int MSG_ON_WEATHER_REQUEST_COMPLETED = 2; + public static final int MSG_WEATHER_REQUEST_FAILED = 3; + public static final int MSG_CANCEL_UPDATE_WEATHER_REQUEST = 4; + + private Handler mHandler; + private boolean mIsProcessingWeatherUpdate = false; private WakeLock mWakeLock; - private Context mContext; + private PendingIntent mTimeoutPendingIntent; + private int mRequestId; + private final CMWeatherManager mWeatherManager; + final private Context mContext; + + public WorkerThread(Context context) { + super("weather-service-worker"); + mContext = context; + mWeatherManager = CMWeatherManager.getInstance(mContext); + } + + public synchronized void prepareHandler() { + mHandler = new Handler(getLooper()) { + @Override + public void handleMessage(Message msg) { + if (D) Log.d(TAG, "Msg " + msg.what); + switch (msg.what) { + case MSG_ON_NEW_WEATHER_REQUEST: + onNewWeatherRequest(); + break; + case MSG_ON_WEATHER_REQUEST_COMPLETED: + WeatherInfo info = (WeatherInfo) msg.obj; + onWeatherRequestCompleted(info); + break; + case MSG_WEATHER_REQUEST_FAILED: + int status = msg.arg1; + onWeatherRequestFailed(status); + break; + case MSG_CANCEL_UPDATE_WEATHER_REQUEST: + onCancelUpdateWeatherRequest(); + break; + default: + //Unknown message, pass it on... + super.handleMessage(msg); + } + } + }; + } + + private void startTimeoutAlarm() { + Intent intent = new Intent(mContext, WeatherUpdateService.class); + intent.setAction(ACTION_CANCEL_UPDATE_WEATHER_REQUEST); + + mTimeoutPendingIntent = PendingIntent.getService(mContext, 0, intent, + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT); + + AlarmManager am = (AlarmManager) mContext.getSystemService(ALARM_SERVICE); + long elapseTime = SystemClock.elapsedRealtime() + WEATHER_UPDATE_REQUEST_TIMEOUT_MS; + am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, elapseTime, mTimeoutPendingIntent); + if (D) Log.v(TAG, "Timeout alarm set to expire in " + elapseTime + " ms"); + } + + private void cancelTimeoutAlarm() { + if (mTimeoutPendingIntent != null) { + AlarmManager am = (AlarmManager) mContext.getSystemService(ALARM_SERVICE); + am.cancel(mTimeoutPendingIntent); + mTimeoutPendingIntent = null; + if (D) Log.v(TAG, "Timeout alarm cancelled"); + } + } - public WeatherUpdateTask() { - if (D) Log.d(TAG, "Starting weather update task"); - PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); + public synchronized Handler getHandler() { + return mHandler; + } + + private void onNewWeatherRequest() { + if (mIsProcessingWeatherUpdate) { + Log.d(TAG, "Already processing weather update, discarding request..."); + return; + } + + mIsProcessingWeatherUpdate = true; + final PowerManager pm + = (PowerManager) mContext.getSystemService(POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); mWakeLock.setReferenceCounted(false); - mContext = WeatherUpdateService.this; + if (D) Log.v(TAG, "ACQUIRING WAKELOCK"); + mWakeLock.acquire(); + + WeatherLocation customWeatherLocation = null; + if (Preferences.useCustomWeatherLocation(mContext)) { + customWeatherLocation = Preferences.getCustomWeatherLocation(mContext); + } + if (customWeatherLocation != null) { + mRequestId = mWeatherManager.requestWeatherUpdate(customWeatherLocation, this); + if (D) Log.d(TAG, "Request submitted using WeatherLocation"); + startTimeoutAlarm(); + } else { + final Location location = getCurrentLocation(); + if (location != null) { + mRequestId = mWeatherManager.requestWeatherUpdate(location, this); + if (D) Log.d(TAG, "Request submitted using Location"); + startTimeoutAlarm(); + } else { + // 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) { + mHandler.obtainMessage(MSG_ON_WEATHER_REQUEST_COMPLETED, + cachedInfo).sendToTarget(); + if (D) Log.d(TAG, "Returning cached weather data [ " + + cachedInfo.toString()+ " ]"); + } else { + mHandler.obtainMessage(MSG_WEATHER_REQUEST_FAILED).sendToTarget(); + } + } + } } - @Override - protected void onPreExecute() { - if (D) Log.d(TAG, "ACQUIRING WAKELOCK"); - mWakeLock.acquire(); + public void tearDown() { + if (D) Log.d(TAG, "Tearing down worker thread"); + if (isProcessing()) mWeatherManager.cancelRequest(mRequestId); + quit(); + } + + public boolean isProcessing() { + return mIsProcessingWeatherUpdate; } private Location getCurrentLocation() { - LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE); + final LocationManager lm + = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); Location location = lm.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER); if (D) Log.v(TAG, "Current location is " + location); @@ -204,7 +352,6 @@ public class WeatherUpdateService extends Service { WeatherLocationListener.registerIfNeeded(mContext, locationProvider); } } - return location; } @@ -214,77 +361,81 @@ public class WeatherUpdateService extends Service { || result == ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED; } - @Override - protected WeatherInfo doInBackground(Void... params) { - WeatherProvider provider = Preferences.weatherProvider(mContext); - boolean metric = Preferences.useMetricUnits(mContext); - String customLocationId = null, customLocationName = null; - - if (Preferences.useCustomWeatherLocation(mContext)) { - customLocationId = Preferences.customWeatherLocationId(mContext); - customLocationName = Preferences.customWeatherLocationCity(mContext); - } + private void onWeatherRequestCompleted(WeatherInfo result) { + if (D) Log.d(TAG, "Weather update received, caching data and updating widget"); + cancelTimeoutAlarm(); + long now = SystemClock.elapsedRealtime(); + Preferences.setCachedWeatherInfo(mContext, now, result); + scheduleUpdate(mContext, Preferences.weatherRefreshIntervalInMs(mContext), false); - if (customLocationId != null) { - return provider.getWeatherInfo(customLocationId, customLocationName, metric); - } + Intent updateIntent = new Intent(mContext, ClockWidgetProvider.class); + mContext.sendBroadcast(updateIntent); + broadcastAndCleanUp(false); + } - Location location = getCurrentLocation(); - if (location != null) { - WeatherInfo info = provider.getWeatherInfo(location, metric); - if (info != null) { - return info; - } + private void onWeatherRequestFailed(int status) { + if (D) Log.d(TAG, "Weather refresh failed ["+status+"]"); + cancelTimeoutAlarm(); + if (status == CMWeatherManager.RequestStatus.ALREADY_IN_PROGRESS) { + if (D) Log.d(TAG, "A request is already in progress, no need to schedule again"); + } else if (status == CMWeatherManager.RequestStatus.FAILED) { + //Something went wrong, let's schedule an update at the next interval from now + //A force update might happen earlier anyway + scheduleUpdate(mContext, Preferences.weatherRefreshIntervalInMs(mContext), false); + } else { + //Wait until the next update is due + scheduleNextUpdate(mContext, false); } + broadcastAndCleanUp(true); + } - // 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(), metric); + private void onCancelUpdateWeatherRequest() { + if (D) Log.d(TAG, "Cancelling active weather request"); + if (mIsProcessingWeatherUpdate) { + cancelTimeoutAlarm(); + mWeatherManager.cancelRequest(mRequestId); + broadcastAndCleanUp(true); } - - return null; } - @Override - protected void onPostExecute(WeatherInfo result) { - finish(result); - } + private void broadcastAndCleanUp(boolean updateCancelled) { + Intent finishedIntent = new Intent(ACTION_UPDATE_FINISHED); + finishedIntent.putExtra(EXTRA_UPDATE_CANCELLED, updateCancelled); + mContext.sendBroadcast(finishedIntent); - @Override - protected void onCancelled() { - finish(null); + if (D) Log.d(TAG, "RELEASING WAKELOCK"); + mWakeLock.release(); + mIsProcessingWeatherUpdate = false; + mContext.stopService(new Intent(mContext, WeatherUpdateService.class)); } - private void finish(WeatherInfo result) { - if (result != null) { - if (D) Log.d(TAG, "Weather update received, caching data and updating widget"); - long now = System.currentTimeMillis(); - Preferences.setCachedWeatherInfo(mContext, now, result); - scheduleUpdate(mContext, Preferences.weatherRefreshIntervalInMs(mContext), false); - - Intent updateIntent = new Intent(mContext, ClockWidgetProvider.class); - sendBroadcast(updateIntent); - } else if (isCancelled()) { - // cancelled, likely due to lost network - we'll get restarted - // when network comes back + @Override + public void onWeatherRequestCompleted(int state, WeatherInfo weatherInfo) { + if (state == CMWeatherManager.RequestStatus.COMPLETED) { + mHandler.obtainMessage(WorkerThread.MSG_ON_WEATHER_REQUEST_COMPLETED, weatherInfo) + .sendToTarget(); } else { - // failure, schedule next download in 30 minutes - if (D) Log.d(TAG, "Weather refresh failed, scheduling update in 30 minutes"); - long interval = 30 * 60 * 1000; - scheduleUpdate(mContext, interval, false); + mHandler.obtainMessage(WorkerThread.MSG_WEATHER_REQUEST_FAILED, state, 0) + .sendToTarget(); } - WeatherContentProvider.updateCachedWeatherInfo(mContext, result); + } + } - Intent finishedIntent = new Intent(ACTION_UPDATE_FINISHED); - finishedIntent.putExtra(EXTRA_UPDATE_CANCELLED, result == null); - sendBroadcast(finishedIntent); + private void sendCancelledBroadcast() { + Intent finishedIntent = new Intent(ACTION_UPDATE_FINISHED); + finishedIntent.putExtra(EXTRA_UPDATE_CANCELLED, true); + sendBroadcast(finishedIntent); + } - if (D) Log.d(TAG, "RELEASING WAKELOCK"); - mWakeLock.release(); - stopSelf(); - } + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy"); + mWorkerThread.tearDown(); } private static class WeatherLocationListener implements LocationListener { @@ -362,7 +513,7 @@ public class WeatherUpdateService extends Service { // 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); + scheduleUpdate(mContext, 0, true); cancelTimeoutAlarm(); sInstance = null; } @@ -374,7 +525,7 @@ public class WeatherUpdateService extends Service { 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); + scheduleUpdate(mContext, 0, true); cancelTimeoutAlarm(); sInstance = null; } @@ -392,21 +543,26 @@ public class WeatherUpdateService extends Service { } } - private static void scheduleUpdate(Context context, long timeFromNow, boolean force) { + private static void scheduleUpdate(Context context, long millisFromNow, boolean force) { AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - long due = System.currentTimeMillis() + timeFromNow; - - if (D) Log.d(TAG, "Scheduling next update at " + new Date(due)); - am.set(AlarmManager.RTC_WAKEUP, due, getUpdateIntent(context, force)); + long due = SystemClock.elapsedRealtime() + millisFromNow; + if (D) Log.d(TAG, "Next update scheduled at " + + new Date(System.currentTimeMillis() + millisFromNow)); + am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, due, getUpdateIntent(context, force)); } public static void scheduleNextUpdate(Context context, boolean force) { - long lastUpdate = Preferences.lastWeatherUpdateTimestamp(context); - if (lastUpdate == 0 || force) { + if (force) { + if (D) Log.d(TAG, "Scheduling next update immediately"); scheduleUpdate(context, 0, true); } else { - long interval = Preferences.weatherRefreshIntervalInMs(context); - scheduleUpdate(context, lastUpdate + interval - System.currentTimeMillis(), false); + final long lastUpdate = Preferences.lastWeatherUpdateTimestamp(context); + final long interval = Preferences.weatherRefreshIntervalInMs(context); + final long now = SystemClock.elapsedRealtime(); + long due = (interval + lastUpdate) - now; + if (due < 0) due = 0; + if (D) Log.d(TAG, "Scheduling in " + due + " ms"); + scheduleUpdate(context, due, false); } } diff --git a/src/com/cyanogenmod/lockclock/weather/YahooWeatherProvider.java b/src/com/cyanogenmod/lockclock/weather/YahooWeatherProvider.java deleted file mode 100644 index 21bc9e4..0000000 --- a/src/com/cyanogenmod/lockclock/weather/YahooWeatherProvider.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright (C) 2013 The CyanogenMod Project - * - * 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.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.weather.WeatherInfo.DayForecast; -import com.cyanogenmod.lockclock.R; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.xml.sax.Attributes; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -import java.io.IOException; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - -public class YahooWeatherProvider implements WeatherProvider { - private static final String TAG = "YahooWeatherProvider"; - - private static final String URL_WEATHER = - "https://weather.yahooapis.com/forecastrss?w=%s&u=%s"; - private static final String URL_LOCATION = - "https://query.yahooapis.com/v1/public/yql?format=json&q=" + - 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 or placetype = 20) and text ="); - private static final String URL_PLACEFINDER = - "https://query.yahooapis.com/v1/public/yql?format=json&q=" + - Uri.encode("select * from geo.places where " + - "text ="); - - private static final String[] LOCALITY_NAMES = new String[] { - "locality1", "locality2", "admin3", "admin2", "admin1" - }; - - private Context mContext; - - public YahooWeatherProvider(Context context) { - mContext = context; - } - - @Override - public int getNameResourceId() { - return R.string.weather_source_yahoo; - } - - @Override - public List<LocationResult> getLocations(String input) { - String language = getLanguage(); - String params = "\"" + input + "\" and lang = \"" + language + "\""; - String url = URL_LOCATION + Uri.encode(params); - JSONObject jsonResults = fetchResults(url); - if (jsonResults == null) { - return null; - } - - try { - JSONArray places = jsonResults.optJSONArray("place"); - if (places == null) { - // Yahoo returns an object instead of an array when there's only one result - places = new JSONArray(); - places.put(jsonResults.getJSONObject("place")); - } - - ArrayList<LocationResult> results = new ArrayList<LocationResult>(); - for (int i = 0; i < places.length(); i++) { - LocationResult result = parsePlace(places.getJSONObject(i)); - if (result != null) { - results.add(result); - } - } - return results; - } catch (JSONException e) { - Log.e(TAG, "Received malformed places data (input=" + input + ", lang=" + language + ")", e); - } - return null; - } - - @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) { - return null; - } - - SAXParserFactory factory = SAXParserFactory.newInstance(); - try { - SAXParser parser = factory.newSAXParser(); - StringReader reader = new StringReader(response); - WeatherHandler handler = new WeatherHandler(); - 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, - handler.condition, handler.conditionCode, handler.temperature, - handler.temperatureUnit, handler.humidity, handler.windSpeed, - 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 (id=" + id + ")", e); - } catch (IOException e) { - Log.e(TAG, "Could not parse weather XML (id=" + id + ")", e); - } - - return null; - } - - private static class WeatherHandler extends DefaultHandler { - String city; - String temperatureUnit, speedUnit; - int windDirection, conditionCode; - float humidity, temperature, windSpeed; - String condition; - ArrayList<DayForecast> forecasts = new ArrayList<DayForecast>(); - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) - throws SAXException { - if (qName.equals("yweather:location")) { - city = attributes.getValue("city"); - } else if (qName.equals("yweather:units")) { - temperatureUnit = attributes.getValue("temperature"); - speedUnit = attributes.getValue("speed"); - } else if (qName.equals("yweather:wind")) { - windDirection = (int) stringToFloat(attributes.getValue("direction"), -1); - windSpeed = stringToFloat(attributes.getValue("speed"), -1); - } else if (qName.equals("yweather:atmosphere")) { - humidity = stringToFloat(attributes.getValue("humidity"), -1); - } else if (qName.equals("yweather:condition")) { - condition = attributes.getValue("text"); - conditionCode = (int) stringToFloat(attributes.getValue("code"), -1); - temperature = stringToFloat(attributes.getValue("temp"), Float.NaN); - } else if (qName.equals("yweather:forecast")) { - 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); - } - } - } - public boolean isComplete() { - return temperatureUnit != null && speedUnit != null && conditionCode >= 0 - && !Float.isNaN(temperature) && !forecasts.isEmpty(); - } - private float stringToFloat(String value, float defaultValue) { - try { - if (value != null) { - return Float.parseFloat(value); - } - } catch (NumberFormatException e) { - // fall through to the return line below - } - return defaultValue; - } - } - - @Override - public WeatherInfo getWeatherInfo(Location location, boolean metric) { - String language = getLanguage(); - String params = String.format(Locale.US, "\"(%f,%f)\" and lang=\"%s\"", - location.getLatitude(), location.getLongitude(), language); - String url = URL_PLACEFINDER + Uri.encode(params); - JSONObject results = fetchResults(url); - if (results == null) { - return null; - } - try { - JSONObject place = results.getJSONObject("place"); - LocationResult result = parsePlace(place); - String woeid = null; - String city = null; - if (result != null) { - woeid = result.id; - city = result.city; - } - // The city name in the placefinder result is HTML encoded :-( - if (city != null) { - city = Html.fromHtml(city).toString(); - } else { - Log.w(TAG, "Can not resolve place name for " + location); - } - - Log.d(TAG, "Resolved location " + location + " to " + city + " (" + woeid + ")"); - - WeatherInfo info = getWeatherInfo(woeid, city, metric); - if (info != null) { - return info; - } - } catch (JSONException e) { - Log.e(TAG, "Received malformed placefinder data (location=" - + location + ", lang=" + language + ")", e); - } - - return null; - } - - private LocationResult parsePlace(JSONObject place) throws JSONException { - LocationResult result = new LocationResult(); - JSONObject country = place.getJSONObject("country"); - - result.id = place.getString("woeid"); - result.country = country.getString("content"); - result.countryId = country.getString("code"); - if (!place.isNull("postal")) { - result.postal = place.getJSONObject("postal").getString("content"); - } - - for (String name : LOCALITY_NAMES) { - if (!place.isNull(name)) { - JSONObject localeObject = place.getJSONObject(name); - result.city = localeObject.getString("content"); - if (localeObject.optString("woeid") != null) { - result.id = localeObject.getString("woeid"); - } - break; - } - } - - 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; - } - - return result; - } - - private JSONObject fetchResults(String url) { - String response = HttpRetriever.retrieve(url); - if (response == null) { - return null; - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Request URL is " + url + ", response is " + response); - } - - try { - JSONObject rootObject = new JSONObject(response); - return rootObject.getJSONObject("query").getJSONObject("results"); - } catch (JSONException 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; - } -} |