diff options
-rw-r--r-- | AndroidManifest.xml | 2 | ||||
-rw-r--r-- | res/drawable-mdpi/appwidget_btn_round_plus.png | bin | 0 -> 644 bytes | |||
-rw-r--r-- | res/layout-land/appwidget_disabled.xml | 117 | ||||
-rw-r--r-- | res/layout/appwidget.xml | 37 | ||||
-rw-r--r-- | res/layout/appwidget_loading.xml | 38 | ||||
-rw-r--r-- | res/layout/appwidget_no_events.xml | 38 | ||||
-rw-r--r-- | res/layout/appwidget_row.xml (renamed from res/layout/appwidget_page.xml) | 35 | ||||
-rw-r--r-- | src/com/android/calendar/widget/CalendarAppWidgetModel.java | 9 | ||||
-rw-r--r-- | src/com/android/calendar/widget/CalendarAppWidgetProvider.java | 88 | ||||
-rw-r--r-- | src/com/android/calendar/widget/CalendarAppWidgetReceiver.java | 18 | ||||
-rw-r--r-- | src/com/android/calendar/widget/CalendarAppWidgetService.java | 1070 | ||||
-rw-r--r-- | tests/src/com/android/calendar/widget/CalendarAppWidgetServiceTest.java | 61 |
12 files changed, 713 insertions, 800 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index d32e4097..9e69cdf2 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -155,7 +155,7 @@ <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget_info" /> </receiver> - <service android:name=".widget.CalendarAppWidgetService" /> + <service android:name=".widget.CalendarAppWidgetService" android:exported="true"/> <activity android:name="CalendarTests" android:label="Calendar Tests"> <intent-filter> diff --git a/res/drawable-mdpi/appwidget_btn_round_plus.png b/res/drawable-mdpi/appwidget_btn_round_plus.png Binary files differnew file mode 100644 index 00000000..27926024 --- /dev/null +++ b/res/drawable-mdpi/appwidget_btn_round_plus.png diff --git a/res/layout-land/appwidget_disabled.xml b/res/layout-land/appwidget_disabled.xml deleted file mode 100644 index 418ae11c..00000000 --- a/res/layout-land/appwidget_disabled.xml +++ /dev/null @@ -1,117 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2009 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/appwidget" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - android:background="@drawable/appwidget_background" - android:focusable="true" - android:clickable="true"> - - <!-- Header --> - <LinearLayout - android:id="@+id/header" - android:layout_width="match_parent" - android:layout_height="40dip" - android:orientation="horizontal" - android:background="@drawable/appwidget_calendar_bgtop_blue"> - - <TextView - android:id="@+id/day_of_week" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:layout_weight="1" - android:layout_marginLeft="7dip" - android:layout_marginRight="7dip" - android:layout_marginBottom="5dip" - android:textColor="@color/appwidget_date" - android:textSize="18sp" - android:gravity="left|bottom" - android:singleLine="true" /> - - <TextView - android:id="@+id/day_of_month" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:layout_marginLeft="7dip" - android:layout_marginRight="7dip" - android:layout_marginBottom="5dip" - android:gravity="right|bottom" - android:textColor="@color/appwidget_date" - android:textSize="20sp" - android:textStyle="bold" - android:singleLine="true" /> - </LinearLayout> - - <!-- No Event --> - <TextView - android:id="@+id/no_events" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_marginBottom="10dip" - android:padding="7dip" - android:gravity="center" - android:textSize="14sp" - android:textStyle="bold" - android:textColor="@color/appwidget_no_events" - android:text="@string/gadget_no_events" /> - - <!-- Event #1 --> - <TextView - android:id="@+id/when1" - style="@style/TextAppearance.WidgetWhen" /> - - <TextView - android:id="@+id/where1" - style="@style/TextAppearance.WidgetWhere" /> - - <TextView - android:id="@+id/title1" - android:layout_marginBottom="-3dip" - style="@style/TextAppearance.WidgetTitle" /> - - <!-- Conflict banner --> - <TextView - android:id="@+id/conflict_landscape" - style="@style/TextAppearance.WidgetConflict" /> - - <!-- These fields are not visible in landscape mode but required to avoid exceptions --> - <TextView - android:id="@+id/when2" - android:visibility="gone" - android:layout_width="0dp" - android:layout_height="0dp" /> - <TextView - android:id="@+id/where2" - android:visibility="gone" - android:layout_width="0dp" - android:layout_height="0dp" /> - <TextView - android:id="@+id/title2" - android:visibility="gone" - android:layout_width="0dp" - android:layout_height="0dp" /> - - <TextView - android:id="@+id/conflict_portrait" - android:visibility="gone" - android:layout_width="0dp" - android:layout_height="0dp" /> - -</LinearLayout> diff --git a/res/layout/appwidget.xml b/res/layout/appwidget.xml index 9432dfaf..7696ae1a 100644 --- a/res/layout/appwidget.xml +++ b/res/layout/appwidget.xml @@ -73,38 +73,13 @@ android:background="#0000" /> </LinearLayout> - <!-- Container to show only a single page --> - <FrameLayout - android:id="@+id/single_page" - android:layout_width="match_parent" - android:layout_height="match_parent"> - </FrameLayout> - - <!-- Flipper for event pages --> - <ViewFlipper - android:id="@+id/page_flipper" - android:autoStart="true" - android:flipInterval="@integer/flip_interval" + <!-- Events container --> + <ListView + android:id="@+id/events_list" android:layout_width="match_parent" android:layout_height="match_parent" - android:inAnimation="@anim/slide_in_fade" - android:outAnimation="@anim/slide_out_fade" - android:animateFirstView="false"> - </ViewFlipper> - - <!-- No Event --> - <TextView - android:id="@+id/no_events" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_marginBottom="10dip" - android:padding="7dip" - android:gravity="center" - android:textSize="14sp" - android:textStyle="bold" - android:textColor="@color/appwidget_no_events" - android:text="@string/gadget_no_events" /> - - + android:cacheColorHint="#00000000" + android:dividerHeight="3dip" + android:divider="#0000" /> </LinearLayout> diff --git a/res/layout/appwidget_loading.xml b/res/layout/appwidget_loading.xml new file mode 100644 index 00000000..ead5a540 --- /dev/null +++ b/res/layout/appwidget_loading.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/appwidget_loading" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:addStatesFromChildren="true"> + + <!-- Loading --> + <TextView + android:id="@+id/loading" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="10dip" + android:padding="7dip" + android:gravity="center" + android:textSize="14sp" + android:textStyle="bold" + android:textColor="@color/appwidget_no_events" + android:text="@string/loading" /> + +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/appwidget_no_events.xml b/res/layout/appwidget_no_events.xml new file mode 100644 index 00000000..e7303a64 --- /dev/null +++ b/res/layout/appwidget_no_events.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/appwidget_no_events" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:addStatesFromChildren="true"> + + <!-- No events --> + <TextView + android:id="@+id/no_events" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="10dip" + android:padding="7dip" + android:gravity="center" + android:textSize="14sp" + android:textStyle="bold" + android:textColor="@color/appwidget_no_events" + android:text="@string/gadget_no_events" /> + +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/appwidget_page.xml b/res/layout/appwidget_row.xml index 67dbd5b7..95af38ba 100644 --- a/res/layout/appwidget_page.xml +++ b/res/layout/appwidget_row.xml @@ -16,46 +16,23 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/appwidget_page" + android:id="@+id/appwidget_row" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - android:addStatesFromChildren="true"> + android:addStatesFromChildren="true" + android:focusableInTouchMode="true"> - <!-- Event #1 --> <TextView - android:id="@+id/when1" - style="@style/TextAppearance.WidgetWhen" /> - - <TextView - android:id="@+id/where1" - style="@style/TextAppearance.WidgetWhere" /> - - <TextView - android:id="@+id/title1" - android:layout_marginBottom="6dip" + android:id="@+id/title" style="@style/TextAppearance.WidgetTitle" /> - <!-- Event #2 --> <TextView - android:id="@+id/when2" + android:id="@+id/when" style="@style/TextAppearance.WidgetWhen" /> <TextView - android:id="@+id/where2" + android:id="@+id/where" style="@style/TextAppearance.WidgetWhere" /> - <TextView - android:id="@+id/title2" - style="@style/TextAppearance.WidgetTitle" /> - - <!-- Page count --> - <TextView - android:id="@+id/page_count" - android:gravity="right|bottom" - android:layout_gravity="bottom" - android:layout_weight="1" - style="@style/TextAppearance.WidgetPageCount"> - </TextView> - </LinearLayout>
\ No newline at end of file diff --git a/src/com/android/calendar/widget/CalendarAppWidgetModel.java b/src/com/android/calendar/widget/CalendarAppWidgetModel.java index 5b6bedde..a8b6dabc 100644 --- a/src/com/android/calendar/widget/CalendarAppWidgetModel.java +++ b/src/com/android/calendar/widget/CalendarAppWidgetModel.java @@ -32,13 +32,12 @@ class CalendarAppWidgetModel { EventInfo[] eventInfos; public CalendarAppWidgetModel() { - this(2); + this(1); } public CalendarAppWidgetModel(int size) { - // we round up to the nearest even integer - eventInfos = new EventInfo[2 * ((size + 1) / 2)]; - for (int i = 0; i < eventInfos.length; i++) { + eventInfos = new EventInfo[size]; + for (int i = 0; i < size; i++) { eventInfos[i] = new EventInfo(); } visibNoEvents = View.GONE; @@ -52,6 +51,8 @@ class CalendarAppWidgetModel { int visibTitle; // Visibility value for Title textview (View.GONE or View.VISIBLE) String title; + long start; + public EventInfo() { visibWhen = View.GONE; visibWhere = View.GONE; diff --git a/src/com/android/calendar/widget/CalendarAppWidgetProvider.java b/src/com/android/calendar/widget/CalendarAppWidgetProvider.java index 048d627c..9530355b 100644 --- a/src/com/android/calendar/widget/CalendarAppWidgetProvider.java +++ b/src/com/android/calendar/widget/CalendarAppWidgetProvider.java @@ -16,6 +16,9 @@ package com.android.calendar.widget; +import com.android.calendar.R; +import com.android.calendar.Utils; + import android.app.AlarmManager; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; @@ -24,7 +27,11 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.net.Uri; import android.text.format.DateUtils; +import android.text.format.Time; +import android.util.Log; +import android.widget.RemoteViews; /** * Simple widget to show next upcoming calendar event. @@ -37,7 +44,6 @@ public class CalendarAppWidgetProvider extends AppWidgetProvider { "com.android.calendar.APPWIDGET_UPDATE"; // TODO Move these to Calendar.java - static final String EXTRA_WIDGET_IDS = "com.android.calendar.EXTRA_WIDGET_IDS"; static final String EXTRA_EVENT_IDS = "com.android.calendar.EXTRA_EVENT_IDS"; /** @@ -49,7 +55,9 @@ public class CalendarAppWidgetProvider extends AppWidgetProvider { // coming in without extras, which AppWidgetProvider then blocks. final String action = intent.getAction(); if (ACTION_CALENDAR_APPWIDGET_UPDATE.equals(action)) { - performUpdate(context, null /* all widgets */, + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + performUpdate(context, appWidgetManager, + appWidgetManager.getAppWidgetIds(getComponentName(context)), null /* no eventIds */); } else { super.onReceive(context, intent); @@ -92,7 +100,7 @@ public class CalendarAppWidgetProvider extends AppWidgetProvider { */ @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { - performUpdate(context, appWidgetIds, null /* no eventIds */); + performUpdate(context, appWidgetManager, appWidgetIds, null /* no eventIds */); } @@ -116,19 +124,42 @@ public class CalendarAppWidgetProvider extends AppWidgetProvider { * @param changedEventIds Specific events known to be changed. If present, * we use it to decide if an update is necessary. */ - private void performUpdate(Context context, int[] appWidgetIds, + private void performUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, long[] changedEventIds) { - // Launch over to service so it can perform update - final Intent updateIntent = new Intent(context, CalendarAppWidgetService.class); - - if (appWidgetIds != null) { - updateIntent.putExtra(EXTRA_WIDGET_IDS, appWidgetIds); - } + // Launch over to service so it can perform update + for (int appWidgetId : appWidgetIds) { + if (LOGD) Log.d(TAG, "Building widget update..."); + Intent updateIntent = new Intent(context, CalendarAppWidgetService.class); + updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); if (changedEventIds != null) { updateIntent.putExtra(EXTRA_EVENT_IDS, changedEventIds); } - - context.startService(updateIntent); + updateIntent.setData(Uri.parse(updateIntent.toUri(Intent.URI_INTENT_SCHEME))); + + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget); + // Calendar header + Time time = new Time(); + time.setToNow(); + String dayOfWeek = DateUtils.getDayOfWeekString( + time.weekDay + 1, DateUtils.LENGTH_MEDIUM).toUpperCase(); + views.setTextViewText(R.id.day_of_week, dayOfWeek); + views.setTextViewText(R.id.day_of_month, Integer.toString(time.monthDay)); + // Attach to list of events + views.setRemoteAdapter(R.id.events_list, updateIntent); + + // Clicking on the widget launches Calendar + // TODO fix this exact behavior? +// long startTime = Math.max(currentTime, events.firstTime); + long startTime = System.currentTimeMillis(); + + PendingIntent pendingIntent = getLaunchPendingIntent(context, startTime); + views.setOnClickPendingIntent(R.id.appwidget, pendingIntent); + + PendingIntent newEventIntent = getNewEventPendingIntent(context); + views.setOnClickPendingIntent(R.id.new_event_button, newEventIntent); + + appWidgetManager.updateAppWidget(appWidgetId, views); + } } /** @@ -144,4 +175,37 @@ public class CalendarAppWidgetProvider extends AppWidgetProvider { return PendingIntent.getBroadcast(context, 0 /* no requestCode */, updateIntent, 0 /* no flags */); } + + /** + * Build a {@link PendingIntent} to launch the Calendar app. This correctly + * sets action, category, and flags so that we don't duplicate tasks when + * Calendar was also launched from a normal desktop icon. If the go to time + * is 0, then calendar will be launched without a starting time. + * + * @param goToTime time that calendar should take the user to, or 0 to + * indicate no specific start time. + */ + static PendingIntent getLaunchPendingIntent(Context context, long goToTime) { + Intent launchIntent = new Intent(); + String dataString = "content://com.android.calendar/time"; + launchIntent.setAction(Intent.ACTION_VIEW); + launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | + Intent.FLAG_ACTIVITY_CLEAR_TOP); + if (goToTime != 0) { + launchIntent.putExtra(Utils.INTENT_KEY_DETAIL_VIEW, true); + dataString += "/" + goToTime; + } + Uri data = Uri.parse(dataString); + launchIntent.setData(data); + return PendingIntent.getActivity(context, 0 /* no requestCode */, + launchIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private static PendingIntent getNewEventPendingIntent(Context context) { + Intent newEventIntent = new Intent(Intent.ACTION_EDIT); + newEventIntent.setType("vnd.android.cursor.item/event"); + return PendingIntent.getActivity(context, 0, newEventIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + } } diff --git a/src/com/android/calendar/widget/CalendarAppWidgetReceiver.java b/src/com/android/calendar/widget/CalendarAppWidgetReceiver.java index 13edcb3d..cfb83aa4 100644 --- a/src/com/android/calendar/widget/CalendarAppWidgetReceiver.java +++ b/src/com/android/calendar/widget/CalendarAppWidgetReceiver.java @@ -16,6 +16,7 @@ package com.android.calendar.widget; +import android.appwidget.AppWidgetManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -29,7 +30,8 @@ public class CalendarAppWidgetReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // Launch over to service so it can perform update - final Intent updateIntent = new Intent(context, CalendarAppWidgetService.class); + final Intent updateIntent = new Intent( + CalendarAppWidgetProvider.ACTION_CALENDAR_APPWIDGET_UPDATE); // Copy over the relevant extra fields if they exist if (intent.hasExtra(CalendarAppWidgetProvider.EXTRA_EVENT_IDS)) { @@ -37,12 +39,18 @@ public class CalendarAppWidgetReceiver extends BroadcastReceiver { updateIntent.putExtra(CalendarAppWidgetProvider.EXTRA_EVENT_IDS, data); } - if (intent.hasExtra(CalendarAppWidgetProvider.EXTRA_WIDGET_IDS)) { - int[] data = intent.getIntArrayExtra(CalendarAppWidgetProvider.EXTRA_WIDGET_IDS); - updateIntent.putExtra(CalendarAppWidgetProvider.EXTRA_WIDGET_IDS, data); + if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) { + int data = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, data); + } + + if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)) { + int[] data = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS); + updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, data); } if (LOGD) Log.d(TAG, "Something changed, updating widget"); - context.startService(updateIntent); + context.sendBroadcast(updateIntent); } } diff --git a/src/com/android/calendar/widget/CalendarAppWidgetService.java b/src/com/android/calendar/widget/CalendarAppWidgetService.java index 7e4a8e6f..249530c5 100644 --- a/src/com/android/calendar/widget/CalendarAppWidgetService.java +++ b/src/com/android/calendar/widget/CalendarAppWidgetService.java @@ -16,25 +16,23 @@ package com.android.calendar.widget; -import com.google.common.annotations.VisibleForTesting; - -import com.android.calendar.R; -import com.android.calendar.Utils; -import com.android.calendar.widget.CalendarAppWidgetModel.EventInfo; - - import android.app.AlarmManager; -import android.app.IntentService; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; -import android.content.ComponentName; +import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; import android.database.Cursor; +import android.database.MatrixCursor; import android.net.Uri; +import android.os.Handler; +import android.provider.Calendar; import android.provider.Calendar.Attendees; import android.provider.Calendar.Calendars; +import android.provider.Calendar.Events; import android.provider.Calendar.Instances; import android.text.TextUtils; import android.text.format.DateFormat; @@ -43,25 +41,28 @@ import android.text.format.Time; import android.util.Log; import android.view.View; import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import com.google.common.annotations.VisibleForTesting; + +import com.android.calendar.R; +import com.android.calendar.Utils; +import com.android.calendar.widget.CalendarAppWidgetModel.EventInfo; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.TimeZone; -public class CalendarAppWidgetService extends IntentService { +public class CalendarAppWidgetService extends RemoteViewsService { private static final String TAG = "CalendarAppWidgetService"; private static final boolean LOGD = false; - /* TODO query doesn't handle all-day events properly, we should fix this in - * the provider in a manner similar to how it is handled in Event.loadEvents - * in the Calendar application. - */ + private static final int EVENT_MAX_COUNT = 10; + private static final String EVENT_SORT_ORDER = Instances.START_DAY + " ASC, " + Instances.START_MINUTE + " ASC, " + Instances.END_DAY + " ASC, " - + Instances.END_MINUTE + " ASC LIMIT 10"; + + Instances.END_MINUTE + " ASC LIMIT " + EVENT_MAX_COUNT; // TODO can't use parameter here because provider is dropping them private static final String EVENT_SELECTION = Calendars.SELECTED + "=1 AND " @@ -89,647 +90,570 @@ public class CalendarAppWidgetService extends IntentService { // update about six hours from now. private static final long UPDATE_NO_EVENTS = DateUtils.HOUR_IN_MILLIS * 6; - public CalendarAppWidgetService() { - super(TAG); + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + return new CalendarFactory(getApplicationContext(), intent); } - @Override - protected void onHandleIntent(Intent intent) { - // These will be null if the extra data doesn't exist - int[] widgetIds = intent.getIntArrayExtra(CalendarAppWidgetProvider.EXTRA_WIDGET_IDS); - long[] eventIds = null; - HashSet<Long> eventIdsSet = null; - if (intent.hasExtra(CalendarAppWidgetProvider.EXTRA_EVENT_IDS)) { - eventIds = intent.getExtras().getLongArray(CalendarAppWidgetProvider.EXTRA_EVENT_IDS); - eventIdsSet = new HashSet<Long>(eventIds.length); - for (int i = 0; i < eventIds.length; i++) { - eventIdsSet.add(eventIds[i]); - } - } - long now = System.currentTimeMillis(); + protected static class MarkedEvents { + + /** + * The row IDs of all events marked for display + */ + List<Integer> markedIds = new ArrayList<Integer>(10); + + /** + * The start time of the first marked event + */ + long firstTime = -1; + + /** The number of events currently in progress */ + int inProgressCount = 0; // Number of events with same start time as the primary evt. + + /** The start time of the next upcoming event */ + long primaryTime = -1; - performUpdate(this, widgetIds, eventIdsSet, now); + /** + * The number of events that share the same start time as the next + * upcoming event + */ + int primaryCount = 0; // Number of events with same start time as the secondary evt. + + /** The start time of the next next upcoming event */ + long secondaryTime = 1; + + /** + * The number of events that share the same start time as the next next + * upcoming event. + */ + int secondaryCount = 0; } - /** - * Process and push out an update for the given appWidgetIds. - * - * @param context Context to use when updating widget. - * @param appWidgetIds List of appWidgetIds to update, or null for all. - * @param changedEventIds Specific events known to be changed, otherwise - * null. If present, we use to decide if an update is necessary. - * @param now System clock time to use during this update. - */ - private void performUpdate(Context context, int[] appWidgetIds, - Set<Long> changedEventIds, long now) { - ContentResolver resolver = context.getContentResolver(); - - Cursor cursor = null; - RemoteViews views = null; - long triggerTime = -1; - - try { - cursor = getUpcomingInstancesCursor(resolver, SEARCH_DURATION, now); - if (cursor != null) { - MarkedEvents events = buildMarkedEvents(cursor, changedEventIds, now); - - boolean shouldUpdate = true; - if (changedEventIds != null && changedEventIds.size() > 0) { - shouldUpdate = events.watchFound; - } + protected static class CalendarFactory implements RemoteViewsService.RemoteViewsFactory { + + private static final String TAG = CalendarFactory.class.getSimpleName(); - if (events.markedIds.isEmpty()) { - views = getAppWidgetNoEvents(context); - } else if (shouldUpdate) { - views = getAppWidgetUpdate(context, cursor, events); - triggerTime = calculateUpdateTime(cursor, events); + private static final boolean LOGD = true; + + private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(Intent.ACTION_TIMEZONE_CHANGED) + || action.equals(Intent.ACTION_TIME_CHANGED) + || action.equals(Intent.ACTION_DATE_CHANGED) + || (action.equals(Intent.ACTION_PROVIDER_CHANGED) + && intent.getData().equals(Calendar.CONTENT_URI))) { + loadData(); } - } else { - views = getAppWidgetNoEvents(context); } - } finally { - if (cursor != null) { - cursor.close(); + }; + + private final ContentObserver mContentObserver = new ContentObserver(new Handler()) { + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + loadData(); } + }; + + private final int mAppWidgetId; + + private Context mContext; + + private CalendarAppWidgetModel mModel; + + private Cursor mCursor; + + protected CalendarFactory(Context context, Intent intent) { + mContext = context; + mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); } - // Bail out early if no update built - if (views == null) { - if (LOGD) Log.d(TAG, "Didn't build update, possibly because changedEventIds=" + - changedEventIds.toString()); - return; + @Override + public void onCreate() { + loadData(); + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction(Intent.ACTION_DATE_CHANGED); + filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + filter.addAction(Intent.ACTION_PROVIDER_CHANGED); + mContext.registerReceiver(mIntentReceiver, filter); + + mContext.getContentResolver().registerContentObserver( + Events.CONTENT_URI, true, mContentObserver); } - AppWidgetManager gm = AppWidgetManager.getInstance(context); - if (appWidgetIds != null && appWidgetIds.length > 0) { - gm.updateAppWidget(appWidgetIds, views); - } else { - ComponentName thisWidget = CalendarAppWidgetProvider.getComponentName(context); - gm.updateAppWidget(thisWidget, views); + @Override + public void onDestroy() { + mCursor.close(); + mContext.unregisterReceiver(mIntentReceiver); + mContext.getContentResolver().unregisterContentObserver(mContentObserver); } - // Schedule an alarm to wake ourselves up for the next update. We also cancel - // all existing wake-ups because PendingIntents don't match against extras. - // If no next-update calculated, or bad trigger time in past, schedule - // update about six hours from now. - if (triggerTime == -1 || triggerTime < now) { - if (LOGD) Log.w(TAG, "Encountered bad trigger time " + - formatDebugTime(triggerTime, now)); - triggerTime = now + UPDATE_NO_EVENTS; + @Override + public RemoteViews getLoadingView() { + RemoteViews views = new RemoteViews(mContext.getPackageName(), + R.layout.appwidget_loading); + return views; } - AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - PendingIntent pendingUpdate = CalendarAppWidgetProvider.getUpdateIntent(context); + @Override + public RemoteViews getViewAt(int position) { + // we use getCount here so that it doesn't return null when empty + if (position < 0 || position >= getCount()) { + return null; + } - am.cancel(pendingUpdate); - am.set(AlarmManager.RTC, triggerTime, pendingUpdate); + if (mModel.eventInfos.length > 0) { + RemoteViews views = new RemoteViews(mContext.getPackageName(), + R.layout.appwidget_row); - if (LOGD) Log.d(TAG, "Scheduled next update at " + formatDebugTime(triggerTime, now)); - } + EventInfo e = mModel.eventInfos[position]; - /** - * Format given time for debugging output. - * - * @param unixTime Target time to report. - * @param now Current system time from {@link System#currentTimeMillis()} - * for calculating time difference. - */ - static private String formatDebugTime(long unixTime, long now) { - Time time = new Time(); - time.set(unixTime); - - long delta = unixTime - now; - if (delta > DateUtils.MINUTE_IN_MILLIS) { - delta /= DateUtils.MINUTE_IN_MILLIS; - return String.format("[%d] %s (%+d mins)", unixTime, time.format("%H:%M:%S"), delta); - } else { - delta /= DateUtils.SECOND_IN_MILLIS; - return String.format("[%d] %s (%+d secs)", unixTime, time.format("%H:%M:%S"), delta); - } - } + updateTextView(views, R.id.when, e.visibWhen, e.when); + updateTextView(views, R.id.where, e.visibWhere, e.where); + updateTextView(views, R.id.title, e.visibTitle, e.title); - /** - * Convert given UTC time into current local time. - * - * @param recycle Time object to recycle, otherwise null. - * @param utcTime Time to convert, in UTC. - */ - static private long convertUtcToLocal(Time recycle, long utcTime) { - if (recycle == null) { - recycle = new Time(); + PendingIntent launchIntent = + CalendarAppWidgetProvider.getLaunchPendingIntent( + mContext, e.start); + views.setOnClickPendingIntent(R.id.appwidget_row, launchIntent); + return views; + } else { + RemoteViews views = new RemoteViews(mContext.getPackageName(), + R.layout.appwidget_no_events); + PendingIntent launchIntent = + CalendarAppWidgetProvider.getLaunchPendingIntent( + mContext, 0); + views.setOnClickPendingIntent(R.id.appwidget_no_events, launchIntent); + return views; + } } - recycle.timezone = Time.TIMEZONE_UTC; - recycle.set(utcTime); - recycle.timezone = TimeZone.getDefault().getID(); - return recycle.normalize(true); - } - /** - * Figure out the next time we should push widget updates, usually the time - * calculated by {@link #getEventFlip(Cursor, long, long, boolean)}. - * - * @param cursor Valid cursor on {@link Instances#CONTENT_URI} - * @param events {@link MarkedEvents} parsed from the cursor - */ - private long calculateUpdateTime(Cursor cursor, MarkedEvents events) { - long result = -1; - if (!events.markedIds.isEmpty()) { - cursor.moveToPosition(events.markedIds.get(0)); - long start = cursor.getLong(INDEX_BEGIN); - long end = cursor.getLong(INDEX_END); - boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0; + @Override + public int getViewTypeCount() { + return 3; + } - // Adjust all-day times into local timezone - if (allDay) { - final Time recycle = new Time(); - start = convertUtcToLocal(recycle, start); - end = convertUtcToLocal(recycle, end); - } + @Override + public int getCount() { + // if there are no events, we still return 1 to represent the "no + // events" view + return Math.max(1, mModel.eventInfos.length); + } - result = getEventFlip(cursor, start, end, allDay); + @Override + public long getItemId(int position) { + return position; + } - // Make sure an update happens at midnight or earlier - long midnight = getNextMidnightTimeMillis(); - result = Math.min(midnight, result); + @Override + public boolean hasStableIds() { + return true; } - return result; - } - private long getNextMidnightTimeMillis() { - Time time = new Time(); - time.setToNow(); - time.monthDay++; - time.hour = 0; - time.minute = 0; - time.second = 0; - long midnight = time.normalize(true); - return midnight; - } + private void loadData() { + long now = System.currentTimeMillis(); + if (LOGD) Log.d(TAG, "Querying for widget events..."); + if (mCursor != null) { + mCursor.close(); + } - /** - * Calculate flipping point for the given event; when we should hide this - * event and show the next one. This is defined as the end time of the - * event. - * - * @param start Event start time in local timezone. - * @param end Event end time in local timezone. - */ - static private long getEventFlip(Cursor cursor, long start, long end, boolean allDay) { - return end; - } + mCursor = getUpcomingInstancesCursor( + mContext.getContentResolver(), SEARCH_DURATION, now); + MarkedEvents markedEvents = buildMarkedEvents(mCursor, now); + mModel = buildAppWidgetModel(mContext, mCursor, markedEvents, now); + long triggerTime = calculateUpdateTime(mCursor, markedEvents); + // Schedule an alarm to wake ourselves up for the next update. We also cancel + // all existing wake-ups because PendingIntents don't match against extras. + + // If no next-update calculated, or bad trigger time in past, schedule + // update about six hours from now. + if (triggerTime == -1 || triggerTime < now) { + if (LOGD) Log.w(TAG, "Encountered bad trigger time " + + formatDebugTime(triggerTime, now)); + triggerTime = now + UPDATE_NO_EVENTS; + } - /** - * Set visibility of various widget components if there are events, or if no - * events were found. - * - * @param views Set of {@link RemoteViews} to apply visibility. - * @param noEvents True if no events found, otherwise false. - */ - private void setNoEventsVisible(RemoteViews views, boolean noEvents) { - views.setViewVisibility(R.id.no_events, noEvents ? View.VISIBLE : View.GONE); - views.setViewVisibility(R.id.page_flipper, View.GONE); - views.setViewVisibility(R.id.single_page, View.GONE); - } + AlarmManager am = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + PendingIntent pendingUpdate = CalendarAppWidgetProvider.getUpdateIntent(mContext); - /** - * Build a set of {@link RemoteViews} that describes how to update any - * widget for a specific event instance. - * - * @param cursor Valid cursor on {@link Instances#CONTENT_URI} - * @param events {@link MarkedEvents} parsed from the cursor - */ - private RemoteViews getAppWidgetUpdate(Context context, Cursor cursor, MarkedEvents events) { - RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget); - setNoEventsVisible(views, false); + am.cancel(pendingUpdate); + am.set(AlarmManager.RTC, triggerTime, pendingUpdate); + if (LOGD) Log.d(TAG, "Scheduled next update at " + formatDebugTime(triggerTime, now)); + } - long currentTime = System.currentTimeMillis(); - CalendarAppWidgetModel model = buildAppWidgetModel(context, cursor, events, currentTime); + /** + * Query across all calendars for upcoming event instances from now until + * some time in the future. + * + * Widen the time range that we query by one day on each end so that we can + * catch all-day events. All-day events are stored starting at midnight in + * UTC but should be included in the list of events starting at midnight + * local time. This may fetch more events than we actually want, so we + * filter them out later. + * + * @param resolver {@link ContentResolver} to use when querying + * {@link Instances#CONTENT_URI}. + * @param searchDuration Distance into the future to look for event + * instances, in milliseconds. + * @param now Current system time to use for this update, possibly from + * {@link System#currentTimeMillis()}. + */ + private Cursor getUpcomingInstancesCursor(ContentResolver resolver, + long searchDuration, long now) { + // Search for events from now until some time in the future - applyModelToView(context, model, views); + // Add a day on either side to catch all-day events + long begin = now - DateUtils.DAY_IN_MILLIS; + long end = now + searchDuration + DateUtils.DAY_IN_MILLIS; - // Clicking on the widget launches Calendar - long startTime = Math.max(currentTime, events.firstTime); + Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI, + String.format("%d/%d", begin, end)); - PendingIntent pendingIntent = getLaunchPendingIntent(context, startTime); - views.setOnClickPendingIntent(R.id.appwidget, pendingIntent); + Cursor cursor = resolver.query(uri, EVENT_PROJECTION, + EVENT_SELECTION, null, EVENT_SORT_ORDER); - PendingIntent newEventIntent = getNewEventPendingIntent(context); - views.setOnClickPendingIntent(R.id.new_event_button, newEventIntent); + // Start managing the cursor ourselves + MatrixCursor matrixCursor = Utils.matrixCursorFromCursor(cursor); + cursor.close(); - return views; - } - - private void applyModelToView(Context context, CalendarAppWidgetModel model, - RemoteViews views) { - views.setTextViewText(R.id.day_of_week, model.dayOfWeek); - views.setTextViewText(R.id.day_of_month, model.dayOfMonth); - views.setViewVisibility(R.id.no_events, model.visibNoEvents); - - // Make sure we have a clean slate first - views.removeAllViews(R.id.page_flipper); - views.removeAllViews(R.id.single_page); - - // If we don't have any events, just hide the relevant views and return - if (model.visibNoEvents != View.GONE) { - views.setViewVisibility(R.id.page_flipper, View.GONE); - views.setViewVisibility(R.id.single_page, View.GONE); - return; + return matrixCursor; } - // Luckily, length of this array is guaranteed to be even - int pages = model.eventInfos.length / 2; - - // We use a separate container for the case of only one page to prevent - // a ViewFlipper from repeatedly animating one view - if (pages > 1) { - views.setViewVisibility(R.id.page_flipper, View.VISIBLE); - views.setViewVisibility(R.id.single_page, View.GONE); - } else { - views.setViewVisibility(R.id.single_page, View.VISIBLE); - views.setViewVisibility(R.id.page_flipper, View.GONE); - } + /** + * Walk the given instances cursor and build a list of marked events to be + * used when updating the widget. This structure is also used to check if + * updates are needed. + * + * @param cursor Valid cursor across {@link Instances#CONTENT_URI}. + * @param watchEventIds Specific events to watch for, setting + * {@link MarkedEvents#watchFound} if found during marking. + * @param now Current system time to use for this update, possibly from + * {@link System#currentTimeMillis()} + */ + @VisibleForTesting + protected static MarkedEvents buildMarkedEvents(Cursor cursor, long now) { + MarkedEvents events = new MarkedEvents(); + final Time recycle = new Time(); + + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + int row = cursor.getPosition(); + long eventId = cursor.getLong(INDEX_EVENT_ID); + long start = cursor.getLong(INDEX_BEGIN); + long end = cursor.getLong(INDEX_END); + + boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0; + + if (LOGD) { + Log.d(TAG, "Row #" + row + " allDay:" + allDay + " start:" + start + + " end:" + end + " eventId:" + eventId); + } - // Iterate two at a time through the events and populate the views - for (int i = 0; i < model.eventInfos.length; i += 2) { - RemoteViews pageViews = new RemoteViews(context.getPackageName(), - R.layout.appwidget_page); - EventInfo e1 = model.eventInfos[i]; - EventInfo e2 = model.eventInfos[i + 1]; - - updateTextView(pageViews, R.id.when1, e1.visibWhen, e1.when); - updateTextView(pageViews, R.id.where1, e1.visibWhere, e1.where); - updateTextView(pageViews, R.id.title1, e1.visibTitle, e1.title); - updateTextView(pageViews, R.id.when2, e2.visibWhen, e2.when); - updateTextView(pageViews, R.id.where2, e2.visibWhere, e2.where); - updateTextView(pageViews, R.id.title2, e2.visibTitle, e2.title); - - if (pages > 1) { - views.addView(R.id.page_flipper, pageViews); - updateTextView(pageViews, R.id.page_count, View.VISIBLE, - makePageCount((i / 2) + 1, pages)); - } else { - views.addView(R.id.single_page, pageViews); - } - } + // Adjust all-day times into local timezone + if (allDay) { + start = convertUtcToLocal(recycle, start); + end = convertUtcToLocal(recycle, end); + } - } + if (end < now) { + // we might get some extra events when querying, in order to + // deal with all-day events + continue; + } - static String makePageCount(int current, int total) { - return Integer.toString(current) + " / " + Integer.toString(total); - } + boolean inProgress = now < end && now > start; - static void updateTextView(RemoteViews views, int id, int visibility, String string) { - views.setViewVisibility(id, visibility); - if (visibility == View.VISIBLE) { - views.setTextViewText(id, string); - } - } + // Skip events that have already passed their flip times + long eventFlip = getEventFlip(cursor, start, end, allDay); + if (LOGD) Log.d(TAG, "Calculated flip time " + formatDebugTime(eventFlip, now)); + if (eventFlip < now) { + continue; + } + +// /* Scan through the events with the following logic: +// * Rule #1 Show A) all the events that are in progress including +// * all day events and B) the next upcoming event and any events +// * with the same start time. +// * +// * Rule #2 If there are no events in progress, show A) the next +// * upcoming event and B) any events with the same start time. +// * +// * Rule #3 If no events start at the same time at A in rule 2, +// * show A) the next upcoming event and B) the following upcoming +// * event + any events with the same start time. +// */ +// if (inProgress) { +// // events for part A of Rule #1 +// events.markedIds.add(row); +// events.inProgressCount++; +// if (events.firstTime == -1) { +// events.firstTime = start; +// } +// } else { +// if (events.primaryCount == 0) { +// // first upcoming event +// events.markedIds.add(row); +// events.primaryTime = start; +// events.primaryCount++; +// if (events.firstTime == -1) { +// events.firstTime = start; +// } +// } else if (events.primaryTime == start) { +// // any events with same start time as first upcoming event +// events.markedIds.add(row); +// events.primaryCount++; +// } else if (events.markedIds.size() == 1) { +// // only one upcoming event, so we take the next upcoming +// events.markedIds.add(row); +// events.secondaryTime = start; +// events.secondaryCount++; +// } else if (events.secondaryCount > 0 +// && events.secondaryTime == start) { +// // any events with same start time as next upcoming +// events.markedIds.add(row); +// events.secondaryCount++; +// } else { +// // looks like we're done +// break; +// } +// } - static CalendarAppWidgetModel buildAppWidgetModel(Context context, Cursor cursor, - MarkedEvents events, long currentTime) { - int eventCount = events.markedIds.size(); - CalendarAppWidgetModel model = new CalendarAppWidgetModel(eventCount); - Time time = new Time(); - time.set(currentTime); - time.monthDay++; - time.hour = 0; - time.minute = 0; - time.second = 0; - long startOfNextDay = time.normalize(true); - - time.set(currentTime); - - // Calendar header - String dayOfWeek = DateUtils.getDayOfWeekString(time.weekDay + 1, DateUtils.LENGTH_MEDIUM) - .toUpperCase(); - - model.dayOfWeek = dayOfWeek; - model.dayOfMonth = Integer.toString(time.monthDay); - - int i = 0; - for (Integer id : events.markedIds) { - populateEvent(context, cursor, id, model, time, i, true, startOfNextDay, currentTime); - i++; + events.markedIds.add(row); + } + return events; } - return model; - } + @VisibleForTesting + protected static CalendarAppWidgetModel buildAppWidgetModel( + Context context, Cursor cursor, MarkedEvents events, long currentTime) { + int eventCount = events.markedIds.size(); + CalendarAppWidgetModel model = new CalendarAppWidgetModel(eventCount); + Time time = new Time(); + time.set(currentTime); + time.monthDay++; + time.hour = 0; + time.minute = 0; + time.second = 0; + long startOfNextDay = time.normalize(true); + + time.set(currentTime); + + // Calendar header + String dayOfWeek = DateUtils.getDayOfWeekString( + time.weekDay + 1, DateUtils.LENGTH_MEDIUM).toUpperCase(); + + model.dayOfWeek = dayOfWeek; + model.dayOfMonth = Integer.toString(time.monthDay); + + int i = 0; + for (Integer id : events.markedIds) { + populateEvent(context, cursor, id, model, time, i, true, + startOfNextDay, currentTime); + i++; + } - /** - * Pulls the information for a single event from the cursor and populates - * the corresponding model object with the data. - * - * @param context a Context to use for accessing resources - * @param cursor the cursor to retrieve the data from - * @param rowId the ID of the row to retrieve - * @param model the model object to populate - * @param recycle a Time instance to recycle - * @param eventIndex which event index in the model to populate - * @param showTitleLocation whether or not to show the title and location - * @param startOfNextDay the beginning of the next day - * @param currentTime the current time - */ - static private void populateEvent(Context context, Cursor cursor, int rowId, - CalendarAppWidgetModel model, Time recycle, int eventIndex, - boolean showTitleLocation, long startOfNextDay, long currentTime) { - cursor.moveToPosition(rowId); - - // When - boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0; - long start = cursor.getLong(INDEX_BEGIN); - long end = cursor.getLong(INDEX_END); - if (allDay) { - start = convertUtcToLocal(recycle, start); - end = convertUtcToLocal(recycle, end); + return model; } - boolean eventIsInProgress = start <= currentTime && end > currentTime; - boolean eventIsToday = start < startOfNextDay; - boolean eventIsTomorrow = !eventIsToday && !eventIsInProgress - && (start < (startOfNextDay + DateUtils.DAY_IN_MILLIS)); - - // Compute a human-readable string for the start time of the event - String whenString; - if (eventIsInProgress && allDay) { - // All day events for the current day display as just "Today" - whenString = context.getString(R.string.today); - } else if (eventIsTomorrow && allDay) { - // All day events for the next day display as just "Tomorrow" - whenString = context.getString(R.string.tomorrow); - } else { - int flags = DateUtils.FORMAT_ABBREV_ALL; - if (allDay) { - flags |= DateUtils.FORMAT_UTC; - } else { - flags |= DateUtils.FORMAT_SHOW_TIME; - if (DateFormat.is24HourFormat(context)) { - flags |= DateUtils.FORMAT_24HOUR; + /** + * Figure out the next time we should push widget updates, usually the time + * calculated by {@link #getEventFlip(Cursor, long, long, boolean)}. + * + * @param cursor Valid cursor on {@link Instances#CONTENT_URI} + * @param events {@link MarkedEvents} parsed from the cursor + */ + private long calculateUpdateTime(Cursor cursor, MarkedEvents events) { + long result = -1; + if (!events.markedIds.isEmpty()) { + cursor.moveToPosition(events.markedIds.get(0)); + long start = cursor.getLong(INDEX_BEGIN); + long end = cursor.getLong(INDEX_END); + boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0; + + // Adjust all-day times into local timezone + if (allDay) { + final Time recycle = new Time(); + start = convertUtcToLocal(recycle, start); + end = convertUtcToLocal(recycle, end); } - } - // Show day of the week if not today or tomorrow - if (!eventIsTomorrow && !eventIsToday) { - flags |= DateUtils.FORMAT_SHOW_WEEKDAY; - } - whenString = DateUtils.formatDateRange(context, start, start, flags); - if (eventIsTomorrow) { - whenString += (", "); - whenString += context.getString(R.string.tomorrow); - } else if (eventIsInProgress) { - whenString += " ("; - whenString += context.getString(R.string.in_progress); - whenString += ")"; - } - } - model.eventInfos[eventIndex].when = whenString; - model.eventInfos[eventIndex].visibWhen = View.VISIBLE; + result = getEventFlip(cursor, start, end, allDay); - if (showTitleLocation) { - // What - String titleString = cursor.getString(INDEX_TITLE); - if (TextUtils.isEmpty(titleString)) { - titleString = context.getString(R.string.no_title_label); + // Make sure an update happens at midnight or earlier + long midnight = getNextMidnightTimeMillis(); + result = Math.min(midnight, result); } - model.eventInfos[eventIndex].title = titleString; - model.eventInfos[eventIndex].visibTitle = View.VISIBLE; - - // Where - String whereString = cursor.getString(INDEX_EVENT_LOCATION); - if (!TextUtils.isEmpty(whereString)) { - model.eventInfos[eventIndex].visibWhere = View.VISIBLE; - model.eventInfos[eventIndex].where = whereString; - } else { - model.eventInfos[eventIndex].visibWhere = View.GONE; - } - if (LOGD) Log.d(TAG, " Title:" + titleString + " Where:" + whereString); + return result; } - } - - /** - * Build a set of {@link RemoteViews} that describes an error state. - */ - private RemoteViews getAppWidgetNoEvents(Context context) { - RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget); - setNoEventsVisible(views, true); - - // Calendar header - Time time = new Time(); - time.setToNow(); - String dayOfWeek = DateUtils.getDayOfWeekString(time.weekDay + 1, DateUtils.LENGTH_MEDIUM) - .toUpperCase(); - views.setTextViewText(R.id.day_of_week, dayOfWeek); - views.setTextViewText(R.id.day_of_month, Integer.toString(time.monthDay)); - - // Clicking on widget launches the agenda view in Calendar - PendingIntent pendingIntent = getLaunchPendingIntent(context, 0); - views.setOnClickPendingIntent(R.id.appwidget, pendingIntent); - - return views; - } - /** - * Build a {@link PendingIntent} to launch the Calendar app. This correctly - * sets action, category, and flags so that we don't duplicate tasks when - * Calendar was also launched from a normal desktop icon. - * @param goToTime time that calendar should take the user to - */ - private PendingIntent getLaunchPendingIntent(Context context, long goToTime) { - Intent launchIntent = new Intent(); - String dataString = "content://com.android.calendar/time"; - launchIntent.setAction(Intent.ACTION_VIEW); - launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | - Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | - Intent.FLAG_ACTIVITY_CLEAR_TOP); - if (goToTime != 0) { - launchIntent.putExtra(Utils.INTENT_KEY_DETAIL_VIEW, true); - dataString += "/" + goToTime; + private static long getNextMidnightTimeMillis() { + Time time = new Time(); + time.setToNow(); + time.monthDay++; + time.hour = 0; + time.minute = 0; + time.second = 0; + long midnight = time.normalize(true); + return midnight; } - Uri data = Uri.parse(dataString); - launchIntent.setData(data); - return PendingIntent.getActivity(context, 0 /* no requestCode */, - launchIntent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent getNewEventPendingIntent(Context context) { - Intent newEventIntent = new Intent(Intent.ACTION_EDIT); - newEventIntent.setType("vnd.android.cursor.item/event"); - return PendingIntent.getActivity(context, 0, newEventIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - } - - static class MarkedEvents { /** - * The row IDs of all events marked for display + * Format given time for debugging output. + * + * @param unixTime Target time to report. + * @param now Current system time from {@link System#currentTimeMillis()} + * for calculating time difference. */ - List<Integer> markedIds = new ArrayList<Integer>(10); + static private String formatDebugTime(long unixTime, long now) { + Time time = new Time(); + time.set(unixTime); + + long delta = unixTime - now; + if (delta > DateUtils.MINUTE_IN_MILLIS) { + delta /= DateUtils.MINUTE_IN_MILLIS; + return String.format("[%d] %s (%+d mins)", unixTime, + time.format("%H:%M:%S"), delta); + } else { + delta /= DateUtils.SECOND_IN_MILLIS; + return String.format("[%d] %s (%+d secs)", unixTime, + time.format("%H:%M:%S"), delta); + } + } /** - * The start time of the first marked event + * Convert given UTC time into current local time. + * + * @param recycle Time object to recycle, otherwise null. + * @param utcTime Time to convert, in UTC. */ - long firstTime = -1; - - /** The number of events currently in progress */ - int inProgressCount = 0; // Number of events with same start time as the primary evt. - - /** The start time of the next upcoming event */ - long primaryTime = -1; + static private long convertUtcToLocal(Time recycle, long utcTime) { + if (recycle == null) { + recycle = new Time(); + } + recycle.timezone = Time.TIMEZONE_UTC; + recycle.set(utcTime); + recycle.timezone = TimeZone.getDefault().getID(); + return recycle.normalize(true); + } /** - * The number of events that share the same start time as the next - * upcoming event + * Calculate flipping point for the given event; when we should hide this + * event and show the next one. This is defined as the end time of the + * event. + * + * @param start Event start time in local timezone. + * @param end Event end time in local timezone. */ - int primaryCount = 0; // Number of events with same start time as the secondary evt. + static private long getEventFlip(Cursor cursor, long start, long end, boolean allDay) { + return end; + } - /** The start time of the next next upcoming event */ - long secondaryTime = 1; + static void updateTextView(RemoteViews views, int id, int visibility, String string) { + views.setViewVisibility(id, visibility); + if (visibility == View.VISIBLE) { + views.setTextViewText(id, string); + } + } /** - * The number of events that share the same start time as the next next - * upcoming event. + * Pulls the information for a single event from the cursor and populates + * the corresponding model object with the data. + * + * @param context a Context to use for accessing resources + * @param cursor the cursor to retrieve the data from + * @param rowId the ID of the row to retrieve + * @param model the model object to populate + * @param recycle a Time instance to recycle + * @param eventIndex which event index in the model to populate + * @param showTitleLocation whether or not to show the title and location + * @param startOfNextDay the beginning of the next day + * @param currentTime the current time */ - int secondaryCount = 0; - - boolean watchFound = false; - } + static private void populateEvent(Context context, Cursor cursor, int rowId, + CalendarAppWidgetModel model, Time recycle, int eventIndex, + boolean showTitleLocation, long startOfNextDay, long currentTime) { + cursor.moveToPosition(rowId); - /** - * Walk the given instances cursor and build a list of marked events to be - * used when updating the widget. This structure is also used to check if - * updates are needed. - * - * @param cursor Valid cursor across {@link Instances#CONTENT_URI}. - * @param watchEventIds Specific events to watch for, setting - * {@link MarkedEvents#watchFound} if found during marking. - * @param now Current system time to use for this update, possibly from - * {@link System#currentTimeMillis()} - */ - @VisibleForTesting - static MarkedEvents buildMarkedEvents(Cursor cursor, Set<Long> watchEventIds, long now) { - MarkedEvents events = new MarkedEvents(); - final Time recycle = new Time(); - - cursor.moveToPosition(-1); - while (cursor.moveToNext()) { - int row = cursor.getPosition(); - long eventId = cursor.getLong(INDEX_EVENT_ID); + // When + boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0; long start = cursor.getLong(INDEX_BEGIN); long end = cursor.getLong(INDEX_END); - - boolean allDay = cursor.getInt(INDEX_ALL_DAY) != 0; - - if (LOGD) { - Log.d(TAG, "Row #" + row + " allDay:" + allDay + " start:" + start + " end:" + end - + " eventId:" + eventId); - } - - // Adjust all-day times into local timezone if (allDay) { start = convertUtcToLocal(recycle, start); end = convertUtcToLocal(recycle, end); } - if (end < now) { - // we might get some extra events when querying, in order to - // deal with all-day events - continue; - } - - boolean inProgress = now < end && now > start; - - // Skip events that have already passed their flip times - long eventFlip = getEventFlip(cursor, start, end, allDay); - if (LOGD) Log.d(TAG, "Calculated flip time " + formatDebugTime(eventFlip, now)); - if (eventFlip < now) { - continue; + boolean eventIsInProgress = start <= currentTime && end > currentTime; + boolean eventIsToday = start < startOfNextDay; + boolean eventIsTomorrow = !eventIsToday && !eventIsInProgress + && (start < (startOfNextDay + DateUtils.DAY_IN_MILLIS)); + + // Compute a human-readable string for the start time of the event + String whenString; + if (eventIsInProgress && allDay) { + // All day events for the current day display as just "Today" + whenString = context.getString(R.string.today); + } else if (eventIsTomorrow && allDay) { + // All day events for the next day display as just "Tomorrow" + whenString = context.getString(R.string.tomorrow); + } else { + int flags = DateUtils.FORMAT_ABBREV_ALL; + if (allDay) { + flags |= DateUtils.FORMAT_UTC; + } else { + flags |= DateUtils.FORMAT_SHOW_TIME; + if (DateFormat.is24HourFormat(context)) { + flags |= DateUtils.FORMAT_24HOUR; + } + } + // Show day of the week if not today or tomorrow + if (!eventIsTomorrow && !eventIsToday) { + flags |= DateUtils.FORMAT_SHOW_WEEKDAY; + } + whenString = DateUtils.formatDateRange(context, start, start, flags); + // TODO better i18n formatting + if (eventIsTomorrow) { + whenString += (", "); + whenString += context.getString(R.string.tomorrow); + } else if (eventIsInProgress) { + whenString += " ("; + whenString += context.getString(R.string.in_progress); + whenString += ")"; + } } - // Mark if we've encountered the watched event - if (watchEventIds != null && watchEventIds.contains(eventId)) { - events.watchFound = true; - } + model.eventInfos[eventIndex].start = start; + model.eventInfos[eventIndex].when = whenString; + model.eventInfos[eventIndex].visibWhen = View.VISIBLE; - /* Scan through the events with the following logic: - * Rule #1 Show A) all the events that are in progress including - * all day events and B) the next upcoming event and any events - * with the same start time. - * - * Rule #2 If there are no events in progress, show A) the next - * upcoming event and B) any events with the same start time. - * - * Rule #3 If no events start at the same time at A in rule 2, - * show A) the next upcoming event and B) the following upcoming - * event + any events with the same start time. - */ - if (inProgress) { - // events for part A of Rule #1 - events.markedIds.add(row); - events.inProgressCount++; - if (events.firstTime == -1) { - events.firstTime = start; + if (showTitleLocation) { + // What + String titleString = cursor.getString(INDEX_TITLE); + if (TextUtils.isEmpty(titleString)) { + titleString = context.getString(R.string.no_title_label); } - } else { - if (events.primaryCount == 0) { - // first upcoming event - events.markedIds.add(row); - events.primaryTime = start; - events.primaryCount++; - if (events.firstTime == -1) { - events.firstTime = start; - } - } else if (events.primaryTime == start) { - // any events with same start time as first upcoming event - events.markedIds.add(row); - events.primaryCount++; - } else if (events.markedIds.size() == 1) { - // only one upcoming event, so we take the next upcoming - events.markedIds.add(row); - events.secondaryTime = start; - events.secondaryCount++; - } else if (events.secondaryCount > 0 - && events.secondaryTime == start) { - // any events with same start time as next upcoming - events.markedIds.add(row); - events.secondaryCount++; + model.eventInfos[eventIndex].title = titleString; + model.eventInfos[eventIndex].visibTitle = View.VISIBLE; + + // Where + String whereString = cursor.getString(INDEX_EVENT_LOCATION); + if (!TextUtils.isEmpty(whereString)) { + model.eventInfos[eventIndex].visibWhere = View.VISIBLE; + model.eventInfos[eventIndex].where = whereString; } else { - // looks like we're done - break; + model.eventInfos[eventIndex].visibWhere = View.GONE; } + if (LOGD) Log.d(TAG, " Title:" + titleString + " Where:" + whereString); } } - return events; - } - - /** - * Query across all calendars for upcoming event instances from now until - * some time in the future. - * - * Widen the time range that we query by one day on each end so that we can - * catch all-day events. All-day events are stored starting at midnight in - * UTC but should be included in the list of events starting at midnight - * local time. This may fetch more events than we actually want, so we - * filter them out later. - * - * @param resolver {@link ContentResolver} to use when querying - * {@link Instances#CONTENT_URI}. - * @param searchDuration Distance into the future to look for event - * instances, in milliseconds. - * @param now Current system time to use for this update, possibly from - * {@link System#currentTimeMillis()}. - */ - private Cursor getUpcomingInstancesCursor(ContentResolver resolver, - long searchDuration, long now) { - // Search for events from now until some time in the future - - // Add a day on either side to catch all-day events - long begin = now - DateUtils.DAY_IN_MILLIS; - long end = now + searchDuration + DateUtils.DAY_IN_MILLIS; - - Uri uri = Uri.withAppendedPath(Instances.CONTENT_URI, - String.format("%d/%d", begin, end)); - - return resolver.query(uri, EVENT_PROJECTION, EVENT_SELECTION, null, - EVENT_SORT_ORDER); } } diff --git a/tests/src/com/android/calendar/widget/CalendarAppWidgetServiceTest.java b/tests/src/com/android/calendar/widget/CalendarAppWidgetServiceTest.java index a07d007f..e06e462d 100644 --- a/tests/src/com/android/calendar/widget/CalendarAppWidgetServiceTest.java +++ b/tests/src/com/android/calendar/widget/CalendarAppWidgetServiceTest.java @@ -19,6 +19,7 @@ package com.android.calendar.widget; import com.android.calendar.widget.CalendarAppWidgetModel; import com.android.calendar.widget.CalendarAppWidgetService; +import com.android.calendar.widget.CalendarAppWidgetService.CalendarFactory; import com.android.calendar.widget.CalendarAppWidgetService.MarkedEvents; import java.util.TimeZone; @@ -26,6 +27,7 @@ import java.util.TimeZone; import android.database.MatrixCursor; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; +import android.test.suitebuilder.annotation.Suppress; import android.text.format.DateUtils; import android.view.View; @@ -104,16 +106,17 @@ public class CalendarAppWidgetServiceTest extends AndroidTestCase { expected.eventInfos[0].title = title; // Test - MarkedEvents events = CalendarAppWidgetService.buildMarkedEvents(cursor, null, now); - CalendarAppWidgetModel actual = CalendarAppWidgetService.buildAppWidgetModel( + MarkedEvents events = CalendarFactory.buildMarkedEvents(cursor, now); + CalendarAppWidgetModel actual = CalendarFactory.buildAppWidgetModel( getContext(), cursor, events, now); assertEquals(expected.toString(), actual.toString()); } - @SmallTest + // TODO re-enable this test when our widget behavior is finalized + @Suppress @SmallTest public void testGetAppWidgetModel_2StaggeredEvents() throws Exception { - CalendarAppWidgetModel expected = new CalendarAppWidgetModel(); + CalendarAppWidgetModel expected = new CalendarAppWidgetModel(2); MatrixCursor cursor = new MatrixCursor(CalendarAppWidgetService.EVENT_PROJECTION, 0); int i = 0; @@ -147,8 +150,8 @@ public class CalendarAppWidgetServiceTest extends AndroidTestCase { ++i; // Test - MarkedEvents events = CalendarAppWidgetService.buildMarkedEvents(cursor, null, now); - CalendarAppWidgetModel actual = CalendarAppWidgetService.buildAppWidgetModel( + MarkedEvents events = CalendarFactory.buildMarkedEvents(cursor, now); + CalendarAppWidgetModel actual = CalendarFactory.buildAppWidgetModel( getContext(), cursor, events, now); assertEquals(expected.toString(), actual.toString()); @@ -159,13 +162,14 @@ public class CalendarAppWidgetServiceTest extends AndroidTestCase { cursor.addRow(getRow(0, sunday + ONE_HOUR, sunday + TWO_HOURS, title + i, location + i, 0)); // Test again - events = CalendarAppWidgetService.buildMarkedEvents(cursor, null, now); - actual = CalendarAppWidgetService.buildAppWidgetModel(getContext(), cursor, events, now); + events = CalendarFactory.buildMarkedEvents(cursor, now); + actual = CalendarFactory.buildAppWidgetModel(getContext(), cursor, events, now); assertEquals(expected.toString(), actual.toString()); } - @SmallTest + // TODO re-enable this test when our widget behavior is finalized + @Suppress @SmallTest public void testGetAppWidgetModel_2SameStartTimeEvents() throws Exception { CalendarAppWidgetModel expected = new CalendarAppWidgetModel(); MatrixCursor cursor = new MatrixCursor(CalendarAppWidgetService.EVENT_PROJECTION, 0); @@ -199,8 +203,8 @@ public class CalendarAppWidgetServiceTest extends AndroidTestCase { ++i; // Test - MarkedEvents events = CalendarAppWidgetService.buildMarkedEvents(cursor, null, now); - CalendarAppWidgetModel actual = CalendarAppWidgetService.buildAppWidgetModel( + MarkedEvents events = CalendarFactory.buildMarkedEvents(cursor, now); + CalendarAppWidgetModel actual = CalendarFactory.buildAppWidgetModel( getContext(), cursor, events, now); assertEquals(expected.toString(), actual.toString()); @@ -211,8 +215,8 @@ public class CalendarAppWidgetServiceTest extends AndroidTestCase { cursor.addRow(getRow(0, now + TWO_HOURS, now + TWO_HOURS + 1, title + i, location + i, 0)); // Test again - events = CalendarAppWidgetService.buildMarkedEvents(cursor, null, now); - actual = CalendarAppWidgetService.buildAppWidgetModel(getContext(), cursor, events, now); + events = CalendarFactory.buildMarkedEvents(cursor, now); + actual = CalendarFactory.buildAppWidgetModel(getContext(), cursor, events, now); assertEquals(expected.toString(), actual.toString()); } @@ -258,14 +262,15 @@ public class CalendarAppWidgetServiceTest extends AndroidTestCase { expected.eventInfos[i].title = title + i; // Test - MarkedEvents events = CalendarAppWidgetService.buildMarkedEvents(cursor, null, now); - CalendarAppWidgetModel actual = CalendarAppWidgetService.buildAppWidgetModel( + MarkedEvents events = CalendarFactory.buildMarkedEvents(cursor, now); + CalendarAppWidgetModel actual = CalendarFactory.buildAppWidgetModel( getContext(), cursor, events, now); assertEquals(expected.toString(), actual.toString()); } - @SmallTest + // TODO re-enable this test when our widget behavior is finalized + @Suppress @SmallTest public void testGetAppWidgetModel_3SameStartTimeEvents() throws Exception { final long now = 1262340000000L; // Fri Jan 01 2010 01:00:00 GMT-0700 (PDT) CalendarAppWidgetModel expected = new CalendarAppWidgetModel(3); @@ -312,8 +317,8 @@ public class CalendarAppWidgetServiceTest extends AndroidTestCase { ++i; // Test - MarkedEvents events = CalendarAppWidgetService.buildMarkedEvents(cursor, null, now); - CalendarAppWidgetModel actual = CalendarAppWidgetService.buildAppWidgetModel( + MarkedEvents events = CalendarFactory.buildMarkedEvents(cursor, now); + CalendarAppWidgetModel actual = CalendarFactory.buildAppWidgetModel( getContext(), cursor, events, now); assertEquals(expected.toString(), actual.toString()); @@ -322,8 +327,8 @@ public class CalendarAppWidgetServiceTest extends AndroidTestCase { cursor.addRow(getRow(0, now + TWO_HOURS, now + TWO_HOURS + 1, title + i, location + i, 0)); // Test again, nothing should have changed, same expected result - events = CalendarAppWidgetService.buildMarkedEvents(cursor, null, now); - actual = CalendarAppWidgetService.buildAppWidgetModel(getContext(), cursor, events, now); + events = CalendarFactory.buildMarkedEvents(cursor, now); + actual = CalendarFactory.buildAppWidgetModel(getContext(), cursor, events, now); assertEquals(expected.toString(), actual.toString()); } @@ -384,8 +389,8 @@ public class CalendarAppWidgetServiceTest extends AndroidTestCase { cursor.addRow(getRow(0, now + TWO_HOURS, now + 4 * ONE_HOUR, title + i, location + i, 0)); // Test - MarkedEvents events = CalendarAppWidgetService.buildMarkedEvents(cursor, null, now); - CalendarAppWidgetModel actual = CalendarAppWidgetService.buildAppWidgetModel( + MarkedEvents events = CalendarFactory.buildMarkedEvents(cursor, now); + CalendarAppWidgetModel actual = CalendarFactory.buildAppWidgetModel( getContext(), cursor, events, now); assertEquals(expected.toString(), actual.toString()); @@ -424,8 +429,8 @@ public class CalendarAppWidgetServiceTest extends AndroidTestCase { cursor.addRow(getRow(0, now + ONE_HOUR, now + TWO_HOURS, title + i, location + i, 0)); // Test - MarkedEvents events = CalendarAppWidgetService.buildMarkedEvents(cursor, null, now); - CalendarAppWidgetModel actual = CalendarAppWidgetService.buildAppWidgetModel( + MarkedEvents events = CalendarFactory.buildMarkedEvents(cursor, now); + CalendarAppWidgetModel actual = CalendarFactory.buildAppWidgetModel( getContext(), cursor, events, now); assertEquals(expected.toString(), actual.toString()); @@ -465,8 +470,8 @@ public class CalendarAppWidgetServiceTest extends AndroidTestCase { cursor.addRow(getRow(1, 1262390400000L, 1262476800000L, title + i, location + i, 0)); // Test - MarkedEvents events = CalendarAppWidgetService.buildMarkedEvents(cursor, null, now); - CalendarAppWidgetModel actual = CalendarAppWidgetService.buildAppWidgetModel( + MarkedEvents events = CalendarFactory.buildMarkedEvents(cursor, now); + CalendarAppWidgetModel actual = CalendarFactory.buildAppWidgetModel( getContext(), cursor, events, now); assertEquals(expected.toString(), actual.toString()); @@ -506,8 +511,8 @@ public class CalendarAppWidgetServiceTest extends AndroidTestCase { cursor.addRow(getRow(1, 1262476800000L, 1262563200000L, title + i, location + i, 0)); // Test - MarkedEvents events = CalendarAppWidgetService.buildMarkedEvents(cursor, null, now); - CalendarAppWidgetModel actual = CalendarAppWidgetService.buildAppWidgetModel( + MarkedEvents events = CalendarAppWidgetService.CalendarFactory.buildMarkedEvents(cursor, now); + CalendarAppWidgetModel actual = CalendarAppWidgetService.CalendarFactory.buildAppWidgetModel( getContext(), cursor, events, now); assertEquals(expected.toString(), actual.toString()); |