PageRenderTime 19ms CodeModel.GetById 2ms app.highlight 13ms RepoModel.GetById 2ms app.codeStats 0ms

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