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