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         boolean allday;
    226     }
    227 
    228     static class DayAdapterInfo {
    229         Cursor cursor;
    230 
    231         AgendaByDayAdapter dayAdapter;
    232 
    233         int start; // start day of the cursor's coverage
    234 
    235         int end; // end day of the cursor's coverage
    236 
    237         int offset; // offset in position in the list view
    238 
    239         int size; // dayAdapter.getCount()
    240 
    241         public DayAdapterInfo(Context context) {
    242             dayAdapter = new AgendaByDayAdapter(context);
    243         }
    244 
    245         @Override
    246         public String toString() {
    247             Time time = new Time();
    248             StringBuilder sb = new StringBuilder();
    249             time.setJulianDay(start);
    250             time.normalize(false);
    251             sb.append("Start:").append(time.toString());
    252             time.setJulianDay(end);
    253             time.normalize(false);
    254             sb.append(" End:").append(time.toString());
    255             sb.append(" Offset:").append(offset);
    256             sb.append(" Size:").append(size);
    257             return sb.toString();
    258         }
    259     }
    260 
    261     public AgendaWindowAdapter(AgendaActivity agendaActivity,
    262             AgendaListView agendaListView) {
    263         mContext = agendaActivity;
    264         mAgendaListView = agendaListView;
    265         mQueryHandler = new QueryHandler(agendaActivity.getContentResolver());
    266 
    267         LayoutInflater inflater = (LayoutInflater) agendaActivity
    268                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    269         mHeaderView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
    270         mFooterView = (TextView)inflater.inflate(R.layout.agenda_header_footer, null);
    271         mHeaderView.setText(R.string.loading);
    272         mAgendaListView.addHeaderView(mHeaderView);
    273     }
    274 
    275     // Method in Adapter
    276     @Override
    277     public int getViewTypeCount() {
    278         return AgendaByDayAdapter.TYPE_LAST;
    279     }
    280 
    281     // Method in BaseAdapter
    282     @Override
    283     public boolean areAllItemsEnabled() {
    284         return false;
    285     }
    286 
    287     // Method in Adapter
    288     @Override
    289     public int getItemViewType(int position) {
    290         DayAdapterInfo info = getAdapterInfoByPosition(position);
    291         if (info != null) {
    292             return info.dayAdapter.getItemViewType(position - info.offset);
    293         } else {
    294             return -1;
    295         }
    296     }
    297 
    298     // Method in BaseAdapter
    299     @Override
    300     public boolean isEnabled(int position) {
    301         DayAdapterInfo info = getAdapterInfoByPosition(position);
    302         if (info != null) {
    303             return info.dayAdapter.isEnabled(position - info.offset);
    304         } else {
    305             return false;
    306         }
    307     }
    308 
    309     // Abstract Method in BaseAdapter
    310     public int getCount() {
    311         return mRowCount;
    312     }
    313 
    314     // Abstract Method in BaseAdapter
    315     public Object getItem(int position) {
    316         DayAdapterInfo info = getAdapterInfoByPosition(position);
    317         if (info != null) {
    318             return info.dayAdapter.getItem(position - info.offset);
    319         } else {
    320             return null;
    321         }
    322     }
    323 
    324     // Method in BaseAdapter
    325     @Override
    326     public boolean hasStableIds() {
    327         return true;
    328     }
    329 
    330     // Abstract Method in BaseAdapter
    331     public long getItemId(int position) {
    332         DayAdapterInfo info = getAdapterInfoByPosition(position);
    333         if (info != null) {
    334             return ((position - info.offset) << 20) + info.start ;
    335         } else {
    336             return -1;
    337         }
    338     }
    339 
    340     // Abstract Method in BaseAdapter
    341     public View getView(int position, View convertView, ViewGroup parent) {
    342         if (position >= (mRowCount - PREFETCH_BOUNDARY)
    343                 && mNewerRequests <= mNewerRequestsProcessed) {
    344             if (DEBUGLOG) Log.e(TAG, "queryForNewerEvents: ");
    345             mNewerRequests++;
    346             queueQuery(new QuerySpec(QUERY_TYPE_NEWER));
    347         }
    348 
    349         if (position < PREFETCH_BOUNDARY
    350                 && mOlderRequests <= mOlderRequestsProcessed) {
    351             if (DEBUGLOG) Log.e(TAG, "queryForOlderEvents: ");
    352             mOlderRequests++;
    353             queueQuery(new QuerySpec(QUERY_TYPE_OLDER));
    354         }
    355 
    356         View v;
    357         DayAdapterInfo info = getAdapterInfoByPosition(position);
    358         if (info != null) {
    359             v = info.dayAdapter.getView(position - info.offset, convertView,
    360                     parent);
    361         } else {
    362             //TODO
    363             Log.e(TAG, "BUG: getAdapterInfoByPosition returned null!!! " + position);
    364             TextView tv = new TextView(mContext);
    365             tv.setText("Bug! " + position);
    366             v = tv;
    367         }
    368 
    369         if (DEBUGLOG) {
    370             Log.e(TAG, "getView " + position + " = " + getViewTitle(v));
    371         }
    372         return v;
    373     }
    374 
    375     private int findDayPositionNearestTime(Time time) {
    376         if (DEBUGLOG) Log.e(TAG, "findDayPositionNearestTime " + time);
    377 
    378         DayAdapterInfo info = getAdapterInfoByTime(time);
    379         if (info != null) {
    380             return info.offset + info.dayAdapter.findDayPositionNearestTime(time);
    381         } else {
    382             return -1;
    383         }
    384     }
    385 
    386     private DayAdapterInfo getAdapterInfoByPosition(int position) {
    387         synchronized (mAdapterInfos) {
    388             if (mLastUsedInfo != null && mLastUsedInfo.offset <= position
    389                     && position < (mLastUsedInfo.offset + mLastUsedInfo.size)) {
    390                 return mLastUsedInfo;
    391             }
    392             for (DayAdapterInfo info : mAdapterInfos) {
    393                 if (info.offset <= position
    394                         && position < (info.offset + info.size)) {
    395                     mLastUsedInfo = info;
    396                     return info;
    397                 }
    398             }
    399         }
    400         return null;
    401     }
    402 
    403     private DayAdapterInfo getAdapterInfoByTime(Time time) {
    404         if (DEBUGLOG) Log.e(TAG, "getAdapterInfoByTime " + time.toString());
    405 
    406         Time tmpTime = new Time(time);
    407         long timeInMillis = tmpTime.normalize(true);
    408         int day = Time.getJulianDay(timeInMillis, tmpTime.gmtoff);
    409         synchronized (mAdapterInfos) {
    410             for (DayAdapterInfo info : mAdapterInfos) {
    411                 if (info.start <= day && day < info.end) {
    412                     return info;
    413                 }
    414             }
    415         }
    416         return null;
    417     }
    418 
    419     public EventInfo getEventByPosition(int position) {
    420         if (DEBUGLOG) Log.e(TAG, "getEventByPosition " + position);
    421 
    422         EventInfo event = new EventInfo();
    423         position -= OFF_BY_ONE_BUG;
    424         DayAdapterInfo info = getAdapterInfoByPosition(position);
    425         if (info == null) {
    426             return null;
    427         }
    428 
    429         position = info.dayAdapter.getCursorPosition(position - info.offset);
    430         if (position == Integer.MIN_VALUE) {
    431             return null;
    432         }
    433 
    434         boolean isDayHeader = false;
    435         if (position < 0) {
    436             position = -position;
    437             isDayHeader = true;
    438         }
    439 
    440         if (position < info.cursor.getCount()) {
    441             info.cursor.moveToPosition(position);
    442             event.begin = info.cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN);
    443             boolean allDay = info.cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
    444 
    445             event.allday = allDay;
    446             if (!isDayHeader) {
    447                 event.end = info.cursor.getLong(AgendaWindowAdapter.INDEX_END);
    448                 event.id = info.cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID);
    449             }
    450             if (allDay) { // UTC
    451                 Time time = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
    452                 time.setJulianDay(Time.getJulianDay(event.begin, 0));
    453                 event.begin = time.toMillis(true /* use isDst */);
    454                 if (!isDayHeader) {
    455                     time.setJulianDay(Time.getJulianDay(event.end, 0));
    456                     event.end = time.toMillis(true);
    457                 }
    458             } else if (isDayHeader) { // Trim to midnight.
    459                 Time time = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
    460                 time.set(event.begin);
    461                 time.hour = 0;
    462                 time.minute = 0;
    463                 time.second = 0;
    464                 event.begin = time.toMillis(false /* use isDst */);
    465             }
    466 
    467             return event;
    468         }
    469         return null;
    470     }
    471 
    472     public void refresh(Time goToTime, boolean forced) {
    473         if (DEBUGLOG) {
    474             Log.e(TAG, "refresh " + goToTime.toString() + (forced ? " forced" : " not forced"));
    475         }
    476 
    477         int startDay = Time.getJulianDay(goToTime.toMillis(false), goToTime.gmtoff);
    478 
    479         if (!forced && isInRange(startDay, startDay)) {
    480             // No need to requery
    481             mAgendaListView.setSelection(findDayPositionNearestTime(goToTime) + OFF_BY_ONE_BUG);
    482             return;
    483         }
    484 
    485         // Query for a total of MIN_QUERY_DURATION days
    486         int endDay = startDay + MIN_QUERY_DURATION;
    487 
    488         queueQuery(startDay, endDay, goToTime, QUERY_TYPE_CLEAN);
    489     }
    490 
    491     public void close() {
    492         mShuttingDown = true;
    493         pruneAdapterInfo(QUERY_TYPE_CLEAN);
    494         if (mQueryHandler != null) {
    495             mQueryHandler.cancelOperation(0);
    496         }
    497     }
    498 
    499     private DayAdapterInfo pruneAdapterInfo(int queryType) {
    500         synchronized (mAdapterInfos) {
    501             DayAdapterInfo recycleMe = null;
    502             if (!mAdapterInfos.isEmpty()) {
    503                 if (mAdapterInfos.size() >= MAX_NUM_OF_ADAPTERS) {
    504                     if (queryType == QUERY_TYPE_NEWER) {
    505                         recycleMe = mAdapterInfos.removeFirst();
    506                     } else if (queryType == QUERY_TYPE_OLDER) {
    507                         recycleMe = mAdapterInfos.removeLast();
    508                         // Keep the size only if the oldest items are removed.
    509                         recycleMe.size = 0;
    510                     }
    511                     if (recycleMe != null) {
    512                         if (recycleMe.cursor != null) {
    513                             recycleMe.cursor.close();
    514                         }
    515                         return recycleMe;
    516                     }
    517                 }
    518 
    519                 if (mRowCount == 0 || queryType == QUERY_TYPE_CLEAN) {
    520                     mRowCount = 0;
    521                     int deletedRows = 0;
    522                     DayAdapterInfo info;
    523                     do {
    524                         info = mAdapterInfos.poll();
    525                         if (info != null) {
    526                             info.cursor.close();
    527                             deletedRows += info.size;
    528                             recycleMe = info;
    529                         }
    530                     } while (info != null);
    531 
    532                     if (recycleMe != null) {
    533                         recycleMe.cursor = null;
    534                         recycleMe.size = deletedRows;
    535                     }
    536                 }
    537             }
    538             return recycleMe;
    539         }
    540     }
    541 
    542     private String buildQuerySelection() {
    543         // Respect the preference to show/hide declined events
    544 
    545         if (mHideDeclined) {
    546             return Calendars.SELECTED + "=1 AND "
    547                     + Instances.SELF_ATTENDEE_STATUS + "!="
    548                     + Attendees.ATTENDEE_STATUS_DECLINED;
    549         } else {
    550             return Calendars.SELECTED + "=1";
    551         }
    552     }
    553 
    554     private Uri buildQueryUri(int start, int end) {
    555         StringBuilder path = new StringBuilder();
    556         path.append(start);
    557         path.append('/');
    558         path.append(end);
    559         Uri uri = Uri.withAppendedPath(Instances.CONTENT_BY_DAY_URI, path.toString());
    560         return uri;
    561     }
    562 
    563     private boolean isInRange(int start, int end) {
    564         synchronized (mAdapterInfos) {
    565             if (mAdapterInfos.isEmpty()) {
    566                 return false;
    567             }
    568             return mAdapterInfos.getFirst().start <= start && end <= mAdapterInfos.getLast().end;
    569         }
    570     }
    571 
    572     private int calculateQueryDuration(int start, int end) {
    573         int queryDuration = MAX_QUERY_DURATION;
    574         if (mRowCount != 0) {
    575             queryDuration = IDEAL_NUM_OF_EVENTS * (end - start + 1) / mRowCount;
    576         }
    577 
    578         if (queryDuration > MAX_QUERY_DURATION) {
    579             queryDuration = MAX_QUERY_DURATION;
    580         } else if (queryDuration < MIN_QUERY_DURATION) {
    581             queryDuration = MIN_QUERY_DURATION;
    582         }
    583 
    584         return queryDuration;
    585     }
    586 
    587     private boolean queueQuery(int start, int end, Time goToTime, int queryType) {
    588         QuerySpec queryData = new QuerySpec(queryType);
    589         queryData.goToTime = goToTime;
    590         queryData.start = start;
    591         queryData.end = end;
    592         return queueQuery(queryData);
    593     }
    594 
    595     private boolean queueQuery(QuerySpec queryData) {
    596         Boolean queuedQuery;
    597         synchronized (mQueryQueue) {
    598             queuedQuery = false;
    599             Boolean doQueryNow = mQueryQueue.isEmpty();
    600             mQueryQueue.add(queryData);
    601             queuedQuery = true;
    602             if (doQueryNow) {
    603                 doQuery(queryData);
    604             }
    605         }
    606         return queuedQuery;
    607     }
    608 
    609     private void doQuery(QuerySpec queryData) {
    610         if (!mAdapterInfos.isEmpty()) {
    611             int start = mAdapterInfos.getFirst().start;
    612             int end = mAdapterInfos.getLast().end;
    613             int queryDuration = calculateQueryDuration(start, end);
    614             switch(queryData.queryType) {
    615                 case QUERY_TYPE_OLDER:
    616                     queryData.end = start - 1;
    617                     queryData.start = queryData.end - queryDuration;
    618                     break;
    619                 case QUERY_TYPE_NEWER:
    620                     queryData.start = end + 1;
    621                     queryData.end = queryData.start + queryDuration;
    622                     break;
    623             }
    624         }
    625 
    626         if (BASICLOG) {
    627             Time time = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
    628             time.setJulianDay(queryData.start);
    629             Time time2 = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
    630             time2.setJulianDay(queryData.end);
    631             Log.v(TAG, "startQuery: " + time.toString() + " to "
    632                     + time2.toString() + " then go to " + queryData.goToTime);
    633         }
    634 
    635         mQueryHandler.cancelOperation(0);
    636         if (BASICLOG) queryData.queryStartMillis = System.nanoTime();
    637         mQueryHandler.startQuery(0, queryData, buildQueryUri(
    638                 queryData.start, queryData.end), PROJECTION,
    639                 buildQuerySelection(), null, AGENDA_SORT_ORDER);
    640     }
    641 
    642     private String formatDateString(int julianDay) {
    643         Time time = new Time(Utils.getTimeZone(mContext, mUpdateTZ));
    644         time.setJulianDay(julianDay);
    645         long millis = time.toMillis(false);
    646         return Utils.formatDateRange(mContext, 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