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