diff options
Diffstat (limited to 'src/com')
18 files changed, 1163 insertions, 1631 deletions
diff --git a/src/com/cyanogenmod/lockclock/ClockWidgetProvider.java b/src/com/cyanogenmod/lockclock/ClockWidgetProvider.java index d13d519..c2ea11c 100644 --- a/src/com/cyanogenmod/lockclock/ClockWidgetProvider.java +++ b/src/com/cyanogenmod/lockclock/ClockWidgetProvider.java @@ -24,11 +24,11 @@ import android.net.ConnectivityManager; import android.util.Log; import com.cyanogenmod.lockclock.misc.Constants; +import com.cyanogenmod.lockclock.misc.Preferences; import com.cyanogenmod.lockclock.misc.WidgetUtils; import com.cyanogenmod.lockclock.weather.ForecastActivity; +import com.cyanogenmod.lockclock.weather.WeatherSourceListenerService; import com.cyanogenmod.lockclock.weather.WeatherUpdateService; -import com.cyanogenmod.lockclock.ClockWidgetService; -import com.cyanogenmod.lockclock.WidgetApplication; public class ClockWidgetProvider extends AppWidgetProvider { private static final String TAG = "ClockWidgetProvider"; @@ -64,10 +64,9 @@ public class ClockWidgetProvider extends AppWidgetProvider { // Boot completed, schedule next weather update } else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { - // On first boot lastUpdate will be 0 thus no need to force an update - // Subsequent boots will use cached data - WeatherUpdateService.scheduleNextUpdate(context, false); - + //Since we're using elapsed time since boot, we can't use the timestamp from the + //previous boot so we need to reset the timer + Preferences.setLastWeatherUpadteTimestamp(context, 0); // A widget has been deleted, prevent our handling and ask the super class handle it } else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action) || AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) { @@ -124,6 +123,7 @@ public class ClockWidgetProvider extends AppWidgetProvider { @Override public void onEnabled(Context context) { if (D) Log.d(TAG, "Scheduling next weather update"); + context.startService(new Intent(context, WeatherSourceListenerService.class)); WeatherUpdateService.scheduleNextUpdate(context, true); // Start the broadcast receiver (API 16 devices) @@ -138,6 +138,7 @@ public class ClockWidgetProvider extends AppWidgetProvider { @Override public void onDisabled(Context context) { if (D) Log.d(TAG, "Cleaning up: Clearing all pending alarms"); + context.stopService(new Intent(context, WeatherSourceListenerService.class)); ClockWidgetService.cancelUpdates(context); WeatherUpdateService.cancelUpdates(context); diff --git a/src/com/cyanogenmod/lockclock/ClockWidgetService.java b/src/com/cyanogenmod/lockclock/ClockWidgetService.java index d5f6635..bf4be2c 100644 --- a/src/com/cyanogenmod/lockclock/ClockWidgetService.java +++ b/src/com/cyanogenmod/lockclock/ClockWidgetService.java @@ -27,21 +27,24 @@ import android.content.Intent; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; -import android.provider.Settings; import android.text.TextUtils; import android.text.format.DateFormat; import android.util.Log; import android.util.TypedValue; import android.view.View; import android.widget.RemoteViews; - import com.cyanogenmod.lockclock.calendar.CalendarViewsService; import com.cyanogenmod.lockclock.misc.Constants; import com.cyanogenmod.lockclock.misc.IconUtils; import com.cyanogenmod.lockclock.misc.Preferences; import com.cyanogenmod.lockclock.misc.WidgetUtils; -import com.cyanogenmod.lockclock.weather.WeatherInfo; +import com.cyanogenmod.lockclock.weather.Utils; import com.cyanogenmod.lockclock.weather.WeatherUpdateService; +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.util.WeatherUtils; import java.text.SimpleDateFormat; import java.util.Date; @@ -60,6 +63,7 @@ public class ClockWidgetService extends IntentService { private int[] mWidgetIds; private AppWidgetManager mAppWidgetManager; + private Context mContext; public ClockWidgetService() { super("ClockWidgetService"); @@ -72,6 +76,7 @@ public class ClockWidgetService extends IntentService { ComponentName thisWidget = new ComponentName(this, ClockWidgetProvider.class); mAppWidgetManager = AppWidgetManager.getInstance(this); mWidgetIds = mAppWidgetManager.getAppWidgetIds(thisWidget); + mContext = getApplicationContext(); } @Override @@ -413,27 +418,48 @@ public class ClockWidgetService extends IntentService { int color = Preferences.weatherFontColor(this); int timestampColor = Preferences.weatherTimestampFontColor(this); String iconsSet = Preferences.getWeatherIconSet(this); + final boolean useMetric = Preferences.useMetricUnits(mContext); // Reset no weather visibility weatherViews.setViewVisibility(R.id.weather_no_data, View.GONE); weatherViews.setViewVisibility(R.id.weather_refresh, View.GONE); // Weather Image - int resId = w.getConditionResource(iconsSet); + int resId = IconUtils.getWeatherIconResource(mContext, iconsSet, w.getConditionCode()); weatherViews.setViewVisibility(R.id.weather_image, View.VISIBLE); if (resId != 0) { - weatherViews.setImageViewResource(R.id.weather_image, w.getConditionResource(iconsSet)); + weatherViews.setImageViewResource(R.id.weather_image, + IconUtils.getWeatherIconResource(mContext, iconsSet, w.getConditionCode())); } else { - weatherViews.setImageViewBitmap(R.id.weather_image, w.getConditionBitmap(iconsSet, color)); + weatherViews.setImageViewBitmap(R.id.weather_image, + IconUtils.getWeatherIconBitmap(mContext, iconsSet, color, + w.getConditionCode())); } // Weather Condition - weatherViews.setTextViewText(R.id.weather_condition, w.getCondition()); + weatherViews.setTextViewText(R.id.weather_condition, + Utils.resolveWeatherCondition(mContext, w.getConditionCode())); weatherViews.setViewVisibility(R.id.weather_condition, View.VISIBLE); weatherViews.setTextColor(R.id.weather_condition, color); // Weather Temps Panel - weatherViews.setTextViewText(R.id.weather_temp, w.getFormattedTemperature()); + 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; + } + weatherViews.setTextViewText(R.id.weather_temp, + WeatherUtils.formatTemperature(temp, tempUnit)); weatherViews.setViewVisibility(R.id.weather_temps_panel, View.VISIBLE); weatherViews.setTextColor(R.id.weather_temp, color); @@ -450,7 +476,7 @@ public class ClockWidgetService extends IntentService { // Weather Update Time if (showTimestamp) { - Date updateTime = w.getTimestamp(); + Date updateTime = new Date(w.getTimestamp()); StringBuilder sb = new StringBuilder(); sb.append(DateFormat.format("E", updateTime)); sb.append(" "); @@ -464,8 +490,8 @@ public class ClockWidgetService extends IntentService { // Weather Temps Panel additional items boolean invertLowhigh = Preferences.invertLowHighTemperature(this); - final String low = w.getFormattedLow(); - final String high = w.getFormattedHigh(); + final String low = WeatherUtils.formatTemperature(todaysLow, tempUnit); + final String high = WeatherUtils.formatTemperature(todaysHigh, tempUnit); weatherViews.setTextViewText(R.id.weather_low_high, invertLowhigh ? high + " | " + low : low + " | " + high); weatherViews.setTextColor(R.id.weather_low_high, color); } @@ -482,8 +508,15 @@ public class ClockWidgetService extends IntentService { boolean firstRun = Preferences.isFirstWeatherUpdate(this); // Hide the normal weather stuff - int providerNameResource = Preferences.weatherProvider(this).getNameResourceId(); - String noData = getString(R.string.weather_cannot_reach_provider, getString(providerNameResource)); + final CMWeatherManager weatherManager = CMWeatherManager.getInstance(mContext); + final String activeProviderLabel = weatherManager.getActiveWeatherServiceProviderLabel(); + String noData; + if (activeProviderLabel != null) { + noData = getString(R.string.weather_cannot_reach_provider, activeProviderLabel); + } else { + noData = getString(R.string.weather_source_title) + " " + + getString(R.string.weather_source_not_selected); + } weatherViews.setViewVisibility(R.id.weather_image, View.INVISIBLE); if (!smallWidget) { weatherViews.setViewVisibility(R.id.weather_city, View.GONE); @@ -493,7 +526,13 @@ public class ClockWidgetService extends IntentService { // Set up the no data and refresh indicators weatherViews.setTextViewText(R.id.weather_no_data, noData); - weatherViews.setTextViewText(R.id.weather_refresh, getString(R.string.weather_tap_to_refresh)); + if (activeProviderLabel != null) { + weatherViews.setTextViewText(R.id.weather_refresh, + getString(R.string.weather_tap_to_refresh)); + } else { + weatherViews.setTextViewText(R.id.weather_refresh, + getString(R.string.weather_tap_to_select_source)); + } weatherViews.setTextColor(R.id.weather_no_data, color); weatherViews.setTextColor(R.id.weather_refresh, color); @@ -509,7 +548,11 @@ public class ClockWidgetService extends IntentService { // Register an onClickListener on Weather with the default (Refresh) action if (!firstRun) { - setWeatherClickListener(weatherViews, true); + if (activeProviderLabel != null) { + setWeatherClickListener(weatherViews, true); + } else { + setWeatherClickListener(weatherViews); + } } } @@ -528,7 +571,13 @@ public class ClockWidgetService extends IntentService { weatherViews.setOnClickPendingIntent(R.id.weather_panel, pi); } - + private void setWeatherClickListener(RemoteViews weatherViews) { + PendingIntent pi = PendingIntent.getActivity(mContext, 0, + new Intent("cyanogenmod.intent.action.MANAGE_WEATHER_PROVIDER_SERVICES"), + PendingIntent.FLAG_UPDATE_CURRENT); + weatherViews.setOnClickPendingIntent(R.id.weather_panel, pi); + } + //=============================================================================================== // Calendar related functionality //=============================================================================================== diff --git a/src/com/cyanogenmod/lockclock/misc/Constants.java b/src/com/cyanogenmod/lockclock/misc/Constants.java index a7113f2..fb35a57 100755 --- a/src/com/cyanogenmod/lockclock/misc/Constants.java +++ b/src/com/cyanogenmod/lockclock/misc/Constants.java @@ -38,14 +38,14 @@ public class Constants { public static final String SHOW_WEATHER = "show_weather"; public static final String WEATHER_SOURCE = "weather_source"; public static final String WEATHER_USE_CUSTOM_LOCATION = "weather_use_custom_location"; - public static final String WEATHER_CUSTOM_LOCATION_ID = "weather_custom_location_id"; public static final String WEATHER_CUSTOM_LOCATION_CITY = "weather_custom_location_city"; + public static final String WEATHER_CUSTOM_LOCATION = "weather_custom_location"; + public static final String WEATHER_LOCATION = "weather_location"; public static final String WEATHER_SHOW_LOCATION = "weather_show_location"; public static final String WEATHER_SHOW_TIMESTAMP = "weather_show_timestamp"; public static final String WEATHER_USE_METRIC = "weather_use_metric"; public static final String WEATHER_INVERT_LOWHIGH = "weather_invert_lowhigh"; public static final String WEATHER_REFRESH_INTERVAL = "weather_refresh_interval"; - public static final String WEATHER_LOCATION_ID = "weather_woeid"; public static final String WEATHER_SHOW_WHEN_MINIMIZED = "weather_show_when_minimized"; public static final String WEATHER_FONT_COLOR = "weather_font_color"; public static final String WEATHER_TIMESTAMP_FONT_COLOR = "weather_timestamp_font_color"; diff --git a/src/com/cyanogenmod/lockclock/misc/IconUtils.java b/src/com/cyanogenmod/lockclock/misc/IconUtils.java index 437d075..3a4f4d0 100644 --- a/src/com/cyanogenmod/lockclock/misc/IconUtils.java +++ b/src/com/cyanogenmod/lockclock/misc/IconUtils.java @@ -30,6 +30,7 @@ import android.graphics.drawable.Drawable; import android.util.DisplayMetrics; import android.util.Log; import com.cyanogenmod.lockclock.R; +import com.cyanogenmod.lockclock.weather.Utils; public class IconUtils { private static final String TAG = "IconUtils"; @@ -41,8 +42,9 @@ public class IconUtils { } final Resources res = context.getResources(); - final int resId = res.getIdentifier("weather_" + iconSet + "_" + conditionCode, - "drawable", context.getPackageName()); + final int resId = res.getIdentifier("weather_" + iconSet + "_" + + Utils.addOffsetToConditionCodeFromWeatherContract(conditionCode), "drawable", + context.getPackageName()); if (resId != 0) { return resId; @@ -62,12 +64,13 @@ public class IconUtils { boolean isMonoSet = Constants.MONOCHROME.equals(iconSet); Resources res = null; int resId = 0; + int fixedConditionCode = Utils.addOffsetToConditionCodeFromWeatherContract(conditionCode); if (iconSet.startsWith("ext:")) { String packageName = iconSet.substring(4); try { res = context.getPackageManager().getResourcesForApplication(packageName); - resId = res.getIdentifier("weather_" + conditionCode, "drawable", packageName); + resId = res.getIdentifier("weather_" + fixedConditionCode, "drawable", packageName); } catch (PackageManager.NameNotFoundException e) { // fall back to colored icons iconSet = Constants.COLOR_STD; @@ -75,7 +78,8 @@ public class IconUtils { } if (resId == 0) { String identifier = isMonoSet - ? "weather_" + conditionCode : "weather_" + iconSet + "_" + conditionCode; + ? "weather_" + fixedConditionCode : "weather_" + + iconSet + "_" + fixedConditionCode; res = context.getResources(); resId = res.getIdentifier(identifier, "drawable", context.getPackageName()); } diff --git a/src/com/cyanogenmod/lockclock/misc/Preferences.java b/src/com/cyanogenmod/lockclock/misc/Preferences.java index 941e761..49a4632 100644 --- a/src/com/cyanogenmod/lockclock/misc/Preferences.java +++ b/src/com/cyanogenmod/lockclock/misc/Preferences.java @@ -19,17 +19,43 @@ package com.cyanogenmod.lockclock.misc; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Color; +import cyanogenmod.weather.WeatherInfo; +import cyanogenmod.weather.WeatherLocation; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; -import com.cyanogenmod.lockclock.weather.OpenWeatherMapProvider; -import com.cyanogenmod.lockclock.weather.WeatherInfo; -import com.cyanogenmod.lockclock.weather.WeatherProvider; -import com.cyanogenmod.lockclock.weather.YahooWeatherProvider; - +import java.util.ArrayList; import java.util.Calendar; import java.util.Locale; import java.util.Set; public class Preferences { + + private static final String WEATHER_LOCATION_CITY_ID = "city_id"; + private static final String WEATHER_LOCATION_CITY_NAME = "city_name"; + private static final String WEATHER_LOCATION_STATE = "state"; + private static final String WEATHER_LOCATION_POSTAL_CODE = "postal_code"; + private static final String WEATHER_LOCATION_COUNTRY_ID = "country_id"; + private static final String WEATHER_LOCATION_COUNTRY_NAME = "country_name"; + + private static final String WEATHER_INFO_CITY = "city"; + private static final String WEATHER_INFO_CONDITION_CODE = "condition_code"; + private static final String WEATHER_INFO_TEMPERATURE = "temperature"; + private static final String WEATHER_INFO_TEMPERATURE_UNIT = "temperature_unit"; + private static final String WEATHER_INFO_TIMESTAMP = "timestamp"; + private static final String WEATHER_INFO_HUMIDITY = "humidity"; + private static final String WEATHER_INFO_TODAYS_HIGH = "todays_high"; + private static final String WEATHER_INFO_TODAYS_LOW = "todays_low"; + private static final String WEATHER_INFO_WIND_SPEED = "wind_speed"; + private static final String WEATHER_INFO_WIND_SPEED_UNIT = "wind_speed_unit"; + private static final String WEATHER_INFO_WIND_SPEED_DIRECTION = "wind_speed_direction"; + private static final String WEATHER_INFO_FORECAST = "forecasts"; + + private static final String DAY_FORECAST_CONDITION_CODE = "condition_code"; + private static final String DAY_FORECAST_LOW = "low"; + private static final String DAY_FORECAST_HIGH = "high"; + private Preferences() { } @@ -172,7 +198,7 @@ public class Preferences { public static long weatherRefreshIntervalInMs(Context context) { String value = getPrefs(context).getString(Constants.WEATHER_REFRESH_INTERVAL, "60"); - return Long.parseLong(value) * 60 * 1000; + return Long.parseLong(value) * 60L * 1000L; } public static boolean useCustomWeatherLocation(Context context) { @@ -183,37 +209,148 @@ public class Preferences { getPrefs(context).edit().putBoolean(Constants.WEATHER_USE_CUSTOM_LOCATION, value).apply(); } - public static String customWeatherLocationId(Context context) { - return getPrefs(context).getString(Constants.WEATHER_CUSTOM_LOCATION_ID, null); + public static String getCustomWeatherLocationCity(Context context) { + return getPrefs(context).getString(Constants.WEATHER_CUSTOM_LOCATION_CITY, null); } - public static void setCustomWeatherLocationId(Context context, String id) { - getPrefs(context).edit().putString(Constants.WEATHER_CUSTOM_LOCATION_ID, id).apply(); + public static void setCustomWeatherLocationCity(Context context, String city) { + getPrefs(context).edit().putString(Constants.WEATHER_CUSTOM_LOCATION_CITY, city).apply(); } - public static String customWeatherLocationCity(Context context) { - return getPrefs(context).getString(Constants.WEATHER_CUSTOM_LOCATION_CITY, null); + public static boolean setCustomWeatherLocation(Context context, WeatherLocation weatherLocation) { + if (weatherLocation == null) { + getPrefs(context).edit() + .remove(Constants.WEATHER_CUSTOM_LOCATION).apply(); + return true; + } + try { + JSONObject jsonObject = weatherLocationToJSON(weatherLocation); + getPrefs(context).edit() + .putString(Constants.WEATHER_CUSTOM_LOCATION, jsonObject.toString()).apply(); + return true; + } catch (JSONException e) { + // We're here because weatherLocationToJSON() or jsonObject.toString() failed. + // Either way, it means the pref was not updated + return false; + } } - public static void setCustomWeatherLocationCity(Context context, String city) { - getPrefs(context).edit().putString(Constants.WEATHER_CUSTOM_LOCATION_CITY, city).apply(); + public static WeatherLocation getCustomWeatherLocation(Context context) { + String weatherLocation = getPrefs(context) + .getString(Constants.WEATHER_CUSTOM_LOCATION, null); + + if (weatherLocation == null) { + return null; + } + + try { + JSONObject jsonObject = new JSONObject(weatherLocation); + return JSONToWeatherLocation(jsonObject); + } catch (JSONException e) { + return null; + } } - public static WeatherProvider weatherProvider(Context context) { - String name = getPrefs(context).getString(Constants.WEATHER_SOURCE, "yahoo"); - if (name.equals("openweathermap")) { - return new OpenWeatherMapProvider(context); + private static WeatherLocation JSONToWeatherLocation(JSONObject jsonObject) + throws JSONException { + String cityId; + String cityName; + String state; + String postalCode; + String countryId; + String countryName; + + cityId = jsonObject.getString(WEATHER_LOCATION_CITY_ID); + cityName = jsonObject.getString(WEATHER_LOCATION_CITY_NAME); + state = jsonObject.getString(WEATHER_LOCATION_STATE); + postalCode = jsonObject.getString(WEATHER_LOCATION_POSTAL_CODE); + countryId = jsonObject.getString(WEATHER_LOCATION_COUNTRY_ID); + countryName = jsonObject.getString(WEATHER_LOCATION_COUNTRY_NAME); + + //We need at least city id and city name to build a WeatherLocation + if (cityId == null && cityName == null) { + return null; } - return new YahooWeatherProvider(context); + + WeatherLocation.Builder location = new WeatherLocation.Builder(cityId, cityName); + if (countryId != null) location.setCountryId(countryId); + if (countryName != null) location.setCountry(countryName); + if (state != null) location.setState(state); + if (postalCode != null) location.setPostalCode(postalCode); + + return location.build(); } - public static void setCachedWeatherInfo(Context context, long timestamp, WeatherInfo data) { + private static JSONObject weatherLocationToJSON(WeatherLocation location) throws JSONException { + return new JSONObject() + .put(WEATHER_LOCATION_CITY_ID, location.getCityId()) + .put(WEATHER_LOCATION_CITY_NAME, location.getCity()) + .put(WEATHER_LOCATION_STATE, location.getState()) + .put(WEATHER_LOCATION_POSTAL_CODE, location.getPostalCode()) + .put(WEATHER_LOCATION_COUNTRY_ID, location.getCountryId()) + .put(WEATHER_LOCATION_COUNTRY_NAME, location.getCountry()); + } + + public static void setCachedWeatherInfo(Context context, long timestamp, WeatherInfo info) { SharedPreferences.Editor editor = getPrefs(context).edit(); editor.putLong(Constants.WEATHER_LAST_UPDATE, timestamp); - if (data != null) { + if (info != null) { // We now have valid weather data to display - editor.putBoolean(Constants.WEATHER_FIRST_UPDATE, false); - editor.putString(Constants.WEATHER_DATA, data.toSerializedString()); + JSONObject jsonObject = new JSONObject(); + boolean serialized = false; + try { + //These members always return a value that can be parsed + jsonObject + .put(WEATHER_INFO_CITY, info.getCity()) + .put(WEATHER_INFO_CONDITION_CODE, info.getConditionCode()) + .put(WEATHER_INFO_TEMPERATURE, info.getTemperature()) + .put(WEATHER_INFO_TEMPERATURE_UNIT, info.getTemperatureUnit()) + .put(WEATHER_INFO_TIMESTAMP, info.getTimestamp()); + + // Handle special cases. JSONObject.put(key, double) does not allow + // Double.NaN, so we store it as a string. JSONObject.getDouble() will parse the + // "NaN" string and return Double.NaN, which is what we want + double humidity = info.getHumidity(); + jsonObject.put(WEATHER_INFO_HUMIDITY, Double.isNaN(humidity) ? "NaN" : humidity); + + double todaysHigh = info.getTodaysHigh(); + jsonObject.put(WEATHER_INFO_TODAYS_HIGH, Double.isNaN(todaysHigh) + ? "NaN" : todaysHigh); + + double todaysLow = info.getTodaysLow(); + jsonObject.put(WEATHER_INFO_TODAYS_LOW, Double.isNaN(todaysLow) + ? "NaN" : todaysLow); + + double windSpeed = info.getWindSpeed(); + double windDirection = info.getWindDirection(); + jsonObject.put(WEATHER_INFO_WIND_SPEED, Double.isNaN(windSpeed) ? "NaN" : windSpeed) + .put(WEATHER_INFO_WIND_SPEED_UNIT, info.getWindSpeedUnit()) + .put(WEATHER_INFO_WIND_SPEED_DIRECTION, Double.isNaN(windDirection) + ? "NaN" : windDirection); + + JSONArray forecastArray = new JSONArray(); + for (WeatherInfo.DayForecast forecast : info.getForecasts()) { + JSONObject jsonForecast = new JSONObject() + .put(DAY_FORECAST_CONDITION_CODE, forecast.getConditionCode()); + + double low = forecast.getLow(); + jsonForecast.put(DAY_FORECAST_LOW, Double.isNaN(low) ? "NaN" : low); + double high = forecast.getHigh(); + jsonForecast.put(DAY_FORECAST_HIGH, Double.isNaN(high) ? "NaN" : high); + forecastArray.put(jsonForecast); + } + jsonObject.put(WEATHER_INFO_FORECAST, forecastArray); + serialized = true; + } catch (JSONException e) { + // We're here because something went wrong while creating the JSON object. + // The code below will check for success and proceed accordingly + } + if (serialized) { + editor.putString(Constants.WEATHER_DATA, jsonObject.toString()); + editor.putBoolean(Constants.WEATHER_FIRST_UPDATE, false); + } + } else { + editor.remove(Constants.WEATHER_DATA); } editor.apply(); } @@ -222,17 +359,78 @@ public class Preferences { return getPrefs(context).getLong(Constants.WEATHER_LAST_UPDATE, 0); } + public static void setLastWeatherUpadteTimestamp(Context context, long timestamp) { + getPrefs(context).edit().putLong(Constants.WEATHER_LAST_UPDATE, timestamp).apply(); + } + public static WeatherInfo getCachedWeatherInfo(Context context) { - return WeatherInfo.fromSerializedString(context, - getPrefs(context).getString(Constants.WEATHER_DATA, null)); + final String cachedInfo = getPrefs(context).getString(Constants.WEATHER_DATA, null); + + if (cachedInfo == null) return null; + + String city; + int conditionCode; + double temperature; + int tempUnit; + double humidity; + double windSpeed; + double windDirection; + double todaysHigh; + double todaysLow; + int windSpeedUnit; + long timestamp; + ArrayList<WeatherInfo.DayForecast> forecastList = new ArrayList<>(); + + try { + JSONObject cached = new JSONObject(cachedInfo); + city = cached.getString(WEATHER_INFO_CITY); + conditionCode = cached.getInt(WEATHER_INFO_CONDITION_CODE); + temperature = cached.getDouble(WEATHER_INFO_TEMPERATURE); + tempUnit = cached.getInt(WEATHER_INFO_TEMPERATURE_UNIT); + humidity = cached.getDouble(WEATHER_INFO_HUMIDITY); + windSpeed = cached.getDouble(WEATHER_INFO_WIND_SPEED); + windDirection = cached.getDouble(WEATHER_INFO_WIND_SPEED_DIRECTION); + windSpeedUnit = cached.getInt(WEATHER_INFO_WIND_SPEED_UNIT); + timestamp = cached.getLong(WEATHER_INFO_TIMESTAMP); + todaysHigh = cached.getDouble(WEATHER_INFO_TODAYS_HIGH); + todaysLow = cached.getDouble(WEATHER_INFO_TODAYS_LOW); + JSONArray forecasts = cached.getJSONArray(WEATHER_INFO_FORECAST); + for (int indx = 0; indx < forecasts.length(); indx++) { + JSONObject forecast = forecasts.getJSONObject(indx); + double low; + double high; + int code; + low = forecast.getDouble(DAY_FORECAST_LOW); + high = forecast.getDouble(DAY_FORECAST_HIGH); + code = forecast.getInt(DAY_FORECAST_CONDITION_CODE); + WeatherInfo.DayForecast.Builder f = new WeatherInfo.DayForecast.Builder(code); + if (!Double.isNaN(low)) f.setLow(low); + if (!Double.isNaN(high)) f.setHigh(high); + forecastList.add(f.build()); + } + WeatherInfo.Builder weatherInfo = new WeatherInfo.Builder(city, temperature, tempUnit) + .setWeatherCondition(conditionCode) + .setTimestamp(timestamp); + + if (!Double.isNaN(humidity)) weatherInfo.setHumidity(humidity); + if (!Double.isNaN(windSpeed) && !Double.isNaN(windDirection)) { + weatherInfo.setWind(windSpeed, windDirection, windSpeedUnit); + } + if (forecastList.size() > 0) weatherInfo.setForecast(forecastList); + if (!Double.isNaN(todaysHigh)) weatherInfo.setTodaysHigh(todaysHigh); + if (!Double.isNaN(todaysLow)) weatherInfo.setTodaysLow(todaysLow); + return weatherInfo.build(); + } catch (JSONException e) { + } + return null; } - public static String getCachedLocationId(Context context) { - return getPrefs(context).getString(Constants.WEATHER_LOCATION_ID, null); + public static void setWeatherSource(Context context, String source) { + getPrefs(context).edit().putString(Constants.WEATHER_SOURCE, source).apply(); } - public static void setCachedLocationId(Context context, String id) { - getPrefs(context).edit().putString(Constants.WEATHER_LOCATION_ID, id).apply(); + public static String getWeatherSource(Context context) { + return getPrefs(context).getString(Constants.WEATHER_SOURCE, null); } public static Set<String> calendarsToDisplay(Context context) { diff --git a/src/com/cyanogenmod/lockclock/preference/CustomLocationPreference.java b/src/com/cyanogenmod/lockclock/preference/CustomLocationPreference.java index 6d0992f..c290cf5 100644 --- a/src/com/cyanogenmod/lockclock/preference/CustomLocationPreference.java +++ b/src/com/cyanogenmod/lockclock/preference/CustomLocationPreference.java @@ -17,27 +17,27 @@ package com.cyanogenmod.lockclock.preference; import android.app.AlertDialog; -import android.app.Dialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; -import android.os.AsyncTask; import android.os.Bundle; +import android.os.Handler; import android.preference.EditTextPreference; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.widget.Button; import android.widget.Toast; - import com.cyanogenmod.lockclock.R; import com.cyanogenmod.lockclock.misc.Preferences; -import com.cyanogenmod.lockclock.weather.WeatherProvider.LocationResult; +import cyanogenmod.weather.CMWeatherManager; +import cyanogenmod.weather.WeatherLocation; import java.util.HashSet; import java.util.List; -public class CustomLocationPreference extends EditTextPreference { +public class CustomLocationPreference extends EditTextPreference + implements CMWeatherManager.LookupCityRequestListener { public CustomLocationPreference(Context context) { super(context); } @@ -48,18 +48,35 @@ public class CustomLocationPreference extends EditTextPreference { super(context, attrs, defStyle); } + private ProgressDialog mProgressDialog; + private int mCustomLocationRequestId; + private Handler mHandler; @Override protected void showDialog(Bundle state) { super.showDialog(state); + mHandler = new Handler(getContext().getMainLooper()); final AlertDialog d = (AlertDialog) getDialog(); - Button okButton = d.getButton(DialogInterface.BUTTON_POSITIVE); - + final Button okButton = d.getButton(DialogInterface.BUTTON_POSITIVE); okButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { CustomLocationPreference.this.onClick(d, DialogInterface.BUTTON_POSITIVE); - new WeatherLocationTask(d, getEditText().getText().toString()).execute(); + final String customLocationToLookUp = getEditText().getText().toString(); + if (TextUtils.equals(customLocationToLookUp, "")) return; + final CMWeatherManager weatherManager = CMWeatherManager.getInstance(getContext()); + mProgressDialog = new ProgressDialog(getContext()); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setMessage(getContext().getString(R.string.weather_progress_title)); + mProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + weatherManager.cancelRequest(mCustomLocationRequestId); + } + }); + mCustomLocationRequestId = weatherManager.lookupCity(customLocationToLookUp, + CustomLocationPreference.this); + mProgressDialog.show(); } }); } @@ -68,10 +85,12 @@ public class CustomLocationPreference extends EditTextPreference { protected void onBindDialogView(View view) { super.onBindDialogView(view); - String location = Preferences.customWeatherLocationCity(getContext()); + String location = Preferences.getCustomWeatherLocationCity(getContext()); if (location != null) { getEditText().setText(location); getEditText().setSelection(location.length()); + } else { + getEditText().setText(""); } } @@ -81,115 +100,88 @@ public class CustomLocationPreference extends EditTextPreference { super.onDialogClosed(false); } - private class WeatherLocationTask extends AsyncTask<Void, Void, List<LocationResult>> { - private Dialog mDialog; - private ProgressDialog mProgressDialog; - private String mLocation; - - public WeatherLocationTask(Dialog dialog, String location) { - mDialog = dialog; - mLocation = location; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - - final Context context = getContext(); + private void handleResultDisambiguation(final List<WeatherLocation> results) { + CharSequence[] items = buildItemList(results); + new AlertDialog.Builder(getContext()) + .setSingleChoiceItems(items, -1, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + applyLocation(results.get(which)); + dialog.dismiss(); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .setTitle(R.string.weather_select_location) + .show(); + } - mProgressDialog = new ProgressDialog(context); - mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); - mProgressDialog.setMessage(context.getString(R.string.weather_progress_title)); - mProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - cancel(true); - } - }); - mProgressDialog.show(); - } + private CharSequence[] buildItemList(List<WeatherLocation> results) { + boolean needCountry = false, needPostal = false; + String countryId = results.get(0).getCountryId(); + HashSet<String> postalIds = new HashSet<>(); - @Override - protected List<LocationResult> doInBackground(Void... input) { - return Preferences.weatherProvider(getContext()).getLocations(mLocation); + for (WeatherLocation result : results) { + if (!TextUtils.equals(result.getCountryId(), countryId)) { + needCountry = true; + } + String postalId = result.getCountryId() + "##" + result.getCity(); + if (postalIds.contains(postalId)) { + needPostal = true; + } + postalIds.add(postalId); + if (needPostal && needCountry) { + break; + } } - @Override - protected void onPostExecute(List<LocationResult> results) { - super.onPostExecute(results); - - final Context context = getContext(); - - if (results == null || results.isEmpty()) { - Toast.makeText(context, - context.getString(R.string.weather_retrieve_location_dialog_title), - Toast.LENGTH_SHORT) - .show(); - } else if (results.size() > 1) { - handleResultDisambiguation(results); - } else { - applyLocation(results.get(0)); + int count = results.size(); + CharSequence[] items = new CharSequence[count]; + for (int i = 0; i < count; i++) { + WeatherLocation result = results.get(i); + StringBuilder builder = new StringBuilder(); + if (needPostal && result.getPostalCode() != null) { + builder.append(result.getPostalCode()).append(" "); } - mProgressDialog.dismiss(); + builder.append(result.getCity()); + if (needCountry) { + String country = result.getCountry() != null + ? result.getCountry() : result.getCountryId(); + builder.append(" (").append(country).append(")"); + } + items[i] = builder.toString(); } + return items; + } - private void handleResultDisambiguation(final List<LocationResult> results) { - CharSequence[] items = buildItemList(results); - new AlertDialog.Builder(getContext()) - .setSingleChoiceItems(items, -1, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - applyLocation(results.get(which)); - dialog.dismiss(); - } - }) - .setNegativeButton(android.R.string.cancel, null) - .setTitle(R.string.weather_select_location) - .show(); + private void applyLocation(final WeatherLocation result) { + if (Preferences.setCustomWeatherLocation(getContext(), result)) { + String cityName = result.getCity(); + String state = result.getState(); + String country = result.getCountry(); + setText(cityName + "," + state + "/" + country); } + final AlertDialog d = (AlertDialog) getDialog(); + d.dismiss(); + } - private CharSequence[] buildItemList(List<LocationResult> results) { - boolean needCountry = false, needPostal = false; - String countryId = results.get(0).countryId; - HashSet<String> postalIds = new HashSet<String>(); - - for (LocationResult result : results) { - if (!TextUtils.equals(result.countryId, countryId)) { - needCountry = true; - } - String postalId = result.countryId + "##" + result.city; - if (postalIds.contains(postalId)) { - needPostal = true; - } - postalIds.add(postalId); - if (needPostal && needCountry) { - break; - } - } - - int count = results.size(); - CharSequence[] items = new CharSequence[count]; - for (int i = 0; i < count; i++) { - LocationResult result = results.get(i); - StringBuilder builder = new StringBuilder(); - if (needPostal && result.postal != null) { - builder.append(result.postal).append(" "); - } - builder.append(result.city); - if (needCountry) { - String country = result.country != null - ? result.country : result.countryId; - builder.append(" (").append(country).append(")"); + @Override + public void onLookupCityRequestCompleted(int status, final List<WeatherLocation> locations) { + mHandler.post(new Runnable() { + @Override + public void run() { + final Context context = getContext(); + if (locations == null || locations.isEmpty()) { + Toast.makeText(context, + context.getString(R.string.weather_retrieve_location_dialog_title), + Toast.LENGTH_SHORT) + .show(); + } else if (locations.size() > 1) { + handleResultDisambiguation(locations); + } else { + applyLocation(locations.get(0)); } - items[i] = builder.toString(); + mProgressDialog.dismiss(); } - return items; - } - - private void applyLocation(final LocationResult result) { - Preferences.setCustomWeatherLocationId(getContext(), result.id); - setText(result.city); - mDialog.dismiss(); - } + }); } } diff --git a/src/com/cyanogenmod/lockclock/preference/WeatherPreferences.java b/src/com/cyanogenmod/lockclock/preference/WeatherPreferences.java index 1a42a20..0824a4b 100644 --- a/src/com/cyanogenmod/lockclock/preference/WeatherPreferences.java +++ b/src/com/cyanogenmod/lockclock/preference/WeatherPreferences.java @@ -19,7 +19,6 @@ package com.cyanogenmod.lockclock.preference; import android.Manifest; import android.app.AlertDialog; import android.app.Dialog; -import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -31,31 +30,24 @@ import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; import android.preference.SwitchPreference; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; - 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.weather.WeatherUpdateService; +import cyanogenmod.weather.CMWeatherManager; public class WeatherPreferences extends PreferenceFragment implements - SharedPreferences.OnSharedPreferenceChangeListener, Preference.OnPreferenceChangeListener { + SharedPreferences.OnSharedPreferenceChangeListener, Preference.OnPreferenceChangeListener, + CMWeatherManager.WeatherServiceProviderChangeListener { private static final String TAG = "WeatherPreferences"; private static final int LOCATION_PERMISSION_REQUEST_CODE = 1; - private static final String[] LOCATION_PREF_KEYS = new String[] { - Constants.WEATHER_USE_CUSTOM_LOCATION, - Constants.WEATHER_CUSTOM_LOCATION_CITY - }; - private static final String[] WEATHER_REFRESH_KEYS = new String[] { - Constants.SHOW_WEATHER, - Constants.WEATHER_REFRESH_INTERVAL - }; - private SwitchPreference mUseCustomLoc; private EditTextPreference mCustomWeatherLoc; private ListPreference mFontColor; @@ -65,8 +57,8 @@ public class WeatherPreferences extends PreferenceFragment implements private SwitchPreference mUseCustomlocation; private SwitchPreference mShowWeather; private Context mContext; - private ContentResolver mResolver; private Runnable mPostResumeRunnable; + private PreferenceScreen mWeatherSource; @Override public void onCreate(Bundle savedInstanceState) { @@ -74,7 +66,6 @@ public class WeatherPreferences extends PreferenceFragment implements getPreferenceManager().setSharedPreferencesName(Constants.PREF_NAME); addPreferencesFromResource(R.xml.preferences_weather); mContext = getActivity(); - mResolver = mContext.getContentResolver(); // Load items that need custom summaries etc. mUseCustomLoc = (SwitchPreference) findPreference(Constants.WEATHER_USE_CUSTOM_LOCATION); @@ -84,6 +75,18 @@ public class WeatherPreferences extends PreferenceFragment implements mIconSet = (IconSelectionPreference) findPreference(Constants.WEATHER_ICONS); mUseMetric = (SwitchPreference) findPreference(Constants.WEATHER_USE_METRIC); mUseCustomlocation = (SwitchPreference) findPreference(Constants.WEATHER_USE_CUSTOM_LOCATION); + mWeatherSource = (PreferenceScreen) findPreference(Constants.WEATHER_SOURCE); + mWeatherSource.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object o) { + if (Preferences.getWeatherSource(mContext) != null && mShowWeather.isChecked()) { + mWeatherSource.notifyDependencyChange(false); + } else { + mWeatherSource.notifyDependencyChange(true); + } + return false; + } + }); mShowWeather = (SwitchPreference) findPreference(Constants.SHOW_WEATHER); mShowWeather.setOnPreferenceChangeListener(this); @@ -116,15 +119,34 @@ public class WeatherPreferences extends PreferenceFragment implements mPostResumeRunnable = null; } + final CMWeatherManager weatherManager = CMWeatherManager.getInstance(mContext); + weatherManager.registerWeatherServiceProviderChangeListener(this); + + mWeatherSource.setEnabled(mShowWeather.isChecked()); + updateLocationSummary(); updateFontColorsSummary(); updateIconSetSummary(); + updateWeatherProviderSummary(getWeatherProviderName()); } @Override public void onPause() { super.onPause(); getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + final CMWeatherManager weatherManager = CMWeatherManager.getInstance(mContext); + weatherManager.unregisterWeatherServiceProviderChangeListener(this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mUseCustomlocation.isChecked() + && Preferences.getCustomWeatherLocationCity(mContext) == null) { + //The user decided to toggle the custom location switch, but forgot to set a custom + //location, we need to go back to geo location + Preferences.setUseCustomWeatherLocation(mContext, false); + } } @Override @@ -152,18 +174,24 @@ public class WeatherPreferences extends PreferenceFragment implements forceWeatherUpdate = true; } - // If the weather source has changes, invalidate the custom location settings and change - // back to GeoLocation to force the user to specify a new custom location if needed if (TextUtils.equals(key, Constants.WEATHER_SOURCE)) { - Preferences.setCustomWeatherLocationId(mContext, null); + // The weather source changed, invalidate the custom location settings and change + // back to GeoLocation to force the user to specify a new custom location if needed Preferences.setCustomWeatherLocationCity(mContext, null); + Preferences.setCustomWeatherLocation(mContext, null); Preferences.setUseCustomWeatherLocation(mContext, false); mUseCustomlocation.setChecked(false); updateLocationSummary(); } - if (key.equals(Constants.WEATHER_USE_CUSTOM_LOCATION) - || key.equals(Constants.WEATHER_CUSTOM_LOCATION_CITY)) { + if (key.equals(Constants.WEATHER_USE_CUSTOM_LOCATION)) { + if (!mUseCustomLoc.isChecked() || (mUseCustomLoc.isChecked() && + Preferences.getCustomWeatherLocation(mContext) != null)) { + forceWeatherUpdate = true; + } + } + + if (key.equals(Constants.WEATHER_CUSTOM_LOCATION_CITY) && mUseCustomLoc.isChecked()) { forceWeatherUpdate = true; } @@ -171,6 +199,15 @@ public class WeatherPreferences extends PreferenceFragment implements needWeatherUpdate = true; } + if (key.equals(Constants.SHOW_WEATHER)) { + mWeatherSource.setEnabled(mShowWeather.isChecked()); + if (Preferences.getWeatherSource(mContext) != null && mShowWeather.isChecked()) { + mWeatherSource.notifyDependencyChange(false); + } else { + mWeatherSource.notifyDependencyChange(true); + } + } + if (Constants.DEBUG) { Log.v(TAG, "Preference " + key + " changed, need update " + needWeatherUpdate + " force update " + forceWeatherUpdate); @@ -199,7 +236,7 @@ public class WeatherPreferences extends PreferenceFragment implements private void updateLocationSummary() { if (mUseCustomLoc.isChecked()) { - String location = Preferences.customWeatherLocationCity(mContext); + String location = Preferences.getCustomWeatherLocationCity(mContext); if (location == null) { location = getResources().getString(R.string.unknown); } @@ -274,4 +311,30 @@ public class WeatherPreferences extends PreferenceFragment implements } return true; } + + @Override + public void onWeatherServiceProviderChanged(String providerName) { + updateWeatherProviderSummary(providerName); + } + + private void updateWeatherProviderSummary(String providerName) { + if (providerName != null) { + mWeatherSource.setSummary(providerName); + Preferences.setWeatherSource(mContext, providerName); + } else { + mWeatherSource.setSummary(R.string.weather_source_not_selected); + Preferences.setWeatherSource(mContext, null); + } + + if (providerName != null && mShowWeather.isChecked()) { + mWeatherSource.notifyDependencyChange(false); + } else { + mWeatherSource.notifyDependencyChange(true); + } + } + + private String getWeatherProviderName() { + final CMWeatherManager weatherManager = CMWeatherManager.getInstance(mContext); + return weatherManager.getActiveWeatherServiceProviderLabel(); + } } 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; - } -} |