/Sky/src/org/jsharkey/sky/UpdateService.java

http://android-sky.googlecode.com/ · Java · 289 lines · 154 code · 41 blank · 94 comment · 23 complexity · 04c7333a57f0ff2b58b092f47f0f89b5 MD5 · raw file

  1. /*
  2. * Copyright (C) 2009 Jeff Sharkey, http://jsharkey.org/
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.jsharkey.sky;
  17. import java.util.LinkedList;
  18. import java.util.Queue;
  19. import org.jsharkey.sky.ForecastProvider.AppWidgets;
  20. import org.jsharkey.sky.ForecastProvider.AppWidgetsColumns;
  21. import org.jsharkey.sky.webservice.WebserviceHelper;
  22. import org.jsharkey.sky.webservice.Forecast.ParseException;
  23. import android.app.AlarmManager;
  24. import android.app.PendingIntent;
  25. import android.app.Service;
  26. import android.appwidget.AppWidgetManager;
  27. import android.appwidget.AppWidgetProviderInfo;
  28. import android.content.ComponentName;
  29. import android.content.ContentResolver;
  30. import android.content.ContentUris;
  31. import android.content.Context;
  32. import android.content.Intent;
  33. import android.database.Cursor;
  34. import android.net.Uri;
  35. import android.os.IBinder;
  36. import android.text.format.DateUtils;
  37. import android.text.format.Time;
  38. import android.util.Log;
  39. import android.widget.RemoteViews;
  40. /**
  41. * Background service to build any requested widget updates. Uses a single
  42. * background thread to walk through an update queue, querying
  43. * {@link WebserviceHelper} as needed to fill database. Also handles scheduling
  44. * of future updates, usually in 6-hour increments.
  45. */
  46. public class UpdateService extends Service implements Runnable {
  47. private static final String TAG = "UpdateService";
  48. private static final String[] PROJECTION_APPWIDGETS = new String[] {
  49. AppWidgetsColumns.CONFIGURED,
  50. AppWidgetsColumns.LAST_UPDATED,
  51. };
  52. private static final int COL_CONFIGURED = 0;
  53. private static final int COL_LAST_UPDATED = 1;
  54. /**
  55. * Interval to wait between background widget updates. Every 6 hours is
  56. * plenty to keep background data usage low and still provide fresh data.
  57. */
  58. private static final long UPDATE_INTERVAL = 6 * DateUtils.HOUR_IN_MILLIS;
  59. /**
  60. * When rounding updates to the nearest-top-of-hour, trigger the update
  61. * slightly early by this amount. This makes sure that we're already updated
  62. * when the user's 6AM alarm clock goes off.
  63. */
  64. private static final long UPDATE_TRIGGER_EARLY = 10 * DateUtils.MINUTE_IN_MILLIS;
  65. /**
  66. * If we calculated an update too quickly in the future, wait this interval
  67. * and try rescheduling.
  68. */
  69. private static final long UPDATE_THROTTLE = 30 * DateUtils.MINUTE_IN_MILLIS;
  70. /**
  71. * Specific {@link Intent#setAction(String)} used when performing a full
  72. * update of all widgets, usually when an update alarm goes off.
  73. */
  74. public static final String ACTION_UPDATE_ALL = "org.jsharkey.sky.UPDATE_ALL";
  75. /**
  76. * Length of time before we consider cached forecasts stale. If a widget
  77. * update is requested, and {@link AppWidgetsColumns#LAST_UPDATED} is inside
  78. * this threshold, we use the cached forecast data to build the update.
  79. * Otherwise, we first trigger an update through {@link WebserviceHelper}.
  80. */
  81. private static final long FORECAST_CACHE_THROTTLE = 3 * DateUtils.HOUR_IN_MILLIS;
  82. /**
  83. * Number of days into the future to request forecasts for.
  84. */
  85. private static final int FORECAST_DAYS = 4;
  86. /**
  87. * Lock used when maintaining queue of requested updates.
  88. */
  89. private static Object sLock = new Object();
  90. /**
  91. * Flag if there is an update thread already running. We only launch a new
  92. * thread if one isn't already running.
  93. */
  94. private static boolean sThreadRunning = false;
  95. /**
  96. * Internal queue of requested widget updates. You <b>must</b> access
  97. * through {@link #requestUpdate(int[])} or {@link #getNextUpdate()} to make
  98. * sure your access is correctly synchronized.
  99. */
  100. private static Queue<Integer> sAppWidgetIds = new LinkedList<Integer>();
  101. /**
  102. * Request updates for the given widgets. Will only queue them up, you are
  103. * still responsible for starting a processing thread if needed, usually by
  104. * starting the parent service.
  105. */
  106. public static void requestUpdate(int[] appWidgetIds) {
  107. synchronized (sLock) {
  108. for (int appWidgetId : appWidgetIds) {
  109. sAppWidgetIds.add(appWidgetId);
  110. }
  111. }
  112. }
  113. /**
  114. * Peek if we have more updates to perform. This method is special because
  115. * it assumes you're calling from the update thread, and that you will
  116. * terminate if no updates remain. (It atomically resets
  117. * {@link #sThreadRunning} when none remain to prevent race conditions.)
  118. */
  119. private static boolean hasMoreUpdates() {
  120. synchronized (sLock) {
  121. boolean hasMore = !sAppWidgetIds.isEmpty();
  122. if (!hasMore) {
  123. sThreadRunning = false;
  124. }
  125. return hasMore;
  126. }
  127. }
  128. /**
  129. * Poll the next widget update in the queue.
  130. */
  131. private static int getNextUpdate() {
  132. synchronized (sLock) {
  133. if (sAppWidgetIds.peek() == null) {
  134. return AppWidgetManager.INVALID_APPWIDGET_ID;
  135. } else {
  136. return sAppWidgetIds.poll();
  137. }
  138. }
  139. }
  140. /**
  141. * Start this service, creating a background processing thread, if not
  142. * already running. If started with {@link #ACTION_UPDATE_ALL}, will
  143. * automatically add all widgets to the requested update queue.
  144. */
  145. @Override
  146. public void onStart(Intent intent, int startId) {
  147. super.onStart(intent, startId);
  148. // If requested, trigger update of all widgets
  149. if (ACTION_UPDATE_ALL.equals(intent.getAction())) {
  150. Log.d(TAG, "Requested UPDATE_ALL action");
  151. AppWidgetManager manager = AppWidgetManager.getInstance(this);
  152. requestUpdate(manager.getAppWidgetIds(new ComponentName(this, MedAppWidget.class)));
  153. requestUpdate(manager.getAppWidgetIds(new ComponentName(this, TinyAppWidget.class)));
  154. }
  155. // Only start processing thread if not already running
  156. synchronized (sLock) {
  157. if (!sThreadRunning) {
  158. sThreadRunning = true;
  159. new Thread(this).start();
  160. }
  161. }
  162. }
  163. /**
  164. * Main thread for running through any requested widget updates until none
  165. * remain. Also sets alarm to perform next update.
  166. */
  167. public void run() {
  168. Log.d(TAG, "Processing thread started");
  169. AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
  170. ContentResolver resolver = getContentResolver();
  171. long now = System.currentTimeMillis();
  172. while (hasMoreUpdates()) {
  173. int appWidgetId = getNextUpdate();
  174. Uri appWidgetUri = ContentUris.withAppendedId(AppWidgets.CONTENT_URI, appWidgetId);
  175. // Check if widget is configured, and if we need to update cache
  176. Cursor cursor = null;
  177. boolean isConfigured = false;
  178. boolean shouldUpdate = false;
  179. try {
  180. cursor = resolver.query(appWidgetUri, PROJECTION_APPWIDGETS, null, null, null);
  181. if (cursor != null && cursor.moveToFirst()) {
  182. isConfigured = cursor.getInt(COL_CONFIGURED) == AppWidgetsColumns.CONFIGURED_TRUE;
  183. long lastUpdated = cursor.getLong(COL_LAST_UPDATED);
  184. long deltaMinutes = (now - lastUpdated) / DateUtils.MINUTE_IN_MILLIS;
  185. Log.d(TAG, "Delta since last forecast update is " + deltaMinutes + " min");
  186. shouldUpdate = (Math.abs(now - lastUpdated) > FORECAST_CACHE_THROTTLE);
  187. }
  188. } finally {
  189. if (cursor != null) {
  190. cursor.close();
  191. }
  192. }
  193. if (!isConfigured) {
  194. // Skip this update if not configured yet
  195. Log.d(TAG, "Not configured yet, so skipping update");
  196. continue;
  197. } else if (shouldUpdate) {
  198. // Last update is outside throttle window, so update again
  199. try {
  200. WebserviceHelper.updateForecasts(this, appWidgetUri, FORECAST_DAYS);
  201. } catch (ParseException e) {
  202. Log.e(TAG, "Problem parsing forecast", e);
  203. }
  204. }
  205. // Process this update through the correct provider
  206. AppWidgetProviderInfo info = appWidgetManager.getAppWidgetInfo(appWidgetId);
  207. String providerName = info.provider.getClassName();
  208. RemoteViews updateViews = null;
  209. if (providerName.equals(MedAppWidget.class.getName())) {
  210. updateViews = MedAppWidget.buildUpdate(this, appWidgetUri);
  211. } else if (providerName.equals(TinyAppWidget.class.getName())) {
  212. updateViews = TinyAppWidget.buildUpdate(this, appWidgetUri);
  213. }
  214. // Push this update to surface
  215. if (updateViews != null) {
  216. appWidgetManager.updateAppWidget(appWidgetId, updateViews);
  217. }
  218. }
  219. // Schedule next update alarm, usually just before a 6-hour block. This
  220. // triggers updates at roughly 5:50AM, 11:50AM, 5:50PM, and 11:50PM.
  221. Time time = new Time();
  222. time.set(System.currentTimeMillis() + UPDATE_INTERVAL + UPDATE_TRIGGER_EARLY);
  223. time.hour -= (time.hour % 6);
  224. time.minute = 0;
  225. time.second = 0;
  226. long nextUpdate = time.toMillis(false) - UPDATE_TRIGGER_EARLY;
  227. long nowMillis = System.currentTimeMillis();
  228. // Throttle our updates just in case the math went funky
  229. if (nextUpdate - nowMillis < UPDATE_THROTTLE) {
  230. Log.d(TAG, "Calculated next update too early, throttling for a few minutes");
  231. nextUpdate = nowMillis + UPDATE_THROTTLE;
  232. }
  233. long deltaMinutes = (nextUpdate - nowMillis) / DateUtils.MINUTE_IN_MILLIS;
  234. Log.d(TAG, "Requesting next update at " + nextUpdate + ", in " + deltaMinutes + " min");
  235. Intent updateIntent = new Intent(ACTION_UPDATE_ALL);
  236. updateIntent.setClass(this, UpdateService.class);
  237. PendingIntent pendingIntent = PendingIntent.getService(this, 0, updateIntent, 0);
  238. // Schedule alarm, and force the device awake for this update
  239. AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
  240. alarmManager.set(AlarmManager.RTC_WAKEUP, nextUpdate, pendingIntent);
  241. // No updates remaining, so stop service
  242. stopSelf();
  243. }
  244. @Override
  245. public IBinder onBind(Intent intent) {
  246. return null;
  247. }
  248. }