CalendarAppWidgetService.java revision 3f888688c0f2644ad3de032d5d1cf623a7b092fd
1/*
2 * Copyright (C) 2009 The Android Open Source Project
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 com.android.calendar.widget;
18
19import com.android.calendar.R;
20import com.android.calendar.Utils;
21import com.android.calendar.widget.CalendarAppWidgetModel.DayInfo;
22import com.android.calendar.widget.CalendarAppWidgetModel.EventInfo;
23import com.android.calendar.widget.CalendarAppWidgetModel.RowInfo;
24
25import android.app.AlarmManager;
26import android.app.PendingIntent;
27import android.appwidget.AppWidgetManager;
28import android.content.BroadcastReceiver;
29import android.content.ContentResolver;
30import android.content.Context;
31import android.content.CursorLoader;
32import android.content.Intent;
33import android.content.IntentFilter;
34import android.content.IntentFilter.MalformedMimeTypeException;
35import android.content.Loader;
36import android.content.res.Resources;
37import android.database.Cursor;
38import android.database.MatrixCursor;
39import android.net.Uri;
40import android.os.Handler;
41import android.provider.Calendar;
42import android.provider.Calendar.Attendees;
43import android.provider.Calendar.Calendars;
44import android.provider.Calendar.Instances;
45import android.text.format.DateUtils;
46import android.text.format.Time;
47import android.util.Log;
48import android.view.View;
49import android.widget.RemoteViews;
50import android.widget.RemoteViewsService;
51
52
53public class CalendarAppWidgetService extends RemoteViewsService {
54    private static final String TAG = "CalendarWidget";
55
56    static final int EVENT_MIN_COUNT = 20;
57    static final int EVENT_MAX_COUNT = 503;
58    // Minimum delay between queries on the database for widget updates in ms
59    static final int WIDGET_UPDATE_THROTTLE = 500;
60
61    private static final String EVENT_SORT_ORDER = Instances.START_DAY + " ASC, "
62            + Instances.START_MINUTE + " ASC, " + Instances.END_DAY + " ASC, "
63            + Instances.END_MINUTE + " ASC LIMIT " + EVENT_MAX_COUNT;
64
65    // TODO can't use parameter here because provider is dropping them
66    private static final String EVENT_SELECTION = Calendars.VISIBLE + "=1 AND "
67            + Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED;
68
69    static final String[] EVENT_PROJECTION = new String[] {
70        Instances.ALL_DAY,
71        Instances.BEGIN,
72        Instances.END,
73        Instances.TITLE,
74        Instances.EVENT_LOCATION,
75        Instances.EVENT_ID,
76        Instances.START_DAY,
77        Instances.END_DAY,
78        Instances.CALENDAR_COLOR
79    };
80
81    static final int INDEX_ALL_DAY = 0;
82    static final int INDEX_BEGIN = 1;
83    static final int INDEX_END = 2;
84    static final int INDEX_TITLE = 3;
85    static final int INDEX_EVENT_LOCATION = 4;
86    static final int INDEX_EVENT_ID = 5;
87    static final int INDEX_START_DAY = 6;
88    static final int INDEX_END_DAY = 7;
89    static final int INDEX_COLOR = 8;
90
91    static final int MAX_DAYS = 7;
92
93    private static final long SEARCH_DURATION = MAX_DAYS * DateUtils.DAY_IN_MILLIS;
94
95    /**
96     * Update interval used when no next-update calculated, or bad trigger time in past.
97     * Unit: milliseconds.
98     */
99    private static final long UPDATE_TIME_NO_EVENTS = DateUtils.HOUR_IN_MILLIS * 6;
100
101    @Override
102    public RemoteViewsFactory onGetViewFactory(Intent intent) {
103        return new CalendarFactory(getApplicationContext(), intent);
104    }
105
106    protected static class CalendarFactory extends BroadcastReceiver implements
107            RemoteViewsService.RemoteViewsFactory, Loader.OnLoadCompleteListener<Cursor> {
108        private static final boolean LOGD = false;
109
110        // Suppress unnecessary logging about update time. Need to be static as this object is
111        // re-instanciated frequently.
112        // TODO: It seems loadData() is called via onCreate() four times, which should mean
113        // unnecessary CalendarFactory object is created and dropped. It is not efficient.
114        private static long sLastUpdateTime = UPDATE_TIME_NO_EVENTS;
115
116        private Context mContext;
117        private Resources mResources;
118        private CalendarAppWidgetModel mModel;
119        private Cursor mCursor;
120        private CursorLoader mLoader;
121        private Handler mHandler = new Handler();
122        private int mAppWidgetId;
123
124        private Runnable mTimezoneChanged = new Runnable() {
125            @Override
126            public void run() {
127                if (mLoader != null) {
128                    mLoader.forceLoad();
129                }
130            }
131        };
132
133        private Runnable mUpdateLoader = new Runnable() {
134            @Override
135            public void run() {
136                if (mLoader != null) {
137                    Uri uri = createLoaderUri();
138                    mLoader.setUri(uri);
139                    mLoader.forceLoad();
140                }
141            }
142        };
143
144        protected CalendarFactory(Context context, Intent intent) {
145            mContext = context;
146            mResources = context.getResources();
147            mAppWidgetId = intent.getIntExtra(
148                    AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
149        }
150
151        @Override
152        public void onCreate() {
153            initLoader();
154        }
155
156        @Override
157        public void onDataSetChanged() {
158        }
159
160        @Override
161        public void onDestroy() {
162            if (mCursor != null) {
163                mCursor.close();
164            }
165            if (mLoader != null) {
166                mLoader.reset();
167            }
168            mContext.unregisterReceiver(this);
169        }
170
171        @Override
172        public RemoteViews getLoadingView() {
173            RemoteViews views = new RemoteViews(mContext.getPackageName(),
174                    R.layout.appwidget_loading);
175            return views;
176        }
177
178        @Override
179        public RemoteViews getViewAt(int position) {
180            // we use getCount here so that it doesn't return null when empty
181            if (position < 0 || position >= getCount()) {
182                return null;
183            }
184
185            if (mModel == null || mModel.mEventInfos.isEmpty() || mModel.mRowInfos.isEmpty()) {
186                RemoteViews views = new RemoteViews(mContext.getPackageName(),
187                        R.layout.appwidget_no_events);
188                final Intent intent =  CalendarAppWidgetProvider.getLaunchFillInIntent(0, 0, 0);
189                views.setOnClickFillInIntent(R.id.appwidget_no_events, intent);
190                return views;
191            }
192
193            RowInfo rowInfo = mModel.mRowInfos.get(position);
194            if (rowInfo.mType == RowInfo.TYPE_DAY) {
195                RemoteViews views = new RemoteViews(mContext.getPackageName(),
196                        R.layout.appwidget_day);
197                DayInfo dayInfo = mModel.mDayInfos.get(rowInfo.mIndex);
198                updateTextView(views, R.id.date, View.VISIBLE, dayInfo.mDayLabel);
199                return views;
200            } else {
201                final RemoteViews views = new RemoteViews(mContext.getPackageName(),
202                        R.layout.appwidget_row);
203                final EventInfo eventInfo = mModel.mEventInfos.get(rowInfo.mIndex);
204
205                final long now = System.currentTimeMillis();
206                if (!eventInfo.allDay && eventInfo.start <= now && now <= eventInfo.end) {
207                    views.setInt(R.id.appwidget_row, "setBackgroundColor",
208                            mResources.getColor(R.color.appwidget_row_in_progress));
209                } else {
210                    views.setInt(R.id.appwidget_row, "setBackgroundResource",
211                            R.drawable.bg_event_cal_widget_holo);
212                }
213
214                updateTextView(views, R.id.when, eventInfo.visibWhen, eventInfo.when);
215                updateTextView(views, R.id.where, eventInfo.visibWhere, eventInfo.where);
216                updateTextView(views, R.id.title, eventInfo.visibTitle, eventInfo.title);
217
218                views.setViewVisibility(R.id.color, View.VISIBLE);
219                views.setInt(R.id.color, "setBackgroundColor", eventInfo.color);
220
221                long start = eventInfo.start;
222                long end = eventInfo.end;
223                // An element in ListView.
224                if (eventInfo.allDay) {
225                    String tz = Utils.getTimeZone(mContext, null);
226                    Time recycle = new Time();
227                    start = Utils.convertAlldayLocalToUTC(recycle, start, tz);
228                    end = Utils.convertAlldayLocalToUTC(recycle, end, tz);
229                }
230                final Intent fillInIntent = CalendarAppWidgetProvider.getLaunchFillInIntent(
231                        eventInfo.id, start, end);
232                views.setOnClickFillInIntent(R.id.appwidget_row, fillInIntent);
233                return views;
234            }
235        }
236
237        @Override
238        public int getViewTypeCount() {
239            return 4;
240        }
241
242        @Override
243        public int getCount() {
244            // if there are no events, we still return 1 to represent the "no
245            // events" view
246            if (mModel == null) {
247                return 1;
248            }
249            return Math.max(1, mModel.mRowInfos.size());
250        }
251
252        @Override
253        public long getItemId(int position) {
254            if (mModel == null ||  mModel.mRowInfos.isEmpty()) {
255                return 0;
256            }
257            RowInfo rowInfo = mModel.mRowInfos.get(position);
258            if (rowInfo.mType == RowInfo.TYPE_DAY) {
259                return rowInfo.mIndex;
260            }
261            EventInfo eventInfo = mModel.mEventInfos.get(rowInfo.mIndex);
262            long prime = 31;
263            long result = 1;
264            result = prime * result + (int) (eventInfo.id ^ (eventInfo.id >>> 32));
265            result = prime * result + (int) (eventInfo.start ^ (eventInfo.start >>> 32));
266            return result;
267        }
268
269        @Override
270        public boolean hasStableIds() {
271            return true;
272        }
273
274        /**
275         * Query across all calendars for upcoming event instances from now
276         * until some time in the future. Widen the time range that we query by
277         * one day on each end so that we can catch all-day events. All-day
278         * events are stored starting at midnight in UTC but should be included
279         * in the list of events starting at midnight local time. This may fetch
280         * more events than we actually want, so we filter them out later.
281         *
282         * @param resolver {@link ContentResolver} to use when querying
283         *            {@link Instances#CONTENT_URI}.
284         * @param searchDuration Distance into the future to look for event
285         *            instances, in milliseconds.
286         * @param now Current system time to use for this update, possibly from
287         *            {@link System#currentTimeMillis()}.
288         */
289        public void initLoader() {
290            if (LOGD)
291                Log.d(TAG, "Querying for widget events...");
292            IntentFilter filter = new IntentFilter();
293            filter.addAction(CalendarAppWidgetProvider.ACTION_CALENDAR_APPWIDGET_SCHEDULED_UPDATE);
294            filter.addDataScheme(ContentResolver.SCHEME_CONTENT);
295            filter.addDataAuthority(Calendar.AUTHORITY, null);
296            try {
297                filter.addDataType(CalendarAppWidgetProvider.APPWIDGET_DATA_TYPE);
298            } catch (MalformedMimeTypeException e) {
299                Log.e(TAG, e.getMessage());
300            }
301            mContext.registerReceiver(this, filter);
302
303            filter = new IntentFilter();
304            filter.addAction(Intent.ACTION_PROVIDER_CHANGED);
305            filter.addDataScheme(ContentResolver.SCHEME_CONTENT);
306            filter.addDataAuthority(Calendar.AUTHORITY, null);
307            mContext.registerReceiver(this, filter);
308
309            filter = new IntentFilter();
310            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
311            filter.addAction(Intent.ACTION_TIME_CHANGED);
312            filter.addAction(Intent.ACTION_DATE_CHANGED);
313            mContext.registerReceiver(this, filter);
314
315            // Search for events from now until some time in the future
316            Uri uri = createLoaderUri();
317
318            mLoader = new CursorLoader(
319                    mContext, uri, EVENT_PROJECTION, EVENT_SELECTION, null, EVENT_SORT_ORDER);
320            mLoader.setUpdateThrottle(WIDGET_UPDATE_THROTTLE);
321            mLoader.startLoading();
322            mLoader.registerListener(mAppWidgetId, this);
323
324        }
325
326        /**
327         * @return The uri for the loader
328         */
329        private Uri createLoaderUri() {
330            long now = System.currentTimeMillis();
331            // Add a day on either side to catch all-day events
332            long begin = now - DateUtils.DAY_IN_MILLIS;
333            long end = now + SEARCH_DURATION + DateUtils.DAY_IN_MILLIS;
334
335            Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI, Long.toString(begin) + "/" + end);
336            return uri;
337        }
338
339        /* @VisibleForTesting */
340        protected static CalendarAppWidgetModel buildAppWidgetModel(
341                Context context, Cursor cursor, String timeZone) {
342            CalendarAppWidgetModel model = new CalendarAppWidgetModel(context, timeZone);
343            model.buildFromCursor(cursor, timeZone);
344            return model;
345        }
346
347        /**
348         * Calculates and returns the next time we should push widget updates.
349         */
350        private long calculateUpdateTime(CalendarAppWidgetModel model, long now, String timeZone) {
351            // Make sure an update happens at midnight or earlier
352            long minUpdateTime = getNextMidnightTimeMillis(timeZone);
353            for (EventInfo event : model.mEventInfos) {
354                final boolean allDay = event.allDay;
355                final long start;
356                final long end;
357                start = event.start;
358                end = event.end;
359
360                // We want to update widget when we enter/exit time range of an event.
361                if (now < start) {
362                    minUpdateTime = Math.min(minUpdateTime, start);
363                } else if (now < end) {
364                    minUpdateTime = Math.min(minUpdateTime, end);
365                }
366            }
367            return minUpdateTime;
368        }
369
370        private static long getNextMidnightTimeMillis(String timezone) {
371            Time time = new Time();
372            time.setToNow();
373            time.monthDay++;
374            time.hour = 0;
375            time.minute = 0;
376            time.second = 0;
377            long midnightDeviceTz = time.normalize(true);
378
379            time.timezone = timezone;
380            time.setToNow();
381            time.monthDay++;
382            time.hour = 0;
383            time.minute = 0;
384            time.second = 0;
385            long midnightHomeTz = time.normalize(true);
386
387            return Math.min(midnightDeviceTz, midnightHomeTz);
388        }
389
390        static void updateTextView(RemoteViews views, int id, int visibility, String string) {
391            views.setViewVisibility(id, visibility);
392            if (visibility == View.VISIBLE) {
393                views.setTextViewText(id, string);
394            }
395        }
396
397        /*
398         * (non-Javadoc)
399         * @see
400         * android.content.Loader.OnLoadCompleteListener#onLoadComplete(android
401         * .content.Loader, java.lang.Object)
402         */
403        @Override
404        public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
405            // Copy it to a local static cursor.
406            MatrixCursor matrixCursor = Utils.matrixCursorFromCursor(cursor);
407            cursor.close();
408
409            final long now = System.currentTimeMillis();
410            if (mCursor != null) {
411                mCursor.close();
412            }
413            mCursor = matrixCursor;
414            String tz = Utils.getTimeZone(mContext, mTimezoneChanged);
415            mModel = buildAppWidgetModel(mContext, mCursor, tz);
416
417            // Schedule an alarm to wake ourselves up for the next update.
418            // We also cancel
419            // all existing wake-ups because PendingIntents don't match
420            // against extras.
421            long triggerTime = calculateUpdateTime(mModel, now, tz);
422
423            // If no next-update calculated, or bad trigger time in past,
424            // schedule
425            // update about six hours from now.
426            if (triggerTime < now) {
427                Log.w(TAG, "Encountered bad trigger time " + formatDebugTime(triggerTime, now));
428                triggerTime = now + UPDATE_TIME_NO_EVENTS;
429            }
430
431
432            final AlarmManager alertManager = (AlarmManager) mContext.getSystemService(
433                    Context.ALARM_SERVICE);
434            final PendingIntent pendingUpdate = CalendarAppWidgetProvider.getUpdateIntent(mContext);
435
436            alertManager.cancel(pendingUpdate);
437            alertManager.set(AlarmManager.RTC, triggerTime, pendingUpdate);
438            Log.d(TAG, "Scheduled next update at " + formatDebugTime(triggerTime, now));
439            Time time = new Time(Utils.getTimeZone(mContext, null));
440            time.setToNow();
441
442            if (time.normalize(true) != sLastUpdateTime) {
443                Time time2 = new Time(Utils.getTimeZone(mContext, null));
444                time2.set(sLastUpdateTime);
445                time2.normalize(true);
446                if (time.year != time2.year || time.yearDay != time2.yearDay) {
447                    final Intent updateIntent = new Intent(
448                            CalendarAppWidgetProvider.ACTION_CALENDAR_APPWIDGET_UPDATE);
449                    mContext.sendBroadcast(updateIntent);
450                }
451
452                sLastUpdateTime = time.toMillis(true);
453            }
454
455            AppWidgetManager.getInstance(mContext).notifyAppWidgetViewDataChanged(
456                    mAppWidgetId, R.id.events_list);
457        }
458
459        @Override
460        public void onReceive(Context context, Intent intent) {
461            mHandler.removeCallbacks(mUpdateLoader);
462            mHandler.post(mUpdateLoader);
463        }
464    }
465
466    /**
467     * Format given time for debugging output.
468     *
469     * @param unixTime Target time to report.
470     * @param now Current system time from {@link System#currentTimeMillis()}
471     *            for calculating time difference.
472     */
473    static String formatDebugTime(long unixTime, long now) {
474        Time time = new Time();
475        time.set(unixTime);
476
477        long delta = unixTime - now;
478        if (delta > DateUtils.MINUTE_IN_MILLIS) {
479            delta /= DateUtils.MINUTE_IN_MILLIS;
480            return String.format("[%d] %s (%+d mins)", unixTime,
481                    time.format("%H:%M:%S"), delta);
482        } else {
483            delta /= DateUtils.SECOND_IN_MILLIS;
484            return String.format("[%d] %s (%+d secs)", unixTime,
485                    time.format("%H:%M:%S"), delta);
486        }
487    }
488}
489