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