Home | History | Annotate | Download | only in calendar
      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;
     18 
     19 import android.content.AsyncQueryHandler;
     20 import android.content.ContentResolver;
     21 import android.content.Context;
     22 import android.database.Cursor;
     23 import android.net.Uri;
     24 import android.provider.Calendar.Attendees;
     25 import android.provider.Calendar.Calendars;
     26 import android.provider.Calendar.Instances;
     27 import android.text.format.DateUtils;
     28 import android.text.format.Time;
     29 import android.util.Log;
     30 import android.view.LayoutInflater;
     31 import android.view.View;
     32 import android.view.View.OnClickListener;
     33 import android.view.ViewGroup;
     34 import android.widget.BaseAdapter;
     35 import android.widget.TextView;
     36 
     37 import java.util.Iterator;
     38 import java.util.LinkedList;
     39 import java.util.concurrent.ConcurrentLinkedQueue;
     40 
     41 /*
     42 Bugs Bugs Bugs:
     43 - At rotation and launch time, the initial position is not set properly. This code is calling
     44  listview.setSelection() in 2 rapid secessions but it dropped or didn't process the first one.
     45 - Scroll using trackball isn't repositioning properly after a new adapter is added.
     46 - Track ball clicks at the header/footer doesn't work.
     47 - Potential ping pong effect if the prefetch window is big and data is limited
     48 - Add index in calendar provider
     49 
     50 ToDo ToDo ToDo:
     51 Get design of header and footer from designer
     52 
     53 Make scrolling smoother.
     54 Test for correctness
     55 Loading speed
     56 Check for leaks and excessive allocations
     57  */
     58 
     59 public class AgendaWindowAdapter extends BaseAdapter {
     60 
     61     static final boolean BASICLOG = false;
     62     static final boolean DEBUGLOG = false;
     63     private static String TAG = "AgendaWindowAdapter";
     64 
     65     private static final String AGENDA_SORT_ORDER = "startDay ASC, begin ASC, title ASC";
     66     public static final int INDEX_TITLE = 1;
     67     public static final int INDEX_EVENT_LOCATION = 2;
     68     public static final int INDEX_ALL_DAY = 3;
     69     public static final int INDEX_HAS_ALARM = 4;
     70     public static final int INDEX_COLOR = 5;
     71     public static final int INDEX_RRULE = 6;
     72     public static final int INDEX_BEGIN = 7;
     73     public static final int INDEX_END = 8;
     74     public static final int INDEX_EVENT_ID = 9;
     75     public static final int INDEX_START_DAY = 10;
     76     public static final int INDEX_END_DAY = 11;
     77     public static final int INDEX_SELF_ATTENDEE_STATUS = 12;
     78 
     79     private static final String[] PROJECTION = new String[] {
     80             Instances._ID, // 0
     81             Instances.TITLE, // 1
     82             Instances.EVENT_LOCATION, // 2
     83             Instances.ALL_DAY, // 3
     84             Instances.HAS_ALARM, // 4
     85             Instances.COLOR, // 5
     86             Instances.RRULE, // 6
     87             Instances.BEGIN, // 7
     88             Instances.END, // 8
     89             Instances.EVENT_ID, // 9
     90             Instances.START_DAY, // 10 Julian start day
     91             Instances.END_DAY, // 11 Julian end day
     92             Instances.SELF_ATTENDEE_STATUS, // 12
     93     };
     94 
     95     // Listview may have a bug where the index/position is not consistent when there's a header.
     96     // TODO Need to look into this.
     97     private static final int OFF_BY_ONE_BUG = 1;
     98 
     99     private static final int MAX_NUM_OF_ADAPTERS = 5;
    100 
    101     private static final int IDEAL_NUM_OF_EVENTS = 50;
    102 
    103     private static final int MIN_QUERY_DURATION = 7; // days
    104 
    105     private static final int MAX_QUERY_DURATION = 60; // days
    106 
    107     private static final int PREFETCH_BOUNDARY = 1;
    108 
    109     // Times to auto-expand/retry query after getting no data
    110     private static final int RETRIES_ON_NO_DATA = 0;
    111 
    112     private Context mContext;
    113 
    114     private QueryHandler mQueryHandler;
    115 
    116     private AgendaListView mAgendaListView;
    117 
    118     private int mRowCount; // The sum of the rows in all the adapters
    119 
    120     private int mEmptyCursorCount;
    121 
    122     private DayAdapterInfo mLastUsedInfo; // Cached value of the last used adapter.
    123 
    124     private LinkedList<DayAdapterInfo> mAdapterInfos = new LinkedList<DayAdapterInfo>();
    125 
    126     private ConcurrentLinkedQueue<QuerySpec> mQueryQueue = new ConcurrentLinkedQueue<QuerySpec>();
    127 
    128     private TextView mHeaderView;
    129 
    130     private TextView mFooterView;
    131 
    132     private boolean mDoneSettingUpHeaderFooter = false;
    133 
    134     /*
    135      * When the user scrolled to the top, a query will be made for older events
    136      * and this will be incremented. Don't make more requests if
    137      * mOlderRequests > mOlderRequestsProcessed.
    138      */
    139     private int mOlderRequests;
    140 
    141     // Number of "older" query that has been processed.
    142     private int mOlderRequestsProcessed;
    143 
    144     /*
    145      * When the user scrolled to the bottom, a query will be made for newer
    146      * events and this will be incremented. Don't make more requests if
    147      * mNewerRequests > mNewerRequestsProcessed.
    148      */
    149     private int mNewerRequests;
    150 
    151     // Number of "newer" query that has been processed.
    152     private int mNewerRequestsProcessed;
    153 
    154     private boolean mShuttingDown;
    155     private boolean mHideDeclined;
    156 
    157     // Types of Query
    158     private static final int QUERY_TYPE_OLDER = 0; // Query for older events
    159     private static final int QUERY_TYPE_NEWER = 1; // Query for newer events
    160     private static final int QUERY_TYPE_CLEAN = 2; // Delete everything and query around a date
    161 
    162     // Placeholder if we need some code for updating the tz later.
    163     private Runnable mUpdateTZ = null;
    164 
    165     private static class QuerySpec {
    166         long queryStartMillis;
    167 
    168         Time goToTime;
    169 
    170         int start;
    171 
    172         int end;
    173 
    174         int queryType;
    175 
    176         public QuerySpec(int queryType) {
    177             this.queryType = queryType;
    178         }
    179 
    180         @Override
    181         public int hashCode() {
    182             final int prime = 31;
    183             int result = 1;
    184             result = prime * result + end;
    185             result = prime * result + (int) (queryStartMillis ^ (queryStartMillis >>> 32));
    186             result = prime * result + queryType;
    187             result = prime * result + start;
    188             if (goToTime != null) {
    189                 long goToTimeMillis = goToTime.toMillis(false);
    190                 result = prime * result + (int) (goToTimeMillis ^ (goToTimeMillis >>> 32));
    191             }
    192             return result;
    193         }
    194 
    195         @Override
    196         public boolean equals(Object obj) {
    197             if (this == obj) return true;
    198             if (obj == null) return false;
    199             if (getClass() != obj.getClass()) return false;
    200             QuerySpec other = (QuerySpec) obj;
    201             if (end != other.end || queryStartMillis != other.queryStartMillis
    202                     || queryType != other.queryType || start != other.start) {
    203                 return false;
    204             }
    205             if (goToTime != null) {
    206                 if (goToTime.toMillis(false) != other.goToTime.toMillis(false)) {
    207                     return false;
    208                 }
    209             } else {
    210                 if (other.goToTime != null) {
    211                     return false;
    212                 }
    213             }
    214             return true;
    215         }
    216     }
    217 
    218     static class EventInfo {
    219         long begin;
    220 
    221         long end;
    222 
    223         long id;
    224     }
    225 
    226     static class DayAdapterInfo {
    227         Cursor cursor;
    228 
    229         AgendaByDayAdapter dayAdapter;
    230 
    231         int start; // start day of the cursor's coverage
    232 
    233         int end; // end day of the cursor's coverage
    234 
    235         int offset; // offset in position in the list view
    236 
    237         int size; // dayAdapter.getCount()
    238 
    239         public DayAdapterInfo(Context context) {
    240             dayAdapter = new AgendaByDayAdapter(context);
    241         }
    242 
    243         @Override
    244         public String toString() {
    245             Time time = new Time();
    246             StringBuilder sb = new StringBuilder();
    247             time.setJulianDay(start);
    248             time.normalize(false);
    249             sb.append("Start:").append(time.toString());
    250             time.setJulianDay(end);
    251             time.normalize(false);
    252             sb.append(" End:").append(time.toString());
    253             sb.append(" Offset:").append(offset);
    254             sb.append(" Size:").append(size);
    255             return sb.toString();
    256         }
    257     }
    258 
    259     public AgendaWindowAdapter(AgendaActivity agendaActivity,
    260             AgendaListView agendaListView) {
    261         mContext = agendaActivity;
    262         mAgendaListView = agendaListView;
    263         mQueryHandler = new QueryHandler(agendaActivity.getContentResolver());
    264 
    265         LayoutInflater inflater = (LayoutInflater) agendaActivity
    266                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    267         mHeaderView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
    268         mFooterView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
    269         mHeaderView.setText(R.string.loading);
    270         mAgendaListView.addHeaderView(mHeaderView);
    271     }
    272 
    273     // Method in Adapter
    274     @Override
    275     public int getViewTypeCount() {
    276         return AgendaByDayAdapter.TYPE_LAST;
    277     }
    278 
    279     // Method in BaseAdapter
    280     @Override
    281     public boolean areAllItemsEnabled() {
    282         return false;
    283     }
    284 
    285     // Method in Adapter
    286     @Override
    287     public int getItemViewType(int position) {
    288         DayAdapterInfo info = getAdapterInfoByPosition(position);
    289         if (info != null) {
    290             return info.dayAdapter.getItemViewType(position - info.offset);
    291         } else {
    292             return -1;
    293         }
    294     }
    295 
    296     // Method in BaseAdapter
    297     @Override
    298     public boolean isEnabled(int position) {
    299         DayAdapterInfo info = getAdapterInfoByPosition(position);
    300         if (info != null) {
    301             return info.dayAdapter.isEnabled(position - info.offset);
    302         } else {
    303             return false;
    304         }
    305     }
    306 
    307     // Abstract Method in BaseAdapter
    308     public int getCount() {
    309         return mRowCount;
    310     }
    311 
    312     // Abstract Method in BaseAdapter
    313     public Object getItem(int position) {
    314         DayAdapterInfo info = getAdapterInfoByPosition(position);
    315         if (info != null) {
    316             return info.dayAdapter.getItem(position - info.offset);
    317         } else {
    318             return null;
    319         }
    320     }
    321 
    322     // Method in BaseAdapter
    323     @Override
    324     public boolean hasStableIds() {
    325         return true;
    326     }
    327 
    328     // Abstract Method in BaseAdapter
    329     public long getItemId(int position) {
    330         DayAdapterInfo info = getAdapterInfoByPosition(position);
    331         if (info != null) {
    332             return ((position - info.offset) << 20) + info.start ;
    333         } else {
    334             return -1;
    335         }
    336     }
    337 
    338     // Abstract Method in BaseAdapter
    339     public View getView(int position, View convertView, ViewGroup parent) {
    340         if (position >= (mRowCount - PREFETCH_BOUNDARY)
    341                 && mNewerRequests <= mNewerRequestsProcessed) {
    342             if (DEBUGLOG) Log.e(TAG, "queryForNewerEvents: ");
    343             mNewerRequests++;
    344             queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
    345         }
    346 
    347         if (position < PREFETCH_BOUNDARY
    348                 && mOlderRequests <= mOlderRequestsProcessed) {
    349             if (DEBUGLOG) Log.e(TAG, "queryForOlderEvents: ");
    350             mOlderRequests++;
    351             queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
    352         }
    353 
    354         View v;
    355         DayAdapterInfo info = getAdapterInfoByPosition(position);
    356         if (info != null) {
    357             v = info.dayAdapter.getView(position - info.offset, convertView,
    358                     parent);
    359         } else {
    360             //TODO
    361             Log.e(TAG, "BUG: getAdapterInfoByPosition returned null!!! " + position);
    362             TextView tv = new TextView(mContext);
    363             tv.setText("Bug! " + position);
    364             v = tv;
    365         }
    366 
    367         if (DEBUGLOG) {
    368             Log.e(TAG, "getView " + position + " = " + getViewTitle(v));
    369         }
    370         return v;
    371     }
    372 
    373     private int findDayPositionNearestTime(Time time) {
    374         if (DEBUGLOG) Log.e(TAG, "findDayPositionNearestTime " + time);
    375 
    376         DayAdapterInfo info = getAdapterInfoByTime(time);
    377         if (info != null) {
    378             return info.offset + info.dayAdapter.findDayPositionNearestTime(time);
    379         } else {
    380             return -1;
    381         }
    382     }
    383 
    384     private DayAdapterInfo getAdapterInfoByPosition(int position) {
    385         synchronized (mAdapterInfos) {
    386             if (mLastUsedInfo != null && mLastUsedInfo.offset <= position
    387                     && position < (mLastUsedInfo.offset + mLastUsedInfo.size)) {
    388                 return mLastUsedInfo;
    389             }
    390             for (DayAdapterInfo info : mAdapterInfos) {
    391                 if (info.offset <= position
    392                         && position < (info.offset + info.size)) {
    393                     mLastUsedInfo = info;
    394                     return info;
    395                 }
    396             }
    397         }
    398         return null;
    399     }
    400 
    401     private DayAdapterInfo getAdapterInfoByTime(Time time) {
    402         if (DEBUGLOG) Log.e(TAG, "getAdapterInfoByTime " + time.toString());
    403 
    404         Time tmpTime = new Time(time);
    405         long timeInMillis = tmpTime.normalize(true);
    406         int day = Time.getJulianDay(timeInMillis, tmpTime.gmtoff);
    407         synchronized (mAdapterInfos) {
    408             for (DayAdapterInfo info : mAdapterInfos) {
    409                 if (info.start <= day && day < info.end) {
    410                     return info;
    411                 }
    412             }
    413         }
    414         return null;
    415     }
    416 
    417     public EventInfo getEventByPosition(int position) {
    418         if (DEBUGLOG) Log.e(TAG, "getEventByPosition " + position);
    419 
    420         EventInfo event = new EventInfo();
    421         position -= OFF_BY_ONE_BUG;
    422         DayAdapterInfo info = getAdapterInfoByPosition(position);
    423         if (info == null) {
    424             return null;
    425         }
    426 
    427         position = info.dayAdapter.getCursorPosition(position - info.offset);
    428         if (position == Integer.MIN_VALUE) {
    429             return null;
    430         }
    431 
    432         boolean isDayHeader = false;
    433         if (position < 0) {
    434             position = -position;
    435             isDayHeader = true;
    436         }
    437 
    438         if (position < info.cursor.getCount()) {
    439             info.cursor.moveToPosition(position);
    440             event.begin = info.cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN);
    441             boolean allDay = info.cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
    442 
    443             if (allDay) { // UTC
    444                 Time time = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
    445                 time.setJulianDay(Time.getJulianDay(event.begin, 0));
    446                 event.begin = time.toMillis(false /* use isDst */);
    447             } else if (isDayHeader) { // Trim to midnight.
    448                 Time time = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
    449                 time.set(event.begin);
    450                 time.hour = 0;
    451                 time.minute = 0;
    452                 time.second = 0;
    453                 event.begin = time.toMillis(false /* use isDst */);
    454             }
    455 
    456             if (!isDayHeader) {
    457                 event.end = info.cursor.getLong(AgendaWindowAdapter.INDEX_END);
    458                 event.id = info.cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID);
    459             }
    460             return event;
    461         }
    462         return null;
    463     }
    464 
    465     public void refresh(Time goToTime, boolean forced) {
    466         if (DEBUGLOG) {
    467             Log.e(TAG, "refresh " + goToTime.toString() + (forced ? " forced" : " not forced"));
    468         }
    469 
    470         int startDay = Time.getJulianDay(goToTime.toMillis(false), goToTime.gmtoff);
    471 
    472         if (!forced && isInRange(startDay, startDay)) {
    473             // No need to requery
    474             mAgendaListView.setSelection(findDayPositionNearestTime(goToTime) + OFF_BY_ONE_BUG);
    475             return;
    476         }
    477 
    478         // Query for a total of MIN_QUERY_DURATION days
    479         int endDay = startDay + MIN_QUERY_DURATION;
    480 
    481         queueQuery(startDay, endDay, goToTime, QUERY_TYPE_CLEAN);
    482     }
    483 
    484     public void close() {
    485         mShuttingDown = true;
    486         pruneAdapterInfo(QUERY_TYPE_CLEAN);
    487         if (mQueryHandler != null) {
    488             mQueryHandler.cancelOperation(0);
    489         }
    490     }
    491 
    492     private DayAdapterInfo pruneAdapterInfo(int queryType) {
    493         synchronized (mAdapterInfos) {
    494             DayAdapterInfo recycleMe = null;
    495             if (!mAdapterInfos.isEmpty()) {
    496                 if (mAdapterInfos.size() >= MAX_NUM_OF_ADAPTERS) {
    497                     if (queryType == QUERY_TYPE_NEWER) {
    498                         recycleMe = mAdapterInfos.removeFirst();
    499                     } else if (queryType == QUERY_TYPE_OLDER) {
    500                         recycleMe = mAdapterInfos.removeLast();
    501                         // Keep the size only if the oldest items are removed.
    502                         recycleMe.size = 0;
    503                     }
    504                     if (recycleMe != null) {
    505                         if (recycleMe.cursor != null) {
    506                             recycleMe.cursor.close();
    507                         }
    508                         return recycleMe;
    509                     }
    510                 }
    511 
    512                 if (mRowCount == 0 || queryType == QUERY_TYPE_CLEAN) {
    513                     mRowCount = 0;
    514                     int deletedRows = 0;
    515                     DayAdapterInfo info;
    516                     do {
    517                         info = mAdapterInfos.poll();
    518                         if (info != null) {
    519                             info.cursor.close();
    520                             deletedRows += info.size;
    521                             recycleMe = info;
    522                         }
    523                     } while (info != null);
    524 
    525                     if (recycleMe != null) {
    526                         recycleMe.cursor = null;
    527                         recycleMe.size = deletedRows;
    528                     }
    529                 }
    530             }
    531             return recycleMe;
    532         }
    533     }
    534 
    535     private String buildQuerySelection() {
    536         // Respect the preference to show/hide declined events
    537 
    538         if (mHideDeclined) {
    539             return Calendars.SELECTED + "=1 AND "
    540                     + Instances.SELF_ATTENDEE_STATUS + "!="
    541                     + Attendees.ATTENDEE_STATUS_DECLINED;
    542         } else {
    543             return Calendars.SELECTED + "=1";
    544         }
    545     }
    546 
    547     private Uri buildQueryUri(int start, int end) {
    548         StringBuilder path = new StringBuilder();
    549         path.append(start);
    550         path.append('/');
    551         path.append(end);
    552         Uri uri = Uri.withAppendedPath(Instances.CONTENT_BY_DAY_URI, path.toString());
    553         return uri;
    554     }
    555 
    556     private boolean isInRange(int start, int end) {
    557         synchronized (mAdapterInfos) {
    558             if (mAdapterInfos.isEmpty()) {
    559                 return false;
    560             }
    561             return mAdapterInfos.getFirst().start <= start && end <= mAdapterInfos.getLast().end;
    562         }
    563     }
    564 
    565     private int calculateQueryDuration(int start, int end) {
    566         int queryDuration = MAX_QUERY_DURATION;
    567         if (mRowCount != 0) {
    568             queryDuration = IDEAL_NUM_OF_EVENTS * (end - start + 1) / mRowCount;
    569         }
    570 
    571         if (queryDuration > MAX_QUERY_DURATION) {
    572             queryDuration = MAX_QUERY_DURATION;
    573         } else if (queryDuration < MIN_QUERY_DURATION) {
    574             queryDuration = MIN_QUERY_DURATION;
    575         }
    576 
    577         return queryDuration;
    578     }
    579 
    580     private boolean queueQuery(int start, int end, Time goToTime, int queryType) {
    581         QuerySpec queryData = new QuerySpec(queryType);
    582         queryData.goToTime = goToTime;
    583         queryData.start = start;
    584         queryData.end = end;
    585         return queueQuery(queryData);
    586     }
    587 
    588     private boolean queueQuery(QuerySpec queryData) {
    589         Boolean queuedQuery;
    590         synchronized (mQueryQueue) {
    591             queuedQuery = false;
    592             Boolean doQueryNow = mQueryQueue.isEmpty();
    593             mQueryQueue.add(queryData);
    594             queuedQuery = true;
    595             if (doQueryNow) {
    596                 doQuery(queryData);
    597             }
    598         }
    599         return queuedQuery;
    600     }
    601 
    602     private void doQuery(QuerySpec queryData) {
    603         if (!mAdapterInfos.isEmpty()) {
    604             int start = mAdapterInfos.getFirst().start;
    605             int end = mAdapterInfos.getLast().end;
    606             int queryDuration = calculateQueryDuration(start, end);
    607             switch(queryData.queryType) {
    608                 case QUERY_TYPE_OLDER:
    609                     queryData.end = start - 1;
    610                     queryData.start = queryData.end - queryDuration;
    611                     break;
    612                 case QUERY_TYPE_NEWER:
    613                     queryData.start = end + 1;
    614                     queryData.end = queryData.start + queryDuration;
    615                     break;
    616             }
    617         }
    618 
    619         if (BASICLOG) {
    620             Time time = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
    621             time.setJulianDay(queryData.start);
    622             Time time2 = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
    623             time2.setJulianDay(queryData.end);
    624             Log.v(TAG, "startQuery: " + time.toString() + " to "
    625                     + time2.toString() + " then go to " + queryData.goToTime);
    626         }
    627 
    628         mQueryHandler.cancelOperation(0);
    629         if (BASICLOG) queryData.queryStartMillis = System.nanoTime();
    630         mQueryHandler.startQuery(0, queryData, buildQueryUri(
    631                 queryData.start, queryData.end), PROJECTION,
    632                 buildQuerySelection(), null, AGENDA_SORT_ORDER);
    633     }
    634 
    635     private String formatDateString(int julianDay) {
    636         Time time = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
    637         time.setJulianDay(julianDay);
    638         long millis = time.toMillis(false);
    639         return Utils.formatDateRange(mContext, millis, millis,
    640                 DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE
    641                         | DateUtils.FORMAT_ABBREV_MONTH).toString();
    642     }
    643 
    644     private void updateHeaderFooter(final int start, final int end) {
    645         mHeaderView.setText(mContext.getString(R.string.show_older_events,
    646                 formatDateString(start)));
    647         mFooterView.setText(mContext.getString(R.string.show_newer_events,
    648                 formatDateString(end)));
    649     }
    650 
    651     private class QueryHandler extends AsyncQueryHandler {
    652 
    653         public QueryHandler(ContentResolver cr) {
    654             super(cr);
    655         }
    656 
    657         @Override
    658         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
    659             QuerySpec data = (QuerySpec)cookie;
    660             if (BASICLOG) {
    661                 long queryEndMillis = System.nanoTime();
    662                 Log.e(TAG, "Query time(ms): "
    663                         + (queryEndMillis - data.queryStartMillis) / 1000000
    664                         + " Count: " + cursor.getCount());
    665             }
    666 
    667             if (mShuttingDown) {
    668                 cursor.close();
    669                 return;
    670             }
    671 
    672             // Notify Listview of changes and update position
    673             int cursorSize = cursor.getCount();
    674             if (cursorSize > 0 || mAdapterInfos.isEmpty() || data.queryType == QUERY_TYPE_CLEAN) {
    675                 final int listPositionOffset = processNewCursor(data, cursor);
    676                 if (data.goToTime == null) { // Typical Scrolling type query
    677                     notifyDataSetChanged();
    678                     if (listPositionOffset != 0) {
    679                         mAgendaListView.shiftSelection(listPositionOffset);
    680                     }
    681                 } else { // refresh() called. Go to the designated position
    682                     final Time goToTime = data.goToTime;
    683                     notifyDataSetChanged();
    684                     int newPosition = findDayPositionNearestTime(goToTime);
    685                     if (newPosition >= 0) {
    686                         mAgendaListView.setSelection(newPosition + OFF_BY_ONE_BUG);
    687                     }
    688                     if (DEBUGLOG) {
    689                         Log.e(TAG, "Setting listview to " +
    690                                 "findDayPositionNearestTime: " + (newPosition + OFF_BY_ONE_BUG));
    691                     }
    692                 }
    693             } else {
    694                 cursor.close();
    695             }
    696 
    697             // Update header and footer
    698             if (!mDoneSettingUpHeaderFooter) {
    699                 OnClickListener headerFooterOnClickListener = new OnClickListener() {
    700                     public void onClick(View v) {
    701                         if (v == mHeaderView) {
    702                             queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
    703                         } else {
    704                             queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
    705                         }
    706                     }};
    707                 mHeaderView.setOnClickListener(headerFooterOnClickListener);
    708                 mFooterView.setOnClickListener(headerFooterOnClickListener);
    709                 mAgendaListView.addFooterView(mFooterView);
    710                 mDoneSettingUpHeaderFooter = true;
    711             }
    712             synchronized (mQueryQueue) {
    713                 int totalAgendaRangeStart = -1;
    714                 int totalAgendaRangeEnd = -1;
    715 
    716                 if (cursorSize != 0) {
    717                     // Remove the query that just completed
    718                     QuerySpec x = mQueryQueue.poll();
    719                     if (BASICLOG && !x.equals(data)) {
    720                         Log.e(TAG, "onQueryComplete - cookie != head of queue");
    721                     }
    722                     mEmptyCursorCount = 0;
    723                     if (data.queryType == QUERY_TYPE_NEWER) {
    724                         mNewerRequestsProcessed++;
    725                     } else if (data.queryType == QUERY_TYPE_OLDER) {
    726                         mOlderRequestsProcessed++;
    727                     }
    728 
    729                     totalAgendaRangeStart = mAdapterInfos.getFirst().start;
    730                     totalAgendaRangeEnd = mAdapterInfos.getLast().end;
    731                 } else { // CursorSize == 0
    732                     QuerySpec querySpec = mQueryQueue.peek();
    733 
    734                     // Update Adapter Info with new start and end date range
    735                     if (!mAdapterInfos.isEmpty()) {
    736                         DayAdapterInfo first = mAdapterInfos.getFirst();
    737                         DayAdapterInfo last = mAdapterInfos.getLast();
    738 
    739                         if (first.start - 1 <= querySpec.end && querySpec.start < first.start) {
    740                             first.start = querySpec.start;
    741                         }
    742 
    743                         if (querySpec.start <= last.end + 1 && last.end < querySpec.end) {
    744                             last.end = querySpec.end;
    745                         }
    746 
    747                         totalAgendaRangeStart = first.start;
    748                         totalAgendaRangeEnd = last.end;
    749                     } else {
    750                         totalAgendaRangeStart = querySpec.start;
    751                         totalAgendaRangeEnd = querySpec.end;
    752                     }
    753 
    754                     // Update query specification with expanded search range
    755                     // and maybe rerun query
    756                     switch (querySpec.queryType) {
    757                         case QUERY_TYPE_OLDER:
    758                             totalAgendaRangeStart = querySpec.start;
    759                             querySpec.start -= MAX_QUERY_DURATION;
    760                             break;
    761                         case QUERY_TYPE_NEWER:
    762                             totalAgendaRangeEnd = querySpec.end;
    763                             querySpec.end += MAX_QUERY_DURATION;
    764                             break;
    765                         case QUERY_TYPE_CLEAN:
    766                             totalAgendaRangeStart = querySpec.start;
    767                             totalAgendaRangeEnd = querySpec.end;
    768                             querySpec.start -= MAX_QUERY_DURATION / 2;
    769                             querySpec.end += MAX_QUERY_DURATION / 2;
    770                             break;
    771                     }
    772 
    773                     if (++mEmptyCursorCount > RETRIES_ON_NO_DATA) {
    774                         // Nothing in the cursor again. Dropping query
    775                         mQueryQueue.poll();
    776                     }
    777                 }
    778 
    779                 updateHeaderFooter(totalAgendaRangeStart, totalAgendaRangeEnd);
    780 
    781                 // Fire off the next query if any
    782                 Iterator<QuerySpec> it = mQueryQueue.iterator();
    783                 while (it.hasNext()) {
    784                     QuerySpec queryData = it.next();
    785                     if (!isInRange(queryData.start, queryData.end)) {
    786                         // Query accepted
    787                         if (DEBUGLOG) Log.e(TAG, "Query accepted. QueueSize:" + mQueryQueue.size());
    788                         doQuery(queryData);
    789                         break;
    790                     } else {
    791                         // Query rejected
    792                         it.remove();
    793                         if (DEBUGLOG) Log.e(TAG, "Query rejected. QueueSize:" + mQueryQueue.size());
    794                     }
    795                 }
    796             }
    797             if (BASICLOG) {
    798                 for (DayAdapterInfo info3 : mAdapterInfos) {
    799                     Log.e(TAG, "> " + info3.toString());
    800                 }
    801             }
    802         }
    803 
    804         /*
    805          * Update the adapter info array with a the new cursor. Close out old
    806          * cursors as needed.
    807          *
    808          * @return number of rows removed from the beginning
    809          */
    810         private int processNewCursor(QuerySpec data, Cursor cursor) {
    811             synchronized (mAdapterInfos) {
    812                 // Remove adapter info's from adapterInfos as needed
    813                 DayAdapterInfo info = pruneAdapterInfo(data.queryType);
    814                 int listPositionOffset = 0;
    815                 if (info == null) {
    816                     info = new DayAdapterInfo(mContext);
    817                 } else {
    818                     if (DEBUGLOG)
    819                         Log.e(TAG, "processNewCursor listPositionOffsetA="
    820                                 + -info.size);
    821                     listPositionOffset = -info.size;
    822                 }
    823 
    824                 // Setup adapter info
    825                 info.start = data.start;
    826                 info.end = data.end;
    827                 info.cursor = cursor;
    828                 info.dayAdapter.changeCursor(info);
    829                 info.size = info.dayAdapter.getCount();
    830 
    831                 // Insert into adapterInfos
    832                 if (mAdapterInfos.isEmpty()
    833                         || data.end <= mAdapterInfos.getFirst().start) {
    834                     mAdapterInfos.addFirst(info);
    835                     listPositionOffset += info.size;
    836                 } else if (BASICLOG && data.start < mAdapterInfos.getLast().end) {
    837                     mAdapterInfos.addLast(info);
    838                     for (DayAdapterInfo info2 : mAdapterInfos) {
    839                         Log.e("========== BUG ==", info2.toString());
    840                     }
    841                 } else {
    842                     mAdapterInfos.addLast(info);
    843                 }
    844 
    845                 // Update offsets in adapterInfos
    846                 mRowCount = 0;
    847                 for (DayAdapterInfo info3 : mAdapterInfos) {
    848                     info3.offset = mRowCount;
    849                     mRowCount += info3.size;
    850                 }
    851                 mLastUsedInfo = null;
    852 
    853                 return listPositionOffset;
    854             }
    855         }
    856     }
    857 
    858     static String getViewTitle(View x) {
    859         String title = "";
    860         if (x != null) {
    861             Object yy = x.getTag();
    862             if (yy instanceof AgendaAdapter.ViewHolder) {
    863                 TextView tv = ((AgendaAdapter.ViewHolder) yy).title;
    864                 if (tv != null) {
    865                     title = (String) tv.getText();
    866                 }
    867             } else if (yy != null) {
    868                 TextView dateView = ((AgendaByDayAdapter.ViewHolder) yy).dateView;
    869                 if (dateView != null) {
    870                     title = (String) dateView.getText();
    871                 }
    872             }
    873         }
    874         return title;
    875     }
    876 
    877     public void setHideDeclinedEvents(boolean hideDeclined) {
    878         mHideDeclined = hideDeclined;
    879     }
    880 }
    881