package vandy.mooc.presenter; import java.lang.ref.WeakReference; import retrofit.Callback; import retrofit.RestAdapter; import retrofit.RetrofitError; import retrofit.client.Response; import vandy.mooc.common.ConfigurableOps; import vandy.mooc.common.ContextView; import vandy.mooc.common.GenericAsyncTask; import vandy.mooc.common.GenericAsyncTaskOps; import vandy.mooc.model.cache.WeatherTimeoutCache; import vandy.mooc.model.webdata.WeatherData; import vandy.mooc.model.webdata.WeatherWebServiceProxy; import android.util.Log; /** * This class implements the client-side operations that obtain * WeatherData from the Weather Sevice web service. It implements * ConfigurableOps so it can be managed by the GenericActivity * framework. It also implements GenericAsyncTaskOps so its * doInBackground() method will run in a background thread. This * class plays the role of the "Presenter" in the Model-View-Presenter * pattern. */ public class WeatherOps implements GenericAsyncTaskOps<String, Void, WeatherData>, ConfigurableOps<WeatherOps.View>, Callback<WeatherData> { /** * Debugging tag used by the Android logger. */ protected final static String TAG = WeatherOps.class.getSimpleName(); /** * This interface defines the minimum interface needed by the * WeatherOps class in the "Presenter" layer to interact with the * WeatherActivity in the "View" layer. */ public interface View extends ContextView { /** * Displays the weather data to the user * * @param weatherList * List of WeatherData to be displayed, which * should not be null. */ public void displayResults(WeatherData wd, String errorReason); } /** * Stores a Weak Reference to the WeatherOps.View so the garbage * collector can remove it when it's not in use. */ protected WeakReference<WeatherOps.View> mWeatherView; /** * Content Provider-based cache for the WeatherData. */ private WeatherTimeoutCache mCache; /** * Retrofit proxy that sends requests to the Weather Service web * service and converts the Json response to an instance of * AcronymData POJO class. */ private WeatherWebServiceProxy mWeatherWebServiceProxy; /** * WeatherData object that is being displayed, which is used to * re-populate the UI after a runtime configuration change. */ private WeatherData mCurrentWeatherData; /** * The GenericAsyncTask used to get the current weather from the * Weather Service web service. */ private GenericAsyncTask<String, Void, WeatherData, WeatherOps> mAsyncTask; /** * Store the requested weather location for error handling. */ private String mLocation; /** * Keeps track of whether a call is already in progress and * ignores subsequent calls until the first call is done. */ private boolean mCallInProgress; /** * Default constructor that's needed by the GenericActivity * framework. */ public WeatherOps() { } /** * Hook method dispatched by the GenericActivity framework to * initialize the HobbitOpsImpl object after it's been created. * * @param view The currently active HobbitOps.View. * @param firstTimeIn Set to "true" if this is the first time the * Ops class is initialized, else set to * "false" if called after a runtime * configuration change. */ public void onConfiguration(WeatherOps.View view, boolean firstTimeIn) { // Reset the mWeatherView WeakReference. mWeatherView = new WeakReference<>(view); if (firstTimeIn) { // Initialize the WeatherTimeoutCache. We use the // Application context to avoid dependencies on the // Activity context, which will change if/when a runtime // configuration change occurs. mCache = new WeatherTimeoutCache(view.getApplicationContext()); // Build the RetroFit RestAdapter, which is used to create // the RetroFit service instance, and then use it to build // the RetrofitWeatherServiceProxy. mWeatherWebServiceProxy = new RestAdapter .Builder() .setEndpoint(WeatherWebServiceProxy.sWeather_Service_URL_Retro) .build() .create(WeatherWebServiceProxy.class); } else if (mCurrentWeatherData != null) // Populate the display if a WeatherData object is stored // in the WeatherOps instance. mWeatherView.get().displayResults (mCurrentWeatherData, ""); } /** * Initiate the asynchronous weather lookup when the user presses * the "Get Weather Async" button. * * @return false if a call is already in progress, else true. */ public boolean getWeatherAsync(String location) { if (mCallInProgress) return false; else { // Don't allow concurrent calls to get the weather. mCallInProgress = true; // Store this for error reporting purposes. mLocation = location; // Try to get the WeatherData from the cache. final WeatherData results = getFromCache(location); if (results != null) // If the location is in the cache then handle a // successful result. handleResults(results, "no weather for " + mLocation + " found"); else // If the location's data wasn't in the cache or was // stale then get the results from Weather Service web // service using a two-way asynchronous Retrofit RPC // call. mWeatherWebServiceProxy.getWeatherData(location, this); return true; } } /** * Called by Retrofit for handling error. */ @Override public void failure(RetrofitError error) { Log.v(TAG, "Retrofit failure"); // Handle a failure result. handleResults(null, mLocation); } /** * Called by Retrofit for handling success result. */ @Override public void success(WeatherData results, Response response) { Log.v(TAG, "Retrofit success"); // Handle a successful result. handleResults(results, mLocation); } /** * Initiate the synchronous weather lookup when the user presses * the "Get Weather Sync" button. * * @return false if a call is already in progress, else true. */ public boolean getWeatherSync(String location) { if (mCallInProgress) return false; else { // Don't allow concurrent calls to get the weather. mCallInProgress = true; if (mAsyncTask != null) // Cancel an ongoing operation to avoid having two // requests run concurrently. mAsyncTask.cancel(true); // Execute the AsyncTask to get the weather without // blocking the caller. mAsyncTask = new GenericAsyncTask<>(this); mAsyncTask.execute(location); return true; } } /** * Get the current weather either from the ContentProvider cache * or from the Weather Service web service. */ @Override public WeatherData doInBackground(String... location) { mLocation = location[0]; try { // First the cache is checked for the location's weather // data. WeatherData weatherData = getFromCache(mLocation); // If data is in cache return it. if (weatherData != null) { Log.v(TAG, location + ": in cache"); return weatherData; } // If the location's data wasn't in the cache or was // stale, use Retrofit to fetch it from the Weather // Service web service. else { Log.v(TAG, location + ": not in cache"); // Get the weather from the Weather Service web // service. return mWeatherWebServiceProxy .getWeatherData(mLocation); } } catch (Exception e) { Log.v(TAG, "doInBackground() " + e); return null; } } /** * Display the results in the UI Thread. */ @Override public void onPostExecute(WeatherData results) { // Indicate we're done with the AsyncTask. mAsyncTask = null; // Handle the result. handleResults(results, "no weather for " + mLocation + " found"); } /** * Try to get the @a location from the cache. */ private WeatherData getFromCache(String location) { // Try to get the results from the cache. WeatherData weatherData = mCache.get(location); // If data is in cache return it. if (weatherData != null) { Log.v(TAG, location + ": in cache"); // Return the results from the cache. return weatherData; } else { Log.v(TAG, location + ": not in cache"); return null; } } /** * Handle the results by putting them into the cache (if they * aren't null) and displaying them. */ private void handleResults(WeatherData results, String reason) { // Put in cache if result is not null. if (results != null && results.getName() != null) { mCurrentWeatherData = results; mCache.put(mLocation, results); } // Try to display the results. mWeatherView.get().displayResults(results, reason); // Allow another call to proceed when this method returns. mCallInProgress = false; } }