diff --git a/background-service/src/main/java/com/androidsx/rainnotifications/backgroundservice/WeatherService.java b/background-service/src/main/java/com/androidsx/rainnotifications/backgroundservice/WeatherService.java index 182ea68..24e56e4 100644 --- a/background-service/src/main/java/com/androidsx/rainnotifications/backgroundservice/WeatherService.java +++ b/background-service/src/main/java/com/androidsx/rainnotifications/backgroundservice/WeatherService.java @@ -14,7 +14,7 @@ import com.androidsx.rainnotifications.backgroundservice.util.NotificationHelper; import com.androidsx.rainnotifications.backgroundservice.util.UserLocationFetcher; import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientException; -import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientResponseListener; +import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientHourlyResponseListener; import com.androidsx.rainnotifications.model.Alert; import com.androidsx.rainnotifications.model.Day; import com.androidsx.rainnotifications.model.DayTemplate; @@ -59,7 +59,7 @@ public int onStartCommand(final Intent intent, int flags, int startId) { UserLocationFetcher.getUserLocation(this, new UserLocationFetcher.UserLocationResultListener() { @Override public void onLocationSuccess(Location location) { - WeatherClientFactory.requestForecastForLocation(getApplicationContext(), location.getLatitude(), location.getLongitude(), new WeatherClientResponseListener() { + WeatherClientFactory.requestHourlyForecastForLocation(getApplicationContext(), location.getLatitude(), location.getLongitude(), new WeatherClientHourlyResponseListener() { @Override public void onForecastSuccess(ForecastTable forecastTable) { if (intent != null && intent.getIntExtra(Constants.Extras.EXTRA_DAY_ALARM, 0) == Constants.Alarms.DAY_ALARM_ID) { diff --git a/dailyclothes/src/main/java/com/androidsx/rainnotifications/dailyclothes/ui/home/DailyForecastAdapter.java b/dailyclothes/src/main/java/com/androidsx/rainnotifications/dailyclothes/ui/home/DailyForecastAdapter.java deleted file mode 100644 index e9e94dc..0000000 --- a/dailyclothes/src/main/java/com/androidsx/rainnotifications/dailyclothes/ui/home/DailyForecastAdapter.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.androidsx.rainnotifications.dailyclothes.ui.home; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import com.androidsx.rainnotifications.dailyclothes.R; -import com.androidsx.rainnotifications.dailyclothes.model.MockDailyForecast; - -import java.util.List; - -public class DailyForecastAdapter extends BaseAdapter { - private List dailyForecasts; - private LayoutInflater inflater; - - public DailyForecastAdapter(LayoutInflater inflater, List dailyForecasts) { - this.inflater = inflater; - this.dailyForecasts = dailyForecasts; - } - - @Override - public int getCount() { - return dailyForecasts.size(); - } - - @Override - public Object getItem(int position) { - return dailyForecasts.get(position); - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if(convertView == null) { - convertView = inflater.inflate(R.layout.item_daily_forecast, null); - convertView.setTag(new DailyForecastHolder(convertView)); - } - - ((DailyForecastHolder) convertView.getTag()).update(dailyForecasts.get(position)); - - return convertView; - } - - private class DailyForecastHolder { - private ImageView icon; - private TextView day; - private TextView minTemperature; - private TextView maxTemperature; - - public DailyForecastHolder(View v) { - icon = (ImageView) v.findViewById(R.id.daily_forecast_icon); - day = (TextView) v.findViewById(R.id.daily_forecast_day); - minTemperature = (TextView) v.findViewById(R.id.daily_forecast_min_temperature); - maxTemperature = (TextView) v.findViewById(R.id.daily_forecast_max_temperature); - } - - public void update(MockDailyForecast mockDailyForecast) { - icon.setImageResource(mockDailyForecast.iconRes); - day.setText(mockDailyForecast.day); - minTemperature.setText("" + mockDailyForecast.minTemperature); - maxTemperature.setText("" + mockDailyForecast.maxTemperature); - } - } -} \ No newline at end of file diff --git a/dailyclothes/src/main/java/com/androidsx/rainnotifications/dailyclothes/ui/home/HomeActivity.java b/dailyclothes/src/main/java/com/androidsx/rainnotifications/dailyclothes/ui/home/HomeActivity.java index 8fcc54b..062c901 100644 --- a/dailyclothes/src/main/java/com/androidsx/rainnotifications/dailyclothes/ui/home/HomeActivity.java +++ b/dailyclothes/src/main/java/com/androidsx/rainnotifications/dailyclothes/ui/home/HomeActivity.java @@ -15,8 +15,10 @@ import android.text.method.LinkMovementMethod; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.AdapterView; +import android.widget.BaseAdapter; import android.widget.Button; import android.widget.HorizontalScrollView; import android.widget.ImageView; @@ -28,10 +30,12 @@ import com.androidsx.rainnotifications.backgroundservice.util.UserLocationFetcher; import com.androidsx.rainnotifications.dailyclothes.R; import com.androidsx.rainnotifications.dailyclothes.model.Clothes; -import com.androidsx.rainnotifications.dailyclothes.model.MockDailyForecast; import com.androidsx.rainnotifications.dailyclothes.ui.widget.customfont.CustomTextView; +import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientDailyResponseListener; import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientException; -import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientResponseListener; +import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientHourlyResponseListener; +import com.androidsx.rainnotifications.model.DailyForecast; +import com.androidsx.rainnotifications.model.DailyForecastTable; import com.androidsx.rainnotifications.model.Day; import com.androidsx.rainnotifications.model.DayTemplate; import com.androidsx.rainnotifications.model.DayTemplateLoaderFactory; @@ -45,6 +49,7 @@ import com.squareup.picasso.Picasso; import org.joda.time.DateTime; +import org.joda.time.DateTimeConstants; import org.joda.time.Duration; import java.lang.reflect.Field; @@ -63,6 +68,7 @@ public class HomeActivity extends FragmentActivity { private static final long HEART_BUTTON_ANIMATION_DURATION = 200; private static final int MAX_FORECAST_ITEMS = 24; private static final int COLOR_TRANSITION_DURATION = 100; + private static final int WEEK_FORECAST_DAYS = 5; private enum ForecastDataState {LOADING, ERROR_LOCATION, ERROR_FORECAST, LOADED, DONE}; @@ -86,6 +92,8 @@ public float getScrollValue() { private ForecastTable forecastTable; private Day day; private String forecastSummaryMessage; + private String city; + private DailyForecastTable dailyForecastTable; private boolean activityDestroyed = false; // Panic mode. It's used for not modify any view. private View frameLoading; @@ -105,6 +113,9 @@ public float getScrollValue() { private LinearLayout hourlyLinear; private HorizontalScrollView hourlyScroll; + private DailyForecastAdapter dailyAdapter; + private CustomTextView cityLabel; + private Integer todayCollapsedBackgroundColor; private Integer todayCollapsedPrimaryColor; private Integer todayCollapsedSecondaryColor; @@ -180,14 +191,21 @@ private void setForecastDataState(ForecastDataState newState) { private void getForecastData() { // FIXME: we do exactly the same in the weather service. grr.. UserLocationFetcher.getUserLocation(this, new UserLocationFetcher.UserLocationResultListener() { + + private boolean hourlyDone = false; + private boolean dailyDone = false; + @Override public void onLocationSuccess(final Location location) { - WeatherClientFactory.requestForecastForLocation(HomeActivity.this, location.getLatitude(), location.getLongitude(), new WeatherClientResponseListener() { + + // FIXME: This happens on UI Thread and skips frames. + HomeActivity.this.city = UserLocationFetcher.getLocationAddress(HomeActivity.this, location.getLatitude(), location.getLongitude()); + + WeatherClientFactory.requestHourlyForecastForLocation(HomeActivity.this, location.getLatitude(), location.getLongitude(), new WeatherClientHourlyResponseListener() { @Override public void onForecastSuccess(ForecastTable forecastTable) { // FIXME: This happens on UI Thread and skips frames. - HomeActivity.this.forecastTable = forecastTable; HomeActivity.this.forecastTableTime = new DateTime(); HomeActivity.this.day = new Day(forecastTable); @@ -199,12 +217,28 @@ public void onForecastSuccess(ForecastTable forecastTable) { HomeActivity.this.forecastSummaryMessage = template.resolveMessage(HomeActivity.this, HomeActivity.this.day); } - setForecastDataState(ForecastDataState.LOADED); + hourlyDone = true; + checkBothRequestDone(); } @Override public void onForecastFailure(WeatherClientException exception) { - Timber.e(exception, "Failed to get the forecast"); + Timber.e(exception, "Failed to get hourly forecast"); + setForecastDataState(ForecastDataState.ERROR_FORECAST); + } + }); + + WeatherClientFactory.requestDailyForecastForLocation(HomeActivity.this, location.getLatitude(), location.getLongitude(), new WeatherClientDailyResponseListener() { + @Override + public void onForecastSuccess(DailyForecastTable dailyForecastTable) { + HomeActivity.this.dailyForecastTable = dailyForecastTable; + dailyDone = true; + checkBothRequestDone(); + } + + @Override + public void onForecastFailure(WeatherClientException weatherClientException) { + Timber.e(weatherClientException, "Failed to get daily forecast"); setForecastDataState(ForecastDataState.ERROR_FORECAST); } }); @@ -215,6 +249,12 @@ public void onLocationFailure(UserLocationFetcher.UserLocationException exceptio Timber.e(exception, "Failed to get the location"); setForecastDataState(ForecastDataState.ERROR_LOCATION); } + + private void checkBothRequestDone() { + if(hourlyDone && dailyDone) { + setForecastDataState(ForecastDataState.LOADED); + } + } }); } @@ -237,6 +277,7 @@ private void setupUI() { heartButton = findViewById(R.id.heart_button); hourlyLinear = (LinearLayout) findViewById(R.id.hourly_forecast); hourlyScroll = (HorizontalScrollView) findViewById(R.id.hourly_scroll); + cityLabel = (CustomTextView) findViewById(R.id.week_forecast_city); todayDivider = findViewById(R.id.today_forecast_divider); todayMinTemperatureIcon = (ImageView) findViewById(R.id.today_min_temp_icon); @@ -302,7 +343,8 @@ private void setupClothesViewPager() { private void setupWeekForecastList() { ListView weekList = (ListView) findViewById(R.id.week_forecast_list_view); - weekList.setAdapter(new DailyForecastAdapter(getLayoutInflater(), MockDailyForecast.getMockList())); + dailyAdapter = new DailyForecastAdapter(getLayoutInflater(), null); + weekList.setAdapter(dailyAdapter); weekList.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { @@ -351,7 +393,14 @@ private void updateUI() { maxTemperature.setText(temperatureFormat.format(day.getMaxTemperature().getWeatherWrapper().getTemperature(localeScale))); slidingPanelSummary.setText(forecastSummaryMessage); updateHourlyForecastList(); - demoPanel(); + updateDailyForecastList(); + + slidingPanel.postDelayed(new Runnable() { + @Override + public void run() { + demoPanel(); + } + }, FORECAST_DATA_DONE_DELAY/2); slidingPanel.postDelayed(new Runnable() { @Override @@ -404,6 +453,11 @@ private void updateHourlyForecastList() { } } + private void updateDailyForecastList() { + cityLabel.setText(city.toUpperCase()); + dailyAdapter.updateForecast(dailyForecastTable.getDailyForecastList()); + } + private int getWeatherIcon(WeatherType type) { // TODO: Pensar que hacer con las versiones night. switch (type) { @@ -505,7 +559,7 @@ private void showPanel() { private void demoPanel() { animateColors(PanelScrollValue.EXPANDED); - slidingPanel.expandPanel(); + slidingPanel.expandPanel(PanelScrollValue.EXPANDED.getScrollValue()); } private void hidePanel() { @@ -582,6 +636,90 @@ public void onPageScrollStateChanged(int state) { } } + private class DailyForecastAdapter extends BaseAdapter { + private List dailyForecasts; + private LayoutInflater inflater; + + public DailyForecastAdapter(LayoutInflater inflater, List dailyForecasts) { + this.inflater = inflater; + this.dailyForecasts = dailyForecasts; + } + + public void updateForecast(List dailyForecasts) { + this.dailyForecasts = dailyForecasts; + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return dailyForecasts != null ? Math.min(WEEK_FORECAST_DAYS, dailyForecasts.size() - 1) : 0; // First is today + } + + @Override + public Object getItem(int position) { + return dailyForecasts.get(position + 1); // First is today + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = inflater.inflate(R.layout.item_daily_forecast, null); + convertView.setTag(new DailyForecastHolder(convertView)); + } + + ((DailyForecastHolder) convertView.getTag()).update(dailyForecasts.get(position + 1)); // First is today + + return convertView; + } + + private class DailyForecastHolder { + private ImageView icon; + private TextView day; + private TextView minTemperature; + private TextView maxTemperature; + + public DailyForecastHolder(View v) { + icon = (ImageView) v.findViewById(R.id.daily_forecast_icon); + day = (TextView) v.findViewById(R.id.daily_forecast_day); + minTemperature = (TextView) v.findViewById(R.id.daily_forecast_min_temperature); + maxTemperature = (TextView) v.findViewById(R.id.daily_forecast_max_temperature); + } + + public void update(DailyForecast dailyForecast) { + icon.setImageResource(getWeatherIcon(dailyForecast.getWeatherWrapper().getWeatherType())); + day.setText(getDayOfWeek(dailyForecast.getDay())); + minTemperature.setText(temperatureFormat.format(dailyForecast.getWeatherWrapper().getMinTemperature(localeScale))); + maxTemperature.setText(temperatureFormat.format(dailyForecast.getWeatherWrapper().getMaxTemperature(localeScale))); + } + } + } + + private String getDayOfWeek(DateTime day) { + switch (day.getDayOfWeek()) { + case DateTimeConstants.MONDAY: + return getString(R.string.day_monday); + case DateTimeConstants.TUESDAY: + return getString(R.string.day_tuesday); + case DateTimeConstants.WEDNESDAY: + return getString(R.string.day_wednesday); + case DateTimeConstants.THURSDAY: + return getString(R.string.day_thursday); + case DateTimeConstants.FRIDAY: + return getString(R.string.day_friday); + case DateTimeConstants.SATURDAY: + return getString(R.string.day_saturday); + case DateTimeConstants.SUNDAY: + return getString(R.string.day_sunday); + default: + return ""; // Impossible + } + } + /** Linked from the XML. */ public void onErrorRetry(View v) { if(dataState.equals(ForecastDataState.ERROR_LOCATION)) { diff --git a/dailyclothes/src/main/res/values/strings.xml b/dailyclothes/src/main/res/values/strings.xml index 910b41f..7c99a62 100644 --- a/dailyclothes/src/main/res/values/strings.xml +++ b/dailyclothes/src/main/res/values/strings.xml @@ -12,4 +12,12 @@ Ops! There has been an error. Please check your internet connection and if the problem persists contact us at weatherup-support@androidsx.com 5-DAY FORECAST ° + + MONDAY + TUESDAY + WEDNESDAY + THURSDAY + FRIDAY + SATURDAY + SUNDAY diff --git a/mobile/src/main/java/com/androidsx/rainnotifications/ui/main/MainMobileActivity.java b/mobile/src/main/java/com/androidsx/rainnotifications/ui/main/MainMobileActivity.java index 0c13953..c70c883 100644 --- a/mobile/src/main/java/com/androidsx/rainnotifications/ui/main/MainMobileActivity.java +++ b/mobile/src/main/java/com/androidsx/rainnotifications/ui/main/MainMobileActivity.java @@ -18,10 +18,10 @@ import com.androidsx.rainnotifications.R; import com.androidsx.rainnotifications.alert.AlertGenerator; import com.androidsx.rainnotifications.alert.DayTemplateGenerator; +import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientHourlyResponseListener; import com.androidsx.rainnotifications.model.DayTemplateLoaderFactory; import com.androidsx.rainnotifications.backgroundservice.util.UserLocationFetcher; import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientException; -import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientResponseListener; import com.androidsx.rainnotifications.model.ForecastTable; import com.androidsx.rainnotifications.ui.debug.DebugActivity; import com.androidsx.rainnotifications.ui.welcome.BaseWelcomeActivity; @@ -66,7 +66,7 @@ protected void onCreate(Bundle savedInstanceState) { UserLocationFetcher.getUserLocation(this, new UserLocationFetcher.UserLocationResultListener() { @Override public void onLocationSuccess(final Location location) { - WeatherClientFactory.requestForecastForLocation(MainMobileActivity.this, location.getLatitude(), location.getLongitude(), new WeatherClientResponseListener() { + WeatherClientFactory.requestHourlyForecastForLocation(MainMobileActivity.this, location.getLatitude(), location.getLongitude(), new WeatherClientHourlyResponseListener() { @Override public void onForecastSuccess(ForecastTable forecastTable) { final String locationAddress = UserLocationFetcher.getLocationAddress( diff --git a/model/src/main/java/com/androidsx/rainnotifications/model/DailyForecast.java b/model/src/main/java/com/androidsx/rainnotifications/model/DailyForecast.java new file mode 100644 index 0000000..fa2fe14 --- /dev/null +++ b/model/src/main/java/com/androidsx/rainnotifications/model/DailyForecast.java @@ -0,0 +1,33 @@ +package com.androidsx.rainnotifications.model; + +import org.joda.time.DateTime; + +/** + * A weather forecast about a particular interval of time in the future. + */ +public class DailyForecast { + private final DateTime day; + private final DailyWeatherWrapper weatherWrapper; + + /** + * @param day + * @param weatherWrapper + */ + public DailyForecast(DateTime day, DailyWeatherWrapper weatherWrapper) { + this.day = day; + this.weatherWrapper = weatherWrapper; + } + + public DateTime getDay() { + return day; + } + + public DailyWeatherWrapper getWeatherWrapper() { + return weatherWrapper; + } + + @Override + public String toString() { + return weatherWrapper + " forecasted on " + day; + } +} diff --git a/model/src/main/java/com/androidsx/rainnotifications/model/DailyForecastTable.java b/model/src/main/java/com/androidsx/rainnotifications/model/DailyForecastTable.java new file mode 100644 index 0000000..2453cf8 --- /dev/null +++ b/model/src/main/java/com/androidsx/rainnotifications/model/DailyForecastTable.java @@ -0,0 +1,67 @@ +package com.androidsx.rainnotifications.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * Table of expected forecasts. The first forecast in the table usually represents the current + * weather. + */ +public class DailyForecastTable { + + private List dailyForecastList; + + /** + * Returns an appropriate {@link com.androidsx.rainnotifications.model.DailyForecastTable} for the given list of Forecast. + * It removes not meaningful Forecasts from given list. + * + * @param dailyForecasts An ordered list of {@link Forecast} without overlaps or gaps in their Intervals. + * Intervals must be of one-hour + * @return {@link com.androidsx.rainnotifications.model.DailyForecastTable} if processed dailyForecastList isn't empty, null in other case. + * @throws IllegalArgumentException if the given dailyForecastList is empty + */ + public static DailyForecastTable fromForecastList(List dailyForecasts) { + // TODO: No teng nada claro esto de eliminar los UNKNOWN... hablarlo con Omar + // TODO: Think about if we need to check the day conditions + if (dailyForecasts.isEmpty()) { + throw new IllegalArgumentException("The list of forecasts is empty. At least one forecast is needed"); + } else { + List meaningfulForecastList = getMeaningfulForecastList(dailyForecasts); + return meaningfulForecastList.isEmpty() ? null : new DailyForecastTable(meaningfulForecastList); + } + } + + private static List getMeaningfulForecastList(List forecastList) { + List meaningfulWeatherTypes = WeatherType.getMeaningfulWeatherTypes(); + List meaningfulForecastList = new ArrayList(); + + for (DailyForecast forecast : forecastList) { + if(meaningfulWeatherTypes.contains(forecast.getWeatherWrapper().getWeatherType())) { + meaningfulForecastList.add(forecast); + } + } + + return meaningfulForecastList; + } + + private DailyForecastTable(List dailyForecastList) { + this.dailyForecastList = dailyForecastList; + } + + /** + * Returns the processed lists of daily forecasts. It is guaranteed to be non-empty. + */ + public List getDailyForecastList() { + return dailyForecastList; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("DAY FORECAST_TABLE:"); + for (DailyForecast forecast : dailyForecastList) { + builder.append("\n " + forecast); + } + return builder.toString(); + } +} diff --git a/model/src/main/java/com/androidsx/rainnotifications/model/DailyWeatherWrapper.java b/model/src/main/java/com/androidsx/rainnotifications/model/DailyWeatherWrapper.java new file mode 100644 index 0000000..cc26efa --- /dev/null +++ b/model/src/main/java/com/androidsx/rainnotifications/model/DailyWeatherWrapper.java @@ -0,0 +1,119 @@ +package com.androidsx.rainnotifications.model; + +import android.content.Context; + +import java.text.DecimalFormat; + +/** + * Status of the weather at a particular point in time (past, present of future). + * + * @see #equals + */ +public class DailyWeatherWrapper { + private final WeatherType type; + private final float minTemperatureCelsius; + private final float minTemperatureFahrenheit; + private final float maxTemperatureCelsius; + private final float maxTemperatureFahrenheit; + + public DailyWeatherWrapper(WeatherType type, float minTemperature, float maxTemperature, WeatherWrapper.TemperatureScale scale) { + this.type = type; + + if(scale.equals(WeatherWrapper.TemperatureScale.CELSIUS)) { + minTemperatureCelsius = minTemperature; + minTemperatureFahrenheit = minTemperature * 1.8f + 32; //Celsius to Fahrenheit : (°C × 1.8) + 32 =°F + + maxTemperatureCelsius = maxTemperature; + maxTemperatureFahrenheit = maxTemperature * 1.8f + 32; //Celsius to Fahrenheit : (°C × 1.8) + 32 =°F + } + else { + minTemperatureFahrenheit = minTemperature; + minTemperatureCelsius = (minTemperature - 32) / 1.8f; //Fahrenheit to Celsius : (°F − 32) ÷ 1.8 =°C + + maxTemperatureFahrenheit = maxTemperature; + maxTemperatureCelsius = (maxTemperature - 32) / 1.8f; //Fahrenheit to Celsius : (°F − 32) ÷ 1.8 =°C + } + } + + public WeatherType getWeatherType() { + return type; + } + + public float getMinTemperature(WeatherWrapper.TemperatureScale scale) { + if(scale.equals(WeatherWrapper.TemperatureScale.CELSIUS)) { + return minTemperatureCelsius; + } + else { + return minTemperatureFahrenheit; + } + } + + public float getMaxTemperature(WeatherWrapper.TemperatureScale scale) { + if(scale.equals(WeatherWrapper.TemperatureScale.CELSIUS)) { + return maxTemperatureCelsius; + } + else { + return maxTemperatureFahrenheit; + } + } + + // TODO: Add some logic for allow user to choose celsius or Fahrenheit. + public String getReadableMinTemperature(Context context) { + if(WeatherWrapper.TemperatureScale.getLocaleScale(context).equals(WeatherWrapper.TemperatureScale.FAHRENHEIT)) { + return new DecimalFormat("#").format(minTemperatureFahrenheit) + WeatherWrapper.FAHRENHEIT_SYMBOL; + } + else { + return new DecimalFormat("#").format(minTemperatureCelsius) + WeatherWrapper.CELSIUS_SYMBOL; + } + } + + // TODO: Add some logic for allow user to choose celsius or Fahrenheit. + public String getReadableMaxTemperature(Context context) { + if(WeatherWrapper.TemperatureScale.getLocaleScale(context).equals(WeatherWrapper.TemperatureScale.FAHRENHEIT)) { + return new DecimalFormat("#").format(maxTemperatureFahrenheit) + WeatherWrapper.FAHRENHEIT_SYMBOL; + } + else { + return new DecimalFormat("#").format(maxTemperatureCelsius) + WeatherWrapper.CELSIUS_SYMBOL; + } + } + + @Override + public String toString() { + return type.toString() + ", " + minTemperatureCelsius + WeatherWrapper.CELSIUS_SYMBOL + ", " + minTemperatureFahrenheit + WeatherWrapper.FAHRENHEIT_SYMBOL + + ", " + maxTemperatureCelsius + WeatherWrapper.CELSIUS_SYMBOL + ", " + maxTemperatureFahrenheit + WeatherWrapper.FAHRENHEIT_SYMBOL; + } + + /** + * {@inheritDoc} + *

+ * Important note about unknown transitions: + *

+ * Transitions like UNKNOWN -> KNOWN are considered transitions. + * Transitions like KNOWN -> UNKNOWN are NOT considered transitions. + *

+ * That implies UNKNOWN -> sunny will end up being "It's gonna be sunny again", but sunny -> UNKNOWN won't trigger + * a change. Then, sunny -> UNKNOWN -> rain will essentially be considered a sunny -> rain transition. + */ + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (other == null || getClass() != other.getClass()) { + return false; + } else { + final DailyWeatherWrapper otherWeather = (DailyWeatherWrapper) other; + if (getWeatherType() == WeatherType.UNKNOWN && !(otherWeather.getWeatherType() == WeatherType.UNKNOWN)) { + return false; + } else if (getWeatherType() == WeatherType.UNKNOWN || ((DailyWeatherWrapper) other).getWeatherType() == WeatherType.UNKNOWN) { + return true; + } else { + return getWeatherType() == otherWeather.getWeatherType(); + } + } + } + + @Override + public int hashCode() { + return getWeatherType() != null ? getWeatherType().hashCode() : 0; + } +} diff --git a/weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientDailyResponseListener.java b/weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientDailyResponseListener.java new file mode 100644 index 0000000..91b12b7 --- /dev/null +++ b/weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientDailyResponseListener.java @@ -0,0 +1,22 @@ +package com.androidsx.rainnotifications.forecastapislibrary; + +import com.androidsx.rainnotifications.model.DailyForecastTable; + +/** + * Listener for a response from a weather client. + */ +public interface WeatherClientDailyResponseListener { + + /** + * Handles the case when the request for weather information succeeded. Note that this method + * is executed in the UI thread. + */ + public void onForecastSuccess(DailyForecastTable dailyForecastTable); + + /** + * Handles the case when the request for weather information failed. + */ + public void onForecastFailure(WeatherClientException weatherClientException); + + +} diff --git a/weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientExecutor.java b/weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientExecutor.java index 663d385..fa69b04 100644 --- a/weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientExecutor.java +++ b/weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientExecutor.java @@ -8,12 +8,16 @@ public interface WeatherClientExecutor { /** - * Executes the request in a background thread. + * Executes an hourly request in a background thread. * * @param responseListener where the results will be returned after the call is performed */ - public void execute(Context context, - double latitude, - double longitude, - WeatherClientResponseListener responseListener); + public void executeHourly(Context context, double latitude, double longitude, WeatherClientHourlyResponseListener responseListener); + + /** + * Executes a daily request in a background thread. + * + * @param responseListener where the results will be returned after the call is performed + */ + public void executeDaily(Context context, double latitude, double longitude, WeatherClientDailyResponseListener responseListener); } diff --git a/weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientResponseListener.java b/weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientHourlyResponseListener.java similarity index 91% rename from weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientResponseListener.java rename to weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientHourlyResponseListener.java index edeeb5c..fa30e06 100644 --- a/weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientResponseListener.java +++ b/weather-client-api/src/main/java/com/androidsx/rainnotifications/forecastapislibrary/WeatherClientHourlyResponseListener.java @@ -5,7 +5,7 @@ /** * Listener for a response from a weather client. */ -public interface WeatherClientResponseListener { +public interface WeatherClientHourlyResponseListener { /** * Handles the case when the request for weather information succeeded. Note that this method diff --git a/weather-client-factory/src/main/java/com/androidsx/rainnotifications/weatherclientfactory/WeatherClientFactory.java b/weather-client-factory/src/main/java/com/androidsx/rainnotifications/weatherclientfactory/WeatherClientFactory.java index 150e80a..9a2d113 100644 --- a/weather-client-factory/src/main/java/com/androidsx/rainnotifications/weatherclientfactory/WeatherClientFactory.java +++ b/weather-client-factory/src/main/java/com/androidsx/rainnotifications/weatherclientfactory/WeatherClientFactory.java @@ -2,24 +2,36 @@ import android.content.Context; -import com.androidsx.rainnotifications.forecast_io.ForecastIoNetworkServiceTask; +import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientDailyResponseListener; import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientExecutor; -import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientResponseListener; +import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientHourlyResponseListener; import com.androidsx.rainnotifications.wunderground.WundergroundNetworkServiceTask; public abstract class WeatherClientFactory { - //WARNING: At the moment only WUNDERGROUND implement temperature, so we can't use forecast.io See also WeatherBuilder on forecast.io module + //WARNING: At the moment only WUNDERGROUND implement temperature and daily, so we can't use forecast.io See also WeatherBuilder on forecast.io module private static final WeatherClient CLIENT = WeatherClient.WUNDERGROUND; - public static void requestForecastForLocation(Context context, double latitude, double longitude, WeatherClientResponseListener responseListener) { + public static void requestHourlyForecastForLocation(Context context, double latitude, double longitude, WeatherClientHourlyResponseListener responseListener) { final WeatherClientExecutor weatherClientExecutor; switch (CLIENT) { - case FORECAST_IO: weatherClientExecutor = new ForecastIoNetworkServiceTask(); break; + //case FORECAST_IO: weatherClientExecutor = new ForecastIoNetworkServiceTask(); break; case WUNDERGROUND: weatherClientExecutor = new WundergroundNetworkServiceTask(); break; default: throw new IllegalArgumentException("Unsupported client: " + CLIENT); } - weatherClientExecutor.execute(context, latitude, longitude, responseListener); + weatherClientExecutor.executeHourly(context, latitude, longitude, responseListener); + } + + public static void requestDailyForecastForLocation(Context context, double latitude, double longitude, WeatherClientDailyResponseListener responseListener) { + + final WeatherClientExecutor weatherClientExecutor; + switch (CLIENT) { + //case FORECAST_IO: weatherClientExecutor = new ForecastIoNetworkServiceTask(); break; + case WUNDERGROUND: weatherClientExecutor = new WundergroundNetworkServiceTask(); break; + default: throw new IllegalArgumentException("Unsupported client: " + CLIENT); + } + + weatherClientExecutor.executeDaily(context, latitude, longitude, responseListener); } } diff --git a/weather-client-forecast-io/src/main/java/com/androidsx/rainnotifications/forecast_io/ForecastIoNetworkServiceTask.java b/weather-client-forecast-io/src/main/java/com/androidsx/rainnotifications/forecast_io/ForecastIoNetworkServiceTask.java index 02cfb32..815c5ec 100644 --- a/weather-client-forecast-io/src/main/java/com/androidsx/rainnotifications/forecast_io/ForecastIoNetworkServiceTask.java +++ b/weather-client-forecast-io/src/main/java/com/androidsx/rainnotifications/forecast_io/ForecastIoNetworkServiceTask.java @@ -3,9 +3,10 @@ import android.content.Context; import android.util.Log; +import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientDailyResponseListener; import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientException; import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientExecutor; -import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientResponseListener; +import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientHourlyResponseListener; import com.androidsx.rainnotifications.model.ForecastTable; import com.androidsx.rainnotifications.model.ForecastTableBuilder; import com.forecast.io.network.responses.INetworkResponse; @@ -21,14 +22,19 @@ */ public final class ForecastIoNetworkServiceTask extends NetworkServiceTask implements WeatherClientExecutor { private static final String TAG = ForecastIoNetworkServiceTask.class.getSimpleName(); - private WeatherClientResponseListener responseListener; + private WeatherClientHourlyResponseListener responseListener; @Override - public void execute(Context context, double latitude, double longitude, WeatherClientResponseListener responseListener) { + public void executeHourly(Context context, double latitude, double longitude, WeatherClientHourlyResponseListener responseListener) { this.responseListener = responseListener; this.execute(new ForecastIoRequest(latitude, longitude).getRequest()); } + @Override + public void executeDaily(Context context, double latitude, double longitude, WeatherClientDailyResponseListener responseListener) { + throw new IllegalArgumentException("Unimplemented method for Forecast.io client"); + } + @Override protected void onPostExecute(INetworkResponse rawNetworkResponse) { if (rawNetworkResponse == null || rawNetworkResponse.getStatus() == NetworkResponse.Status.FAIL) { diff --git a/weather-client-wunderground/src/main/java/com/androidsx/rainnotifications/model/WundergroundDailyTableBuilder.java b/weather-client-wunderground/src/main/java/com/androidsx/rainnotifications/model/WundergroundDailyTableBuilder.java new file mode 100644 index 0000000..3272cc6 --- /dev/null +++ b/weather-client-wunderground/src/main/java/com/androidsx/rainnotifications/model/WundergroundDailyTableBuilder.java @@ -0,0 +1,47 @@ +package com.androidsx.rainnotifications.model; + +import org.joda.time.DateTime; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * Builder for {@link com.androidsx.rainnotifications.model.DailyForecastTable}. + *

+ * Should not be used from outside of this project. + */ +public class WundergroundDailyTableBuilder { + public static DailyForecastTable buildFromWunderground(JSONObject response) throws JSONException { + + if(response.has("forecast")) { + JSONObject forecast = response.getJSONObject("forecast"); + if(forecast.has("simpleforecast")) { + JSONObject simpleforecast = forecast.getJSONObject("simpleforecast"); + if(simpleforecast.has("forecastday")) { + JSONArray forecastday = simpleforecast.getJSONArray("forecastday"); + if(forecastday != null && forecastday.length() > 0) { + return DailyForecastTable.fromForecastList(getForecastListFromDaily(forecastday)); + } + } + + } + } + + return null; + } + + private static List getForecastListFromDaily(JSONArray daily) throws JSONException { + List forecasts = new ArrayList(); + for (int i = 0 ; i < daily.length() - 1 ; i++) { + forecasts.add(new DailyForecast(getDay(daily.getJSONObject(i)), WundergroundDailyWeatherBuilder.buildFromWunderground(daily.getJSONObject(i)))); + } + return forecasts; + } + + private static DateTime getDay(JSONObject dayForecast) throws JSONException { + return new DateTime(Long.parseLong(dayForecast.getJSONObject("date").get("epoch").toString()) * 1000); + } +} diff --git a/weather-client-wunderground/src/main/java/com/androidsx/rainnotifications/model/WundergroundDailyWeatherBuilder.java b/weather-client-wunderground/src/main/java/com/androidsx/rainnotifications/model/WundergroundDailyWeatherBuilder.java new file mode 100644 index 0000000..37f79c1 --- /dev/null +++ b/weather-client-wunderground/src/main/java/com/androidsx/rainnotifications/model/WundergroundDailyWeatherBuilder.java @@ -0,0 +1,27 @@ +package com.androidsx.rainnotifications.model; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Builder for {@link com.androidsx.rainnotifications.model.WeatherWrapper}. + *

+ * Should not be used from outside of this project. + */ +public class WundergroundDailyWeatherBuilder { + public static DailyWeatherWrapper buildFromWunderground(JSONObject weather) throws JSONException { + return new DailyWeatherWrapper(retrieveWeatherType(weather), retrieveMinCelsiusTemperature(weather), retrieveMaxCelsiusTemperature(weather), WeatherWrapper.TemperatureScale.CELSIUS); + } + + private static WeatherType retrieveWeatherType(JSONObject weather) throws JSONException { + return WundergroundWeatherTypeBuilder.buildFromWunderground(weather.getString("icon")); + } + + private static float retrieveMinCelsiusTemperature(JSONObject weather) throws JSONException { + return (float) weather.getJSONObject("low").getDouble("celsius"); + } + + private static float retrieveMaxCelsiusTemperature(JSONObject weather) throws JSONException { + return (float) weather.getJSONObject("high").getDouble("celsius"); + } +} diff --git a/weather-client-wunderground/src/main/java/com/androidsx/rainnotifications/wunderground/WundergroundNetworkServiceTask.java b/weather-client-wunderground/src/main/java/com/androidsx/rainnotifications/wunderground/WundergroundNetworkServiceTask.java index 45513f5..7a0ff06 100644 --- a/weather-client-wunderground/src/main/java/com/androidsx/rainnotifications/wunderground/WundergroundNetworkServiceTask.java +++ b/weather-client-wunderground/src/main/java/com/androidsx/rainnotifications/wunderground/WundergroundNetworkServiceTask.java @@ -3,11 +3,14 @@ import android.content.Context; import android.util.Log; +import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientDailyResponseListener; import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientException; import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientExecutor; -import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientResponseListener; +import com.androidsx.rainnotifications.forecastapislibrary.WeatherClientHourlyResponseListener; +import com.androidsx.rainnotifications.model.DailyForecastTable; import com.androidsx.rainnotifications.model.Day; import com.androidsx.rainnotifications.model.ForecastTable; +import com.androidsx.rainnotifications.model.WundergroundDailyTableBuilder; import com.androidsx.rainnotifications.model.WundergroundTableBuilder; import com.loopj.android.http.AsyncHttpClient; import com.loopj.android.http.JsonHttpResponseHandler; @@ -20,15 +23,19 @@ public final class WundergroundNetworkServiceTask implements WeatherClientExecut private static final String TAG = WundergroundNetworkServiceTask.class.getSimpleName(); private static final String WUNDERGROUND_BASE_URL = "http://api.wunderground.com/api/" + Constants.WUNDERGROUND_API_KEY; - private static final String[] FEATURES = { + private static final String[] FEATURES_HOURLY = { "conditions", // Current time, http://www.wunderground.com/weather/api/d/docs?d=data/conditions "hourly", // Hourly forecast, http://www.wunderground.com/weather/api/d/docs?d=data/hourly "astronomy"}; // Sunrise/Sunset time, http://www.wunderground.com/weather/api/d/docs?d=data/astronomy + private static final String[] FEATURES_DAILY = { + "forecast10day" }; // Daily forecast, http://www.wunderground.com/weather/api/d/docs?d=data/forecast10day + + @Override - public void execute(Context context, double latitude, double longitude, final WeatherClientResponseListener responseListener) { + public void executeHourly(Context context, double latitude, double longitude, final WeatherClientHourlyResponseListener responseListener) { String url = WUNDERGROUND_BASE_URL; - for(String f : FEATURES) { + for(String f : FEATURES_HOURLY) { url += "/" + f; } url += "/q/" + latitude + "," + longitude + ".json"; @@ -39,7 +46,7 @@ public void execute(Context context, double latitude, double longitude, final We public void onSuccess(int statusCode, Header[] headers, JSONObject response) { super.onSuccess(statusCode, headers, response); try { - Log.v(TAG, "Raw response from Wunderground:\n" + response.toString(1)); + Log.v(TAG, "Raw hourly response from Wunderground:\n" + response.toString(1)); final ForecastTable forecastTable = WundergroundTableBuilder.buildFromWunderground(response); if (forecastTable != null) { Log.d(TAG, "ForecastTable: " + forecastTable); @@ -47,17 +54,53 @@ public void onSuccess(int statusCode, Header[] headers, JSONObject response) { responseListener.onForecastSuccess(forecastTable); } else { responseListener.onForecastFailure(new WeatherClientException( - "The forecast table is null for the WUnderground response " + response)); + "The forecast table is null for the hourly WUnderground response " + response)); + } + } catch (JSONException e) { + responseListener.onForecastFailure(new WeatherClientException("Failed to process hourly WUnderground response", e)); + } + } + + @Override + public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) { + responseListener.onForecastFailure(new WeatherClientException( + "Failed to read from hourly WUnderground: " + statusCode, throwable)); + } + }); + } + + @Override + public void executeDaily(Context context, double latitude, double longitude, final WeatherClientDailyResponseListener responseListener) { + String url = WUNDERGROUND_BASE_URL; + for(String f : FEATURES_DAILY) { + url += "/" + f; + } + url += "/q/" + latitude + "," + longitude + ".json"; + + AsyncHttpClient httpClient = new AsyncHttpClient(); + httpClient.get(context, url, new JsonHttpResponseHandler(){ + @Override + public void onSuccess(int statusCode, Header[] headers, JSONObject response) { + super.onSuccess(statusCode, headers, response); + try { + Log.v(TAG, "Raw daily response from Wunderground:\n" + response.toString(1)); + final DailyForecastTable dailyForecastTable = WundergroundDailyTableBuilder.buildFromWunderground(response); + if (dailyForecastTable != null) { + Log.d(TAG, "DailyForecastTable: " + dailyForecastTable); + responseListener.onForecastSuccess(dailyForecastTable); + } else { + responseListener.onForecastFailure(new WeatherClientException( + "The forecast table is null for the daily WUnderground response " + response)); } } catch (JSONException e) { - responseListener.onForecastFailure(new WeatherClientException("Failed to process WUnderground response", e)); + responseListener.onForecastFailure(new WeatherClientException("Failed to process daily WUnderground response", e)); } } @Override public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) { responseListener.onForecastFailure(new WeatherClientException( - "Failed to read from WUnderground: " + statusCode, throwable)); + "Failed to read from daily WUnderground: " + statusCode, throwable)); } }); }