CalendarAppWidgetService.java revision ffaeace621183dfe8770471a30b2f1138aac5f86
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.google.common.annotations.VisibleForTesting;
20
21import com.android.calendar.R;
22import com.android.calendar.Utils;
23import com.android.calendar.widget.CalendarAppWidgetModel.DayInfo;
24import com.android.calendar.widget.CalendarAppWidgetModel.EventInfo;
25import com.android.calendar.widget.CalendarAppWidgetModel.RowInfo;
26
27import android.app.AlarmManager;
28import android.app.PendingIntent;
29import android.content.ContentResolver;
30import android.content.Context;
31import android.content.Intent;
32import android.content.res.Resources;
33import android.database.Cursor;
34import android.database.MatrixCursor;
35import android.net.Uri;
36import android.provider.Calendar.Attendees;
37import android.provider.Calendar.CalendarCache;
38import android.provider.Calendar.Calendars;
39import android.provider.Calendar.Instances;
40import android.text.TextUtils;
41import android.text.format.DateUtils;
42import android.text.format.Time;
43import android.util.Log;
44import android.view.View;
45import android.widget.RemoteViews;
46import android.widget.RemoteViewsService;
47
48
49public class CalendarAppWidgetService extends RemoteViewsService {
50    private static final String TAG = "CalendarWidget";
51
52    static final int EVENT_MIN_COUNT = 20;
53    static final int EVENT_MAX_COUNT = 503;
54
55    private static final String EVENT_SORT_ORDER = Instances.START_DAY + " ASC, "
56            + Instances.START_MINUTE + " ASC, " + Instances.END_DAY + " ASC, "
57            + Instances.END_MINUTE + " ASC LIMIT " + EVENT_MAX_COUNT;
58
59    // TODO can't use parameter here because provider is dropping them
60    private static final String EVENT_SELECTION = Calendars.SELECTED + "=1 AND "
61            + Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED;
62
63    static final String[] EVENT_PROJECTION = new String[] {
64        Instances.ALL_DAY,
65        Instances.BEGIN,
66        Instances.END,
67        Instances.TITLE,
68        Instances.EVENT_LOCATION,
69        Instances.EVENT_ID,
70        Instances.START_DAY,
71        Instances.END_DAY,
72        Instances.COLOR
73    };
74
75    static final int INDEX_ALL_DAY = 0;
76    static final int INDEX_BEGIN = 1;
77    static final int INDEX_END = 2;
78    static final int INDEX_TITLE = 3;
79    static final int INDEX_EVENT_LOCATION = 4;
80    static final int INDEX_EVENT_ID = 5;
81    static final int INDEX_START_DAY = 6;
82    static final int INDEX_END_DAY = 7;
83    static final int INDEX_COLOR = 8;
84
85    static final int MAX_DAYS = 7;
86
87    private static final long SEARCH_DURATION = MAX_DAYS * DateUtils.DAY_IN_MILLIS;
88
89    /**
90     * Update interval used when no next-update calculated, or bad trigger time in past.
91     * Unit: milliseconds.
92     */
93    private static final long UPDATE_TIME_NO_EVENTS = DateUtils.HOUR_IN_MILLIS * 6;
94
95    @Override
96    public RemoteViewsFactory onGetViewFactory(Intent intent) {
97        return new CalendarFactory(getApplicationContext(), intent);
98    }
99
100    protected static class CalendarFactory implements RemoteViewsService.RemoteViewsFactory {
101        private static final boolean LOGD = false;
102
103        // Suppress unnecessary logging about update time. Need to be static as this object is
104        // re-instanciated frequently.
105        // TODO: It seems loadData() is called via onCreate() four times, which should mean
106        // unnecessary CalendarFactory object is created and dropped. It is not efficient.
107        private static long sLastUpdateTime = UPDATE_TIME_NO_EVENTS;
108
109        private Context mContext;
110        private Resources mResources;
111        private CalendarAppWidgetModel mModel;
112        private Cursor mCursor;
113
114        protected CalendarFactory(Context context, Intent intent) {
115            mContext = context;
116            mResources = context.getResources();
117        }
118
119        @Override
120        public void onCreate() {
121            loadData();
122        }
123
124        @Override
125        public void onDataSetChanged() {
126            loadData();
127        }
128
129        @Override
130        public void onDestroy() {
131            mCursor.close();
132        }
133
134        @Override
135        public RemoteViews getLoadingView() {
136            RemoteViews views = new RemoteViews(mContext.getPackageName(),
137                    R.layout.appwidget_loading);
138            return views;
139        }
140
141        @Override
142        public RemoteViews getViewAt(int position) {
143            // we use getCount here so that it doesn't return null when empty
144            if (position < 0 || position >= getCount()) {
145                return null;
146            }
147
148            if (mModel.mEventInfos.isEmpty() || mModel.mRowInfos.isEmpty()) {
149                RemoteViews views = new RemoteViews(mContext.getPackageName(),
150                        R.layout.appwidget_no_events);
151                final Intent intent =  CalendarAppWidgetProvider.getLaunchFillInIntent(0);
152                views.setOnClickFillInIntent(R.id.appwidget_no_events, intent);
153                return views;
154            }
155
156            RowInfo rowInfo = mModel.mRowInfos.get(position);
157            if (rowInfo.mType == RowInfo.TYPE_DAY) {
158                RemoteViews views = new RemoteViews(mContext.getPackageName(),
159                        R.layout.appwidget_day);
160                DayInfo dayInfo = mModel.mDayInfos.get(rowInfo.mIndex);
161                updateTextView(views, R.id.date, View.VISIBLE, dayInfo.mDayLabel);
162                return views;
163            } else {
164                final RemoteViews views = new RemoteViews(mContext.getPackageName(),
165                        R.layout.appwidget_row);
166                final EventInfo eventInfo = mModel.mEventInfos.get(rowInfo.mIndex);
167
168                final long now = System.currentTimeMillis();
169                if (!eventInfo.allDay && eventInfo.start <= now && now <= eventInfo.end) {
170                    views.setInt(R.id.appwidget_row, "setBackgroundColor",
171                            mResources.getColor(R.color.appwidget_row_in_progress));
172                } else {
173                    views.setInt(R.id.appwidget_row, "setBackgroundColor",
174                            mResources.getColor(R.color.appwidget_row_default));
175                }
176
177                updateTextView(views, R.id.when, eventInfo.visibWhen, eventInfo.when);
178                updateTextView(views, R.id.where, eventInfo.visibWhere, eventInfo.where);
179                updateTextView(views, R.id.title, eventInfo.visibTitle, eventInfo.title);
180
181                views.setViewVisibility(R.id.color, View.VISIBLE);
182                views.setInt(R.id.color, "setBackgroundColor", eventInfo.color);
183
184                // An element in ListView.
185                final Intent fillInIntent =
186                        CalendarAppWidgetProvider.getLaunchFillInIntent(eventInfo.start);
187                views.setOnClickFillInIntent(R.id.appwidget_row, fillInIntent);
188                return views;
189            }
190        }
191
192        @Override
193        public int getViewTypeCount() {
194            return 4;
195        }
196
197        @Override
198        public int getCount() {
199            // if there are no events, we still return 1 to represent the "no
200            // events" view
201            return Math.max(1, mModel.mRowInfos.size());
202        }
203
204        @Override
205        public long getItemId(int position) {
206            return position;
207        }
208
209        @Override
210        public boolean hasStableIds() {
211            return true;
212        }
213
214        private void loadData() {
215            final long now = System.currentTimeMillis();
216            if (LOGD) Log.d(TAG, "Querying for widget events...");
217            if (mCursor != null) {
218                mCursor.close();
219            }
220
221            final ContentResolver resolver = mContext.getContentResolver();
222            mCursor = getUpcomingInstancesCursor(resolver, SEARCH_DURATION, now);
223            String tz = getTimeZoneFromDB(resolver);
224            mModel = buildAppWidgetModel(mContext, mCursor, tz);
225
226            // Schedule an alarm to wake ourselves up for the next update.  We also cancel
227            // all existing wake-ups because PendingIntents don't match against extras.
228            long triggerTime = calculateUpdateTime(mModel, now);
229
230            // If no next-update calculated, or bad trigger time in past, schedule
231            // update about six hours from now.
232            if (triggerTime < now) {
233                Log.w(TAG, "Encountered bad trigger time " + formatDebugTime(triggerTime, now));
234                triggerTime = now + UPDATE_TIME_NO_EVENTS;
235            }
236
237            final AlarmManager alertManager =
238                    (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
239            final PendingIntent pendingUpdate =
240                    CalendarAppWidgetProvider.getUpdateIntent(mContext);
241
242            alertManager.cancel(pendingUpdate);
243            alertManager.set(AlarmManager.RTC, triggerTime, pendingUpdate);
244            if (triggerTime != sLastUpdateTime) {
245                Log.d(TAG, "Scheduled next update at " + formatDebugTime(triggerTime, now));
246                sLastUpdateTime = triggerTime;
247            }
248        }
249
250        /**
251         * Query across all calendars for upcoming event instances from now until
252         * some time in the future.
253         *
254         * Widen the time range that we query by one day on each end so that we can
255         * catch all-day events. All-day events are stored starting at midnight in
256         * UTC but should be included in the list of events starting at midnight
257         * local time. This may fetch more events than we actually want, so we
258         * filter them out later.
259         *
260         * @param resolver {@link ContentResolver} to use when querying
261         *            {@link Instances#CONTENT_URI}.
262         * @param searchDuration Distance into the future to look for event
263         *            instances, in milliseconds.
264         * @param now Current system time to use for this update, possibly from
265         *            {@link System#currentTimeMillis()}.
266         */
267        private Cursor getUpcomingInstancesCursor(ContentResolver resolver,
268                long searchDuration, long now) {
269            // Search for events from now until some time in the future
270
271            // Add a day on either side to catch all-day events
272            long begin = now - DateUtils.DAY_IN_MILLIS;
273            long end = now + searchDuration + DateUtils.DAY_IN_MILLIS;
274
275            Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI,
276                    String.format("%d/%d", begin, end));
277
278            Cursor cursor = resolver.query(uri, EVENT_PROJECTION,
279                    EVENT_SELECTION, null, EVENT_SORT_ORDER);
280
281            // Start managing the cursor ourselves
282            MatrixCursor matrixCursor = Utils.matrixCursorFromCursor(cursor);
283            cursor.close();
284
285            return matrixCursor;
286        }
287
288        private String getTimeZoneFromDB(ContentResolver resolver) {
289            String tz = null;
290            Cursor tzCursor = null;
291            try {
292                tzCursor = resolver.query(
293                        CalendarCache.URI, CalendarCache.POJECTION, null, null, null);
294                if (tzCursor != null) {
295                    int keyColumn = tzCursor.getColumnIndexOrThrow(CalendarCache.KEY);
296                    int valueColumn = tzCursor.getColumnIndexOrThrow(CalendarCache.VALUE);
297                    while (tzCursor.moveToNext()) {
298                        if (TextUtils.equals(tzCursor.getString(keyColumn),
299                                CalendarCache.TIMEZONE_KEY_INSTANCES)) {
300                            tz = tzCursor.getString(valueColumn);
301                        }
302                    }
303                }
304                if (tz == null) {
305                    tz = Time.getCurrentTimezone();
306                }
307            } finally {
308                if (tzCursor != null) {
309                    tzCursor.close();
310                }
311            }
312            return tz;
313        }
314
315        @VisibleForTesting
316        protected static CalendarAppWidgetModel buildAppWidgetModel(
317                Context context, Cursor cursor, String timeZone) {
318            CalendarAppWidgetModel model = new CalendarAppWidgetModel(context);
319            model.buildFromCursor(cursor, timeZone);
320            return model;
321        }
322
323        /**
324         * Calculates and returns the next time we should push widget updates.
325         */
326        private long calculateUpdateTime(CalendarAppWidgetModel model, long now) {
327            // Make sure an update happens at midnight or earlier
328            long minUpdateTime = getNextMidnightTimeMillis();
329            for (EventInfo event : model.mEventInfos) {
330                final boolean allDay = event.allDay;
331                final long start;
332                final long end;
333                if (allDay) {
334                    // Adjust all-day times into local timezone
335                    final Time recycle = new Time();
336                    start = Utils.convertUtcToLocal(recycle, event.start);
337                    end = Utils.convertUtcToLocal(recycle, event.end);
338                } else {
339                    start = event.start;
340                    end = event.end;
341                }
342
343                // We want to update widget when we enter/exit time range of an event.
344                if (now < start) {
345                    minUpdateTime = Math.min(minUpdateTime, start);
346                } else if (now < end) {
347                    minUpdateTime = Math.min(minUpdateTime, end);
348                }
349            }
350            return minUpdateTime;
351        }
352
353        private static long getNextMidnightTimeMillis() {
354            Time time = new Time();
355            time.setToNow();
356            time.monthDay++;
357            time.hour = 0;
358            time.minute = 0;
359            time.second = 0;
360            long midnight = time.normalize(true);
361            return midnight;
362        }
363
364        static void updateTextView(RemoteViews views, int id, int visibility, String string) {
365            views.setViewVisibility(id, visibility);
366            if (visibility == View.VISIBLE) {
367                views.setTextViewText(id, string);
368            }
369        }
370    }
371
372    /**
373     * Format given time for debugging output.
374     *
375     * @param unixTime Target time to report.
376     * @param now Current system time from {@link System#currentTimeMillis()}
377     *            for calculating time difference.
378     */
379    static String formatDebugTime(long unixTime, long now) {
380        Time time = new Time();
381        time.set(unixTime);
382
383        long delta = unixTime - now;
384        if (delta > DateUtils.MINUTE_IN_MILLIS) {
385            delta /= DateUtils.MINUTE_IN_MILLIS;
386            return String.format("[%d] %s (%+d mins)", unixTime,
387                    time.format("%H:%M:%S"), delta);
388        } else {
389            delta /= DateUtils.SECOND_IN_MILLIS;
390            return String.format("[%d] %s (%+d secs)", unixTime,
391                    time.format("%H:%M:%S"), delta);
392        }
393    }
394}
395