CalendarAppWidgetService.java revision 9a3cb14e28536e4133dddbe952f47189fe344ec1
1bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang/*
2bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * Copyright (C) 2009 The Android Open Source Project
3bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang *
4bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * Licensed under the Apache License, Version 2.0 (the "License");
5bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * you may not use this file except in compliance with the License.
6bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * You may obtain a copy of the License at
7bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang *
8bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang *      http://www.apache.org/licenses/LICENSE-2.0
9bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang *
10bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * Unless required by applicable law or agreed to in writing, software
11bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * distributed under the License is distributed on an "AS IS" BASIS,
12bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * See the License for the specific language governing permissions and
14bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang * limitations under the License.
15bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang */
16bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
179a3cb14e28536e4133dddbe952f47189fe344ec1Mason Tangpackage com.android.calendar.widget;
18bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
19bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport com.google.common.annotations.VisibleForTesting;
20bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
219a3cb14e28536e4133dddbe952f47189fe344ec1Mason Tangimport com.android.calendar.R;
229a3cb14e28536e4133dddbe952f47189fe344ec1Mason Tangimport com.android.calendar.Utils;
239a3cb14e28536e4133dddbe952f47189fe344ec1Mason Tangimport com.android.calendar.widget.CalendarAppWidgetModel.EventInfo;
24bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
25bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
26bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.app.AlarmManager;
27bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.app.IntentService;
28bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.app.PendingIntent;
29bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.appwidget.AppWidgetManager;
30bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.content.ComponentName;
31bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.content.ContentResolver;
32bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.content.Context;
33bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.content.Intent;
34bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.database.Cursor;
35bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.net.Uri;
36bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.provider.Calendar.Attendees;
37bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.provider.Calendar.Calendars;
38bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.provider.Calendar.Instances;
39bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.text.TextUtils;
40bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.text.format.DateFormat;
41bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.text.format.DateUtils;
42bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.text.format.Time;
43bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.util.Log;
44bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.view.View;
45bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport android.widget.RemoteViews;
46bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
47bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport java.util.ArrayList;
48bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport java.util.HashSet;
49bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport java.util.List;
50bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport java.util.Set;
51bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangimport java.util.TimeZone;
52bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
53bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
54bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tangpublic class CalendarAppWidgetService extends IntentService {
55bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private static final String TAG = "CalendarAppWidgetService";
56bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private static final boolean LOGD = false;
57bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
58bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /* TODO query doesn't handle all-day events properly, we should fix this in
59bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * the provider in a manner similar to how it is handled in Event.loadEvents
60bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * in the Calendar application.
61bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
62bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private static final String EVENT_SORT_ORDER = Instances.START_DAY + " ASC, "
63bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            + Instances.START_MINUTE + " ASC, " + Instances.END_DAY + " ASC, "
64bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            + Instances.END_MINUTE + " ASC LIMIT 10";
65bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
66bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    // TODO can't use parameter here because provider is dropping them
67bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private static final String EVENT_SELECTION = Calendars.SELECTED + "=1 AND "
68bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            + Instances.SELF_ATTENDEE_STATUS + "!=" + Attendees.ATTENDEE_STATUS_DECLINED;
69bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
70bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final String[] EVENT_PROJECTION = new String[] {
71bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Instances.ALL_DAY,
72bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Instances.BEGIN,
73bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Instances.END,
74bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Instances.TITLE,
75bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Instances.EVENT_LOCATION,
76bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Instances.EVENT_ID,
77bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    };
78bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
79bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final int INDEX_ALL_DAY = 0;
80bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final int INDEX_BEGIN = 1;
81bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final int INDEX_END = 2;
82bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final int INDEX_TITLE = 3;
83bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final int INDEX_EVENT_LOCATION = 4;
84bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static final int INDEX_EVENT_ID = 5;
85bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
86bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private static final long SEARCH_DURATION = DateUtils.WEEK_IN_MILLIS;
87bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
88bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    // If no next-update calculated, or bad trigger time in past, schedule
89bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    // update about six hours from now.
90bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private static final long UPDATE_NO_EVENTS = DateUtils.HOUR_IN_MILLIS * 6;
91bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
92bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    public CalendarAppWidgetService() {
93bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        super(TAG);
94bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
95bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
96bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    @Override
97bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    protected void onHandleIntent(Intent intent) {
98bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // These will be null if the extra data doesn't exist
99bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        int[] widgetIds = intent.getIntArrayExtra(CalendarAppWidgetProvider.EXTRA_WIDGET_IDS);
100bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long[] eventIds = null;
101bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        HashSet<Long> eventIdsSet = null;
102bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (intent.hasExtra(CalendarAppWidgetProvider.EXTRA_EVENT_IDS)) {
103bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            eventIds = intent.getExtras().getLongArray(CalendarAppWidgetProvider.EXTRA_EVENT_IDS);
104bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            eventIdsSet = new HashSet<Long>(eventIds.length);
105bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            for (int i = 0; i < eventIds.length; i++) {
106bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                eventIdsSet.add(eventIds[i]);
107bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
108bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
109bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long now = System.currentTimeMillis();
110bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
111bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        performUpdate(this, widgetIds, eventIdsSet, now);
112bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
113bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
114bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /**
115bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Process and push out an update for the given appWidgetIds.
116bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *
117bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param context Context to use when updating widget.
118bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param appWidgetIds List of appWidgetIds to update, or null for all.
119bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param changedEventIds Specific events known to be changed, otherwise
120bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *            null. If present, we use to decide if an update is necessary.
121bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param now System clock time to use during this update.
122bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
123bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private void performUpdate(Context context, int[] appWidgetIds,
124bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            Set<Long> changedEventIds, long now) {
125bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        ContentResolver resolver = context.getContentResolver();
126bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
127bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Cursor cursor = null;
128bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        RemoteViews views = null;
129bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long triggerTime = -1;
130bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
131bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        try {
132bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            cursor = getUpcomingInstancesCursor(resolver, SEARCH_DURATION, now);
133bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (cursor != null) {
134bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                MarkedEvents events = buildMarkedEvents(cursor, changedEventIds, now);
135bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
136bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                boolean shouldUpdate = true;
137bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                if (changedEventIds != null && changedEventIds.size() > 0) {
138bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    shouldUpdate = events.watchFound;
139bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                }
140bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
141bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                if (events.markedIds.isEmpty()) {
142bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    views = getAppWidgetNoEvents(context);
143bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                } else if (shouldUpdate) {
144bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    views = getAppWidgetUpdate(context, cursor, events);
145bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    triggerTime = calculateUpdateTime(cursor, events);
146bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                }
147bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            } else {
148bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                views = getAppWidgetNoEvents(context);
149bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
150bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        } finally {
151bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (cursor != null) {
152bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                cursor.close();
153bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
154bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
155bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
156bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // Bail out early if no update built
157bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (views == null) {
158bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (LOGD) Log.d(TAG, "Didn't build update, possibly because changedEventIds=" +
159bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    changedEventIds.toString());
160bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            return;
161bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
162bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
163bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        AppWidgetManager gm = AppWidgetManager.getInstance(context);
164bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (appWidgetIds != null && appWidgetIds.length > 0) {
165bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            gm.updateAppWidget(appWidgetIds, views);
166bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        } else {
167bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            ComponentName thisWidget = CalendarAppWidgetProvider.getComponentName(context);
168bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            gm.updateAppWidget(thisWidget, views);
169bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
170bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
171bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // Schedule an alarm to wake ourselves up for the next update.  We also cancel
172bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // all existing wake-ups because PendingIntents don't match against extras.
173bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
174bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // If no next-update calculated, or bad trigger time in past, schedule
175bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // update about six hours from now.
176bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (triggerTime == -1 || triggerTime < now) {
177bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (LOGD) Log.w(TAG, "Encountered bad trigger time " +
178bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    formatDebugTime(triggerTime, now));
179bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            triggerTime = now + UPDATE_NO_EVENTS;
180bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
181bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
182bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
183bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        PendingIntent pendingUpdate = CalendarAppWidgetProvider.getUpdateIntent(context);
184bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
185bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        am.cancel(pendingUpdate);
186bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        am.set(AlarmManager.RTC, triggerTime, pendingUpdate);
187bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
188bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (LOGD) Log.d(TAG, "Scheduled next update at " + formatDebugTime(triggerTime, now));
189bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
190bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
191bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /**
192bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Format given time for debugging output.
193bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *
194bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param unixTime Target time to report.
195bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param now Current system time from {@link System#currentTimeMillis()}
196bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *            for calculating time difference.
197bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
198bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static private String formatDebugTime(long unixTime, long now) {
199bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Time time = new Time();
200bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.set(unixTime);
201bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
202bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long delta = unixTime - now;
203bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (delta > DateUtils.MINUTE_IN_MILLIS) {
204bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            delta /= DateUtils.MINUTE_IN_MILLIS;
205bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            return String.format("[%d] %s (%+d mins)", unixTime, time.format("%H:%M:%S"), delta);
206bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        } else {
207bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            delta /= DateUtils.SECOND_IN_MILLIS;
208bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            return String.format("[%d] %s (%+d secs)", unixTime, time.format("%H:%M:%S"), delta);
209bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
210bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
211bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
212bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /**
213bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Convert given UTC time into current local time.
214bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *
215bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param recycle Time object to recycle, otherwise null.
216bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param utcTime Time to convert, in UTC.
217bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
218bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static private long convertUtcToLocal(Time recycle, long utcTime) {
219bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (recycle == null) {
220bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            recycle = new Time();
221bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
222bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        recycle.timezone = Time.TIMEZONE_UTC;
223bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        recycle.set(utcTime);
224bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        recycle.timezone = TimeZone.getDefault().getID();
225bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        return recycle.normalize(true);
226bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
227bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
228bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /**
229bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Figure out the next time we should push widget updates, usually the time
230bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * calculated by {@link #getEventFlip(Cursor, long, long, boolean)}.
231bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *
232bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param cursor Valid cursor on {@link Instances#CONTENT_URI}
233bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param events {@link MarkedEvents} parsed from the cursor
234bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
235bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private long calculateUpdateTime(Cursor cursor, MarkedEvents events) {
236bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long result = -1;
237bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (!events.markedIds.isEmpty()) {
238bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            cursor.moveToPosition(events.markedIds.get(0));
239bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            long start = cursor.getLong(INDEX_BEGIN);
240bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            long end = cursor.getLong(INDEX_END);
241bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
242bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
243bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            // Adjust all-day times into local timezone
244bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (allDay) {
245bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                final Time recycle = new Time();
246bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                start = convertUtcToLocal(recycle, start);
247bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                end = convertUtcToLocal(recycle, end);
248bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
249bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
250bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            result = getEventFlip(cursor, start, end, allDay);
251bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
252bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            // Make sure an update happens at midnight or earlier
253bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            long midnight = getNextMidnightTimeMillis();
254bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            result = Math.min(midnight, result);
255bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
256bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        return result;
257bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
258bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
259bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private long getNextMidnightTimeMillis() {
260bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Time time = new Time();
261bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.setToNow();
262bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.monthDay++;
263bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.hour = 0;
264bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.minute = 0;
265bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.second = 0;
266bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long midnight = time.normalize(true);
267bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        return midnight;
268bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
269bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
270bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /**
271bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Calculate flipping point for the given event; when we should hide this
272bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * event and show the next one. This is defined as the end time of the
273bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * event.
274bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *
275bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param start Event start time in local timezone.
276bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param end Event end time in local timezone.
277bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
278bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static private long getEventFlip(Cursor cursor, long start, long end, boolean allDay) {
279bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        return end;
280bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
281bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
282bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /**
283bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Set visibility of various widget components if there are events, or if no
284bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * events were found.
285bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *
286bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param views Set of {@link RemoteViews} to apply visibility.
287bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param noEvents True if no events found, otherwise false.
288bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
289bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private void setNoEventsVisible(RemoteViews views, boolean noEvents) {
290bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.setViewVisibility(R.id.no_events, noEvents ? View.VISIBLE : View.GONE);
291bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.setViewVisibility(R.id.page_flipper, View.GONE);
292bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.setViewVisibility(R.id.single_page, View.GONE);
293bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
294bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
295bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /**
296bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Build a set of {@link RemoteViews} that describes how to update any
297bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * widget for a specific event instance.
298bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *
299bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param cursor Valid cursor on {@link Instances#CONTENT_URI}
300bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param events {@link MarkedEvents} parsed from the cursor
301bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
302bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private RemoteViews getAppWidgetUpdate(Context context, Cursor cursor, MarkedEvents events) {
303bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget);
304bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        setNoEventsVisible(views, false);
305bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
306bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long currentTime = System.currentTimeMillis();
307bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        CalendarAppWidgetModel model = buildAppWidgetModel(context, cursor, events, currentTime);
308bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
309bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        applyModelToView(context, model, views);
310bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
311bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // Clicking on the widget launches Calendar
312bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long startTime = Math.max(currentTime, events.firstTime);
313bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
314bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        PendingIntent pendingIntent = getLaunchPendingIntent(context, startTime);
315bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.setOnClickPendingIntent(R.id.appwidget, pendingIntent);
316bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
317e700c16ec2464cbba86f91f8f757ae59cbed34b0Mason Tang        PendingIntent newEventIntent = getNewEventPendingIntent(context);
318e700c16ec2464cbba86f91f8f757ae59cbed34b0Mason Tang        views.setOnClickPendingIntent(R.id.new_event_button, newEventIntent);
319e700c16ec2464cbba86f91f8f757ae59cbed34b0Mason Tang
320bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        return views;
321bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
322bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
323bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private void applyModelToView(Context context, CalendarAppWidgetModel model,
324bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            RemoteViews views) {
325bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.setTextViewText(R.id.day_of_week, model.dayOfWeek);
326bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.setTextViewText(R.id.day_of_month, model.dayOfMonth);
327bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.setViewVisibility(R.id.no_events, model.visibNoEvents);
328bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
329bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // Make sure we have a clean slate first
330bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.removeAllViews(R.id.page_flipper);
331bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.removeAllViews(R.id.single_page);
332bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
333bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // If we don't have any events, just hide the relevant views and return
334bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (model.visibNoEvents != View.GONE) {
335bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            views.setViewVisibility(R.id.page_flipper, View.GONE);
336bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            views.setViewVisibility(R.id.single_page, View.GONE);
337bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            return;
338bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
339bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
340bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // Luckily, length of this array is guaranteed to be even
341bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        int pages = model.eventInfos.length / 2;
342bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
343bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // We use a separate container for the case of only one page to prevent
344bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // a ViewFlipper from repeatedly animating one view
345bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (pages > 1) {
346bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            views.setViewVisibility(R.id.page_flipper, View.VISIBLE);
347bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            views.setViewVisibility(R.id.single_page, View.GONE);
348bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        } else {
349bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            views.setViewVisibility(R.id.single_page, View.VISIBLE);
350bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            views.setViewVisibility(R.id.page_flipper, View.GONE);
351bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
352bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
353bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // Iterate two at a time through the events and populate the views
354bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        for (int i = 0; i < model.eventInfos.length; i += 2) {
355bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            RemoteViews pageViews = new RemoteViews(context.getPackageName(),
356bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    R.layout.appwidget_page);
357bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            EventInfo e1 = model.eventInfos[i];
358bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            EventInfo e2 = model.eventInfos[i + 1];
359bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
360bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            updateTextView(pageViews, R.id.when1, e1.visibWhen, e1.when);
361bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            updateTextView(pageViews, R.id.where1, e1.visibWhere, e1.where);
362bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            updateTextView(pageViews, R.id.title1, e1.visibTitle, e1.title);
363bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            updateTextView(pageViews, R.id.when2, e2.visibWhen, e2.when);
364bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            updateTextView(pageViews, R.id.where2, e2.visibWhere, e2.where);
365bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            updateTextView(pageViews, R.id.title2, e2.visibTitle, e2.title);
366bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
367bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (pages > 1) {
368bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                views.addView(R.id.page_flipper, pageViews);
369bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                updateTextView(pageViews, R.id.page_count, View.VISIBLE,
370bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                        makePageCount((i / 2) + 1, pages));
371bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            } else {
372bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                views.addView(R.id.single_page, pageViews);
373bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
374bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
375bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
376bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
377bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
378bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static String makePageCount(int current, int total) {
379bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        return Integer.toString(current) + " / " + Integer.toString(total);
380bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
381bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
382bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static void updateTextView(RemoteViews views, int id, int visibility, String string) {
383bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.setViewVisibility(id, visibility);
384bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (visibility == View.VISIBLE) {
385bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            views.setTextViewText(id, string);
386bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
387bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
388bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
389bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static CalendarAppWidgetModel buildAppWidgetModel(Context context, Cursor cursor,
390bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            MarkedEvents events, long currentTime) {
391bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        int eventCount = events.markedIds.size();
392bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        CalendarAppWidgetModel model = new CalendarAppWidgetModel(eventCount);
393bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Time time = new Time();
394bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.set(currentTime);
395bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.monthDay++;
396bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.hour = 0;
397bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.minute = 0;
398bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.second = 0;
399bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long startOfNextDay = time.normalize(true);
400bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
401bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.set(currentTime);
402bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
403bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // Calendar header
404bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        String dayOfWeek = DateUtils.getDayOfWeekString(time.weekDay + 1, DateUtils.LENGTH_MEDIUM)
405bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                .toUpperCase();
406bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
407bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        model.dayOfWeek = dayOfWeek;
408bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        model.dayOfMonth = Integer.toString(time.monthDay);
409bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
410bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        int i = 0;
411bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        for (Integer id : events.markedIds) {
412bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            populateEvent(context, cursor, id, model, time, i, true, startOfNextDay, currentTime);
413bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            i++;
414bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
415bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
416bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        return model;
417bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
418bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
419bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /**
420bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Pulls the information for a single event from the cursor and populates
421bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * the corresponding model object with the data.
422bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *
423bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param context a Context to use for accessing resources
424bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param cursor the cursor to retrieve the data from
425bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param rowId the ID of the row to retrieve
426bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param model the model object to populate
427bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param recycle a Time instance to recycle
428bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param eventIndex which event index in the model to populate
429bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param showTitleLocation whether or not to show the title and location
430bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param startOfNextDay the beginning of the next day
431bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param currentTime the current time
432bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
433bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static private void populateEvent(Context context, Cursor cursor, int rowId,
434bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            CalendarAppWidgetModel model, Time recycle, int eventIndex,
435bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            boolean showTitleLocation, long startOfNextDay, long currentTime) {
436bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        cursor.moveToPosition(rowId);
437bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
438bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // When
439bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
440bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long start = cursor.getLong(INDEX_BEGIN);
441bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long end = cursor.getLong(INDEX_END);
442bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (allDay) {
443bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            start = convertUtcToLocal(recycle, start);
444bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            end = convertUtcToLocal(recycle, end);
445bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
446bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
447bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        boolean eventIsInProgress = start <= currentTime && end > currentTime;
448bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        boolean eventIsToday = start < startOfNextDay;
449bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        boolean eventIsTomorrow = !eventIsToday && !eventIsInProgress
450bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                && (start < (startOfNextDay + DateUtils.DAY_IN_MILLIS));
451bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
452bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // Compute a human-readable string for the start time of the event
453bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        String whenString;
454bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (eventIsInProgress && allDay) {
455bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            // All day events for the current day display as just "Today"
456bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            whenString = context.getString(R.string.today);
457bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        } else if (eventIsTomorrow && allDay) {
458bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            // All day events for the next day display as just "Tomorrow"
459bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            whenString = context.getString(R.string.tomorrow);
460bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        } else {
461bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            int flags = DateUtils.FORMAT_ABBREV_ALL;
462bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (allDay) {
463bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                flags |= DateUtils.FORMAT_UTC;
464bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            } else {
465bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                flags |= DateUtils.FORMAT_SHOW_TIME;
466bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                if (DateFormat.is24HourFormat(context)) {
467bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    flags |= DateUtils.FORMAT_24HOUR;
468bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                }
469bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
470bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            // Show day of the week if not today or tomorrow
471bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (!eventIsTomorrow && !eventIsToday) {
472bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                flags |= DateUtils.FORMAT_SHOW_WEEKDAY;
473bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
474bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            whenString = DateUtils.formatDateRange(context, start, start, flags);
475bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (eventIsTomorrow) {
476bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                whenString += (", ");
477bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                whenString += context.getString(R.string.tomorrow);
478bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            } else if (eventIsInProgress) {
479bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                whenString += " (";
480bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                whenString += context.getString(R.string.in_progress);
481bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                whenString += ")";
482bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
483bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
484bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
485bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        model.eventInfos[eventIndex].when = whenString;
486bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        model.eventInfos[eventIndex].visibWhen = View.VISIBLE;
487bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
488bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (showTitleLocation) {
489bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            // What
490bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            String titleString = cursor.getString(INDEX_TITLE);
491bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (TextUtils.isEmpty(titleString)) {
492bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                titleString = context.getString(R.string.no_title_label);
493bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
494bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            model.eventInfos[eventIndex].title = titleString;
495bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            model.eventInfos[eventIndex].visibTitle = View.VISIBLE;
496bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
497bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            // Where
498bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            String whereString = cursor.getString(INDEX_EVENT_LOCATION);
499bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (!TextUtils.isEmpty(whereString)) {
500bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                model.eventInfos[eventIndex].visibWhere = View.VISIBLE;
501bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                model.eventInfos[eventIndex].where = whereString;
502bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            } else {
503bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                model.eventInfos[eventIndex].visibWhere = View.GONE;
504bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
505bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (LOGD) Log.d(TAG, " Title:" + titleString + " Where:" + whereString);
506bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
507bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
508bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
509bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /**
510bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Build a set of {@link RemoteViews} that describes an error state.
511bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
512bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private RemoteViews getAppWidgetNoEvents(Context context) {
513bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget);
514bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        setNoEventsVisible(views, true);
515bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
516bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // Calendar header
517bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Time time = new Time();
518bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        time.setToNow();
519bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        String dayOfWeek = DateUtils.getDayOfWeekString(time.weekDay + 1, DateUtils.LENGTH_MEDIUM)
520bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                .toUpperCase();
521bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.setTextViewText(R.id.day_of_week, dayOfWeek);
522bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.setTextViewText(R.id.day_of_month, Integer.toString(time.monthDay));
523bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
524bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // Clicking on widget launches the agenda view in Calendar
525bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        PendingIntent pendingIntent = getLaunchPendingIntent(context, 0);
526bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        views.setOnClickPendingIntent(R.id.appwidget, pendingIntent);
527bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
528bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        return views;
529bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
530bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
531bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /**
532bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Build a {@link PendingIntent} to launch the Calendar app. This correctly
533bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * sets action, category, and flags so that we don't duplicate tasks when
534bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Calendar was also launched from a normal desktop icon.
535bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param goToTime time that calendar should take the user to
536bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
537bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private PendingIntent getLaunchPendingIntent(Context context, long goToTime) {
538bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Intent launchIntent = new Intent();
539bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        String dataString = "content://com.android.calendar/time";
540bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        launchIntent.setAction(Intent.ACTION_VIEW);
541bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
542bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED |
543bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                Intent.FLAG_ACTIVITY_CLEAR_TOP);
544bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        if (goToTime != 0) {
545d6734dbbd704cdb1bc331d1bd74b7a3be58f69ffMichael Chan            launchIntent.putExtra(Utils.INTENT_KEY_DETAIL_VIEW, true);
546bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            dataString += "/" + goToTime;
547bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
548bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Uri data = Uri.parse(dataString);
549bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        launchIntent.setData(data);
550bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        return PendingIntent.getActivity(context, 0 /* no requestCode */,
551bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                launchIntent, PendingIntent.FLAG_UPDATE_CURRENT);
552bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
553bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
554e700c16ec2464cbba86f91f8f757ae59cbed34b0Mason Tang    private PendingIntent getNewEventPendingIntent(Context context) {
555e700c16ec2464cbba86f91f8f757ae59cbed34b0Mason Tang        Intent newEventIntent = new Intent(Intent.ACTION_EDIT);
556e700c16ec2464cbba86f91f8f757ae59cbed34b0Mason Tang        newEventIntent.setType("vnd.android.cursor.item/event");
557e700c16ec2464cbba86f91f8f757ae59cbed34b0Mason Tang        return PendingIntent.getActivity(context, 0, newEventIntent,
558e700c16ec2464cbba86f91f8f757ae59cbed34b0Mason Tang                PendingIntent.FLAG_UPDATE_CURRENT);
559e700c16ec2464cbba86f91f8f757ae59cbed34b0Mason Tang    }
560e700c16ec2464cbba86f91f8f757ae59cbed34b0Mason Tang
561bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static class MarkedEvents {
562bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
563bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        /**
564bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang         * The row IDs of all events marked for display
565bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang         */
566bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        List<Integer> markedIds = new ArrayList<Integer>(10);
567bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
568bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        /**
569bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang         * The start time of the first marked event
570bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang         */
571bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long firstTime = -1;
572bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
573bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        /** The number of events currently in progress */
574bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        int inProgressCount = 0; // Number of events with same start time as the primary evt.
575bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
576bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        /** The start time of the next upcoming event */
577bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long primaryTime = -1;
578bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
579bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        /**
580bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang         * The number of events that share the same start time as the next
581bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang         * upcoming event
582bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang         */
583bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        int primaryCount = 0; // Number of events with same start time as the secondary evt.
584bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
585bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        /** The start time of the next next upcoming event */
586bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long secondaryTime = 1;
587bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
588bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        /**
589bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang         * The number of events that share the same start time as the next next
590bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang         * upcoming event.
591bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang         */
592bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        int secondaryCount = 0;
593bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
594bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        boolean watchFound = false;
595bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
596bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
597bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /**
598bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Walk the given instances cursor and build a list of marked events to be
599bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * used when updating the widget. This structure is also used to check if
600bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * updates are needed.
601bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *
602bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param cursor Valid cursor across {@link Instances#CONTENT_URI}.
603bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param watchEventIds Specific events to watch for, setting
604bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *            {@link MarkedEvents#watchFound} if found during marking.
605bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param now Current system time to use for this update, possibly from
606bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *            {@link System#currentTimeMillis()}
607bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
608bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    @VisibleForTesting
609bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    static MarkedEvents buildMarkedEvents(Cursor cursor, Set<Long> watchEventIds, long now) {
610bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        MarkedEvents events = new MarkedEvents();
611bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        final Time recycle = new Time();
612bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
613bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        cursor.moveToPosition(-1);
614bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        while (cursor.moveToNext()) {
615bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            int row = cursor.getPosition();
616bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            long eventId = cursor.getLong(INDEX_EVENT_ID);
617bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            long start = cursor.getLong(INDEX_BEGIN);
618bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            long end = cursor.getLong(INDEX_END);
619bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
620bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0;
621bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
622bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (LOGD) {
623bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                Log.d(TAG, "Row #" + row + " allDay:" + allDay + " start:" + start + " end:" + end
624bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                        + " eventId:" + eventId);
625bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
626bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
627bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            // Adjust all-day times into local timezone
628bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (allDay) {
629bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                start = convertUtcToLocal(recycle, start);
630bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                end = convertUtcToLocal(recycle, end);
631bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
632bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
633bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            boolean inProgress = now < end && now > start;
634bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
635bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            // Skip events that have already passed their flip times
636bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            long eventFlip = getEventFlip(cursor, start, end, allDay);
637bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (LOGD) Log.d(TAG, "Calculated flip time " + formatDebugTime(eventFlip, now));
638bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (eventFlip < now) {
639bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                continue;
640bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
641bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
642bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            // Mark if we've encountered the watched event
643bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (watchEventIds != null && watchEventIds.contains(eventId)) {
644bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                events.watchFound = true;
645bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
646bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
647bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            /* Scan through the events with the following logic:
648bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang             *   Rule #1 Show A) all the events that are in progress including
649bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang             *     all day events and B) the next upcoming event and any events
650bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang             *     with the same start time.
651bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang             *
652bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang             *   Rule #2 If there are no events in progress, show A) the next
653bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang             *     upcoming event and B) any events with the same start time.
654bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang             *
655bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang             *   Rule #3 If no events start at the same time at A in rule 2,
656bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang             *     show A) the next upcoming event and B) the following upcoming
657bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang             *     event + any events with the same start time.
658bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang             */
659bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            if (inProgress) {
660bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                // events for part A of Rule #1
661bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                events.markedIds.add(row);
662bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                events.inProgressCount++;
663bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                if (events.firstTime == -1) {
664bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    events.firstTime = start;
665bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                }
666bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            } else {
667bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                if (events.primaryCount == 0) {
668bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    // first upcoming event
669bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    events.markedIds.add(row);
670bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    events.primaryTime = start;
671bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    events.primaryCount++;
672bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    if (events.firstTime == -1) {
673bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                        events.firstTime = start;
674bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    }
675bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                } else if (events.primaryTime == start) {
676bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    // any events with same start time as first upcoming event
677bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    events.markedIds.add(row);
678bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    events.primaryCount++;
679bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                } else if (events.markedIds.size() == 1) {
680bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    // only one upcoming event, so we take the next upcoming
681bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    events.markedIds.add(row);
682bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    events.secondaryTime = start;
683bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    events.secondaryCount++;
684bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                } else if (events.secondaryCount > 0
685bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                        && events.secondaryTime == start) {
686bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    // any events with same start time as next upcoming
687bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    events.markedIds.add(row);
688bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    events.secondaryCount++;
689bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                } else {
690bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    // looks like we're done
691bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                    break;
692bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                }
693bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            }
694bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        }
695bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        return events;
696bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
697bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
698bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    /**
699bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * Query across all calendars for upcoming event instances from now until
700bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * some time in the future.
701bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *
702bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param resolver {@link ContentResolver} to use when querying
703bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *            {@link Instances#CONTENT_URI}.
704bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param searchDuration Distance into the future to look for event
705bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *            instances, in milliseconds.
706bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     * @param now Current system time to use for this update, possibly from
707bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     *            {@link System#currentTimeMillis()}.
708bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang     */
709bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    private Cursor getUpcomingInstancesCursor(ContentResolver resolver,
710bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang            long searchDuration, long now) {
711bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        // Search for events from now until some time in the future
712bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        long end = now + searchDuration;
713bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
714bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI,
715bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                String.format("%d/%d", now, end));
716bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang
717bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang        return resolver.query(uri, EVENT_PROJECTION, EVENT_SELECTION, null,
718bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang                EVENT_SORT_ORDER);
719bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang    }
720bb3f08abac01e3083d837e5e823b311e3c039e90Mason Tang}
721