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