/* * Copyright 2014 Sebastiano Poggi and Francesco Pontillo * * 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 net.frakbot.FWeather.util; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.location.Location; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.preference.PreferenceManager; import android.text.TextUtils; import net.frakbot.FWeather.R; import net.frakbot.FWeather.updater.weather.CantGetWeatherException; import net.frakbot.FWeather.updater.weather.YahooWeatherApiClient; import net.frakbot.FWeather.updater.weather.model.WeatherData; import net.frakbot.global.Const; import net.frakbot.util.log.FLog; import java.io.IOException; import java.util.Arrays; import static net.frakbot.FWeather.updater.weather.YahooWeatherApiClient.getLocationInfo; /** * Helper class for retrieving weather information. * <p/> * Parts from Roman Nurik's DashClock. * * @author Francesco Pontillo and Sebastiano Poggi */ public class WeatherHelper { private static final String TAG = WeatherHelper.class.getSimpleName(); private static final long WEATHER_CACHE_DURATION_MILLIS = 2 * 60 * 60 * 1000; // Two hours cache expiry time private static WeatherData mCachedWeather = null; private static long mCachedWeatherTimestamp = Long.MIN_VALUE; private static boolean mCacheDataRead = false; public static WeatherData getWeather(Context context) throws LocationHelper.LocationNotReadyYetException, IOException { return getWeather(context, false); } /** * Gets the current weather at the user's location * * @param context The current {@link Context}. * * @return Returns the weather info, if available, or null * if there was any error during the download. */ public static WeatherData getWeather(Context context, boolean forced) throws LocationHelper.LocationNotReadyYetException, IOException { FLog.i(context, TAG, "Starting weather update"); if (forced) { FLog.i(context, TAG, "Update was forced, clear the cache."); clearCache(context); } // Read the cached data if needed if (!mCacheDataRead) { readDataFromCache(context); mCacheDataRead = true; } WeatherData weather; if (!checkNetwork(context)) { FLog.w(TAG, "No network seems to be available!"); // Try to resort to cached weather data weather = getLatestWeather(); if (weather != null) { FLog.i(TAG, "Using cached weather data..."); return weather; } // No cached weather (or stale cached weather data), nothing we can do... FLog.e(TAG, "No cached weather available, can't use it either. That's a wrap, ladies and gentlemen"); weather = new WeatherData(); weather.conditionCode = WeatherData.WEATHER_ID_ERR_NO_NETWORK; return weather; } // Use manual location if defined String manualLocationWoeid = null; final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); if (sp != null) { manualLocationWoeid = WeatherLocationPreference.getWoeidFromValue( sp.getString(context.getString(R.string.pref_key_weather_location), null)); } if (!TextUtils.isEmpty(manualLocationWoeid)) { FLog.d(TAG, "Using manual location WOEID"); YahooWeatherApiClient.LocationInfo locationInfo = new YahooWeatherApiClient.LocationInfo(); locationInfo.woeids = Arrays.asList(manualLocationWoeid); weather = getWeatherDataForLocationInfo(locationInfo); } else { // Get the current location final Location location = getLocation(context); if (location == null) { TrackerHelper.sendException(context, "No location found", false); FLog.e(context, TAG, "No location available, can't update"); WeatherData errWeather = new WeatherData(); errWeather.conditionCode = WeatherData.WEATHER_ID_ERR_NO_LOCATION; return errWeather; } weather = getWeatherDataForLocation(location); } FLog.i(context, TAG, "Weather update done"); if (weather != null) { FLog.d(context, TAG, "Got weather:\n\t> " + weather); } else { FLog.v(context, TAG, "No weather received"); } saveDataToCache(context, weather); return weather; } /** * Saves the current weather data to both the permanent and * in-memory caches. * * @param context The current {@link Context}. * @param weather The weather data to save in the cache */ private static void saveDataToCache(Context context, WeatherData weather) { final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); if (sp == null) { FLog.e(TAG, "Unable to access the shared preferences, can't save to cache"); return; } if (weather == null) { FLog.v(TAG, "Clearing cached weather information (null data)"); clearCache(context, true); return; } // Update the cached value mCachedWeather = weather; mCachedWeatherTimestamp = System.currentTimeMillis(); SharedPreferences.Editor e = sp.edit(); e.putString(Const.Preferences.LOCATION_CACHE, weather.serializeToString()) .putLong(Const.Preferences.LOCATION_CACHE_TIMESTAMP, mCachedWeatherTimestamp) .commit(); FLog.v(context, TAG, "Cached weather information updated"); } /** * Retrieved the last weather data from the permanent cache, if still valid. * * @param context The current {@link android.content.Context}. */ private static void readDataFromCache(Context context) { final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); if (sp == null) { FLog.e(TAG, "Unable to access the shared preferences, can't read from cache"); return; } // Read the cached value mCachedWeatherTimestamp = sp.getLong(Const.Preferences.LOCATION_CACHE_TIMESTAMP, Long.MIN_VALUE); mCachedWeather = WeatherData.deserializeFromString(sp.getString(Const.Preferences.LOCATION_CACHE, null)); // Validate the cache age if (!isLatestWeatherStillGood()) { mCachedWeatherTimestamp = Long.MIN_VALUE; } FLog.v(context, TAG, "Cached weather information retrieved from permanent storage"); } /** * Clear the cache and resets the cache-handling objects. * @param context The current {@link Context}. * @param persist true to persist to {@link android.content.SharedPreferences}, false to clear the memory cache */ private static void clearCache(Context context, boolean persist) { FLog.v(TAG, "Clearing cached weather information (as requested)"); mCachedWeather = null; mCachedWeatherTimestamp = Long.MIN_VALUE; if (persist) { final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); if (sp == null) { FLog.e(TAG, "Unable to access the shared preferences, can't save to cache"); return; } SharedPreferences.Editor e = sp.edit(); e.remove(Const.Preferences.LOCATION_CACHE) .remove(Const.Preferences.LOCATION_CACHE_TIMESTAMP) .commit(); } } /** * Clear the in memory cache and resets the cache-handling objects. * @param context The current {@link Context}. */ private static void clearCache(Context context) { clearCache(context, false); } /** * Gets the current location. * * @param context The current {@link Context}. * * @return Returns the current location */ public static Location getLocation(Context context) throws LocationHelper.LocationNotReadyYetException { final Intent intent = WidgetHelper.getUpdaterIntent(context, false, false); final PendingIntent pendingIntent = PendingIntent.getService(context, 42, intent, 0); return LocationHelper.getLastKnownSurroundings(pendingIntent); } /** * Checks if there is any network connection active (or activating). * * @param context The current {@link Context}. * * @return Returns true if there is an active connection, false otherwise */ public static boolean checkNetwork(Context context) { ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); } /** * Returns the latest known weather information. * * @return The cached {@link WeatherData} */ public static WeatherData getLatestWeather() { // If the cached weather has become stale if (!isLatestWeatherStillGood()) { // Clear the cached timestamp FLog.v(TAG, "Stale cache detected, clearing the cached weather"); mCachedWeatherTimestamp = Long.MIN_VALUE; // Clear the cache value mCachedWeather = null; } return mCachedWeather; } /** * Returns a value indicating wether the latest known weather information * is still fresh enough to be used, or if it's become stale. * * @return Returns true if there is a cached weather and if said cached * weather data is not stale. */ public static boolean isLatestWeatherStillGood() { final long weatherAgeMillis = getLatestWeatherAgeMillis(); return mCachedWeatherTimestamp != Long.MIN_VALUE && mCachedWeather != null && weatherAgeMillis < WEATHER_CACHE_DURATION_MILLIS; } /** * Returns a value indicating the age in milliseconds of the latest known * weather information, if any is available. * * @return Returns the cache age if there is a cached weather, or * {@link Long#MIN_VALUE} if there is no cached weather. */ public static long getLatestWeatherAgeMillis() { if (mCachedWeather == null) { mCachedWeatherTimestamp = Long.MIN_VALUE; return mCachedWeatherTimestamp; } return System.currentTimeMillis() - mCachedWeatherTimestamp; } private static WeatherData getWeatherDataForLocation(Location location) { WeatherData weatherData = null; try { FLog.d(TAG, "Using location: " + location.getLatitude() + "," + location.getLongitude()); weatherData = getWeatherWithRetry(getLocationInfoWithRetry(location)); } catch (CantGetWeatherException e) { FLog.e(TAG, "Unable to retrieve weather", e); } return weatherData; } private static WeatherData getWeatherDataForLocationInfo(YahooWeatherApiClient.LocationInfo location) { try { FLog.d(TAG, "Using manual location. WOEIDs count: " + location.woeids.size()); return getWeatherWithRetry(location); } catch (CantGetWeatherException e) { FLog.e(TAG, "Unable to retrieve weather", e); return null; } catch (NullPointerException e) { FLog.e(TAG, "Unable to retrieve weather: no WOEIDs!", e); return null; } } /** * Internal method to retry weather fetching from the Yahoo weather provider. * @param location The {@link net.frakbot.FWeather.updater.weather.YahooWeatherApiClient.LocationInfo} * @return The {@link net.frakbot.FWeather.updater.weather.model.WeatherData} containing weather information * @throws CantGetWeatherException If there's some network error */ private static WeatherData getWeatherWithRetry(YahooWeatherApiClient.LocationInfo location) throws CantGetWeatherException { CantGetWeatherException lastException = null; for (int i = 0; i < Const.Thresholds.MAX_FETCH_WEATHER_ATTEMPTS; i++) { try { WeatherData weatherData = YahooWeatherApiClient.getWeatherForLocationInfo(location); return weatherData; } catch (CantGetWeatherException e) { FLog.w(TAG, String.format( "Weather fetching attempt number %d has failed. %d attempts remaining.", i+1, Const.Thresholds.MAX_FETCH_WEATHER_ATTEMPTS-i-1)); // Save the last exception for me lastException = e; } } FLog.e(TAG, String.format( "Maximum number (%d) of weather fetching attempts reached. Giving up.", Const.Thresholds.MAX_FETCH_WEATHER_ATTEMPTS)); // If we are here, it means that MAX_FETCH_WEATHER_ATTEMPTS have been made without a result, give up! throw lastException; } /** * Internal method to retry location fetching from the Yahoo weather provider. * @param location The {@link net.frakbot.FWeather.updater.weather.YahooWeatherApiClient.LocationInfo} * @return The {@link net.frakbot.FWeather.updater.weather.model.WeatherData} containing weather information * @throws CantGetWeatherException If there's some network error */ /** * Internal method to retry location fetching from the Yahoo weather provider. * @param location The known {@link android.location.Location} * @return The {@link net.frakbot.FWeather.updater.weather.YahooWeatherApiClient.LocationInfo} returned * by the Yahoo weather provider * @throws CantGetWeatherException If there's some parsing or network error */ private static YahooWeatherApiClient.LocationInfo getLocationInfoWithRetry(Location location) throws CantGetWeatherException { CantGetWeatherException lastException = null; for (int i = 0; i < Const.Thresholds.MAX_FETCH_LOCATION_ATTEMPTS; i++) { try { YahooWeatherApiClient.LocationInfo locationInfo = getLocationInfo(location); return locationInfo; } catch (CantGetWeatherException e) { FLog.w(TAG, String.format( "Location fetching attempt number %d has failed. %d attempts remaining.", i+1, Const.Thresholds.MAX_FETCH_LOCATION_ATTEMPTS-i-1)); // Save the last exception for me lastException = e; } } FLog.e(TAG, String.format( "Maximum number (%d) of Location fetching attempts reached. Giving up.", Const.Thresholds.MAX_FETCH_LOCATION_ATTEMPTS)); // If we are here, it means that MAX_FETCH_WEATHER_ATTEMPTS have been made without a result, give up! throw lastException; } }