package vandy.mooc.model.cache; import java.util.ArrayList; import vandy.mooc.common.TimeoutCache; import vandy.mooc.model.provider.WeatherContract; import vandy.mooc.model.provider.WeatherContract.WeatherConditionsEntry; import vandy.mooc.model.provider.WeatherContract.WeatherValuesEntry; import vandy.mooc.model.webdata.WeatherData; import vandy.mooc.model.webdata.WeatherData.Main; import vandy.mooc.model.webdata.WeatherData.Sys; import vandy.mooc.model.webdata.WeatherData.Weather; import vandy.mooc.model.webdata.WeatherData.Wind; import android.app.AlarmManager; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.os.SystemClock; import android.util.Log; /** * Timeout cache that uses a content provider to store data and the * Alarm manager and a broadcast receiver to remove expired cache * entries */ public class WeatherTimeoutCache implements TimeoutCache<String, WeatherData> { /** * LogCat tag. */ private final static String TAG = WeatherTimeoutCache.class.getSimpleName(); /** * Default cache timeout in to 25 seconds (in milliseconds). */ private static final long DEFAULT_TIMEOUT = Long.valueOf(25000L); /** * Cache is cleaned up at regular intervals (i.e., twice a day) to * remove expired WeatherData. */ public static final long CLEANUP_SCHEDULER_TIME_INTERVAL = AlarmManager.INTERVAL_HALF_DAY; /** * AlarmManager provides access to the system alarm services. * Used to schedule Cache cleanup at regular intervals to remove * expired Weather Values. */ private AlarmManager mAlarmManager; /** * Defines the selection clause used to query for weather values * that has a specific id. */ private static final String WEATHER_VALUES_LOCATION_KEY_SELECTION = WeatherValuesEntry.COLUMN_LOCATION_KEY + " = ?"; /** * Defines the selection clause used to query for weather values * that has a specific id and expiration time. */ private static final String WEATHER_VALUES_LOCATION_TIME_KEY_SELECTION = WeatherValuesEntry.COLUMN_LOCATION_KEY + " = ?" + " AND " + WeatherValuesEntry.COLUMN_EXPIRATION_TIME + " = ?"; /** * Defines the selection clause used to query for weather * conditions that have a specific parent id. */ private static final String WEATHER_CONDITIONS_LOCATION_KEY_SELECTION = WeatherConditionsEntry.COLUMN_LOCATION_KEY + " = ?"; /** * Defines the selection clause used to query for weather * conditions that have a specific parent id and expiration time. */ private static final String WEATHER_CONDITIONS_LOCATION_TIME_KEY_SELECTION = WeatherConditionsEntry.COLUMN_LOCATION_KEY + " = ?" + " AND " + WeatherConditionsEntry.COLUMN_EXPIRATION_TIME + " = ?"; /** * The timeout for an instance of this class in milliseconds. */ private long mDefaultTimeout; /** * Context used to access the contentResolver. */ private Context mContext; /** * Constructor that sets the default timeout for the cache (in * seconds). */ public WeatherTimeoutCache(Context context) { // Store the context. mContext = context; // Set the timeout in milliseconds. mDefaultTimeout = DEFAULT_TIMEOUT; // Get the AlarmManager system service. mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); // If Cache Cleanup is not scheduled, then schedule it. scheduleCacheCleanup(context); } /** * Helper method that creates a content values object that can be * inserted into the db's WeatherValuesEntry table from a given * WeatherData object. */ private ContentValues makeWeatherDataContentValues(WeatherData wd, long expirationTime, String locationKey) { ContentValues cvs = new ContentValues(); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_LOCATION_KEY, locationKey); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_NAME, wd.getName()); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_DATE, wd.getDate()); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_COD, wd.getCod()); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_SUNRISE, wd.getSys().getSunrise()); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_SUNSET, wd.getSys().getSunset()); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_COUNTRY, wd.getSys().getCountry()); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_TEMP, wd.getMain().getTemp()); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_HUMIDITY, wd.getMain().getHumidity()); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_PRESSURE, wd.getMain().getPressure()); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_SPEED, wd.getWind().getSpeed()); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_DEG, wd.getWind().getDeg()); cvs.put(WeatherContract.WeatherValuesEntry.COLUMN_EXPIRATION_TIME, expirationTime); return cvs; } /** * Helper method that creates a content values object that can be * inserted into the db's WeatherConditionsEntry table from a * given WeatherData object. */ private ContentValues makeWeatherConditionsContentValues(Weather wo, long expirationTime, String locationKey) { ContentValues cvs = new ContentValues(); cvs.put(WeatherContract.WeatherConditionsEntry.COLUMN_WEATHER_CONDITIONS_OBJECT_ID, wo.getId()); cvs.put(WeatherContract.WeatherConditionsEntry.COLUMN_MAIN, wo.getMain()); cvs.put(WeatherContract.WeatherConditionsEntry.COLUMN_DESCRIPTION, wo.getDescription()); cvs.put(WeatherContract.WeatherConditionsEntry.COLUMN_ICON, wo.getIcon()); cvs.put(WeatherContract.WeatherConditionsEntry.COLUMN_LOCATION_KEY, locationKey); cvs.put(WeatherContract.WeatherConditionsEntry.COLUMN_EXPIRATION_TIME, expirationTime); return cvs; } /** * Place the WeatherData object into the cache. It assumes that a * get() method has already attempted to find this object's * location in the cache, and returned null. */ @Override public void put(String key, WeatherData obj) { putImpl(key, obj, mDefaultTimeout); } /** * Places the WeatherData object into the cache with a user specified * timeout. */ @Override public void put(String key, WeatherData obj, int timeout) { putImpl(key, obj, // Timeout must be expressed in milliseconds. timeout * 1000); } /** * Helper method that places a WeatherData object into the * database. */ private void putImpl(String locationKey, WeatherData wd, long timeout) { // Determine the data's expiration time final long expirationTime = System.currentTimeMillis() + timeout; // Enter the main WeatherData into the WeatherValues table. mContext.getContentResolver().insert (WeatherContract.WeatherValuesEntry.WEATHER_VALUES_CONTENT_URI, makeWeatherDataContentValues(wd, expirationTime, locationKey)); // Create an array of ContentValues to bulk insert into the // database. ContentValues[] cvsArray = new ContentValues[wd.getWeathers().size()]; // Index into cvsArray. int i = 0; // Insert each weather object into the ContentValues array. for (Weather weather : wd.getWeathers()) { cvsArray[i++] = makeWeatherConditionsContentValues(weather, expirationTime, locationKey); } // Bulk insert the rows into the WeatherConditions table. mContext.getContentResolver() .bulkInsert (WeatherContract.WeatherConditionsEntry.WEATHER_CONDITIONS_CONTENT_URI, cvsArray); } /** * Attempts to retrieve the given key's corresponding WeatherData * object. If the key doesn't exist or has timed out, null is * returned. */ @Override public WeatherData get(final String locationKey) { // Attempt to retrieve the location's data from the content // provider. try (Cursor wdCursor = mContext.getContentResolver().query (WeatherContract.ACCESS_ALL_DATA_FOR_LOCATION_URI, null, WEATHER_VALUES_LOCATION_KEY_SELECTION, new String[] { locationKey }, null)) { // Check that the cursor isn't null and contains an item. if (wdCursor != null && wdCursor.moveToFirst()) { Log.v(TAG, "Cursor not null and has first item"); // If cursor has a Weather Values object corresponding // to the location, check to see if it has expired. // If it has, delete it concurrently, else return the // data. final long expirationTime = wdCursor.getLong (wdCursor.getColumnIndex (WeatherContract.WeatherValuesEntry.COLUMN_EXPIRATION_TIME)); if (expirationTime < System.currentTimeMillis()) { // Concurrently delete the stale data from the db // in a new thread. new Thread(new Runnable() { public void run() { // Remove the key that has the designated // expiration time. remove(locationKey, expirationTime); } }).start(); return null; } else // Convert the contents of the cursor into a // WeatherData object. return getWeatherDataFromCursor(wdCursor); } else // Query was empty or returned null. return null; } } /** * Constructor using a cursor returned by the WeatherProvider. * This cursor must contain all the data for the object - i.e., it * must contain a row for each Weather object corresponding to the * Weather object. */ private WeatherData getWeatherDataFromCursor(Cursor data) { if (data == null || !data.moveToFirst()) return null; else { // Obtain data from the first row. final String name = data.getString(data.getColumnIndex(WeatherValuesEntry.COLUMN_NAME)); final long date = data.getLong(data.getColumnIndex(WeatherValuesEntry.COLUMN_DATE)); final long cod = data.getLong(data.getColumnIndex(WeatherValuesEntry.COLUMN_COD)); final Sys sys = new Sys(data.getLong(data.getColumnIndex (WeatherValuesEntry.COLUMN_SUNRISE)), data.getLong(data.getColumnIndex (WeatherValuesEntry.COLUMN_SUNSET)), data.getString(data.getColumnIndex (WeatherValuesEntry.COLUMN_COUNTRY))); final Main main = new Main(data.getDouble(data.getColumnIndex (WeatherValuesEntry.COLUMN_TEMP)), data.getLong(data.getColumnIndex (WeatherValuesEntry.COLUMN_HUMIDITY)), data.getDouble(data.getColumnIndex (WeatherValuesEntry.COLUMN_PRESSURE))); final Wind wind = new Wind(data.getDouble(data.getColumnIndex (WeatherValuesEntry.COLUMN_SPEED)), data.getDouble(data.getColumnIndex (WeatherValuesEntry.COLUMN_DEG))); final ArrayList<Weather> weathers = new ArrayList<>(); // Once the Weather Values are processed, loop through the // cursor to get all the Weather Conditions. do { weathers.add(new Weather (data.getLong (data.getColumnIndex (WeatherConditionsEntry.COLUMN_WEATHER_CONDITIONS_OBJECT_ID)), data.getString (data.getColumnIndex (WeatherConditionsEntry.COLUMN_MAIN)), data.getString (data.getColumnIndex (WeatherConditionsEntry.COLUMN_DESCRIPTION)), data.getString (data.getColumnIndex (WeatherConditionsEntry.COLUMN_ICON)))); } while (data.moveToNext()); // Return a WeatherData object. return new WeatherData(name, date, cod, sys, main, wind, weathers); } } /** * Delete the Weather Values and Weather Conditions associated * with a @a locationKey and a specific @a expirationTime. */ public void remove(String locationKey, long expirationTime) { // Delete expired entries from the WeatherValues table. mContext.getContentResolver().delete (WeatherValuesEntry.WEATHER_VALUES_CONTENT_URI, WEATHER_VALUES_LOCATION_TIME_KEY_SELECTION, new String[] { locationKey, Long.toString(expirationTime) }); // Delete expired entries from the WeatherConditions table. mContext.getContentResolver().delete (WeatherConditionsEntry.WEATHER_CONDITIONS_CONTENT_URI, WEATHER_CONDITIONS_LOCATION_TIME_KEY_SELECTION, new String[] { locationKey, Long.toString(expirationTime) }); } /** * Return the current number of WeatherData objects in Database. * * @return size */ @Override public int size() { // Query the data for all rows of the Weather Values table. try (Cursor cursor = mContext.getContentResolver().query (WeatherContract.WeatherValuesEntry.WEATHER_VALUES_CONTENT_URI, new String[] {WeatherValuesEntry._ID}, null, null, null)) { // Return the number of rows in the table, which is equivlent // to the number of objects return cursor.getCount(); } } /** * Remove all expired WeatherData rows from the database. This * method is called periodically via the AlarmManager. */ public void removeExpiredWeatherData() { // Defines the selection clause used to query for weather values // that has expired. final String EXPIRATION_SELECTION = WeatherValuesEntry.COLUMN_EXPIRATION_TIME + " <= ?"; // First query the db to find all expired Weather Values // objects' ids. try (Cursor expiredData = mContext.getContentResolver().query (WeatherValuesEntry.WEATHER_VALUES_CONTENT_URI, new String[] { WeatherValuesEntry.COLUMN_LOCATION_KEY, WeatherValuesEntry.COLUMN_EXPIRATION_TIME }, EXPIRATION_SELECTION, new String[] {String.valueOf(System.currentTimeMillis())}, null)) { // Use the expired data id's to delete the designated // entries from both tables. if (expiredData != null && expiredData.moveToFirst()) { do { // Get the location to delete. final String deleteLocation = expiredData.getString (expiredData.getColumnIndex (WeatherValuesEntry.COLUMN_LOCATION_KEY)); final long expirationTime = expiredData.getLong (expiredData.getColumnIndex (WeatherValuesEntry.COLUMN_EXPIRATION_TIME)); remove(deleteLocation, expirationTime); } while (expiredData.moveToNext()); } } } /** * Helper method that uses AlarmManager to schedule Cache Cleanup at regular * intervals. * * @param context */ private void scheduleCacheCleanup(Context context) { // Only schedule the Alarm if it's not already scheduled. if (!isAlarmActive(context)) { // Schedule an alarm after a certain timeout to start a // service to delete expired data from Database. mAlarmManager.setInexactRepeating (AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + CLEANUP_SCHEDULER_TIME_INTERVAL, CLEANUP_SCHEDULER_TIME_INTERVAL, CacheCleanupReceiver.makeReceiverPendingIntent(context)); } } /** * Helper method to check whether the Alarm is already active or not. * * @param context * @return boolean, whether the Alarm is already active or not */ private boolean isAlarmActive(Context context) { // Check whether the Pending Intent already exists or not. return CacheCleanupReceiver.makeCheckAlarmPendingIntent(context) != null; } }