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