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